diff --git a/src/components/NodeShapeViewer.vue b/src/components/NodeShapeViewer.vue index e252e1d..68bef92 100644 --- a/src/components/NodeShapeViewer.vue +++ b/src/components/NodeShapeViewer.vue @@ -17,7 +17,7 @@ > - + Type: {{ toCURIE(record.subtitle, allPrefixes) }}  
@@ -66,6 +66,12 @@ + + +   + + +
@@ -309,9 +315,12 @@ import { getNodeShapePropertyWithAnnotations, getSubjectQuad, getDisplayName, + quadsToTripleObject, + findBlankNodeLink, } from '../modules/utils'; import { RDF, SHACL } from '@/modules/namespaces'; import MoreOrLessRecordsViewer from './MoreOrLessRecordsViewer.vue'; +import SpecialButton from '@/components/SpecialButton.vue' import { useCompConfig } from '@/composables/useCompConfig'; import { useDisplay } from 'vuetify' const { mobile } = useDisplay() @@ -367,7 +376,9 @@ const ttlDialog_name = ref(''); const ttlDialog_type = ref(''); const ttlDialog_content = ref(''); const fetchingRecords = ref(false); -const canEditClass = ref(false) +const canEditClass = ref(false); +const showSpecialButtons = ref(false); +const specialButtons = reactive({}); const emit = defineEmits(['namedNodeSelected']); function selectNamedNode(recordClass, recordPID) { @@ -465,9 +476,11 @@ async function updateRecord(fetchData, from) { BlankNode: {}, NamedNode: {}, }; - for (const rQ of record.relatedQuads) { - await addRecordProperty(rQ, fetchData); - } + const promises = record.relatedQuads.map(rQ => + addRecordProperty(rQ, fetchData) + ) + await Promise.all(promises) + const end = performance.now() record.displayLabel = getRecordDisplayLabel(record.quad.subject, rdfDS, allPrefixes, configVarsMain) // Now we have all record.triples, and we need to get displaylabels for blanknodes for (const triplePred in record.triples['BlankNode']) { @@ -482,6 +495,22 @@ async function updateRecord(fetchData, from) { record.triples['BlankNode'][triplePred].prefLabels.push(pL) } } + // Now let's check for clickable data + if (componentConfig?.specialButtons && typeof componentConfig?.specialButtons === 'object' + && Object.keys(componentConfig?.specialButtons).length > 0 + ) { + for (const sB of Object.keys(componentConfig?.specialButtons)) { + let foundSB = findBlankNodeLink(record, componentConfig.specialButtons[sB], allPrefixes) + if (foundSB) { + specialButtons[sB] = {}; + specialButtons[sB].returnValue = foundSB; + specialButtons[sB].config = componentConfig.specialButtons[sB]; + } + } + if (Object.keys(specialButtons).length) { + showSpecialButtons.value = true; + } + } } async function addRecordProperty(quad, fetchData) { @@ -503,6 +532,7 @@ async function addRecordProperty(quad, fetchData) { displayLabels: [], prefLabels: [], keyPropertyRoles: [], + relatedTriples: [], }; } let kpr = null @@ -511,6 +541,8 @@ async function addRecordProperty(quad, fetchData) { let keyPropertyShape = getNodeShapePropertyWithAnnotations(ps[SHACL.class.value], shapesDS, {"dash:propertyRole": "dash:KeyInfoRole"}, allPrefixes) let keyPropertyRole = keyPropertyShape ? keyPropertyShape[SHACL.path.value] : null let bnRelatedQuads = rdfDS.getSubjectTriples(quad.object); + let relatedTriples = quadsToTripleObject(bnRelatedQuads, allPrefixes) + record.triples[termType][quad.predicate.value]['relatedTriples'].push(relatedTriples) for (const bnQuad of bnRelatedQuads) { if (bnQuad.object.termType === 'NamedNode') { console.log("Also fetching blank node object record:") diff --git a/src/components/SVGIcon.vue b/src/components/SVGIcon.vue index de82323..9d02021 100644 --- a/src/components/SVGIcon.vue +++ b/src/components/SVGIcon.vue @@ -18,9 +18,18 @@ diff --git a/src/components/SpecialButton.vue b/src/components/SpecialButton.vue new file mode 100644 index 0000000..296d6ad --- /dev/null +++ b/src/components/SpecialButton.vue @@ -0,0 +1,63 @@ + + + \ No newline at end of file diff --git a/src/composables/useWizard.js b/src/composables/useWizard.js index 1db2c13..26fb402 100644 --- a/src/composables/useWizard.js +++ b/src/composables/useWizard.js @@ -1,5 +1,5 @@ import { ref, reactive } from "vue"; -import { getContent, fillStringTemplate, findObjectByKey, findObjectIndexByKey, nodeShapeHasProperty} from "@/modules/utils"; +import { getContent, fillStringTemplate, findObjectByKey, findObjectIndexByKey, nodeShapeHasProperty, getIcon} from "@/modules/utils"; import { toCURIE, toIRI } from "shacl-tulip"; import { RDF } from "@/modules/namespaces"; import { DataFactory } from 'n3'; @@ -55,7 +55,7 @@ export function useWizard() { console.log(`adding wizard '${wizard}' for class '${classCurie}' and context '${context}'`) wizardEditors[wizard] = configVarsMain.wizardEditors[wizard] wizardEditors[wizard].template = getContent(configVarsMain.content, wizardEditors[wizard].template) - wizardEditors[wizard].iconFig = getIcon(wizardEditors[wizard], configVarsMain) + wizardEditors[wizard].iconFig = getIcon(wizardEditors[wizard].icon, configVarsMain) } } // then slot-based wizards @@ -67,7 +67,7 @@ export function useWizard() { if (wizard in wizardEditors) continue; wizardEditors[wizard] = configVarsMain.wizardEditors[wizard] wizardEditors[wizard].template = getContent(configVarsMain.content, wizardEditors[wizard].template) - wizardEditors[wizard].iconFig = getIcon(wizardEditors[wizard], configVarsMain) + wizardEditors[wizard].iconFig = getIcon(wizardEditors[wizard].icon, configVarsMain) } } } @@ -160,31 +160,6 @@ export function useWizard() { } } - function getIcon(wizard, configVarsMain) { - if (wizard.icon) { - if (wizard.icon.startsWith('mdi-')) { - return { - type: 'mdi', - icon: wizard.icon - } - } else if (wizard.icon.startsWith('content:')) { - return { - type: 'svg', - icon: getContent(configVarsMain.content, wizard.icon) - } - } else { - return { - type: 'svg', - icon: wizard.icon - } - } - } - return { - type: 'mdi', - icon: 'mdi-plus-box' - } - } - function onFormWithWizardCancel(savedNodes, nodesToSubmit, rdfDS) { for (const q of wizardAddedQuads.value) { // remove named nodes from savedNodes and nodesToSubmit diff --git a/src/modules/utils.js b/src/modules/utils.js index df14d6b..d24a913 100644 --- a/src/modules/utils.js +++ b/src/modules/utils.js @@ -851,4 +851,72 @@ export function updatePropertyGroups(configVarsMain, shapesDS) { shapesDS.data.propertyGroups['_default'] = {}; shapesDS.data.propertyGroups['_default'][RDFS.label.value] = "Additional properties"; shapesDS.data.propertyGroups['_default'][SHACL.order.value] = high_order + 100; +} + +export function findBlankNodeLink(data, config, allPrefixes) { + + const { slot, match = [], return: returnKey } = config; + + let slotIRI = toIRI(slot, allPrefixes) + + // 1. Check that the slot exists + const blankNodes = data?.triples?.BlankNode; + if (!blankNodes || !blankNodes[slotIRI]) { + return undefined; + } + + const relatedTriples = blankNodes[slotIRI]?.relatedTriples; + if (!Array.isArray(relatedTriples)) { + return undefined; + } + console.log(relatedTriples) + + // 2. Filter triples that satisfy ALL match conditions + const matches = relatedTriples.filter(triple => { + return match.every(({ key, val }) => { + const keyIRI = toIRI(key, allPrefixes) + console.log(key) + const valIRI = toIRI(val, allPrefixes); + console.log(valIRI) + const tripleValue = triple[key]; + console.log(tripleValue) + // key must exist and value must be an array containing val + return Array.isArray(tripleValue) && tripleValue.includes(valIRI); + }); + }); + + if (matches.length === 0) { + return undefined; + } + + // 3. Return the requested key values + const results = matches + .map(triple => triple[returnKey]) + .filter(Boolean) // remove undefined + .flat(); // flatten arrays like ["url"] + + return results.length ? results : undefined; +} + +export function getIcon(iconText, configVarsMain, defaultIcon={type: 'mdi',icon: 'mdi-plus-box'}) { + if (iconText) { + if (iconText.startsWith('mdi-')) { + return { + type: 'mdi', + icon: iconText + } + } else if (iconText.startsWith('content:')) { + return { + type: 'svg', + icon: getContent(configVarsMain.content, iconText) + } + } else { + return { + type: 'svg', + icon: iconText + } + } + } else { + return defaultIcon + } } \ No newline at end of file