From 751c8340d1c0b177c9fd3ff0a606c575368ee623 Mon Sep 17 00:00:00 2001 From: Stephan Heunis Date: Tue, 24 Feb 2026 23:03:17 +0100 Subject: [PATCH] Introduce 'BackLinkViewer' component and associated config This new component allows viewing so-called backlinks for a record, i.e. other records that reference the current record via some property, directly or indirectly. Upon mounting, the component will first fetch all 'Thing' records (importantly: including subclasses) that somehow include the PID of the current record (via 'matching' parament) from the backend service. Determining which referencing records to show required a new utility function 'getReferencingRecords' which does the following: - get all quads with the current PID as object - if the quad has a named node as subject, save it as a referencing record - if the quad has a blank node as subject, get all quads with the blank node ID as object This means it recurses into the second level to find referencing records, and no further. This is a restriction that could be reconsidered and addressed in future, although local testing on actual data compliant to the xyzri schema has not shown this to be necessary. Saving a referencing record also includes saving the quad's predicate, which is taken as the referencing property, e.g. a 'Person' is referenced by a 'Dataset' via 'attributed_to'. This allows grouping referencing records by property in the 'BackLinkViewer' component. Lastly, a new config sub-option 'hideBackLinks' is introduces for the 'NodeShapeViewer's 'component_config'. An example: component_config: NodeShapeViewer: hideBackLinks: - dlthings:AnnotationTag - xyzri:AgentRole 'hideBackLinks' can be a boolean or array. It is set to 'true' by default, meaning that backlinks won't be displayed by default (i.e. when the option is not configured). If an array with class CURIEs is supplied, the back links will be displayed for records of all classes except for those included in the configured array. --- src/components/BackLinkViewer.vue | 48 ++++++ src/components/NodeShapeViewer.vue | 260 ++++++++++++++++------------- src/composables/configuration.js | 1 + src/modules/utils.js | 50 ++++++ 4 files changed, 239 insertions(+), 120 deletions(-) create mode 100644 src/components/BackLinkViewer.vue diff --git a/src/components/BackLinkViewer.vue b/src/components/BackLinkViewer.vue new file mode 100644 index 0000000..8257b6c --- /dev/null +++ b/src/components/BackLinkViewer.vue @@ -0,0 +1,48 @@ + + + \ No newline at end of file diff --git a/src/components/NodeShapeViewer.vue b/src/components/NodeShapeViewer.vue index a227caa..d5ec991 100644 --- a/src/components/NodeShapeViewer.vue +++ b/src/components/NodeShapeViewer.vue @@ -75,46 +75,12 @@ - Persistent Identifier:  {{ record.title}}
+ + + Persistent Identifier:  {{ record.title}}
- - - - - {{ - nameOrCURIE( - propertyShapes[k], - shapesDS.data.prefixes, - true - ) - }} - : - - - {{ k }}: - - - -
 -
-   -
-
-
- -
- - - - - - - - - + + {{ @@ -124,92 +90,136 @@ true ) }} - :   - - -
 - 
- - + : +
+ + {{ k }}: + + + +
 -
+   +
+
+
+ +
+ + + + + + + + + + + + {{ + nameOrCURIE( + propertyShapes[k], + shapesDS.data.prefixes, + true + ) + }} + :   + + +
 - 
+ + +
+
+
+ + {{ k }}: + + +
 -
+  {{ el.value }} +
+
+
+
+
+
+
+ + + + + {{ + nameOrCURIE( + propertyShapes[k], + shapesDS.data.prefixes, + true + ) + }} + :   + + + +  -  + + + + + + + +  -  - - {{ k }}: - - -
 -
-  {{ el.value }} -
-
-
-
-
-
- - - - - {{ - nameOrCURIE( - propertyShapes[k], - shapesDS.data.prefixes, - true - ) - }} - :   - - - -  -  - - - - - - - -  -  - - - - +
+ + + + + + +
-
+
{ fetchingRecords.value = true; await updateRecord(true); fetchingRecords.value = false; + firstUpdateDone.value = true; let recordPIDprefix = toCURIE(props.quad.subject.value, allPrefixes, 'parts').prefix; if (configVarsMain['idResolvesExternally'].indexOf(recordPIDprefix) >= 0) { resolveExternally.value = true; diff --git a/src/composables/configuration.js b/src/composables/configuration.js index 66a1a13..32ec641 100644 --- a/src/composables/configuration.js +++ b/src/composables/configuration.js @@ -52,6 +52,7 @@ const mainVarsToLoad = { NodeShapeViewer: { recordNumberStepSize: 5, textTruncateWidth: "85%", + hideBackLinks: true, }, URIEditor: { default: "curie", diff --git a/src/modules/utils.js b/src/modules/utils.js index 74c85b0..a82b0e9 100644 --- a/src/modules/utils.js +++ b/src/modules/utils.js @@ -302,6 +302,56 @@ export function getSubjectQuad(subj, graph) { } } +export function getReferencingRecords(objVal, graph) { + let referencingRecords = {}; + const firstLevelQuads = graph.getQuads( + null, + null, + namedNode(objVal), + null + ); + for (const q of firstLevelQuads) { + if (q.subject.termType === 'BlankNode') { + var secondLevelQuads = graph.getQuads( + null, + null, + q.subject, + null + ); + if (secondLevelQuads && secondLevelQuads.length) { + if (!referencingRecords.hasOwnProperty(secondLevelQuads[0].predicate.value)) { + referencingRecords[secondLevelQuads[0].predicate.value] = [] + } + referencingRecords[secondLevelQuads[0].predicate.value].push({ + record_id: secondLevelQuads[0].subject.value + }) + var sQ = getSubjectQuad(secondLevelQuads[0].subject, graph) + if (sQ) { + referencingRecords[secondLevelQuads[0].predicate.value].at(-1).class_iri = sQ.object.value + referencingRecords[secondLevelQuads[0].predicate.value].at(-1).quad = sQ + } + if (secondLevelQuads.length > 1) { + console.error("! secondLevelQuads has length > 1 !") + } + } + } else { + if (!referencingRecords.hasOwnProperty(q.predicate.value)) { + referencingRecords[q.predicate.value] = [] + } + referencingRecords[q.predicate.value].push({ + record_id: q.subject.value + }) + var sQ = getSubjectQuad(q.subject, graph) + if (sQ) { + referencingRecords[q.predicate.value].at(-1).class_iri = sQ.object.value + referencingRecords[q.predicate.value].at(-1).quad = sQ + } + + } + } + return referencingRecords +} + export function objectsEqual(obj1, obj2) { if (Object.keys(obj1).length !== Object.keys(obj2).length) { return false; -- 2.52.0