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 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/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/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"; }