diff --git a/frontend/language/src/nb.json b/frontend/language/src/nb.json index 81f83324557..e493ad4fe61 100644 --- a/frontend/language/src/nb.json +++ b/frontend/language/src/nb.json @@ -1068,6 +1068,7 @@ "schema_editor.custom_props_help": "Dette er egenskaper som ikke støttes av Altinn Studio, men som er satt inn på datamodellen ved hjelp av andre verktøy. Her kan man slette egenskepene eller endre deres verdier, men det er ikke mulig å legge til nye. Vær oppmerksom på at egenskaper som slettes fra denne listen ikke kan legges til igjen ved hjelp av Altinn Studio.", "schema_editor.custom_props_unknown_format": "Ukjent format", "schema_editor.custom_props_unknown_format_help": "Denne egenskapen har et format som ikke kan forhåndsvises eller redigeres i Altinn Studio.", + "schema_editor.datamodel_definition_field_deletion_text": "Feltet du vil slette tilhører en aktiv type og vil påvirke alle tilknyttede områder. Er du sikker på slettingen?", "schema_editor.datamodel_field_deletion_confirm": "Ja, slett feltet", "schema_editor.datamodel_field_deletion_info": "Forsikre deg om at feltet ikke er knyttet til noen oppsett eller regler før du sletter det.", "schema_editor.datamodel_field_deletion_text": "Er du sikker på at du vil slette dette feltet?", diff --git a/frontend/packages/schema-editor/src/components/SchemaTree/SchemaNode/ActionButtons/ActionButtons.tsx b/frontend/packages/schema-editor/src/components/SchemaTree/SchemaNode/ActionButtons/ActionButtons.tsx index 828f9bd4625..58ab84ece2e 100644 --- a/frontend/packages/schema-editor/src/components/SchemaTree/SchemaNode/ActionButtons/ActionButtons.tsx +++ b/frontend/packages/schema-editor/src/components/SchemaTree/SchemaNode/ActionButtons/ActionButtons.tsx @@ -52,7 +52,10 @@ const useDeleteNode = (pointer: string, savableModel: SavableSchemaModel) => { const { setSelectedNodePointer } = useSchemaEditorAppContext(); return useCallback(() => { - if (confirm(t('schema_editor.datamodel_field_deletion_text'))) { + const confirmMessage = savableModel.areDefinitionParentsInUse(pointer) + ? t('schema_editor.datamodel_definition_field_deletion_text') + : t('schema_editor.datamodel_field_deletion_text'); + if (confirm(confirmMessage)) { setSelectedNodePointer(null); savableModel.deleteNode(pointer); } diff --git a/frontend/packages/schema-editor/src/components/SchemaTree/SchemaNode/SchemaNode.test.tsx b/frontend/packages/schema-editor/src/components/SchemaTree/SchemaNode/SchemaNode.test.tsx index 490b077f390..77e1fe1be2c 100644 --- a/frontend/packages/schema-editor/src/components/SchemaTree/SchemaNode/SchemaNode.test.tsx +++ b/frontend/packages/schema-editor/src/components/SchemaTree/SchemaNode/SchemaNode.test.tsx @@ -12,6 +12,7 @@ import { import { definitionNodeMock, objectNodeMock, + referenceNodeMock, referredNodeMock, uiSchemaNodesMock, } from '../../../../test/mocks/uiSchemaMock'; @@ -101,6 +102,17 @@ describe('SchemaNode', () => { expect(deleteButton).toBeEnabled(); }); + it('Enables the deletion of a child node from a definition that is currently in use', async () => { + const { pointer } = referenceNodeMock; + const save = jest.fn(); + render({ pointer, save }); + + const deleteButton = screen.getByRole('button', { name: textMock('general.delete') }); + jest.spyOn(window, 'confirm').mockImplementation(() => true); + await act(() => user.click(deleteButton)); + expect(save).toHaveBeenCalledTimes(1); + }); + it('Saves the model correctly when a node is deleted', async () => { const { pointer } = objectNodeMock; const schemaModel = setupSchemaModel(); diff --git a/frontend/packages/schema-model/src/lib/SchemaModel.test.ts b/frontend/packages/schema-model/src/lib/SchemaModel.test.ts index a4e59df3c18..98cb547a846 100644 --- a/frontend/packages/schema-model/src/lib/SchemaModel.test.ts +++ b/frontend/packages/schema-model/src/lib/SchemaModel.test.ts @@ -6,7 +6,6 @@ import { combinationNodeWithMultipleChildrenMock, defNodeMock, defNodeWithChildrenChildMock, - defNodeWithChildrenGrandchildMock, defNodeWithChildrenMock, enumNodeMock, nodeWithSameNameAsStringNodeMock, @@ -221,6 +220,18 @@ describe('SchemaModel', () => { }); }); + describe('areDefinitionParentsInUse', () => { + it('Returns false if definition parent not in use', () => { + const result = schemaModel.areDefinitionParentsInUse(unusedDefinitionMock.pointer); + expect(result).toBeFalsy(); + }); + + it('Returns true if definition parent is in use', () => { + const result = schemaModel.areDefinitionParentsInUse(defNodeWithChildrenChildMock.pointer); + expect(result).toBeTruthy(); + }); + }); + describe('getFinalNode', () => { it('Returns the node itself when it is not a reference', () => { const result = schemaModel.getFinalNode(parentNodeMock.pointer); @@ -696,18 +707,10 @@ describe('SchemaModel', () => { expect(model.asArray()).toEqual(schemaModel.asArray()); }); - it('Throws an error and keeps the model unchanged if trying to delete a definition node that is in use', () => { + it('Should not throw an error if trying to delete a child node of a definition in use', () => { const model = schemaModel.deepClone(); - expect(() => model.deleteNode(defNodeMock.pointer)).toThrowError(); - expect(() => model.deleteNode(defNodeWithChildrenMock.pointer)).toThrowError(); - expect(model.asArray()).toEqual(schemaModel.asArray()); - }); - - it('Throws an error and keeps the model unchanged if trying to delete a child node of a definition in use', () => { - const model = schemaModel.deepClone(); - expect(() => model.deleteNode(defNodeWithChildrenChildMock.pointer)).toThrowError(); - expect(() => model.deleteNode(defNodeWithChildrenGrandchildMock.pointer)).toThrowError(); - expect(model.asArray()).toEqual(schemaModel.asArray()); + expect(() => model.deleteNode(defNodeWithChildrenChildMock.pointer)).not.toThrowError(); + expect(model.asArray()).not.toEqual(schemaModel.asArray()); }); }); diff --git a/frontend/packages/schema-model/src/lib/SchemaModel.ts b/frontend/packages/schema-model/src/lib/SchemaModel.ts index fabe24e85f4..4929ff6dbd7 100644 --- a/frontend/packages/schema-model/src/lib/SchemaModel.ts +++ b/frontend/packages/schema-model/src/lib/SchemaModel.ts @@ -1,5 +1,10 @@ -import type { NodePosition, UiSchemaNode, UiSchemaNodes } from '../types'; -import { CombinationKind, FieldType } from '../types'; +import { + CombinationKind, + FieldType, + type NodePosition, + type UiSchemaNode, + type UiSchemaNodes, +} from '../types'; import type { FieldNode } from '../types/FieldNode'; import type { CombinationNode } from '../types/CombinationNode'; import type { NodeMap } from '../types/NodeMap'; @@ -417,6 +422,7 @@ export class SchemaModel { referringNodes.push(node); } } + return referringNodes; } @@ -432,9 +438,10 @@ export class SchemaModel { } public deleteNode(pointer: string): SchemaModel { - if (pointer === ROOT_POINTER) throw new Error('It is not possible to delete the root node.'); - if (this.isDefinitionInUse(pointer)) - throw new Error('Cannot delete a definition that is in use.'); + if (pointer === ROOT_POINTER) { + throw new Error('It is not possible to delete the root node.'); + } + return this.deleteNodeWithChildrenRecursively(pointer); } @@ -448,6 +455,7 @@ export class SchemaModel { private isDefinitionInUse(pointer: string): boolean { const node = this.getNode(pointer); if (!isDefinition(node)) return false; + return this.hasReferringNodes(pointer) || this.areDefinitionParentsInUse(pointer); } @@ -456,7 +464,7 @@ export class SchemaModel { return !!referringNodes.length; } - private areDefinitionParentsInUse(pointer: string): boolean { + public areDefinitionParentsInUse(pointer: string): boolean { const parent = this.getParentNode(pointer); return this.isDefinitionInUse(parent.pointer); }