Merge pull request 'Adjust group creation to avoid a full reload' (#283) from group-specs into main
All checks were successful
Check usage of free licenses / build-static-assets (push) Successful in 44s
Playwright Tests / test (push) Successful in 4m18s
Build Nginx-based docker image / build-static-assets (push) Successful in 5m1s

Reviewed-on: #283
This commit is contained in:
bustikiller 2025-06-14 17:05:46 +00:00
commit 8306cfe249
5 changed files with 162 additions and 113 deletions

View File

@ -18,6 +18,7 @@ import { Toast } from 'primereact/toast';
import { Suspense, useEffect, useRef, useState } from 'react';
import InvitationsBoard from '@/app/ui/invitations/board';
import { Invitation, InvitationSerializer } from '@/app/lib/invitation';
import { Entity } from '@/app/lib/definitions';
export default function Page() {
@ -54,20 +55,20 @@ export default function Page() {
fetch(`/api/${slug}/groups/affinities/reset`, {
method: 'POST',
headers: {
'Accept': 'application/json',
'X-CSRF-TOKEN': getCsrfToken(),
'Accept': 'application/json',
'X-CSRF-TOKEN': getCsrfToken(),
}
})
.then(response => {
if (response.ok) {
showAffinitiesResetSuccess();
} else {
console.error('Failed to reset affinities');
}
})
.catch(error => {
console.error('Error resetting affinities:', error);
});
.then(response => {
if (response.ok) {
showAffinitiesResetSuccess();
} else {
console.error('Failed to reset affinities');
}
})
.catch(error => {
console.error('Error resetting affinities:', error);
});
}
function showAffinitiesResetSuccess() {
@ -88,7 +89,22 @@ export default function Page() {
const [invitations, setInvitations] = useState<Array<Invitation>>([]);
function updateList<T extends Entity>(originalList: T[], element: T): T[] {
{
const index = originalList.findIndex(g => g.id === element?.id);
if (index !== -1) {
// Replace existing element
return [
element!,
...originalList.slice(0, index),
...originalList.slice(index + 1)
];
} else {
// Add new element at the start
return [element!, ...originalList];
}
}
}
return (
<div className="w-full">
@ -100,21 +116,8 @@ export default function Page() {
key={guestBeingEdited?.id}
groups={groups}
onCreate={(newGuest) => {
setGuests(prevGuests => {
const index = prevGuests.findIndex(g => g.id === newGuest?.id);
if (index !== -1) {
// Replace existing guest
return [
newGuest!,
...prevGuests.slice(0, index),
...prevGuests.slice(index + 1)
];
} else {
// Add new guest at the start
return [newGuest!, ...prevGuests];
}
});
setGuestBeingEdited(undefined) ;
setGuests(guests => updateList(guests, newGuest));
setGuestBeingEdited(undefined);
}}
guest={guestBeingEdited}
visible={guestBeingEdited !== undefined}
@ -141,7 +144,10 @@ export default function Page() {
<GroupFormDialog
key={groupBeingEdited?.id}
groups={groups}
onCreate={() => { refreshGroups(); setGroupBeingEdited(undefined) }}
onCreate={(newGroup) => {
setGroups(groups => updateList(groups, newGroup));
setGroupBeingEdited(undefined)
}}
group={groupBeingEdited}
visible={groupBeingEdited !== undefined}
onHide={() => { setGroupBeingEdited(undefined) }}
@ -165,7 +171,7 @@ export default function Page() {
</div>
</ TabPanel>
<TabPanel header="Invitations" leftIcon="pi pi-envelope mx-2">
<InvitationsBoard guests={guests} invitations={invitations}/>
<InvitationsBoard guests={guests} invitations={invitations} />
</TabPanel>
</ TabView>
</div>

View File

@ -14,7 +14,7 @@ import { useState } from 'react';
export default function GroupFormDialog({ groups, onCreate, onHide, group, visible }: {
groups: Group[],
onCreate?: () => void,
onCreate?: (newGroup: Group) => void,
onHide: () => void,
group?: Group,
visible: boolean,
@ -46,15 +46,15 @@ export default function GroupFormDialog({ groups, onCreate, onHide, group, visib
group.color = color;
group.parentId = parentId;
api.update(serializer, group, () => {
api.update(serializer, group, (newGroup) => {
resetForm();
onCreate && onCreate();
onCreate && onCreate(newGroup);
});
} else {
api.create(serializer, new Group(undefined, name, undefined, icon, undefined, parentId, color), () => {
api.create(serializer, new Group(undefined, name, undefined, icon, undefined, parentId, color), (newGroup) => {
resetForm();
onCreate && onCreate();
onCreate && onCreate(newGroup);
});
}
}

65
tests/groups.spec.ts Normal file
View File

@ -0,0 +1,65 @@
import { test, expect, Page } from '@playwright/test'
import mockGroupsAPI from './mocks/groups';
import mockGuestsAPI from './mocks/guests';
test('should allow CRUD on groups', async ({ page }) => {
await mockGuestsAPI({ page });
await mockGroupsAPI({ page });
await page.goto('/default/dashboard/guests');
await page.getByRole('tab', { name: 'Groups' }).click();
await expect(page.getByRole('button', { name: 'Add new' })).toBeVisible();
await expect(page.getByRole('button', { name: 'Reset affinities' })).toBeVisible();
// List all groups
await expect(page.getByRole('row')).toHaveCount(3); // 1 header row + 2 data rows
await expect(page.getByRole('row').nth(0).getByRole('columnheader').nth(0)).toHaveText('Name');
await expect(page.getByRole('row').nth(0).getByRole('columnheader').nth(1)).toHaveText('Color');
await expect(page.getByRole('row').nth(0).getByRole('columnheader').nth(2)).toHaveText('Confirmed');
await expect(page.getByRole('row').nth(0).getByRole('columnheader').nth(3)).toHaveText('Tentative');
await expect(page.getByRole('row').nth(0).getByRole('columnheader').nth(4)).toHaveText('Pending');
await expect(page.getByRole('row').nth(0).getByRole('columnheader').nth(5)).toHaveText('Declined');
await expect(page.getByRole('row').nth(0).getByRole('columnheader').nth(6)).toHaveText('Considered');
await expect(page.getByRole('row').nth(0).getByRole('columnheader').nth(7)).toHaveText('Total');
await expect(page.getByRole('row').nth(0).getByRole('columnheader').nth(8)).toHaveText('Actions');
await expect(page.getByRole('row').nth(1).getByRole('cell').nth(0)).toContainText('Pam\'s family');
await expect(page.getByRole('row').nth(1).getByRole('cell').nth(2)).toHaveText('0');
await expect(page.getByRole('row').nth(1).getByRole('cell').nth(3)).toHaveText('0');
await expect(page.getByRole('row').nth(1).getByRole('cell').nth(4)).toHaveText('1');
await expect(page.getByRole('row').nth(1).getByRole('cell').nth(5)).toHaveText('0');
await expect(page.getByRole('row').nth(1).getByRole('cell').nth(6)).toHaveText('2');
await expect(page.getByRole('row').nth(1).getByRole('cell').nth(7)).toHaveText('3');
await expect(page.getByRole('row').nth(1).locator('svg:visible')).toHaveCount(3);
await expect(page.getByRole('row').nth(2).getByRole('cell').nth(0)).toContainText('Pam\'s work');
await expect(page.getByRole('row').nth(2).getByRole('cell').nth(2)).toHaveText('0');
await expect(page.getByRole('row').nth(2).getByRole('cell').nth(3)).toHaveText('2');
await expect(page.getByRole('row').nth(2).getByRole('cell').nth(4)).toHaveText('0');
await expect(page.getByRole('row').nth(2).getByRole('cell').nth(5)).toHaveText('0');
await expect(page.getByRole('row').nth(2).getByRole('cell').nth(6)).toHaveText('0');
await expect(page.getByRole('row').nth(2).getByRole('cell').nth(7)).toHaveText('2');
await expect(page.getByRole('row').nth(2).locator('svg:visible')).toHaveCount(3);
// Add a new group
await page.getByRole('button', { name: 'Add new' }).click();
const dialog = page.getByRole('dialog');
await expect(dialog).toBeVisible();
await dialog.getByLabel('Name').fill("Pam's friends");
await dialog.getByRole('button', { name: 'Create' }).click();
await expect(dialog).not.toBeVisible();
await expect(page.getByRole('row').nth(1).getByRole('cell').nth(0)).toContainText('Pam\'s friends');
await expect(page.getByRole('row').nth(1).getByRole('cell').nth(2)).toHaveText('0');
await expect(page.getByRole('row').nth(1).getByRole('cell').nth(3)).toHaveText('0');
await expect(page.getByRole('row').nth(1).getByRole('cell').nth(4)).toHaveText('0');
await expect(page.getByRole('row').nth(1).getByRole('cell').nth(5)).toHaveText('0');
await expect(page.getByRole('row').nth(1).getByRole('cell').nth(6)).toHaveText('0');
await expect(page.getByRole('row').nth(1).getByRole('cell').nth(7)).toHaveText('0');
await expect(page.getByRole('row').nth(1).locator('svg:visible')).toHaveCount(3);
});

View File

@ -73,45 +73,3 @@ test('should allow CRUD on guests', async ({ page }) => {
await page.getByRole('row').nth(1).locator('svg').nth(0).click(); // Click delete icon
await expect(page.getByText('There are 2 elements in the list')).toBeVisible();
});
test('should display the list of groups', async ({ page }) => {
await mockGroupsAPI({ page });
await page.goto('/default/dashboard/guests');
await page.getByRole('tab', { name: 'Groups' }).click();
await expect(page.getByRole('button', { name: 'Add new' })).toBeVisible();
await expect(page.getByRole('button', { name: 'Reset affinities' })).toBeVisible();
await expect(page.getByRole('row')).toHaveCount(3); // 1 header row + 2 data rows
await expect(page.getByRole('row').nth(0).getByRole('columnheader').nth(0)).toHaveText('Name');
await expect(page.getByRole('row').nth(0).getByRole('columnheader').nth(1)).toHaveText('Color');
await expect(page.getByRole('row').nth(0).getByRole('columnheader').nth(2)).toHaveText('Confirmed');
await expect(page.getByRole('row').nth(0).getByRole('columnheader').nth(3)).toHaveText('Tentative');
await expect(page.getByRole('row').nth(0).getByRole('columnheader').nth(4)).toHaveText('Pending');
await expect(page.getByRole('row').nth(0).getByRole('columnheader').nth(5)).toHaveText('Declined');
await expect(page.getByRole('row').nth(0).getByRole('columnheader').nth(6)).toHaveText('Considered');
await expect(page.getByRole('row').nth(0).getByRole('columnheader').nth(7)).toHaveText('Total');
await expect(page.getByRole('row').nth(0).getByRole('columnheader').nth(8)).toHaveText('Actions');
await expect(page.getByRole('row').nth(1).getByRole('cell').nth(0)).toContainText('Pam\'s family');
await expect(page.getByRole('row').nth(1).getByRole('cell').nth(2)).toHaveText('0');
await expect(page.getByRole('row').nth(1).getByRole('cell').nth(3)).toHaveText('0');
await expect(page.getByRole('row').nth(1).getByRole('cell').nth(4)).toHaveText('1');
await expect(page.getByRole('row').nth(1).getByRole('cell').nth(5)).toHaveText('0');
await expect(page.getByRole('row').nth(1).getByRole('cell').nth(6)).toHaveText('2');
await expect(page.getByRole('row').nth(1).getByRole('cell').nth(7)).toHaveText('3');
await expect(page.getByRole('row').nth(1).locator('svg:visible')).toHaveCount(3);
await expect(page.getByRole('row').nth(2).getByRole('cell').nth(0)).toContainText('Pam\'s work');
await expect(page.getByRole('row').nth(2).getByRole('cell').nth(2)).toHaveText('0');
await expect(page.getByRole('row').nth(2).getByRole('cell').nth(3)).toHaveText('2');
await expect(page.getByRole('row').nth(2).getByRole('cell').nth(4)).toHaveText('0');
await expect(page.getByRole('row').nth(2).getByRole('cell').nth(5)).toHaveText('0');
await expect(page.getByRole('row').nth(2).getByRole('cell').nth(6)).toHaveText('0');
await expect(page.getByRole('row').nth(2).getByRole('cell').nth(7)).toHaveText('2');
await expect(page.getByRole('row').nth(2).locator('svg:visible')).toHaveCount(3);
});

View File

@ -2,39 +2,59 @@ import { Page } from "@playwright/test";
export default async function mockGroupsAPI({ page }: { page: Page }): Promise<void> {
page.route('*/**/api/default/groups', async route => {
const json = [
{
"id": "ee44ffb9-1147-4842-a378-9eaeb0f0871a",
"name": "Pam's family",
"icon": "pi pi-users",
"parent_id": null,
"color": "#ff0000",
"attendance": {
"total": 3,
"considered": 2,
"invited": 1,
"confirmed": 0,
"declined": 0,
"tentative": 0
}
},
{
"id": "c8bda6ca-d8af-4bb8-b2bf-e6ec1c21b1e6",
"name": "Pam's work",
"icon": "pi pi-desktop",
"parent_id": null,
"color": "#00ff00",
"attendance": {
"total": 2,
"considered": 0,
"invited": 0,
"confirmed": 0,
"declined": 0,
"tentative": 2
}
},
];
if (route.request().method() === 'GET') {
const json = [
{
"id": "ee44ffb9-1147-4842-a378-9eaeb0f0871a",
"name": "Pam's family",
"icon": "pi pi-users",
"parent_id": null,
"color": "#ff0000",
"attendance": {
"total": 3,
"considered": 2,
"invited": 1,
"confirmed": 0,
"declined": 0,
"tentative": 0
}
},
{
"id": "c8bda6ca-d8af-4bb8-b2bf-e6ec1c21b1e6",
"name": "Pam's work",
"icon": "pi pi-desktop",
"parent_id": null,
"color": "#00ff00",
"attendance": {
"total": 2,
"considered": 0,
"invited": 0,
"confirmed": 0,
"declined": 0,
"tentative": 2
}
},
];
await route.fulfill({ json })
await route.fulfill({ json })
} else if (route.request().method() === 'POST') {
const json = {
"id": "4d55bc34-6f42-4e2e-82a1-71ae32da2466",
"name": "Pam's friends",
"icon": "pi pi-desktop",
"parent_id": null,
"color": "#0000ff",
"attendance": {
"total": 0,
"considered": 0,
"invited": 0,
"confirmed": 0,
"declined": 0,
"tentative": 0
}
}
await route.fulfill({ json })
}
})
}