only allow one one-to-one record
This commit is contained in:
parent
85af8a5859
commit
7e0ef77158
@ -8,6 +8,7 @@
|
|||||||
name: string
|
name: string
|
||||||
identifier: string
|
identifier: string
|
||||||
type: string
|
type: string
|
||||||
|
relationshipType?: 'ONE_TO_ONE' | 'ONE_TO_MANY'
|
||||||
}
|
}
|
||||||
interface LinkedEntity {
|
interface LinkedEntity {
|
||||||
entity: { id: number, name: string, identifier: string, fields: Field[] }
|
entity: { id: number, name: string, identifier: string, fields: Field[] }
|
||||||
@ -36,6 +37,14 @@
|
|||||||
return linked.entity.fields.filter(field => field.id !== linked.relationshipField.id)
|
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) {
|
function inputType (field: Field) {
|
||||||
if (['NUMBER', 'RELATIONSHIP', 'USER'].includes(field.type)) return 'number'
|
if (['NUMBER', 'RELATIONSHIP', 'USER'].includes(field.type)) return 'number'
|
||||||
if (field.type === 'DATE') return 'date'
|
if (field.type === 'DATE') return 'date'
|
||||||
@ -136,15 +145,22 @@
|
|||||||
<div class="entity-heading">
|
<div class="entity-heading">
|
||||||
<div>
|
<div>
|
||||||
<h2>{{ linked.entity.name }}</h2>
|
<h2>{{ linked.entity.name }}</h2>
|
||||||
<p>{{ linked.records.length }} linked {{ linked.records.length === 1 ? 'record' : 'records' }}</p>
|
<p v-if="isOneToOne(linked)">{{ linked.records.length === 0 ? 'Not created' : 'Created' }}</p>
|
||||||
|
<p v-else>{{ linked.records.length }} linked {{ linked.records.length === 1 ? 'record' : 'records' }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button type="button" @click="startAddingChild(linked)">Add {{ linked.entity.name }}</button>
|
<button
|
||||||
|
v-if="!isOneToOne(linked) || linked.records.length === 0"
|
||||||
|
type="button"
|
||||||
|
@click="startAddingChild(linked)"
|
||||||
|
>
|
||||||
|
{{ createLabel(linked) }}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form
|
<form
|
||||||
v-if="addingChildKey === childKey(linked)"
|
v-if="addingChildKey === childKey(linked)"
|
||||||
:aria-label="`Add ${linked.entity.name}`"
|
:aria-label="createLabel(linked)"
|
||||||
class="child-form"
|
class="child-form"
|
||||||
@submit.prevent="addChild(linked)"
|
@submit.prevent="addChild(linked)"
|
||||||
>
|
>
|
||||||
@ -157,12 +173,21 @@
|
|||||||
</label>
|
</label>
|
||||||
|
|
||||||
<div class="form-actions">
|
<div class="form-actions">
|
||||||
<button :disabled="saving" type="submit">{{ saving ? 'Adding…' : `Add ${linked.entity.name}` }}</button>
|
<button :disabled="saving" type="submit">{{ saving ? 'Saving…' : createLabel(linked) }}</button>
|
||||||
<button :disabled="saving" type="button" @click="addingChildKey = ''">Cancel</button>
|
<button :disabled="saving" type="button" @click="addingChildKey = ''">Cancel</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<p v-if="linked.records.length === 0" class="no-records">No records linked to this user.</p>
|
<p v-if="linked.records.length === 0" class="no-records">
|
||||||
|
{{ isOneToOne(linked) ? `No ${linked.entity.name} has been created.` : 'No records linked to this user.' }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<dl v-else-if="isOneToOne(linked)" class="record-details">
|
||||||
|
<template v-for="field in linked.entity.fields" :key="field.id">
|
||||||
|
<dt>{{ field.name }}</dt>
|
||||||
|
<dd>{{ displayValue(linked.records[0][field.identifier]) }}</dd>
|
||||||
|
</template>
|
||||||
|
</dl>
|
||||||
|
|
||||||
<div v-else class="table-scroll">
|
<div v-else class="table-scroll">
|
||||||
<table>
|
<table>
|
||||||
@ -217,6 +242,9 @@
|
|||||||
.entity-heading p { margin: 0.35rem 0 0; font-size: 0.9rem; }
|
.entity-heading p { margin: 0.35rem 0 0; font-size: 0.9rem; }
|
||||||
.empty-state, .no-records { padding: 1.5rem; }
|
.empty-state, .no-records { padding: 1.5rem; }
|
||||||
.table-scroll { overflow-x: auto; }
|
.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; }
|
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, 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; }
|
th { color: var(--v0-on-surface-variant); font-size: 0.75rem; text-transform: uppercase; }
|
||||||
|
|||||||
@ -16,6 +16,7 @@ test('admin opens a user file and views a linked entity record', async ({ page }
|
|||||||
const tasks: Record<string, unknown>[] = []
|
const tasks: Record<string, unknown>[] = []
|
||||||
const notes: Record<string, unknown>[] = []
|
const notes: Record<string, unknown>[] = []
|
||||||
const milestones: Record<string, unknown>[] = []
|
const milestones: Record<string, unknown>[] = []
|
||||||
|
const profiles: Record<string, unknown>[] = []
|
||||||
const projectResponse = () => ({
|
const projectResponse = () => ({
|
||||||
entity: {
|
entity: {
|
||||||
id: 3,
|
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' },
|
relationshipField: { id: 12, name: 'Owner', identifier: 'owner', type: 'USER' },
|
||||||
records: projects,
|
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: [] },
|
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 => {
|
await page.route('**/api/users/7/entities/Project/records/42', async route => {
|
||||||
if (route.request().method() === 'PATCH') {
|
if (route.request().method() === 'PATCH') {
|
||||||
const values = new URLSearchParams(route.request().postData() ?? '')
|
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('heading', { name: 'Project' })).toBeVisible()
|
||||||
await expect(page.getByRole('row').filter({ hasText: 'Website refresh' })).toContainText('7')
|
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()
|
await page.getByRole('button', { name: 'Add Project' }).click()
|
||||||
const projectForm = page.getByRole('form', { name: 'Add Project' })
|
const projectForm = page.getByRole('form', { name: 'Add Project' })
|
||||||
await projectForm.getByLabel('Title').fill('Mobile application')
|
await projectForm.getByLabel('Title').fill('Mobile application')
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user