New approach to generating/maintaining list of records + ShaclVueStarter feature #349
16 changed files with 1866 additions and 322 deletions
12
src/App.vue
12
src/App.vue
|
|
@ -1,15 +1,23 @@
|
||||||
<template>
|
<template>
|
||||||
<v-app>
|
<v-app>
|
||||||
<ShaclVue></ShaclVue>
|
<!-- <component :is="appVariant" /> -->
|
||||||
<!-- <ShaclVue :configUrl="confURL"></ShaclVue> -->
|
<component :is="appVariant" :configUrl="confURL"></component>
|
||||||
</v-app>
|
</v-app>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import { defineAsyncComponent } from 'vue';
|
||||||
// A specific config URL can be provided:
|
// A specific config URL can be provided:
|
||||||
// const confURL = '';
|
// const confURL = '';
|
||||||
// If not provided, the default config URLs will be tried in order at the base URL:
|
// If not provided, the default config URLs will be tried in order at the base URL:
|
||||||
// 1. config.yaml
|
// 1. config.yaml
|
||||||
// 2. config.yml
|
// 2. config.yml
|
||||||
// 3. config.json
|
// 3. config.json
|
||||||
|
// Now we set the main component based on VITE_SHACLVUE_VARIANT environment variable
|
||||||
|
const variant = import.meta.env.VITE_SHACLVUE_VARIANT
|
||||||
|
const componentMap = {
|
||||||
|
default: defineAsyncComponent(() => import('@/components/ShaclVue.vue')),
|
||||||
|
starter: defineAsyncComponent(() => import('@/components/ShaclVueStarter.vue')),
|
||||||
|
}
|
||||||
|
const appVariant = componentMap[variant] || componentMap.default;
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import { reactive, ref } from 'vue';
|
||||||
import { RdfDataset } from 'shacl-tulip';
|
import { RdfDataset } from 'shacl-tulip';
|
||||||
import { DataFactory, Store, Parser } from 'n3';
|
import { DataFactory, Store, Parser } from 'n3';
|
||||||
import { RDF } from '@/modules/namespaces';
|
import { RDF } from '@/modules/namespaces';
|
||||||
import { hashSubgraph, getNodeContextKey, collectBlankNodeHierarchy} from '@/modules/utils';
|
import { hashSubgraph, getNodeContextKey, collectBlankNodeHierarchy, getUniqueRootNodes} from '@/modules/utils';
|
||||||
const { blankNode} = DataFactory;
|
const { blankNode} = DataFactory;
|
||||||
|
|
||||||
export class ReactiveRdfDataset extends RdfDataset {
|
export class ReactiveRdfDataset extends RdfDataset {
|
||||||
|
|
@ -92,7 +92,16 @@ export class ReactiveRdfDataset extends RdfDataset {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
emitAddedRecords(records) {
|
||||||
|
this.dispatchEvent(
|
||||||
|
new CustomEvent('recordsChanged', {
|
||||||
|
detail: {records: records}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async parseTTLandDedup(ttlString) {
|
async parseTTLandDedup(ttlString) {
|
||||||
|
let uniqueRecords
|
||||||
const parser = new Parser();
|
const parser = new Parser();
|
||||||
const tempStore = new Store();
|
const tempStore = new Store();
|
||||||
let addedQuads = [];
|
let addedQuads = [];
|
||||||
|
|
@ -128,7 +137,9 @@ export class ReactiveRdfDataset extends RdfDataset {
|
||||||
console.warn('No root named node detected in TTL, skipping deduplication steo and adding all quads to graph.');
|
console.warn('No root named node detected in TTL, skipping deduplication steo and adding all quads to graph.');
|
||||||
let bnQuads = tempStore.getQuads(null, null, null, null)
|
let bnQuads = tempStore.getQuads(null, null, null, null)
|
||||||
this.data.graph.addQuads(bnQuads);
|
this.data.graph.addQuads(bnQuads);
|
||||||
return addedQuads.concat(bnQuads);
|
let allAddedQuads = addedQuads.concat(bnQuads);
|
||||||
|
uniqueRecords = getUniqueRootNodes(allAddedQuads, this.data.graph)
|
||||||
|
return {quads: allAddedQuads, records: uniqueRecords}
|
||||||
}
|
}
|
||||||
// Now get all unique blank-node subject values
|
// Now get all unique blank-node subject values
|
||||||
const allTempQuads = tempStore.getQuads(null, null, null, null);
|
const allTempQuads = tempStore.getQuads(null, null, null, null);
|
||||||
|
|
@ -136,7 +147,10 @@ export class ReactiveRdfDataset extends RdfDataset {
|
||||||
allTempQuads.filter(q => q.subject.termType === 'BlankNode').map(q => q.subject.value)
|
allTempQuads.filter(q => q.subject.termType === 'BlankNode').map(q => q.subject.value)
|
||||||
);
|
);
|
||||||
// If there are no blank-node subject values, no need to deduplicate
|
// If there are no blank-node subject values, no need to deduplicate
|
||||||
if (blankSubjects.size == 0) return addedQuads;
|
if (blankSubjects.size == 0) {
|
||||||
|
uniqueRecords = getUniqueRootNodes(addedQuads, this.data.graph)
|
||||||
|
return {quads: addedQuads, records: uniqueRecords}
|
||||||
|
}
|
||||||
// Initialize set of fingerprints in the root-node context
|
// Initialize set of fingerprints in the root-node context
|
||||||
if (!this.data.subgraphFingerprintsByRoot.has(root_node)) {
|
if (!this.data.subgraphFingerprintsByRoot.has(root_node)) {
|
||||||
this.data.subgraphFingerprintsByRoot.set(root_node, new Set());
|
this.data.subgraphFingerprintsByRoot.set(root_node, new Set());
|
||||||
|
|
@ -169,6 +183,7 @@ export class ReactiveRdfDataset extends RdfDataset {
|
||||||
// console.log(`Skipping duplicate subgraph for root ${root_node}, blank node ${bnodeId}`);
|
// console.log(`Skipping duplicate subgraph for root ${root_node}, blank node ${bnodeId}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return addedQuads;
|
uniqueRecords = getUniqueRootNodes(addedQuads, this.data.graph)
|
||||||
|
return {quads: addedQuads, records: uniqueRecords}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -368,6 +368,8 @@ async function saveForm() {
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
nodesToSubmit.value.push(saved_node);
|
nodesToSubmit.value.push(saved_node);
|
||||||
|
// we also need to add/update the record to the global list
|
||||||
|
rdfDS.emitAddedRecords([saved_node.node_iri])
|
||||||
}
|
}
|
||||||
removeForm(saved_node);
|
removeForm(saved_node);
|
||||||
if (nodesToSubmit.value.length) {
|
if (nodesToSubmit.value.length) {
|
||||||
|
|
|
||||||
|
|
@ -90,6 +90,7 @@ const templates = reactive({
|
||||||
})
|
})
|
||||||
const hasCreatedQuads = ref(false)
|
const hasCreatedQuads = ref(false)
|
||||||
const createdQuads = ref([])
|
const createdQuads = ref([])
|
||||||
|
const createdRecords = ref([])
|
||||||
const createdDistributions = new Set()
|
const createdDistributions = new Set()
|
||||||
const savedNodes = inject('savedNodes');
|
const savedNodes = inject('savedNodes');
|
||||||
const nodesToSubmit = inject('nodesToSubmit');
|
const nodesToSubmit = inject('nodesToSubmit');
|
||||||
|
|
@ -163,9 +164,12 @@ async function onUploadComplete(result) {
|
||||||
TTLdata.pid = toIRI(TTLdata.pid, allPrefixes)
|
TTLdata.pid = toIRI(TTLdata.pid, allPrefixes)
|
||||||
let newTTL = fillStringTemplate(templates.ttl, TTLdata)
|
let newTTL = fillStringTemplate(templates.ttl, TTLdata)
|
||||||
let newQuads = await rdfDS.parseTTLandDedup(newTTL);
|
let newQuads = await rdfDS.parseTTLandDedup(newTTL);
|
||||||
rdfDS.triggerReactivity();
|
|
||||||
// Keep track of quads that were added, so that we can delete them if form is cancelled
|
// Keep track of quads that were added, so that we can delete them if form is cancelled
|
||||||
createdQuads.value = createdQuads.value.concat(newQuads)
|
// For the same reason, we also don't yet call rdfDS.emitAddedRecords() here, as we do in useData
|
||||||
|
// after a call to rdfDS.parseTTLandDedup; if the form is cancelled we don't want these items in
|
||||||
|
// the global list. We let the form save function handle this.
|
||||||
|
createdQuads.value = createdQuads.value.concat(newQuads.quads)
|
||||||
|
createdRecords.value = createdRecords.value.concat(newQuads.records)
|
||||||
hasCreatedQuads.value = true;
|
hasCreatedQuads.value = true;
|
||||||
// Keep track of distributions that were added
|
// Keep track of distributions that were added
|
||||||
createdDistributions.add(hash)
|
createdDistributions.add(hash)
|
||||||
|
|
@ -204,6 +208,8 @@ onBeforeUnmount( () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// we also emit the added records
|
||||||
|
rdfDS.emitAddedRecords(createdRecords.value)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
392
src/components/NodeShapeViewerMini.vue
Normal file
392
src/components/NodeShapeViewerMini.vue
Normal file
|
|
@ -0,0 +1,392 @@
|
||||||
|
<template>
|
||||||
|
<v-card :variant="props.variant" style="width: fit-content;">
|
||||||
|
<v-card-text v-if="!props.formOpen" :class="mobile ? 'text-caption' : ''" style="display: flex; align-items: center; gap: 6px; padding: 5px">
|
||||||
|
<v-tooltip :text="toCURIE(record.subtitle, allPrefixes)" location="top left">
|
||||||
|
<template v-slot:activator="{ props }">
|
||||||
|
<v-icon v-bind="props" color="primary">{{ getClassIcon(record.subtitle) }}</v-icon>
|
||||||
|
</template>
|
||||||
|
</v-tooltip>
|
||||||
|
<v-menu>
|
||||||
|
<template v-slot:activator="{ props }">
|
||||||
|
<v-btn
|
||||||
|
variant="tonal"
|
||||||
|
size="x-small"
|
||||||
|
class="rounded-lg"
|
||||||
|
:disabled="props.formOpen || !canEditClass"
|
||||||
|
v-bind="props"
|
||||||
|
density="comfortable"
|
||||||
|
icon="mdi-dots-vertical">
|
||||||
|
</v-btn>
|
||||||
|
</template>
|
||||||
|
<v-list density="compact">
|
||||||
|
<v-list-item
|
||||||
|
:key="1"
|
||||||
|
:value="1"
|
||||||
|
density="compact"
|
||||||
|
@click="editInstanceItem(record)"
|
||||||
|
>
|
||||||
|
<v-list-item-title class="small-text"><v-icon :icon="'mdi-pencil'"></v-icon> Edit record</v-list-item-title>
|
||||||
|
</v-list-item>
|
||||||
|
<v-list-item
|
||||||
|
:key="2"
|
||||||
|
:value="2"
|
||||||
|
density="compact"
|
||||||
|
@click="viewRDF()"
|
||||||
|
>
|
||||||
|
<v-list-item-title class="small-text"><v-icon :icon="'mdi-file-eye-outline'"></v-icon> View RDF</v-list-item-title>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
</v-menu>
|
||||||
|
|
||||||
|
<span class="card-title">
|
||||||
|
{{ record.prefLabel ? record.prefLabel : ( record.displayLabel ? record.displayLabel : record.title) }}
|
||||||
|
</span>
|
||||||
|
<span v-if="resolveExternally">
|
||||||
|
<sup
|
||||||
|
><a
|
||||||
|
class="inline-icon-btn"
|
||||||
|
:href="toIRI(record.title, allPrefixes)"
|
||||||
|
target="_blank"
|
||||||
|
><small><v-icon>mdi-arrow-top-right-thick</v-icon></small></a
|
||||||
|
></sup
|
||||||
|
>
|
||||||
|
</span>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
|
||||||
|
<v-dialog
|
||||||
|
v-model="ttlDialog"
|
||||||
|
:max-width="mobile ? '90%' : '60%'"
|
||||||
|
@click:outside="ttlDialog = false"
|
||||||
|
>
|
||||||
|
<v-card>
|
||||||
|
<v-card-title
|
||||||
|
>RDF record for: <em>{{ ttlDialog_name }}</em></v-card-title
|
||||||
|
>
|
||||||
|
<v-card-subtitle
|
||||||
|
><v-icon>{{ ttlDialog_icon }}</v-icon>
|
||||||
|
{{ ttlDialog_type }}</v-card-subtitle
|
||||||
|
>
|
||||||
|
<v-card-text>
|
||||||
|
<code>
|
||||||
|
<pre style="overflow-x: scroll">
|
||||||
|
{{ ttlDialog_content }}
|
||||||
|
</pre
|
||||||
|
>
|
||||||
|
</code>
|
||||||
|
</v-card-text>
|
||||||
|
<v-card-actions>
|
||||||
|
<v-btn prepend-icon="mdi-download" @click="downloadTTL()"
|
||||||
|
>Download</v-btn
|
||||||
|
>
|
||||||
|
<v-btn prepend-icon="mdi-close-box" @click="ttlDialog = false"
|
||||||
|
>Close</v-btn
|
||||||
|
>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import {
|
||||||
|
reactive,
|
||||||
|
onBeforeMount,
|
||||||
|
inject,
|
||||||
|
ref,
|
||||||
|
watch,
|
||||||
|
provide,
|
||||||
|
computed,
|
||||||
|
} from 'vue';
|
||||||
|
import {
|
||||||
|
makeReadable,
|
||||||
|
getPrefLabel,
|
||||||
|
nameOrCURIE,
|
||||||
|
getPidQuad,
|
||||||
|
dlTTL,
|
||||||
|
toSnakeCase,
|
||||||
|
quadsToTTL,
|
||||||
|
getRecordQuads,
|
||||||
|
getRecordDisplayLabel,
|
||||||
|
hasConfigDisplayLabel,
|
||||||
|
getNodeShapePropertyWithAnnotations,
|
||||||
|
getSubjectQuad,
|
||||||
|
getDisplayName,
|
||||||
|
quadsToTripleObject,
|
||||||
|
findBlankNodeLink,
|
||||||
|
toIRI,
|
||||||
|
toCURIE,
|
||||||
|
} from '@/modules/utils';
|
||||||
|
import { RDF, SHACL } from '@/modules/namespaces';
|
||||||
|
import MoreOrLessRecordsViewer from '@/components/MoreOrLessRecordsViewer.vue';
|
||||||
|
import SpecialButton from '@/components/SpecialButton.vue'
|
||||||
|
import { useCompConfig } from '@/composables/useCompConfig';
|
||||||
|
import { useDisplay } from 'vuetify'
|
||||||
|
import BackLinkViewer from './BackLinkViewer.vue';
|
||||||
|
const { mobile } = useDisplay()
|
||||||
|
// Define component properties
|
||||||
|
const props = defineProps({
|
||||||
|
classIRI: String,
|
||||||
|
quad: Object,
|
||||||
|
variant: String,
|
||||||
|
formOpen: Boolean,
|
||||||
|
});
|
||||||
|
|
||||||
|
const editInstanceItem = inject('editInstanceItem');
|
||||||
|
const configVarsMain = inject('configVarsMain');
|
||||||
|
const allPrefixes = inject('allPrefixes');
|
||||||
|
const fetchFromService = inject('fetchFromService');
|
||||||
|
const getClassIcon = inject('getClassIcon');
|
||||||
|
const rdfDS = inject('rdfDS');
|
||||||
|
const shapesDS = inject('shapesDS');
|
||||||
|
const lastSavedNode = inject('lastSavedNode');
|
||||||
|
const record = reactive({});
|
||||||
|
const showBlankNodes = ref(false);
|
||||||
|
const shape_obj = shapesDS.data.nodeShapes[props.classIRI];
|
||||||
|
const resolveExternally = ref(false);
|
||||||
|
const linkCopied = ref(false)
|
||||||
|
const showCopyLink = ref(false)
|
||||||
|
const propertyShapes = {};
|
||||||
|
for (var p of shape_obj.properties) {
|
||||||
|
propertyShapes[p[SHACL.path.value]] = p;
|
||||||
|
}
|
||||||
|
const {componentName, componentConfig} = useCompConfig(configVarsMain);
|
||||||
|
const defaultStep = componentConfig?.recordNumberStepSize ? componentConfig.recordNumberStepSize : 5;
|
||||||
|
let textTruncateWidth;
|
||||||
|
if (componentConfig?.textTruncateWidth === false) {
|
||||||
|
textTruncateWidth = null
|
||||||
|
} else if (!componentConfig?.textTruncateWidth) {
|
||||||
|
textTruncateWidth = '85%'
|
||||||
|
} else {
|
||||||
|
textTruncateWidth = componentConfig.textTruncateWidth
|
||||||
|
}
|
||||||
|
const textWrapping = textTruncateWidth ? 'nowrap' : 'wrap'
|
||||||
|
const showCounts = reactive(
|
||||||
|
{
|
||||||
|
'Literal': {},
|
||||||
|
'NamedNode': {},
|
||||||
|
'BlankNode': {},
|
||||||
|
'BlankNodeSpecial': {},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const ttlDialog = ref(false);
|
||||||
|
const ttlDialog_icon = ref('');
|
||||||
|
const ttlDialog_name = ref('');
|
||||||
|
const ttlDialog_type = ref('');
|
||||||
|
const ttlDialog_content = ref('');
|
||||||
|
const fetchingRecords = ref(false);
|
||||||
|
const canEditClass = ref(false);
|
||||||
|
const showSpecialButtons = ref(false);
|
||||||
|
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']);
|
||||||
|
function selectNamedNode(recordClass, recordPID) {
|
||||||
|
emit('namedNodeSelected', { recordClass, recordPID });
|
||||||
|
}
|
||||||
|
provide('selectNamedNode', selectNamedNode);
|
||||||
|
|
||||||
|
onBeforeMount(async () => {
|
||||||
|
if (configVarsMain.allowCopyRecordUrls === true ||
|
||||||
|
( Array.isArray(configVarsMain.allowCopyRecordUrls) &&
|
||||||
|
configVarsMain.allowCopyRecordUrls.indexOf(props.classIRI) >= 0 )
|
||||||
|
) {
|
||||||
|
showCopyLink.value = true;
|
||||||
|
}
|
||||||
|
canEditClass.value = configVarsMain.noEditClasses.indexOf(toCURIE(props.classIRI, allPrefixes)) < 0 ? true : false
|
||||||
|
fetchingRecords.value = true;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const specialBlankNodes = computed( () => {
|
||||||
|
const triples = record.triples?.['BlankNode'] ?? {};
|
||||||
|
const result = {};
|
||||||
|
for (const [key, v] of Object.entries(triples)) {
|
||||||
|
if (!v.configDisplayLabel || !Array.isArray(v.values)) continue
|
||||||
|
const merged = v.values.map((value, i) => ({
|
||||||
|
value,
|
||||||
|
displayLabel: v.displayLabels?.[i] ?? '',
|
||||||
|
keyPropertyRole: v.keyPropertyRoles?.[i] ?? null,
|
||||||
|
}))
|
||||||
|
const sorted = merged.sort((a, b) => {
|
||||||
|
// display labels starting with 'http' are deprioritized
|
||||||
|
const aIsHttp = a.displayLabel.trim().toLowerCase().startsWith('http')
|
||||||
|
const bIsHttp = b.displayLabel.trim().toLowerCase().startsWith('http')
|
||||||
|
if (aIsHttp && !bIsHttp) return 1
|
||||||
|
if (!aIsHttp && bIsHttp) return -1
|
||||||
|
// within each group, sort alphabetically
|
||||||
|
return a.displayLabel.localeCompare(b.displayLabel, undefined, { sensitivity: 'base' })
|
||||||
|
})
|
||||||
|
result[key] = {
|
||||||
|
...v,
|
||||||
|
items: sorted,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
async function viewRDF() {
|
||||||
|
ttlDialog.value = false;
|
||||||
|
ttlDialog_icon.value = getClassIcon(props.classIRI);
|
||||||
|
ttlDialog_name.value = record.prefLabel ? record.prefLabel : record.title;
|
||||||
|
ttlDialog_type.value = toCURIE(record.subtitle, allPrefixes);
|
||||||
|
var rQs = getRecordQuads(record.value, rdfDS.data.graph, true)
|
||||||
|
var tmpStr = await quadsToTTL(rQs, allPrefixes);
|
||||||
|
ttlDialog_content.value = tmpStr.replace(/^\s+/g, '');
|
||||||
|
ttlDialog_content.value = '\n' + ttlDialog_content.value;
|
||||||
|
ttlDialog.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function downloadTTL() {
|
||||||
|
dlTTL(ttlDialog_content.value, toSnakeCase(ttlDialog_name.value) + '.ttl');
|
||||||
|
}
|
||||||
|
|
||||||
|
function showHideBlankNodes() {
|
||||||
|
showBlankNodes.value = !showBlankNodes.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function updateRecord(fetchData, from) {
|
||||||
|
record.title = props.quad.subject.value;
|
||||||
|
record.quad = props.quad;
|
||||||
|
record.value = props.quad.subject.value;
|
||||||
|
record.subtitle = props.quad.object.value;
|
||||||
|
record.relatedQuads = rdfDS.getSubjectTriples(props.quad.subject);
|
||||||
|
record.prefLabel = getPrefLabel(props.quad.subject, rdfDS, allPrefixes);
|
||||||
|
record.triples = {
|
||||||
|
Literal: {},
|
||||||
|
BlankNode: {},
|
||||||
|
NamedNode: {},
|
||||||
|
};
|
||||||
|
record.displayLabel = getRecordDisplayLabel(record.quad.subject, rdfDS, allPrefixes, configVarsMain)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addRecordProperty(quad, fetchData) {
|
||||||
|
var termType = quad.object.termType;
|
||||||
|
if (
|
||||||
|
termType === 'NamedNode' &&
|
||||||
|
quad.predicate.value != RDF.type.value &&
|
||||||
|
fetchData
|
||||||
|
) {
|
||||||
|
const results = await fetchFromService(
|
||||||
|
'get-record',
|
||||||
|
quad.object.value,
|
||||||
|
allPrefixes
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!record.triples[termType].hasOwnProperty(quad.predicate.value)) {
|
||||||
|
record.triples[termType][quad.predicate.value] = {
|
||||||
|
values: [],
|
||||||
|
displayLabels: [],
|
||||||
|
prefLabels: [],
|
||||||
|
keyPropertyRoles: [],
|
||||||
|
relatedTriples: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
let kpr = null
|
||||||
|
if (termType === 'BlankNode') {
|
||||||
|
let ps = propertyShapes[quad.predicate.value]
|
||||||
|
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:")
|
||||||
|
console.log(bnQuad.object.value)
|
||||||
|
const results = await fetchFromService(
|
||||||
|
'get-record',
|
||||||
|
bnQuad.object.value,
|
||||||
|
allPrefixes
|
||||||
|
);
|
||||||
|
console.log(results)
|
||||||
|
}
|
||||||
|
if (keyPropertyRole && bnQuad.predicate.value === keyPropertyRole) {
|
||||||
|
let iri = null
|
||||||
|
let subjQ = getSubjectQuad(bnQuad.object, rdfDS.data.graph)
|
||||||
|
if (subjQ) {
|
||||||
|
iri = subjQ?.object?.value
|
||||||
|
} else {
|
||||||
|
iri = keyPropertyShape[SHACL.class.value];
|
||||||
|
}
|
||||||
|
kpr = {
|
||||||
|
classIRI: iri,
|
||||||
|
recordPID: bnQuad.object.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (kpr) {
|
||||||
|
record.triples[termType][quad.predicate.value].keyPropertyRoles.push(kpr);
|
||||||
|
} else {
|
||||||
|
record.triples[termType][quad.predicate.value].keyPropertyRoles.push(null);
|
||||||
|
}
|
||||||
|
// selectNamedNode(currentClassIRI, currentRecordPID)
|
||||||
|
record.triples[termType][quad.predicate.value].values.push(quad.object);
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyRecordLink() {
|
||||||
|
var nodeShapeCurie = toCURIE(props.classIRI, allPrefixes);
|
||||||
|
var pidCurie = toCURIE(props.quad.subject.value, allPrefixes);
|
||||||
|
var nsQPvar = encodeURIComponent('sh:NodeShape')
|
||||||
|
var nsQP = encodeURIComponent(nodeShapeCurie)
|
||||||
|
var pidQP = encodeURIComponent(pidCurie)
|
||||||
|
var queryParams = `?${nsQPvar}=${nsQP}&pid=${pidQP}`;
|
||||||
|
var urlText = window.location.origin + window.location.pathname + queryParams
|
||||||
|
copyTextToClipboard(urlText)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function copyTextToClipboard(text) {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(text);
|
||||||
|
linkCopied.value = true
|
||||||
|
setTimeout(() => {
|
||||||
|
linkCopied.value = false;
|
||||||
|
}, 1000);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Clipboard copy failed:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.line-item {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
display: inline-block;
|
||||||
|
text-wrap: wrap;
|
||||||
|
max-width: 90%;
|
||||||
|
line-height: 1.2em;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
.mobile-scaled {
|
||||||
|
transform: scale(0.75);
|
||||||
|
transform-origin: top left;
|
||||||
|
width: 120%;
|
||||||
|
}
|
||||||
|
.small-text {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -135,11 +135,10 @@
|
||||||
<ShaclVueRecords
|
<ShaclVueRecords
|
||||||
:selectedIRI="selectedIRI"
|
:selectedIRI="selectedIRI"
|
||||||
:classRecordsLoading="classRecordsLoading"
|
:classRecordsLoading="classRecordsLoading"
|
||||||
:instanceItemsComp="instanceItemsComp"
|
|
||||||
:mobile="mobile"
|
:mobile="mobile"
|
||||||
:showScrollTopBtn="showScrollTopBtn"
|
:showScrollTopBtn="showScrollTopBtn"
|
||||||
:matchedInstanceItemsComp="matchedInstanceItemsComp"
|
:filteredRecords="(includeSubClasses ? filteredRecordItemsForClassWithSubclassItems : filteredRecordItemsByClass[selectedIRI]) || []"
|
||||||
:filteredInstanceItemsComp="filteredInstanceItemsComp"
|
:fetchedItemCount="fetchedItemCount"
|
||||||
:showFetchingPageLoader="showFetchingPageLoader"
|
:showFetchingPageLoader="showFetchingPageLoader"
|
||||||
v-model:searchText="searchText"
|
v-model:searchText="searchText"
|
||||||
v-model:textMatchType="textMatchType"
|
v-model:textMatchType="textMatchType"
|
||||||
|
|
@ -337,7 +336,25 @@ const {
|
||||||
// Classes from OWL
|
// Classes from OWL
|
||||||
const { classDS, getClassData, allSubClasses, processSubClasses} = useClasses(config);
|
const { classDS, getClassData, allSubClasses, processSubClasses} = useClasses(config);
|
||||||
// Shapes from SHACL
|
// Shapes from SHACL
|
||||||
const { shapesDS, getSHACLschema, updateShapesFromDefault, updateShapes, updatePropertyGroups} = useShapes(config);
|
const {
|
||||||
|
shapesDS,
|
||||||
|
getSHACLschema,
|
||||||
|
updateShapesFromDefault,
|
||||||
|
updateShapes,
|
||||||
|
updatePropertyGroups,
|
||||||
|
idFilteredNodeShapeNames,
|
||||||
|
noEditClassList,
|
||||||
|
filteredNodeShapeNames,
|
||||||
|
priorityFilteredNodeShapeNames,
|
||||||
|
orderedNodeShapeNames,
|
||||||
|
allClassItems,
|
||||||
|
getIdFilteredNodeShapeNames,
|
||||||
|
getNoEditClassList,
|
||||||
|
getFilteredNodeShapeNames,
|
||||||
|
getPriorityFilteredNodeShapeNames,
|
||||||
|
getOrderedNodeShapeNames,
|
||||||
|
getAllClassItems,
|
||||||
|
} = useShapes(config);
|
||||||
// Graph data
|
// Graph data
|
||||||
const {
|
const {
|
||||||
fetchedPages,
|
fetchedPages,
|
||||||
|
|
@ -358,14 +375,9 @@ const {
|
||||||
classRecordsLoading,
|
classRecordsLoading,
|
||||||
currentProgress,
|
currentProgress,
|
||||||
fetchedItemCount,
|
fetchedItemCount,
|
||||||
filteredInstanceItemsComp,
|
|
||||||
getInstanceItems,
|
|
||||||
headingHover,
|
headingHover,
|
||||||
includeSubClasses,
|
includeSubClasses,
|
||||||
instanceItemsComp,
|
|
||||||
isFetchingPage,
|
isFetchingPage,
|
||||||
matchedInstanceItemsComp,
|
|
||||||
newTypeSelected,
|
|
||||||
onScrollEnd,
|
onScrollEnd,
|
||||||
onUserTyping,
|
onUserTyping,
|
||||||
orderTopDown,
|
orderTopDown,
|
||||||
|
|
@ -375,6 +387,10 @@ const {
|
||||||
showScrollTopBtn,
|
showScrollTopBtn,
|
||||||
textMatchType,
|
textMatchType,
|
||||||
totalItemCount,
|
totalItemCount,
|
||||||
|
recordItemsByClass,
|
||||||
|
filteredRecordItemsAll,
|
||||||
|
filteredRecordItemsByClass,
|
||||||
|
filteredRecordItemsForClassWithSubclassItems,
|
||||||
} = useRecords(
|
} = useRecords(
|
||||||
allPrefixes,
|
allPrefixes,
|
||||||
allSubClasses,
|
allSubClasses,
|
||||||
|
|
@ -408,7 +424,6 @@ const {
|
||||||
onEditInstanceItem: afterEditInstanceItem,
|
onEditInstanceItem: afterEditInstanceItem,
|
||||||
onAddForm: scrollToTop,
|
onAddForm: scrollToTop,
|
||||||
onRemoveForm: afterFormsClosed, // will run when last form is closed
|
onRemoveForm: afterFormsClosed, // will run when last form is closed
|
||||||
onRemoveFormSaved: getInstanceItems, // will run when last form is closed via save
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
// App navigation
|
// App navigation
|
||||||
|
|
@ -521,6 +536,13 @@ watch(
|
||||||
processSearchableFields();
|
processSearchableFields();
|
||||||
// Set component states from URL query parameters
|
// Set component states from URL query parameters
|
||||||
setViewFromQuery();
|
setViewFromQuery();
|
||||||
|
// Get all class-related data
|
||||||
|
idFilteredNodeShapeNames.value = getIdFilteredNodeShapeNames(configVarsMain, ID_IRI);
|
||||||
|
noEditClassList.value = getNoEditClassList(configVarsMain, allPrefixes);
|
||||||
|
filteredNodeShapeNames.value = getFilteredNodeShapeNames(configVarsMain, allPrefixes);
|
||||||
|
priorityFilteredNodeShapeNames.value = getPriorityFilteredNodeShapeNames(priorityClassList);
|
||||||
|
orderedNodeShapeNames.value = getOrderedNodeShapeNames(configVarsMain, allPrefixes);
|
||||||
|
allClassItems.value = getAllClassItems(configVarsMain, allPrefixes, getClassIcon);
|
||||||
page_ready.value = true;
|
page_ready.value = true;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -538,111 +560,6 @@ onBeforeUnmount(() => {
|
||||||
// -------------- //
|
// -------------- //
|
||||||
// Computed props //
|
// Computed props //
|
||||||
// -------------- //
|
// -------------- //
|
||||||
// These are all arays of classes that are eventually represented in the
|
|
||||||
// main class-selection pane in this component
|
|
||||||
const idFilteredNodeShapeNames = computed(() => {
|
|
||||||
if (configVarsMain.showShapesWoId === true) {
|
|
||||||
return shapesDS.data.nodeShapeNamesArray;
|
|
||||||
}
|
|
||||||
var shapeNames = [];
|
|
||||||
for (var n of shapesDS.data.nodeShapeNamesArray) {
|
|
||||||
if (
|
|
||||||
findObjectByKey(
|
|
||||||
shapesDS.data.nodeShapes[shapesDS.data.nodeShapeNames[n]]
|
|
||||||
.properties,
|
|
||||||
SHACL.path.value,
|
|
||||||
ID_IRI.value
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
shapeNames.push(n);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return shapeNames;
|
|
||||||
});
|
|
||||||
|
|
||||||
const filteredNodeShapeNames = computed(() => {
|
|
||||||
var names = idFilteredNodeShapeNames.value;
|
|
||||||
// If all relevant config arrays are empty, show all classes
|
|
||||||
if (
|
|
||||||
configVarsMain.showClasses?.length == 0 &&
|
|
||||||
configVarsMain.showClassesWithPrefix?.length == 0 &&
|
|
||||||
configVarsMain.hideClasses?.length == 0 &&
|
|
||||||
configVarsMain.hideClassesWithPrefix?.length == 0 &&
|
|
||||||
configVarsMain.noEditClasses?.length == 0
|
|
||||||
) {
|
|
||||||
return names;
|
|
||||||
}
|
|
||||||
var shapeNames = [];
|
|
||||||
for (var n of names) {
|
|
||||||
// First get IRI and prefix
|
|
||||||
var n_iri = shapesDS.data.nodeShapeNames[n]
|
|
||||||
if (includeClass(n_iri, configVarsMain, allPrefixes) && configVarsMain.noEditClasses.indexOf(toCURIE(n_iri, allPrefixes)) < 0) {
|
|
||||||
shapeNames.push(n);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return shapeNames;
|
|
||||||
});
|
|
||||||
|
|
||||||
const priorityFilteredNodeShapeNames = computed(() => {
|
|
||||||
var names = filteredNodeShapeNames.value;
|
|
||||||
var shapeNames = [];
|
|
||||||
for (var n of names) {
|
|
||||||
var n_iri = shapesDS.data.nodeShapeNames[n]
|
|
||||||
if (!priorityClassList.value.includes(n_iri)) {
|
|
||||||
shapeNames.push(n);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return shapeNames;
|
|
||||||
})
|
|
||||||
|
|
||||||
const orderedNodeShapeNames = computed(() => {
|
|
||||||
return priorityFilteredNodeShapeNames.value.sort((a, b) =>
|
|
||||||
getDisplayName(
|
|
||||||
shapesDS.data.nodeShapeNames[a],
|
|
||||||
configVarsMain,
|
|
||||||
allPrefixes,
|
|
||||||
shapesDS.data.nodeShapes[shapesDS.data.nodeShapeNames[a]]
|
|
||||||
).toLowerCase()
|
|
||||||
.localeCompare(
|
|
||||||
getDisplayName(
|
|
||||||
shapesDS.data.nodeShapeNames[b],
|
|
||||||
configVarsMain,
|
|
||||||
allPrefixes,
|
|
||||||
shapesDS.data.nodeShapes[shapesDS.data.nodeShapeNames[b]]
|
|
||||||
).toLowerCase()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
})
|
|
||||||
|
|
||||||
const noEditClassList = computed(() => {
|
|
||||||
if (configVarsMain.noEditClasses?.length == 0) return []
|
|
||||||
var names = idFilteredNodeShapeNames.value;
|
|
||||||
var shapeNames = [];
|
|
||||||
for (var n of names) {
|
|
||||||
// First get IRI and prefix
|
|
||||||
var n_iri = shapesDS.data.nodeShapeNames[n]
|
|
||||||
if (includeClass(n_iri, configVarsMain, allPrefixes) &&
|
|
||||||
configVarsMain.noEditClasses?.indexOf(toCURIE(n_iri, allPrefixes)) >= 0) {
|
|
||||||
shapeNames.push(n);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return shapeNames.sort((a, b) =>
|
|
||||||
getDisplayName(
|
|
||||||
shapesDS.data.nodeShapeNames[a],
|
|
||||||
configVarsMain,
|
|
||||||
allPrefixes,
|
|
||||||
shapesDS.data.nodeShapes[shapesDS.data.nodeShapeNames[a]]
|
|
||||||
).toLowerCase()
|
|
||||||
.localeCompare(
|
|
||||||
getDisplayName(
|
|
||||||
shapesDS.data.nodeShapeNames[b],
|
|
||||||
configVarsMain,
|
|
||||||
allPrefixes,
|
|
||||||
shapesDS.data.nodeShapes[shapesDS.data.nodeShapeNames[b]]
|
|
||||||
).toLowerCase()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
})
|
|
||||||
|
|
||||||
// --------- //
|
// --------- //
|
||||||
// Functions //
|
// Functions //
|
||||||
|
|
@ -688,7 +605,6 @@ async function selectType(IRI, fromUser, fromBackButton, includeSubs=false) {
|
||||||
totalItemCount.value = 0
|
totalItemCount.value = 0
|
||||||
isFetchingPage.value = false;
|
isFetchingPage.value = false;
|
||||||
showScrollTopBtn.value = false;
|
showScrollTopBtn.value = false;
|
||||||
newTypeSelected.value = true;
|
|
||||||
var tempSearchText = searchText.value;
|
var tempSearchText = searchText.value;
|
||||||
var tempIRI = selectedIRI.value;
|
var tempIRI = selectedIRI.value;
|
||||||
searchText.value = '';
|
searchText.value = '';
|
||||||
|
|
@ -708,10 +624,8 @@ async function selectType(IRI, fromUser, fromBackButton, includeSubs=false) {
|
||||||
console.error(result.error);
|
console.error(result.error);
|
||||||
classRecordsLoading.value = false;
|
classRecordsLoading.value = false;
|
||||||
}
|
}
|
||||||
// If any of the results were successful, don't set classRecordsLoading to false
|
// If any of the results were successful, do nothing
|
||||||
// because it will be set during the watch event for instanceItemsComp
|
|
||||||
if (result.status.length && result.status.indexOf('success') >= 0) {
|
if (result.status.length && result.status.indexOf('success') >= 0) {
|
||||||
// do nothing
|
|
||||||
} else {
|
} else {
|
||||||
classRecordsLoading.value = false;
|
classRecordsLoading.value = false;
|
||||||
}
|
}
|
||||||
|
|
@ -723,7 +637,7 @@ async function selectType(IRI, fromUser, fromBackButton, includeSubs=false) {
|
||||||
totalItemCount.value = totalItems
|
totalItemCount.value = totalItems
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
getInstanceItems();
|
classRecordsLoading.value = false
|
||||||
scrollToTop();
|
scrollToTop();
|
||||||
if (fromUser) {
|
if (fromUser) {
|
||||||
updateURL(IRI, false, null, allPrefixes);
|
updateURL(IRI, false, null, allPrefixes);
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,8 @@
|
||||||
<v-skeleton-loader type="list-item-avatar"></v-skeleton-loader>
|
<v-skeleton-loader type="list-item-avatar"></v-skeleton-loader>
|
||||||
</span>
|
</span>
|
||||||
<span v-else>
|
<span v-else>
|
||||||
<div v-if="props.instanceItemsComp.length">
|
<div v-if="props.fetchedItemCount">
|
||||||
<v-row v-if="props.instanceItemsComp.length">
|
<v-row v-if="props.fetchedItemCount">
|
||||||
<v-col :cols="props.mobile ? 12 : 8">
|
<v-col :cols="props.mobile ? 12 : 8">
|
||||||
<v-text-field
|
<v-text-field
|
||||||
v-model="searchText"
|
v-model="searchText"
|
||||||
|
|
@ -64,11 +64,7 @@
|
||||||
</template>
|
</template>
|
||||||
</v-tooltip>
|
</v-tooltip>
|
||||||
<DynamicScroller
|
<DynamicScroller
|
||||||
:items="
|
:items="props.filteredRecords"
|
||||||
textMatchType == 'exact' ?
|
|
||||||
props.matchedInstanceItemsComp :
|
|
||||||
props.filteredInstanceItemsComp
|
|
||||||
"
|
|
||||||
page-mode
|
page-mode
|
||||||
:min-item-size="50"
|
:min-item-size="50"
|
||||||
key-field="title"
|
key-field="title"
|
||||||
|
|
@ -123,13 +119,12 @@ import {
|
||||||
// ----- //
|
// ----- //
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
selectedIRI: String,
|
selectedIRI: String,
|
||||||
instanceItemsComp: Array,
|
|
||||||
classRecordsLoading: Boolean,
|
classRecordsLoading: Boolean,
|
||||||
mobile: Boolean,
|
mobile: Boolean,
|
||||||
showScrollTopBtn: Boolean,
|
showScrollTopBtn: Boolean,
|
||||||
scrollToTop: Function,
|
scrollToTop: Function,
|
||||||
matchedInstanceItemsComp: Array,
|
filteredRecords: Array,
|
||||||
filteredInstanceItemsComp: Array,
|
fetchedItemCount: Number,
|
||||||
showFetchingPageLoader: Boolean,
|
showFetchingPageLoader: Boolean,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
125
src/components/ShaclVueRecordsMini.vue
Normal file
125
src/components/ShaclVueRecordsMini.vue
Normal file
|
|
@ -0,0 +1,125 @@
|
||||||
|
<template>
|
||||||
|
<span v-if="props.classRecordsLoading">
|
||||||
|
<v-skeleton-loader type="list-item-avatar"></v-skeleton-loader>
|
||||||
|
</span>
|
||||||
|
<span v-else>
|
||||||
|
<div v-if="props.items.length">
|
||||||
|
<DynamicScroller
|
||||||
|
:items="props.items"
|
||||||
|
:min-item-size="100"
|
||||||
|
key-field="value"
|
||||||
|
class="virtual-scroller"
|
||||||
|
@scroll-end="scrollEnd()"
|
||||||
|
>
|
||||||
|
<template v-slot="{ item, index, active, }">
|
||||||
|
<DynamicScrollerItem
|
||||||
|
:item="item"
|
||||||
|
:index="index"
|
||||||
|
:active="active"
|
||||||
|
class="scroller-item"
|
||||||
|
:ref="itemRefs[index]"
|
||||||
|
>
|
||||||
|
<template #default>
|
||||||
|
<NodeShapeViewerMini
|
||||||
|
:classIRI="props.classIRI"
|
||||||
|
:quad="item.props.quad"
|
||||||
|
:key="props.selectedIRI + '-' + item.title"
|
||||||
|
variant="outlined"
|
||||||
|
@namedNodeSelected="onNamedNodeSelected"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</DynamicScrollerItem>
|
||||||
|
</template>
|
||||||
|
<template #after>
|
||||||
|
<div class="after-loader" :style="'color: ' + configVarsMain.appTheme.link_color + ';'">
|
||||||
|
<v-progress-circular v-show="props.showFetchingPageLoader" indeterminate :size="20" :width="4"></v-progress-circular>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</DynamicScroller>
|
||||||
|
</div>
|
||||||
|
<div v-else style="margin-top: 1em; margin-left: 1em;">
|
||||||
|
<em>No items</em>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
|
||||||
|
import { computed, inject, ref, onBeforeMount} from 'vue';
|
||||||
|
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css';
|
||||||
|
import {
|
||||||
|
DynamicScroller,
|
||||||
|
DynamicScrollerItem,
|
||||||
|
} from 'vue-virtual-scroller';
|
||||||
|
import NodeShapeViewerMini from './NodeShapeViewerMini.vue';
|
||||||
|
import { useDisplay } from 'vuetify'
|
||||||
|
|
||||||
|
const { mdAndUp, lgAndUp } = useDisplay()
|
||||||
|
|
||||||
|
// ----- //
|
||||||
|
// PROPS //
|
||||||
|
// ----- //
|
||||||
|
const props = defineProps({
|
||||||
|
classIRI: String,
|
||||||
|
items: Array,
|
||||||
|
classRecordsLoading: Boolean,
|
||||||
|
showFetchingPageLoader: Boolean,
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits([
|
||||||
|
'handle-internal-navigation',
|
||||||
|
'scroll-end',
|
||||||
|
])
|
||||||
|
|
||||||
|
const configVarsMain = inject('configVarsMain')
|
||||||
|
const itemRefs = ref([]);
|
||||||
|
|
||||||
|
function onNamedNodeSelected(payload) {
|
||||||
|
emit('handle-internal-navigation', payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
function scrollEnd() {
|
||||||
|
emit('scroll-end')
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
|
||||||
|
.row-sheet {
|
||||||
|
max-height: 30vh;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-col {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-col {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.virtual-scroller {
|
||||||
|
max-height: 25vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scroller-item {
|
||||||
|
padding-bottom: 3px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.after-loader {
|
||||||
|
height: 60px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.mobile-scaled {
|
||||||
|
transform: scale(0.75);
|
||||||
|
transform-origin: top left;
|
||||||
|
width: 120%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
841
src/components/ShaclVueStarter.vue
Normal file
841
src/components/ShaclVueStarter.vue
Normal file
|
|
@ -0,0 +1,841 @@
|
||||||
|
<template>
|
||||||
|
<AppHeader v-if="configReady" :logo="configVarsMain.appTheme.logo" @tokenDialogOpened="onTokenDialogOpened"/>
|
||||||
|
<v-main>
|
||||||
|
<v-container fluid>
|
||||||
|
<span v-if="page_ready">
|
||||||
|
<v-card>
|
||||||
|
<v-layout>
|
||||||
|
<!-- Button to open/close class selection pane -->
|
||||||
|
<v-btn
|
||||||
|
v-if="mobile"
|
||||||
|
:icon="drawer ? 'mdi-chevron-left' : 'mdi-chevron-right'"
|
||||||
|
size="40"
|
||||||
|
class="drawer-fab"
|
||||||
|
@click="drawer = !drawer"
|
||||||
|
theme="dark"
|
||||||
|
:color="configVarsMain.appTheme.panel_color"
|
||||||
|
></v-btn>
|
||||||
|
<!-- Class selection pane -->
|
||||||
|
<v-navigation-drawer
|
||||||
|
theme="dark"
|
||||||
|
:color="configVarsMain.appTheme.panel_color"
|
||||||
|
v-model="drawer"
|
||||||
|
style="overflow-y: auto"
|
||||||
|
:permanent="!mobile"
|
||||||
|
:temporary="mobile"
|
||||||
|
app
|
||||||
|
>
|
||||||
|
<v-list
|
||||||
|
nav
|
||||||
|
v-model:selected="classSelection"
|
||||||
|
select-strategy="leaf"
|
||||||
|
>
|
||||||
|
<v-list-item>
|
||||||
|
<template #prepend>
|
||||||
|
<v-list-item-action start>
|
||||||
|
<v-checkbox-btn
|
||||||
|
:model-value="allSelected"
|
||||||
|
:indeterminate="isIndeterminate"
|
||||||
|
@update:model-value="toggleAll"
|
||||||
|
/>
|
||||||
|
</v-list-item-action>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<v-list-item-title>
|
||||||
|
<h4>All Data Types</h4>
|
||||||
|
</v-list-item-title>
|
||||||
|
</v-list-item>
|
||||||
|
<v-list-item
|
||||||
|
v-for="item in allClassItems"
|
||||||
|
:key="item.value"
|
||||||
|
:title="item.title"
|
||||||
|
:value="item.value"
|
||||||
|
>
|
||||||
|
<template v-slot:prepend="{ isSelected, select }">
|
||||||
|
<v-list-item-action start>
|
||||||
|
<v-checkbox-btn :model-value="isSelected" @update:model-value="select"></v-checkbox-btn>
|
||||||
|
</v-list-item-action>
|
||||||
|
</template>
|
||||||
|
<template v-slot:append="{ }">
|
||||||
|
<v-tooltip :text="item.props.description" location="end" max-width="500px">
|
||||||
|
<template v-slot:activator="{ props }">
|
||||||
|
<v-icon v-bind="props">{{ item.props.icon }}</v-icon>
|
||||||
|
</template>
|
||||||
|
</v-tooltip>
|
||||||
|
</template>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
</v-navigation-drawer>
|
||||||
|
<!-- The main view containing records and forms -->
|
||||||
|
<v-main
|
||||||
|
ref="mainContent"
|
||||||
|
style="height: 90vh; overflow-y: auto"
|
||||||
|
>
|
||||||
|
<!-- Everything is inside one container and one row -->
|
||||||
|
<v-container fluid>
|
||||||
|
<v-row v-if="fetchingFirstPages" justify="center" class="mt-4">
|
||||||
|
<div class="text-center">
|
||||||
|
<v-progress-circular indeterminate :size="90" :width="3" :color="configVarsMain.appTheme.link_color">
|
||||||
|
<template v-slot:default><em> Fetching<br>data... </em></template>
|
||||||
|
</v-progress-circular>
|
||||||
|
</div>
|
||||||
|
</v-row>
|
||||||
|
<v-row v-else>
|
||||||
|
<!-- Column for records -->
|
||||||
|
<v-col
|
||||||
|
v-show="formOpen ? false : true"
|
||||||
|
class="transition-all ml-1"
|
||||||
|
:class="formOpen ? 'opacity-column' : ''"
|
||||||
|
>
|
||||||
|
<h1 style="margin-bottom: 0.5em;">All of the Things</h1>
|
||||||
|
<v-row>
|
||||||
|
<v-col :cols="props.mobile ? 12 : 8">
|
||||||
|
<v-text-field
|
||||||
|
v-model="searchInput"
|
||||||
|
density="compact"
|
||||||
|
variant="outlined"
|
||||||
|
:label="`Enter at least ${configVarsMain.serviceConstrainedSearch.min_characters} characters to search all records`"
|
||||||
|
hide-details="auto"
|
||||||
|
style="margin-bottom: 1em;"
|
||||||
|
:disabled="openForms.length > 0"
|
||||||
|
:class="props.mobile ? 'mobile-scaled' : '' "
|
||||||
|
>
|
||||||
|
<template v-slot:append-inner>
|
||||||
|
<v-icon
|
||||||
|
v-if="searchInput"
|
||||||
|
class="mr-2"
|
||||||
|
@click.stop="clearField()"
|
||||||
|
@mousedown.stop.prevent
|
||||||
|
>
|
||||||
|
mdi-close-circle
|
||||||
|
</v-icon>
|
||||||
|
</template>
|
||||||
|
</v-text-field>
|
||||||
|
</v-col>
|
||||||
|
<v-col v-if="!props.mobile"></v-col>
|
||||||
|
</v-row>
|
||||||
|
<v-tooltip text="Scroll to top" location="top end">
|
||||||
|
<template v-slot:activator="{ props: activatorProps }">
|
||||||
|
<v-fab
|
||||||
|
v-if="props.showScrollTopBtn && openForms.length == 0"
|
||||||
|
@click="scrollToTop"
|
||||||
|
icon="mdi-arrow-up-bold"
|
||||||
|
:app="true"
|
||||||
|
style="bottom: 2em;"
|
||||||
|
v-bind="activatorProps"
|
||||||
|
></v-fab>
|
||||||
|
</template>
|
||||||
|
</v-tooltip>
|
||||||
|
<!-- All Class records -->
|
||||||
|
<span v-for="cl in allClassItems" :key="cl.value">
|
||||||
|
<v-sheet
|
||||||
|
v-show="classSelection.includes(cl.value) && cl.props.totalItemCount && itemsByClass[cl.value]?.length"
|
||||||
|
class="row-sheet border rounded pa-4 mb-4"
|
||||||
|
>
|
||||||
|
<v-row class="h-25" style="overflow-y: scroll;">
|
||||||
|
<v-col cols="6" class="left-col">
|
||||||
|
<h3 style="margin-bottom: 1em;">
|
||||||
|
<v-icon>{{ cl.props.icon }}</v-icon> {{ cl.title }}
|
||||||
|
<span v-if="showWizardGroup(
|
||||||
|
configVarsMain,
|
||||||
|
'_class',
|
||||||
|
cl.value,
|
||||||
|
allPrefixes,
|
||||||
|
shapesDS
|
||||||
|
)">
|
||||||
|
<WizardGroup :context="'_class'" :classUri="cl.value"></WizardGroup>
|
||||||
|
</span>
|
||||||
|
</h3>
|
||||||
|
<p>{{ cl.props.description }}</p>
|
||||||
|
</v-col>
|
||||||
|
<v-col class="right-col">
|
||||||
|
<ShaclVueRecordsMini
|
||||||
|
:classIRI="cl.value"
|
||||||
|
:items="itemsByClass[cl.value] || []"
|
||||||
|
class="right-col-div"
|
||||||
|
@scroll-end="onScrollEndOfClass(cl.value)"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-sheet>
|
||||||
|
</span>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<!-- Column for Form(s) -->
|
||||||
|
<v-col v-if="formOpen">
|
||||||
|
<v-expansion-panels
|
||||||
|
variant="accordion"
|
||||||
|
v-model="currentOpenForm"
|
||||||
|
class="custompanels"
|
||||||
|
>
|
||||||
|
<v-expansion-panel
|
||||||
|
v-for="(f, i) in openForms"
|
||||||
|
:key="
|
||||||
|
f.shapeIRI +
|
||||||
|
'-' +
|
||||||
|
f.nodeIDX +
|
||||||
|
'-expansionpanel'
|
||||||
|
"
|
||||||
|
:value="'panel' + (i + 1).toString()"
|
||||||
|
:disabled="f.disabled"
|
||||||
|
>
|
||||||
|
<v-expansion-panel-title>
|
||||||
|
<h2>
|
||||||
|
<em>
|
||||||
|
{{ f.formType === 'new' ? 'Adding' : 'Editing' }}:
|
||||||
|
{{
|
||||||
|
getDisplayName(
|
||||||
|
f.shapeIRI,
|
||||||
|
configVarsMain,
|
||||||
|
allPrefixes,
|
||||||
|
shapesDS.data.nodeShapes[f.shapeIRI]
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</em>
|
||||||
|
</h2>
|
||||||
|
</v-expansion-panel-title>
|
||||||
|
<v-expansion-panel-text density="compact" eager>
|
||||||
|
<div v-show="currentOpenForm === ('panel' + (i + 1))">
|
||||||
|
<FormEditor
|
||||||
|
:key="
|
||||||
|
f.shapeIRI +
|
||||||
|
'-' +
|
||||||
|
f.nodeIDX +
|
||||||
|
'-form-' +
|
||||||
|
f.formType
|
||||||
|
"
|
||||||
|
:shape_iri="f.shapeIRI"
|
||||||
|
:node_idx="f.nodeIDX"
|
||||||
|
></FormEditor>
|
||||||
|
</div>
|
||||||
|
</v-expansion-panel-text>
|
||||||
|
</v-expansion-panel>
|
||||||
|
</v-expansion-panels>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-container>
|
||||||
|
|
||||||
|
</v-main>
|
||||||
|
<!-- Button to open/close submission drawer -->
|
||||||
|
<span v-if="configVarsMain.useService">
|
||||||
|
<v-navigation-drawer
|
||||||
|
theme="dark"
|
||||||
|
:color="configVarsMain.appTheme.panel_color"
|
||||||
|
v-model="submissionDrawer"
|
||||||
|
style="overflow-y: auto"
|
||||||
|
:temporary="true"
|
||||||
|
location="right"
|
||||||
|
:width="800"
|
||||||
|
app
|
||||||
|
>
|
||||||
|
<SubmitComp v-if="submissionDrawer" v-model:selectedNodesToSubmit="selectedNodesToSubmit" :openCloseFn="submitFn"></SubmitComp>
|
||||||
|
</v-navigation-drawer>
|
||||||
|
</span>
|
||||||
|
</v-layout>
|
||||||
|
</v-card>
|
||||||
|
</span>
|
||||||
|
<span v-else>
|
||||||
|
<v-skeleton-loader type="article"></v-skeleton-loader>
|
||||||
|
</span>
|
||||||
|
</v-container>
|
||||||
|
</v-main>
|
||||||
|
<AppFooter />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!------------>
|
||||||
|
<!-- SCRIPT -->
|
||||||
|
<!------------>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
// ------- //
|
||||||
|
// IMPORTS //
|
||||||
|
// ------- //
|
||||||
|
import {
|
||||||
|
computed,
|
||||||
|
nextTick,
|
||||||
|
onBeforeUnmount,
|
||||||
|
onMounted,
|
||||||
|
provide,
|
||||||
|
reactive,
|
||||||
|
ref,
|
||||||
|
toRaw,
|
||||||
|
watch,
|
||||||
|
} from 'vue';
|
||||||
|
import { useConfig } from '@/composables/configuration';
|
||||||
|
import {
|
||||||
|
findObjectByKey,
|
||||||
|
getDisplayName,
|
||||||
|
includeClass,
|
||||||
|
toCURIE,
|
||||||
|
toIRI,
|
||||||
|
} from '@/modules/utils';
|
||||||
|
import editorMatchers from '@/modules/editors';
|
||||||
|
// Leave the viewerMatchers import here to load viewers, even if unused in this component
|
||||||
|
import viewerMatchers from '@/modules/viewers';
|
||||||
|
import defaultEditor from '@/components/UnknownEditor.vue';
|
||||||
|
import { useData } from '@/composables/useData';
|
||||||
|
import { useClasses } from '@/composables/useClasses';
|
||||||
|
import { useShapes } from '@/composables/useShapes';
|
||||||
|
import { useForm } from '@/composables/useForm';
|
||||||
|
import { useToken } from '@/composables/tokens';
|
||||||
|
import { RDFS, SHACL } from '@/modules/namespaces';
|
||||||
|
import { useDisplay } from 'vuetify'
|
||||||
|
import ShaclVueRecordsMini from '@/components/ShaclVueRecordsMini.vue';
|
||||||
|
import { useRecords } from '@/composables/useRecords';
|
||||||
|
import { useNavigation } from '@/composables/useNavigation';
|
||||||
|
import { useSubmit } from '@/composables/useSubmit';
|
||||||
|
import { showWizardGroup } from '@/composables/useWizard'
|
||||||
|
|
||||||
|
// ----- //
|
||||||
|
// PROPS //
|
||||||
|
// ----- //
|
||||||
|
// We only receive the config file url, everything depends on it
|
||||||
|
const props = defineProps({
|
||||||
|
configUrl: String,
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------- //
|
||||||
|
// MAIN DATA FOR UI //
|
||||||
|
// ---------------- //
|
||||||
|
const { mobile } = useDisplay()
|
||||||
|
const page_ready = ref(false);
|
||||||
|
const internalHistory = ref([]);
|
||||||
|
const firstNavigationDone = ref(false);
|
||||||
|
const mainContent = ref(null);
|
||||||
|
var selectedShape = ref(null);
|
||||||
|
var selectedIRI = ref(null);
|
||||||
|
const canSubmit = ref(true);
|
||||||
|
var selectedItem = ref(null);
|
||||||
|
const drawer = mobile.value ? ref(false) : ref(true);
|
||||||
|
const canEditClass = ref(true)
|
||||||
|
const openForms = reactive([]);
|
||||||
|
const allItems = reactive({})
|
||||||
|
const fetchingFirstPages = ref(false);
|
||||||
|
|
||||||
|
// ------------------- //
|
||||||
|
// RUN ALL COMPOSABLES //
|
||||||
|
// ------------------- //
|
||||||
|
// Note: order is important for config and data|classes|chapes|forms
|
||||||
|
// Token handling
|
||||||
|
const { token, setToken, clearToken } = useToken();
|
||||||
|
// Configuration
|
||||||
|
const {
|
||||||
|
allPrefixes,
|
||||||
|
config,
|
||||||
|
configError,
|
||||||
|
configReady,
|
||||||
|
configVarsMain,
|
||||||
|
frontPageHTML,
|
||||||
|
getClassIcon,
|
||||||
|
ID_IRI,
|
||||||
|
mergePrefixes,
|
||||||
|
priorityClassList,
|
||||||
|
processPriorityClasses,
|
||||||
|
processPropertyGroups,
|
||||||
|
processSearchableFields,
|
||||||
|
processShapeUpdates,
|
||||||
|
processUpfrontServiceRequests,
|
||||||
|
searchableFields,
|
||||||
|
} = useConfig(props.configUrl);
|
||||||
|
// Classes from OWL
|
||||||
|
const { classDS, getClassData, allSubClasses, processSubClasses} = useClasses(config);
|
||||||
|
// Shapes from SHACL
|
||||||
|
const {
|
||||||
|
shapesDS,
|
||||||
|
getSHACLschema,
|
||||||
|
updateShapesFromDefault,
|
||||||
|
updateShapes,
|
||||||
|
updatePropertyGroups,
|
||||||
|
idFilteredNodeShapeNames,
|
||||||
|
noEditClassList,
|
||||||
|
filteredNodeShapeNames,
|
||||||
|
priorityFilteredNodeShapeNames,
|
||||||
|
orderedNodeShapeNames,
|
||||||
|
allClassItems,
|
||||||
|
getIdFilteredNodeShapeNames,
|
||||||
|
getNoEditClassList,
|
||||||
|
getFilteredNodeShapeNames,
|
||||||
|
getPriorityFilteredNodeShapeNames,
|
||||||
|
getOrderedNodeShapeNames,
|
||||||
|
getAllClassItems,
|
||||||
|
} = useShapes(config);
|
||||||
|
// Graph data
|
||||||
|
const {
|
||||||
|
fetchedPages,
|
||||||
|
fetchFromService,
|
||||||
|
firstPageFetched,
|
||||||
|
getRdfData,
|
||||||
|
getTotalItems,
|
||||||
|
hasUnfetchedPages,
|
||||||
|
http401response,
|
||||||
|
nodesToSubmit,
|
||||||
|
rdfDS,
|
||||||
|
savedNodes,
|
||||||
|
submitRdfData,
|
||||||
|
submittedNodes,
|
||||||
|
} = useData(config);
|
||||||
|
// Record list
|
||||||
|
const {
|
||||||
|
classRecordsLoading,
|
||||||
|
currentProgress,
|
||||||
|
fetchedItemCount,
|
||||||
|
fetchNextPage,
|
||||||
|
headingHover,
|
||||||
|
includedClasses,
|
||||||
|
includeSubClasses,
|
||||||
|
isFetchingPage,
|
||||||
|
onScrollEnd,
|
||||||
|
onUserTyping,
|
||||||
|
orderTopDown,
|
||||||
|
searchText,
|
||||||
|
showFetchingPageLoader,
|
||||||
|
showProgress,
|
||||||
|
showScrollTopBtn,
|
||||||
|
textMatchType,
|
||||||
|
totalItemCount,
|
||||||
|
recordItemsByClass,
|
||||||
|
filteredRecordItemsAll,
|
||||||
|
} = useRecords(
|
||||||
|
allPrefixes,
|
||||||
|
allSubClasses,
|
||||||
|
config,
|
||||||
|
configVarsMain,
|
||||||
|
fetchFromService,
|
||||||
|
firstPageFetched,
|
||||||
|
hasUnfetchedPages,
|
||||||
|
openForms,
|
||||||
|
rdfDS,
|
||||||
|
searchableFields,
|
||||||
|
selectedIRI,
|
||||||
|
);
|
||||||
|
// Form functionality
|
||||||
|
const {
|
||||||
|
addForm,
|
||||||
|
addInstanceItem,
|
||||||
|
currentOpenForm,
|
||||||
|
editInstanceItem,
|
||||||
|
editMode,
|
||||||
|
formData,
|
||||||
|
formOpen,
|
||||||
|
lastSavedNode,
|
||||||
|
removeForm,
|
||||||
|
} = useForm({
|
||||||
|
openForms,
|
||||||
|
rdfDS,
|
||||||
|
allPrefixes,
|
||||||
|
callbacks: {
|
||||||
|
onAddInstanceItem: afterAddInstanceItem,
|
||||||
|
onEditInstanceItem: afterEditInstanceItem,
|
||||||
|
onAddForm: scrollToTop,
|
||||||
|
onRemoveForm: afterFormsClosed, // will run when last form is closed
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// App navigation
|
||||||
|
const {
|
||||||
|
goBack,
|
||||||
|
handleInternalNavigation,
|
||||||
|
setViewFromQuery,
|
||||||
|
updateURL,
|
||||||
|
} = useNavigation(
|
||||||
|
addInstanceItem,
|
||||||
|
allPrefixes,
|
||||||
|
configVarsMain,
|
||||||
|
editInstanceItem,
|
||||||
|
internalHistory,
|
||||||
|
rdfDS,
|
||||||
|
searchText,
|
||||||
|
selectedItem,
|
||||||
|
selectType,
|
||||||
|
setToken,
|
||||||
|
shapesDS,
|
||||||
|
textMatchType,
|
||||||
|
)
|
||||||
|
// Form submission
|
||||||
|
const {
|
||||||
|
selectedNodesToSubmit,
|
||||||
|
submissionDrawer,
|
||||||
|
submitFn,
|
||||||
|
submitWarning,
|
||||||
|
tokenWarning,
|
||||||
|
} = useSubmit(nodesToSubmit)
|
||||||
|
|
||||||
|
// ------------------------------------------------- //
|
||||||
|
// PROVIDE DATA TO COMPONENTS LOWER IN THE HIERARCHY //
|
||||||
|
// ------------------------------------------------- //
|
||||||
|
provide('allPrefixes', allPrefixes);
|
||||||
|
provide('editMode', editMode);
|
||||||
|
provide('formOpen', formOpen);
|
||||||
|
provide('editInstanceItem', editInstanceItem);
|
||||||
|
provide('addForm', addForm);
|
||||||
|
provide('openForms', openForms);
|
||||||
|
provide('removeForm', removeForm);
|
||||||
|
provide('lastSavedNode', lastSavedNode);
|
||||||
|
provide('canSubmit', canSubmit);
|
||||||
|
provide('formData', formData);
|
||||||
|
provide('config', config);
|
||||||
|
provide('configError', configError);
|
||||||
|
provide('configVarsMain', configVarsMain);
|
||||||
|
provide('ID_IRI', ID_IRI);
|
||||||
|
provide('rdfDS', rdfDS);
|
||||||
|
provide('shapesDS', shapesDS);
|
||||||
|
provide('classDS', classDS);
|
||||||
|
provide('allSubClasses', allSubClasses);
|
||||||
|
provide('fetchFromService', fetchFromService);
|
||||||
|
provide('hasUnfetchedPages', hasUnfetchedPages);
|
||||||
|
provide('getTotalItems', getTotalItems);
|
||||||
|
provide('firstPageFetched', firstPageFetched);
|
||||||
|
provide('http401response', http401response)
|
||||||
|
provide('submitRdfData', submitRdfData);
|
||||||
|
provide('savedNodes', savedNodes);
|
||||||
|
provide('submittedNodes', submittedNodes);
|
||||||
|
provide('nodesToSubmit', nodesToSubmit);
|
||||||
|
provide('searchableFields', searchableFields);
|
||||||
|
provide('editorMatchers', editorMatchers);
|
||||||
|
provide('defaultEditor', defaultEditor);
|
||||||
|
provide('getClassIcon', getClassIcon);
|
||||||
|
provide('submitFn', submitFn);
|
||||||
|
provide('tokenWarning', tokenWarning);
|
||||||
|
provide('submitWarning', submitWarning);
|
||||||
|
|
||||||
|
// --------------------- //
|
||||||
|
// Lifecycle/Vue methods //
|
||||||
|
// --------------------- //
|
||||||
|
// Once config is loaded and processed (a.k.a. ready) we load all shapes and data
|
||||||
|
watch(
|
||||||
|
configReady,
|
||||||
|
async (newValue) => {
|
||||||
|
if (newValue) {
|
||||||
|
formData.ID_IRI = ID_IRI.value;
|
||||||
|
await getRdfData();
|
||||||
|
await getClassData();
|
||||||
|
await getSHACLschema();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
// Some config processing can only be done once all shapes and data have been loaded (which we know by
|
||||||
|
// watching "shapes prefixes loaded" as a proxy). This is mainly because we require all prefixes.
|
||||||
|
// - merge all prefixes
|
||||||
|
// - fetch data from service (if necessary)
|
||||||
|
// - set component states from URL query parameters (if necessary)
|
||||||
|
watch(
|
||||||
|
() => shapesDS.data.prefixesLoaded,
|
||||||
|
async (newValue) => {
|
||||||
|
if (newValue) {
|
||||||
|
// Get all prefixes and derive context from it
|
||||||
|
// Prefixes are necessary for all the following steps
|
||||||
|
mergePrefixes([shapesDS.data.prefixes, rdfDS.data.prefixes, classDS.data.prefixes])
|
||||||
|
// Get list of priority classes from config
|
||||||
|
processPriorityClasses();
|
||||||
|
// Fetch data if configured via 'service_fetch_before'
|
||||||
|
processUpfrontServiceRequests(fetchFromService)
|
||||||
|
// Process configured shape updates
|
||||||
|
processShapeUpdates(updateShapesFromDefault, updateShapes)
|
||||||
|
// Add any configured property groups to shapes dataset
|
||||||
|
processPropertyGroups(updatePropertyGroups)
|
||||||
|
// Prepare allSubClasses object with class URIs as keys, and their respective subclass URIs as arrays
|
||||||
|
// This is required for the InstancesSelectEditor
|
||||||
|
processSubClasses();
|
||||||
|
// Now transform/derive the searchable fields for "filter records by"
|
||||||
|
processSearchableFields();
|
||||||
|
// Set component states from URL query parameters
|
||||||
|
setViewFromQuery();
|
||||||
|
|
||||||
|
// Get all class-related data
|
||||||
|
idFilteredNodeShapeNames.value = getIdFilteredNodeShapeNames(configVarsMain, ID_IRI);
|
||||||
|
filteredNodeShapeNames.value = getFilteredNodeShapeNames(configVarsMain, allPrefixes);
|
||||||
|
priorityFilteredNodeShapeNames.value = getPriorityFilteredNodeShapeNames(priorityClassList);
|
||||||
|
orderedNodeShapeNames.value = getOrderedNodeShapeNames(configVarsMain, allPrefixes);
|
||||||
|
allClassItems.value = getAllClassItems(configVarsMain, allPrefixes, getClassIcon);
|
||||||
|
page_ready.value = true
|
||||||
|
// then fetch first page per class
|
||||||
|
fetchingFirstPages.value = true;
|
||||||
|
await getFirstPages();
|
||||||
|
fetchingFirstPages.value = false;
|
||||||
|
// Starter components needs 'selectType' to only run once at startup
|
||||||
|
let mainIRI = 'https://concepts.datalad.org/s/things/v2/Thing';
|
||||||
|
selectType(mainIRI, true, false, true)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
// Warn if there are any pending records to submit when the user closes the tab/window
|
||||||
|
onMounted(() => {
|
||||||
|
window.addEventListener('beforeunload', handleBeforeUnload);
|
||||||
|
});
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
window.removeEventListener('beforeunload', handleBeforeUnload);
|
||||||
|
});
|
||||||
|
|
||||||
|
// -------------- //
|
||||||
|
// Computed props //
|
||||||
|
// -------------- //
|
||||||
|
const itemsByClass = computed(() => {
|
||||||
|
const map = {};
|
||||||
|
for (const item of filteredRecordItemsAll.value) {
|
||||||
|
const cl = item.props.subtitle;
|
||||||
|
if (!map[cl]) map[cl] = [];
|
||||||
|
map[cl].push(item);
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
});
|
||||||
|
|
||||||
|
const visibleClasses = computed(() =>
|
||||||
|
allClassItems.value.filter(cl =>
|
||||||
|
classSelection.includes(cl.value) &&
|
||||||
|
itemsByClass.value[cl.value]?.length
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const searchInput = ref('');
|
||||||
|
let debounceTypingTimer = null;
|
||||||
|
watch(searchInput, (val) => {
|
||||||
|
clearTimeout(debounceTypingTimer);
|
||||||
|
|
||||||
|
debounceTypingTimer = setTimeout(() => {
|
||||||
|
searchText.value = val;
|
||||||
|
}, 200); // or 300ms
|
||||||
|
});
|
||||||
|
|
||||||
|
const allValues = computed(() => allClassItems.value.map(i => i.value))
|
||||||
|
const allSelected = computed(() =>
|
||||||
|
allValues.value.length > 0 &&
|
||||||
|
allValues.value.every(v => classSelection.value.includes(v))
|
||||||
|
)
|
||||||
|
const isIndeterminate = computed(() =>
|
||||||
|
classSelection.value.length > 0 &&
|
||||||
|
classSelection.value.length < allValues.value.length
|
||||||
|
)
|
||||||
|
const classSelection = ref([])
|
||||||
|
watch(
|
||||||
|
allClassItems,
|
||||||
|
(items) => {
|
||||||
|
if (items?.length) {
|
||||||
|
classSelection.value = items.map(i => i.value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
function toggleAll(val) {
|
||||||
|
classSelection.value = val ? [...allValues.value] : []
|
||||||
|
}
|
||||||
|
watch(
|
||||||
|
classSelection,
|
||||||
|
(items) => {
|
||||||
|
includedClasses.value = classSelection.value;
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
// --------- //
|
||||||
|
// Functions //
|
||||||
|
// --------- //
|
||||||
|
async function getFirstPages() {
|
||||||
|
const promises = allClassItems.value.map(async (item) => {
|
||||||
|
const result = await fetchFromService(
|
||||||
|
'get-paginated-records-constrained',
|
||||||
|
item.value,
|
||||||
|
allPrefixes
|
||||||
|
);
|
||||||
|
const totalItems = getTotalItems(item.value);
|
||||||
|
if (totalItems > 0) {
|
||||||
|
item.props.totalItemCount = totalItems;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await Promise.all(promises);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onScrollEndOfClass(classIRI) {
|
||||||
|
console.log(`Scroll end for class: ${classIRI}`)
|
||||||
|
fetchNextPage(classIRI)
|
||||||
|
}
|
||||||
|
|
||||||
|
function afterFormsClosed() {
|
||||||
|
drawer.value = mobile.value ? false : true;
|
||||||
|
canSubmit.value = true;
|
||||||
|
classRecordsLoading.value = false;
|
||||||
|
updateURL(selectedIRI.value, false, null, allPrefixes);
|
||||||
|
}
|
||||||
|
|
||||||
|
function afterAddInstanceItem() {
|
||||||
|
if (mobile.value) drawer.value = false;
|
||||||
|
canSubmit.value = false;
|
||||||
|
updateURL(selectedIRI.value, true, null, allPrefixes);
|
||||||
|
}
|
||||||
|
|
||||||
|
function afterEditInstanceItem(editShapeIRI, editItemIdx) {
|
||||||
|
if (mobile.value) drawer.value = false;
|
||||||
|
canSubmit.value = false;
|
||||||
|
updateURL(editShapeIRI, true, editItemIdx, allPrefixes);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleBeforeUnload(event) {
|
||||||
|
if (nodesToSubmit.value.length > 0) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.returnValue = '';
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function scrollToTop() {
|
||||||
|
nextTick(() => {
|
||||||
|
const el = mainContent.value?.$el || mainContent.value;
|
||||||
|
if (el) el.scrollTop = 0;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearField() {
|
||||||
|
searchInput.value = '';
|
||||||
|
searchText.value = '';
|
||||||
|
textMatchType.value = 'partial';
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleOrder() {
|
||||||
|
orderTopDown.value = !orderTopDown.value;
|
||||||
|
if (orderTopDown.value) {
|
||||||
|
orderIcon.value = 'mdi-arrow-down-thick';
|
||||||
|
} else {
|
||||||
|
orderIcon.value = 'mdi-arrow-up-thick';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async function selectType(IRI, fromUser, fromBackButton, includeSubs=false) {
|
||||||
|
// Let's document this
|
||||||
|
// Currently, this is called either when a class is selected from the side pane
|
||||||
|
// or when a class is specified via URL query parameter.
|
||||||
|
// Much of what happens on selection is specific to rendering a single class
|
||||||
|
// and its items for the conventional `ShaclVue` component.
|
||||||
|
// Apart from that, which steps are actually minimally necessary?
|
||||||
|
|
||||||
|
var tempIncludeSubs = includeSubClasses.value;
|
||||||
|
includeSubClasses.value = includeSubs ? includeSubs : false;
|
||||||
|
fetchedItemCount.value = null;
|
||||||
|
totalItemCount.value = 0
|
||||||
|
isFetchingPage.value = false;
|
||||||
|
showScrollTopBtn.value = false;
|
||||||
|
var tempSearchText = searchText.value;
|
||||||
|
var tempIRI = selectedIRI.value;
|
||||||
|
searchText.value = '';
|
||||||
|
textMatchType.value = 'partial';
|
||||||
|
selectedIRI.value = IRI;
|
||||||
|
selectedShape.value = shapesDS.data.nodeShapes[IRI];
|
||||||
|
canEditClass.value = configVarsMain.noEditClasses.indexOf(toCURIE(IRI, allPrefixes)) < 0 ? true : false;
|
||||||
|
scrollToTop();
|
||||||
|
if (fromUser) {
|
||||||
|
updateURL(IRI, false, null, allPrefixes);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fromUser || fromBackButton) {
|
||||||
|
selectedItem.value = [IRI];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (firstNavigationDone.value) {
|
||||||
|
if (!fromBackButton) {
|
||||||
|
if (IRI != tempIRI) {
|
||||||
|
internalHistory.value.push({
|
||||||
|
iri: tempIRI,
|
||||||
|
searchText: tempSearchText,
|
||||||
|
includeSubs: tempIncludeSubs,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
firstNavigationDone.value = true;
|
||||||
|
}
|
||||||
|
if (mobile.value) {
|
||||||
|
drawer.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onTokenDialogOpened() {
|
||||||
|
// Replace url with one where token is not included
|
||||||
|
const url = new URL(window.location);
|
||||||
|
url.searchParams.delete('token');
|
||||||
|
window.history.replaceState(null, '', url);
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.code-style {
|
||||||
|
color: #ff0000;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
padding: 0.1em 0.2em;
|
||||||
|
font-family: monospace;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
}
|
||||||
|
.v-expansion-panel-text {
|
||||||
|
display: unset !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
.v-expansion-panel-text__wrapper {
|
||||||
|
margin: 0 !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--link-color);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
color: var(--hover-color);
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:visited {
|
||||||
|
color: var(--visited-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
a:active {
|
||||||
|
color: var(--active-color);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.opacity-column {
|
||||||
|
opacity: 0.5; /* Set opacity value between 0 (fully transparent) and 1 (fully opaque) */
|
||||||
|
}
|
||||||
|
.custompanels {
|
||||||
|
border: 1px solid #ccc !important; /* Change to your preferred color */
|
||||||
|
box-shadow: none !important; /* Remove elevation */
|
||||||
|
border-radius: 8px; /* Optional: Adjust border rounding */
|
||||||
|
}
|
||||||
|
.custompanels .v-expansion-panel {
|
||||||
|
border-bottom: 1px solid #ddd !important; /* Adds a subtle divider between panels */
|
||||||
|
box-shadow: none !important;
|
||||||
|
border-radius: 8px; /* Optional: Adjust border rounding */
|
||||||
|
}
|
||||||
|
.drawer-fab {
|
||||||
|
position: fixed;
|
||||||
|
top: var(--v-layout-top);
|
||||||
|
left: 0;
|
||||||
|
border-radius: 0 6px 6px 0;
|
||||||
|
box-shadow: 2px 0 6px rgba(0, 0, 0, 0.3);
|
||||||
|
z-index: 2000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row-sheet {
|
||||||
|
max-height: 30vh;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-col {
|
||||||
|
height: 100%;
|
||||||
|
overflow-y: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-col {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-col-div {
|
||||||
|
max-height: 30vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -12,6 +12,7 @@
|
||||||
<v-textarea
|
<v-textarea
|
||||||
v-else-if="input.type === 'text-paragraph'"
|
v-else-if="input.type === 'text-paragraph'"
|
||||||
v-model="value"
|
v-model="value"
|
||||||
|
:placeholder="input.placeholder"
|
||||||
:rules="rules"
|
:rules="rules"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
density="compact"
|
density="compact"
|
||||||
|
|
|
||||||
|
|
@ -88,7 +88,7 @@ const mainVarsToLoad = {
|
||||||
service_base_url: [],
|
service_base_url: [],
|
||||||
service_constrained_search: {
|
service_constrained_search: {
|
||||||
min_characters: 4,
|
min_characters: 4,
|
||||||
typing_debounce: 800,
|
typing_debounce: 500,
|
||||||
},
|
},
|
||||||
class_name_display: 'name',
|
class_name_display: 'name',
|
||||||
footer_links: [],
|
footer_links: [],
|
||||||
|
|
|
||||||
|
|
@ -271,23 +271,25 @@ export function useData(config) {
|
||||||
// First is fetching a single TTL document that expects TTL text
|
// First is fetching a single TTL document that expects TTL text
|
||||||
if (expect === 'ttl') {
|
if (expect === 'ttl') {
|
||||||
const text = await response.text();
|
const text = await response.text();
|
||||||
await rdfDS.parseTTLandDedup(text);
|
let ttlData = await rdfDS.parseTTLandDedup(text);
|
||||||
rdfDS.triggerReactivity();
|
rdfDS.emitAddedRecords(ttlData.records)
|
||||||
return { success: true, url: getURL };
|
return { success: true, url: getURL };
|
||||||
}
|
}
|
||||||
// Other response types both expect json first
|
// Other response types both expect json first
|
||||||
const json = await response.json();
|
const json = await response.json();
|
||||||
// JSON array of TTL strings
|
// JSON array of TTL strings
|
||||||
if (expect === 'json') {
|
if (expect === 'json') {
|
||||||
|
let jsonRecords = [];
|
||||||
for (const element of json) {
|
for (const element of json) {
|
||||||
await rdfDS.parseTTLandDedup(element);
|
let jsonData = await rdfDS.parseTTLandDedup(element);
|
||||||
|
jsonRecords = jsonRecords.concat(jsonData.records)
|
||||||
}
|
}
|
||||||
rdfDS.triggerReactivity();
|
rdfDS.emitAddedRecords(jsonRecords)
|
||||||
return { success: true, url: getURL };
|
return { success: true, url: getURL };
|
||||||
}
|
}
|
||||||
// Paginated response
|
// Paginated response
|
||||||
if (expect === 'paged') {
|
if (expect === 'paged') {
|
||||||
|
let pagedRecords = [];
|
||||||
const metadata = {
|
const metadata = {
|
||||||
page: json.page,
|
page: json.page,
|
||||||
pages: json.pages,
|
pages: json.pages,
|
||||||
|
|
@ -295,9 +297,10 @@ export function useData(config) {
|
||||||
size: json.size,
|
size: json.size,
|
||||||
};
|
};
|
||||||
for (const element of json.items) {
|
for (const element of json.items) {
|
||||||
await rdfDS.parseTTLandDedup(element);
|
let pagedData = await rdfDS.parseTTLandDedup(element);
|
||||||
|
pagedRecords = pagedRecords.concat(pagedData.records)
|
||||||
}
|
}
|
||||||
rdfDS.triggerReactivity();
|
rdfDS.emitAddedRecords(pagedRecords)
|
||||||
return { success: true, url: getURL, pageMeta: metadata };
|
return { success: true, url: getURL, pageMeta: metadata };
|
||||||
}
|
}
|
||||||
throw new Error(`Unsupported fetch type: ${expect}`);
|
throw new Error(`Unsupported fetch type: ${expect}`);
|
||||||
|
|
|
||||||
|
|
@ -9,13 +9,14 @@ It contains all functionality related to the display and interactions of
|
||||||
records on the main page.
|
records on the main page.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ref, computed, watch, watchEffect} from 'vue';
|
import { ref, computed, watch, watchEffect, onMounted, nextTick, reactive} from 'vue';
|
||||||
import { debounce } from 'lodash-es';
|
import { debounce } from 'lodash-es';
|
||||||
import { RDF, SKOS } from '@/modules/namespaces';
|
import { RDF, SKOS } from '@/modules/namespaces';
|
||||||
import {
|
import {
|
||||||
getConfigDisplayLabel,
|
getConfigDisplayLabel,
|
||||||
hasConfigDisplayLabel,
|
hasConfigDisplayLabel,
|
||||||
toCURIE,
|
toCURIE,
|
||||||
|
getPidQuad,
|
||||||
} from '@/modules/utils';
|
} from '@/modules/utils';
|
||||||
import { DataFactory } from 'n3';
|
import { DataFactory } from 'n3';
|
||||||
const { namedNode } = DataFactory;
|
const { namedNode } = DataFactory;
|
||||||
|
|
@ -38,24 +39,42 @@ export function useRecords(
|
||||||
// ---- //
|
// ---- //
|
||||||
const totalItemCount = ref(0);
|
const totalItemCount = ref(0);
|
||||||
const isFetchingPage = ref(false);
|
const isFetchingPage = ref(false);
|
||||||
const showScrollTopBtn = ref(false);
|
|
||||||
const showFetchingPageLoader = ref(false)
|
const showFetchingPageLoader = ref(false)
|
||||||
const searchText = ref('');
|
const searchText = ref('');
|
||||||
const textMatchType = ref('partial');
|
const textMatchType = ref('partial');
|
||||||
const instanceItemsComp = ref([]);
|
|
||||||
const newTypeSelected = ref(false);
|
|
||||||
const itemsTrigger = ref(false);
|
|
||||||
const fetchedItemCount = ref(null)
|
|
||||||
const classRecordsLoading = ref(false);
|
const classRecordsLoading = ref(false);
|
||||||
const headingHover = ref(false);
|
const headingHover = ref(false);
|
||||||
const orderTopDown = ref(true);
|
const orderTopDown = ref(true);
|
||||||
const includeSubClasses = ref(false);
|
const includeSubClasses = ref(false);
|
||||||
|
const includedClasses = ref(null);
|
||||||
let hideTimeout = null
|
let hideTimeout = null
|
||||||
let debounceTypingTimer = null;
|
let debounceTypingTimer = null;
|
||||||
|
const itemQueue = new Set();
|
||||||
|
let isProcessingItemQueue = false;
|
||||||
|
// We needed to decide between 1 and 2:
|
||||||
|
// 1: shallowRef({}), with shallow copy every time a new item is added
|
||||||
|
// records.value = {
|
||||||
|
// ...records.value,
|
||||||
|
// [pid]: item
|
||||||
|
// };
|
||||||
|
// The shallow copy is necessary in order to trigger reactivity because a (shallow?) ref does not
|
||||||
|
// track deeper reactivity.
|
||||||
|
// 2: reactive({})
|
||||||
|
// After consideration: using ref([]) and shallowRef([]) means every time a record is added we first
|
||||||
|
// need to check if it exists in the array already, which is another step that adds to the cost.
|
||||||
|
// i.e. => use reactive.
|
||||||
|
const recordItemsAll = reactive({});
|
||||||
|
const recordItemsByClass = reactive({});
|
||||||
|
|
||||||
// --------------------- //
|
// --------------------- //
|
||||||
// Lifecycle/Vue methods //
|
// Lifecycle/Vue methods //
|
||||||
// --------------------- //
|
// --------------------- //
|
||||||
|
onMounted(() => {
|
||||||
|
rdfDS.addEventListener('recordsChanged', (e) => {
|
||||||
|
enqueueChanges(e.detail.records);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
watch(isFetchingPage, (newVal) => {
|
watch(isFetchingPage, (newVal) => {
|
||||||
if (newVal) {
|
if (newVal) {
|
||||||
// If fetching starts → show immediately
|
// If fetching starts → show immediately
|
||||||
|
|
@ -73,20 +92,6 @@ export function useRecords(
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(
|
|
||||||
instanceItemsComp,
|
|
||||||
(newVal, oldVal) => {
|
|
||||||
if (newTypeSelected.value) {
|
|
||||||
newTypeSelected.value = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (classRecordsLoading.value) {
|
|
||||||
classRecordsLoading.value = false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ deep: true }
|
|
||||||
);
|
|
||||||
|
|
||||||
watchEffect(async () => {
|
watchEffect(async () => {
|
||||||
// If we are using a backend service AND
|
// If we are using a backend service AND
|
||||||
// there are a minimum amount of characters in the search field AND
|
// there are a minimum amount of characters in the search field AND
|
||||||
|
|
@ -98,7 +103,7 @@ export function useRecords(
|
||||||
hasUnfetchedPages(selectedIRI.value, searchText.value)) {
|
hasUnfetchedPages(selectedIRI.value, searchText.value)) {
|
||||||
// Only trigger fetch if not already fetching
|
// Only trigger fetch if not already fetching
|
||||||
if (!isFetchingPage.value) {
|
if (!isFetchingPage.value) {
|
||||||
await fetchNextPage(searchText.value);
|
await fetchNextPage(selectedIRI.value, searchText.value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -112,13 +117,6 @@ export function useRecords(
|
||||||
onTypingPause(newVal);
|
onTypingPause(newVal);
|
||||||
}, configVarsMain.serviceConstrainedSearch.typing_debounce);
|
}, configVarsMain.serviceConstrainedSearch.typing_debounce);
|
||||||
});
|
});
|
||||||
// regenerate list if the graph data is updated
|
|
||||||
const debouncedUpdate = debounce(() => {
|
|
||||||
if (openForms.length == 0) {
|
|
||||||
getInstanceItems();
|
|
||||||
}
|
|
||||||
}, 500);
|
|
||||||
watch(() => rdfDS.data.graphChanged, debouncedUpdate, { deep: true });
|
|
||||||
|
|
||||||
// -------------- //
|
// -------------- //
|
||||||
// Computed props //
|
// Computed props //
|
||||||
|
|
@ -143,38 +141,78 @@ export function useRecords(
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const filteredInstanceItemsComp = computed(() => {
|
const showScrollTopBtn = computed(() => {
|
||||||
let txt = searchText.value.toLowerCase().trim();
|
if (filteredRecordItemsByClass.value[selectedIRI.value]?.length > 7) return true;
|
||||||
return sortItems(
|
return false;
|
||||||
[...instanceItemsComp.value].filter((item) => {
|
|
||||||
if (txt.length == 0) return true;
|
|
||||||
return searchableFields.some((field) => {
|
|
||||||
if (!(field in item.props)) return false;
|
|
||||||
const value = item.props[field]?.toString().toLowerCase().trim();
|
|
||||||
if (Array.isArray(value)) {
|
|
||||||
return value.some((val) => {
|
|
||||||
return val.includes(txt);
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
return value.includes(txt);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const fetchedItemCount = computed(() => {
|
||||||
|
if (includeSubClasses.value && Array.isArray(allSubClasses[selectedIRI.value]) && allSubClasses[selectedIRI.value].length > 0 ) {
|
||||||
|
let allclass_array = [selectedIRI.value].concat(allSubClasses[selectedIRI.value])
|
||||||
|
let itemCount = 0;
|
||||||
|
for (const cl of allclass_array) {
|
||||||
|
if (recordItemsByClass[cl]) {
|
||||||
|
itemCount += Object.values(recordItemsByClass[cl]).length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return itemCount;
|
||||||
|
}
|
||||||
|
if (recordItemsByClass[selectedIRI.value]) {
|
||||||
|
return Object.values(recordItemsByClass[selectedIRI.value]).length;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
const filteredRecordItemsAll = computed(() => {
|
||||||
|
let txt = searchText.value.toLowerCase().trim();
|
||||||
|
if (txt.length == 0) return sortItems(Object.values(recordItemsAll))
|
||||||
|
return sortItems(
|
||||||
|
Object.values(recordItemsAll).filter((item) => {
|
||||||
|
if (txt.length == 0) return true;
|
||||||
|
if (!('_searchBlob' in item.props)) return false;
|
||||||
|
return item.props._searchBlob.includes(txt);
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
const matchedInstanceItemsComp = computed(() => {
|
const filteredRecordItemsByClass = computed(() => {
|
||||||
let txt = searchText.value.toLowerCase().trim();
|
let txt = searchText.value.toLowerCase().trim();
|
||||||
return sortItems(
|
const map = {};
|
||||||
[...instanceItemsComp.value].filter((item) => {
|
for (const cl of Object.keys(recordItemsByClass)) {
|
||||||
|
map[cl] = sortItems(
|
||||||
|
Object.values(recordItemsByClass[cl]).filter((item) => {
|
||||||
if (txt.length == 0) return true;
|
if (txt.length == 0) return true;
|
||||||
|
if (!('_searchBlob' in item.props)) return false;
|
||||||
|
if (textMatchType.value == 'exact') {
|
||||||
return searchableFields.some((field) => {
|
return searchableFields.some((field) => {
|
||||||
if (!(field in item.props)) return false;
|
if (!(field in item.props)) return false;
|
||||||
const value = item.props[field]?.toString().toLowerCase().trim();
|
const value = item.props[field]?.toString().toLowerCase().trim();
|
||||||
return value === txt;
|
return value === txt;
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
return item.props._searchBlob.includes(txt);
|
||||||
|
}
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
return map
|
||||||
|
});
|
||||||
|
|
||||||
|
const filteredRecordItemsForClassWithSubclassItems = computed(() => {
|
||||||
|
let items = [];
|
||||||
|
if (!includeSubClasses.value) {
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
if (Array.isArray(allSubClasses[selectedIRI.value]) && allSubClasses[selectedIRI.value].length > 0 ) {
|
||||||
|
let allclass_array = [selectedIRI.value].concat(allSubClasses[selectedIRI.value])
|
||||||
|
for (const cl of allclass_array) {
|
||||||
|
if (filteredRecordItemsByClass.value[cl]) {
|
||||||
|
items = items.concat(filteredRecordItemsByClass.value[cl])
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sortItems(items)
|
||||||
});
|
});
|
||||||
|
|
||||||
// --------- //
|
// --------- //
|
||||||
|
|
@ -183,22 +221,21 @@ export function useRecords(
|
||||||
|
|
||||||
// fetch new items at bottom of scroller
|
// fetch new items at bottom of scroller
|
||||||
function onScrollEnd() {
|
function onScrollEnd() {
|
||||||
debouncedScrollEnd();
|
debouncedScrollEnd(selectedIRI.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
const debouncedScrollEnd = debounce(async () => {
|
const debouncedScrollEnd = debounce(async (classIRI) => {
|
||||||
// Only fetch new items at bottom of scroller if there is not any search text
|
// Only fetch new items at bottom of scroller if there is not any search text
|
||||||
// Continued fetching of more items while there is search text will be handled
|
// Continued fetching of more items while there is search text will be handled
|
||||||
// by the watcheffect function.
|
// by the watcheffect function.
|
||||||
if (searchText.value) {
|
if (searchText.value) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.value.use_service) {
|
if (config.value.use_service) {
|
||||||
if (hasUnfetchedPages(selectedIRI.value) && !isFetchingPage.value) {
|
if (hasUnfetchedPages(classIRI) && !isFetchingPage.value) {
|
||||||
await fetchNextPage();
|
await fetchNextPage(classIRI);
|
||||||
} else {
|
} else {
|
||||||
console.log("Last page already fetched")
|
console.log(`Last page already fetched: ${classIRI}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, 1000);
|
}, 1000);
|
||||||
|
|
@ -211,7 +248,7 @@ export function useRecords(
|
||||||
|
|
||||||
async function onTypingPause(textVal) {
|
async function onTypingPause(textVal) {
|
||||||
if (!searchText.value || searchText.value.length < configVarsMain.serviceConstrainedSearch.min_characters ) return;
|
if (!searchText.value || searchText.value.length < configVarsMain.serviceConstrainedSearch.min_characters ) return;
|
||||||
await fetchNextPage(searchText.value);
|
await fetchNextPage(selectedIRI.value, searchText.value);
|
||||||
}
|
}
|
||||||
// User types, debounce effect monitors pauses and waits for configured time
|
// User types, debounce effect monitors pauses and waits for configured time
|
||||||
// before making the first constrained request.
|
// before making the first constrained request.
|
||||||
|
|
@ -225,20 +262,19 @@ export function useRecords(
|
||||||
// Continued fetching of more items while there is search text is handled by
|
// Continued fetching of more items while there is search text is handled by
|
||||||
// the watcheffect function.
|
// the watcheffect function.
|
||||||
|
|
||||||
async function fetchNextPage(matchText='') {
|
async function fetchNextPage(classIRI, matchText='') {
|
||||||
if (isFetchingPage.value || !hasUnfetchedPages(selectedIRI.value, matchText)) return;
|
if (isFetchingPage.value || !hasUnfetchedPages(classIRI, matchText)) return;
|
||||||
isFetchingPage.value = true;
|
isFetchingPage.value = true;
|
||||||
try {
|
try {
|
||||||
const result = await fetchFromService(
|
const result = await fetchFromService(
|
||||||
'get-paginated-records-constrained',
|
'get-paginated-records-constrained',
|
||||||
selectedIRI.value,
|
classIRI,
|
||||||
allPrefixes,
|
allPrefixes,
|
||||||
matchText
|
matchText
|
||||||
);
|
);
|
||||||
if (result.status === null) {
|
if (result.status === null) {
|
||||||
console.error(result.error);
|
console.error(result.error);
|
||||||
}
|
}
|
||||||
getInstanceItems(); // rebuild local list of items
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -246,57 +282,76 @@ export function useRecords(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getInstanceItems() {
|
function getSortValue(item) {
|
||||||
// ---
|
for (const field of searchableFields) {
|
||||||
// The goal of this method is to populate the list of data objects of the selected type
|
const value = item.props[field];
|
||||||
// ---
|
if (value) return value.toString().toLowerCase().trim();
|
||||||
var x = itemsTrigger.value;
|
|
||||||
if (!selectedIRI.value) {
|
|
||||||
return [];
|
|
||||||
}
|
}
|
||||||
// find nodes with triple predicate == rdf:type, and triple object == the selected class
|
return null;
|
||||||
// if the class is a configured priority class with include_subclasses = true, find nodes
|
|
||||||
// for the selected class and all of its subclasses
|
|
||||||
var quads;
|
|
||||||
if (includeSubClasses.value) {
|
|
||||||
let allclass_array = [selectedIRI.value]
|
|
||||||
if (Array.isArray(allSubClasses[selectedIRI.value]) && allSubClasses[selectedIRI.value].length > 0 ) {
|
|
||||||
allclass_array = allclass_array.concat(allSubClasses[selectedIRI.value])
|
|
||||||
}
|
}
|
||||||
quads = [];
|
|
||||||
for (const cl of allclass_array) {
|
function sortItems(arr) {
|
||||||
const mySubArray = rdfDS.getLiteralAndNamedNodes(
|
const c = orderTopDown.value ? 1 : -1;
|
||||||
namedNode(RDF.type.value),
|
return arr.sort((a, b) => {
|
||||||
cl,
|
const aVal = getSortValue(a);
|
||||||
allPrefixes
|
const bVal = getSortValue(b);
|
||||||
)
|
// if both are missing labels, consider them equal
|
||||||
quads = quads.concat(mySubArray);
|
if (!aVal && !bVal) return 0;
|
||||||
|
// if only a is missing, a goes first
|
||||||
|
if (!aVal) return -1 * c;
|
||||||
|
// if only b is missing, b goes first
|
||||||
|
if (!bVal) return 1 * c;
|
||||||
|
// otherwise compare alphabetically
|
||||||
|
return c * aVal.localeCompare(bVal);
|
||||||
|
})
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
quads = rdfDS.getLiteralAndNamedNodes(
|
function enqueueChanges(records) {
|
||||||
namedNode(RDF.type.value),
|
for (const r of records) {
|
||||||
selectedIRI.value,
|
itemQueue.add(r);
|
||||||
allPrefixes
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
// Create list items from quads
|
processQueue();
|
||||||
var instanceItemsArr = [];
|
|
||||||
quads.forEach((quad) => {
|
|
||||||
var extra = '';
|
|
||||||
if (quad.subject.termType === 'BlankNode') {
|
|
||||||
extra = ' (BlankNode)';
|
|
||||||
}
|
}
|
||||||
var relatedTrips = rdfDS.getSubjectTriples(quad.subject);
|
|
||||||
|
async function processQueue() {
|
||||||
|
if (isProcessingItemQueue) return;
|
||||||
|
isProcessingItemQueue = true;
|
||||||
|
while (itemQueue.size > 0) {
|
||||||
|
// take a batch
|
||||||
|
const batch = Array.from(itemQueue).slice(0, 10);
|
||||||
|
batch.forEach((record) =>{
|
||||||
|
itemQueue.delete(record)
|
||||||
|
updateRecordItem(record)
|
||||||
|
});
|
||||||
|
// yield to UI thread
|
||||||
|
await nextTick();
|
||||||
|
}
|
||||||
|
isProcessingItemQueue = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateRecordItem(record) {
|
||||||
|
// This function does not care if a record item already exists in the
|
||||||
|
// object; it builds the item and adds it nevertheless.
|
||||||
|
// First we get the record's quad
|
||||||
|
let mainQuad = getPidQuad(record, rdfDS.data.graph);
|
||||||
|
if (!mainQuad) {
|
||||||
|
console.log(`No PID quad found in graph for record: ${record}; skipping update of this item`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let recordClass = mainQuad.object.value;
|
||||||
|
// Now get related quads
|
||||||
|
var relatedTrips = rdfDS.getSubjectTriples(mainQuad.subject);
|
||||||
|
// Initialize item
|
||||||
var item = {
|
var item = {
|
||||||
title: quad.subject.value + extra,
|
title: record,
|
||||||
value: quad.subject.value,
|
value: record,
|
||||||
props: {
|
props: {
|
||||||
subtitle: quad.object.value,
|
subtitle: recordClass,
|
||||||
quad: quad,
|
quad: mainQuad,
|
||||||
itemValue: quad.subject.value,
|
itemValue: record,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
let labelTemplate = hasConfigDisplayLabel(quad.object.value, allPrefixes, configVarsMain)
|
let labelTemplate = hasConfigDisplayLabel(recordClass, allPrefixes, configVarsMain)
|
||||||
let labelParts = {}
|
let labelParts = {}
|
||||||
relatedTrips.forEach((quad) => {
|
relatedTrips.forEach((quad) => {
|
||||||
if (!Object.hasOwn(item.props, quad.predicate.value)) {
|
if (!Object.hasOwn(item.props, quad.predicate.value)) {
|
||||||
|
|
@ -333,35 +388,26 @@ export function useRecords(
|
||||||
item.props._displayLabel = displayLabel;
|
item.props._displayLabel = displayLabel;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
instanceItemsArr.push(item);
|
// Now put together single searchable blob
|
||||||
});
|
item.props._searchBlob = ''
|
||||||
instanceItemsComp.value = [...instanceItemsArr];
|
|
||||||
if (instanceItemsComp.value.length > 7) showScrollTopBtn.value = true;
|
|
||||||
fetchedItemCount.value = instanceItemsComp.value.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSortValue(item) {
|
|
||||||
for (const field of searchableFields) {
|
for (const field of searchableFields) {
|
||||||
const value = item.props[field];
|
if (!(field in item.props)) continue;
|
||||||
if (value) return value.toString().toLowerCase().trim();
|
let value = item.props[field]?.toString().toLowerCase().trim();
|
||||||
|
if (!Array.isArray(value)) {
|
||||||
|
value = [value]
|
||||||
}
|
}
|
||||||
return null;
|
for (const v of value) {
|
||||||
|
item.props._searchBlob = item.props._searchBlob + v
|
||||||
}
|
}
|
||||||
|
}
|
||||||
function sortItems(arr) {
|
// Now that we have the complete item, we can add it to the tracking objects
|
||||||
const c = orderTopDown.value ? 1 : -1;
|
// Class records tracker
|
||||||
return arr.sort((a, b) => {
|
if (!recordItemsByClass.hasOwnProperty(recordClass)) {
|
||||||
const aVal = getSortValue(a);
|
recordItemsByClass[recordClass] = {};
|
||||||
const bVal = getSortValue(b);
|
}
|
||||||
// if both are missing labels, consider them equal
|
recordItemsByClass[recordClass][record] = item;
|
||||||
if (!aVal && !bVal) return 0;
|
// All records tracker
|
||||||
// if only a is missing, a goes first
|
recordItemsAll[record] = item;
|
||||||
if (!aVal) return -1 * c;
|
|
||||||
// if only b is missing, b goes first
|
|
||||||
if (!bVal) return 1 * c;
|
|
||||||
// otherwise compare alphabetically
|
|
||||||
return c * aVal.localeCompare(bVal);
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------- //
|
// ------- //
|
||||||
|
|
@ -371,14 +417,11 @@ export function useRecords(
|
||||||
classRecordsLoading,
|
classRecordsLoading,
|
||||||
currentProgress,
|
currentProgress,
|
||||||
fetchedItemCount,
|
fetchedItemCount,
|
||||||
filteredInstanceItemsComp,
|
fetchNextPage,
|
||||||
getInstanceItems,
|
|
||||||
headingHover,
|
headingHover,
|
||||||
|
includedClasses,
|
||||||
includeSubClasses,
|
includeSubClasses,
|
||||||
instanceItemsComp,
|
|
||||||
isFetchingPage,
|
isFetchingPage,
|
||||||
matchedInstanceItemsComp,
|
|
||||||
newTypeSelected,
|
|
||||||
onScrollEnd,
|
onScrollEnd,
|
||||||
onUserTyping,
|
onUserTyping,
|
||||||
orderTopDown,
|
orderTopDown,
|
||||||
|
|
@ -388,5 +431,9 @@ export function useRecords(
|
||||||
showScrollTopBtn,
|
showScrollTopBtn,
|
||||||
textMatchType,
|
textMatchType,
|
||||||
totalItemCount,
|
totalItemCount,
|
||||||
|
recordItemsByClass,
|
||||||
|
filteredRecordItemsAll,
|
||||||
|
filteredRecordItemsByClass,
|
||||||
|
filteredRecordItemsForClassWithSubclassItems,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -3,9 +3,9 @@
|
||||||
* @description This composable reads a ttl file with shacl shapes and returns
|
* @description This composable reads a ttl file with shacl shapes and returns
|
||||||
* a set of reactive variables used by the root application component
|
* a set of reactive variables used by the root application component
|
||||||
*/
|
*/
|
||||||
import { reactive, toRaw} from 'vue';
|
import { reactive, toRaw, ref} from 'vue';
|
||||||
import { ShapesDataset } from 'shacl-tulip';
|
import { ShapesDataset } from 'shacl-tulip';
|
||||||
import { findObjectByKey, toIRI } from '@/modules/utils'
|
import { findObjectByKey, toIRI, toCURIE, getDisplayName, includeClass} from '@/modules/utils'
|
||||||
import { SHACL, RDFS} from '@/modules/namespaces';
|
import { SHACL, RDFS} from '@/modules/namespaces';
|
||||||
|
|
||||||
const basePath = import.meta.env.BASE_URL || '/';
|
const basePath = import.meta.env.BASE_URL || '/';
|
||||||
|
|
@ -16,6 +16,14 @@ export function useShapes(config) {
|
||||||
// ---- //
|
// ---- //
|
||||||
const defaultURL = `${basePath}dlschemas_shacl.ttl`;
|
const defaultURL = `${basePath}dlschemas_shacl.ttl`;
|
||||||
const shapesDS = new ShapesDataset(reactive({}));
|
const shapesDS = new ShapesDataset(reactive({}));
|
||||||
|
// These are all arays of classes that are eventually represented in the
|
||||||
|
// main class-selection pane in the ShaclVue* components
|
||||||
|
const idFilteredNodeShapeNames = ref([]);
|
||||||
|
const noEditClassList = ref([]);
|
||||||
|
const filteredNodeShapeNames = ref([]);
|
||||||
|
const priorityFilteredNodeShapeNames = ref([]);
|
||||||
|
const orderedNodeShapeNames = ref([]);
|
||||||
|
const allClassItems = ref([]);
|
||||||
|
|
||||||
// ----------------- //
|
// ----------------- //
|
||||||
// Lifecycle methods //
|
// Lifecycle methods //
|
||||||
|
|
@ -135,6 +143,138 @@ export function useShapes(config) {
|
||||||
shapesDS.data.propertyGroups['_default'][SHACL.order.value] = high_order + 100;
|
shapesDS.data.propertyGroups['_default'][SHACL.order.value] = high_order + 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getIdFilteredNodeShapeNames = ((configVarsMain, ID_IRI) => {
|
||||||
|
if (configVarsMain.showShapesWoId === true) {
|
||||||
|
return shapesDS.data.nodeShapeNamesArray;
|
||||||
|
}
|
||||||
|
var shapeNames = [];
|
||||||
|
for (var n of shapesDS.data.nodeShapeNamesArray) {
|
||||||
|
if (
|
||||||
|
findObjectByKey(
|
||||||
|
shapesDS.data.nodeShapes[shapesDS.data.nodeShapeNames[n]].properties,
|
||||||
|
SHACL.path.value,
|
||||||
|
ID_IRI.value
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
shapeNames.push(n);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return shapeNames;
|
||||||
|
});
|
||||||
|
|
||||||
|
const getNoEditClassList = ((configVarsMain, allPrefixes) => {
|
||||||
|
if (configVarsMain.noEditClasses?.length == 0) return []
|
||||||
|
var names = idFilteredNodeShapeNames.value;
|
||||||
|
var shapeNames = [];
|
||||||
|
for (var n of names) {
|
||||||
|
// First get IRI and prefix
|
||||||
|
var n_iri = shapesDS.data.nodeShapeNames[n]
|
||||||
|
if (includeClass(n_iri, configVarsMain, allPrefixes) &&
|
||||||
|
configVarsMain.noEditClasses?.indexOf(toCURIE(n_iri, allPrefixes)) >= 0) {
|
||||||
|
shapeNames.push(n);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return shapeNames.sort((a, b) =>
|
||||||
|
getDisplayName(
|
||||||
|
shapesDS.data.nodeShapeNames[a],
|
||||||
|
configVarsMain,
|
||||||
|
allPrefixes,
|
||||||
|
shapesDS.data.nodeShapes[shapesDS.data.nodeShapeNames[a]]
|
||||||
|
).toLowerCase()
|
||||||
|
.localeCompare(
|
||||||
|
getDisplayName(
|
||||||
|
shapesDS.data.nodeShapeNames[b],
|
||||||
|
configVarsMain,
|
||||||
|
allPrefixes,
|
||||||
|
shapesDS.data.nodeShapes[shapesDS.data.nodeShapeNames[b]]
|
||||||
|
).toLowerCase()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const getFilteredNodeShapeNames = ((configVarsMain, allPrefixes) => {
|
||||||
|
var names = idFilteredNodeShapeNames.value;
|
||||||
|
// If all relevant config arrays are empty, show all classes
|
||||||
|
if (
|
||||||
|
configVarsMain.showClasses?.length == 0 &&
|
||||||
|
configVarsMain.showClassesWithPrefix?.length == 0 &&
|
||||||
|
configVarsMain.hideClasses?.length == 0 &&
|
||||||
|
configVarsMain.hideClassesWithPrefix?.length == 0 &&
|
||||||
|
configVarsMain.noEditClasses?.length == 0
|
||||||
|
) {
|
||||||
|
return names;
|
||||||
|
}
|
||||||
|
var shapeNames = [];
|
||||||
|
for (var n of names) {
|
||||||
|
// First get IRI and prefix
|
||||||
|
var n_iri = shapesDS.data.nodeShapeNames[n]
|
||||||
|
if (includeClass(n_iri, configVarsMain, allPrefixes) && configVarsMain.noEditClasses.indexOf(toCURIE(n_iri, allPrefixes)) < 0) {
|
||||||
|
shapeNames.push(n);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return shapeNames;
|
||||||
|
});
|
||||||
|
|
||||||
|
const getPriorityFilteredNodeShapeNames = ((priorityClassList) => {
|
||||||
|
var names = filteredNodeShapeNames.value;
|
||||||
|
var shapeNames = [];
|
||||||
|
for (var n of names) {
|
||||||
|
var n_iri = shapesDS.data.nodeShapeNames[n]
|
||||||
|
if (!priorityClassList.value.includes(n_iri)) {
|
||||||
|
shapeNames.push(n);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return shapeNames;
|
||||||
|
})
|
||||||
|
|
||||||
|
const getOrderedNodeShapeNames = ((configVarsMain, allPrefixes) => {
|
||||||
|
return priorityFilteredNodeShapeNames.value.sort((a, b) =>
|
||||||
|
getDisplayName(
|
||||||
|
shapesDS.data.nodeShapeNames[a],
|
||||||
|
configVarsMain,
|
||||||
|
allPrefixes,
|
||||||
|
shapesDS.data.nodeShapes[shapesDS.data.nodeShapeNames[a]]
|
||||||
|
).toLowerCase()
|
||||||
|
.localeCompare(
|
||||||
|
getDisplayName(
|
||||||
|
shapesDS.data.nodeShapeNames[b],
|
||||||
|
configVarsMain,
|
||||||
|
allPrefixes,
|
||||||
|
shapesDS.data.nodeShapes[shapesDS.data.nodeShapeNames[b]]
|
||||||
|
).toLowerCase()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
})
|
||||||
|
|
||||||
|
const getAllClassItems = ((configVarsMain, allPrefixes, getClassIcon) => {
|
||||||
|
let items = [];
|
||||||
|
for (const node of orderedNodeShapeNames.value) {
|
||||||
|
const classIRI = shapesDS.data.nodeShapeNames[node];
|
||||||
|
const displayName = getDisplayName(
|
||||||
|
classIRI,
|
||||||
|
configVarsMain,
|
||||||
|
allPrefixes,
|
||||||
|
shapesDS.data.nodeShapes[classIRI]
|
||||||
|
);
|
||||||
|
const description = shapesDS.data.nodeShapes[classIRI][RDFS.comment.value];
|
||||||
|
items.push(
|
||||||
|
{
|
||||||
|
title: displayName,
|
||||||
|
value: classIRI,
|
||||||
|
props: {
|
||||||
|
title: displayName,
|
||||||
|
iri: classIRI,
|
||||||
|
subtitle: toCURIE(classIRI, allPrefixes),
|
||||||
|
icon: getClassIcon(classIRI),
|
||||||
|
description: description,
|
||||||
|
totalItemCount: null,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
})
|
||||||
|
|
||||||
// ------- //
|
// ------- //
|
||||||
// Returns //
|
// Returns //
|
||||||
// ------- //
|
// ------- //
|
||||||
|
|
@ -144,5 +284,17 @@ export function useShapes(config) {
|
||||||
updateShapesFromDefault,
|
updateShapesFromDefault,
|
||||||
updateShapes,
|
updateShapes,
|
||||||
updatePropertyGroups,
|
updatePropertyGroups,
|
||||||
|
idFilteredNodeShapeNames,
|
||||||
|
noEditClassList,
|
||||||
|
filteredNodeShapeNames,
|
||||||
|
priorityFilteredNodeShapeNames,
|
||||||
|
orderedNodeShapeNames,
|
||||||
|
allClassItems,
|
||||||
|
getIdFilteredNodeShapeNames,
|
||||||
|
getNoEditClassList,
|
||||||
|
getFilteredNodeShapeNames,
|
||||||
|
getPriorityFilteredNodeShapeNames,
|
||||||
|
getOrderedNodeShapeNames,
|
||||||
|
getAllClassItems,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { ref, reactive, toRaw} from "vue";
|
import { ref, reactive, toRaw } from "vue";
|
||||||
import { fillStringTemplate, findObjectByKey, findObjectIndexByKey, nodeShapeHasProperty} from "@/modules/utils";
|
import { fillStringTemplate, findObjectByKey, findObjectIndexByKey, nodeShapeHasProperty } 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';
|
||||||
|
|
@ -8,10 +8,11 @@ import { useNunjucks } from "@/composables/useNunjucks";
|
||||||
const { fillNunjucksTemplate } = useNunjucks();
|
const { fillNunjucksTemplate } = useNunjucks();
|
||||||
|
|
||||||
export function showWizardGroup(configVarsMain, context, classUri, allPrefixes, shapesDS) {
|
export function showWizardGroup(configVarsMain, context, classUri, allPrefixes, shapesDS) {
|
||||||
console.log("Checking if wizard group should be shown")
|
|
||||||
const classCurie = toCURIE(classUri, allPrefixes);
|
const classCurie = toCURIE(classUri, allPrefixes);
|
||||||
|
// all classes wizards
|
||||||
|
const all_class_selection = context == '_class' && configVarsMain.wizardEditorSelection?._classes;
|
||||||
// class-based wizards ?
|
// class-based wizards ?
|
||||||
const selection = configVarsMain.wizardEditorSelection?.[classCurie]?.[context]
|
const selection = configVarsMain.wizardEditorSelection?.[classCurie]?.[context];
|
||||||
// slot-based wizards ?
|
// slot-based wizards ?
|
||||||
let slot_selection = false;
|
let slot_selection = false;
|
||||||
if (configVarsMain.wizardEditorSelection?._slots) {
|
if (configVarsMain.wizardEditorSelection?._slots) {
|
||||||
|
|
@ -27,7 +28,7 @@ export function showWizardGroup(configVarsMain, context, classUri, allPrefixes,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const rval = slot_selection || selection && Array.isArray(selection) && selection.length > 0;
|
const rval = all_class_selection || slot_selection || selection && Array.isArray(selection) && selection.length > 0;
|
||||||
return rval
|
return rval
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -52,7 +53,13 @@ export function useWizard() {
|
||||||
let classCurie = toCURIE(class_IRI, allPrefixes)
|
let classCurie = toCURIE(class_IRI, allPrefixes)
|
||||||
// Load wizard editors if any
|
// Load wizard editors if any
|
||||||
let wizardsToAdd = new Set();
|
let wizardsToAdd = new Set();
|
||||||
// First, class-specific wizards
|
// First, any wizards that should show for all classes
|
||||||
|
if (context == '_class' && configVarsMain.wizardEditorSelection?._classes){
|
||||||
|
for (const wizard of configVarsMain.wizardEditorSelection?._classes) {
|
||||||
|
wizardsToAdd.add(wizard)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// then class-specific wizards
|
||||||
if (configVarsMain.wizardEditorSelection?.[classCurie]?.[context]){
|
if (configVarsMain.wizardEditorSelection?.[classCurie]?.[context]){
|
||||||
for (const wizard of configVarsMain.wizardEditorSelection?.[classCurie]?.[context] || []) {
|
for (const wizard of configVarsMain.wizardEditorSelection?.[classCurie]?.[context] || []) {
|
||||||
wizardsToAdd.add(wizard)
|
wizardsToAdd.add(wizard)
|
||||||
|
|
@ -90,6 +97,8 @@ export function useWizard() {
|
||||||
async function handleWizardSave(context, class_uri, wizardData, rdfDS, savedNodes, nodesToSubmit, subject_uri=null, formData) {
|
async function handleWizardSave(context, class_uri, wizardData, rdfDS, savedNodes, nodesToSubmit, subject_uri=null, formData) {
|
||||||
wizardDialog.value = false;
|
wizardDialog.value = false;
|
||||||
selectedWizard.value = null;
|
selectedWizard.value = null;
|
||||||
|
// Add class uri to wizard data
|
||||||
|
wizardData.class_uri = class_uri;
|
||||||
// if the context is '_record', add the current formData node ID as "pid"
|
// if the context is '_record', add the current formData node ID as "pid"
|
||||||
if (context == '_record') {
|
if (context == '_record') {
|
||||||
wizardData.pid = subject_uri;
|
wizardData.pid = subject_uri;
|
||||||
|
|
@ -103,12 +112,11 @@ export function useWizard() {
|
||||||
}
|
}
|
||||||
// And then parse TTL, adding quads to graph data
|
// And then parse TTL, adding quads to graph data
|
||||||
let newQuads = await rdfDS.parseTTLandDedup(newTTL);
|
let newQuads = await rdfDS.parseTTLandDedup(newTTL);
|
||||||
rdfDS.triggerReactivity();
|
|
||||||
// Now we process each added quad differently based on context:
|
// Now we process each added quad differently based on context:
|
||||||
// if context is _record, we need to work with formData of current record being edited
|
// if context is _record, we need to work with formData of current record being edited
|
||||||
// if context is _class or higher level, we can ignore formData because everything happens via template
|
// if context is _class or higher level, we can ignore formData because everything happens via template
|
||||||
if (context == '_record') {
|
if (context == '_record') {
|
||||||
for (const q of newQuads) {
|
for (const q of newQuads.quads) {
|
||||||
// If the quad has the current node ID as subject, we need to add it to formdata, and also remove the quad from graph store
|
// If the quad has the current node ID as subject, we need to add it to formdata, and also remove the quad from graph store
|
||||||
// If the quad has a different named node as subject, we need to keep track of it for submission purposes
|
// If the quad has a different named node as subject, we need to keep track of it for submission purposes
|
||||||
if (q.subject.value == subject_uri) {
|
if (q.subject.value == subject_uri) {
|
||||||
|
|
@ -140,8 +148,8 @@ export function useWizard() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log("The context was not _record...")
|
rdfDS.emitAddedRecords(newQuads.records)
|
||||||
for (const q of newQuads) {
|
for (const q of newQuads.quads) {
|
||||||
// Here we do not have to keep track of quads added to the graph,
|
// Here we do not have to keep track of quads added to the graph,
|
||||||
// because there's no parent form that can still be cancelled.
|
// because there's no parent form that can still be cancelled.
|
||||||
// We need to keep track of the named nodes saved to the graph, for submission
|
// We need to keep track of the named nodes saved to the graph, for submission
|
||||||
|
|
|
||||||
|
|
@ -108,6 +108,14 @@ export function addCodeTagsToText(text, prepend, append) {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function truncateText(str, n, useWordBoundary){
|
||||||
|
if (str.length <= n) { return str; }
|
||||||
|
const subString = str.slice(0, n-1);
|
||||||
|
return (useWordBoundary
|
||||||
|
? subString.slice(0, subString.lastIndexOf(" "))
|
||||||
|
: subString) + "…";
|
||||||
|
};
|
||||||
|
|
||||||
export function findObjectByKey(array, key, value) {
|
export function findObjectByKey(array, key, value) {
|
||||||
return array.find((obj) => obj[key] === value);
|
return array.find((obj) => obj[key] === value);
|
||||||
}
|
}
|
||||||
|
|
@ -621,6 +629,33 @@ export function collectBlankNodeHierarchy(store, rootBNode) {
|
||||||
return collected;
|
return collected;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getRootNodes(store, node, visited = new Set()) {
|
||||||
|
if (visited.has(node.value)) return [];
|
||||||
|
visited.add(node.value);
|
||||||
|
const parents = store.getQuads(null, null, node, null);
|
||||||
|
let roots = [];
|
||||||
|
for (const p of parents) {
|
||||||
|
if (p.subject.termType === 'NamedNode') {
|
||||||
|
roots.push(p.subject.value);
|
||||||
|
} else {
|
||||||
|
roots = roots.concat(getRootNodes(store, p.subject, visited));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return roots;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getUniqueRootNodes(quads, store) {
|
||||||
|
let uniqueRecords = new Set();
|
||||||
|
for (const q of quads) {
|
||||||
|
if (q.subject.termType === 'NamedNode') {
|
||||||
|
uniqueRecords.add(q.subject.value);
|
||||||
|
} else {
|
||||||
|
let rootNodeValues = getRootNodes(store, q.subject)
|
||||||
|
for (const r of rootNodeValues) uniqueRecords.add(r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Array.from(uniqueRecords)
|
||||||
|
}
|
||||||
|
|
||||||
export function getRecordQuads(pid, graph, recursive=false) {
|
export function getRecordQuads(pid, graph, recursive=false) {
|
||||||
// Return an array of quads related to a specific named node
|
// Return an array of quads related to a specific named node
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue