From 85af8a5859b307b20bbfe8f3a8fa6959b3afb607 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maxime=20Duch=C3=AAne-Savard?= Date: Sat, 1 Aug 2026 23:28:39 -0400 Subject: [PATCH] allow one-to-one user relationship --- frontend/src/pages/entity-configuration.vue | 44 +++++++++++++++++-- frontend/tests/entity-configuration.spec.ts | 3 +- .../bolts/web/EntityDefinitionController.kt | 5 +++ 3 files changed, 47 insertions(+), 5 deletions(-) diff --git a/frontend/src/pages/entity-configuration.vue b/frontend/src/pages/entity-configuration.vue index 6a0537a..c46a42c 100644 --- a/frontend/src/pages/entity-configuration.vue +++ b/frontend/src/pages/entity-configuration.vue @@ -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 @@ - @@ -508,6 +524,15 @@ + + @@ -522,7 +547,9 @@ {{ targetName(field) }} · {{ relationshipLabel(field.relationshipType) }} - Built-in User entity + + Built-in User entity · {{ userRelationshipLabel(field.relationshipType) }} +
@@ -549,7 +576,7 @@ - @@ -569,6 +596,15 @@ + +
diff --git a/frontend/tests/entity-configuration.spec.ts b/frontend/tests/entity-configuration.spec.ts index 2f87917..9a443d1 100644 --- a/frontend/tests/entity-configuration.spec.ts +++ b/frontend/tests/entity-configuration.spec.ts @@ -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() diff --git a/src/main/kotlin/dev/mduchene/bolts/web/EntityDefinitionController.kt b/src/main/kotlin/dev/mduchene/bolts/web/EntityDefinitionController.kt index ec7d56a..d113a33 100644 --- a/src/main/kotlin/dev/mduchene/bolts/web/EntityDefinitionController.kt +++ b/src/main/kotlin/dev/mduchene/bolts/web/EntityDefinitionController.kt @@ -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()