From 32b979ea699996d5139c31290d5ceb97ca145c54 Mon Sep 17 00:00:00 2001 From: Philipp Date: Fri, 10 Apr 2026 16:40:02 +0200 Subject: [PATCH 01/18] temp --- .../src/lib/dialog/ModifyDataDialog.svelte | 2 ++ .../mapper/map-dto-to-reactive-object.js | 4 ++- .../models/reactive/reactive-class.svelte.js | 24 ++++++++++++-- .../reactive-objects-array-wrapper.svelte.js | 9 ++++++ .../validity-rules/validityFunctions.js | 32 +++++++++++++++++++ .../mainpage/classEditor/classEditor.svelte | 2 +- .../AssociationEditorDialog.svelte | 10 +++++- 7 files changed, 77 insertions(+), 6 deletions(-) diff --git a/frontend/src/lib/dialog/ModifyDataDialog.svelte b/frontend/src/lib/dialog/ModifyDataDialog.svelte index ffc549cf..7d412c48 100644 --- a/frontend/src/lib/dialog/ModifyDataDialog.svelte +++ b/frontend/src/lib/dialog/ModifyDataDialog.svelte @@ -53,11 +53,13 @@ function discardAndClose() { showDialog = false; discardChanges(); + onClose(); } function saveAndClose() { showDialog = false; saveChanges(); + onClose(); } diff --git a/frontend/src/lib/models/reactive/mapper/map-dto-to-reactive-object.js b/frontend/src/lib/models/reactive/mapper/map-dto-to-reactive-object.js index 1292eab4..90b39c9b 100644 --- a/frontend/src/lib/models/reactive/mapper/map-dto-to-reactive-object.js +++ b/frontend/src/lib/models/reactive/mapper/map-dto-to-reactive-object.js @@ -21,9 +21,10 @@ import { ReactiveClass } from "$lib/models/reactive/reactive-class.svelte.js"; * Maps a class DTO to a ReactiveClass instance * @param {Object} classDto - The class data transfer object from the API * @param {Array} classes - Array of existing classes for resolving references + * @param {function} getClassByUuid - A function that returns the class with the given uuid * @returns {ReactiveClass} The reactive class instance */ -export function mapClassDtoToReactiveClass(classDto, classes) { +export function mapClassDtoToReactiveClass(classDto, classes, getClassByUuid) { let superClass = null; if (classDto.superClass) { superClass = classes.find( @@ -55,6 +56,7 @@ export function mapClassDtoToReactiveClass(classDto, classes) { attributes, associations, enumEntries, + getClassByUuid, ); } diff --git a/frontend/src/lib/models/reactive/reactive-class.svelte.js b/frontend/src/lib/models/reactive/reactive-class.svelte.js index 25dd1e80..f92370f9 100644 --- a/frontend/src/lib/models/reactive/reactive-class.svelte.js +++ b/frontend/src/lib/models/reactive/reactive-class.svelte.js @@ -21,11 +21,11 @@ import { ReactiveEnumEntry } from "$lib/models/reactive/reactive-enum-entry.svel import { ReactiveObjectsArrayWrapper } from "$lib/models/reactive/reactive-wrappers/reactive-objects-array-wrapper.svelte.js"; import { ReactiveValueWrapper } from "$lib/models/reactive/reactive-wrappers/reactive-value-wrapper.svelte.js"; import { - hasUniqueLabel, + hasUniqueLabel, isInvalidAssociationLabel, isInvalidInverseAssociationLabel, isInvalidLabel, isInvalidNamespace, isInvalidStereotype, - isInvalidUuid, + isInvalidUuid } from "$lib/models/reactive/validity-rules/validityFunctions.js"; function initializeStereotypeViolationChecks(stereotype, stereotypesArray) { @@ -33,6 +33,23 @@ function initializeStereotypeViolationChecks(stereotype, stereotypesArray) { isInvalidStereotype(stereotype, stereotypesArray), ); } + +function initializeAssociationViolationChecks(association, associationsArray, getClassByUuid) { + association.label.violationChecks.push(() => + isInvalidAssociationLabel( + association, + associationsArray, + ) + ); + association.inverse.label.violationChecks.push(() => + isInvalidInverseAssociationLabel( + association, + associationsArray, + getClassByUuid, + ) + ) +} + function initializeUniqueLabelChecks(reactiveObject, enumEntriesArray) { reactiveObject.label.violationChecks.push(label => hasUniqueLabel(label, enumEntriesArray), @@ -51,6 +68,7 @@ export class ReactiveClass { attributes = [], associations = [], enumEntries = [], + getClassByUuid = () => undefined, ) { this.uuid = new ReactiveValueWrapper(uuid, isInvalidUuid); this.namespace = new ReactiveValueWrapper( @@ -74,7 +92,7 @@ export class ReactiveClass { this.associations = new ReactiveObjectsArrayWrapper( associations, ReactiveAssociation, - initializeUniqueLabelChecks, + (association, associationsArray) => initializeAssociationViolationChecks(association, associationsArray, getClassByUuid), ); this.enumEntries = new ReactiveObjectsArrayWrapper( enumEntries, diff --git a/frontend/src/lib/models/reactive/reactive-wrappers/reactive-objects-array-wrapper.svelte.js b/frontend/src/lib/models/reactive/reactive-wrappers/reactive-objects-array-wrapper.svelte.js index 0fd3952b..f435c570 100644 --- a/frontend/src/lib/models/reactive/reactive-wrappers/reactive-objects-array-wrapper.svelte.js +++ b/frontend/src/lib/models/reactive/reactive-wrappers/reactive-objects-array-wrapper.svelte.js @@ -95,6 +95,15 @@ export class ReactiveObjectsArrayWrapper { this.#entryClassViolationChecksInit(newEntry, this.values); } + /** + * Adds a value without wrapping into a new class to the end of the array + * @param value - The value to be added + */ + appendClass(value) { + this.values.push(value); + this.#entryClassViolationChecksInit(value, this.values); + } + /** * Adds a value to the beginning of the array * @param value - The value to be added diff --git a/frontend/src/lib/models/reactive/validity-rules/validityFunctions.js b/frontend/src/lib/models/reactive/validity-rules/validityFunctions.js index 13a44fd9..941c3768 100644 --- a/frontend/src/lib/models/reactive/validity-rules/validityFunctions.js +++ b/frontend/src/lib/models/reactive/validity-rules/validityFunctions.js @@ -18,6 +18,7 @@ import { validate as uuidValidate } from "uuid"; import { IriValidationStrategy, validateIri } from "validate-iri"; import { getNCNameViolations } from "$lib/rdf-syntax-grammar/namespace/prefix/index.js"; +import { getContext } from "svelte"; export function isInvalidUuid(uuid) { const violations = []; @@ -36,6 +37,37 @@ export function isInvalidLabel(label) { return violations; } +export function isInvalidAssociationLabel(association, associations) { + const violations = []; + const assocList = Array.isArray(associations) ? associations : associations?.values ?? []; + if (assocList.filter(a => a.label.value === association?.label?.value && a.namespace.value === association.namespace?.value && a.uuid.value !== association?.uuid?.value).length > 0) { + violations.push("must be unique"); + } else if (association && association.domain && association.target && association.inverse && association.label) { + if (association.domain?.value === association.target?.value && association.inverse?.label?.value === association.label?.value) { + violations.push("must be unique"); + } + } + return violations; +} + +export function isInvalidInverseAssociationLabel(association, associations, getClassByUuid) { + const violations = []; + const targetClassDto = getClassByUuid(association.target?.value); + console.log({ targetClassDto }); + const assocList = targetClassDto?.associationPairs?.map(pair => pair.to) ?? []; + console.log({ assocList }); + if (assocList.filter(a => a.label === association?.inverse?.label?.value && + a.prefix === association.inverse?.namespace?.value && + a.uuid !== association.inverse?.uuid?.value).length > 0) { + violations.push("must be unique"); + } else if (association && association.domain && association.target && association.inverse && association.label) { + if (association.domain?.value === association.target?.value && association.inverse?.label?.value === association.label?.value) { + violations.push("must be unique"); + } + } + return violations; +} + export function isInvalidNamespace(namespace) { const violations = []; if (!namespace || namespace.trim() === "") { diff --git a/frontend/src/routes/mainpage/classEditor/classEditor.svelte b/frontend/src/routes/mainpage/classEditor/classEditor.svelte index 662e4536..94129c88 100644 --- a/frontend/src/routes/mainpage/classEditor/classEditor.svelte +++ b/frontend/src/routes/mainpage/classEditor/classEditor.svelte @@ -150,7 +150,7 @@ const classDto = await ( await bec.getClassInfo(datasetName, graphUri, classUuid) ).json(); - reactiveClass = mapClassDtoToReactiveClass(classDto, context.classes); + reactiveClass = mapClassDtoToReactiveClass(classDto, context.classes, (uuid) => context.classes.find(cls => cls.uuid === uuid)); loadingClass = false; console.log({ reactiveClass }); } diff --git a/frontend/src/routes/mainpage/classEditor/components/associations/associationEditorDialog/AssociationEditorDialog.svelte b/frontend/src/routes/mainpage/classEditor/components/associations/associationEditorDialog/AssociationEditorDialog.svelte index 960ffc0c..60f7e2dd 100644 --- a/frontend/src/routes/mainpage/classEditor/components/associations/associationEditorDialog/AssociationEditorDialog.svelte +++ b/frontend/src/routes/mainpage/classEditor/components/associations/associationEditorDialog/AssociationEditorDialog.svelte @@ -43,6 +43,7 @@ namespace: classEditorContext.reactiveClass.namespace.value, }, }); + associations.appendClass(association); } else { isNewAssociation = false; } @@ -68,16 +69,23 @@ association.uuid.value = result.associationUUIDs.fromUUID; association.inverse.uuid.value = result.associationUUIDs.toUUID; if (isNewAssociation) { - associations.append(association); isNewAssociation = false; } association.save(); } + + function onClose() { + if (isNewAssociation) { + associations.remove(association); + } + } + association.reset()} hasChanges={isNewAssociation || association?.isModified} From e99f4804ed243230e4bf2e6d2578543e030ea604 Mon Sep 17 00:00:00 2001 From: Philipp Date: Tue, 14 Apr 2026 13:50:51 +0200 Subject: [PATCH 02/18] association violation checks are now functioning correctly --- .../models/cim/data/CIMObjectFactory.java | 2 +- .../models/cim/queries/select/CIMQueries.java | 1 + .../cim/queries/select/CIMQueryBuilder.java | 11 ++++ .../src/lib/dialog/ModifyDataDialog.svelte | 4 +- .../models/reactive/reactive-class.svelte.js | 28 ++++++--- .../validity-rules/validityFunctions.js | 61 ++++++++++++++----- .../mainpage/classEditor/classEditor.svelte | 33 +++++++++- .../AssociationEditorDialog.svelte | 29 ++++++++- 8 files changed, 140 insertions(+), 29 deletions(-) diff --git a/backend/src/main/java/org/rdfarchitect/models/cim/data/CIMObjectFactory.java b/backend/src/main/java/org/rdfarchitect/models/cim/data/CIMObjectFactory.java index c0669079..5aa5c10c 100644 --- a/backend/src/main/java/org/rdfarchitect/models/cim/data/CIMObjectFactory.java +++ b/backend/src/main/java/org/rdfarchitect/models/cim/data/CIMObjectFactory.java @@ -152,7 +152,7 @@ public static CIMAssociation createCIMAssociation(QuerySolution associationQuery public static CIMAssociation createInverseCIMAssociation(QuerySolution associationQuerySolution) { var parser = new CIMQuerySolutionParser(associationQuerySolution); return CIMAssociation.builder() - .uuid(parser.getUUID(CIMQueryVars.UUID)) + .uuid(parser.getUUID(CIMQueryVars.Inverse.UUID)) .uri(parser.getURI(CIMQueryVars.INVERSE_ROLE_NAME)) .label(parser.getLabel(CIMQueryVars.Inverse.LABEL)) .multiplicity(parser.getMultiplicity(CIMQueryVars.Inverse.MULTIPLICITY)) diff --git a/backend/src/main/java/org/rdfarchitect/models/cim/queries/select/CIMQueries.java b/backend/src/main/java/org/rdfarchitect/models/cim/queries/select/CIMQueries.java index 958fbe9d..6627186e 100644 --- a/backend/src/main/java/org/rdfarchitect/models/cim/queries/select/CIMQueries.java +++ b/backend/src/main/java/org/rdfarchitect/models/cim/queries/select/CIMQueries.java @@ -124,6 +124,7 @@ public SelectBuilder getAssociationPairsQuery(PrefixMapping prefixMapping, Strin .appendDomainQuery(REQUIRED) .appendCommentQuery(OPTIONAL) //inverse + .appendInverseUuidQuery(REQUIRED) .appendInverseLabelQuery(REQUIRED) .appendInverseMultiplicityQuery(REQUIRED) .appendInverseAssociationUsedQuery(REQUIRED) diff --git a/backend/src/main/java/org/rdfarchitect/models/cim/queries/select/CIMQueryBuilder.java b/backend/src/main/java/org/rdfarchitect/models/cim/queries/select/CIMQueryBuilder.java index 79082b06..6c8f60de 100644 --- a/backend/src/main/java/org/rdfarchitect/models/cim/queries/select/CIMQueryBuilder.java +++ b/backend/src/main/java/org/rdfarchitect/models/cim/queries/select/CIMQueryBuilder.java @@ -309,6 +309,17 @@ public CIMQueryBuilder appendPackageQuery(Mode mode) { //inverse + /** + * Appends query for the uuid of objects connected via "cims:inverseRoleName" as {@link CIMQueryVars Inverse.LABEL}. + * + * @param mode Whether the result of this is mode. + * + * @return {@link CIMQueryBuilder this} + */ + public CIMQueryBuilder appendInverseUuidQuery(Mode mode) { + return appendInverseSingleQuery(RDFA.uuid, CIMQueryVars.Inverse.UUID, mode); + } + /** * Appends query for the label of objects connected via "cims:inverseRoleName" as {@link CIMQueryVars Inverse.LABEL}. * diff --git a/frontend/src/lib/dialog/ModifyDataDialog.svelte b/frontend/src/lib/dialog/ModifyDataDialog.svelte index 7d412c48..9606b7f4 100644 --- a/frontend/src/lib/dialog/ModifyDataDialog.svelte +++ b/frontend/src/lib/dialog/ModifyDataDialog.svelte @@ -56,9 +56,9 @@ onClose(); } - function saveAndClose() { + async function saveAndClose() { showDialog = false; - saveChanges(); + await saveChanges(); onClose(); } diff --git a/frontend/src/lib/models/reactive/reactive-class.svelte.js b/frontend/src/lib/models/reactive/reactive-class.svelte.js index f92370f9..4aec3de7 100644 --- a/frontend/src/lib/models/reactive/reactive-class.svelte.js +++ b/frontend/src/lib/models/reactive/reactive-class.svelte.js @@ -21,11 +21,13 @@ import { ReactiveEnumEntry } from "$lib/models/reactive/reactive-enum-entry.svel import { ReactiveObjectsArrayWrapper } from "$lib/models/reactive/reactive-wrappers/reactive-objects-array-wrapper.svelte.js"; import { ReactiveValueWrapper } from "$lib/models/reactive/reactive-wrappers/reactive-value-wrapper.svelte.js"; import { - hasUniqueLabel, isInvalidAssociationLabel, isInvalidInverseAssociationLabel, + hasUniqueLabel, + isInvalidAssociationLabel, + isInvalidInverseAssociationLabel, isInvalidLabel, isInvalidNamespace, isInvalidStereotype, - isInvalidUuid + isInvalidUuid, } from "$lib/models/reactive/validity-rules/validityFunctions.js"; function initializeStereotypeViolationChecks(stereotype, stereotypesArray) { @@ -34,20 +36,21 @@ function initializeStereotypeViolationChecks(stereotype, stereotypesArray) { ); } -function initializeAssociationViolationChecks(association, associationsArray, getClassByUuid) { +function initializeAssociationViolationChecks( + association, + associationsArray, + getClassByUuid, +) { association.label.violationChecks.push(() => - isInvalidAssociationLabel( - association, - associationsArray, - ) + isInvalidAssociationLabel(association, associationsArray), ); association.inverse.label.violationChecks.push(() => isInvalidInverseAssociationLabel( association, associationsArray, getClassByUuid, - ) - ) + ), + ); } function initializeUniqueLabelChecks(reactiveObject, enumEntriesArray) { @@ -92,7 +95,12 @@ export class ReactiveClass { this.associations = new ReactiveObjectsArrayWrapper( associations, ReactiveAssociation, - (association, associationsArray) => initializeAssociationViolationChecks(association, associationsArray, getClassByUuid), + (association, associationsArray) => + initializeAssociationViolationChecks( + association, + associationsArray, + getClassByUuid, + ), ); this.enumEntries = new ReactiveObjectsArrayWrapper( enumEntries, diff --git a/frontend/src/lib/models/reactive/validity-rules/validityFunctions.js b/frontend/src/lib/models/reactive/validity-rules/validityFunctions.js index 941c3768..f6157e95 100644 --- a/frontend/src/lib/models/reactive/validity-rules/validityFunctions.js +++ b/frontend/src/lib/models/reactive/validity-rules/validityFunctions.js @@ -18,7 +18,6 @@ import { validate as uuidValidate } from "uuid"; import { IriValidationStrategy, validateIri } from "validate-iri"; import { getNCNameViolations } from "$lib/rdf-syntax-grammar/namespace/prefix/index.js"; -import { getContext } from "svelte"; export function isInvalidUuid(uuid) { const violations = []; @@ -39,29 +38,63 @@ export function isInvalidLabel(label) { export function isInvalidAssociationLabel(association, associations) { const violations = []; - const assocList = Array.isArray(associations) ? associations : associations?.values ?? []; - if (assocList.filter(a => a.label.value === association?.label?.value && a.namespace.value === association.namespace?.value && a.uuid.value !== association?.uuid?.value).length > 0) { + const assocList = Array.isArray(associations) + ? associations + : (associations?.values ?? []); + if ( + assocList.filter( + a => + a.label.value === association?.label?.value && + a.namespace.value === association.namespace?.value && + a.uuid.value !== association?.uuid?.value, + ).length > 0 + ) { violations.push("must be unique"); - } else if (association && association.domain && association.target && association.inverse && association.label) { - if (association.domain?.value === association.target?.value && association.inverse?.label?.value === association.label?.value) { + } else if ( + association && + association.domain && + association.target && + association.inverse && + association.label + ) { + if ( + association.domain?.value === association.target?.value && + association.inverse?.label?.value === association.label?.value + ) { violations.push("must be unique"); } } return violations; } -export function isInvalidInverseAssociationLabel(association, associations, getClassByUuid) { +export function isInvalidInverseAssociationLabel( + association, + associations, + getClassByUuid, +) { const violations = []; const targetClassDto = getClassByUuid(association.target?.value); - console.log({ targetClassDto }); - const assocList = targetClassDto?.associationPairs?.map(pair => pair.to) ?? []; - console.log({ assocList }); - if (assocList.filter(a => a.label === association?.inverse?.label?.value && - a.prefix === association.inverse?.namespace?.value && - a.uuid !== association.inverse?.uuid?.value).length > 0) { + const assocList = targetClassDto?.associationPairs?.map(pair => pair) ?? []; + if ( + assocList.filter( + a => + a.from.label === association?.inverse?.label?.value && + a.from.prefix === association.inverse?.namespace?.value && + a.from.uuid !== association.inverse?.uuid?.value, + ).length > 0 + ) { violations.push("must be unique"); - } else if (association && association.domain && association.target && association.inverse && association.label) { - if (association.domain?.value === association.target?.value && association.inverse?.label?.value === association.label?.value) { + } else if ( + association && + association.domain && + association.target && + association.inverse && + association.label + ) { + if ( + association.domain?.value === association.target?.value && + association.inverse?.label?.value === association.label?.value + ) { violations.push("must be unique"); } } diff --git a/frontend/src/routes/mainpage/classEditor/classEditor.svelte b/frontend/src/routes/mainpage/classEditor/classEditor.svelte index 94129c88..18d10735 100644 --- a/frontend/src/routes/mainpage/classEditor/classEditor.svelte +++ b/frontend/src/routes/mainpage/classEditor/classEditor.svelte @@ -59,6 +59,7 @@ datatypes: [], packages: [], classes: [], + targetClassInfos: [], }; let isDatasetReadOnly = $state(false); @@ -150,9 +151,28 @@ const classDto = await ( await bec.getClassInfo(datasetName, graphUri, classUuid) ).json(); - reactiveClass = mapClassDtoToReactiveClass(classDto, context.classes, (uuid) => context.classes.find(cls => cls.uuid === uuid)); + reactiveClass = mapClassDtoToReactiveClass( + classDto, + context.classes, + uuid => context.targetClassInfos.find(cls => cls.uuid === uuid), + ); loadingClass = false; console.log({ reactiveClass }); + + const targetUuids = [ + ...new Set( + reactiveClass.associations.values + .map(assoc => assoc.target.value) + .filter(uuid => uuid != null), + ), + ]; + + context.targetClassInfos = await Promise.all( + targetUuids.map(async uuid => { + const res = await bec.getClassInfo(datasetName, graphUri, uuid); + return res.json(); + }), + ); } function openPropertySHACLRulesDialog(property) { @@ -188,12 +208,20 @@ get reactiveClass() { return reactiveClass; }, + get targetClassInfos() { + return context.targetClassInfos; + }, // get Objects by identifier functions get getClassByUuid() { return function (uuid) { return context.classes.find(cls => cls.uuid === uuid); }; }, + get getTargetClassInfoByUuid() { + return function (uuid) { + return context.targetClassInfos.find(cls => cls.uuid === uuid); + }; + }, get getSubstitutedNamespace() { return function (namespace) { const namespaceObj = context.namespaces.find( @@ -220,6 +248,9 @@ return context.packages.find(pkg => pkg.uuid === uuid); }; }, + addTargetClassInfo(classInfo) { + context.targetClassInfos = [...context.targetClassInfos, classInfo]; + }, }); diff --git a/frontend/src/routes/mainpage/classEditor/components/associations/associationEditorDialog/AssociationEditorDialog.svelte b/frontend/src/routes/mainpage/classEditor/components/associations/associationEditorDialog/AssociationEditorDialog.svelte index 60f7e2dd..e56467cb 100644 --- a/frontend/src/routes/mainpage/classEditor/components/associations/associationEditorDialog/AssociationEditorDialog.svelte +++ b/frontend/src/routes/mainpage/classEditor/components/associations/associationEditorDialog/AssociationEditorDialog.svelte @@ -18,6 +18,8 @@ Date: Fri, 10 Apr 2026 17:12:02 +0200 Subject: [PATCH 03/18] fix: load SPARQL templates from classpath in packaged backend (#73) The loader was calling ClassPathResource#getFile(), which which only works when the resource exists as a real filesystem file. In a packaged jar it becomes a nested classpath resource, so getFile() throws a FileNotFoundException. --- .../cim/queries/templates/SparqlTemplateLoader.java | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/backend/src/main/java/org/rdfarchitect/models/cim/queries/templates/SparqlTemplateLoader.java b/backend/src/main/java/org/rdfarchitect/models/cim/queries/templates/SparqlTemplateLoader.java index 1c7c6e8f..04e0ee40 100644 --- a/backend/src/main/java/org/rdfarchitect/models/cim/queries/templates/SparqlTemplateLoader.java +++ b/backend/src/main/java/org/rdfarchitect/models/cim/queries/templates/SparqlTemplateLoader.java @@ -19,17 +19,24 @@ import org.apache.jena.query.ParameterizedSparqlString; import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; +import java.io.IOException; import java.nio.charset.StandardCharsets; -import java.nio.file.Files; public class SparqlTemplateLoader { public static ParameterizedSparqlString loadTemplate(String path) { try { var resource = new ClassPathResource("sparql-templates/" + path + ".sparql"); - return new ParameterizedSparqlString(Files.readString(resource.getFile().toPath(), StandardCharsets.UTF_8)); + return new ParameterizedSparqlString(readTemplate(resource)); } catch (Exception e) { throw new RuntimeException("Failed to load SPARQL template: " + path, e); } } + + static String readTemplate(Resource resource) throws IOException { + try (var inputStream = resource.getInputStream()) { + return new String(inputStream.readAllBytes(), StandardCharsets.UTF_8); + } + } } From e6c741f7870f6216da9bba49c41483fb4da13cef Mon Sep 17 00:00:00 2001 From: Maximilian Reisch Date: Mon, 13 Apr 2026 09:27:29 +0200 Subject: [PATCH 04/18] fix: added unsaved changes adoption, when saving a class property (#65, RDFA-371) * - added unsaved changes adoption, when saving a class property. - moved files in model/reactive for better hierarchy * fixed bugs: - where previously removed properties were added again when merging 2 classes - where the property editors didn't clear after creating a new attribute - Where creating a property would create it 2 times --- frontend/src/lib/GraphExport.svelte | 2 +- .../mapper/map-dto-to-reactive-object.js | 2 +- .../mapper/map-reactive-object-to-dto.js | 16 ++- .../reactive-ontology-entry.svelte.js | 0 .../ontology/reactive-ontology.svelte.js | 2 +- .../reactive-association.svelte.js | 0 .../{ => models}/reactive-attribute.svelte.js | 0 .../{ => models}/reactive-class.svelte.js | 6 +- .../reactive-enum-entry.svelte.js | 0 .../{ => models}/reactive-namespace.svelte.js | 0 .../{ => models}/reactive-package.svelte.js | 0 .../reactive-value-wrapper.svelte.js | 4 +- .../utils/adopt-model-changes-utils.js | 103 ++++++++++++++++++ .../reactive-objects-control-button-utils.js} | 0 frontend/src/routes/NamespacesDialog.svelte | 4 +- .../mainpage/classEditor/classEditor.svelte | 13 +-- .../classEditor/components/Comment.svelte | 2 +- .../classEditor/components/Label.svelte | 2 +- .../classEditor/components/Namespace.svelte | 2 +- .../classEditor/components/Package.svelte | 2 +- .../classEditor/components/SuperClass.svelte | 2 +- .../associations/Association.svelte | 2 +- .../AssociationEditorDialog.svelte | 11 +- .../associationEditorDialog/Direct.svelte | 2 +- .../associationEditorDialog/Inverse.svelte | 2 +- .../components/attributes/Attribute.svelte | 2 +- .../attributes/AttributeEditorDialog.svelte | 18 ++- .../components/enum-entries/EnumEntry.svelte | 2 +- .../enum-entries/EnumEntryEditorDialog.svelte | 18 ++- .../components/stereotypes/Stereotype.svelte | 2 +- .../mainpage/packageEditorDialog.svelte | 4 +- .../OntologyDialog.svelte | 2 +- .../shacl/SHACLPropertySpecificDialog.svelte | 4 +- 33 files changed, 178 insertions(+), 53 deletions(-) rename frontend/src/lib/models/reactive/{ => models}/ontology/reactive-ontology-entry.svelte.js (100%) rename frontend/src/lib/models/reactive/{ => models}/ontology/reactive-ontology.svelte.js (96%) rename frontend/src/lib/models/reactive/{ => models}/reactive-association.svelte.js (100%) rename frontend/src/lib/models/reactive/{ => models}/reactive-attribute.svelte.js (100%) rename frontend/src/lib/models/reactive/{ => models}/reactive-class.svelte.js (96%) rename frontend/src/lib/models/reactive/{ => models}/reactive-enum-entry.svelte.js (100%) rename frontend/src/lib/models/reactive/{ => models}/reactive-namespace.svelte.js (100%) rename frontend/src/lib/models/reactive/{ => models}/reactive-package.svelte.js (100%) create mode 100644 frontend/src/lib/models/reactive/utils/adopt-model-changes-utils.js rename frontend/src/lib/models/reactive/{reactive-utils.js => utils/reactive-objects-control-button-utils.js} (100%) diff --git a/frontend/src/lib/GraphExport.svelte b/frontend/src/lib/GraphExport.svelte index 969a3d3a..cf06112c 100644 --- a/frontend/src/lib/GraphExport.svelte +++ b/frontend/src/lib/GraphExport.svelte @@ -25,7 +25,7 @@ import { DropdownMenu } from "$lib/components/bitsui/dropdown/index"; import DatasetAndGraphSelection from "$lib/components/DatasetAndGraphSelection.svelte"; import { PUBLIC_BACKEND_URL } from "$lib/config/runtime"; - import { ReactiveOntology } from "$lib/models/reactive/ontology/reactive-ontology.svelte.js"; + import { ReactiveOntology } from "$lib/models/reactive/models/ontology/reactive-ontology.svelte.js"; import { forceReloadTrigger } from "$lib/sharedState.svelte.js"; import { saveFile, supportedRDFMediaTypes } from "$lib/utils/fileUtils.ts"; diff --git a/frontend/src/lib/models/reactive/mapper/map-dto-to-reactive-object.js b/frontend/src/lib/models/reactive/mapper/map-dto-to-reactive-object.js index 90b39c9b..8c2ba37e 100644 --- a/frontend/src/lib/models/reactive/mapper/map-dto-to-reactive-object.js +++ b/frontend/src/lib/models/reactive/mapper/map-dto-to-reactive-object.js @@ -15,7 +15,7 @@ * */ -import { ReactiveClass } from "$lib/models/reactive/reactive-class.svelte.js"; +import { ReactiveClass } from "$lib/models/reactive/models/reactive-class.svelte.js"; /** * Maps a class DTO to a ReactiveClass instance diff --git a/frontend/src/lib/models/reactive/mapper/map-reactive-object-to-dto.js b/frontend/src/lib/models/reactive/mapper/map-reactive-object-to-dto.js index 95bd25dd..887e529c 100644 --- a/frontend/src/lib/models/reactive/mapper/map-reactive-object-to-dto.js +++ b/frontend/src/lib/models/reactive/mapper/map-reactive-object-to-dto.js @@ -14,12 +14,12 @@ * limitations under the License. * */ -import { ReactiveAssociation } from "$lib/models/reactive/reactive-association.svelte.js"; -import { ReactiveAttribute } from "$lib/models/reactive/reactive-attribute.svelte.js"; -import { ReactiveClass } from "$lib/models/reactive/reactive-class.svelte.js"; -import { ReactiveEnumEntry } from "$lib/models/reactive/reactive-enum-entry.svelte.js"; -import { ReactiveNamespace } from "$lib/models/reactive/reactive-namespace.svelte.js"; -import { ReactivePackage } from "$lib/models/reactive/reactive-package.svelte.js"; +import { ReactiveAssociation } from "$lib/models/reactive/models/reactive-association.svelte.js"; +import { ReactiveAttribute } from "$lib/models/reactive/models/reactive-attribute.svelte.js"; +import { ReactiveClass } from "$lib/models/reactive/models/reactive-class.svelte.js"; +import { ReactiveEnumEntry } from "$lib/models/reactive/models/reactive-enum-entry.svelte.js"; +import { ReactiveNamespace } from "$lib/models/reactive/models/reactive-namespace.svelte.js"; +import { ReactivePackage } from "$lib/models/reactive/models/reactive-package.svelte.js"; /** * Maps a ReactiveClass to a class DTO for API submission @@ -170,7 +170,11 @@ export function mapReactiveAssociationToAssociationDto( getClassByUuid, ) { if (cls instanceof ReactiveClass) { + const labelBackup = cls.label.backup; + const namespaceBackup = cls.namespace.backup; cls = cls.getPlainObject(); + cls.label = labelBackup; + cls.namespace = namespaceBackup; } if (association instanceof ReactiveAssociation) { association = association.getPlainObject(); diff --git a/frontend/src/lib/models/reactive/ontology/reactive-ontology-entry.svelte.js b/frontend/src/lib/models/reactive/models/ontology/reactive-ontology-entry.svelte.js similarity index 100% rename from frontend/src/lib/models/reactive/ontology/reactive-ontology-entry.svelte.js rename to frontend/src/lib/models/reactive/models/ontology/reactive-ontology-entry.svelte.js diff --git a/frontend/src/lib/models/reactive/ontology/reactive-ontology.svelte.js b/frontend/src/lib/models/reactive/models/ontology/reactive-ontology.svelte.js similarity index 96% rename from frontend/src/lib/models/reactive/ontology/reactive-ontology.svelte.js rename to frontend/src/lib/models/reactive/models/ontology/reactive-ontology.svelte.js index cb833861..23d5e547 100644 --- a/frontend/src/lib/models/reactive/ontology/reactive-ontology.svelte.js +++ b/frontend/src/lib/models/reactive/models/ontology/reactive-ontology.svelte.js @@ -15,7 +15,7 @@ * */ -import { ReactiveOntologyEntry } from "$lib/models/reactive/ontology/reactive-ontology-entry.svelte.js"; +import { ReactiveOntologyEntry } from "$lib/models/reactive/models/ontology/reactive-ontology-entry.svelte.js"; import { ReactiveObjectsArrayWrapper } from "$lib/models/reactive/reactive-wrappers/reactive-objects-array-wrapper.svelte.js"; import { ReactiveValueWrapper } from "$lib/models/reactive/reactive-wrappers/reactive-value-wrapper.svelte.js"; import { diff --git a/frontend/src/lib/models/reactive/reactive-association.svelte.js b/frontend/src/lib/models/reactive/models/reactive-association.svelte.js similarity index 100% rename from frontend/src/lib/models/reactive/reactive-association.svelte.js rename to frontend/src/lib/models/reactive/models/reactive-association.svelte.js diff --git a/frontend/src/lib/models/reactive/reactive-attribute.svelte.js b/frontend/src/lib/models/reactive/models/reactive-attribute.svelte.js similarity index 100% rename from frontend/src/lib/models/reactive/reactive-attribute.svelte.js rename to frontend/src/lib/models/reactive/models/reactive-attribute.svelte.js diff --git a/frontend/src/lib/models/reactive/reactive-class.svelte.js b/frontend/src/lib/models/reactive/models/reactive-class.svelte.js similarity index 96% rename from frontend/src/lib/models/reactive/reactive-class.svelte.js rename to frontend/src/lib/models/reactive/models/reactive-class.svelte.js index 4aec3de7..ec030b90 100644 --- a/frontend/src/lib/models/reactive/reactive-class.svelte.js +++ b/frontend/src/lib/models/reactive/models/reactive-class.svelte.js @@ -15,9 +15,9 @@ * */ -import { ReactiveAssociation } from "$lib/models/reactive/reactive-association.svelte.js"; -import { ReactiveAttribute } from "$lib/models/reactive/reactive-attribute.svelte.js"; -import { ReactiveEnumEntry } from "$lib/models/reactive/reactive-enum-entry.svelte.js"; +import { ReactiveAssociation } from "$lib/models/reactive/models/reactive-association.svelte.js"; +import { ReactiveAttribute } from "$lib/models/reactive/models/reactive-attribute.svelte.js"; +import { ReactiveEnumEntry } from "$lib/models/reactive/models/reactive-enum-entry.svelte.js"; import { ReactiveObjectsArrayWrapper } from "$lib/models/reactive/reactive-wrappers/reactive-objects-array-wrapper.svelte.js"; import { ReactiveValueWrapper } from "$lib/models/reactive/reactive-wrappers/reactive-value-wrapper.svelte.js"; import { diff --git a/frontend/src/lib/models/reactive/reactive-enum-entry.svelte.js b/frontend/src/lib/models/reactive/models/reactive-enum-entry.svelte.js similarity index 100% rename from frontend/src/lib/models/reactive/reactive-enum-entry.svelte.js rename to frontend/src/lib/models/reactive/models/reactive-enum-entry.svelte.js diff --git a/frontend/src/lib/models/reactive/reactive-namespace.svelte.js b/frontend/src/lib/models/reactive/models/reactive-namespace.svelte.js similarity index 100% rename from frontend/src/lib/models/reactive/reactive-namespace.svelte.js rename to frontend/src/lib/models/reactive/models/reactive-namespace.svelte.js diff --git a/frontend/src/lib/models/reactive/reactive-package.svelte.js b/frontend/src/lib/models/reactive/models/reactive-package.svelte.js similarity index 100% rename from frontend/src/lib/models/reactive/reactive-package.svelte.js rename to frontend/src/lib/models/reactive/models/reactive-package.svelte.js diff --git a/frontend/src/lib/models/reactive/reactive-wrappers/reactive-value-wrapper.svelte.js b/frontend/src/lib/models/reactive/reactive-wrappers/reactive-value-wrapper.svelte.js index 4c212d0d..8ae7e697 100644 --- a/frontend/src/lib/models/reactive/reactive-wrappers/reactive-value-wrapper.svelte.js +++ b/frontend/src/lib/models/reactive/reactive-wrappers/reactive-value-wrapper.svelte.js @@ -21,7 +21,9 @@ export class ReactiveValueWrapper { * @param {Array | function(*): string[]} violationChecks - An array of functions to validate the value */ constructor(value, violationChecks = []) { + let backup = value; if (value instanceof ReactiveValueWrapper) { + backup = value.backup; value = value.value; } const checks = Array.isArray(violationChecks) @@ -29,7 +31,7 @@ export class ReactiveValueWrapper { : [violationChecks]; this.violationChecks.push(...checks); - this.backup = value; + this.backup = backup; this.value = value; } diff --git a/frontend/src/lib/models/reactive/utils/adopt-model-changes-utils.js b/frontend/src/lib/models/reactive/utils/adopt-model-changes-utils.js new file mode 100644 index 00000000..3ddbd4bc --- /dev/null +++ b/frontend/src/lib/models/reactive/utils/adopt-model-changes-utils.js @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2024-2026 SOPTIM AG + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ReactiveValueWrapper } from "$lib/models/reactive/reactive-wrappers/reactive-value-wrapper.svelte.js"; + +export function adoptUnsavedClassChanges(newClass, oldClass) { + if (!oldClass || newClass.uuid.backup !== oldClass.uuid.backup) { + return newClass; + } + + adoptModifiedProperties(newClass, oldClass); + + adoptModifiedArrayEntries( + newClass.stereotypes, + oldClass.stereotypes, + (a, b) => a.backup === b.backup, + (newEntry, oldEntry) => { + newEntry.value = oldEntry.value; + }, + ); + + adoptModifiedArrayEntries( + newClass.attributes, + oldClass.attributes, + (a, b) => a.uuid.backup === b.uuid.backup, + adoptModifiedProperties, + ); + + adoptModifiedArrayEntries( + newClass.associations, + oldClass.associations, + (a, b) => a.uuid.backup === b.uuid.backup, + adoptUnsavedAssociationChanges, + ); + + adoptModifiedArrayEntries( + newClass.enumEntries, + oldClass.enumEntries, + (a, b) => a.uuid.backup === b.uuid.backup, + adoptModifiedProperties, + ); + + return newClass; +} + +function adoptIfModified(target, source) { + if (source.isModified) { + target.value = source.value; + } +} + +function adoptModifiedProperties(target, source) { + for (const key of Object.keys(source)) { + if (source[key] instanceof ReactiveValueWrapper) { + adoptIfModified(target[key], source[key]); + } + } +} + +function adoptModifiedArrayEntries(newArray, oldArray, matchFn, adoptFn) { + if (!oldArray.isModified) { + return; + } + for (const oldEntry of oldArray.values) { + const newEntry = newArray.values.find(e => matchFn(e, oldEntry)); + if (newEntry) { + adoptFn(newEntry, oldEntry); + } else { + newArray.append(oldEntry); + } + } + + // Remove deleted entries + for (const backupEntry of oldArray.backup) { + const wasDeleted = !oldArray.values.some(e => matchFn(e, backupEntry)); + if (wasDeleted) { + const entryInNewArray = newArray.values.find(e => + matchFn(e, backupEntry), + ); + if (entryInNewArray) { + newArray.remove(entryInNewArray, true); + } + } + } +} + +function adoptUnsavedAssociationChanges(newAssociation, oldAssociation) { + adoptModifiedProperties(newAssociation, oldAssociation); + adoptModifiedProperties(newAssociation.inverse, oldAssociation.inverse); +} diff --git a/frontend/src/lib/models/reactive/reactive-utils.js b/frontend/src/lib/models/reactive/utils/reactive-objects-control-button-utils.js similarity index 100% rename from frontend/src/lib/models/reactive/reactive-utils.js rename to frontend/src/lib/models/reactive/utils/reactive-objects-control-button-utils.js diff --git a/frontend/src/routes/NamespacesDialog.svelte b/frontend/src/routes/NamespacesDialog.svelte index d1f920db..77dc0446 100644 --- a/frontend/src/routes/NamespacesDialog.svelte +++ b/frontend/src/routes/NamespacesDialog.svelte @@ -29,9 +29,9 @@ import ModifyDataDialog from "$lib/dialog/ModifyDataDialog.svelte"; import { mapNamespaceDtoToReactiveNamespace } from "$lib/models/reactive/mapper/map-dto-to-reactive-object.js"; import { mapReactiveNamespaceToNamespaceDto } from "$lib/models/reactive/mapper/map-reactive-object-to-dto.js"; - import { ReactiveNamespace } from "$lib/models/reactive/reactive-namespace.svelte.js"; - import { getControlButtonsForReactiveObject } from "$lib/models/reactive/reactive-utils.js"; + import { ReactiveNamespace } from "$lib/models/reactive/models/reactive-namespace.svelte.js"; import { ReactiveObjectsArrayWrapper } from "$lib/models/reactive/reactive-wrappers/reactive-objects-array-wrapper.svelte.js"; + import { getControlButtonsForReactiveObject } from "$lib/models/reactive/utils/reactive-objects-control-button-utils.js"; import { namespacePrefixesAreUnique } from "$lib/models/reactive/validity-rules/validityFunctions.js"; import { editorState, diff --git a/frontend/src/routes/mainpage/classEditor/classEditor.svelte b/frontend/src/routes/mainpage/classEditor/classEditor.svelte index 18d10735..eb25b0e4 100644 --- a/frontend/src/routes/mainpage/classEditor/classEditor.svelte +++ b/frontend/src/routes/mainpage/classEditor/classEditor.svelte @@ -25,6 +25,7 @@ import { PUBLIC_BACKEND_URL } from "$lib/config/runtime"; import { eventStack } from "$lib/eventhandling/closeEventManager.svelte.js"; import { mapClassDtoToReactiveClass } from "$lib/models/reactive/mapper/map-dto-to-reactive-object.js"; + import { adoptUnsavedClassChanges } from "$lib/models/reactive/utils/adopt-model-changes-utils.js"; import { editorState } from "$lib/sharedState.svelte.js"; import { @@ -100,12 +101,6 @@ isDatasetReadOnly = await isReadOnly(datasetName); }); - onMount(async () => { - isDatasetReadOnly = await isReadOnly(datasetName); - await loadContext(); - await loadReactiveClass(); - }); - onMount(() => eventStack.addEvent(closeClassEditor)); onDestroy(() => eventStack.removeEvent(closeClassEditor)); @@ -151,11 +146,15 @@ const classDto = await ( await bec.getClassInfo(datasetName, graphUri, classUuid) ).json(); - reactiveClass = mapClassDtoToReactiveClass( + const newReactiveClass = mapClassDtoToReactiveClass( classDto, context.classes, uuid => context.targetClassInfos.find(cls => cls.uuid === uuid), ); + reactiveClass = adoptUnsavedClassChanges( + newReactiveClass, + reactiveClass, + ); loadingClass = false; console.log({ reactiveClass }); diff --git a/frontend/src/routes/mainpage/classEditor/components/Comment.svelte b/frontend/src/routes/mainpage/classEditor/components/Comment.svelte index abc0249f..11c36985 100644 --- a/frontend/src/routes/mainpage/classEditor/components/Comment.svelte +++ b/frontend/src/routes/mainpage/classEditor/components/Comment.svelte @@ -21,7 +21,7 @@ import FaIconButton from "$lib/components/FaIconButton.svelte"; import TextAreaControl from "$lib/components/TextAreaControl.svelte"; - import { getControlButtonsForReactiveObject } from "$lib/models/reactive/reactive-utils.js"; + import { getControlButtonsForReactiveObject } from "$lib/models/reactive/utils/reactive-objects-control-button-utils.js"; import { editorState } from "$lib/sharedState.svelte.js"; import AsciidocComment from "../asciidocComment.svelte"; diff --git a/frontend/src/routes/mainpage/classEditor/components/Label.svelte b/frontend/src/routes/mainpage/classEditor/components/Label.svelte index 1b16ee7d..1e429496 100644 --- a/frontend/src/routes/mainpage/classEditor/components/Label.svelte +++ b/frontend/src/routes/mainpage/classEditor/components/Label.svelte @@ -20,7 +20,7 @@ import TextEditControl from "$lib/components/TextEditControl.svelte"; import ViolationMessages from "$lib/components/ViolationMessages.svelte"; - import { getControlButtonsForReactiveObject } from "$lib/models/reactive/reactive-utils.js"; + import { getControlButtonsForReactiveObject } from "$lib/models/reactive/utils/reactive-objects-control-button-utils.js"; import { editorState } from "$lib/sharedState.svelte.js"; let { label } = $props(); diff --git a/frontend/src/routes/mainpage/classEditor/components/Namespace.svelte b/frontend/src/routes/mainpage/classEditor/components/Namespace.svelte index 11cac318..1a9cf2d9 100644 --- a/frontend/src/routes/mainpage/classEditor/components/Namespace.svelte +++ b/frontend/src/routes/mainpage/classEditor/components/Namespace.svelte @@ -20,7 +20,7 @@ import SearchableSelect from "$lib/components/SearchableSelect.svelte"; import ViolationMessages from "$lib/components/ViolationMessages.svelte"; - import { getControlButtonsForReactiveObject } from "$lib/models/reactive/reactive-utils.js"; + import { getControlButtonsForReactiveObject } from "$lib/models/reactive/utils/reactive-objects-control-button-utils.js"; import { editorState } from "$lib/sharedState.svelte.js"; import { getNsPrefixNsUriString } from "$lib/utils/namespace.js"; diff --git a/frontend/src/routes/mainpage/classEditor/components/Package.svelte b/frontend/src/routes/mainpage/classEditor/components/Package.svelte index 8fea39ee..9fa8456a 100644 --- a/frontend/src/routes/mainpage/classEditor/components/Package.svelte +++ b/frontend/src/routes/mainpage/classEditor/components/Package.svelte @@ -20,7 +20,7 @@ import SearchableSelect from "$lib/components/SearchableSelect.svelte"; import ViolationMessages from "$lib/components/ViolationMessages.svelte"; - import { getControlButtonsForReactiveObject } from "$lib/models/reactive/reactive-utils.js"; + import { getControlButtonsForReactiveObject } from "$lib/models/reactive/utils/reactive-objects-control-button-utils.js"; import { editorState } from "$lib/sharedState.svelte.js"; let { pack } = $props(); diff --git a/frontend/src/routes/mainpage/classEditor/components/SuperClass.svelte b/frontend/src/routes/mainpage/classEditor/components/SuperClass.svelte index 7a8956e3..3f4ba80a 100644 --- a/frontend/src/routes/mainpage/classEditor/components/SuperClass.svelte +++ b/frontend/src/routes/mainpage/classEditor/components/SuperClass.svelte @@ -21,7 +21,7 @@ import SearchableSelect from "$lib/components/SearchableSelect.svelte"; import ViolationMessages from "$lib/components/ViolationMessages.svelte"; - import { getControlButtonsForReactiveObject } from "$lib/models/reactive/reactive-utils.js"; + import { getControlButtonsForReactiveObject } from "$lib/models/reactive/utils/reactive-objects-control-button-utils.js"; import { editorState } from "$lib/sharedState.svelte.js"; let { superClass } = $props(); diff --git a/frontend/src/routes/mainpage/classEditor/components/associations/Association.svelte b/frontend/src/routes/mainpage/classEditor/components/associations/Association.svelte index 093d0255..b623690f 100644 --- a/frontend/src/routes/mainpage/classEditor/components/associations/Association.svelte +++ b/frontend/src/routes/mainpage/classEditor/components/associations/Association.svelte @@ -28,7 +28,7 @@ import NumberInputControl from "$lib/components/NumberInputControl.svelte"; import SearchableSelect from "$lib/components/SearchableSelect.svelte"; import ViolationMessages from "$lib/components/ViolationMessages.svelte"; - import { getControlButtonsForReactiveObject } from "$lib/models/reactive/reactive-utils.js"; + import { getControlButtonsForReactiveObject } from "$lib/models/reactive/utils/reactive-objects-control-button-utils.js"; import { editorState } from "$lib/sharedState.svelte.js"; const { diff --git a/frontend/src/routes/mainpage/classEditor/components/associations/associationEditorDialog/AssociationEditorDialog.svelte b/frontend/src/routes/mainpage/classEditor/components/associations/associationEditorDialog/AssociationEditorDialog.svelte index e56467cb..ec7114f3 100644 --- a/frontend/src/routes/mainpage/classEditor/components/associations/associationEditorDialog/AssociationEditorDialog.svelte +++ b/frontend/src/routes/mainpage/classEditor/components/associations/associationEditorDialog/AssociationEditorDialog.svelte @@ -22,7 +22,7 @@ import { PUBLIC_BACKEND_URL } from "$lib/config/runtime.js"; import ModifyDataDialog from "$lib/dialog/ModifyDataDialog.svelte"; import { mapReactiveAssociationToAssociationDto } from "$lib/models/reactive/mapper/map-reactive-object-to-dto.js"; - import { ReactiveAssociation } from "$lib/models/reactive/reactive-association.svelte.js"; + import { ReactiveAssociation } from "$lib/models/reactive/models/reactive-association.svelte.js"; import Direct from "./Direct.svelte"; import { saveApiAssociationToBackend } from "../save-association-to-backend.js"; @@ -76,6 +76,11 @@ } } + function onClose() { + association = null; + isNewAssociation = true; + } + async function saveAssociation() { const apiAssociation = mapReactiveAssociationToAssociationDto( association, @@ -95,10 +100,10 @@ association.uuid.value = result.associationUUIDs.fromUUID; association.inverse.uuid.value = result.associationUUIDs.toUUID; + association.save(); if (isNewAssociation) { isNewAssociation = false; } - association.save(); } function onClose() { @@ -115,7 +120,7 @@ {onClose} saveChanges={saveAssociation} discardChanges={() => association.reset()} - hasChanges={isNewAssociation || association?.isModified} + hasChanges={association?.isModified} isValid={association?.isValid} size="w-2/3" {readonly} diff --git a/frontend/src/routes/mainpage/classEditor/components/associations/associationEditorDialog/Direct.svelte b/frontend/src/routes/mainpage/classEditor/components/associations/associationEditorDialog/Direct.svelte index f4f0dc6b..7ddfe7bc 100644 --- a/frontend/src/routes/mainpage/classEditor/components/associations/associationEditorDialog/Direct.svelte +++ b/frontend/src/routes/mainpage/classEditor/components/associations/associationEditorDialog/Direct.svelte @@ -23,7 +23,7 @@ import TextAreaControl from "$lib/components/TextAreaControl.svelte"; import TextEditControl from "$lib/components/TextEditControl.svelte"; import ViolationMessages from "$lib/components/ViolationMessages.svelte"; - import { getControlButtonsForReactiveObject } from "$lib/models/reactive/reactive-utils.js"; + import { getControlButtonsForReactiveObject } from "$lib/models/reactive/utils/reactive-objects-control-button-utils.js"; import { getNsPrefixNsUriString } from "$lib/utils/namespace.js"; const { association } = $props(); diff --git a/frontend/src/routes/mainpage/classEditor/components/associations/associationEditorDialog/Inverse.svelte b/frontend/src/routes/mainpage/classEditor/components/associations/associationEditorDialog/Inverse.svelte index 193c1c7c..fd69893a 100644 --- a/frontend/src/routes/mainpage/classEditor/components/associations/associationEditorDialog/Inverse.svelte +++ b/frontend/src/routes/mainpage/classEditor/components/associations/associationEditorDialog/Inverse.svelte @@ -23,7 +23,7 @@ import TextAreaControl from "$lib/components/TextAreaControl.svelte"; import TextEditControl from "$lib/components/TextEditControl.svelte"; import ViolationMessages from "$lib/components/ViolationMessages.svelte"; - import { getControlButtonsForReactiveObject } from "$lib/models/reactive/reactive-utils.js"; + import { getControlButtonsForReactiveObject } from "$lib/models/reactive/utils/reactive-objects-control-button-utils.js"; import { getNsPrefixNsUriString } from "$lib/utils/namespace.js"; const { association } = $props(); diff --git a/frontend/src/routes/mainpage/classEditor/components/attributes/Attribute.svelte b/frontend/src/routes/mainpage/classEditor/components/attributes/Attribute.svelte index 1e789647..602d522c 100644 --- a/frontend/src/routes/mainpage/classEditor/components/attributes/Attribute.svelte +++ b/frontend/src/routes/mainpage/classEditor/components/attributes/Attribute.svelte @@ -27,7 +27,7 @@ import SearchableSelect from "$lib/components/SearchableSelect.svelte"; import TextEditControl from "$lib/components/TextEditControl.svelte"; import ViolationMessages from "$lib/components/ViolationMessages.svelte"; - import { getControlButtonsForReactiveObject } from "$lib/models/reactive/reactive-utils.js"; + import { getControlButtonsForReactiveObject } from "$lib/models/reactive/utils/reactive-objects-control-button-utils.js"; import { editorState } from "$lib/sharedState.svelte.js"; let { diff --git a/frontend/src/routes/mainpage/classEditor/components/attributes/AttributeEditorDialog.svelte b/frontend/src/routes/mainpage/classEditor/components/attributes/AttributeEditorDialog.svelte index a7bd9b05..11146de0 100644 --- a/frontend/src/routes/mainpage/classEditor/components/attributes/AttributeEditorDialog.svelte +++ b/frontend/src/routes/mainpage/classEditor/components/attributes/AttributeEditorDialog.svelte @@ -24,8 +24,8 @@ import ViolationMessages from "$lib/components/ViolationMessages.svelte"; import ModifyDataDialog from "$lib/dialog/ModifyDataDialog.svelte"; import { mapReactiveAttributeToAttributeDto } from "$lib/models/reactive/mapper/map-reactive-object-to-dto.js"; - import { ReactiveAttribute } from "$lib/models/reactive/reactive-attribute.svelte.js"; - import { getControlButtonsForReactiveObject } from "$lib/models/reactive/reactive-utils.js"; + import { ReactiveAttribute } from "$lib/models/reactive/models/reactive-attribute.svelte.js"; + import { getControlButtonsForReactiveObject } from "$lib/models/reactive/utils/reactive-objects-control-button-utils.js"; import { getNsPrefixNsUriString } from "$lib/utils/namespace.js"; import { saveApiAttributeToBackend } from "./save-attribute-to-backend.js"; @@ -50,6 +50,11 @@ } } + function onClose() { + attribute = null; + isNewAttribute = true; + } + function getDatatypeLabelByUri(uri) { const datatype = classEditorContext.getDatatypeByUri(uri); if (!datatype) { @@ -62,8 +67,8 @@ const apiAttribute = mapReactiveAttributeToAttributeDto( attribute, classEditorContext.getDatatypeByUri, - classEditorContext.reactiveClass.namespace.value + - classEditorContext.reactiveClass.label.value, + classEditorContext.reactiveClass.namespace.backup + + classEditorContext.reactiveClass.label.backup, ); const result = await saveApiAttributeToBackend( classEditorContext.datasetName, @@ -77,20 +82,21 @@ } attribute.uuid.value = result.attributeUUID; + attribute.save(); if (isNewAttribute) { attributes.append(attribute); isNewAttribute = false; } - attribute.save(); } attribute.reset()} - hasChanges={isNewAttribute || attribute?.isModified} + hasChanges={attribute?.isModified} isValid={attribute?.isValid} title={isNewAttribute ? "Create new attribute" diff --git a/frontend/src/routes/mainpage/classEditor/components/enum-entries/EnumEntry.svelte b/frontend/src/routes/mainpage/classEditor/components/enum-entries/EnumEntry.svelte index d0626e86..cd79a53b 100644 --- a/frontend/src/routes/mainpage/classEditor/components/enum-entries/EnumEntry.svelte +++ b/frontend/src/routes/mainpage/classEditor/components/enum-entries/EnumEntry.svelte @@ -22,7 +22,7 @@ import FaIconButton from "$lib/components/FaIconButton.svelte"; import TextEditControl from "$lib/components/TextEditControl.svelte"; import ViolationMessages from "$lib/components/ViolationMessages.svelte"; - import { getControlButtonsForReactiveObject } from "$lib/models/reactive/reactive-utils.js"; + import { getControlButtonsForReactiveObject } from "$lib/models/reactive/utils/reactive-objects-control-button-utils.js"; import { editorState } from "$lib/sharedState.svelte.js"; const { enumEntries, enumEntry, openEnumEntryEditor } = $props(); diff --git a/frontend/src/routes/mainpage/classEditor/components/enum-entries/EnumEntryEditorDialog.svelte b/frontend/src/routes/mainpage/classEditor/components/enum-entries/EnumEntryEditorDialog.svelte index 6dc46cd4..3dc5c3ed 100644 --- a/frontend/src/routes/mainpage/classEditor/components/enum-entries/EnumEntryEditorDialog.svelte +++ b/frontend/src/routes/mainpage/classEditor/components/enum-entries/EnumEntryEditorDialog.svelte @@ -23,8 +23,8 @@ import ViolationMessages from "$lib/components/ViolationMessages.svelte"; import ModifyDataDialog from "$lib/dialog/ModifyDataDialog.svelte"; import { mapReactiveEnumEntryToEnumEntryDto } from "$lib/models/reactive/mapper/map-reactive-object-to-dto.js"; - import { ReactiveEnumEntry } from "$lib/models/reactive/reactive-enum-entry.svelte.js"; - import { getControlButtonsForReactiveObject } from "$lib/models/reactive/reactive-utils.js"; + import { ReactiveEnumEntry } from "$lib/models/reactive/models/reactive-enum-entry.svelte.js"; + import { getControlButtonsForReactiveObject } from "$lib/models/reactive/utils/reactive-objects-control-button-utils.js"; import { getNsPrefixNsUriString } from "$lib/utils/namespace.js"; import { saveApiEnumEntryToBackend } from "./save-enum-entry-to-backend.js"; @@ -48,11 +48,16 @@ } } + function onClose() { + enumEntry = null; + isNewEnumEntry = true; + } + async function saveEnumEntry() { const apiEnumEntry = mapReactiveEnumEntryToEnumEntryDto( enumEntry, - classEditorContext.reactiveClass.namespace.value + - classEditorContext.reactiveClass.label.value, + classEditorContext.reactiveClass.namespace.backup + + classEditorContext.reactiveClass.label.backup, ); const result = await saveApiEnumEntryToBackend( classEditorContext.datasetName, @@ -66,20 +71,21 @@ } enumEntry.uuid.value = result.enumEntryUUID; + enumEntry.save(); if (isNewEnumEntry) { enumEntries.append(enumEntry); isNewEnumEntry = false; } - enumEntry.save(); } enumEntry.reset()} - hasChanges={isNewEnumEntry || enumEntry?.isModified} + hasChanges={enumEntry?.isModified} isValid={enumEntry?.isValid} {readonly} title={isNewEnumEntry diff --git a/frontend/src/routes/mainpage/classEditor/components/stereotypes/Stereotype.svelte b/frontend/src/routes/mainpage/classEditor/components/stereotypes/Stereotype.svelte index d7f82f4d..5223754c 100644 --- a/frontend/src/routes/mainpage/classEditor/components/stereotypes/Stereotype.svelte +++ b/frontend/src/routes/mainpage/classEditor/components/stereotypes/Stereotype.svelte @@ -22,7 +22,7 @@ import ComboBoxEditControl from "$lib/components/ComboBoxEditControl.svelte"; import FaIconButton from "$lib/components/FaIconButton.svelte"; import ViolationMessages from "$lib/components/ViolationMessages.svelte"; - import { getControlButtonsForReactiveObject } from "$lib/models/reactive/reactive-utils.js"; + import { getControlButtonsForReactiveObject } from "$lib/models/reactive/utils/reactive-objects-control-button-utils.js"; import { editorState } from "$lib/sharedState.svelte.js"; let { classStereotypes, stereotype } = $props(); diff --git a/frontend/src/routes/mainpage/packageEditorDialog.svelte b/frontend/src/routes/mainpage/packageEditorDialog.svelte index b63473bc..cad65386 100644 --- a/frontend/src/routes/mainpage/packageEditorDialog.svelte +++ b/frontend/src/routes/mainpage/packageEditorDialog.svelte @@ -23,8 +23,8 @@ import { PUBLIC_BACKEND_URL } from "$lib/config/runtime"; import ModifyDataDialog from "$lib/dialog/ModifyDataDialog.svelte"; import { mapReactivePackageToPackageDto } from "$lib/models/reactive/mapper/map-reactive-object-to-dto.js"; - import { ReactivePackage } from "$lib/models/reactive/reactive-package.svelte.js"; - import { getControlButtonsForReactiveObject } from "$lib/models/reactive/reactive-utils.js"; + import { ReactivePackage } from "$lib/models/reactive/models/reactive-package.svelte.js"; + import { getControlButtonsForReactiveObject } from "$lib/models/reactive/utils/reactive-objects-control-button-utils.js"; import { forceReloadTrigger } from "$lib/sharedState.svelte.js"; import { getNamespaces } from "./classEditor/fetch-class-editor-context.js"; diff --git a/frontend/src/routes/mainpage/packageNavigation/ontology-editor-dialog/OntologyDialog.svelte b/frontend/src/routes/mainpage/packageNavigation/ontology-editor-dialog/OntologyDialog.svelte index 0abc4837..57b96659 100644 --- a/frontend/src/routes/mainpage/packageNavigation/ontology-editor-dialog/OntologyDialog.svelte +++ b/frontend/src/routes/mainpage/packageNavigation/ontology-editor-dialog/OntologyDialog.svelte @@ -27,7 +27,7 @@ import { PUBLIC_BACKEND_URL } from "$lib/config/runtime"; import ActionDialog from "$lib/dialog/ActionDialog.svelte"; import DiscardCancelConfirmDialog from "$lib/dialog/DiscardCancelConfirmDialog.svelte"; - import { ReactiveOntology } from "$lib/models/reactive/ontology/reactive-ontology.svelte.js"; + import { ReactiveOntology } from "$lib/models/reactive/models/ontology/reactive-ontology.svelte.js"; import { forceReloadTrigger } from "$lib/sharedState.svelte.js"; import AddKnownFieldsDialog from "./AddKnownFieldsDialog.svelte"; diff --git a/frontend/src/routes/shacl/SHACLPropertySpecificDialog.svelte b/frontend/src/routes/shacl/SHACLPropertySpecificDialog.svelte index 555980f1..6bcf8ded 100644 --- a/frontend/src/routes/shacl/SHACLPropertySpecificDialog.svelte +++ b/frontend/src/routes/shacl/SHACLPropertySpecificDialog.svelte @@ -19,8 +19,8 @@ import ButtonControl from "$lib/components/ButtonControl.svelte"; import { PUBLIC_BACKEND_URL } from "$lib/config/runtime"; import ActionDialog from "$lib/dialog/ActionDialog.svelte"; - import { ReactiveAssociation } from "$lib/models/reactive/reactive-association.svelte.js"; - import { ReactiveAttribute } from "$lib/models/reactive/reactive-attribute.svelte.js"; + import { ReactiveAssociation } from "$lib/models/reactive/models/reactive-association.svelte.js"; + import { ReactiveAttribute } from "$lib/models/reactive/models/reactive-attribute.svelte.js"; import { editorState } from "$lib/sharedState.svelte.js"; import TtlCodeEditor from "$lib/ttl/TtlCodeEditor.svelte"; From cce983660b85843271aae791a6fa159e14386be7 Mon Sep 17 00:00:00 2001 From: Maximilian Reisch Date: Mon, 13 Apr 2026 09:51:26 +0200 Subject: [PATCH 05/18] fix(navigation): faulty api calls (#64, RDFA-433) * refactored navigation to remove faulty api calls * fixed bugs where: - default packages were always shown as empty - no reload happened on enable/disable editing also added hot reload for undo/redo * fixed bug, where the package editor didn't work * fixed bug, where the ontology editor didn't reload properly * fixed bug where graph navigation section was not updated on modifying data * fixed bug where undo redo didn't trigger a reload in the class editor --- .../src/lib/models/nav/NavEntry.svelte.js | 66 ++++ .../DatasetDeleteDialog.svelte | 2 +- frontend/src/routes/NamespacesDialog.svelte | 3 - .../src/routes/layout/menu-bar/Edit.svelte | 2 +- .../src/routes/layout/menu-bar/File.svelte | 2 +- .../mainpage/classEditor/classEditor.svelte | 7 +- .../components/ClassEditorButtons.svelte | 1 + .../AssociationEditorDialog.svelte | 3 + .../attributes/AttributeEditorDialog.svelte | 2 + .../enum-entries/EnumEntryEditorDialog.svelte | 3 + .../mainpage/packageEditorDialog.svelte | 26 +- .../packageNavigation/ClassEntry.svelte | 73 ++-- .../packageNavigation/DatasetSection.svelte | 167 +++----- .../packageNavigation/GraphSection.svelte | 371 ++++-------------- .../packageNavigation/PackageButton.svelte | 166 +++----- .../packageNavigation/build-nav-object.js | 278 +++++++++++++ .../OntologyDialog.svelte | 19 +- .../packageNavigation.svelte | 57 ++- .../packageNavigationUtils.svelte.js | 11 +- 19 files changed, 660 insertions(+), 599 deletions(-) create mode 100644 frontend/src/lib/models/nav/NavEntry.svelte.js rename frontend/src/routes/{mainpage/packageNavigation => }/DatasetDeleteDialog.svelte (98%) create mode 100644 frontend/src/routes/mainpage/packageNavigation/build-nav-object.js diff --git a/frontend/src/lib/models/nav/NavEntry.svelte.js b/frontend/src/lib/models/nav/NavEntry.svelte.js new file mode 100644 index 00000000..84c9ae11 --- /dev/null +++ b/frontend/src/lib/models/nav/NavEntry.svelte.js @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2024-2026 SOPTIM AG + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +export class NavEntry { + label = $state(); + id = $state(); + tooltip = $state(); + #isOpen = $state(false); + children = $state([]); + data = $state(null); + + /** @type {NavEntry | null} */ + parent = $state(null); + + /** @returns {boolean} */ + get isOpen() { + return this.#isOpen; + } + + /** @param {{ label?: string, id?: string, tooltip?: string, isOpen?: boolean, data?: any }} config */ + constructor(config = {}) { + this.label = config.label ?? ""; + this.tooltip = config.tooltip ?? ""; + this.#isOpen = config.isOpen ?? false; + this.id = config.id ?? this.label; + this.data = config.data ?? null; + } + + /** Opens this entry and all ancestors. */ + open() { + this.#isOpen = true; + this.parent?.open(); + } + + /** Closes this entry and all descendants recursively. */ + close() { + this.#isOpen = false; + this.children.forEach(child => child.close()); + } + + /** Toggles the open state. Opening trickles up, closing trickles down. */ + toggle() { + if (this.isOpen) { + this.close(); + } else { + this.open(); + } + } + + /** @type {boolean} */ + hasChildren = $derived(this.children.length > 0); +} diff --git a/frontend/src/routes/mainpage/packageNavigation/DatasetDeleteDialog.svelte b/frontend/src/routes/DatasetDeleteDialog.svelte similarity index 98% rename from frontend/src/routes/mainpage/packageNavigation/DatasetDeleteDialog.svelte rename to frontend/src/routes/DatasetDeleteDialog.svelte index 07c46292..b1927693 100644 --- a/frontend/src/routes/mainpage/packageNavigation/DatasetDeleteDialog.svelte +++ b/frontend/src/routes/DatasetDeleteDialog.svelte @@ -19,7 +19,7 @@ import { faExclamation } from "@fortawesome/free-solid-svg-icons"; import { BackendConnection } from "$lib/api/backend.js"; - import { PUBLIC_BACKEND_URL } from "$lib/config/runtime"; + import { PUBLIC_BACKEND_URL } from "$lib/config/runtime.js"; import ActionDialog from "$lib/dialog/ActionDialog.svelte"; import { forceReloadTrigger, diff --git a/frontend/src/routes/NamespacesDialog.svelte b/frontend/src/routes/NamespacesDialog.svelte index 77dc0446..d8dea45d 100644 --- a/frontend/src/routes/NamespacesDialog.svelte +++ b/frontend/src/routes/NamespacesDialog.svelte @@ -83,9 +83,6 @@ return mapReactiveNamespaceToNamespaceDto(namespace); }); await bec.replaceNamespaces(datasetName, namespaceDTOs); - editorState.selectedDataset.trigger(); - editorState.selectedGraph.trigger(); - editorState.selectedClassUUID.trigger(); forceReloadTrigger.trigger(); } diff --git a/frontend/src/routes/layout/menu-bar/Edit.svelte b/frontend/src/routes/layout/menu-bar/Edit.svelte index d2c375e0..b5ba2696 100644 --- a/frontend/src/routes/layout/menu-bar/Edit.svelte +++ b/frontend/src/routes/layout/menu-bar/Edit.svelte @@ -124,7 +124,7 @@ } await enableEditing(selectedDataset); await reload(); - editorState.selectedPackageUUID.trigger(); + forceReloadTrigger.trigger(); } async function requestDisableEditing() { diff --git a/frontend/src/routes/layout/menu-bar/File.svelte b/frontend/src/routes/layout/menu-bar/File.svelte index 5f081f37..34ed72bc 100644 --- a/frontend/src/routes/layout/menu-bar/File.svelte +++ b/frontend/src/routes/layout/menu-bar/File.svelte @@ -28,10 +28,10 @@ import { Menubar } from "$lib/components/bitsui/menubar"; import { editorState } from "$lib/sharedState.svelte.js"; + import DatasetDeleteDialog from "../../DatasetDeleteDialog.svelte"; import ExportDialog from "../../ExportDialog.svelte"; import GraphDeleteDialog from "../../GraphDeleteDialog.svelte"; import ImportDialog from "../../ImportDialog.svelte"; - import DatasetDeleteDialog from "../../mainpage/packageNavigation/DatasetDeleteDialog.svelte"; import SHACLExportDialog from "../../shacl/SHACLExportDialog.svelte"; import SHACLUploadDialog from "../../shacl/SHACLUploadDialog.svelte"; import SnapshotDialog from "../../SnapshotDialog.svelte"; diff --git a/frontend/src/routes/mainpage/classEditor/classEditor.svelte b/frontend/src/routes/mainpage/classEditor/classEditor.svelte index eb25b0e4..b66999c3 100644 --- a/frontend/src/routes/mainpage/classEditor/classEditor.svelte +++ b/frontend/src/routes/mainpage/classEditor/classEditor.svelte @@ -26,7 +26,10 @@ import { eventStack } from "$lib/eventhandling/closeEventManager.svelte.js"; import { mapClassDtoToReactiveClass } from "$lib/models/reactive/mapper/map-dto-to-reactive-object.js"; import { adoptUnsavedClassChanges } from "$lib/models/reactive/utils/adopt-model-changes-utils.js"; - import { editorState } from "$lib/sharedState.svelte.js"; + import { + editorState, + forceReloadTrigger, + } from "$lib/sharedState.svelte.js"; import { getClasses, @@ -88,6 +91,7 @@ $effect(async () => { editorState.selectedClassUUID.subscribe(); + forceReloadTrigger.subscribe(); loadingContext = true; loadingClass = true; @@ -98,6 +102,7 @@ $effect(async () => { editorState.selectedPackageUUID.subscribe(); + forceReloadTrigger.subscribe(); isDatasetReadOnly = await isReadOnly(datasetName); }); diff --git a/frontend/src/routes/mainpage/classEditor/components/ClassEditorButtons.svelte b/frontend/src/routes/mainpage/classEditor/components/ClassEditorButtons.svelte index cf6a6f02..0fcc7627 100644 --- a/frontend/src/routes/mainpage/classEditor/components/ClassEditorButtons.svelte +++ b/frontend/src/routes/mainpage/classEditor/components/ClassEditorButtons.svelte @@ -102,6 +102,7 @@ responseText, ); } + forceReloadTrigger.trigger(); } diff --git a/frontend/src/routes/mainpage/classEditor/components/associations/associationEditorDialog/AssociationEditorDialog.svelte b/frontend/src/routes/mainpage/classEditor/components/associations/associationEditorDialog/AssociationEditorDialog.svelte index ec7114f3..cd707e76 100644 --- a/frontend/src/routes/mainpage/classEditor/components/associations/associationEditorDialog/AssociationEditorDialog.svelte +++ b/frontend/src/routes/mainpage/classEditor/components/associations/associationEditorDialog/AssociationEditorDialog.svelte @@ -23,6 +23,7 @@ import ModifyDataDialog from "$lib/dialog/ModifyDataDialog.svelte"; import { mapReactiveAssociationToAssociationDto } from "$lib/models/reactive/mapper/map-reactive-object-to-dto.js"; import { ReactiveAssociation } from "$lib/models/reactive/models/reactive-association.svelte.js"; + import { forceReloadTrigger } from "$lib/sharedState.svelte.js"; import Direct from "./Direct.svelte"; import { saveApiAssociationToBackend } from "../save-association-to-backend.js"; @@ -104,6 +105,8 @@ if (isNewAssociation) { isNewAssociation = false; } + association.save(); + forceReloadTrigger.trigger(); } function onClose() { diff --git a/frontend/src/routes/mainpage/classEditor/components/attributes/AttributeEditorDialog.svelte b/frontend/src/routes/mainpage/classEditor/components/attributes/AttributeEditorDialog.svelte index 11146de0..90148298 100644 --- a/frontend/src/routes/mainpage/classEditor/components/attributes/AttributeEditorDialog.svelte +++ b/frontend/src/routes/mainpage/classEditor/components/attributes/AttributeEditorDialog.svelte @@ -26,6 +26,7 @@ import { mapReactiveAttributeToAttributeDto } from "$lib/models/reactive/mapper/map-reactive-object-to-dto.js"; import { ReactiveAttribute } from "$lib/models/reactive/models/reactive-attribute.svelte.js"; import { getControlButtonsForReactiveObject } from "$lib/models/reactive/utils/reactive-objects-control-button-utils.js"; + import { forceReloadTrigger } from "$lib/sharedState.svelte.js"; import { getNsPrefixNsUriString } from "$lib/utils/namespace.js"; import { saveApiAttributeToBackend } from "./save-attribute-to-backend.js"; @@ -87,6 +88,7 @@ attributes.append(attribute); isNewAttribute = false; } + forceReloadTrigger.trigger(); } diff --git a/frontend/src/routes/mainpage/classEditor/components/enum-entries/EnumEntryEditorDialog.svelte b/frontend/src/routes/mainpage/classEditor/components/enum-entries/EnumEntryEditorDialog.svelte index 3dc5c3ed..74abc9d9 100644 --- a/frontend/src/routes/mainpage/classEditor/components/enum-entries/EnumEntryEditorDialog.svelte +++ b/frontend/src/routes/mainpage/classEditor/components/enum-entries/EnumEntryEditorDialog.svelte @@ -25,6 +25,7 @@ import { mapReactiveEnumEntryToEnumEntryDto } from "$lib/models/reactive/mapper/map-reactive-object-to-dto.js"; import { ReactiveEnumEntry } from "$lib/models/reactive/models/reactive-enum-entry.svelte.js"; import { getControlButtonsForReactiveObject } from "$lib/models/reactive/utils/reactive-objects-control-button-utils.js"; + import { forceReloadTrigger } from "$lib/sharedState.svelte.js"; import { getNsPrefixNsUriString } from "$lib/utils/namespace.js"; import { saveApiEnumEntryToBackend } from "./save-enum-entry-to-backend.js"; @@ -76,6 +77,8 @@ enumEntries.append(enumEntry); isNewEnumEntry = false; } + enumEntry.save(); + forceReloadTrigger.trigger(); } diff --git a/frontend/src/routes/mainpage/packageEditorDialog.svelte b/frontend/src/routes/mainpage/packageEditorDialog.svelte index cad65386..66ff2ba8 100644 --- a/frontend/src/routes/mainpage/packageEditorDialog.svelte +++ b/frontend/src/routes/mainpage/packageEditorDialog.svelte @@ -27,11 +27,13 @@ import { getControlButtonsForReactiveObject } from "$lib/models/reactive/utils/reactive-objects-control-button-utils.js"; import { forceReloadTrigger } from "$lib/sharedState.svelte.js"; - import { getNamespaces } from "./classEditor/fetch-class-editor-context.js"; + import { + getNamespaces, + getPackages, + } from "./classEditor/fetch-class-editor-context.js"; let { showDialog = $bindable(), - packages = [], pack, readonly = false, datasetName = null, @@ -43,6 +45,7 @@ let pkg = $state(null); let isNewPackage = $state(true); let namespaces = $state([]); + let packages = $state([]); function getSubstitutedNamespace(namespace) { const namespaceObj = namespaces.find(n => n.prefix === namespace); @@ -50,6 +53,7 @@ } async function onOpen() { + await fetchPackages(); if (pack) { isNewPackage = false; pkg = new ReactivePackage({ @@ -76,8 +80,26 @@ namespaces = await getNamespaces(datasetName); } } + async function fetchPackages() { + if (!datasetName || !graphUri) { + packages = []; + return; + } + try { + packages = await getPackages(datasetName, graphUri); + } catch (err) { + console.error("Failed to load packages:", err); + packages = []; + } + } async function savePackage() { + console.log( + "Saving package in dataset", + datasetName, + "and graph", + graphUri, + ); if (!datasetName || !graphUri) { return; } diff --git a/frontend/src/routes/mainpage/packageNavigation/ClassEntry.svelte b/frontend/src/routes/mainpage/packageNavigation/ClassEntry.svelte index 2804b64b..9298c560 100644 --- a/frontend/src/routes/mainpage/packageNavigation/ClassEntry.svelte +++ b/frontend/src/routes/mainpage/packageNavigation/ClassEntry.svelte @@ -29,54 +29,42 @@ import { editorState } from "$lib/sharedState.svelte.js"; import { shortenIri } from "$lib/utils/iri.js"; - import { - getUri, - isSelectedClass, - } from "./packageNavigationUtils.svelte.js"; + import { isSelectedClass } from "./packageNavigationUtils.svelte.js"; import DeleteClassConfirmDialog from "../../DeleteClassConfirmDialog.svelte"; import SHACLClassSpecificPopUp from "../../shacl/shaclclassspecific/SHACLClassSpecificPopUp.svelte"; let { - dataset, - graph, - pack, - cls, - prefixes = [], - readOnly = false, + datasetNavEntry, + graphNavEntry, + classNavEntry, + namespaces = [], + readonly = false, onPackChange = () => {}, } = $props(); let showDeleteDialog = $state(false); let showSHACLDialog = $state(false); - const highlightLabel = $derived( - shortenIri( - prefixes, - cls?.prefix ? `${cls.prefix}${cls.label}` : (cls?.label ?? ""), - ), - ); + const highlightLabel = $derived(shortenIri(namespaces, classNavEntry.id)); const shaclClass = $derived({ - uuid: { value: cls?.uuid }, - label: { value: cls?.label ?? "" }, + uuid: { value: classNavEntry?.id }, + label: { value: classNavEntry?.label ?? "" }, }); function selectClass() { - onPackChange({ - ...pack, - showContents: true, - userCollapsed: false, - }); + onPackChange(); if (!editorState.selectedClassUUID.getValue()) { - eventStack.executeNewestEvent(cls.uuid); - editorState.selectedClassDataset.updateValue(dataset.label); - editorState.selectedClassGraph.updateValue(getUri(graph)); - editorState.selectedClassUUID.updateValue(cls.uuid); + eventStack.executeNewestEvent(classNavEntry.id); + editorState.selectedClassDataset.updateValue(datasetNavEntry.id); + editorState.selectedClassGraph.updateValue(graphNavEntry.id); + editorState.selectedClassUUID.updateValue(classNavEntry.id); return; } + //The event executed to open the discard confirm delete dialog eventStack.executeNewestEvent({ - datasetName: dataset.label, - graphUri: getUri(graph), - classUuid: cls.uuid, + datasetName: datasetNavEntry.id, + graphUri: graphNavEntry.id, + classUuid: classNavEntry.id, }); } @@ -85,10 +73,14 @@ @@ -114,7 +106,7 @@ selectClass(); showDeleteDialog = true; }} - disabled={readOnly} + disabled={readonly} faIcon={faTrash} variant="danger" > @@ -125,15 +117,14 @@ diff --git a/frontend/src/routes/mainpage/packageNavigation/DatasetSection.svelte b/frontend/src/routes/mainpage/packageNavigation/DatasetSection.svelte index fc1b0d4b..80deb8ac 100644 --- a/frontend/src/routes/mainpage/packageNavigation/DatasetSection.svelte +++ b/frontend/src/routes/mainpage/packageNavigation/DatasetSection.svelte @@ -26,144 +26,95 @@ faLock, faDiagramProject, } from "@fortawesome/free-solid-svg-icons"; - import { onMount } from "svelte"; + import { getContext } from "svelte"; import { getNamespaces, isReadOnly } from "$lib/api/apiDatasetUtils.js"; import { BackendConnection } from "$lib/api/backend.js"; import { ContextMenu } from "$lib/components/bitsui/contextmenu"; import NavigationEntry from "$lib/components/navigation/NavigationEntry.svelte"; import { PUBLIC_BACKEND_URL } from "$lib/config/runtime"; - import { URI } from "$lib/models/dto"; - import { - forceReloadTrigger, - editorState, - } from "$lib/sharedState.svelte.js"; + import { forceReloadTrigger } from "$lib/sharedState.svelte.js"; + import { editorState } from "$lib/sharedState.svelte.js"; - import DatasetDeleteDialog from "./DatasetDeleteDialog.svelte"; import GraphSection from "./GraphSection.svelte"; - import { - isSelectedDataset, - isSelectedGraph, - getUri, - } from "./packageNavigationUtils.svelte.js"; + import { isSelectedDataset } from "./packageNavigationUtils.svelte.js"; + import DatasetDeleteDialog from "../../DatasetDeleteDialog.svelte"; import ImportDialog from "../../ImportDialog.svelte"; import NamespacesDialog from "../../NamespacesDialog.svelte"; import NewGraphDialog from "../../NewGraphDialog.svelte"; import SnapshotDialog from "../../SnapshotDialog.svelte"; - let { dataset } = $props(); + let { datasetNavEntry } = $props(); const bec = new BackendConnection(fetch, PUBLIC_BACKEND_URL); - let graphs = $state([]); let showImportDialog = $state(false); let showNewGraphDialog = $state(false); let showSnapshotDialog = $state(false); let showDatasetDeleteDialog = $state(false); let showNamespacesDialog = $state(false); let readonly = $state(false); - let prefixes = $state([]); + let namespaces = $state([]); + + let wasDatasetSelected = false; + + const isDatasetSelected = $derived( + isSelectedDataset(datasetNavEntry.label), + ); $effect(async () => { - forceReloadTrigger.subscribe(); - readonly = dataset ? await isReadOnly(dataset.label) : false; - await fetchGraphs(); - await updateReadonly(); + getContext("packageNavigation").reloadTrigger?.subscribe(); + readonly = await isReadOnly(datasetNavEntry.label); await fetchNamespaces(); }); - - onMount(() => { - updateReadonly(); - fetchNamespaces(); - fetchGraphs(); - }); - - async function getGraphNames(datasetName) { - const res = await bec.getGraphNames(datasetName); - return await res.json(); - } - - async function fetchGraphs() { - let graphUris = await getGraphNames(dataset.label); - let newGraphs = []; - const previous = graphs ?? []; - - for (let graphUri of graphUris) { - const uriString = getUri(graphUri); - const prev = previous.find(g => getUri(g.uri) === uriString); - const keepExpanded = prev?.showContents ?? false; - - newGraphs.push({ - uri: new URI(graphUri), - showContents: - keepExpanded || isSelectedGraph(dataset.label, graphUri), - }); + $effect(() => { + if (isDatasetSelected && !wasDatasetSelected) { + datasetNavEntry.parent?.open(); } - newGraphs = newGraphs.sort((a, b) => - a.uri.suffix.localeCompare(b.uri.suffix), - ); - graphs = newGraphs; - } + wasDatasetSelected = isDatasetSelected; + }); async function fetchNamespaces() { - if (!dataset?.label) { - prefixes = []; + if (!datasetNavEntry?.label) { + namespaces = []; return; } try { - prefixes = await getNamespaces(dataset.label); + namespaces = await getNamespaces(datasetNavEntry.label); } catch (err) { - console.error("Failed to load prefixes:", err); - prefixes = []; + console.error("Failed to load namespaces:", err); + namespaces = []; } } function selectDataset() { - const nextDataset = dataset.label; - const previousDataset = editorState.selectedDataset.getValue(); - const datasetChanged = previousDataset !== nextDataset; - - editorState.selectedDataset.updateValue(nextDataset); - if (datasetChanged) { - editorState.selectedGraph.updateValue(null); - editorState.selectedPackageUUID.updateValue(null); - } - } - - function toggleDatasetContentsVisibility(dataset) { - dataset.showContents = !dataset.showContents; - } - - function ensureDatasetExpanded() { - if (!dataset?.showContents) { - dataset.showContents = true; + if (editorState.selectedDataset.getValue() === datasetNavEntry.label) { + return; } - } - - async function updateReadonly() { - readonly = await isReadOnly(dataset.label); + editorState.selectedGraph.updateValue(null); + editorState.selectedPackageUUID.updateValue(null); + editorState.selectedDataset.updateValue(datasetNavEntry.label); } async function enableEditing() { - if (!dataset?.label || !readonly) { + if (!datasetNavEntry?.id || !readonly) { return; } - await bec.enableEditing(dataset.label); - await updateReadonly(); - forceReloadTrigger.trigger(); - editorState.selectedPackageUUID.trigger(); - editorState.selectedClassUUID.trigger(); + + await bec.enableEditing(datasetNavEntry.id).then(() => { + readonly = false; + forceReloadTrigger.trigger(); + }); } async function disableEditing() { - if (!dataset?.label || readonly) { + if (!datasetNavEntry?.id || readonly) { return; } - await bec.disableEditing(dataset.label); - await updateReadonly(); - forceReloadTrigger.trigger(); - editorState.selectedPackageUUID.trigger(); - editorState.selectedClassUUID.trigger(); + await bec.disableEditing(datasetNavEntry.id).then(() => { + readonly = true; + forceReloadTrigger.trigger(); + }); } @@ -172,18 +123,16 @@ 0} - expanded={dataset.showContents} - isSelected={isSelectedDataset(dataset)} - title={dataset.label} + hasChildren={datasetNavEntry.children?.length > 0} + expanded={datasetNavEntry.isOpen} + isSelected={isDatasetSelected} + title={datasetNavEntry.tooltip} badgeText={readonly ? "Read-only" : ""} badgeVariant="readonly" - onclick={() => { - selectDataset(); - }} - onToggle={() => toggleDatasetContentsVisibility(dataset)} + onclick={selectDataset} + onToggle={() => datasetNavEntry.toggle()} /> @@ -258,16 +207,16 @@ - {#if dataset.showContents} + {#if datasetNavEntry.isOpen}
- {#each graphs as graph} + {#each datasetNavEntry.children as graphNavEntry} {/each}
@@ -276,18 +225,18 @@ diff --git a/frontend/src/routes/mainpage/packageNavigation/GraphSection.svelte b/frontend/src/routes/mainpage/packageNavigation/GraphSection.svelte index 2389f25a..985027d8 100644 --- a/frontend/src/routes/mainpage/packageNavigation/GraphSection.svelte +++ b/frontend/src/routes/mainpage/packageNavigation/GraphSection.svelte @@ -34,7 +34,7 @@ faRotateRight, faGear, } from "@fortawesome/free-solid-svg-icons"; - import { onMount } from "svelte"; + import { getContext } from "svelte"; import { undo, @@ -42,7 +42,6 @@ redo, fetchCanRedo, } from "$lib/actions/versionControlActions.js"; - import { isReadOnly } from "$lib/api/apiDatasetUtils.js"; import { BackendConnection } from "$lib/api/backend.js"; import { ContextMenu } from "$lib/components/bitsui/contextmenu"; import NavigationEntry from "$lib/components/navigation/NavigationEntry.svelte"; @@ -54,11 +53,7 @@ import { shortenIri } from "$lib/utils/iri.js"; import PackageButton from "./PackageButton.svelte"; - import { - getUri, - isSelectedGraph, - getPackageId, - } from "./packageNavigationUtils.svelte.js"; + import { isSelectedGraph } from "./packageNavigationUtils.svelte.js"; import CompareDialog from "../../compare/CompareDialog.svelte"; import ExportDialog from "../../ExportDialog.svelte"; import GraphDeleteDialog from "../../GraphDeleteDialog.svelte"; @@ -71,22 +66,15 @@ import { goto } from "$app/navigation"; let { - dataset, - graph, - onExpandDataset = () => {}, - prefixes = [], + datasetNavEntry, + graphNavEntry, + namespaces = [], + readonly = false, } = $props(); const bec = new BackendConnection(fetch, PUBLIC_BACKEND_URL); - let packages = $state([]); let ontology = $state(); - let packagesLoading = $state(false); - let packagesRequestId = 0; - let classesByPackage = $state({}); - let classPackageByUuid = $state({}); - let selectedClassPackageId = $state(null); - let classesRequestId = 0; let showExportDialog = $state(false); let showDeleteDialog = $state(false); let showNewPackageDialog = $state(false); @@ -94,125 +82,41 @@ let showSHACLUploadDialog = $state(false); let showSHACLExportDialog = $state(false); let showSHACLFullViewDialog = $state(false); - let readOnly = $state(false); let canUndo = $state(false); let canRedo = $state(false); let showEditOntologyDialog = $state(false); - let graphHighlightLabel = $derived(shortenIri(prefixes, getUri(graph))); + let wasGraphSelected = false; - $effect(async () => { - forceReloadTrigger.subscribe(); - await loadGraphData(); - }); + let graphHighlightLabel = $derived( + shortenIri(namespaces, graphNavEntry.id), + ); + const isGraphSelected = $derived( + isSelectedGraph(datasetNavEntry.id, graphNavEntry.id), + ); $effect(() => { - editorState.selectedClassUUID.subscribe(); - editorState.selectedClassDataset.subscribe(); - editorState.selectedClassGraph.subscribe(); - updateSelectedClassPackageId(); - ensureExpandedPackages(); - }); - - $effect(() => { - editorState.selectedPackageUUID.subscribe(); - ensureExpandedPackages(); + if (isGraphSelected && !wasGraphSelected) { + graphNavEntry.parent?.open(); + } + wasGraphSelected = isGraphSelected; }); - onMount(() => { - loadGraphData(); + $effect(async () => { + getContext("packageNavigation").reloadTrigger?.subscribe(); + await initialize(); }); - async function getPackages(datasetName, graphURI) { - const res = await bec.getPackages(datasetName, graphURI); - return await res.json(); - } - - async function getClasses(datasetName, graphURI) { - const res = await bec.getClasses(datasetName, graphURI); - return await res.json(); - } - - async function loadGraphData() { - await fetchClasses(); + async function initialize() { ontology = await getOntology(); - await createPackageList(); - await updateReadOnly(); - await updateUndoRedo(); - ensureExpandedPackages(); - } - - async function createPackageList() { - const requestId = ++packagesRequestId; - packagesLoading = true; - - try { - const packageStructure = await getPackages( - dataset.label, - getUri(graph), - ); - if (requestId !== packagesRequestId) { - return; - } - - let localPackagesList = []; - const previous = packages ?? []; - const selectedPackageId = - editorState.selectedPackageUUID.getValue(); - const selectedClassPackageIdSnapshot = selectedClassPackageId; - - packageStructure.internalPackageList.forEach(pack => { - localPackagesList.push({ - uuid: pack.uuid, - prefix: pack.prefix, - label: pack.label, - comment: pack.comment, - external: false, - }); - }); - packageStructure.externalPackageList.forEach(pack => { - localPackagesList.push({ - uuid: pack.uuid, - prefix: pack.prefix, - label: pack.label, - comment: pack.comment, - external: true, - }); - }); - localPackagesList = localPackagesList.map(pack => { - const packageId = getPackageId(pack); - const prev = previous.find(p => getPackageId(p) === packageId); - const keepExpanded = prev?.showContents ?? false; - const userCollapsed = prev?.userCollapsed ?? !keepExpanded; - const isSelected = - isSelectedGraph(dataset, graph) && - selectedPackageId === packageId; - const hasSelectedClass = - selectedClassPackageIdSnapshot === packageId; - return { - ...pack, - userCollapsed, - showContents: userCollapsed - ? false - : keepExpanded || isSelected || hasSelectedClass, - }; - }); - packages = localPackagesList.sort((a, b) => { - if (!a || !a.label || a.label === "default") return 1; - if (!b || !b.label || b.label === "default") return -1; - return a.label.localeCompare(b.label); - }); - } catch (err) { - console.error("Failed to load packages:", err); - } finally { - if (requestId === packagesRequestId) { - packagesLoading = false; - } - } + canUndo = await fetchCanUndo(datasetNavEntry.id, graphNavEntry.id); + canRedo = await fetchCanRedo(datasetNavEntry.id, graphNavEntry.id); } - async function getOntology() { - const res = await bec.getOntology(dataset.label, getUri(graph)); + const res = await bec.getOntology( + datasetNavEntry.label, + graphNavEntry.id, + ); let content = await res.text(); if (!content) { return null; @@ -220,128 +124,9 @@ return JSON.parse(content); } - async function fetchClasses() { - const requestId = ++classesRequestId; - - try { - const classList = - (await getClasses(dataset.label, getUri(graph))) ?? []; - if (requestId !== classesRequestId) { - return; - } - - const grouped = {}; - const uuidMap = {}; - - for (const cls of classList) { - const packageId = getPackageId(cls.package); - uuidMap[cls.uuid] = packageId; - if (!grouped[packageId]) { - grouped[packageId] = []; - } - grouped[packageId].push({ - ...cls, - packageUUID: packageId, - }); - } - - for (const key of Object.keys(grouped)) { - grouped[key].sort((a, b) => - (a.label ?? "").localeCompare(b.label ?? "", undefined, { - sensitivity: "base", - }), - ); - } - - classesByPackage = grouped; - classPackageByUuid = uuidMap; - updateSelectedClassPackageId(); - } catch (err) { - console.error("Failed to load classes:", err); - classesByPackage = {}; - classPackageByUuid = {}; - selectedClassPackageId = null; - } - } - - function updateSelectedClassPackageId() { - const selectedClassUUID = editorState.selectedClassUUID.getValue(); - const selectedClassDataset = - editorState.selectedClassDataset.getValue(); - const selectedClassGraph = editorState.selectedClassGraph.getValue(); - - if ( - !selectedClassUUID || - selectedClassDataset !== dataset.label || - selectedClassGraph !== getUri(graph) - ) { - selectedClassPackageId = null; - return; - } - - selectedClassPackageId = classPackageByUuid[selectedClassUUID] ?? null; - } - - function ensureExpandedPackages() { - const selectedPackageId = editorState.selectedPackageUUID.getValue(); - const selectedClassPackageIdSnapshot = selectedClassPackageId; - - let updated = false; - const nextPackages = packages.map(pack => { - const packageId = getPackageId(pack); - const shouldBeExpanded = pack.userCollapsed - ? false - : pack.showContents || - packageId === selectedPackageId || - packageId === selectedClassPackageIdSnapshot; - if (shouldBeExpanded !== pack.showContents) { - updated = true; - return { ...pack, showContents: shouldBeExpanded }; - } - return pack; - }); - - if (updated) { - packages = nextPackages; - } - } - - function toggleGraphContentsVisibility(graph) { - graph.showContents = !graph.showContents; - } - - async function updateReadOnly() { - readOnly = await isReadOnly(dataset.label); - } - - function updatePackage(updatedPack) { - ensureGraphIsExpanded(); - if (!updatedPack) { - return; - } - const updatedId = getPackageId(updatedPack); - packages = packages.map(existingPack => - getPackageId(existingPack) === updatedId - ? updatedPack - : existingPack, - ); - } - - function ensureGraphIsExpanded() { - if (!graph?.showContents) { - graph.showContents = true; - } - onExpandDataset(); - } - - async function updateUndoRedo() { - canUndo = await fetchCanUndo(dataset.label, getUri(graph)); - canRedo = await fetchCanRedo(dataset.label, getUri(graph)); - } - function focusGraphContext() { - const nextDataset = dataset.label; - const nextGraph = getUri(graph); + const nextDataset = datasetNavEntry.label; + const nextGraph = graphNavEntry.id; const previousDataset = editorState.selectedDataset.getValue(); const previousGraph = editorState.selectedGraph.getValue(); const graphChanged = @@ -353,33 +138,22 @@ editorState.selectedPackageUUID.updateValue(null); } } - - function triggerReload() { - editorState.selectedDataset.trigger(); - editorState.selectedGraph.trigger(); - editorState.selectedClassUUID.trigger(); - forceReloadTrigger.trigger(); - } -
+
0} - expanded={graph.showContents} - isSelected={isSelectedGraph(dataset, graph)} - title={graph.uri.suffix} + hasChildren={graphNavEntry.children.length > 0} + expanded={graphNavEntry.isOpen} + isSelected={isGraphSelected} + title={graphNavEntry.tooltip} highlightLabel={graphHighlightLabel} - onclick={() => { - focusGraphContext(); - }} - onToggle={() => toggleGraphContentsVisibility(graph)} + onclick={focusGraphContext} + onToggle={() => graphNavEntry.toggle()} /> @@ -388,7 +162,7 @@ focusGraphContext(); showNewPackageDialog = true; }} - disabled={readOnly} + disabled={readonly} faIcon={faPlus} > New Package @@ -397,11 +171,11 @@ { focusGraphContext(); - undo(dataset.label, getUri(graph)).then(success => { - if (success) triggerReload(); + undo(datasetNavEntry.id, graphNavEntry.id).then(success => { + if (success) forceReloadTrigger.trigger(); }); }} - disabled={readOnly || !canUndo} + disabled={readonly || !canUndo} faIcon={faRotateLeft} > Undo @@ -409,16 +183,16 @@ { focusGraphContext(); - redo(dataset.label, getUri(graph)).then(success => { - if (success) triggerReload(); + redo(datasetNavEntry.id, graphNavEntry.id).then(success => { + if (success) forceReloadTrigger.trigger(); }); }} - disabled={readOnly || !canRedo} + disabled={readonly || !canRedo} faIcon={faRotateRight} > Redo - {#if !readOnly} + {#if !readonly} {#if ontology} { - bec.deleteOntology(dataset.label, getUri(graph)); - forceReloadTrigger.trigger(); + bec.deleteOntology( + datasetNavEntry.id, + graphNavEntry.id, + ); + initialize(); }} variant="danger" faIcon={faTrash} @@ -498,7 +275,7 @@ focusGraphContext(); showSHACLUploadDialog = true; }} - disabled={readOnly} + disabled={readonly} faIcon={faUpload} > Import @@ -538,7 +315,7 @@ focusGraphContext(); showDeleteDialog = true; }} - disabled={readOnly} + disabled={readonly} faIcon={faTrash} variant="danger" > @@ -546,19 +323,17 @@ - {#if graph.showContents} + {#if graphNavEntry.isOpen}
- {#each packages as pack (getPackageId(pack))} + {#each graphNavEntry.children as packageNavEntry (packageNavEntry.id)} {/each}
@@ -567,35 +342,37 @@ diff --git a/frontend/src/routes/mainpage/packageNavigation/PackageButton.svelte b/frontend/src/routes/mainpage/packageNavigation/PackageButton.svelte index 39e75ba1..e2a7a9b6 100644 --- a/frontend/src/routes/mainpage/packageNavigation/PackageButton.svelte +++ b/frontend/src/routes/mainpage/packageNavigation/PackageButton.svelte @@ -26,85 +26,74 @@ faTrash, faEye, } from "@fortawesome/free-solid-svg-icons"; - import { onMount } from "svelte"; + import { getContext } from "svelte"; - import { isReadOnly } from "$lib/api/apiDatasetUtils.js"; import { ContextMenu } from "$lib/components/bitsui/contextmenu"; import NavigationEntry from "$lib/components/navigation/NavigationEntry.svelte"; - import { - editorState, - forceReloadTrigger, - } from "$lib/sharedState.svelte.js"; + import { editorState } from "$lib/sharedState.svelte.js"; import { shortenIri } from "$lib/utils/iri.js"; import ClassEntry from "./ClassEntry.svelte"; import PackageDeleteDialog from "./PackageDeleteDialog.svelte"; - import { - isSelectedPackage, - getUri, - getPackageId, - } from "./packageNavigationUtils.svelte.js"; + import { isSelectedPackage } from "./packageNavigationUtils.svelte.js"; import NewClassDialog from "../../NewClassDialog.svelte"; import PackageEditorDialog from "../packageEditorDialog.svelte"; let { - dataset, - graph, - pack, - packages = [], - classes = [], - prefixes = [], - onPackChange = () => {}, + datasetNavEntry, + graphNavEntry, + packageNavEntry, + namespaces = [], + readonly, } = $props(); - const disablePackageDelete = pack?.label === "default" || pack?.external; - let readOnly = $state(false); let showNewClassDialog = $state(false); let showPackageEditorDialog = $state(false); let showDeletePackageDialog = $state(false); + let wasPackageSelected = false; + + let isProtectedPackage = $derived( + packageNavEntry?.id === "default" || packageNavEntry?.data.external, + ); // Ensure selection-dependent UI updates without remounting the component. const selectionTrigger = $derived([ editorState.selectedDataset.subscribe(), editorState.selectedGraph.subscribe(), editorState.selectedPackageUUID.subscribe(), - forceReloadTrigger.subscribe(), + getContext("packageNavigation").reloadTrigger?.subscribe(), ]); - let disablePackageEditing = $derived( - pack?.label === "default" || pack?.external, - ); let isPackageSelected = $derived( - selectionTrigger && isSelectedPackage(dataset, graph, pack), + selectionTrigger && + isSelectedPackage( + datasetNavEntry.id, + graphNavEntry.id, + packageNavEntry.id, + ), ); - let packageIcon = $derived(pack.showContents ? faFolderOpen : faFolder); + let packageHighlightLabel = $derived( - shortenIri( - prefixes, - pack?.prefix ? `${pack.prefix}${pack.label}` : pack?.label, - ), + shortenIri(namespaces, packageNavEntry.tooltip), ); - const packageActionLabel = $derived(readOnly ? "View" : "Edit"); - const packageActionIcon = $derived(readOnly ? faEye : faPencil); + const packageActionLabel = $derived(readonly ? "View" : "Edit"); + const packageActionIcon = $derived(readonly ? faEye : faPencil); const disablePackageAction = $derived( - readOnly ? false : disablePackageEditing, + readonly ? false : isProtectedPackage, ); - const hasClasses = $derived(classes?.length > 0); - - $effect(async () => { - forceReloadTrigger.subscribe(); - await updateReadOnly(); - }); - - onMount(async () => { - await updateReadOnly(); + const hasClasses = $derived(packageNavEntry?.children?.length > 0); + $effect(() => { + if (selectionTrigger && isPackageSelected && !wasPackageSelected) { + packageNavEntry.parent?.open(); + } + wasPackageSelected = isPackageSelected; }); function copyDatasetUrl() { const params = new URLSearchParams({ - dataset: dataset.label, - graph: getUri(graph), - package: getPackageId(pack), + dataset: datasetNavEntry.id, + graph: graphNavEntry.id, + package: packageNavEntry.id, }); const url = `${window.location.origin}/mainpage?${params}`; navigator.clipboard @@ -114,34 +103,10 @@ ); } - async function updateReadOnly() { - readOnly = await isReadOnly(dataset.label); - } - - function updatePackState(updates) { - if (!pack) return; - onPackChange({ - ...pack, - ...updates, - }); - } - - function togglePackageContentsVisibility() { - const next = !pack.showContents; - updatePackState({ - showContents: next, - userCollapsed: !next, - }); - } - function selectPackage() { - const nextDataset = dataset.label; - const nextGraph = getUri(graph); - const nextPackage = getPackageId(pack); - - editorState.selectedDataset.updateValue(nextDataset); - editorState.selectedGraph.updateValue(nextGraph); - editorState.selectedPackageUUID.updateValue(nextPackage); + editorState.selectedDataset.updateValue(datasetNavEntry.id); + editorState.selectedGraph.updateValue(graphNavEntry.id); + editorState.selectedPackageUUID.updateValue(packageNavEntry.id); } @@ -150,17 +115,19 @@ packageNavEntry.toggle()} /> @@ -168,7 +135,7 @@ onSelect={() => { showNewClassDialog = true; }} - disabled={readOnly} + disabled={readonly} faIcon={faPlus} > New Class @@ -191,7 +158,7 @@ onSelect={() => { showDeletePackageDialog = true; }} - disabled={readOnly || disablePackageDelete} + disabled={readonly || isProtectedPackage} faIcon={faTrash} variant="danger" > @@ -199,19 +166,17 @@ - {#if pack.showContents && hasClasses} + {#if packageNavEntry.isOpen && hasClasses}
- {#each classes as cls (cls.uuid)} + {#each packageNavEntry.children as classNavEntry (classNavEntry.id)} {/each}
@@ -220,23 +185,22 @@ diff --git a/frontend/src/routes/mainpage/packageNavigation/build-nav-object.js b/frontend/src/routes/mainpage/packageNavigation/build-nav-object.js new file mode 100644 index 00000000..51bde250 --- /dev/null +++ b/frontend/src/routes/mainpage/packageNavigation/build-nav-object.js @@ -0,0 +1,278 @@ +/* + * Copyright (c) 2024-2026 SOPTIM AG + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import { BackendConnection } from "$lib/api/backend.js"; +import { PUBLIC_BACKEND_URL } from "$lib/config/runtime.js"; +import { URI } from "$lib/models/dto/index.ts"; +import { NavEntry } from "$lib/models/nav/NavEntry.svelte.js"; + +import { + getUri, + isSelectedGraph, + isSelectedPackage, + isSelectedClass, +} from "./packageNavigationUtils.svelte.js"; + +const bec = new BackendConnection(fetch, PUBLIC_BACKEND_URL); + +/** + * @description Reuses an existing NavEntry by id or creates a new one. Preserves isOpen. + * @param {NavEntry[]} existingList + * @param {object} props + * @returns {NavEntry} + */ +function reuseOrCreate(existingList, props) { + const existing = existingList?.find(e => e.id === props.id); + if (existing) { + existing.label = props.label; + if (props.tooltip !== undefined) existing.tooltip = props.tooltip; + if (props.data !== undefined) existing.data = props.data; + return existing; + } + return new NavEntry(props); +} + +/** + * @description Replaces the contents of targetArray in place, keeping the array reference. Sets parent on each entry. + * @param {NavEntry[]} targetArray + * @param {NavEntry[]} freshEntries + * @param {NavEntry|null} parent + */ +function syncList(targetArray, freshEntries, parent = null) { + targetArray.length = 0; + targetArray.push(...freshEntries); + freshEntries.forEach(entry => (entry.parent = parent)); +} + +/** + * @description Loads all datasets and populates their children. Reuses existing NavEntries to preserve UI state. + * @param {NavEntry[]} existingDatasetNavList + * @returns {Promise} + */ +export async function getNavEntryList(existingDatasetNavList) { + console.log( + "started Building navObj with existingNavObj: ", + existingDatasetNavList, + ); + + const freshEntries = (await getDatasetNames()) + .sort((a, b) => a.localeCompare(b)) + .map(label => + reuseOrCreate(existingDatasetNavList, { label, id: label }), + ); + + const result = existingDatasetNavList ?? []; + syncList(result, freshEntries, null); + + for (const datasetNavEntry of result) { + await populateDataset(datasetNavEntry); + } + + console.log("finished Building navObj: ", result); + return result; +} + +async function getDatasetNames() { + try { + const res = await bec.getDatasetNames(); + return await res.json(); + } catch (err) { + console.error("Error fetching dataset names", err); + return []; + } +} + +async function populateDataset(datasetNavEntry) { + const existingGraphNavList = datasetNavEntry.children; + + const freshEntries = (await getGraphNames(datasetNavEntry.id)) + .sort((a, b) => getUri(a).localeCompare(getUri(b))) + .map(uri => { + const fullUri = getUri(uri); + return reuseOrCreate(existingGraphNavList, { + label: new URI(fullUri).suffix, + tooltip: fullUri, + id: fullUri, + }); + }); + + if (datasetNavEntry.children) { + syncList(datasetNavEntry.children, freshEntries, datasetNavEntry); + } else { + datasetNavEntry.children = freshEntries; + freshEntries.forEach(entry => (entry.parent = datasetNavEntry)); + } + + for (const graphNavEntry of datasetNavEntry.children) { + if (isSelectedGraph(datasetNavEntry.id, graphNavEntry.id)) { + graphNavEntry.parent?.open(); + } + await populateGraph(datasetNavEntry, graphNavEntry); + } +} + +async function getGraphNames(datasetName) { + try { + const res = await bec.getGraphNames(datasetName); + return await res.json(); + } catch (err) { + console.error( + "Error fetching graph names for dataset " + datasetName, + err, + ); + return []; + } +} + +export async function populateGraph(datasetNavObject, graphNavObject) { + const existingPackageList = graphNavObject.children; + const packageApiObject = await getPackages( + datasetNavObject.id, + graphNavObject.id, + ); + const allClasses = await getClasses(datasetNavObject.id, graphNavObject.id); + + const freshEntries = [ + ...packageApiObject.internalPackageList.map(pack => + reuseOrCreatePackage(existingPackageList, pack, false), + ), + ...packageApiObject.externalPackageList.map(pack => + reuseOrCreatePackage(existingPackageList, pack, true), + ), + ].sort((a, b) => { + if (!a?.label || a.label === "default") return 1; + if (!b?.label || b.label === "default") return -1; + return a.label.localeCompare(b.label); + }); + + if (graphNavObject.children) { + syncList(graphNavObject.children, freshEntries, graphNavObject); + } else { + graphNavObject.children = freshEntries; + freshEntries.forEach(entry => (entry.parent = graphNavObject)); + } + + for (const packageNavEntry of graphNavObject.children) { + if ( + isSelectedPackage( + datasetNavObject.id, + graphNavObject.id, + packageNavEntry.id, + ) + ) { + packageNavEntry.parent?.open(); + } + populatePackage( + packageNavEntry, + allClasses, + datasetNavObject.id, + graphNavObject.id, + ); + } + return graphNavObject; +} + +/** + * @description Reuses or creates a package NavEntry. + * @param {NavEntry[]} existingPackageList + * @param {object} packObj + * @param {boolean} isExternal + * @returns {NavEntry} + */ +function reuseOrCreatePackage(existingPackageList, packObj, isExternal) { + const id = packObj.uuid ?? "default"; + return reuseOrCreate(existingPackageList, { + id, + tooltip: packObj.prefix + packObj.label, + label: packObj.label, + data: { + uuid: id, + prefix: packObj.prefix, + label: packObj.label, + comment: packObj.comment, + external: isExternal, + }, + }); +} + +async function getPackages(datasetName, graphURI) { + try { + const res = await bec.getPackages(datasetName, graphURI); + return await res.json(); + } catch (err) { + console.error( + "Error fetching packages for dataset " + + datasetName + + " and graph " + + graphURI, + err, + ); + return { internalPackageList: [], externalPackageList: [] }; + } +} + +function populatePackage(packageNavObject, allClasses, datasetId, graphId) { + const existingClassList = packageNavObject.children; + + const freshEntries = allClasses + .filter(cls => packageNavObject.id === (cls.package?.uuid ?? "default")) + .sort((a, b) => a.label.localeCompare(b.label)) + .map(cls => + reuseOrCreate(existingClassList, { + id: cls.uuid, + tooltip: cls.package?.prefix + cls.label, + label: cls.label, + data: { + uuid: cls.uuid, + prefix: cls.package?.prefix, + label: cls.label, + comment: cls.comment, + }, + }), + ); + + if (packageNavObject.children) { + syncList(packageNavObject.children, freshEntries, packageNavObject); + } else { + packageNavObject.children = freshEntries; + freshEntries.forEach(entry => (entry.parent = packageNavObject)); + } + + for (const classNavEntry of packageNavObject.children) { + if (isSelectedClass(datasetId, graphId, classNavEntry.id)) { + classNavEntry.parent?.open(); + } + } + + return packageNavObject; +} + +async function getClasses(datasetName, graphURI) { + try { + const res = await bec.getClasses(datasetName, graphURI); + return await res.json(); + } catch (err) { + console.error( + "Error fetching classes for dataset " + + datasetName + + " and graph " + + graphURI, + err, + ); + return []; + } +} diff --git a/frontend/src/routes/mainpage/packageNavigation/ontology-editor-dialog/OntologyDialog.svelte b/frontend/src/routes/mainpage/packageNavigation/ontology-editor-dialog/OntologyDialog.svelte index 57b96659..c818f7b0 100644 --- a/frontend/src/routes/mainpage/packageNavigation/ontology-editor-dialog/OntologyDialog.svelte +++ b/frontend/src/routes/mainpage/packageNavigation/ontology-editor-dialog/OntologyDialog.svelte @@ -37,8 +37,10 @@ showDialog = $bindable(), dataset, graphUri, + namespaces, ontology = $bindable(), readonly, + onSubmit, } = $props(); const bec = new BackendConnection(fetch, PUBLIC_BACKEND_URL); @@ -46,7 +48,6 @@ let showAddKnownEntriesPopUp = $state(false); let showDiscardSaveConfirmDialog = $state(false); - let namespaces = $state([]); let tableContainerRef = $state(null); let ontologyObject = $state(); @@ -58,7 +59,9 @@ let disableSubmit = $derived(!hasChanges || !isValid); async function onOpen() { - namespaces = await getNamespaces(dataset); + if (!namespaces) { + namespaces = await getNamespaces(dataset); + } if (!ontology) { ontologyObject = new ReactiveOntology(); } else { @@ -91,10 +94,14 @@ ontologyObject.reset(); } - function save() { - saveOntology(dataset, graphUri, ontologyObject); + async function save() { + await saveOntology(dataset, graphUri, ontologyObject); ontologyObject.save(); - forceReloadTrigger.trigger(); + if (onSubmit) { + onSubmit(); + } else { + forceReloadTrigger.trigger(); + } } function discard() { @@ -125,7 +132,7 @@ } function getSubstitutedNamespace(namespace) { - const namespaceObj = namespaces.find(p => p?.prefix === namespace); + const namespaceObj = namespaces?.find(p => p?.prefix === namespace); return namespaceObj ? namespaceObj.substitutedPrefix : namespace; } diff --git a/frontend/src/routes/mainpage/packageNavigation/packageNavigation.svelte b/frontend/src/routes/mainpage/packageNavigation/packageNavigation.svelte index c15220c9..d7d0d518 100644 --- a/frontend/src/routes/mainpage/packageNavigation/packageNavigation.svelte +++ b/frontend/src/routes/mainpage/packageNavigation/packageNavigation.svelte @@ -20,52 +20,37 @@ faDiagramProject, faFileImport, } from "@fortawesome/free-solid-svg-icons"; + import { setContext, untrack } from "svelte"; - import { BackendConnection } from "$lib/api/backend.js"; import { ContextMenu } from "$lib/components/bitsui/contextmenu"; - import { PUBLIC_BACKEND_URL } from "$lib/config/runtime"; import { forceReloadTrigger } from "$lib/sharedState.svelte.js"; + import { SimpleTrigger } from "$lib/statePrimitives.svelte.js"; + import { getNavEntryList } from "./build-nav-object.js"; import DatasetSection from "./DatasetSection.svelte"; - import { isSelectedDataset } from "./packageNavigationUtils.svelte.js"; import ImportDialog from "../../ImportDialog.svelte"; import NewGraphDialog from "../../NewGraphDialog.svelte"; - const bec = new BackendConnection(fetch, PUBLIC_BACKEND_URL); - let datasetList = $state([]); + const localReloadTrigger = new SimpleTrigger(); let initialDatasetsLoaded = $state(false); let showImportDialog = $state(false); let showNewGraphDialog = $state(false); + let datasetNavEntryList = $state(null); $effect(async () => { forceReloadTrigger.subscribe(); - await fetchDatasets(); - }); - - async function getDatasetNames() { - const res = await bec.getDatasetNames(); - return await res.json(); - } - - async function fetchDatasets() { - let datasetNames = await getDatasetNames(); - let newDatasetList = []; - - const previous = datasetList ?? []; - for (const datasetName of datasetNames) { - const prev = previous.find(d => d.label === datasetName); - const keepExpanded = prev?.showContents ?? false; - newDatasetList.push({ - label: datasetName, - showContents: keepExpanded || isSelectedDataset(datasetName), - }); - } - newDatasetList = newDatasetList.sort((a, b) => - a.label.localeCompare(b.label), + await untrack( + async () => + (datasetNavEntryList = + await getNavEntryList(datasetNavEntryList)), ); - datasetList = newDatasetList; initialDatasetsLoaded = true; - } + localReloadTrigger.trigger(); + }); + + setContext("packageNavigation", { + reloadTrigger: localReloadTrigger, + });
@@ -80,17 +65,19 @@
- {#if datasetList && datasetList.length > 0} + {#if datasetNavEntryList && datasetNavEntryList.length > 0}
- {#each datasetList as dataset} - - {/each} + {#key datasetNavEntryList} + {#each datasetNavEntryList as datasetNavEntry (datasetNavEntry.id)} + + {/each} + {/key}
{:else if initialDatasetsLoaded}
- No data available + Not yet loaded
{/if}
diff --git a/frontend/src/routes/mainpage/packageNavigation/packageNavigationUtils.svelte.js b/frontend/src/routes/mainpage/packageNavigation/packageNavigationUtils.svelte.js index 67ccdb3c..0375d2ed 100644 --- a/frontend/src/routes/mainpage/packageNavigation/packageNavigationUtils.svelte.js +++ b/frontend/src/routes/mainpage/packageNavigation/packageNavigationUtils.svelte.js @@ -39,6 +39,9 @@ export function isSelectedPackage(dataset, graph, pack) { } export function isSelectedClass(dataset, graph, cls) { + if (typeof cls === "string") { + cls = { uuid: cls }; + } const datasetLabel = dataset?.label ?? dataset; const graphUri = getUri(graph); return ( @@ -49,10 +52,16 @@ export function isSelectedClass(dataset, graph, cls) { } export function getUri(resource) { + if (typeof resource === "string") { + return resource; + } const uri = resource.uri ? resource.uri : resource; return uri.prefix ? uri.prefix + uri.suffix : uri.suffix; } -export function getPackageId(pack) { +function getPackageId(pack) { + if (typeof pack === "string") { + return pack; + } return pack?.uuid ?? "default"; } From 174ea17402ec0541ba0c875417ed4686f56c4676 Mon Sep 17 00:00:00 2001 From: Philipp Date: Fri, 10 Apr 2026 16:40:02 +0200 Subject: [PATCH 06/18] temp --- .../reactive/models/reactive-class.svelte.js | 17 ++++++++--------- .../validity-rules/validityFunctions.js | 1 + .../mainpage/classEditor/classEditor.svelte | 6 +----- 3 files changed, 10 insertions(+), 14 deletions(-) diff --git a/frontend/src/lib/models/reactive/models/reactive-class.svelte.js b/frontend/src/lib/models/reactive/models/reactive-class.svelte.js index ec030b90..4c314bc7 100644 --- a/frontend/src/lib/models/reactive/models/reactive-class.svelte.js +++ b/frontend/src/lib/models/reactive/models/reactive-class.svelte.js @@ -27,7 +27,7 @@ import { isInvalidLabel, isInvalidNamespace, isInvalidStereotype, - isInvalidUuid, + isInvalidUuid } from "$lib/models/reactive/validity-rules/validityFunctions.js"; function initializeStereotypeViolationChecks(stereotype, stereotypesArray) { @@ -36,21 +36,20 @@ function initializeStereotypeViolationChecks(stereotype, stereotypesArray) { ); } -function initializeAssociationViolationChecks( - association, - associationsArray, - getClassByUuid, -) { +function initializeAssociationViolationChecks(association, associationsArray, getClassByUuid) { association.label.violationChecks.push(() => - isInvalidAssociationLabel(association, associationsArray), + isInvalidAssociationLabel( + association, + associationsArray, + ) ); association.inverse.label.violationChecks.push(() => isInvalidInverseAssociationLabel( association, associationsArray, getClassByUuid, - ), - ); + ) + ) } function initializeUniqueLabelChecks(reactiveObject, enumEntriesArray) { diff --git a/frontend/src/lib/models/reactive/validity-rules/validityFunctions.js b/frontend/src/lib/models/reactive/validity-rules/validityFunctions.js index f6157e95..c4378ca1 100644 --- a/frontend/src/lib/models/reactive/validity-rules/validityFunctions.js +++ b/frontend/src/lib/models/reactive/validity-rules/validityFunctions.js @@ -18,6 +18,7 @@ import { validate as uuidValidate } from "uuid"; import { IriValidationStrategy, validateIri } from "validate-iri"; import { getNCNameViolations } from "$lib/rdf-syntax-grammar/namespace/prefix/index.js"; +import { getContext } from "svelte"; export function isInvalidUuid(uuid) { const violations = []; diff --git a/frontend/src/routes/mainpage/classEditor/classEditor.svelte b/frontend/src/routes/mainpage/classEditor/classEditor.svelte index b66999c3..b9e964ca 100644 --- a/frontend/src/routes/mainpage/classEditor/classEditor.svelte +++ b/frontend/src/routes/mainpage/classEditor/classEditor.svelte @@ -151,11 +151,7 @@ const classDto = await ( await bec.getClassInfo(datasetName, graphUri, classUuid) ).json(); - const newReactiveClass = mapClassDtoToReactiveClass( - classDto, - context.classes, - uuid => context.targetClassInfos.find(cls => cls.uuid === uuid), - ); + const newReactiveClass = mapClassDtoToReactiveClass(classDto, context.classes, (uuid) => context.classes.find(cls => cls.uuid === uuid)); reactiveClass = adoptUnsavedClassChanges( newReactiveClass, reactiveClass, From 051e3a7783eddf74d40f166f2f837d083570a1c9 Mon Sep 17 00:00:00 2001 From: Philipp Date: Tue, 14 Apr 2026 13:50:51 +0200 Subject: [PATCH 07/18] association violation checks are now functioning correctly --- .../reactive/models/reactive-class.svelte.js | 17 +++++++++-------- .../validity-rules/validityFunctions.js | 1 - .../mainpage/classEditor/classEditor.svelte | 6 +++++- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/frontend/src/lib/models/reactive/models/reactive-class.svelte.js b/frontend/src/lib/models/reactive/models/reactive-class.svelte.js index 4c314bc7..ec030b90 100644 --- a/frontend/src/lib/models/reactive/models/reactive-class.svelte.js +++ b/frontend/src/lib/models/reactive/models/reactive-class.svelte.js @@ -27,7 +27,7 @@ import { isInvalidLabel, isInvalidNamespace, isInvalidStereotype, - isInvalidUuid + isInvalidUuid, } from "$lib/models/reactive/validity-rules/validityFunctions.js"; function initializeStereotypeViolationChecks(stereotype, stereotypesArray) { @@ -36,20 +36,21 @@ function initializeStereotypeViolationChecks(stereotype, stereotypesArray) { ); } -function initializeAssociationViolationChecks(association, associationsArray, getClassByUuid) { +function initializeAssociationViolationChecks( + association, + associationsArray, + getClassByUuid, +) { association.label.violationChecks.push(() => - isInvalidAssociationLabel( - association, - associationsArray, - ) + isInvalidAssociationLabel(association, associationsArray), ); association.inverse.label.violationChecks.push(() => isInvalidInverseAssociationLabel( association, associationsArray, getClassByUuid, - ) - ) + ), + ); } function initializeUniqueLabelChecks(reactiveObject, enumEntriesArray) { diff --git a/frontend/src/lib/models/reactive/validity-rules/validityFunctions.js b/frontend/src/lib/models/reactive/validity-rules/validityFunctions.js index c4378ca1..f6157e95 100644 --- a/frontend/src/lib/models/reactive/validity-rules/validityFunctions.js +++ b/frontend/src/lib/models/reactive/validity-rules/validityFunctions.js @@ -18,7 +18,6 @@ import { validate as uuidValidate } from "uuid"; import { IriValidationStrategy, validateIri } from "validate-iri"; import { getNCNameViolations } from "$lib/rdf-syntax-grammar/namespace/prefix/index.js"; -import { getContext } from "svelte"; export function isInvalidUuid(uuid) { const violations = []; diff --git a/frontend/src/routes/mainpage/classEditor/classEditor.svelte b/frontend/src/routes/mainpage/classEditor/classEditor.svelte index b9e964ca..b66999c3 100644 --- a/frontend/src/routes/mainpage/classEditor/classEditor.svelte +++ b/frontend/src/routes/mainpage/classEditor/classEditor.svelte @@ -151,7 +151,11 @@ const classDto = await ( await bec.getClassInfo(datasetName, graphUri, classUuid) ).json(); - const newReactiveClass = mapClassDtoToReactiveClass(classDto, context.classes, (uuid) => context.classes.find(cls => cls.uuid === uuid)); + const newReactiveClass = mapClassDtoToReactiveClass( + classDto, + context.classes, + uuid => context.targetClassInfos.find(cls => cls.uuid === uuid), + ); reactiveClass = adoptUnsavedClassChanges( newReactiveClass, reactiveClass, From 72491061195bc52ec31f06a38cf9a991f10e6bbe Mon Sep 17 00:00:00 2001 From: Philipp Date: Tue, 14 Apr 2026 15:27:55 +0200 Subject: [PATCH 08/18] format --- .../associationEditorDialog/AssociationEditorDialog.svelte | 5 ----- 1 file changed, 5 deletions(-) diff --git a/frontend/src/routes/mainpage/classEditor/components/associations/associationEditorDialog/AssociationEditorDialog.svelte b/frontend/src/routes/mainpage/classEditor/components/associations/associationEditorDialog/AssociationEditorDialog.svelte index cd707e76..233b8c96 100644 --- a/frontend/src/routes/mainpage/classEditor/components/associations/associationEditorDialog/AssociationEditorDialog.svelte +++ b/frontend/src/routes/mainpage/classEditor/components/associations/associationEditorDialog/AssociationEditorDialog.svelte @@ -77,11 +77,6 @@ } } - function onClose() { - association = null; - isNewAssociation = true; - } - async function saveAssociation() { const apiAssociation = mapReactiveAssociationToAssociationDto( association, From 64c02e7001afc9ec3bb9f7e595650ba549b5efa8 Mon Sep 17 00:00:00 2001 From: Philipp Date: Wed, 15 Apr 2026 15:55:31 +0200 Subject: [PATCH 09/18] now only shows one violation at the same time --- .../models/reactive-association.svelte.js | 4 +- .../validity-rules/validityFunctions.js | 80 ++++++++++--------- 2 files changed, 44 insertions(+), 40 deletions(-) diff --git a/frontend/src/lib/models/reactive/models/reactive-association.svelte.js b/frontend/src/lib/models/reactive/models/reactive-association.svelte.js index 7274b49c..fa206951 100644 --- a/frontend/src/lib/models/reactive/models/reactive-association.svelte.js +++ b/frontend/src/lib/models/reactive/models/reactive-association.svelte.js @@ -50,7 +50,7 @@ export class ReactiveAssociation { // Set top-level properties this.uuid = new ReactiveValueWrapper(uuid, isInvalidUuid); - this.label = new ReactiveValueWrapper(label, isInvalidLabel); + this.label = new ReactiveValueWrapper(label); this.namespace = new ReactiveValueWrapper( namespace, isInvalidNamespace, @@ -79,7 +79,7 @@ export class ReactiveAssociation { // Set inverse properties this.inverse = { uuid: new ReactiveValueWrapper(inverseUuid, isInvalidUuid), - label: new ReactiveValueWrapper(inverseLabel, isInvalidLabel), + label: new ReactiveValueWrapper(inverseLabel), namespace: new ReactiveValueWrapper( inverseNamespace, isInvalidNamespace, diff --git a/frontend/src/lib/models/reactive/validity-rules/validityFunctions.js b/frontend/src/lib/models/reactive/validity-rules/validityFunctions.js index f6157e95..a45ecf2d 100644 --- a/frontend/src/lib/models/reactive/validity-rules/validityFunctions.js +++ b/frontend/src/lib/models/reactive/validity-rules/validityFunctions.js @@ -37,31 +37,33 @@ export function isInvalidLabel(label) { } export function isInvalidAssociationLabel(association, associations) { - const violations = []; + const violations = isNotEmptyValidation(association?.label?.value); const assocList = Array.isArray(associations) ? associations : (associations?.values ?? []); - if ( - assocList.filter( - a => - a.label.value === association?.label?.value && - a.namespace.value === association.namespace?.value && - a.uuid.value !== association?.uuid?.value, - ).length > 0 - ) { - violations.push("must be unique"); - } else if ( - association && - association.domain && - association.target && - association.inverse && - association.label - ) { + if (violations.length === 0) { if ( - association.domain?.value === association.target?.value && - association.inverse?.label?.value === association.label?.value + assocList.filter( + a => + a.label.value === association?.label?.value && + a.namespace.value === association.namespace?.value && + a.uuid.value !== association?.uuid?.value, + ).length > 0 ) { violations.push("must be unique"); + } else if ( + association && + association.domain && + association.target && + association.inverse && + association.label + ) { + if ( + association.domain?.value === association.target?.value && + association.inverse?.label?.value === association.label?.value + ) { + violations.push("must be unique"); + } } } return violations; @@ -72,30 +74,32 @@ export function isInvalidInverseAssociationLabel( associations, getClassByUuid, ) { - const violations = []; + const violations = isNotEmptyValidation(association?.inverse?.label?.value); const targetClassDto = getClassByUuid(association.target?.value); const assocList = targetClassDto?.associationPairs?.map(pair => pair) ?? []; - if ( - assocList.filter( - a => - a.from.label === association?.inverse?.label?.value && - a.from.prefix === association.inverse?.namespace?.value && - a.from.uuid !== association.inverse?.uuid?.value, - ).length > 0 - ) { - violations.push("must be unique"); - } else if ( - association && - association.domain && - association.target && - association.inverse && - association.label - ) { + if (violations.length === 0) { if ( - association.domain?.value === association.target?.value && - association.inverse?.label?.value === association.label?.value + assocList.filter( + a => + a.from.label === association?.inverse?.label?.value && + a.from.prefix === association.inverse?.namespace?.value && + a.from.uuid !== association.inverse?.uuid?.value, + ).length > 0 ) { violations.push("must be unique"); + } else if ( + association && + association.domain && + association.target && + association.inverse && + association.label + ) { + if ( + association.domain?.value === association.target?.value && + association.inverse?.label?.value === association.label?.value + ) { + violations.push("must be unique"); + } } } return violations; From d2f0ece4614924ef55dbced3766bd6d159b14e04 Mon Sep 17 00:00:00 2001 From: Philipp Date: Wed, 15 Apr 2026 15:57:03 +0200 Subject: [PATCH 10/18] format --- .../lib/models/reactive/models/reactive-association.svelte.js | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/src/lib/models/reactive/models/reactive-association.svelte.js b/frontend/src/lib/models/reactive/models/reactive-association.svelte.js index fa206951..97c61450 100644 --- a/frontend/src/lib/models/reactive/models/reactive-association.svelte.js +++ b/frontend/src/lib/models/reactive/models/reactive-association.svelte.js @@ -16,7 +16,6 @@ */ import { ReactiveValueWrapper } from "$lib/models/reactive/reactive-wrappers/reactive-value-wrapper.svelte.js"; import { - isInvalidLabel, isInvalidMultiplicityLowerBound, isInvalidMultiplicityUpperBound, isInvalidNamespace, From 33862f89cfd36796e954cb7b341ca421b1bb9be6 Mon Sep 17 00:00:00 2001 From: Philipp Date: Thu, 16 Apr 2026 11:03:30 +0200 Subject: [PATCH 11/18] fixed that after discarding changes in the AttributeEditorDialog or AssociationEditorDialog opening one of those to edit would create a new association/attribute --- .../lib/models/reactive/models/reactive-class.svelte.js | 1 - .../AssociationEditorDialog.svelte | 8 +++++++- .../components/attributes/AttributeEditorDialog.svelte | 9 +++++++-- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/frontend/src/lib/models/reactive/models/reactive-class.svelte.js b/frontend/src/lib/models/reactive/models/reactive-class.svelte.js index d6ffa4b0..53256955 100644 --- a/frontend/src/lib/models/reactive/models/reactive-class.svelte.js +++ b/frontend/src/lib/models/reactive/models/reactive-class.svelte.js @@ -25,7 +25,6 @@ import { hasUniqueLabel, isInvalidAssociationLabel, isInvalidInverseAssociationLabel, - isInvalidLabel, isInvalidClassLabel, isInvalidNamespace, isInvalidStereotype, diff --git a/frontend/src/routes/mainpage/classEditor/components/associations/associationEditorDialog/AssociationEditorDialog.svelte b/frontend/src/routes/mainpage/classEditor/components/associations/associationEditorDialog/AssociationEditorDialog.svelte index 233b8c96..a3754f31 100644 --- a/frontend/src/routes/mainpage/classEditor/components/associations/associationEditorDialog/AssociationEditorDialog.svelte +++ b/frontend/src/routes/mainpage/classEditor/components/associations/associationEditorDialog/AssociationEditorDialog.svelte @@ -29,12 +29,17 @@ import { saveApiAssociationToBackend } from "../save-association-to-backend.js"; import Inverse from "./Inverse.svelte"; - let { showDialog = $bindable(), associations, association } = $props(); + let { + showDialog = $bindable(), + associations, + association: associationProp, + } = $props(); const bec = new BackendConnection(fetch, PUBLIC_BACKEND_URL); let classEditorContext = $state(); let isNewAssociation = $state(true); + let association = $derived(associationProp); let readonly = $derived(classEditorContext?.readonly); $effect(async () => { @@ -62,6 +67,7 @@ function onOpen() { classEditorContext = getContext("classEditor"); + association = associationProp; if (!associations.contains(association)) { isNewAssociation = true; association = new ReactiveAssociation({ diff --git a/frontend/src/routes/mainpage/classEditor/components/attributes/AttributeEditorDialog.svelte b/frontend/src/routes/mainpage/classEditor/components/attributes/AttributeEditorDialog.svelte index 94dcfe87..ec28196e 100644 --- a/frontend/src/routes/mainpage/classEditor/components/attributes/AttributeEditorDialog.svelte +++ b/frontend/src/routes/mainpage/classEditor/components/attributes/AttributeEditorDialog.svelte @@ -31,16 +31,21 @@ import { saveApiAttributeToBackend } from "./save-attribute-to-backend.js"; - let { showDialog = $bindable(), attribute, attributes } = $props(); - + let { + showDialog = $bindable(), + attribute: attributeProp, + attributes, + } = $props(); let classEditorContext = $state(); let isNewAttribute = $state(true); + let attribute = $derived(attributeProp); let readonly = $derived(classEditorContext?.readonly); let datatypes = $derived(classEditorContext?.datatypes); function onOpen() { classEditorContext = getContext("classEditor"); + attribute = attributeProp; if (!attributes.contains(attribute)) { isNewAttribute = true; attribute = new ReactiveAttribute({ From 55509bc26b348a18ec9f4b4cc35999297ac2dd1a Mon Sep 17 00:00:00 2001 From: Philipp Date: Thu, 16 Apr 2026 11:21:21 +0200 Subject: [PATCH 12/18] Revert "fixed that after discarding changes in the AttributeEditorDialog or AssociationEditorDialog opening one of those to edit would create a new association/attribute" This reverts commit 33862f89cfd36796e954cb7b341ca421b1bb9be6. --- .../lib/models/reactive/models/reactive-class.svelte.js | 1 + .../AssociationEditorDialog.svelte | 8 +------- .../components/attributes/AttributeEditorDialog.svelte | 9 ++------- 3 files changed, 4 insertions(+), 14 deletions(-) diff --git a/frontend/src/lib/models/reactive/models/reactive-class.svelte.js b/frontend/src/lib/models/reactive/models/reactive-class.svelte.js index 53256955..d6ffa4b0 100644 --- a/frontend/src/lib/models/reactive/models/reactive-class.svelte.js +++ b/frontend/src/lib/models/reactive/models/reactive-class.svelte.js @@ -25,6 +25,7 @@ import { hasUniqueLabel, isInvalidAssociationLabel, isInvalidInverseAssociationLabel, + isInvalidLabel, isInvalidClassLabel, isInvalidNamespace, isInvalidStereotype, diff --git a/frontend/src/routes/mainpage/classEditor/components/associations/associationEditorDialog/AssociationEditorDialog.svelte b/frontend/src/routes/mainpage/classEditor/components/associations/associationEditorDialog/AssociationEditorDialog.svelte index a3754f31..233b8c96 100644 --- a/frontend/src/routes/mainpage/classEditor/components/associations/associationEditorDialog/AssociationEditorDialog.svelte +++ b/frontend/src/routes/mainpage/classEditor/components/associations/associationEditorDialog/AssociationEditorDialog.svelte @@ -29,17 +29,12 @@ import { saveApiAssociationToBackend } from "../save-association-to-backend.js"; import Inverse from "./Inverse.svelte"; - let { - showDialog = $bindable(), - associations, - association: associationProp, - } = $props(); + let { showDialog = $bindable(), associations, association } = $props(); const bec = new BackendConnection(fetch, PUBLIC_BACKEND_URL); let classEditorContext = $state(); let isNewAssociation = $state(true); - let association = $derived(associationProp); let readonly = $derived(classEditorContext?.readonly); $effect(async () => { @@ -67,7 +62,6 @@ function onOpen() { classEditorContext = getContext("classEditor"); - association = associationProp; if (!associations.contains(association)) { isNewAssociation = true; association = new ReactiveAssociation({ diff --git a/frontend/src/routes/mainpage/classEditor/components/attributes/AttributeEditorDialog.svelte b/frontend/src/routes/mainpage/classEditor/components/attributes/AttributeEditorDialog.svelte index ec28196e..94dcfe87 100644 --- a/frontend/src/routes/mainpage/classEditor/components/attributes/AttributeEditorDialog.svelte +++ b/frontend/src/routes/mainpage/classEditor/components/attributes/AttributeEditorDialog.svelte @@ -31,21 +31,16 @@ import { saveApiAttributeToBackend } from "./save-attribute-to-backend.js"; - let { - showDialog = $bindable(), - attribute: attributeProp, - attributes, - } = $props(); + let { showDialog = $bindable(), attribute, attributes } = $props(); + let classEditorContext = $state(); let isNewAttribute = $state(true); - let attribute = $derived(attributeProp); let readonly = $derived(classEditorContext?.readonly); let datatypes = $derived(classEditorContext?.datatypes); function onOpen() { classEditorContext = getContext("classEditor"); - attribute = attributeProp; if (!attributes.contains(attribute)) { isNewAttribute = true; attribute = new ReactiveAttribute({ From 53ee15dacf99b78b83899b14263e9ab912dcba95 Mon Sep 17 00:00:00 2001 From: Philipp Date: Thu, 16 Apr 2026 11:21:59 +0200 Subject: [PATCH 13/18] format --- frontend/src/lib/models/reactive/models/reactive-class.svelte.js | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/src/lib/models/reactive/models/reactive-class.svelte.js b/frontend/src/lib/models/reactive/models/reactive-class.svelte.js index d6ffa4b0..53256955 100644 --- a/frontend/src/lib/models/reactive/models/reactive-class.svelte.js +++ b/frontend/src/lib/models/reactive/models/reactive-class.svelte.js @@ -25,7 +25,6 @@ import { hasUniqueLabel, isInvalidAssociationLabel, isInvalidInverseAssociationLabel, - isInvalidLabel, isInvalidClassLabel, isInvalidNamespace, isInvalidStereotype, From 50f8e849df78dedf55cce7a33d2648251c6d9bf7 Mon Sep 17 00:00:00 2001 From: Philipp Date: Fri, 17 Apr 2026 15:29:13 +0200 Subject: [PATCH 14/18] refactoring --- .../cim/queries/select/CIMQueryBuilder.java | 2 +- .../reactive/models/reactive-class.svelte.js | 6 +-- .../validity-rules/validityFunctions.js | 8 +--- .../mainpage/classEditor/classEditor.svelte | 26 +++++++--- .../AssociationEditorDialog.svelte | 47 ++++++++++--------- 5 files changed, 47 insertions(+), 42 deletions(-) diff --git a/backend/src/main/java/org/rdfarchitect/models/cim/queries/select/CIMQueryBuilder.java b/backend/src/main/java/org/rdfarchitect/models/cim/queries/select/CIMQueryBuilder.java index 6c8f60de..cf76a1f7 100644 --- a/backend/src/main/java/org/rdfarchitect/models/cim/queries/select/CIMQueryBuilder.java +++ b/backend/src/main/java/org/rdfarchitect/models/cim/queries/select/CIMQueryBuilder.java @@ -310,7 +310,7 @@ public CIMQueryBuilder appendPackageQuery(Mode mode) { //inverse /** - * Appends query for the uuid of objects connected via "cims:inverseRoleName" as {@link CIMQueryVars Inverse.LABEL}. + * Appends query for the uuid of objects connected via "cims:inverseRoleName" as {@link CIMQueryVars Inverse.UUID}. * * @param mode Whether the result of this is mode. * diff --git a/frontend/src/lib/models/reactive/models/reactive-class.svelte.js b/frontend/src/lib/models/reactive/models/reactive-class.svelte.js index 53256955..af73b1d0 100644 --- a/frontend/src/lib/models/reactive/models/reactive-class.svelte.js +++ b/frontend/src/lib/models/reactive/models/reactive-class.svelte.js @@ -46,11 +46,7 @@ function initializeAssociationViolationChecks( isInvalidAssociationLabel(association, associationsArray), ); association.inverse.label.violationChecks.push(() => - isInvalidInverseAssociationLabel( - association, - associationsArray, - getClassByUuid, - ), + isInvalidInverseAssociationLabel(association, getClassByUuid), ); } diff --git a/frontend/src/lib/models/reactive/validity-rules/validityFunctions.js b/frontend/src/lib/models/reactive/validity-rules/validityFunctions.js index e1e19f81..cea3ceed 100644 --- a/frontend/src/lib/models/reactive/validity-rules/validityFunctions.js +++ b/frontend/src/lib/models/reactive/validity-rules/validityFunctions.js @@ -61,7 +61,7 @@ export function isInvalidAssociationLabel(association, associations) { a => a.label.value === association?.label?.value && a.namespace.value === association.namespace?.value && - a.uuid.value !== association?.uuid?.value, + a !== association, ).length > 0 ) { violations.push("must be unique"); @@ -83,11 +83,7 @@ export function isInvalidAssociationLabel(association, associations) { return violations; } -export function isInvalidInverseAssociationLabel( - association, - associations, - getClassByUuid, -) { +export function isInvalidInverseAssociationLabel(association, getClassByUuid) { const violations = isNotEmptyValidation(association?.inverse?.label?.value); const targetClassDto = getClassByUuid(association.target?.value); const assocList = targetClassDto?.associationPairs?.map(pair => pair) ?? []; diff --git a/frontend/src/routes/mainpage/classEditor/classEditor.svelte b/frontend/src/routes/mainpage/classEditor/classEditor.svelte index b66999c3..7243983b 100644 --- a/frontend/src/routes/mainpage/classEditor/classEditor.svelte +++ b/frontend/src/routes/mainpage/classEditor/classEditor.svelte @@ -89,15 +89,22 @@ reactiveClass?.stereotypes.contains(enumerationStereotype), ); - $effect(async () => { + $effect(() => { editorState.selectedClassUUID.subscribe(); forceReloadTrigger.subscribe(); + + const cancellation = { cancelled: false }; loadingContext = true; loadingClass = true; + (async () => { + isDatasetReadOnly = await isReadOnly(datasetName); + await loadContext(); + await loadReactiveClass(cancellation); + })(); - isDatasetReadOnly = await isReadOnly(datasetName); - await loadContext(); - await loadReactiveClass(); + return () => { + cancellation.cancelled = true; + }; }); $effect(async () => { @@ -147,10 +154,11 @@ editorState.selectedContext.trigger(); } - async function loadReactiveClass() { + async function loadReactiveClass(cancelled) { const classDto = await ( await bec.getClassInfo(datasetName, graphUri, classUuid) ).json(); + if (cancelled.cancelled) return; const newReactiveClass = mapClassDtoToReactiveClass( classDto, context.classes, @@ -161,7 +169,6 @@ reactiveClass, ); loadingClass = false; - console.log({ reactiveClass }); const targetUuids = [ ...new Set( @@ -171,12 +178,14 @@ ), ]; - context.targetClassInfos = await Promise.all( + let targetClassInfos = await Promise.all( targetUuids.map(async uuid => { const res = await bec.getClassInfo(datasetName, graphUri, uuid); return res.json(); }), ); + if (cancelled.cancelled) return; + context.targetClassInfos = targetClassInfos; } function openPropertySHACLRulesDialog(property) { @@ -215,6 +224,9 @@ get targetClassInfos() { return context.targetClassInfos; }, + get backendConnection() { + return bec; + }, // get Objects by identifier functions get getClassByUuid() { return function (uuid) { diff --git a/frontend/src/routes/mainpage/classEditor/components/associations/associationEditorDialog/AssociationEditorDialog.svelte b/frontend/src/routes/mainpage/classEditor/components/associations/associationEditorDialog/AssociationEditorDialog.svelte index 233b8c96..36d21179 100644 --- a/frontend/src/routes/mainpage/classEditor/components/associations/associationEditorDialog/AssociationEditorDialog.svelte +++ b/frontend/src/routes/mainpage/classEditor/components/associations/associationEditorDialog/AssociationEditorDialog.svelte @@ -18,8 +18,6 @@