From 0bd4dd92cb0c590debcd6379cb7e810aa61f5890 Mon Sep 17 00:00:00 2001 From: Maximilian Date: Fri, 27 Mar 2026 22:32:44 +0100 Subject: [PATCH 1/7] started impl, still has a few bugs, but api errors are gone --- .../src/lib/models/nav/NavEntry.svelte.js | 101 +++++ frontend/src/routes/DeleteGraphDialog.svelte | 103 +++++ .../src/routes/layout/menu-bar/File.svelte | 1 - .../packageNavigation/ClassEntry.svelte | 73 ++-- .../packageNavigation/DatasetSection.svelte | 152 +++----- .../packageNavigation/GraphSection.svelte | 355 ++++-------------- .../packageNavigation/PackageButton.svelte | 148 +++----- .../packageNavigation/build-nav-object.js | 230 ++++++++++++ .../OntologyDialog.svelte | 8 +- .../packageNavigation.svelte | 50 +-- .../packageNavigationUtils.svelte.js | 9 + 11 files changed, 670 insertions(+), 560 deletions(-) create mode 100644 frontend/src/lib/models/nav/NavEntry.svelte.js create mode 100644 frontend/src/routes/DeleteGraphDialog.svelte 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..7ee5a264 --- /dev/null +++ b/frontend/src/lib/models/nav/NavEntry.svelte.js @@ -0,0 +1,101 @@ +/* + * 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); + + /** + * @param {{ label?: string, id?:string, tooltip?: string, isOpen?: boolean, children?: NavEntry[], 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; + + if (config.children?.length) { + this.addChildren(config.children); + } + } + + /** + * Opens this entry and all parent entries + */ + open() { + this.isOpen = true; + this.parent?.open(); + } + + /** + * Closes this entry and all children recursively + */ + close() { + this.isOpen = false; + this.children.forEach(child => child.close()); + } + + /** + * Toggles the open state + */ + toggle() { + if (this.isOpen) { + this.close(); + } else { + this.open(); + } + } + + /** + * @param {NavEntry} child + */ + addChild(child) { + child.parent = this; + this.children.push(child); + } + + /** + * @param {NavEntry[]} children + */ + addChildren(children) { + children.forEach(child => this.addChild(child)); + } + + /** + * @param {NavEntry} child + */ + removeChild(child) { + child.parent = null; + this.children = this.children.filter(c => c !== child); + } + + /** + * Whether this entry has children + * @type {boolean} + */ + hasChildren = $derived(this.children.length > 0); +} diff --git a/frontend/src/routes/DeleteGraphDialog.svelte b/frontend/src/routes/DeleteGraphDialog.svelte new file mode 100644 index 00000000..eb087a0e --- /dev/null +++ b/frontend/src/routes/DeleteGraphDialog.svelte @@ -0,0 +1,103 @@ + + + + + +
+ + {#if disableSubmit} +

+ Select a dataset and graph before deleting. +

+ {/if} +
+
+ +
diff --git a/frontend/src/routes/layout/menu-bar/File.svelte b/frontend/src/routes/layout/menu-bar/File.svelte index 5f081f37..59405b44 100644 --- a/frontend/src/routes/layout/menu-bar/File.svelte +++ b/frontend/src/routes/layout/menu-bar/File.svelte @@ -31,7 +31,6 @@ 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/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..c48447f4 100644 --- a/frontend/src/routes/mainpage/packageNavigation/DatasetSection.svelte +++ b/frontend/src/routes/mainpage/packageNavigation/DatasetSection.svelte @@ -33,137 +33,82 @@ 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 { 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 DeleteDatasetDialog from "../../DeleteDatasetDialog.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([]); - $effect(async () => { - forceReloadTrigger.subscribe(); - readonly = dataset ? await isReadOnly(dataset.label) : false; - await fetchGraphs(); - await updateReadonly(); + onMount(async () => { + 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), - }); - } - newGraphs = newGraphs.sort((a, b) => - a.uri.suffix.localeCompare(b.uri.suffix), - ); - graphs = newGraphs; - } - 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); + if (editorState.selectedDataset.getValue() === datasetNavEntry.label) { + return; } + editorState.selectedGraph.updateValue(null); + editorState.selectedPackageUUID.updateValue(null); + editorState.selectedDataset.updateValue(datasetNavEntry.label); } - function toggleDatasetContentsVisibility(dataset) { - dataset.showContents = !dataset.showContents; + function toggleDatasetContentsVisibility() { + datasetNavEntry.isOpen = !datasetNavEntry.isOpen; } function ensureDatasetExpanded() { - if (!dataset?.showContents) { - dataset.showContents = true; + if (!datasetNavEntry?.isOpen) { + datasetNavEntry.isOpen = true; } } - async function updateReadonly() { - readonly = await isReadOnly(dataset.label); - } - async function enableEditing() { - if (!dataset?.label || !readonly) { + if (!datasetNavEntry?.label || !readonly) { return; } - await bec.enableEditing(dataset.label); - await updateReadonly(); - forceReloadTrigger.trigger(); - editorState.selectedPackageUUID.trigger(); - editorState.selectedClassUUID.trigger(); + + await bec.enableEditing(datasetNavEntry.label).then(() => { + readonly = true; + }); } async function disableEditing() { - if (!dataset?.label || readonly) { + if (!datasetNavEntry?.label || readonly) { return; } - await bec.disableEditing(dataset.label); - await updateReadonly(); - forceReloadTrigger.trigger(); - editorState.selectedPackageUUID.trigger(); - editorState.selectedClassUUID.trigger(); + await bec.disableEditing(datasetNavEntry.label).then(() => { + readonly = false; + }); } @@ -172,18 +117,16 @@ 0} - expanded={dataset.showContents} - isSelected={isSelectedDataset(dataset)} - title={dataset.label} + hasChildren={datasetNavEntry.children?.length > 0} + expanded={datasetNavEntry.isOpen} + isSelected={isSelectedDataset(datasetNavEntry.label)} + title={datasetNavEntry.tooltip} badgeText={readonly ? "Read-only" : ""} badgeVariant="readonly" - onclick={() => { - selectDataset(); - }} - onToggle={() => toggleDatasetContentsVisibility(dataset)} + onclick={selectDataset} + onToggle={toggleDatasetContentsVisibility} /> @@ -258,16 +201,17 @@ - {#if dataset.showContents} + {#if datasetNavEntry.isOpen}
- {#each graphs as graph} + {#each datasetNavEntry.children as graphNavEntry} {/each}
@@ -276,18 +220,18 @@ - diff --git a/frontend/src/routes/mainpage/packageNavigation/GraphSection.svelte b/frontend/src/routes/mainpage/packageNavigation/GraphSection.svelte index 2389f25a..d117dd06 100644 --- a/frontend/src/routes/mainpage/packageNavigation/GraphSection.svelte +++ b/frontend/src/routes/mainpage/packageNavigation/GraphSection.svelte @@ -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,12 +53,9 @@ 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 DeleteGraphDialog from "../../DeleteGraphDialog.svelte"; import ExportDialog from "../../ExportDialog.svelte"; import GraphDeleteDialog from "../../GraphDeleteDialog.svelte"; import NewPackageDialog from "../../NewPackageDialog.svelte"; @@ -71,22 +67,16 @@ import { goto } from "$app/navigation"; let { - dataset, - graph, + datasetNavEntry, + graphNavEntry, onExpandDataset = () => {}, - prefixes = [], + 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 +84,25 @@ 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))); - - $effect(async () => { - forceReloadTrigger.subscribe(); - await loadGraphData(); - }); - - $effect(() => { - editorState.selectedClassUUID.subscribe(); - editorState.selectedClassDataset.subscribe(); - editorState.selectedClassGraph.subscribe(); - updateSelectedClassPackageId(); - ensureExpandedPackages(); - }); - - $effect(() => { - editorState.selectedPackageUUID.subscribe(); - ensureExpandedPackages(); - }); - - onMount(() => { - loadGraphData(); - }); - - 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(); - } + let graphHighlightLabel = $derived( + shortenIri(namespaces, graphNavEntry.id), + ); - async function loadGraphData() { - await fetchClasses(); + onMount(async () => { 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 +110,20 @@ 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 toggleGraphContentsVisibility() { + graphNavEntry.isOpen = !graphNavEntry.isOpen; } function ensureGraphIsExpanded() { - if (!graph?.showContents) { - graph.showContents = true; + if (!graphNavEntry?.isOpen) { + graphNavEntry.isOpen = 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 = @@ -359,27 +141,27 @@ editorState.selectedGraph.trigger(); editorState.selectedClassUUID.trigger(); forceReloadTrigger.trigger(); + console.log("triggered reload from graph section"); } -
+
0} - expanded={graph.showContents} - isSelected={isSelectedGraph(dataset, graph)} - title={graph.uri.suffix} + hasChildren={graphNavEntry.children.length > 0} + expanded={graphNavEntry.isOpen} + isSelected={isSelectedGraph( + datasetNavEntry.id, + graphNavEntry.id, + )} + title={graphNavEntry.tooltip} highlightLabel={graphHighlightLabel} - onclick={() => { - focusGraphContext(); - }} - onToggle={() => toggleGraphContentsVisibility(graph)} + onclick={focusGraphContext} + onToggle={toggleGraphContentsVisibility} /> @@ -388,7 +170,7 @@ focusGraphContext(); showNewPackageDialog = true; }} - disabled={readOnly} + disabled={readonly} faIcon={faPlus} > New Package @@ -397,11 +179,11 @@ { focusGraphContext(); - undo(dataset.label, getUri(graph)).then(success => { - if (success) triggerReload(); + undo(datasetNavEntry.id, graphNavEntry.id).then(success => { + if (success) triggerReload(); //TODO: Hier könnte ich doch lokal reloaden, weil ich weiß, dass die aktion sich nur auf graph und drunter auswirkt }); }} - disabled={readOnly || !canUndo} + disabled={readonly || !canUndo} faIcon={faRotateLeft} > Undo @@ -409,16 +191,16 @@ { focusGraphContext(); - redo(dataset.label, getUri(graph)).then(success => { + redo(datasetNavEntry.id, graphNavEntry.id).then(success => { if (success) triggerReload(); }); }} - disabled={readOnly || !canRedo} + disabled={readonly || !canRedo} faIcon={faRotateRight} > Redo - {#if !readOnly} + {#if !readonly} {#if ontology} { - bec.deleteOntology(dataset.label, getUri(graph)); + bec.deleteOntology( + datasetNavEntry.id, + graphNavEntry.id, + ); forceReloadTrigger.trigger(); }} variant="danger" @@ -498,7 +283,7 @@ focusGraphContext(); showSHACLUploadDialog = true; }} - disabled={readOnly} + disabled={readonly} faIcon={faUpload} > Import @@ -538,7 +323,7 @@ focusGraphContext(); showDeleteDialog = true; }} - disabled={readOnly} + disabled={readonly} faIcon={faTrash} variant="danger" > @@ -546,19 +331,18 @@ - {#if graph.showContents} + {#if graphNavEntry.isOpen}
- {#each packages as pack (getPackageId(pack))} + {#each graphNavEntry.children as packageNavEntry (packageNavEntry.id)} {/each}
@@ -567,35 +351,36 @@ diff --git a/frontend/src/routes/mainpage/packageNavigation/PackageButton.svelte b/frontend/src/routes/mainpage/packageNavigation/PackageButton.svelte index 39e75ba1..4a833680 100644 --- a/frontend/src/routes/mainpage/packageNavigation/PackageButton.svelte +++ b/frontend/src/routes/mainpage/packageNavigation/PackageButton.svelte @@ -26,9 +26,7 @@ faTrash, faEye, } from "@fortawesome/free-solid-svg-icons"; - import { onMount } 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 { @@ -39,30 +37,27 @@ 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 = [], + datasetNavEntry, + graphNavEntry, + packageNavEntry, + namespaces = [], + readonly, onPackChange = () => {}, } = $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 isProtectedPackage = $derived( + packageNavEntry?.id === "default" || packageNavEntry?.data.external, + ); + // Ensure selection-dependent UI updates without remounting the component. const selectionTrigger = $derived([ editorState.selectedDataset.subscribe(), @@ -71,40 +66,30 @@ forceReloadTrigger.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); 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 +99,14 @@ ); } - 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, - }); + packageNavEntry.isOpen = !packageNavEntry.isOpen; } 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,15 +115,17 @@ @@ -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,18 +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 +186,23 @@ + 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..80ed7f38 --- /dev/null +++ b/frontend/src/routes/mainpage/packageNavigation/build-nav-object.js @@ -0,0 +1,230 @@ +/* + * 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 } from "./new/packageNavigationUtils.svelte.js"; + +const bec = new BackendConnection(fetch, PUBLIC_BACKEND_URL); + +/** + * Hier werden alle datasets geladen. + * Falls bereits ein Dataset bereits existiert, werden beinhaltete flags übernommen, damit die Navigation im gleichen openstate bleibt + * @param existingDatasetNavList + * @returns {Promise} + */ +export async function getNavEntryList(existingDatasetNavList) { + console.log( + "started Building navObj with existingNavObj: ", + existingDatasetNavList, + ); + + const newDatasetNavList = (await getDatasetNames()) + .sort((a, b) => a.localeCompare(b)) + .map(label => new NavEntry({ label, id: label })); + for (const newDatasetNavEntry of newDatasetNavList) { + const existingDatasetNavEntry = existingDatasetNavList?.find( + existingNavEntry => existingNavEntry.id === newDatasetNavEntry.id, + ); + newDatasetNavEntry.isOpen = existingDatasetNavEntry?.isOpen ?? false; + await populateDataset( + newDatasetNavEntry, + existingDatasetNavEntry?.children, + ); + } + console.log("finished Building navObj: ", newDatasetNavList); + return newDatasetNavList; +} + +async function getDatasetNames() { + try { + const res = await bec.getDatasetNames(); + return await res.json(); + } catch (err) { + console.error("Error fetching dataset names", err); + return []; + } +} + +/** + * Hier wird das angegebe Dataset mit Graphen gefüllt + * Falls bereits Graphen existieren, werden beinhaltete flags übernommen, damit die Navigation im gleichen openstate bleibt + * @returns {Promise} + */ +export async function populateDataset(datasetNavEntry, existingGraphNavList) { + datasetNavEntry.children = (await getGraphNames(datasetNavEntry.id)) + .sort((a, b) => getUri(a).localeCompare(getUri(b))) // change this to sort by suffix if wanted + .map(uri => { + const newUri = new URI(getUri(uri)); + const argumentObject = { + label: newUri.suffix, + tooltip: newUri.prefix + newUri.suffix, + id: newUri.prefix + newUri.suffix, + }; + return new NavEntry(argumentObject); + }); + for (const graphNavEntry of datasetNavEntry.children) { + const existingGraph = existingGraphNavList?.find( + g => g.id === graphNavEntry.id, + ); + graphNavEntry.isOpen = existingGraph?.isOpen ?? false; + await populateGraph( + datasetNavEntry, + graphNavEntry, + existingGraph?.children, + ); + } +} + +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 []; + } +} + +/** + * hier werden graphen des angegeben datasets mit Packages gefüllt. + * Falls bereits Packages existieren, werden beinhaltete flags übernommen, damit die Navigation im gleichen openstate bleibt + * @param graph + * @returns {Promise} + */ +export async function populateGraph( + datasetNavObject, + graphNavObject, + existingPackageList, +) { + const packageApiObject = await getPackages( + datasetNavObject.id, + graphNavObject.id, + ); + const allClasses = await getClasses(datasetNavObject.id, graphNavObject.id); + + graphNavObject.children = packageApiObject.internalPackageList + .map(pack => buildPackageNavEntry(pack, false)) + .concat( + packageApiObject.externalPackageList.map(pack => + buildPackageNavEntry(pack, true), + ), + ) + .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); + }); + + for (const packageNavEntry of graphNavObject.children) { + const prev = existingPackageList?.find( + p => p.id === packageNavEntry.id, + ); + packageNavEntry.isOpen = prev?.isOpen ?? false; + populatePackage(packageNavEntry, allClasses); + } + return graphNavObject; +} + +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 buildPackageNavEntry(packObj, isExternal) { + const dataObj = { + uuid: packObj.uuid, + prefix: packObj.prefix, + label: packObj.label, + comment: packObj.comment, + external: isExternal, + }; + return new NavEntry({ + id: packObj.uuid, + tooltip: packObj.prefix + packObj.label, + label: packObj.label, + data: dataObj, + }); +} + +/** + * Hier wird ein Package mit seinen Klassen gefüllt + * @param datasetNavObject + * @param GraphNavObject + * @param packageNavObject + * @returns + */ +function populatePackage(packageNavObject, allClasses) { + //TODO: contents of the default package are currently not loaded + const packageUuid = packageNavObject.data?.uuid ?? packageNavObject.id; + packageNavObject.children = allClasses + .filter(cls => cls.package?.uuid === packageUuid) + .map(cls => buildClassNavEntry(cls)) + .sort((a, b) => a.label.localeCompare(b.label)); + + return packageNavObject; +} + +function buildClassNavEntry(cls) { + return new NavEntry({ + 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, + }, + }); +} + +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..7ba89906 100644 --- a/frontend/src/routes/mainpage/packageNavigation/ontology-editor-dialog/OntologyDialog.svelte +++ b/frontend/src/routes/mainpage/packageNavigation/ontology-editor-dialog/OntologyDialog.svelte @@ -37,6 +37,7 @@ showDialog = $bindable(), dataset, graphUri, + namespaces, ontology = $bindable(), readonly, } = $props(); @@ -46,7 +47,6 @@ let showAddKnownEntriesPopUp = $state(false); let showDiscardSaveConfirmDialog = $state(false); - let namespaces = $state([]); let tableContainerRef = $state(null); let ontologyObject = $state(); @@ -58,7 +58,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 { @@ -125,7 +127,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..562a51c6 100644 --- a/frontend/src/routes/mainpage/packageNavigation/packageNavigation.svelte +++ b/frontend/src/routes/mainpage/packageNavigation/packageNavigation.svelte @@ -20,52 +20,30 @@ faDiagramProject, faFileImport, } from "@fortawesome/free-solid-svg-icons"; + import { 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 { 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([]); 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; - } + });
@@ -80,17 +58,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..e8244b75 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) { + if (typeof pack === "string") { + return pack; + } return pack?.uuid ?? "default"; } From 6a49afc0b5a142118f08695bca8697941b849aef Mon Sep 17 00:00:00 2001 From: Maximilian Date: Mon, 30 Mar 2026 10:23:25 +0200 Subject: [PATCH 2/7] fixed bugs where: - default packages were always shown as empty - no reload happened on enable/disable editing also added hotrelaod for undo/redo --- .../packageNavigation/DatasetSection.svelte | 12 +++--- .../packageNavigation/GraphSection.svelte | 25 ++++++++----- .../packageNavigation/build-nav-object.js | 37 +++++-------------- .../packageNavigationUtils.svelte.js | 2 +- 4 files changed, 31 insertions(+), 45 deletions(-) diff --git a/frontend/src/routes/mainpage/packageNavigation/DatasetSection.svelte b/frontend/src/routes/mainpage/packageNavigation/DatasetSection.svelte index c48447f4..88f185c4 100644 --- a/frontend/src/routes/mainpage/packageNavigation/DatasetSection.svelte +++ b/frontend/src/routes/mainpage/packageNavigation/DatasetSection.svelte @@ -93,21 +93,21 @@ } async function enableEditing() { - if (!datasetNavEntry?.label || !readonly) { + if (!datasetNavEntry?.id || !readonly) { return; } - await bec.enableEditing(datasetNavEntry.label).then(() => { - readonly = true; + await bec.enableEditing(datasetNavEntry.id).then(() => { + readonly = false; }); } async function disableEditing() { - if (!datasetNavEntry?.label || readonly) { + if (!datasetNavEntry?.id || readonly) { return; } - await bec.disableEditing(datasetNavEntry.label).then(() => { - readonly = false; + await bec.disableEditing(datasetNavEntry.id).then(() => { + readonly = true; }); } diff --git a/frontend/src/routes/mainpage/packageNavigation/GraphSection.svelte b/frontend/src/routes/mainpage/packageNavigation/GraphSection.svelte index d117dd06..aa16c781 100644 --- a/frontend/src/routes/mainpage/packageNavigation/GraphSection.svelte +++ b/frontend/src/routes/mainpage/packageNavigation/GraphSection.svelte @@ -52,6 +52,7 @@ } from "$lib/sharedState.svelte.js"; import { shortenIri } from "$lib/utils/iri.js"; + import { populateGraph } from "./build-nav-object.js"; import PackageButton from "./PackageButton.svelte"; import { isSelectedGraph } from "./packageNavigationUtils.svelte.js"; import CompareDialog from "../../compare/CompareDialog.svelte"; @@ -93,11 +94,14 @@ ); onMount(async () => { + await initialize(); + }); + + async function initialize() { ontology = await getOntology(); canUndo = await fetchCanUndo(datasetNavEntry.id, graphNavEntry.id); canRedo = await fetchCanRedo(datasetNavEntry.id, graphNavEntry.id); - }); - + } async function getOntology() { const res = await bec.getOntology( datasetNavEntry.label, @@ -136,12 +140,13 @@ } } - function triggerReload() { - editorState.selectedDataset.trigger(); - editorState.selectedGraph.trigger(); - editorState.selectedClassUUID.trigger(); - forceReloadTrigger.trigger(); - console.log("triggered reload from graph section"); + async function hotReload() { + await populateGraph( + datasetNavEntry, + graphNavEntry, + graphNavEntry.children, + ); + await initialize(); } @@ -180,7 +185,7 @@ onSelect={() => { focusGraphContext(); undo(datasetNavEntry.id, graphNavEntry.id).then(success => { - if (success) triggerReload(); //TODO: Hier könnte ich doch lokal reloaden, weil ich weiß, dass die aktion sich nur auf graph und drunter auswirkt + if (success) hotReload(); }); }} disabled={readonly || !canUndo} @@ -192,7 +197,7 @@ onSelect={() => { focusGraphContext(); redo(datasetNavEntry.id, graphNavEntry.id).then(success => { - if (success) triggerReload(); + if (success) hotReload(); }); }} disabled={readonly || !canRedo} diff --git a/frontend/src/routes/mainpage/packageNavigation/build-nav-object.js b/frontend/src/routes/mainpage/packageNavigation/build-nav-object.js index 80ed7f38..18381381 100644 --- a/frontend/src/routes/mainpage/packageNavigation/build-nav-object.js +++ b/frontend/src/routes/mainpage/packageNavigation/build-nav-object.js @@ -20,7 +20,7 @@ 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 } from "./new/packageNavigationUtils.svelte.js"; +import { getUri } from "./packageNavigationUtils.svelte.js"; const bec = new BackendConnection(fetch, PUBLIC_BACKEND_URL); @@ -63,20 +63,16 @@ async function getDatasetNames() { } } -/** - * Hier wird das angegebe Dataset mit Graphen gefüllt - * Falls bereits Graphen existieren, werden beinhaltete flags übernommen, damit die Navigation im gleichen openstate bleibt - * @returns {Promise} - */ -export async function populateDataset(datasetNavEntry, existingGraphNavList) { +async function populateDataset(datasetNavEntry, existingGraphNavList) { datasetNavEntry.children = (await getGraphNames(datasetNavEntry.id)) .sort((a, b) => getUri(a).localeCompare(getUri(b))) // change this to sort by suffix if wanted .map(uri => { - const newUri = new URI(getUri(uri)); + const fullUri = getUri(uri); + const newUri = new URI(fullUri); const argumentObject = { label: newUri.suffix, - tooltip: newUri.prefix + newUri.suffix, - id: newUri.prefix + newUri.suffix, + tooltip: fullUri, + id: fullUri, }; return new NavEntry(argumentObject); }); @@ -106,12 +102,6 @@ async function getGraphNames(datasetName) { } } -/** - * hier werden graphen des angegeben datasets mit Packages gefüllt. - * Falls bereits Packages existieren, werden beinhaltete flags übernommen, damit die Navigation im gleichen openstate bleibt - * @param graph - * @returns {Promise} - */ export async function populateGraph( datasetNavObject, graphNavObject, @@ -167,32 +157,23 @@ async function getPackages(datasetName, graphURI) { function buildPackageNavEntry(packObj, isExternal) { const dataObj = { - uuid: packObj.uuid, + uuid: packObj.uuid ? packObj.uuid : "default", prefix: packObj.prefix, label: packObj.label, comment: packObj.comment, external: isExternal, }; return new NavEntry({ - id: packObj.uuid, + id: packObj.uuid ? packObj.uuid : "default", tooltip: packObj.prefix + packObj.label, label: packObj.label, data: dataObj, }); } -/** - * Hier wird ein Package mit seinen Klassen gefüllt - * @param datasetNavObject - * @param GraphNavObject - * @param packageNavObject - * @returns - */ function populatePackage(packageNavObject, allClasses) { - //TODO: contents of the default package are currently not loaded - const packageUuid = packageNavObject.data?.uuid ?? packageNavObject.id; packageNavObject.children = allClasses - .filter(cls => cls.package?.uuid === packageUuid) + .filter(cls => packageNavObject.id === (cls.package?.uuid ?? "default")) .map(cls => buildClassNavEntry(cls)) .sort((a, b) => a.label.localeCompare(b.label)); diff --git a/frontend/src/routes/mainpage/packageNavigation/packageNavigationUtils.svelte.js b/frontend/src/routes/mainpage/packageNavigation/packageNavigationUtils.svelte.js index e8244b75..0375d2ed 100644 --- a/frontend/src/routes/mainpage/packageNavigation/packageNavigationUtils.svelte.js +++ b/frontend/src/routes/mainpage/packageNavigation/packageNavigationUtils.svelte.js @@ -59,7 +59,7 @@ export function getUri(resource) { return uri.prefix ? uri.prefix + uri.suffix : uri.suffix; } -export function getPackageId(pack) { +function getPackageId(pack) { if (typeof pack === "string") { return pack; } From a4396b24fdd9a6c0a3119854f4cc77fa4232c2ba Mon Sep 17 00:00:00 2001 From: Maximilian Date: Mon, 30 Mar 2026 13:27:59 +0200 Subject: [PATCH 3/7] fixed bug, where the package editor didn't work fixed bug, where the ontology editor didn't reload properly --- .../mainpage/packageEditorDialog.svelte | 26 +++++++++++++++++-- .../packageNavigation/GraphSection.svelte | 8 +++--- .../packageNavigation/PackageButton.svelte | 5 ++-- .../OntologyDialog.svelte | 11 +++++--- 4 files changed, 37 insertions(+), 13 deletions(-) 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/GraphSection.svelte b/frontend/src/routes/mainpage/packageNavigation/GraphSection.svelte index aa16c781..1c20b8ba 100644 --- a/frontend/src/routes/mainpage/packageNavigation/GraphSection.svelte +++ b/frontend/src/routes/mainpage/packageNavigation/GraphSection.svelte @@ -46,10 +46,7 @@ import { ContextMenu } from "$lib/components/bitsui/contextmenu"; import NavigationEntry from "$lib/components/navigation/NavigationEntry.svelte"; import { PUBLIC_BACKEND_URL } from "$lib/config/runtime"; - import { - editorState, - forceReloadTrigger, - } from "$lib/sharedState.svelte.js"; + import { editorState } from "$lib/sharedState.svelte.js"; import { shortenIri } from "$lib/utils/iri.js"; import { populateGraph } from "./build-nav-object.js"; @@ -222,7 +219,7 @@ datasetNavEntry.id, graphNavEntry.id, ); - forceReloadTrigger.trigger(); + initialize(); }} variant="danger" faIcon={faTrash} @@ -388,4 +385,5 @@ {namespaces} bind:ontology {readonly} + onSubmit={initialize} /> diff --git a/frontend/src/routes/mainpage/packageNavigation/PackageButton.svelte b/frontend/src/routes/mainpage/packageNavigation/PackageButton.svelte index 4a833680..cbf2df91 100644 --- a/frontend/src/routes/mainpage/packageNavigation/PackageButton.svelte +++ b/frontend/src/routes/mainpage/packageNavigation/PackageButton.svelte @@ -193,12 +193,11 @@ - Date: Mon, 30 Mar 2026 14:58:23 +0200 Subject: [PATCH 4/7] fixed rebase and update bugs --- .../DatasetDeleteDialog.svelte | 2 +- frontend/src/routes/DeleteGraphDialog.svelte | 103 --------- frontend/src/routes/NamespacesDialog.svelte | 3 - .../src/routes/layout/menu-bar/Edit.svelte | 2 +- .../src/routes/layout/menu-bar/File.svelte | 1 + .../packageNavigation/DatasetSection.svelte | 11 +- .../packageNavigation/GraphSection.svelte | 1 - .../packageNavigation/PackageButton.svelte | 8 +- .../packageNavigation/build-nav-object.js | 217 ++++++++++-------- 9 files changed, 139 insertions(+), 209 deletions(-) rename frontend/src/routes/{mainpage/packageNavigation => }/DatasetDeleteDialog.svelte (98%) delete mode 100644 frontend/src/routes/DeleteGraphDialog.svelte 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/DeleteGraphDialog.svelte b/frontend/src/routes/DeleteGraphDialog.svelte deleted file mode 100644 index eb087a0e..00000000 --- a/frontend/src/routes/DeleteGraphDialog.svelte +++ /dev/null @@ -1,103 +0,0 @@ - - - - - -
- - {#if disableSubmit} -

- Select a dataset and graph before deleting. -

- {/if} -
-
- -
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 59405b44..34ed72bc 100644 --- a/frontend/src/routes/layout/menu-bar/File.svelte +++ b/frontend/src/routes/layout/menu-bar/File.svelte @@ -28,6 +28,7 @@ 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"; diff --git a/frontend/src/routes/mainpage/packageNavigation/DatasetSection.svelte b/frontend/src/routes/mainpage/packageNavigation/DatasetSection.svelte index 88f185c4..34faa327 100644 --- a/frontend/src/routes/mainpage/packageNavigation/DatasetSection.svelte +++ b/frontend/src/routes/mainpage/packageNavigation/DatasetSection.svelte @@ -26,18 +26,18 @@ faLock, faDiagramProject, } from "@fortawesome/free-solid-svg-icons"; - import { onMount } 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 { forceReloadTrigger } from "$lib/sharedState.svelte.js"; import { editorState } from "$lib/sharedState.svelte.js"; import GraphSection from "./GraphSection.svelte"; import { isSelectedDataset } from "./packageNavigationUtils.svelte.js"; - import DeleteDatasetDialog from "../../DeleteDatasetDialog.svelte"; + import DatasetDeleteDialog from "../../DatasetDeleteDialog.svelte"; import ImportDialog from "../../ImportDialog.svelte"; import NamespacesDialog from "../../NamespacesDialog.svelte"; import NewGraphDialog from "../../NewGraphDialog.svelte"; @@ -55,7 +55,8 @@ let readonly = $state(false); let namespaces = $state([]); - onMount(async () => { + $effect(async () => { + forceReloadTrigger.subscribe(); readonly = await isReadOnly(datasetNavEntry.label); await fetchNamespaces(); }); @@ -99,6 +100,7 @@ await bec.enableEditing(datasetNavEntry.id).then(() => { readonly = false; + forceReloadTrigger.trigger(); }); } @@ -108,6 +110,7 @@ } await bec.disableEditing(datasetNavEntry.id).then(() => { readonly = true; + forceReloadTrigger.trigger(); }); } @@ -231,7 +234,7 @@ lockedDatasetName={datasetNavEntry.label} /> - diff --git a/frontend/src/routes/mainpage/packageNavigation/GraphSection.svelte b/frontend/src/routes/mainpage/packageNavigation/GraphSection.svelte index 1c20b8ba..8d7b96d0 100644 --- a/frontend/src/routes/mainpage/packageNavigation/GraphSection.svelte +++ b/frontend/src/routes/mainpage/packageNavigation/GraphSection.svelte @@ -53,7 +53,6 @@ import PackageButton from "./PackageButton.svelte"; import { isSelectedGraph } from "./packageNavigationUtils.svelte.js"; import CompareDialog from "../../compare/CompareDialog.svelte"; - import DeleteGraphDialog from "../../DeleteGraphDialog.svelte"; import ExportDialog from "../../ExportDialog.svelte"; import GraphDeleteDialog from "../../GraphDeleteDialog.svelte"; import NewPackageDialog from "../../NewPackageDialog.svelte"; diff --git a/frontend/src/routes/mainpage/packageNavigation/PackageButton.svelte b/frontend/src/routes/mainpage/packageNavigation/PackageButton.svelte index cbf2df91..cf97c3d0 100644 --- a/frontend/src/routes/mainpage/packageNavigation/PackageButton.svelte +++ b/frontend/src/routes/mainpage/packageNavigation/PackageButton.svelte @@ -57,7 +57,6 @@ let isProtectedPackage = $derived( packageNavEntry?.id === "default" || packageNavEntry?.data.external, ); - // Ensure selection-dependent UI updates without remounting the component. const selectionTrigger = $derived([ editorState.selectedDataset.subscribe(), @@ -85,6 +84,13 @@ ); const hasClasses = $derived(packageNavEntry?.children?.length > 0); + $effect(() => { + if (selectionTrigger) { + packageNavEntry.isOpen = + packageNavEntry.isOpen || isPackageSelected; + } + }); + function copyDatasetUrl() { const params = new URLSearchParams({ dataset: datasetNavEntry.id, diff --git a/frontend/src/routes/mainpage/packageNavigation/build-nav-object.js b/frontend/src/routes/mainpage/packageNavigation/build-nav-object.js index 18381381..c097a1e6 100644 --- a/frontend/src/routes/mainpage/packageNavigation/build-nav-object.js +++ b/frontend/src/routes/mainpage/packageNavigation/build-nav-object.js @@ -25,32 +25,51 @@ import { getUri } from "./packageNavigationUtils.svelte.js"; const bec = new BackendConnection(fetch, PUBLIC_BACKEND_URL); /** - * Hier werden alle datasets geladen. - * Falls bereits ein Dataset bereits existiert, werden beinhaltete flags übernommen, damit die Navigation im gleichen openstate bleibt - * @param existingDatasetNavList + * @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. + * @param {NavEntry[]} targetArray + * @param {NavEntry[]} freshEntries + */ +function syncList(targetArray, freshEntries) { + targetArray.length = 0; + targetArray.push(...freshEntries); +} + +/** + * @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 newDatasetNavList = (await getDatasetNames()) + const freshEntries = (await getDatasetNames()) .sort((a, b) => a.localeCompare(b)) - .map(label => new NavEntry({ label, id: label })); - for (const newDatasetNavEntry of newDatasetNavList) { - const existingDatasetNavEntry = existingDatasetNavList?.find( - existingNavEntry => existingNavEntry.id === newDatasetNavEntry.id, - ); - newDatasetNavEntry.isOpen = existingDatasetNavEntry?.isOpen ?? false; - await populateDataset( - newDatasetNavEntry, - existingDatasetNavEntry?.children, + .map(label => + reuseOrCreate(existingDatasetNavList, { label, id: label }), ); + + const result = existingDatasetNavList ?? []; + syncList(result, freshEntries); + + for (const datasetNavEntry of result) { + await populateDataset(datasetNavEntry); } - console.log("finished Building navObj: ", newDatasetNavList); - return newDatasetNavList; + return result; } async function getDatasetNames() { @@ -63,29 +82,28 @@ async function getDatasetNames() { } } -async function populateDataset(datasetNavEntry, existingGraphNavList) { - datasetNavEntry.children = (await getGraphNames(datasetNavEntry.id)) - .sort((a, b) => getUri(a).localeCompare(getUri(b))) // change this to sort by suffix if wanted +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); - const newUri = new URI(fullUri); - const argumentObject = { - label: newUri.suffix, + return reuseOrCreate(existingGraphNavList, { + label: new URI(fullUri).suffix, tooltip: fullUri, id: fullUri, - }; - return new NavEntry(argumentObject); + }); }); + + if (datasetNavEntry.children) { + syncList(datasetNavEntry.children, freshEntries); + } else { + datasetNavEntry.children = freshEntries; + } + for (const graphNavEntry of datasetNavEntry.children) { - const existingGraph = existingGraphNavList?.find( - g => g.id === graphNavEntry.id, - ); - graphNavEntry.isOpen = existingGraph?.isOpen ?? false; - await populateGraph( - datasetNavEntry, - graphNavEntry, - existingGraph?.children, - ); + await populateGraph(datasetNavEntry, graphNavEntry); } } @@ -102,40 +120,62 @@ async function getGraphNames(datasetName) { } } -export async function populateGraph( - datasetNavObject, - graphNavObject, - existingPackageList, -) { +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); - graphNavObject.children = packageApiObject.internalPackageList - .map(pack => buildPackageNavEntry(pack, false)) - .concat( - packageApiObject.externalPackageList.map(pack => - buildPackageNavEntry(pack, true), - ), - ) - .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); - }); + 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); + } else { + graphNavObject.children = freshEntries; + } for (const packageNavEntry of graphNavObject.children) { - const prev = existingPackageList?.find( - p => p.id === packageNavEntry.id, - ); - packageNavEntry.isOpen = prev?.isOpen ?? false; populatePackage(packageNavEntry, allClasses); } 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); @@ -148,50 +188,37 @@ async function getPackages(datasetName, graphURI) { graphURI, err, ); - return { - internalPackageList: [], - externalPackageList: [], - }; + return { internalPackageList: [], externalPackageList: [] }; } } -function buildPackageNavEntry(packObj, isExternal) { - const dataObj = { - uuid: packObj.uuid ? packObj.uuid : "default", - prefix: packObj.prefix, - label: packObj.label, - comment: packObj.comment, - external: isExternal, - }; - return new NavEntry({ - id: packObj.uuid ? packObj.uuid : "default", - tooltip: packObj.prefix + packObj.label, - label: packObj.label, - data: dataObj, - }); -} - function populatePackage(packageNavObject, allClasses) { - packageNavObject.children = allClasses + const existingClassList = packageNavObject.children; + + const freshEntries = allClasses .filter(cls => packageNavObject.id === (cls.package?.uuid ?? "default")) - .map(cls => buildClassNavEntry(cls)) - .sort((a, b) => a.label.localeCompare(b.label)); + .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, + }, + }), + ); - return packageNavObject; -} + if (packageNavObject.children) { + syncList(packageNavObject.children, freshEntries); + } else { + packageNavObject.children = freshEntries; + } -function buildClassNavEntry(cls) { - return new NavEntry({ - 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, - }, - }); + return packageNavObject; } async function getClasses(datasetName, graphURI) { From ab8ba203b7dae7115528264878373516121ce343 Mon Sep 17 00:00:00 2001 From: Maximilian Date: Mon, 30 Mar 2026 15:58:59 +0200 Subject: [PATCH 5/7] fixed another reload bug --- .../src/lib/models/nav/NavEntry.svelte.js | 65 +++++-------------- .../packageNavigation/DatasetSection.svelte | 30 +++++---- .../packageNavigation/GraphSection.svelte | 38 +++++------ .../packageNavigation/PackageButton.svelte | 29 ++++----- .../packageNavigation/build-nav-object.js | 58 ++++++++++++++--- .../packageNavigation.svelte | 9 ++- 6 files changed, 114 insertions(+), 115 deletions(-) diff --git a/frontend/src/lib/models/nav/NavEntry.svelte.js b/frontend/src/lib/models/nav/NavEntry.svelte.js index 7ee5a264..84c9ae11 100644 --- a/frontend/src/lib/models/nav/NavEntry.svelte.js +++ b/frontend/src/lib/models/nav/NavEntry.svelte.js @@ -19,49 +19,40 @@ export class NavEntry { label = $state(); id = $state(); tooltip = $state(); - isOpen = $state(false); + #isOpen = $state(false); children = $state([]); data = $state(null); - /** - * @type {NavEntry | null} - */ + /** @type {NavEntry | null} */ parent = $state(null); - /** - * @param {{ label?: string, id?:string, tooltip?: string, isOpen?: boolean, children?: NavEntry[], data?: any }} config - */ + /** @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.#isOpen = config.isOpen ?? false; this.id = config.id ?? this.label; this.data = config.data ?? null; - - if (config.children?.length) { - this.addChildren(config.children); - } } - /** - * Opens this entry and all parent entries - */ + /** Opens this entry and all ancestors. */ open() { - this.isOpen = true; + this.#isOpen = true; this.parent?.open(); } - /** - * Closes this entry and all children recursively - */ + /** Closes this entry and all descendants recursively. */ close() { - this.isOpen = false; + this.#isOpen = false; this.children.forEach(child => child.close()); } - /** - * Toggles the open state - */ + /** Toggles the open state. Opening trickles up, closing trickles down. */ toggle() { if (this.isOpen) { this.close(); @@ -70,32 +61,6 @@ export class NavEntry { } } - /** - * @param {NavEntry} child - */ - addChild(child) { - child.parent = this; - this.children.push(child); - } - - /** - * @param {NavEntry[]} children - */ - addChildren(children) { - children.forEach(child => this.addChild(child)); - } - - /** - * @param {NavEntry} child - */ - removeChild(child) { - child.parent = null; - this.children = this.children.filter(c => c !== child); - } - - /** - * Whether this entry has children - * @type {boolean} - */ + /** @type {boolean} */ hasChildren = $derived(this.children.length > 0); } diff --git a/frontend/src/routes/mainpage/packageNavigation/DatasetSection.svelte b/frontend/src/routes/mainpage/packageNavigation/DatasetSection.svelte index 34faa327..80deb8ac 100644 --- a/frontend/src/routes/mainpage/packageNavigation/DatasetSection.svelte +++ b/frontend/src/routes/mainpage/packageNavigation/DatasetSection.svelte @@ -26,6 +26,7 @@ faLock, faDiagramProject, } from "@fortawesome/free-solid-svg-icons"; + import { getContext } from "svelte"; import { getNamespaces, isReadOnly } from "$lib/api/apiDatasetUtils.js"; import { BackendConnection } from "$lib/api/backend.js"; @@ -55,11 +56,23 @@ let readonly = $state(false); let namespaces = $state([]); + let wasDatasetSelected = false; + + const isDatasetSelected = $derived( + isSelectedDataset(datasetNavEntry.label), + ); + $effect(async () => { - forceReloadTrigger.subscribe(); + getContext("packageNavigation").reloadTrigger?.subscribe(); readonly = await isReadOnly(datasetNavEntry.label); await fetchNamespaces(); }); + $effect(() => { + if (isDatasetSelected && !wasDatasetSelected) { + datasetNavEntry.parent?.open(); + } + wasDatasetSelected = isDatasetSelected; + }); async function fetchNamespaces() { if (!datasetNavEntry?.label) { @@ -83,16 +96,6 @@ editorState.selectedDataset.updateValue(datasetNavEntry.label); } - function toggleDatasetContentsVisibility() { - datasetNavEntry.isOpen = !datasetNavEntry.isOpen; - } - - function ensureDatasetExpanded() { - if (!datasetNavEntry?.isOpen) { - datasetNavEntry.isOpen = true; - } - } - async function enableEditing() { if (!datasetNavEntry?.id || !readonly) { return; @@ -124,12 +127,12 @@ icon={faDatabase} hasChildren={datasetNavEntry.children?.length > 0} expanded={datasetNavEntry.isOpen} - isSelected={isSelectedDataset(datasetNavEntry.label)} + isSelected={isDatasetSelected} title={datasetNavEntry.tooltip} badgeText={readonly ? "Read-only" : ""} badgeVariant="readonly" onclick={selectDataset} - onToggle={toggleDatasetContentsVisibility} + onToggle={() => datasetNavEntry.toggle()} /> @@ -212,7 +215,6 @@ diff --git a/frontend/src/routes/mainpage/packageNavigation/GraphSection.svelte b/frontend/src/routes/mainpage/packageNavigation/GraphSection.svelte index 8d7b96d0..a0c38334 100644 --- a/frontend/src/routes/mainpage/packageNavigation/GraphSection.svelte +++ b/frontend/src/routes/mainpage/packageNavigation/GraphSection.svelte @@ -66,7 +66,6 @@ let { datasetNavEntry, graphNavEntry, - onExpandDataset = () => {}, namespaces = [], readonly = false, } = $props(); @@ -85,10 +84,22 @@ let canRedo = $state(false); let showEditOntologyDialog = $state(false); + let wasGraphSelected = false; + let graphHighlightLabel = $derived( shortenIri(namespaces, graphNavEntry.id), ); + const isGraphSelected = $derived( + isSelectedGraph(datasetNavEntry.id, graphNavEntry.id), + ); + $effect(() => { + if (isGraphSelected && !wasGraphSelected) { + graphNavEntry.parent?.open(); + } + wasGraphSelected = isGraphSelected; + }); + onMount(async () => { await initialize(); }); @@ -110,17 +121,6 @@ return JSON.parse(content); } - function toggleGraphContentsVisibility() { - graphNavEntry.isOpen = !graphNavEntry.isOpen; - } - - function ensureGraphIsExpanded() { - if (!graphNavEntry?.isOpen) { - graphNavEntry.isOpen = true; - } - onExpandDataset(); - } - function focusGraphContext() { const nextDataset = datasetNavEntry.label; const nextGraph = graphNavEntry.id; @@ -137,11 +137,7 @@ } async function hotReload() { - await populateGraph( - datasetNavEntry, - graphNavEntry, - graphNavEntry.children, - ); + await populateGraph(datasetNavEntry, graphNavEntry); await initialize(); } @@ -155,14 +151,11 @@ icon={faDiagramProject} hasChildren={graphNavEntry.children.length > 0} expanded={graphNavEntry.isOpen} - isSelected={isSelectedGraph( - datasetNavEntry.id, - graphNavEntry.id, - )} + isSelected={isGraphSelected} title={graphNavEntry.tooltip} highlightLabel={graphHighlightLabel} onclick={focusGraphContext} - onToggle={toggleGraphContentsVisibility} + onToggle={() => graphNavEntry.toggle()} /> @@ -343,7 +336,6 @@ {packageNavEntry} {namespaces} {readonly} - onPackChange={ensureGraphIsExpanded} /> {/each}
diff --git a/frontend/src/routes/mainpage/packageNavigation/PackageButton.svelte b/frontend/src/routes/mainpage/packageNavigation/PackageButton.svelte index cf97c3d0..e2a7a9b6 100644 --- a/frontend/src/routes/mainpage/packageNavigation/PackageButton.svelte +++ b/frontend/src/routes/mainpage/packageNavigation/PackageButton.svelte @@ -26,13 +26,11 @@ faTrash, faEye, } from "@fortawesome/free-solid-svg-icons"; + import { getContext } from "svelte"; 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"; @@ -47,13 +45,14 @@ packageNavEntry, namespaces = [], readonly, - onPackChange = () => {}, } = $props(); let showNewClassDialog = $state(false); let showPackageEditorDialog = $state(false); let showDeletePackageDialog = $state(false); + let wasPackageSelected = false; + let isProtectedPackage = $derived( packageNavEntry?.id === "default" || packageNavEntry?.data.external, ); @@ -62,7 +61,7 @@ editorState.selectedDataset.subscribe(), editorState.selectedGraph.subscribe(), editorState.selectedPackageUUID.subscribe(), - forceReloadTrigger.subscribe(), + getContext("packageNavigation").reloadTrigger?.subscribe(), ]); let isPackageSelected = $derived( @@ -83,12 +82,11 @@ readonly ? false : isProtectedPackage, ); const hasClasses = $derived(packageNavEntry?.children?.length > 0); - $effect(() => { - if (selectionTrigger) { - packageNavEntry.isOpen = - packageNavEntry.isOpen || isPackageSelected; + if (selectionTrigger && isPackageSelected && !wasPackageSelected) { + packageNavEntry.parent?.open(); } + wasPackageSelected = isPackageSelected; }); function copyDatasetUrl() { @@ -105,10 +103,6 @@ ); } - function togglePackageContentsVisibility() { - packageNavEntry.isOpen = !packageNavEntry.isOpen; - } - function selectPackage() { editorState.selectedDataset.updateValue(datasetNavEntry.id); editorState.selectedGraph.updateValue(graphNavEntry.id); @@ -133,7 +127,7 @@ ? "external" : "default"} onclick={selectPackage} - onToggle={togglePackageContentsVisibility} + onToggle={() => packageNavEntry.toggle()} /> @@ -183,7 +177,6 @@ {classNavEntry} {namespaces} {readonly} - {onPackChange} /> {/each}
@@ -207,7 +200,7 @@ diff --git a/frontend/src/routes/mainpage/packageNavigation/build-nav-object.js b/frontend/src/routes/mainpage/packageNavigation/build-nav-object.js index c097a1e6..51bde250 100644 --- a/frontend/src/routes/mainpage/packageNavigation/build-nav-object.js +++ b/frontend/src/routes/mainpage/packageNavigation/build-nav-object.js @@ -20,7 +20,12 @@ 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 } from "./packageNavigationUtils.svelte.js"; +import { + getUri, + isSelectedGraph, + isSelectedPackage, + isSelectedClass, +} from "./packageNavigationUtils.svelte.js"; const bec = new BackendConnection(fetch, PUBLIC_BACKEND_URL); @@ -42,13 +47,15 @@ function reuseOrCreate(existingList, props) { } /** - * @description Replaces the contents of targetArray in place, keeping the array reference. + * @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) { +function syncList(targetArray, freshEntries, parent = null) { targetArray.length = 0; targetArray.push(...freshEntries); + freshEntries.forEach(entry => (entry.parent = parent)); } /** @@ -57,6 +64,11 @@ function syncList(targetArray, freshEntries) { * @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 => @@ -64,11 +76,13 @@ export async function getNavEntryList(existingDatasetNavList) { ); const result = existingDatasetNavList ?? []; - syncList(result, freshEntries); + syncList(result, freshEntries, null); for (const datasetNavEntry of result) { await populateDataset(datasetNavEntry); } + + console.log("finished Building navObj: ", result); return result; } @@ -97,12 +111,16 @@ async function populateDataset(datasetNavEntry) { }); if (datasetNavEntry.children) { - syncList(datasetNavEntry.children, freshEntries); + 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); } } @@ -142,13 +160,28 @@ export async function populateGraph(datasetNavObject, graphNavObject) { }); if (graphNavObject.children) { - syncList(graphNavObject.children, freshEntries); + syncList(graphNavObject.children, freshEntries, graphNavObject); } else { graphNavObject.children = freshEntries; + freshEntries.forEach(entry => (entry.parent = graphNavObject)); } for (const packageNavEntry of graphNavObject.children) { - populatePackage(packageNavEntry, allClasses); + if ( + isSelectedPackage( + datasetNavObject.id, + graphNavObject.id, + packageNavEntry.id, + ) + ) { + packageNavEntry.parent?.open(); + } + populatePackage( + packageNavEntry, + allClasses, + datasetNavObject.id, + graphNavObject.id, + ); } return graphNavObject; } @@ -192,7 +225,7 @@ async function getPackages(datasetName, graphURI) { } } -function populatePackage(packageNavObject, allClasses) { +function populatePackage(packageNavObject, allClasses, datasetId, graphId) { const existingClassList = packageNavObject.children; const freshEntries = allClasses @@ -213,9 +246,16 @@ function populatePackage(packageNavObject, allClasses) { ); if (packageNavObject.children) { - syncList(packageNavObject.children, freshEntries); + 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; diff --git a/frontend/src/routes/mainpage/packageNavigation/packageNavigation.svelte b/frontend/src/routes/mainpage/packageNavigation/packageNavigation.svelte index 562a51c6..d7d0d518 100644 --- a/frontend/src/routes/mainpage/packageNavigation/packageNavigation.svelte +++ b/frontend/src/routes/mainpage/packageNavigation/packageNavigation.svelte @@ -20,16 +20,18 @@ faDiagramProject, faFileImport, } from "@fortawesome/free-solid-svg-icons"; - import { untrack } from "svelte"; + import { setContext, untrack } from "svelte"; import { ContextMenu } from "$lib/components/bitsui/contextmenu"; 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 ImportDialog from "../../ImportDialog.svelte"; import NewGraphDialog from "../../NewGraphDialog.svelte"; + const localReloadTrigger = new SimpleTrigger(); let initialDatasetsLoaded = $state(false); let showImportDialog = $state(false); let showNewGraphDialog = $state(false); @@ -43,6 +45,11 @@ await getNavEntryList(datasetNavEntryList)), ); initialDatasetsLoaded = true; + localReloadTrigger.trigger(); + }); + + setContext("packageNavigation", { + reloadTrigger: localReloadTrigger, }); From 40932755f5eeeb3e34994f3f3b2c98776264a3b2 Mon Sep 17 00:00:00 2001 From: Maximilian Date: Tue, 7 Apr 2026 11:11:49 +0200 Subject: [PATCH 6/7] fixed bug where graph navigation section was not updated on modifying data --- .../classEditor/components/ClassEditorButtons.svelte | 1 + .../associationEditorDialog/AssociationEditorDialog.svelte | 3 +++ .../components/attributes/AttributeEditorDialog.svelte | 2 ++ .../components/enum-entries/EnumEntryEditorDialog.svelte | 3 +++ .../routes/mainpage/packageNavigation/GraphSection.svelte | 5 +++-- 5 files changed, 12 insertions(+), 2 deletions(-) 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 3d7b8a3a..21e262bc 100644 --- a/frontend/src/routes/mainpage/classEditor/components/associations/associationEditorDialog/AssociationEditorDialog.svelte +++ b/frontend/src/routes/mainpage/classEditor/components/associations/associationEditorDialog/AssociationEditorDialog.svelte @@ -21,6 +21,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"; @@ -77,6 +78,8 @@ associations.append(association); isNewAssociation = false; } + association.save(); + forceReloadTrigger.trigger(); } 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/packageNavigation/GraphSection.svelte b/frontend/src/routes/mainpage/packageNavigation/GraphSection.svelte index a0c38334..1bd236b7 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, @@ -100,7 +100,8 @@ wasGraphSelected = isGraphSelected; }); - onMount(async () => { + $effect(async () => { + getContext("packageNavigation").reloadTrigger?.subscribe(); await initialize(); }); From d4dbca9012a3204922f2aaf5add425746c49da39 Mon Sep 17 00:00:00 2001 From: Maximilian Date: Tue, 7 Apr 2026 13:00:22 +0200 Subject: [PATCH 7/7] fixed bug where undo redo didnt trigger a reload in the class editor --- .../mainpage/classEditor/classEditor.svelte | 7 ++++++- .../packageNavigation/GraphSection.svelte | 15 ++++++--------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/frontend/src/routes/mainpage/classEditor/classEditor.svelte b/frontend/src/routes/mainpage/classEditor/classEditor.svelte index 07d1b62a..4f7bc908 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, @@ -87,6 +90,7 @@ $effect(async () => { editorState.selectedClassUUID.subscribe(); + forceReloadTrigger.subscribe(); loadingContext = true; loadingClass = true; @@ -97,6 +101,7 @@ $effect(async () => { editorState.selectedPackageUUID.subscribe(); + forceReloadTrigger.subscribe(); isDatasetReadOnly = await isReadOnly(datasetName); }); diff --git a/frontend/src/routes/mainpage/packageNavigation/GraphSection.svelte b/frontend/src/routes/mainpage/packageNavigation/GraphSection.svelte index 1bd236b7..985027d8 100644 --- a/frontend/src/routes/mainpage/packageNavigation/GraphSection.svelte +++ b/frontend/src/routes/mainpage/packageNavigation/GraphSection.svelte @@ -46,10 +46,12 @@ import { ContextMenu } from "$lib/components/bitsui/contextmenu"; import NavigationEntry from "$lib/components/navigation/NavigationEntry.svelte"; import { PUBLIC_BACKEND_URL } from "$lib/config/runtime"; - import { editorState } from "$lib/sharedState.svelte.js"; + import { + editorState, + forceReloadTrigger, + } from "$lib/sharedState.svelte.js"; import { shortenIri } from "$lib/utils/iri.js"; - import { populateGraph } from "./build-nav-object.js"; import PackageButton from "./PackageButton.svelte"; import { isSelectedGraph } from "./packageNavigationUtils.svelte.js"; import CompareDialog from "../../compare/CompareDialog.svelte"; @@ -136,11 +138,6 @@ editorState.selectedPackageUUID.updateValue(null); } } - - async function hotReload() { - await populateGraph(datasetNavEntry, graphNavEntry); - await initialize(); - }
@@ -175,7 +172,7 @@ onSelect={() => { focusGraphContext(); undo(datasetNavEntry.id, graphNavEntry.id).then(success => { - if (success) hotReload(); + if (success) forceReloadTrigger.trigger(); }); }} disabled={readonly || !canUndo} @@ -187,7 +184,7 @@ onSelect={() => { focusGraphContext(); redo(datasetNavEntry.id, graphNavEntry.id).then(success => { - if (success) hotReload(); + if (success) forceReloadTrigger.trigger(); }); }} disabled={readonly || !canRedo}