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') }}
-
-
-
-
+
+
+
+ 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') }}
-
-
-
-
-
-
-
+
+
- 3. {{ $t('admin.rolle.enterRollenname') }}
+ 1. {{ $t('admin.administrationsebene.assignAdministrationsebene') }}
-
-
-
-
-
- 4. {{ $t('admin.rolle.assignMerkmale') }}
-
-
-
+
- 5. {{ $t('admin.serviceProvider.assignServiceProvider') }}
+ 2. {{ $t('admin.rolle.assignRollenart') }}
-
+ v-bind="selectedRollenArtProps"
+ v-model="selectedRollenArt"
+ >
-
-
- 6. {{ $t('admin.rolle.assignSystemrechte') }}
-
-
-
-
-
+
+
+
+ 3. {{ $t('admin.rolle.enterRollenname') }}
+
+
+
+
+
+
+
+ 4. {{ $t('admin.rolle.assignMerkmale') }}
+
+
+
+
+
+
+
+ 5. {{ $t('admin.serviceProvider.assignServiceProvider') }}
+
+
+
+
+
+
+
+ 6. {{ $t('admin.rolle.assignSystemrechte') }}
+
+
+
+
+
+
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 @@