feat: Add default index view setting for modules
This commit introduces a new customization option in the appearance settings, allowing you to choose your default index view (card or table) for each module (Empleados, Tareas, Planillas, Asistencias).
Key changes:
- **Store Updates (`useUi.js`):**
- Added new state properties (e.g., `defaultViewEmpleados`) to store the preferred view for each module.
- Added corresponding actions (e.g., `setDefaultViewEmpleados`) to update these settings.
- Settings are persisted to local storage.
- **Settings UI (`SettingsView.vue`):**
- Added dropdown selectors in the module-specific sections of the appearance settings page for you to choose 'Table' or 'Card' as your default view.
- These selectors are bound to the new store properties.
- **Module Index Views (e.g., `EmpleadosIndex.vue`):**
- Modified to read the default view setting from the `useUi` store.
- Conditionally render either the table component or the card component based on this setting.
- Removed manual view toggle buttons, as the default is now managed via settings.
- **Testing:**
- I added unit tests for the new store actions and state.
- I added component tests for `SettingsView.vue` to verify the new UI elements and their interaction with the store.
- I added component tests for each module's index view to ensure they render the correct view (table or card) based on the global setting and display data or "no data" messages appropriately.
This feature provides you with more control over your preferred data display format within each module, enhancing the user experience.
This commit is contained in:
@@ -7,15 +7,7 @@
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<div class="mb-6 flex justify-end items-center space-x-3">
|
||||
<span class="text-sm font-medium text-gray-700">Cambiar Vista:</span>
|
||||
<button @click="currentView = 'card'" :class="btnClass('card')">
|
||||
Tarjetas
|
||||
</button>
|
||||
<button @click="currentView = 'table'" :class="btnClass('table')">
|
||||
Tabla
|
||||
</button>
|
||||
</div>
|
||||
<!-- Removed manual view toggle buttons -->
|
||||
|
||||
<div v-if="isLoading" class="loading-message">
|
||||
Cargando asistencias...
|
||||
@@ -60,18 +52,21 @@
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import { useAsistenciasStore } from '../../stores/useAsistencias';
|
||||
import { useUi } from '../../stores/useUi'; // Import useUi
|
||||
import { useRouter } from 'vue-router';
|
||||
import TablaAsistencias from '../../components/asistencias/tablaAsistencias.vue';
|
||||
import CardAsistencia from '../../components/asistencias/cardAsistencia.vue';
|
||||
|
||||
const asistenciasStore = useAsistenciasStore();
|
||||
const ui = useUi(); // Access the ui store
|
||||
const router = useRouter();
|
||||
|
||||
const isLoading = ref(true);
|
||||
const errorLoading = ref(false);
|
||||
const errorMessage = ref('');
|
||||
|
||||
const currentView = ref('table'); // Default to table view
|
||||
// Initialize currentView from the store's default setting for asistencias
|
||||
const currentView = ref(ui.defaultViewAsistencias);
|
||||
|
||||
const asistenciasList = computed(() => asistenciasStore.asistencias);
|
||||
|
||||
@@ -100,13 +95,7 @@ const handleEditAsistencia = (asistenciaId) => {
|
||||
router.push({ name: 'asistencias-edit', params: { id: asistenciaId } });
|
||||
};
|
||||
|
||||
const btnClass = (view) => {
|
||||
const baseClasses = 'px-4 py-2 rounded-md text-sm font-medium focus:outline-none focus:ring-2 focus:ring-offset-2';
|
||||
if (currentView.value === view) {
|
||||
return `${baseClasses} text-white shadow-sm view-toggle-active-asistencias`;
|
||||
}
|
||||
return `${baseClasses} bg-gray-200 text-gray-700 hover:bg-gray-300 focus:ring-gray-400`;
|
||||
};
|
||||
// Removed btnClass as manual toggle buttons are removed
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
135
ui/src/views/asistencias/__tests__/AsistenciasIndex.spec.js
Normal file
135
ui/src/views/asistencias/__tests__/AsistenciasIndex.spec.js
Normal file
@@ -0,0 +1,135 @@
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { useUi } from '@/stores/useUi'
|
||||
import { useAsistenciasStore } from '@/stores/useAsistencias'
|
||||
import AsistenciasIndex from '../AsistenciasIndex.vue'
|
||||
import TablaAsistencias from '@/components/asistencias/tablaAsistencias.vue'
|
||||
import CardAsistencia from '@/components/asistencias/cardAsistencia.vue'
|
||||
|
||||
// Mock child components
|
||||
vi.mock('@/components/asistencias/tablaAsistencias.vue', () => ({
|
||||
default: {
|
||||
name: 'TablaAsistencias',
|
||||
props: ['asistencias', 'isLoading'], // Match actual props if different
|
||||
emits: ['edit'],
|
||||
template: '<div data-testid="tabla-asistencias"></div>',
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/components/asistencias/cardAsistencia.vue', () => ({
|
||||
default: {
|
||||
name: 'CardAsistencia',
|
||||
props: ['asistencia'], // Match actual props
|
||||
emits: ['edit'],
|
||||
template: '<div data-testid="card-asistencia"></div>',
|
||||
},
|
||||
}))
|
||||
|
||||
// Mock stores
|
||||
const mockSetDefaultViewAsistencias = vi.fn();
|
||||
const mockFetchAsistencias = vi.fn();
|
||||
|
||||
vi.mock('@/stores/useUi', () => ({
|
||||
useUi: vi.fn(() => ({
|
||||
defaultViewAsistencias: 'table', // Default mock value
|
||||
setDefaultViewAsistencias: mockSetDefaultViewAsistencias,
|
||||
})),
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/useAsistencias', () => ({
|
||||
useAsistenciasStore: vi.fn(() => ({
|
||||
asistencias: [],
|
||||
fetchAsistencias: mockFetchAsistencias,
|
||||
})),
|
||||
}))
|
||||
|
||||
describe('AsistenciasIndex.vue', () => {
|
||||
let uiStoreMock
|
||||
let asistenciasStoreMock
|
||||
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
mockFetchAsistencias.mockClear().mockResolvedValue([])
|
||||
mockSetDefaultViewAsistencias.mockClear()
|
||||
|
||||
uiStoreMock = useUi()
|
||||
asistenciasStoreMock = useAsistenciasStore()
|
||||
})
|
||||
|
||||
const mountComponent = () => {
|
||||
return mount(AsistenciasIndex, {
|
||||
global: {
|
||||
// Stubs can be defined here too if not using vi.mock for everything
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
it('fetches asistencias on mount', async () => {
|
||||
mountComponent()
|
||||
// It seems the component has its own isLoading ref, wait for it to resolve
|
||||
await mockFetchAsistencias(); // Ensure the promise resolves
|
||||
expect(mockFetchAsistencias).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
describe('View Rendering based on useUi store', () => {
|
||||
it('renders TablaAsistencias when defaultViewAsistencias is "table"', async () => {
|
||||
uiStoreMock.defaultViewAsistencias = 'table'
|
||||
asistenciasStoreMock.asistencias = [{ id: 1, empleado: 'Test Employee', fecha: '2023-01-01' }]
|
||||
|
||||
const wrapper = mountComponent()
|
||||
// Wait for internal loading state and then data processing
|
||||
await mockFetchAsistencias();
|
||||
await wrapper.vm.$nextTick()
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
|
||||
expect(wrapper.findComponent({ name: 'TablaAsistencias' }).exists()).toBe(true)
|
||||
expect(wrapper.findComponent({ name: 'TablaAsistencias' }).props('asistencias')).toEqual(asistenciasStoreMock.asistencias)
|
||||
expect(wrapper.findComponent({ name: 'CardAsistencia' }).exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('renders CardAsistencia when defaultViewAsistencias is "card"', async () => {
|
||||
uiStoreMock.defaultViewAsistencias = 'card'
|
||||
asistenciasStoreMock.asistencias = [{ id: 1, empleado: 'E1' }, { id: 2, empleado: 'E2' }]
|
||||
|
||||
const wrapper = mountComponent()
|
||||
await mockFetchAsistencias();
|
||||
await wrapper.vm.$nextTick()
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
const cardWrappers = wrapper.findAllComponents({ name: 'CardAsistencia' })
|
||||
expect(cardWrappers.length).toBe(asistenciasStoreMock.asistencias.length)
|
||||
expect(cardWrappers[0].props('asistencia')).toEqual(asistenciasStoreMock.asistencias[0])
|
||||
expect(wrapper.findComponent({ name: 'TablaAsistencias' }).exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('renders no data message for table view when no asistencias exist', async () => {
|
||||
uiStoreMock.defaultViewAsistencias = 'table';
|
||||
asistenciasStoreMock.asistencias = [];
|
||||
|
||||
const wrapper = mountComponent();
|
||||
await mockFetchAsistencias();
|
||||
await wrapper.vm.$nextTick();
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(wrapper.findComponent({ name: 'TablaAsistencias' }).exists()).toBe(true);
|
||||
// The component might show "Cargando asistencias..." initially, then the no data message.
|
||||
// We need to ensure loading is complete.
|
||||
expect(wrapper.text()).toContain('No hay asistencias para mostrar');
|
||||
});
|
||||
|
||||
it('renders no data message for card view when no asistencias exist', async () => {
|
||||
uiStoreMock.defaultViewAsistencias = 'card';
|
||||
asistenciasStoreMock.asistencias = [];
|
||||
|
||||
const wrapper = mountComponent();
|
||||
await mockFetchAsistencias();
|
||||
await wrapper.vm.$nextTick();
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(wrapper.findAllComponents({ name: 'CardAsistencia' }).length).toBe(0);
|
||||
expect(wrapper.text()).toContain('No hay asistencias para mostrar');
|
||||
});
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user