From 7e0ef771588d54256601c572f2b22234f50ec686 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maxime=20Duch=C3=AAne-Savard?= Date: Sat, 1 Aug 2026 23:40:54 -0400 Subject: [PATCH] only allow one one-to-one record --- frontend/src/pages/user-file.vue | 38 +++++++++++++++++++++++++++---- frontend/tests/user-file.spec.ts | 39 ++++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+), 5 deletions(-) diff --git a/frontend/src/pages/user-file.vue b/frontend/src/pages/user-file.vue index abeb856..8b150fb 100644 --- a/frontend/src/pages/user-file.vue +++ b/frontend/src/pages/user-file.vue @@ -8,6 +8,7 @@ name: string identifier: string type: string + relationshipType?: 'ONE_TO_ONE' | 'ONE_TO_MANY' } interface LinkedEntity { entity: { id: number, name: string, identifier: string, fields: Field[] } @@ -36,6 +37,14 @@ return linked.entity.fields.filter(field => field.id !== linked.relationshipField.id) } + function isOneToOne (linked: LinkedEntity) { + return linked.relationshipField.relationshipType === 'ONE_TO_ONE' + } + + function createLabel (linked: LinkedEntity) { + return `${isOneToOne(linked) ? 'Create' : 'Add'} ${linked.entity.name}` + } + function inputType (field: Field) { if (['NUMBER', 'RELATIONSHIP', 'USER'].includes(field.type)) return 'number' if (field.type === 'DATE') return 'date' @@ -136,15 +145,22 @@

{{ linked.entity.name }}

-

{{ linked.records.length }} linked {{ linked.records.length === 1 ? 'record' : 'records' }}

+

{{ linked.records.length === 0 ? 'Not created' : 'Created' }}

+

{{ linked.records.length }} linked {{ linked.records.length === 1 ? 'record' : 'records' }}

- +
@@ -157,12 +173,21 @@
- +
-

No records linked to this user.

+

+ {{ isOneToOne(linked) ? `No ${linked.entity.name} has been created.` : 'No records linked to this user.' }} +

+ +
+ +
@@ -217,6 +242,9 @@ .entity-heading p { margin: 0.35rem 0 0; font-size: 0.9rem; } .empty-state, .no-records { padding: 1.5rem; } .table-scroll { overflow-x: auto; } + .record-details { display: grid; grid-template-columns: minmax(8rem, 12rem) 1fr; margin: 0; padding: 1.25rem; gap: 0.75rem 1rem; } + .record-details dt { color: var(--v0-on-surface-variant); font-size: 0.8rem; font-weight: 700; text-transform: uppercase; } + .record-details dd { margin: 0; } table { width: 100%; border-collapse: collapse; text-align: left; white-space: nowrap; } th, td { padding: 0.85rem 1rem; border-bottom: 1px solid var(--v0-divider); } th { color: var(--v0-on-surface-variant); font-size: 0.75rem; text-transform: uppercase; } diff --git a/frontend/tests/user-file.spec.ts b/frontend/tests/user-file.spec.ts index 7903e94..5857979 100644 --- a/frontend/tests/user-file.spec.ts +++ b/frontend/tests/user-file.spec.ts @@ -16,6 +16,7 @@ test('admin opens a user file and views a linked entity record', async ({ page } const tasks: Record[] = [] const notes: Record[] = [] const milestones: Record[] = [] + const profiles: Record[] = [] const projectResponse = () => ({ entity: { id: 3, @@ -107,6 +108,24 @@ test('admin opens a user file and views a linked entity record', async ({ page } }, relationshipField: { id: 12, name: 'Owner', identifier: 'owner', type: 'USER' }, records: projects, + }, { + entity: { + id: 8, + name: 'Profile', + identifier: 'profile', + fields: [ + { id: 50, name: 'Biography', identifier: 'biography', type: 'TEXT' }, + { id: 51, name: 'User', identifier: 'user', type: 'USER', relationshipType: 'ONE_TO_ONE' }, + ], + }, + relationshipField: { + id: 51, + name: 'User', + identifier: 'user', + type: 'USER', + relationshipType: 'ONE_TO_ONE', + }, + records: profiles, }], }, })) @@ -119,6 +138,15 @@ test('admin opens a user file and views a linked entity record', async ({ page } json: { entity: projectResponse().entity, values: created, children: [] }, }) }) + await page.route('**/api/users/7/children/Profile/51', async route => { + const values = new URLSearchParams(route.request().postData() ?? '') + const created = { id: 70, biography: values.get('biography'), user: 7 } + profiles.push(created) + await route.fulfill({ + status: 201, + json: { entity: { id: 8, name: 'Profile' }, values: created, children: [] }, + }) + }) await page.route('**/api/users/7/entities/Project/records/42', async route => { if (route.request().method() === 'PATCH') { const values = new URLSearchParams(route.request().postData() ?? '') @@ -190,6 +218,17 @@ test('admin opens a user file and views a linked entity record', async ({ page } await expect(page.getByRole('heading', { name: 'Project' })).toBeVisible() await expect(page.getByRole('row').filter({ hasText: 'Website refresh' })).toContainText('7') + const profile = page.locator('.entity-card').filter({ has: page.getByRole('heading', { name: 'Profile' }) }) + await expect(profile.getByText('No Profile has been created.')).toBeVisible() + await expect(profile.locator('table')).not.toBeVisible() + await profile.getByRole('button', { name: 'Create Profile' }).click() + const profileForm = profile.getByRole('form', { name: 'Create Profile' }) + await profileForm.getByLabel('Biography').fill('Product specialist') + await profileForm.getByRole('button', { name: 'Create Profile' }).click() + await expect(profile.getByText('Product specialist')).toBeVisible() + await expect(profile.getByRole('button', { name: 'Create Profile' })).not.toBeVisible() + await expect(profile.locator('table')).not.toBeVisible() + await page.getByRole('button', { name: 'Add Project' }).click() const projectForm = page.getByRole('form', { name: 'Add Project' }) await projectForm.getByLabel('Title').fill('Mobile application')