New Special Button feature for record viewer #335
5 changed files with 180 additions and 33 deletions
|
|
@ -66,6 +66,12 @@
|
||||||
</template>
|
</template>
|
||||||
</v-tooltip>
|
</v-tooltip>
|
||||||
</span>
|
</span>
|
||||||
|
<span v-if="showSpecialButtons">
|
||||||
|
<span v-for="button in specialButtons">
|
||||||
|
|
||||||
|
<SpecialButton :returnVal="button.returnValue" :config="button.config"></SpecialButton>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
</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' : ''">
|
||||||
|
|
@ -309,9 +315,12 @@ import {
|
||||||
getNodeShapePropertyWithAnnotations,
|
getNodeShapePropertyWithAnnotations,
|
||||||
getSubjectQuad,
|
getSubjectQuad,
|
||||||
getDisplayName,
|
getDisplayName,
|
||||||
|
quadsToTripleObject,
|
||||||
|
findBlankNodeLink,
|
||||||
} from '../modules/utils';
|
} from '../modules/utils';
|
||||||
import { RDF, SHACL } from '@/modules/namespaces';
|
import { RDF, SHACL } from '@/modules/namespaces';
|
||||||
import MoreOrLessRecordsViewer from './MoreOrLessRecordsViewer.vue';
|
import MoreOrLessRecordsViewer from './MoreOrLessRecordsViewer.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'
|
||||||
const { mobile } = useDisplay()
|
const { mobile } = useDisplay()
|
||||||
|
|
@ -367,7 +376,9 @@ const ttlDialog_name = ref('');
|
||||||
const ttlDialog_type = ref('');
|
const ttlDialog_type = ref('');
|
||||||
const ttlDialog_content = ref('');
|
const ttlDialog_content = ref('');
|
||||||
const fetchingRecords = ref(false);
|
const fetchingRecords = ref(false);
|
||||||
const canEditClass = ref(false)
|
const canEditClass = ref(false);
|
||||||
|
const showSpecialButtons = ref(false);
|
||||||
|
const specialButtons = reactive({});
|
||||||
|
|
||||||
const emit = defineEmits(['namedNodeSelected']);
|
const emit = defineEmits(['namedNodeSelected']);
|
||||||
function selectNamedNode(recordClass, recordPID) {
|
function selectNamedNode(recordClass, recordPID) {
|
||||||
|
|
@ -465,9 +476,11 @@ async function updateRecord(fetchData, from) {
|
||||||
BlankNode: {},
|
BlankNode: {},
|
||||||
NamedNode: {},
|
NamedNode: {},
|
||||||
};
|
};
|
||||||
for (const rQ of record.relatedQuads) {
|
const promises = record.relatedQuads.map(rQ =>
|
||||||
await addRecordProperty(rQ, fetchData);
|
addRecordProperty(rQ, fetchData)
|
||||||
}
|
)
|
||||||
|
await Promise.all(promises)
|
||||||
|
const end = performance.now()
|
||||||
record.displayLabel = getRecordDisplayLabel(record.quad.subject, rdfDS, allPrefixes, configVarsMain)
|
record.displayLabel = getRecordDisplayLabel(record.quad.subject, rdfDS, allPrefixes, configVarsMain)
|
||||||
// Now we have all record.triples, and we need to get displaylabels for blanknodes
|
// Now we have all record.triples, and we need to get displaylabels for blanknodes
|
||||||
for (const triplePred in record.triples['BlankNode']) {
|
for (const triplePred in record.triples['BlankNode']) {
|
||||||
|
|
@ -482,6 +495,22 @@ async function updateRecord(fetchData, from) {
|
||||||
record.triples['BlankNode'][triplePred].prefLabels.push(pL)
|
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) {
|
async function addRecordProperty(quad, fetchData) {
|
||||||
|
|
@ -503,6 +532,7 @@ async function addRecordProperty(quad, fetchData) {
|
||||||
displayLabels: [],
|
displayLabels: [],
|
||||||
prefLabels: [],
|
prefLabels: [],
|
||||||
keyPropertyRoles: [],
|
keyPropertyRoles: [],
|
||||||
|
relatedTriples: [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
let kpr = null
|
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 keyPropertyShape = getNodeShapePropertyWithAnnotations(ps[SHACL.class.value], shapesDS, {"dash:propertyRole": "dash:KeyInfoRole"}, allPrefixes)
|
||||||
let keyPropertyRole = keyPropertyShape ? keyPropertyShape[SHACL.path.value] : null
|
let keyPropertyRole = keyPropertyShape ? keyPropertyShape[SHACL.path.value] : null
|
||||||
let bnRelatedQuads = rdfDS.getSubjectTriples(quad.object);
|
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) {
|
for (const bnQuad of bnRelatedQuads) {
|
||||||
if (bnQuad.object.termType === 'NamedNode') {
|
if (bnQuad.object.termType === 'NamedNode') {
|
||||||
console.log("Also fetching blank node object record:")
|
console.log("Also fetching blank node object record:")
|
||||||
|
|
|
||||||
|
|
@ -18,9 +18,18 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
.custom-svg {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.custom-svg svg {
|
.custom-svg svg {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
display: block;
|
||||||
fill: currentColor;
|
fill: currentColor;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
63
src/components/SpecialButton.vue
Normal file
63
src/components/SpecialButton.vue
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
<template>
|
||||||
|
<v-tooltip v-for="b in btns" :text="props.config.tooltip" location="bottom">
|
||||||
|
<template v-slot:activator="{ props }">
|
||||||
|
<v-btn
|
||||||
|
icon
|
||||||
|
variant="tonal"
|
||||||
|
size="x-small"
|
||||||
|
class="rounded-lg"
|
||||||
|
@click="openInNewTab(b.link)"
|
||||||
|
v-bind="props"
|
||||||
|
style="cursor: pointer;"
|
||||||
|
>
|
||||||
|
<span v-if="b.iconFig.type == 'mdi'">
|
||||||
|
<v-icon>{{ b.iconFig.icon }}</v-icon>
|
||||||
|
</span>
|
||||||
|
<span v-else>
|
||||||
|
<SVGIcon :icon="b.iconFig.icon"></SVGIcon>
|
||||||
|
</span>
|
||||||
|
</v-btn>
|
||||||
|
</template>
|
||||||
|
</v-tooltip>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onBeforeMount, onMounted, inject, toRaw } from 'vue';
|
||||||
|
import SVGIcon from '@/components/SVGIcon.vue'
|
||||||
|
import { fillStringTemplate, getIcon} from '@/modules/utils';
|
||||||
|
|
||||||
|
// --------------- //
|
||||||
|
// Component props //
|
||||||
|
// --------------- //
|
||||||
|
const props = defineProps({
|
||||||
|
returnVal: Array,
|
||||||
|
config: Object,
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---- //
|
||||||
|
// Data //
|
||||||
|
// ---- //
|
||||||
|
const btns = ref([]);
|
||||||
|
const configVarsMain = inject('configVarsMain')
|
||||||
|
|
||||||
|
// ----------------- //
|
||||||
|
// Lifecycle methods //
|
||||||
|
// ----------------- //
|
||||||
|
onBeforeMount(() => {
|
||||||
|
for (const sBval of props.returnVal) {
|
||||||
|
let btn = {}
|
||||||
|
btn.link = fillStringTemplate(props.config.template, {'return': sBval})
|
||||||
|
btn.iconFig = getIcon(props.config.icon, configVarsMain)
|
||||||
|
btns.value.push(btn)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// --------- //
|
||||||
|
// Functions //
|
||||||
|
// --------- //
|
||||||
|
function openInNewTab(url) {
|
||||||
|
window.open(url, '_blank').focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { ref, reactive } from "vue";
|
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 { toCURIE, toIRI } from "shacl-tulip";
|
||||||
import { RDF } from "@/modules/namespaces";
|
import { RDF } from "@/modules/namespaces";
|
||||||
import { DataFactory } from 'n3';
|
import { DataFactory } from 'n3';
|
||||||
|
|
@ -55,7 +55,7 @@ export function useWizard() {
|
||||||
console.log(`adding wizard '${wizard}' for class '${classCurie}' and context '${context}'`)
|
console.log(`adding wizard '${wizard}' for class '${classCurie}' and context '${context}'`)
|
||||||
wizardEditors[wizard] = configVarsMain.wizardEditors[wizard]
|
wizardEditors[wizard] = configVarsMain.wizardEditors[wizard]
|
||||||
wizardEditors[wizard].template = getContent(configVarsMain.content, wizardEditors[wizard].template)
|
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
|
// then slot-based wizards
|
||||||
|
|
@ -67,7 +67,7 @@ export function useWizard() {
|
||||||
if (wizard in wizardEditors) continue;
|
if (wizard in wizardEditors) continue;
|
||||||
wizardEditors[wizard] = configVarsMain.wizardEditors[wizard]
|
wizardEditors[wizard] = configVarsMain.wizardEditors[wizard]
|
||||||
wizardEditors[wizard].template = getContent(configVarsMain.content, wizardEditors[wizard].template)
|
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) {
|
function onFormWithWizardCancel(savedNodes, nodesToSubmit, rdfDS) {
|
||||||
for (const q of wizardAddedQuads.value) {
|
for (const q of wizardAddedQuads.value) {
|
||||||
// remove named nodes from savedNodes and nodesToSubmit
|
// remove named nodes from savedNodes and nodesToSubmit
|
||||||
|
|
|
||||||
|
|
@ -852,3 +852,71 @@ export function updatePropertyGroups(configVarsMain, shapesDS) {
|
||||||
shapesDS.data.propertyGroups['_default'][RDFS.label.value] = "Additional properties";
|
shapesDS.data.propertyGroups['_default'][RDFS.label.value] = "Additional properties";
|
||||||
shapesDS.data.propertyGroups['_default'][SHACL.order.value] = high_order + 100;
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue