Compare commits
No commits in common. "main" and "improve-specs-guests" have entirely different histories.
main
...
improve-sp
2
.github/workflows/playwright.yml
vendored
2
.github/workflows/playwright.yml
vendored
@ -17,7 +17,7 @@ jobs:
|
|||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version-file: .nvmrc
|
node-version: lts/*
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm install -g pnpm && pnpm install
|
run: npm install -g pnpm && pnpm install
|
||||||
- name: Build the service that will be tested
|
- name: Build the service that will be tested
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
# Based on https://github.com/vercel/next.js/blob/canary/examples/with-docker/Dockerfile
|
# Based on https://github.com/vercel/next.js/blob/canary/examples/with-docker/Dockerfile
|
||||||
|
|
||||||
FROM node:24-alpine AS base
|
FROM node:23-alpine AS base
|
||||||
|
|
||||||
# Install dependencies only when needed
|
# Install dependencies only when needed
|
||||||
FROM base AS deps
|
FROM base AS deps
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
# Based on https://github.com/vercel/next.js/blob/canary/examples/with-docker/Dockerfile
|
# Based on https://github.com/vercel/next.js/blob/canary/examples/with-docker/Dockerfile
|
||||||
|
|
||||||
FROM node:24-alpine AS base
|
FROM node:23-alpine AS base
|
||||||
|
|
||||||
# Install dependencies only when needed
|
# Install dependencies only when needed
|
||||||
FROM base AS deps
|
FROM base AS deps
|
||||||
|
@ -18,7 +18,6 @@ import { Toast } from 'primereact/toast';
|
|||||||
import { Suspense, useEffect, useRef, useState } from 'react';
|
import { Suspense, useEffect, useRef, useState } from 'react';
|
||||||
import InvitationsBoard from '@/app/ui/invitations/board';
|
import InvitationsBoard from '@/app/ui/invitations/board';
|
||||||
import { Invitation, InvitationSerializer } from '@/app/lib/invitation';
|
import { Invitation, InvitationSerializer } from '@/app/lib/invitation';
|
||||||
import { Entity } from '@/app/lib/definitions';
|
|
||||||
|
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
@ -30,9 +29,9 @@ export default function Page() {
|
|||||||
refreshGuests();
|
refreshGuests();
|
||||||
refreshInvitations();
|
refreshInvitations();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const toast = useRef<Toast>(null);
|
const toast = useRef<Toast>(null);
|
||||||
|
|
||||||
function refreshGuests() {
|
function refreshGuests() {
|
||||||
new AbstractApi<Guest>().getAll(new GuestSerializer(), (objects: Guest[]) => {
|
new AbstractApi<Guest>().getAll(new GuestSerializer(), (objects: Guest[]) => {
|
||||||
setGuests(objects);
|
setGuests(objects);
|
||||||
@ -55,20 +54,20 @@ export default function Page() {
|
|||||||
fetch(`/api/${slug}/groups/affinities/reset`, {
|
fetch(`/api/${slug}/groups/affinities/reset`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Accept': 'application/json',
|
'Accept': 'application/json',
|
||||||
'X-CSRF-TOKEN': getCsrfToken(),
|
'X-CSRF-TOKEN': getCsrfToken(),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.then(response => {
|
.then(response => {
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
showAffinitiesResetSuccess();
|
showAffinitiesResetSuccess();
|
||||||
} else {
|
} else {
|
||||||
console.error('Failed to reset affinities');
|
console.error('Failed to reset affinities');
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error('Error resetting affinities:', error);
|
console.error('Error resetting affinities:', error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function showAffinitiesResetSuccess() {
|
function showAffinitiesResetSuccess() {
|
||||||
@ -89,22 +88,7 @@ export default function Page() {
|
|||||||
|
|
||||||
const [invitations, setInvitations] = useState<Array<Invitation>>([]);
|
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 (
|
return (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
@ -115,9 +99,22 @@ export default function Page() {
|
|||||||
<GuestFormDialog
|
<GuestFormDialog
|
||||||
key={guestBeingEdited?.id}
|
key={guestBeingEdited?.id}
|
||||||
groups={groups}
|
groups={groups}
|
||||||
onCreate={(newGuest) => {
|
onCreate={(newGuest) => {
|
||||||
setGuests(guests => updateList(guests, newGuest));
|
setGuests(prevGuests => {
|
||||||
setGuestBeingEdited(undefined);
|
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) ;
|
||||||
}}
|
}}
|
||||||
guest={guestBeingEdited}
|
guest={guestBeingEdited}
|
||||||
visible={guestBeingEdited !== undefined}
|
visible={guestBeingEdited !== undefined}
|
||||||
@ -144,10 +141,7 @@ export default function Page() {
|
|||||||
<GroupFormDialog
|
<GroupFormDialog
|
||||||
key={groupBeingEdited?.id}
|
key={groupBeingEdited?.id}
|
||||||
groups={groups}
|
groups={groups}
|
||||||
onCreate={(newGroup) => {
|
onCreate={() => { refreshGroups(); setGroupBeingEdited(undefined) }}
|
||||||
setGroups(groups => updateList(groups, newGroup));
|
|
||||||
setGroupBeingEdited(undefined)
|
|
||||||
}}
|
|
||||||
group={groupBeingEdited}
|
group={groupBeingEdited}
|
||||||
visible={groupBeingEdited !== undefined}
|
visible={groupBeingEdited !== undefined}
|
||||||
onHide={() => { setGroupBeingEdited(undefined) }}
|
onHide={() => { setGroupBeingEdited(undefined) }}
|
||||||
@ -171,7 +165,7 @@ export default function Page() {
|
|||||||
</div>
|
</div>
|
||||||
</ TabPanel>
|
</ TabPanel>
|
||||||
<TabPanel header="Invitations" leftIcon="pi pi-envelope mx-2">
|
<TabPanel header="Invitations" leftIcon="pi pi-envelope mx-2">
|
||||||
<InvitationsBoard guests={guests} invitations={invitations} />
|
<InvitationsBoard guests={guests} invitations={invitations}/>
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
</ TabView>
|
</ TabView>
|
||||||
</div>
|
</div>
|
||||||
|
@ -3,15 +3,12 @@
|
|||||||
import { Entity } from '@/app/lib/definitions';
|
import { Entity } from '@/app/lib/definitions';
|
||||||
import { getCsrfToken, getSlug } from '@/app/lib/utils';
|
import { getCsrfToken, getSlug } from '@/app/lib/utils';
|
||||||
|
|
||||||
|
|
||||||
export interface Api<T extends Entity> {
|
export interface Api<T extends Entity> {
|
||||||
getAll(serializable: Serializable<T>, callback: (objets: T[]) => void): void;
|
getAll(serializable: Serializable<T>, callback: (objets: T[]) => void): void;
|
||||||
get(serializable: Serializable<T>, id: string, callback: (object: T) => void): void;
|
get(serializable: Serializable<T>, id: string, callback: (object: T) => void): void;
|
||||||
create(serializable: Serializable<T>, object: T, callback: (object: T) => void): void;
|
create(serializable: Serializable<T>, object: T, callback: (object: T) => void): void;
|
||||||
update(serializable: Serializable<T>, object: T, callback: () => void): void;
|
update(serializable: Serializable<T>, object: T, callback: () => void): void;
|
||||||
destroy(serializable: Serializable<T>, object: T, callback: () => void): void;
|
destroy(serializable: Serializable<T>, object: T, callback: () => void): void;
|
||||||
|
|
||||||
post(serializable: Serializable<T>, path: string, callback: () => void): void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Serializable<T> {
|
export interface Serializable<T> {
|
||||||
@ -33,18 +30,6 @@ export class AbstractApi<T extends Entity> implements Api<T> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getAllPdf(serializable: Serializable<T>, callback: () => void): void {
|
|
||||||
fetch(`/api/${getSlug()}/${serializable.apiPath()}`, {
|
|
||||||
headers: {
|
|
||||||
'Accept': 'application/pdf',
|
|
||||||
}
|
|
||||||
}).then(res => res.blob())
|
|
||||||
.then(blob => {
|
|
||||||
var file = window.URL.createObjectURL(blob);
|
|
||||||
window.location.assign(file);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
get(serializable: Serializable<T>, id: (string | undefined), callback: (object: T) => void): void {
|
get(serializable: Serializable<T>, id: (string | undefined), callback: (object: T) => void): void {
|
||||||
const endpoint = id ? `/api/${getSlug()}/${serializable.apiPath()}/${id}` : `/api/${getSlug()}/${serializable.apiPath()}`;
|
const endpoint = id ? `/api/${getSlug()}/${serializable.apiPath()}/${id}` : `/api/${getSlug()}/${serializable.apiPath()}`;
|
||||||
fetch(endpoint)
|
fetch(endpoint)
|
||||||
@ -100,14 +85,4 @@ export class AbstractApi<T extends Entity> implements Api<T> {
|
|||||||
}).then(callback)
|
}).then(callback)
|
||||||
.catch((error) => console.error(error));
|
.catch((error) => console.error(error));
|
||||||
}
|
}
|
||||||
|
|
||||||
post(serializable: Serializable<T>, path: string, callback: () => void): void {
|
|
||||||
fetch(`/api/${getSlug()}/${serializable.apiPath()}/${path}`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'X-CSRF-TOKEN': getCsrfToken(),
|
|
||||||
}
|
|
||||||
}).then(callback)
|
|
||||||
.catch((error) => console.error(error));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -34,7 +34,7 @@ export class TableSimulationSerializer implements Serializable<TableSimulation>
|
|||||||
return new TableSimulation(data.id, data.tables.map((table: any) => {
|
return new TableSimulation(data.id, data.tables.map((table: any) => {
|
||||||
return {
|
return {
|
||||||
number: table.number,
|
number: table.number,
|
||||||
guests: table.guests.map((guest: any) => new Guest(guest.id, guest.name, guest.color, guest.status, [], guest.group)),
|
guests: table.guests.map((guest: any) => new Guest(guest.id, guest.name, guest.group?.name, guest.group?.id, guest.color)),
|
||||||
discomfort: {
|
discomfort: {
|
||||||
discomfort: table.discomfort.discomfort,
|
discomfort: table.discomfort.discomfort,
|
||||||
breakdown: {
|
breakdown: {
|
||||||
|
@ -5,7 +5,7 @@ import clsx from "clsx";
|
|||||||
type ButtonColor = 'primary' | 'blue' | 'green' | 'red' | 'yellow' | 'gray';
|
type ButtonColor = 'primary' | 'blue' | 'green' | 'red' | 'yellow' | 'gray';
|
||||||
|
|
||||||
export function classNames(type: ButtonColor) {
|
export function classNames(type: ButtonColor) {
|
||||||
return (clsx("text-white py-1 px-2 m-2 rounded disabled:opacity-50 disabled:cursor-not-allowed", {
|
return (clsx("text-white py-1 px-2 mx-1 rounded disabled:opacity-50 disabled:cursor-not-allowed", {
|
||||||
'bg-blue-400 hover:bg-blue-600': type === 'primary' || type === 'blue',
|
'bg-blue-400 hover:bg-blue-600': type === 'primary' || type === 'blue',
|
||||||
'bg-green-500 hover:bg-green-600': type === 'green',
|
'bg-green-500 hover:bg-green-600': type === 'green',
|
||||||
'bg-red-500 hover:bg-red-600': type === 'red',
|
'bg-red-500 hover:bg-red-600': type === 'red',
|
||||||
|
@ -14,7 +14,7 @@ import { useState } from 'react';
|
|||||||
|
|
||||||
export default function GroupFormDialog({ groups, onCreate, onHide, group, visible }: {
|
export default function GroupFormDialog({ groups, onCreate, onHide, group, visible }: {
|
||||||
groups: Group[],
|
groups: Group[],
|
||||||
onCreate?: (newGroup: Group) => void,
|
onCreate?: () => void,
|
||||||
onHide: () => void,
|
onHide: () => void,
|
||||||
group?: Group,
|
group?: Group,
|
||||||
visible: boolean,
|
visible: boolean,
|
||||||
@ -46,15 +46,15 @@ export default function GroupFormDialog({ groups, onCreate, onHide, group, visib
|
|||||||
group.color = color;
|
group.color = color;
|
||||||
group.parentId = parentId;
|
group.parentId = parentId;
|
||||||
|
|
||||||
api.update(serializer, group, (newGroup) => {
|
api.update(serializer, group, () => {
|
||||||
resetForm();
|
resetForm();
|
||||||
onCreate && onCreate(newGroup);
|
onCreate && onCreate();
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
api.create(serializer, new Group(undefined, name, undefined, icon, undefined, parentId, color), (newGroup) => {
|
api.create(serializer, new Group(undefined, name, undefined, icon, undefined, parentId, color), () => {
|
||||||
resetForm();
|
resetForm();
|
||||||
onCreate && onCreate(newGroup);
|
onCreate && onCreate();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,12 +21,7 @@ export default function GroupsTable({ groups, onUpdate, onEdit, onEditAffinities
|
|||||||
|
|
||||||
const actions = (group: Group) => (
|
const actions = (group: Group) => (
|
||||||
<div className="flex flex-row items-center">
|
<div className="flex flex-row items-center">
|
||||||
<TrashIcon className='size-6 cursor-pointer' onClick={() => {
|
<TrashIcon className='size-6 cursor-pointer' onClick={() => { api.destroy(serializer, group, onUpdate) }} />
|
||||||
if (window.confirm(`Are you sure you want to delete guest "${group.name}"?`)) {
|
|
||||||
api.destroy(serializer, group, onUpdate)
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<PencilIcon className='size-6 cursor-pointer' onClick={() => onEdit(group)} />
|
<PencilIcon className='size-6 cursor-pointer' onClick={() => onEdit(group)} />
|
||||||
<AdjustmentsHorizontalIcon className='size-6 cursor-pointer' onClick={() => onEditAffinities(group)} />
|
<AdjustmentsHorizontalIcon className='size-6 cursor-pointer' onClick={() => onEditAffinities(group)} />
|
||||||
</div>
|
</div>
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { AbstractApi } from '@/app/api/abstract-api';
|
import { AbstractApi } from '@/app/api/abstract-api';
|
||||||
import { Guest, GuestSerializer } from '@/app/lib/guest';
|
import { Guest , GuestSerializer} from '@/app/lib/guest';
|
||||||
import { PencilIcon, TrashIcon } from '@heroicons/react/24/outline';
|
import { PencilIcon, TrashIcon } from '@heroicons/react/24/outline';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import TableOfContents from '../components/table-of-contents';
|
import TableOfContents from '../components/table-of-contents';
|
||||||
@ -48,12 +48,7 @@ export default function guestsTable({ guests, onUpdate, onEdit }: {
|
|||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div className="flex flex-row items-center">
|
<div className="flex flex-row items-center">
|
||||||
<TrashIcon className='size-6 cursor-pointer' onClick={() => {
|
<TrashIcon className='size-6 cursor-pointer' onClick={() => { api.destroy(serializer, guest, onUpdate)}} />
|
||||||
if (window.confirm(`Are you sure you want to delete guest "${guest.name}"?`)) {
|
|
||||||
api.destroy(serializer, guest, onUpdate)
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<PencilIcon className='size-6 cursor-pointer' onClick={() => onEdit(guest)} />
|
<PencilIcon className='size-6 cursor-pointer' onClick={() => onEdit(guest)} />
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
@ -10,8 +10,6 @@ import { draggable, dropTargetForElements } from '@atlaskit/pragmatic-drag-and-d
|
|||||||
import { LinkIcon, TrashIcon } from "@heroicons/react/24/outline";
|
import { LinkIcon, TrashIcon } from "@heroicons/react/24/outline";
|
||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { classNames } from "../components/button";
|
|
||||||
import { Toast } from "primereact/toast";
|
|
||||||
|
|
||||||
function InvitationCard({ invitation, allGuests, onGuestAdded, onDestroy }: {
|
function InvitationCard({ invitation, allGuests, onGuestAdded, onDestroy }: {
|
||||||
invitation: Invitation,
|
invitation: Invitation,
|
||||||
@ -118,7 +116,6 @@ export default function InvitationsBoard({ guests, invitations: originalInvitati
|
|||||||
guests: Array<Guest>,
|
guests: Array<Guest>,
|
||||||
invitations: Array<Invitation>
|
invitations: Array<Invitation>
|
||||||
}) {
|
}) {
|
||||||
const toast = useRef<Toast>(null);
|
|
||||||
const api = new AbstractApi<Invitation>();
|
const api = new AbstractApi<Invitation>();
|
||||||
const serializer = new InvitationSerializer();
|
const serializer = new InvitationSerializer();
|
||||||
|
|
||||||
@ -147,20 +144,9 @@ export default function InvitationsBoard({ guests, invitations: originalInvitati
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleDownloadQrCodes() {
|
|
||||||
api.post(serializer, 'email', () => {
|
|
||||||
toast.current?.show({
|
|
||||||
severity: 'success',
|
|
||||||
summary: 'Email scheduled',
|
|
||||||
detail: 'A document with the QR codes will be sent to the organizer\'s email.'
|
|
||||||
});
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen">
|
<div className="flex h-screen">
|
||||||
{/* Left Column: Guests */}
|
{/* Left Column: Guests */}
|
||||||
<Toast ref={toast} />
|
|
||||||
<div className="w-1/4 h-full overflow-auto border-r border-gray-300 p-4">
|
<div className="w-1/4 h-full overflow-auto border-r border-gray-300 p-4">
|
||||||
<h2 className="text-lg font-semibold mb-4">{unassignedGuests.length} guests without invitation</h2>
|
<h2 className="text-lg font-semibold mb-4">{unassignedGuests.length} guests without invitation</h2>
|
||||||
<div>
|
<div>
|
||||||
@ -178,18 +164,11 @@ export default function InvitationsBoard({ guests, invitations: originalInvitati
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={handleCreateInvitation}
|
onClick={handleCreateInvitation}
|
||||||
className={classNames('primary')}
|
className="mb-4 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
|
||||||
>
|
>
|
||||||
Create New Invitation
|
Create New Invitation
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={handleDownloadQrCodes}
|
|
||||||
className={classNames('primary')}
|
|
||||||
>
|
|
||||||
Send QR codes via email
|
|
||||||
</button>
|
|
||||||
|
|
||||||
|
|
||||||
<div className="grid grid-cols-4 gap-6">
|
<div className="grid grid-cols-4 gap-6">
|
||||||
|
|
||||||
|
12
package.json
12
package.json
@ -16,9 +16,9 @@
|
|||||||
"bcrypt": "^6.0.0",
|
"bcrypt": "^6.0.0",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"dompurify": "^3.2.6",
|
"dompurify": "^3.2.6",
|
||||||
"next": "15.4.5",
|
"next": "15.3.3",
|
||||||
"next-auth": "5.0.0-beta.29",
|
"next-auth": "5.0.0-beta.28",
|
||||||
"postcss": "8.5.6",
|
"postcss": "8.5.5",
|
||||||
"primeicons": "^7.0.0",
|
"primeicons": "^7.0.0",
|
||||||
"primereact": "^10.8.2",
|
"primereact": "^10.8.2",
|
||||||
"react": "19.0.0-rc-f38c22b244-20240704",
|
"react": "19.0.0-rc-f38c22b244-20240704",
|
||||||
@ -27,12 +27,12 @@
|
|||||||
"typescript": "5.8.3",
|
"typescript": "5.8.3",
|
||||||
"use-debounce": "^10.0.1",
|
"use-debounce": "^10.0.1",
|
||||||
"uuid": "11.1.0",
|
"uuid": "11.1.0",
|
||||||
"zod": "^4.0.0"
|
"zod": "^3.23.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@playwright/test": "^1.52.0",
|
"@playwright/test": "^1.52.0",
|
||||||
"@types/bcrypt": "^5.0.2",
|
"@types/bcrypt": "^5.0.2",
|
||||||
"@types/node": "24.0.10",
|
"@types/node": "22.15.31",
|
||||||
"@types/react": "18.3.23",
|
"@types/react": "18.3.23",
|
||||||
"@types/react-dom": "18.3.7",
|
"@types/react-dom": "18.3.7",
|
||||||
"wait-on": "^8.0.3"
|
"wait-on": "^8.0.3"
|
||||||
@ -40,5 +40,5 @@
|
|||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=23.0.0"
|
"node": ">=23.0.0"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@10.13.1+sha512.37ebf1a5c7a30d5fabe0c5df44ee8da4c965ca0c5af3dbab28c3a1681b70a256218d05c81c9c0dcf767ef6b8551eb5b960042b9ed4300c59242336377e01cfad"
|
"packageManager": "pnpm@10.12.1+sha512.f0dda8580f0ee9481c5c79a1d927b9164f2c478e90992ad268bbb2465a736984391d6333d2c327913578b2804af33474ca554ba29c04a8b13060a717675ae3ac"
|
||||||
}
|
}
|
||||||
|
790
pnpm-lock.yaml
generated
790
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -1,82 +0,0 @@
|
|||||||
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(page.getByRole('row')).toHaveCount(4); // 1 header row + 3 data rows
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
// Modify the newly added group
|
|
||||||
await page.getByRole('row').nth(1).locator('svg').nth(2).click(); // Click edit icon
|
|
||||||
await expect(dialog).toBeVisible();
|
|
||||||
await expect(dialog.getByLabel('Name')).toHaveValue("Pam's friends");
|
|
||||||
await dialog.getByLabel('Name').fill('Pam\'s best friends');
|
|
||||||
await dialog.getByRole('button', { name: 'Update' }).click();
|
|
||||||
|
|
||||||
await expect(page.getByRole('row').nth(1).getByRole('cell').nth(0)).toContainText('Pam\'s best friends');
|
|
||||||
|
|
||||||
// Delete the newly added group
|
|
||||||
page.on('dialog', dialog => dialog.accept());
|
|
||||||
|
|
||||||
await page.getByRole('row').nth(1).locator('svg').nth(1).click(); // Click delete icon
|
|
||||||
await expect(page.getByRole('row')).toHaveCount(3); // 1 header row + 2 data rows
|
|
||||||
});
|
|
@ -1,6 +1,104 @@
|
|||||||
import { expect, test } from '@playwright/test';
|
import { test, expect, Page } from '@playwright/test'
|
||||||
import mockGroupsAPI from './mocks/groups';
|
import { mock } from 'node:test';
|
||||||
import mockGuestsAPI from './mocks/guests';
|
|
||||||
|
const mockGuestsAPI = ({ page }: { page: Page }) => {
|
||||||
|
page.route('*/**/api/default/guests', async route => {
|
||||||
|
if (route.request().method() === 'GET') {
|
||||||
|
const json = [
|
||||||
|
{
|
||||||
|
"id": "f4a09c28-40ea-4553-90a5-96935a59cac6",
|
||||||
|
"status": "tentative",
|
||||||
|
"name": "Kristofer Rohan DVM",
|
||||||
|
"group": {
|
||||||
|
"id": "ee44ffb9-1147-4842-a378-9eaeb0f0871a",
|
||||||
|
"name": "Pam's family",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "bd585c40-0937-4cde-960a-bb23acfd6f18",
|
||||||
|
"status": "invited",
|
||||||
|
"name": "Olevia Quigley Jr.",
|
||||||
|
"group": {
|
||||||
|
"id": "c8bda6ca-d8af-4bb8-b2bf-e6ec1c21b1e6",
|
||||||
|
"name": "Pam's work",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
await route.fulfill({ json })
|
||||||
|
} else if (route.request().method() === 'POST') {
|
||||||
|
const json = {
|
||||||
|
"id": "ff58aa2d-643d-4c29-be9c-50e10ae6853c",
|
||||||
|
"name": "John Snow",
|
||||||
|
"status": "invited",
|
||||||
|
"group": {
|
||||||
|
"id": "c8bda6ca-d8af-4bb8-b2bf-e6ec1c21b1e6",
|
||||||
|
"name": "Pam's work",
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
await route.fulfill({ json });
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
page.route("*/**/api/default/guests/*", async route => {
|
||||||
|
if (route.request().method() === 'PUT') {
|
||||||
|
const json = {
|
||||||
|
"id": "ff58aa2d-643d-4c29-be9c-50e10ae6853c",
|
||||||
|
"name": "John Fire",
|
||||||
|
"status": "declined",
|
||||||
|
"group": {
|
||||||
|
"id": "ee44ffb9-1147-4842-a378-9eaeb0f0871a",
|
||||||
|
"name": "Pam's family",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await route.fulfill({ json });
|
||||||
|
|
||||||
|
} else if (route.request().method() === 'DELETE') {
|
||||||
|
const json = {}
|
||||||
|
await route.fulfill({ json });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const mockGroupsAPI = ({ page }: { page: Page }) => {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
await route.fulfill({ json })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
test('should allow CRUD on guests', async ({ page }) => {
|
test('should allow CRUD on guests', async ({ page }) => {
|
||||||
await mockGuestsAPI({ page });
|
await mockGuestsAPI({ page });
|
||||||
@ -70,7 +168,48 @@ test('should allow CRUD on guests', async ({ page }) => {
|
|||||||
await expect(page.getByText('There are 3 elements in the list')).toBeVisible();
|
await expect(page.getByText('There are 3 elements in the list')).toBeVisible();
|
||||||
|
|
||||||
// Delete John Fire
|
// Delete John Fire
|
||||||
page.on('dialog', dialog => dialog.accept());
|
|
||||||
await page.getByRole('row').nth(1).locator('svg').nth(0).click(); // Click delete icon
|
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();
|
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);
|
||||||
|
|
||||||
|
});
|
@ -1,86 +0,0 @@
|
|||||||
import { Page } from "@playwright/test";
|
|
||||||
|
|
||||||
export default async function mockGroupsAPI({ page }: { page: Page }): Promise<void> {
|
|
||||||
page.route('*/**/api/default/groups', async route => {
|
|
||||||
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 })
|
|
||||||
} 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 })
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
page.route("*/**/api/default/groups/*", async route => {
|
|
||||||
if (route.request().method() === 'PUT') {
|
|
||||||
const json = {
|
|
||||||
"id": "4d55bc34-6f42-4e2e-82a1-71ae32da2466",
|
|
||||||
"name": "Pam's best 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 });
|
|
||||||
} else if (route.request().method() === 'DELETE') {
|
|
||||||
const json = {}
|
|
||||||
|
|
||||||
await route.fulfill({ json });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
@ -1,61 +0,0 @@
|
|||||||
import { Page } from "@playwright/test";
|
|
||||||
|
|
||||||
export default async function mockGuestsAPI({ page }: { page: Page }): Promise<void> {
|
|
||||||
page.route('*/**/api/default/guests', async route => {
|
|
||||||
if (route.request().method() === 'GET') {
|
|
||||||
const json = [
|
|
||||||
{
|
|
||||||
"id": "f4a09c28-40ea-4553-90a5-96935a59cac6",
|
|
||||||
"status": "tentative",
|
|
||||||
"name": "Kristofer Rohan DVM",
|
|
||||||
"group": {
|
|
||||||
"id": "ee44ffb9-1147-4842-a378-9eaeb0f0871a",
|
|
||||||
"name": "Pam's family",
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "bd585c40-0937-4cde-960a-bb23acfd6f18",
|
|
||||||
"status": "invited",
|
|
||||||
"name": "Olevia Quigley Jr.",
|
|
||||||
"group": {
|
|
||||||
"id": "c8bda6ca-d8af-4bb8-b2bf-e6ec1c21b1e6",
|
|
||||||
"name": "Pam's work",
|
|
||||||
}
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
await route.fulfill({ json })
|
|
||||||
} else if (route.request().method() === 'POST') {
|
|
||||||
const json = {
|
|
||||||
"id": "ff58aa2d-643d-4c29-be9c-50e10ae6853c",
|
|
||||||
"name": "John Snow",
|
|
||||||
"status": "invited",
|
|
||||||
"group": {
|
|
||||||
"id": "c8bda6ca-d8af-4bb8-b2bf-e6ec1c21b1e6",
|
|
||||||
"name": "Pam's work",
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
await route.fulfill({ json });
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
page.route("*/**/api/default/guests/*", async route => {
|
|
||||||
if (route.request().method() === 'PUT') {
|
|
||||||
const json = {
|
|
||||||
"id": "ff58aa2d-643d-4c29-be9c-50e10ae6853c",
|
|
||||||
"name": "John Fire",
|
|
||||||
"status": "declined",
|
|
||||||
"group": {
|
|
||||||
"id": "ee44ffb9-1147-4842-a378-9eaeb0f0871a",
|
|
||||||
"name": "Pam's family",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
await route.fulfill({ json });
|
|
||||||
|
|
||||||
} else if (route.request().method() === 'DELETE') {
|
|
||||||
const json = {}
|
|
||||||
await route.fulfill({ json });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
Loading…
x
Reference in New Issue
Block a user