allow one-to-one user relationship

This commit is contained in:
Maxime Duchêne-Savard 2026-08-01 23:28:39 -04:00
parent 5693657b54
commit 85af8a5859
3 changed files with 47 additions and 5 deletions

View File

@ -149,6 +149,14 @@
editingFieldIdentifier.value = toIdentifier(editingFieldName.value)
}
function updateNewFieldType (entityId: number) {
newFields[entityId].relationshipType = newFields[entityId].type === 'USER' ? 'ONE_TO_MANY' : 'ONE_TO_ONE'
}
function updateEditingFieldType () {
editingRelationshipType.value = editingFieldType.value === 'USER' ? 'ONE_TO_MANY' : 'ONE_TO_ONE'
}
async function addEntity () {
const name = newEntityName.value.trim()
const identifier = newEntityIdentifier.value.trim()
@ -208,6 +216,8 @@
if (!draft.targetEntityId) return
values.targetEntityId = String(draft.targetEntityId)
values.relationshipType = draft.relationshipType
} else if (draft.type === 'USER') {
values.relationshipType = draft.relationshipType
}
const response = await request(`/api/entity-definitions/${entity.id}/fields`, 'POST', values)
if (!response.ok) throw new Error('Field create failed')
@ -227,7 +237,7 @@
editingFieldIdentifier.value = field.identifier
editingFieldType.value = field.type
editingTargetEntityId.value = field.targetEntityId
editingRelationshipType.value = field.relationshipType ?? 'ONE_TO_ONE'
editingRelationshipType.value = field.relationshipType ?? (field.type === 'USER' ? 'ONE_TO_MANY' : 'ONE_TO_ONE')
}
async function saveField (entity: EntityDefinition, field: EntityField) {
@ -244,6 +254,8 @@
if (!editingTargetEntityId.value) return
values.targetEntityId = String(editingTargetEntityId.value)
values.relationshipType = editingRelationshipType.value
} else if (editingFieldType.value === 'USER') {
values.relationshipType = editingRelationshipType.value
}
const response = await request(`/api/entity-definitions/${entity.id}/fields/${field.id}`, 'PATCH', values)
if (!response.ok) throw new Error('Field update failed')
@ -346,6 +358,10 @@
return type === 'ONE_TO_MANY' ? 'One to many' : 'One to one'
}
function userRelationshipLabel (type?: RelationshipType) {
return type === 'ONE_TO_ONE' ? 'One to one' : 'Many to one'
}
function targetName (field: EntityField) {
return field.targetEntityName
?? entities.value.find(entity => entity.id === field.targetEntityId)?.name
@ -488,7 +504,7 @@
<input :id="`edit-field-identifier-${field.id}`" v-model="editingFieldIdentifier" required>
<label class="sr-only" :for="`edit-type-${field.id}`">Field type</label>
<select :id="`edit-type-${field.id}`" v-model="editingFieldType">
<select :id="`edit-type-${field.id}`" v-model="editingFieldType" @change="updateEditingFieldType">
<option v-for="type in fieldTypes" :key="type" :value="type">{{ typeLabel(type) }}</option>
</select>
@ -508,6 +524,15 @@
</select>
</template>
<template v-else-if="editingFieldType === 'USER'">
<label class="sr-only" :for="`edit-user-relationship-${field.id}`">User relationship type</label>
<select :id="`edit-user-relationship-${field.id}`" v-model="editingRelationshipType">
<option value="ONE_TO_MANY">Many to one</option>
<option value="ONE_TO_ONE">One to one</option>
</select>
</template>
<button class="text-button" type="submit">Save field</button>
<button class="text-button muted" type="button" @click="editingFieldId = undefined">Cancel</button>
</form>
@ -522,7 +547,9 @@
{{ targetName(field) }} · {{ relationshipLabel(field.relationshipType) }}
</span>
<span v-else-if="field.type === 'USER'" class="reference-detail">Built-in User entity</span>
<span v-else-if="field.type === 'USER'" class="reference-detail">
Built-in User entity · {{ userRelationshipLabel(field.relationshipType) }}
</span>
</div>
<div class="actions">
@ -549,7 +576,7 @@
<input :id="`new-field-identifier-${entity.id}`" v-model="newFields[entity.id].identifier" placeholder="Identifier" required>
<label class="sr-only" :for="`new-type-${entity.id}`">Field type</label>
<select :id="`new-type-${entity.id}`" v-model="newFields[entity.id].type">
<select :id="`new-type-${entity.id}`" v-model="newFields[entity.id].type" @change="updateNewFieldType(entity.id)">
<option v-for="type in fieldTypes" :key="type" :value="type">{{ typeLabel(type) }}</option>
</select>
@ -569,6 +596,15 @@
</select>
</template>
<template v-else-if="newFields[entity.id].type === 'USER'">
<label class="sr-only" :for="`new-user-relationship-${entity.id}`">User relationship type</label>
<select :id="`new-user-relationship-${entity.id}`" v-model="newFields[entity.id].relationshipType">
<option value="ONE_TO_MANY">Many to one</option>
<option value="ONE_TO_ONE">One to one</option>
</select>
</template>
<button class="secondary-button" :disabled="saving" type="submit">Add field</button>
</div>
</form>

View File

@ -184,9 +184,10 @@ test('admin can add, edit, and remove an entity and its fields', async ({ page }
await organizationFieldForm.getByLabel('New field').fill('Account owner')
await organizationFieldForm.getByLabel('Field type').selectOption('USER')
await organizationFieldForm.getByLabel('User relationship type').selectOption('ONE_TO_ONE')
await organizationFieldForm.getByRole('button', { name: 'Add field' }).click()
await expect(organization.locator('.type-badge').filter({ hasText: 'User reference' })).toBeVisible()
await expect(organization.getByText('Built-in User entity')).toBeVisible()
await expect(organization.getByText('Built-in User entity · One to one')).toBeVisible()
await organization.getByRole('button', { name: 'Remove entity' }).click()
await expect(page.getByRole('heading', { name: 'Organization' })).not.toBeVisible()

View File

@ -140,6 +140,11 @@ class EntityDefinitionController(
}
private fun Context.relationshipDetails(fieldType: FieldType?): RelationshipDetails? {
if (fieldType == FieldType.USER) {
val relationshipType = formParam("relationshipType")?.let(RelationshipType::from)
?: RelationshipType.ONE_TO_MANY
return RelationshipDetails(null, relationshipType)
}
if (fieldType != FieldType.RELATIONSHIP) return RelationshipDetails(null, null)
val targetEntityId = formParam("targetEntityId")?.toLongOrNull()