diff --git a/src/components/admin/ResultTable.spec.ts b/src/components/admin/ResultTable.spec.ts index 434f69da4..b638a6ff8 100644 --- a/src/components/admin/ResultTable.spec.ts +++ b/src/components/admin/ResultTable.spec.ts @@ -1,29 +1,38 @@ -import { expect, test } from 'vitest'; -import { VueWrapper, mount } from '@vue/test-utils'; +import { expect, test, describe, beforeEach } from 'vitest'; +import { DOMWrapper, VueWrapper, mount } from '@vue/test-utils'; import ResultTable from './ResultTable.vue'; import type { VDataTableServer } from 'vuetify/lib/components/index.mjs'; +import type { VueNode } from '@vue/test-utils/dist/types'; let wrapper: VueWrapper | null = null; +const mockItems: { + id: number; + name: string; +}[] = [ + { id: 1, name: 'Alice' }, + { id: 2, name: 'Bob' }, + { id: 3, name: 'Charlie' }, +]; + beforeEach(() => { document.body.innerHTML = `
- `; + `; + type ReadonlyHeaders = InstanceType['headers']; - const headers: ReadonlyHeaders = [{ title: 'title', key: 'key', align: 'start' }]; + const headers: ReadonlyHeaders = [{ title: 'Name', key: 'name', align: 'start' }]; wrapper = mount(ResultTable, { attachTo: document.getElementById('app') || '', props: { - items: [], + items: mockItems, itemsPerPage: 10, loading: false, - password: 'qwertzuiop', totalItems: 25, headers: headers, - header: 'header', itemValuePath: 'id', disableRowClick: false, }, @@ -35,8 +44,78 @@ beforeEach(() => { }); }); -describe('result table', () => { - test('it renders the result table', () => { - expect(wrapper?.get('[data-testid="result-table"]')).not.toBeNull(); +describe('Row Index and Item Retrieval', () => { + test('manually find row index in DOM and match corresponding item', () => { + // Simulate getting rows and finding index + const tbody: DOMWrapper | undefined = wrapper?.find('tbody'); + const rows: DOMWrapper[] | undefined = tbody?.findAll('tr'); + + // Verify we have correct number of rows matching mock items + expect(rows?.length).toBe(mockItems.length); + + rows?.forEach((row: DOMWrapper, domIndex: number) => { + // Convert row to raw DOM element + const rowElement: VueNode = row.element; + + // Simulate finding parent and converting to array + const rowsArray: Element[] = Array.from(rowElement.parentElement!.children); + + // Find index of current row + const calculatedIndex: number = rowsArray.indexOf(rowElement); + + // Verify calculated index matches expected DOM index + expect(calculatedIndex).toBe(domIndex); + + // Verify item retrieval matches our mock data + // This simulates the logic in the component + const retrievedItem: + | { + id: number; + name: string; + } + | undefined = mockItems[calculatedIndex]; + expect(retrievedItem).toEqual(mockItems[domIndex]); + }); + }); + + test('row index matches item array order', () => { + const tbody: DOMWrapper | undefined = wrapper?.find('tbody'); + const rows: DOMWrapper[] | undefined = tbody?.findAll('tr'); + + rows?.forEach((row: DOMWrapper, index: number) => { + const rowElement: VueNode = row.element; + const rowsArray: Element[] = Array.from(rowElement.parentElement!.children); + const calculatedIndex: number = rowsArray.indexOf(rowElement); + + expect(calculatedIndex).toBe(index); + }); + }); + test('handles empty items array', () => { + // Remount with empty items + wrapper = mount(ResultTable, { + props: { + items: [], + itemsPerPage: 10, + loading: false, + totalItems: 0, + headers: [{ title: 'Name', key: 'name', align: 'start' }], + itemValuePath: 'id', + disableRowClick: false, + }, + }); + + const tbody: DOMWrapper = wrapper.find('tbody'); + const rows: DOMWrapper[] = tbody.findAll('tr'); + + // Use the prop passed to the component to check for no data + const noDataText: string | undefined = wrapper.find('[data-testid="result-table"]').attributes('no-data-text'); + + // Either no rows or only a single "no data" row + expect(rows.length).toBeLessThanOrEqual(1); + + // Optional: Check for specific no data text if needed + if (rows.length === 1) { + expect(rows[0]?.text()).toContain(noDataText || 'Keine Daten gefunden.'); + } }); }); diff --git a/src/components/admin/ResultTable.vue b/src/components/admin/ResultTable.vue index 75eebfbfb..b9052dbd5 100644 --- a/src/components/admin/ResultTable.vue +++ b/src/components/admin/ResultTable.vue @@ -4,7 +4,7 @@ */ import { SortOrder } from '@/stores/PersonStore'; import { useSearchFilterStore, type SearchFilterStore } from '@/stores/SearchFilterStore'; - import { onMounted } from 'vue'; + import { onMounted, onUnmounted } from 'vue'; import type { VDataTableServer } from 'vuetify/lib/components/index.mjs'; const searchFilterStore: SearchFilterStore = useSearchFilterStore(); @@ -46,6 +46,39 @@ const emit: Emits = defineEmits(); + function handleKeyDown(event: KeyboardEvent): void { + // Check if the pressed key is Enter + if (event.key === 'Enter') { + const target: HTMLElement = event.target as HTMLElement; + + // Check if the target or its closest parent is a header checkbox, if so then we shouldn't trigger anything when "Enter" is pressed as it's not a table row. + const isHeaderCheckbox: boolean = + target.closest('thead')?.querySelector('input[type="checkbox"]') === target || + target.querySelector('input[type="checkbox"]') !== null; + + // If it's a header checkbox, do nothing + if (isHeaderCheckbox) { + return; + } + + const row: HTMLTableRowElement | null = target.closest('tr'); + + if (row) { + // Find the corresponding item for this row. + const rowIndex: number = Array.from(row.parentElement!.children).indexOf(row); + const item: TableItem | undefined = props.items[rowIndex]; + + if (item) { + // Prevent default Enter key behavior + event.preventDefault(); + + // Emit the same event as handleRowClick + emit('onHandleRowClick', event as unknown as PointerEvent, { item }); + } + } + } + } + function handleRowClick(event: PointerEvent, item: TableRow): void { if (!props.disableRowClick) { emit('onHandleRowClick', event, item); @@ -94,6 +127,11 @@ }); } } + window.addEventListener('keydown', handleKeyDown); + }); + + onUnmounted(() => { + window.removeEventListener('keydown', handleKeyDown); }); diff --git a/src/components/admin/personen/PasswordReset.vue b/src/components/admin/personen/PasswordReset.vue index 390ab3b62..33a41fb99 100644 --- a/src/components/admin/personen/PasswordReset.vue +++ b/src/components/admin/personen/PasswordReset.vue @@ -37,6 +37,8 @@ firstname: props.person.person.name.vorname, lastname: props.person.person.name.familienname, })}`; + } else { + message = `${t('admin.person.resetPasswordSuccessMessage')}\n\n` + message; } return message; }); @@ -155,7 +157,7 @@

- + diff --git a/src/components/form/KlasseForm.vue b/src/components/form/KlasseForm.vue index 9feb1dea6..050701850 100644 --- a/src/components/form/KlasseForm.vue +++ b/src/components/form/KlasseForm.vue @@ -14,6 +14,7 @@ const personenkontextStore: PersonenkontextStore = usePersonenkontextStore(); type Props = { + errorCode?: string; schulen?: Array<{ value: string; title: string }>; readonly?: boolean; selectedSchuleProps?: BaseFieldProps & { error: boolean; 'error-messages': Array }; @@ -83,7 +84,7 @@ :confirmUnsavedChangesAction="onHandleConfirmUnsavedChanges" :createButtonLabel="$t('admin.klasse.create')" :discardButtonLabel="$t('admin.klasse.discard')" - :hideActions="readonly" + :hideActions="readonly || !!props.errorCode" id="klasse-form" :isLoading="isLoading" :onDiscard="onHandleDiscard" @@ -91,63 +92,68 @@ :onSubmit="onSubmit" :showUnsavedChangesDialog="showUnsavedChangesDialog" > - - -

1. {{ $t('admin.schule.assignSchule') }}

-
- - - + + - - -

2. {{ $t('admin.klasse.enterKlassenname') }}

-
- - - + diff --git a/src/components/form/RolleForm.vue b/src/components/form/RolleForm.vue index ee2519023..93297ff5b 100644 --- a/src/components/form/RolleForm.vue +++ b/src/components/form/RolleForm.vue @@ -9,6 +9,7 @@ type Props = { administrationsebenen?: Array<{ value: string; title: string }>; readonly?: boolean; + errorCode?: string; selectedAdministrationsebeneProps?: BaseFieldProps & { error: boolean; 'error-messages': Array }; selectedRollenArtProps?: BaseFieldProps & { error: boolean; 'error-messages': Array }; selectedRollenNameProps?: BaseFieldProps & { error: boolean; 'error-messages': Array }; @@ -28,7 +29,7 @@ onSubmit: () => void; }; - defineProps(); + const props: Props = defineProps(); // Define the V-model for each field so the parent component can pass in the values for it. const selectedAdministrationsebene: ModelRef = @@ -45,7 +46,7 @@ :confirmUnsavedChangesAction="onHandleConfirmUnsavedChanges" :createButtonLabel="$t('admin.rolle.create')" :discardButtonLabel="$t('admin.rolle.discard')" - :hideActions="readonly" + :hideActions="readonly || !!props.errorCode" id="rolle-form" :isLoading="isLoading" :onDiscard="onHandleDiscard" @@ -54,177 +55,182 @@ ref="form-wrapper" :showUnsavedChangesDialog="showUnsavedChangesDialog" > - - -

1. {{ $t('admin.administrationsebene.assignAdministrationsebene') }}

-
- - - + + - - -

2. {{ $t('admin.rolle.assignRollenart') }}

-
- - - - - diff --git a/src/components/layout/TheFooter.vue b/src/components/layout/TheFooter.vue index 24bac422c..76b439ec6 100644 --- a/src/components/layout/TheFooter.vue +++ b/src/components/layout/TheFooter.vue @@ -76,7 +76,7 @@