Introduce BackLinkViewer component and associated config #342
4 changed files with 235 additions and 116 deletions
48
src/components/BackLinkViewer.vue
Normal file
48
src/components/BackLinkViewer.vue
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
<template>
|
||||||
|
<h4 v-if="Object.keys(refRecords).length" style="margin-bottom: 1em;" >Records referencing <em>{{props.record.displayLabel}}</em>:</h4>
|
||||||
|
<div v-for="(arr, key) in refRecords" style="margin-bottom: 0.5em;">
|
||||||
|
<em>via <strong>{{makeReadable(toCURIE(key, allPrefixes, 'parts').property)}}</strong></em>:
|
||||||
|
<span v-for="r in arr">
|
||||||
|
<br> -
|
||||||
|
<NamedNodeViewer
|
||||||
|
:textVal="r.record_id"
|
||||||
|
:prefLabel="getPrefLabel(r.quad.subject, rdfDS, allPrefixes)"
|
||||||
|
:displayLabel="getRecordDisplayLabel(r.quad.subject, rdfDS, allPrefixes, configVarsMain)"
|
||||||
|
:quad="r.quad"
|
||||||
|
:targetClass="r.class_iri"
|
||||||
|
:allowLink="true"
|
||||||
|
>
|
||||||
|
</NamedNodeViewer>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
|
||||||
|
import { inject, onMounted, ref } from 'vue';
|
||||||
|
import { toCURIE, getReferencingRecords, getPrefLabel, getRecordDisplayLabel, makeReadable} from '@/modules/utils';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
record: Object,
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['has-referencing-records'])
|
||||||
|
|
||||||
|
const fetchFromService = inject('fetchFromService')
|
||||||
|
const allPrefixes = inject('allPrefixes')
|
||||||
|
const rdfDS = inject('rdfDS')
|
||||||
|
const configVarsMain = inject('configVarsMain')
|
||||||
|
const refRecords = ref({});
|
||||||
|
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
const arg = allPrefixes['dlthings'] + 'Thing';
|
||||||
|
await fetchFromService('get-paginated-records-constrained', arg, allPrefixes, props.record.title )
|
||||||
|
await fetchFromService('get-paginated-records-constrained', arg, allPrefixes, toCURIE(props.record.title, allPrefixes))
|
||||||
|
refRecords.value = getReferencingRecords(props.record.title, rdfDS.data.graph)
|
||||||
|
if (Object.keys(refRecords.value).length > 0) {
|
||||||
|
emit('has-referencing-records')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
@ -75,46 +75,12 @@
|
||||||
</span>
|
</span>
|
||||||
</v-card-subtitle>
|
</v-card-subtitle>
|
||||||
<v-card-text v-if="!props.formOpen" :class="mobile ? 'text-caption' : ''">
|
<v-card-text v-if="!props.formOpen" :class="mobile ? 'text-caption' : ''">
|
||||||
<strong>Persistent Identifier</strong>: {{ record.title}}<br/>
|
<v-row align="stretch">
|
||||||
|
<v-col :cols="showBackLinks ? 6 : 12">
|
||||||
|
<strong>Persistent Identifier</strong>: {{ record.title}}<br/>
|
||||||
|
|
||||||
<!-- Literal nodes -->
|
<!-- Literal nodes -->
|
||||||
<span v-for="(v, k, index) in record.triples['Literal']">
|
<span v-for="(v, k, index) in record.triples['Literal']">
|
||||||
<span v-if="propertyShapes[k]">
|
|
||||||
<strong>
|
|
||||||
{{
|
|
||||||
nameOrCURIE(
|
|
||||||
propertyShapes[k],
|
|
||||||
shapesDS.data.prefixes,
|
|
||||||
true
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</strong>:
|
|
||||||
</span>
|
|
||||||
<span v-else>
|
|
||||||
<strong>{{ k }}</strong>:
|
|
||||||
</span>
|
|
||||||
<span v-for="(el, i) in v.values">
|
|
||||||
<span v-if="i < showCounts['Literal'][k]">
|
|
||||||
<span v-if="v.values.length > 1"><br/> -</span>
|
|
||||||
<LiteralNodeViewer v-if="el.value" :textVal="el.value" :wrap="textWrapping" :width="textTruncateWidth"></LiteralNodeViewer>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
<br/>
|
|
||||||
<MoreOrLessRecordsViewer
|
|
||||||
:records="v.values"
|
|
||||||
v-model:count="showCounts['Literal'][k]"
|
|
||||||
:stepSize="defaultStep"
|
|
||||||
></MoreOrLessRecordsViewer>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
|
|
||||||
<!-- Named nodes -->
|
|
||||||
<span v-if="fetchingRecords">
|
|
||||||
<v-skeleton-loader type="paragraph"></v-skeleton-loader>
|
|
||||||
</span>
|
|
||||||
<span v-else>
|
|
||||||
<span v-for="(v, k, index) in record.triples['NamedNode']">
|
|
||||||
<span v-if="k != RDF.type.value">
|
|
||||||
<span v-if="propertyShapes[k]">
|
<span v-if="propertyShapes[k]">
|
||||||
<strong>
|
<strong>
|
||||||
{{
|
{{
|
||||||
|
|
@ -124,92 +90,136 @@
|
||||||
true
|
true
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
</strong>: <MoreOrLessRecordsViewer
|
</strong>:
|
||||||
:records="v.values"
|
</span>
|
||||||
v-model:count="showCounts['NamedNode'][k]"
|
<span v-else>
|
||||||
:stepSize="defaultStep"
|
<strong>{{ k }}</strong>:
|
||||||
></MoreOrLessRecordsViewer>
|
</span>
|
||||||
<span v-for="(el, i) in v.values">
|
<span v-for="(el, i) in v.values">
|
||||||
<span v-if="i < showCounts['NamedNode'][k]">
|
<span v-if="i < showCounts['Literal'][k]">
|
||||||
<span v-if="v.values.length > 1"><br /> - </span>
|
<span v-if="v.values.length > 1"><br/> -</span>
|
||||||
<NamedNodeViewer
|
<LiteralNodeViewer v-if="el.value" :textVal="el.value" :wrap="textWrapping" :width="textTruncateWidth"></LiteralNodeViewer>
|
||||||
v-if="el.value"
|
</span>
|
||||||
:textVal="el.value"
|
</span>
|
||||||
:prefLabel="
|
<br/>
|
||||||
getPrefLabel(el, rdfDS, allPrefixes)
|
<MoreOrLessRecordsViewer
|
||||||
"
|
:records="v.values"
|
||||||
:displayLabel="
|
v-model:count="showCounts['Literal'][k]"
|
||||||
getRecordDisplayLabel(el, rdfDS, allPrefixes, configVarsMain)
|
:stepSize="defaultStep"
|
||||||
"
|
></MoreOrLessRecordsViewer>
|
||||||
:quad="
|
</span>
|
||||||
getPidQuad(el.value, rdfDS.data.graph)
|
|
||||||
"
|
|
||||||
:targetClass="
|
<!-- Named nodes -->
|
||||||
propertyShapes[k][SHACL.class.value]
|
<span v-if="fetchingRecords">
|
||||||
"
|
<v-skeleton-loader type="paragraph"></v-skeleton-loader>
|
||||||
>
|
</span>
|
||||||
</NamedNodeViewer>
|
<span v-else>
|
||||||
|
<span v-for="(v, k, index) in record.triples['NamedNode']">
|
||||||
|
<span v-if="k != RDF.type.value">
|
||||||
|
<span v-if="propertyShapes[k]">
|
||||||
|
<strong>
|
||||||
|
{{
|
||||||
|
nameOrCURIE(
|
||||||
|
propertyShapes[k],
|
||||||
|
shapesDS.data.prefixes,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</strong>: <MoreOrLessRecordsViewer
|
||||||
|
:records="v.values"
|
||||||
|
v-model:count="showCounts['NamedNode'][k]"
|
||||||
|
:stepSize="defaultStep"
|
||||||
|
></MoreOrLessRecordsViewer>
|
||||||
|
<span v-for="(el, i) in v.values">
|
||||||
|
<span v-if="i < showCounts['NamedNode'][k]">
|
||||||
|
<span v-if="v.values.length > 1"><br /> - </span>
|
||||||
|
<NamedNodeViewer
|
||||||
|
v-if="el.value"
|
||||||
|
:textVal="el.value"
|
||||||
|
:prefLabel="
|
||||||
|
getPrefLabel(el, rdfDS, allPrefixes)
|
||||||
|
"
|
||||||
|
:displayLabel="
|
||||||
|
getRecordDisplayLabel(el, rdfDS, allPrefixes, configVarsMain)
|
||||||
|
"
|
||||||
|
:quad="
|
||||||
|
getPidQuad(el.value, rdfDS.data.graph)
|
||||||
|
"
|
||||||
|
:targetClass="
|
||||||
|
propertyShapes[k][SHACL.class.value]
|
||||||
|
"
|
||||||
|
>
|
||||||
|
</NamedNodeViewer>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span v-else>
|
||||||
|
<strong>{{ k }}</strong
|
||||||
|
>:
|
||||||
|
<span v-for="(el, i) in v.values">
|
||||||
|
<span v-if="i < showCounts['NamedNode'][k]">
|
||||||
|
<span v-if="v.values.length > 1"><br /> -</span>
|
||||||
|
{{ el.value }}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<br>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<!-- Now show all blank nodes for which a display label has been configured, which makes them special-->
|
||||||
|
<!-- TODO: how do we deal with preflabel here ??? -->
|
||||||
|
<span v-for="(v,k) in specialBlankNodes">
|
||||||
|
<strong>
|
||||||
|
{{
|
||||||
|
nameOrCURIE(
|
||||||
|
propertyShapes[k],
|
||||||
|
shapesDS.data.prefixes,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</strong>: <MoreOrLessRecordsViewer
|
||||||
|
:records="v.items.map(i => i.value)"
|
||||||
|
v-model:count="showCounts['BlankNodeSpecial'][k]"
|
||||||
|
:stepSize="defaultStep"
|
||||||
|
></MoreOrLessRecordsViewer>
|
||||||
|
<span v-for="(item, i) in v.items">
|
||||||
|
<span v-if="i < showCounts['BlankNodeSpecial'][k]" class="line-item">
|
||||||
|
<span v-if="item.keyPropertyRole?.classIRI && item.keyPropertyRole?.recordPID">
|
||||||
|
-
|
||||||
|
<v-tooltip location="top start">
|
||||||
|
<template v-slot:activator="{ props }">
|
||||||
|
<a
|
||||||
|
v-bind="props"
|
||||||
|
style="cursor: pointer"
|
||||||
|
@click.prevent="selectNamedNode(item.keyPropertyRole.classIRI, item.keyPropertyRole.recordPID)"
|
||||||
|
>{{ item.displayLabel }}</a
|
||||||
|
>
|
||||||
|
</template>
|
||||||
|
<template v-slot:default="{ isActive }">
|
||||||
|
<v-icon >{{ getClassIcon(item.keyPropertyRole.classIRI, allPrefixes) }}</v-icon>
|
||||||
|
{{ getDisplayName(item.keyPropertyRole.classIRI, configVarsMain, allPrefixes, shapesDS.data.nodeShapes[item.keyPropertyRole.classIRI]) }}
|
||||||
|
</template>
|
||||||
|
</v-tooltip>
|
||||||
|
|
||||||
|
</span>
|
||||||
|
<span v-else>
|
||||||
|
- <LiteralNodeViewer :textVal="item.displayLabel" :wrap="'wrap'" :allowLink="false"></LiteralNodeViewer>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<span v-else>
|
|
||||||
<strong>{{ k }}</strong
|
|
||||||
>:
|
|
||||||
<span v-for="(el, i) in v.values">
|
|
||||||
<span v-if="i < showCounts['NamedNode'][k]">
|
|
||||||
<span v-if="v.values.length > 1"><br /> -</span>
|
|
||||||
{{ el.value }}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
<br>
|
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</v-col>
|
||||||
</span>
|
<v-col v-if="showBackLinks && !hideBackLinks" cols="auto" class="d-flex">
|
||||||
<!-- Now show all blank nodes for which a display label has been configured, which makes them special-->
|
<v-divider vertical />
|
||||||
<!-- TODO: how do we deal with preflabel here ??? -->
|
</v-col>
|
||||||
<span v-for="(v,k) in specialBlankNodes">
|
<v-col>
|
||||||
<strong>
|
<BackLinkViewer v-if="firstUpdateDone && !hideBackLinks" v-show="showBackLinks" :record="record" @has-referencing-records="showBackLinks = true"></BackLinkViewer>
|
||||||
{{
|
</v-col>
|
||||||
nameOrCURIE(
|
</v-row>
|
||||||
propertyShapes[k],
|
|
||||||
shapesDS.data.prefixes,
|
|
||||||
true
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</strong>: <MoreOrLessRecordsViewer
|
|
||||||
:records="v.items.map(i => i.value)"
|
|
||||||
v-model:count="showCounts['BlankNodeSpecial'][k]"
|
|
||||||
:stepSize="defaultStep"
|
|
||||||
></MoreOrLessRecordsViewer>
|
|
||||||
<span v-for="(item, i) in v.items">
|
|
||||||
<span v-if="i < showCounts['BlankNodeSpecial'][k]" class="line-item">
|
|
||||||
<span v-if="item.keyPropertyRole?.classIRI && item.keyPropertyRole?.recordPID">
|
|
||||||
-
|
|
||||||
<v-tooltip location="top start">
|
|
||||||
<template v-slot:activator="{ props }">
|
|
||||||
<a
|
|
||||||
v-bind="props"
|
|
||||||
style="cursor: pointer"
|
|
||||||
@click.prevent="selectNamedNode(item.keyPropertyRole.classIRI, item.keyPropertyRole.recordPID)"
|
|
||||||
>{{ item.displayLabel }}</a
|
|
||||||
>
|
|
||||||
</template>
|
|
||||||
<template v-slot:default="{ isActive }">
|
|
||||||
<v-icon >{{ getClassIcon(item.keyPropertyRole.classIRI, allPrefixes) }}</v-icon>
|
|
||||||
{{ getDisplayName(item.keyPropertyRole.classIRI, configVarsMain, allPrefixes, shapesDS.data.nodeShapes[item.keyPropertyRole.classIRI]) }}
|
|
||||||
</template>
|
|
||||||
</v-tooltip>
|
|
||||||
|
|
||||||
</span>
|
|
||||||
<span v-else>
|
|
||||||
- <LiteralNodeViewer :textVal="item.displayLabel" :wrap="'wrap'" :allowLink="false"></LiteralNodeViewer>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
<!-- Blank nodes -->
|
<!-- Blank nodes -->
|
||||||
<br />
|
<span v-if="showBackLinks && !hideBackLinks"><br /></span>
|
||||||
<v-btn
|
<v-btn
|
||||||
no-gutters
|
no-gutters
|
||||||
v-if="Object.keys(record.triples['BlankNode']).length > 0"
|
v-if="Object.keys(record.triples['BlankNode']).length > 0"
|
||||||
|
|
@ -324,6 +334,7 @@ import MoreOrLessRecordsViewer from '@/components/MoreOrLessRecordsViewer.vue';
|
||||||
import SpecialButton from '@/components/SpecialButton.vue'
|
import SpecialButton from '@/components/SpecialButton.vue'
|
||||||
import { useCompConfig } from '@/composables/useCompConfig';
|
import { useCompConfig } from '@/composables/useCompConfig';
|
||||||
import { useDisplay } from 'vuetify'
|
import { useDisplay } from 'vuetify'
|
||||||
|
import BackLinkViewer from './BackLinkViewer.vue';
|
||||||
const { mobile } = useDisplay()
|
const { mobile } = useDisplay()
|
||||||
// Define component properties
|
// Define component properties
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
|
|
@ -380,6 +391,14 @@ const fetchingRecords = ref(false);
|
||||||
const canEditClass = ref(false);
|
const canEditClass = ref(false);
|
||||||
const showSpecialButtons = ref(false);
|
const showSpecialButtons = ref(false);
|
||||||
const specialButtons = reactive({});
|
const specialButtons = reactive({});
|
||||||
|
const showBackLinks = ref(false);
|
||||||
|
const firstUpdateDone = ref(false);
|
||||||
|
const hideBackLinksConfig = componentConfig?.hideBackLinks;
|
||||||
|
const hideBackLinks = ref(true);
|
||||||
|
if (hideBackLinksConfig === false || Array.isArray(hideBackLinksConfig) &&
|
||||||
|
!hideBackLinksConfig.includes(toCURIE(props.classIRI, allPrefixes))) {
|
||||||
|
hideBackLinks.value = false
|
||||||
|
}
|
||||||
|
|
||||||
const emit = defineEmits(['namedNodeSelected']);
|
const emit = defineEmits(['namedNodeSelected']);
|
||||||
function selectNamedNode(recordClass, recordPID) {
|
function selectNamedNode(recordClass, recordPID) {
|
||||||
|
|
@ -398,6 +417,7 @@ onBeforeMount(async () => {
|
||||||
fetchingRecords.value = true;
|
fetchingRecords.value = true;
|
||||||
await updateRecord(true);
|
await updateRecord(true);
|
||||||
fetchingRecords.value = false;
|
fetchingRecords.value = false;
|
||||||
|
firstUpdateDone.value = true;
|
||||||
let recordPIDprefix = toCURIE(props.quad.subject.value, allPrefixes, 'parts').prefix;
|
let recordPIDprefix = toCURIE(props.quad.subject.value, allPrefixes, 'parts').prefix;
|
||||||
if (configVarsMain['idResolvesExternally'].indexOf(recordPIDprefix) >= 0) {
|
if (configVarsMain['idResolvesExternally'].indexOf(recordPIDprefix) >= 0) {
|
||||||
resolveExternally.value = true;
|
resolveExternally.value = true;
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,7 @@ const mainVarsToLoad = {
|
||||||
NodeShapeViewer: {
|
NodeShapeViewer: {
|
||||||
recordNumberStepSize: 5,
|
recordNumberStepSize: 5,
|
||||||
textTruncateWidth: "85%",
|
textTruncateWidth: "85%",
|
||||||
|
hideBackLinks: true,
|
||||||
},
|
},
|
||||||
URIEditor: {
|
URIEditor: {
|
||||||
default: "curie",
|
default: "curie",
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
export function objectsEqual(obj1, obj2) {
|
||||||
if (Object.keys(obj1).length !== Object.keys(obj2).length) {
|
if (Object.keys(obj1).length !== Object.keys(obj2).length) {
|
||||||
return false;
|
return false;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue