File upload support for wizard feature #329

Merged
jsheunis merged 1 commit from wizuploader into main 2026-02-10 21:58:26 +00:00
7 changed files with 491 additions and 50 deletions

View file

@ -0,0 +1,369 @@
<template>
<v-row
v-if="showTargetSelector || showRepoSelector"
no-gutters
align="stretch"
class="ma-0 pa-0"
>
<v-col cols="6" v-if="showTargetSelector">
<v-select
v-model="selectedTarget"
label="Target"
:items="targetItems"
density="compact"
variant="outlined"
/>
</v-col>
<v-col cols="6" v-if="showRepoSelector">
<v-select
v-model="selectedRepo"
label="Repository"
:items="repoItems"
density="compact"
variant="outlined"
/>
</v-col>
</v-row>
<v-row no-gutters align="stretch" class="ma-0 pa-0">
<v-col class="d-flex align-stretch justify-center" style="padding: 0; padding-left: 2px;">
<v-card
class="drag-drop-area"
:class="{ dragover: isDragging }"
@click="onCardClickGuarded"
@dragover.prevent="onDragOver"
@dragleave.prevent="onDragLeave"
@drop.prevent="onFileDrop"
variant="outlined"
:disabled="props.disabled"
ref="mainCard"
>
<!-- Hidden file input -->
<input
type="file"
ref="fileInput"
class="hidden"
@change="onFileSelect"
/>
<!-- Centered icon -->
<span v-if="isUploading">
<v-progress-circular indeterminate></v-progress-circular>
</span>
<span v-else>
<span v-if="uploadSuccess">
<v-icon size="28" color="success">mdi-check</v-icon>
</span>
<span v-else-if="uploadFailure">
<v-icon size="28" color="error">mdi-alert-circle-outline</v-icon>
</span>
<span v-else>
<v-icon size="28" color="#616161">mdi-paperclip</v-icon>
</span>
</span>
<v-tooltip
:activator="'parent'"
v-model="errorDialog"
location="top"
:open-on-click="false"
:open-on-hover="false"
:interactive="true"
>
<v-row no-gutters>
<v-col cols="11">{{ uploadFailureError.error }}</v-col>
<v-col>
<v-btn
variant="text"
density="compact"
size="small"
icon="mdi-close-circle-outline"
@click="errorDialog = false"
>
</v-btn>
</v-col>
</v-row>
</v-tooltip>
</v-card>
</v-col>
</v-row>
<v-row v-if="showUploadedFile" no-gutters class="ma-0 pa-1">
<small>
<em>
<v-icon>mdi-file-check-outline</v-icon>&nbsp;
{{ uploadedFileData.name }} ({{ formatBytes(uploadedFileData.size) }})
<a :href="uploadedFileData.downloadUrl"><v-icon>mdi-download</v-icon></a>
</em>
</small>
</v-row>
</template>
<script setup>
import { ref, inject, toRaw, onMounted, watch} from 'vue'
const props = defineProps({
modelValue: Object,
config: Object,
disabled: Boolean,
});
import { useToken } from '@/composables/tokens';
const { token, setToken, clearToken } = useToken();
const mainCard = ref(null);
const tokenExists = ref(false)
const emit = defineEmits(['uploadComplete', 'update:modelValue'])
const isDragging = ref(false)
const fileData = ref({})
const fileInput = ref(null)
const tokenWarning = inject('tokenWarning');
const clientUuid = props.config.client_uuid
const targets = props.config.targets || []
const selectedTarget = ref(null)
const selectedRepo = ref(null)
const targetItems = ref([])
const repoItems = ref([])
const showTargetSelector = ref(false)
const showRepoSelector = ref(false)
const isUploading = ref(false)
const uploadSuccess = ref(false)
const uploadFailure = ref(false)
const uploadFailureError = ref({})
const errorDialog = ref(false)
const showUploadedFile = ref(false)
const uploadedFileData = ref(null)
watch(selectedTarget, () => {
updateRepositories()
})
onMounted(() => {
if (token.value !== null && token.value !== 'null') {
tokenExists.value = true;
}
// Build target selector
if (targets.length > 1) {
showTargetSelector.value = true
targetItems.value = targets.map(t => ({
title: t.name,
value: t
}))
}
// Select default target
selectedTarget.value = targets[0] || null
updateRepositories()
})
const updateRepositories = () => {
if (!selectedTarget.value) return
const repos = selectedTarget.value.repositories || []
if (repos.length > 1) {
showRepoSelector.value = true
repoItems.value = repos.map(r => ({
title: r.name,
value: r
}))
} else {
showRepoSelector.value = false
}
selectedRepo.value = repos[0] || null
}
// Format file sizes
const formatBytes = (bytes, decimals = 2) => {
if (bytes === 0) return '0 Bytes'
const k = 1024
const dm = decimals < 0 ? 0 : decimals
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]
}
// Handle file selection from the file input
const onFileSelect = (event) => {
const file = event.target.files[0]
if (file) {
console.log(file)
validateAndReadFile(file)
}
}
// Handle dragging over the drop area
const onDragOver = () => {
isDragging.value = true
}
// Handle drag leave (when the file is dragged out of the area)
const onDragLeave = () => {
isDragging.value = false
}
// Handle file drop event
const onFileDrop = (event) => {
isDragging.value = false
const file = event.dataTransfer.files[0]
if (file) {
validateAndReadFile(file)
}
}
const onCardClickGuarded = (event) => {
if (props.disabled) {
event.preventDefault()
event.stopImmediatePropagation()
return
}
onCardClick()
}
const onCardClick = () => {
if (beforeUploadCheck() === false) {
// Do NOT open file dialog
return;
}
fileInput.value.click()
}
function beforeUploadCheck() {
if (token.value !== null && token.value !== 'null') {
tokenExists.value = true;
}
if (!tokenExists.value) {
// showTokenDialog.value = true;
tokenWarning.value = true;
return false;
}
return true;
}
// Validate file type and read it
const validateAndReadFile = async (file) => {
let result = { status: null, error: null }
try {
const arrayBuffer = await file.arrayBuffer()
const hashBuffer = await crypto.subtle.digest('SHA-256', arrayBuffer)
// Convert hash buffer to hex string
const hashArray = Array.from(new Uint8Array(hashBuffer))
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('')
// get file extension
const extension = file.name.includes('.') ? file.name.split('.').pop().toLowerCase() : null
// construct git annex key
const gitAnnexKey = `SHA256E-s${file.size}--${hashHex}${extension !== null ? '.'+extension : ''}`
fileData.value = {
file: file,
name: file.name,
size: file.size,
type: file.type || 'Unknown',
ext: extension,
hash: hashHex,
url: URL.createObjectURL(file),
annexKey: gitAnnexKey,
downloadUrl: `${selectedTarget.value.base_url}/${selectedRepo.value.annex_uuid}/key/${encodeURIComponent(gitAnnexKey)}`
}
isUploading.value = true
result = await uploadFile()
isUploading.value = false
if (result.status == 'ok') {
emit('update:modelValue', toRaw(fileData.value))
uploadedFileData.value = toRaw(fileData.value)
showUploadedFile.value = true;
uploadSuccess.value = true;
uploadFailure.value = false;
setTimeout(() => {
uploadSuccess.value = false;
}, 1000);
} else {
uploadSuccess.value = false;
uploadFailure.value = true;
setTimeout(() => {
uploadFailure.value = false;
}, 1000);
uploadFailureError.value = result;
errorDialog.value = true;
}
} catch (error) {
alert('Failed to process file: ' + error.message)
result.status = 'error';
result.error = error;
uploadSuccess.value = false;
uploadFailure.value = true;
uploadFailureError.value = error;
setTimeout(() => {
uploadFailure.value = false;
}, 1500);
errorDialog.value = true;
}
// Emit upload result to parent
emit('uploadComplete', {
status: result.status,
error: result.error || null,
fileData: toRaw(fileData.value),
})
}
const uploadFile = async () => {
// During development, change baseUrl in config to '/forgejo-api' to circumvent CORS issues;
// it sends the request to the local proxy server instead of directly to the baseUrl
const endpoint = `${selectedTarget.value.base_url}/${selectedRepo.value.annex_uuid}/v4/put?key=${encodeURIComponent(fileData.value.annexKey)}&clientuuid=${encodeURIComponent(clientUuid)}`
try {
const response = await fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/octet-stream',
'X-git-annex-data-length': fileData.value.size,
'Authorization': 'Basic ' + btoa(`${token.value}:`)
},
body: fileData.value.file
})
if (!response.ok) {
throw new Error(`Upload failed with status ${response.status}: ${response.statusText}`)
}
const result = await response.text()
console.log('Upload successful:', result)
return {
status: 'ok'
}
} catch (error) {
console.error('Upload error:', error)
return {
status: 'error',
'error': error,
}
}
}
</script>
<style scoped>
.drag-drop-area {
display: flex;
align-items: center;
justify-content: center;
height: 40px;
min-height: 40px;
width: 100%;
cursor: pointer;
border-color: #b4b4b4;
transition: all 0.3s ease;
}
.drag-drop-area.dragover {
border: dashed #3f51b5;
background-color: #f0f0f0;
filter: grayscale(50%);
}
.drag-drop-area:hover {
border-color: black;
}
.hidden {
display: none;
}
.v-card--disabled {
pointer-events: auto;
}
.v-card--disabled * {
pointer-events: none;
}
</style>

View file

@ -1,7 +1,7 @@
<template> <template>
<span v-if="ready"> <span v-if="ready">
<span v-if="showWizardGroup(configVarsMain, '_record', localShapeIri, allPrefixes)"> <span v-if="showWizardGroup(configVarsMain, '_record', localShapeIri, allPrefixes, shapesDS)">
<v-row no-gutters align="center"> <v-row no-gutters align="center" style="margin-bottom: 1em;">
<v-col cols="4"> <v-col cols="4">
<v-icon>mdi-wizard-hat</v-icon> Wizards: <v-icon>mdi-wizard-hat</v-icon> Wizards:
</v-col> </v-col>

View file

@ -11,7 +11,7 @@
validate-on="lazy input" validate-on="lazy input"
@submit.prevent="saveForm()" @submit.prevent="saveForm()"
> >
<v-row no-gutters align="center" v-for="input in props.wizardConfig.inputs" :key="input.prop"> <v-row no-gutters align="stretch" v-for="input in props.wizardConfig.inputs" :key="input.prop">
<v-col cols="4"> <v-col cols="4">
<span v-if="input.description"> <span v-if="input.description">
<v-tooltip :text="input.description" location="end top" origin="start bottom"> <v-tooltip :text="input.description" location="end top" origin="start bottom">
@ -35,6 +35,12 @@
<span v-else-if="input.type == 'boolean'"> <span v-else-if="input.type == 'boolean'">
<v-switch v-model="modelVals[input.prop]" density="compact" variant="outlined" :label="input.placeholder ? input.placeholder : 'select value'" inset hide-details="auto" :rules="rules[input.prop]"></v-switch> <v-switch v-model="modelVals[input.prop]" density="compact" variant="outlined" :label="input.placeholder ? input.placeholder : 'select value'" inset hide-details="auto" :rules="rules[input.prop]"></v-switch>
</span> </span>
<span v-else-if="input.type == 'upload'">
<GitAnnexUploader4Wiz
:config="uploadConfig"
v-model="modelVals[input.prop]"
></GitAnnexUploader4Wiz>
</span>
<span v-else> <span v-else>
kaaaaaaaaa kaaaaaaaaa
</span> </span>
@ -67,7 +73,8 @@
</template> </template>
<script setup> <script setup>
import { reactive, ref, toRaw, watch} from 'vue'; import { reactive, ref, toRaw, watch, inject} from 'vue';
import GitAnnexUploader4Wiz from '@/components/GitAnnexUploader4Wiz.vue'
// Define component props // Define component props
const props = defineProps({ const props = defineProps({
@ -75,11 +82,14 @@ const props = defineProps({
}); });
const emit = defineEmits(['save', 'cancel']) const emit = defineEmits(['save', 'cancel'])
// Refs // Refs
const wizardForm = ref(null); const wizardForm = ref(null);
const wizardFormValid = ref(null); const wizardFormValid = ref(null);
const modelVals = reactive({}) const modelVals = reactive({})
const rules = reactive({}); const rules = reactive({});
const baseRules = {} const baseRules = {}
const configVarsMain = inject('configVarsMain');
const uploadConfig = configVarsMain.gitannexP2phttpConfigWizard ?? {};
watch( watch(
() => props.wizardConfig, () => props.wizardConfig,

View file

@ -43,6 +43,7 @@ const configVarsMain = inject('configVarsMain')
const allPrefixes = inject('allPrefixes') const allPrefixes = inject('allPrefixes')
const rdfDS = inject('rdfDS') const rdfDS = inject('rdfDS')
const formData = inject('formData') const formData = inject('formData')
const shapesDS = inject('shapesDS')
const savedNodes = inject('savedNodes') const savedNodes = inject('savedNodes')
const nodesToSubmit = inject('nodesToSubmit') const nodesToSubmit = inject('nodesToSubmit')
const { const {
@ -54,16 +55,18 @@ const {
openWizard, openWizard,
handleWizardCancel, handleWizardCancel,
handleWizardSave, handleWizardSave,
onFormWithWizardCancel onFormWithWizardCancel,
onFormWithWizardSave,
} = useWizard(); } = useWizard();
// ----------------- // // ----------------- //
// Lifecycle methods // // Lifecycle methods //
// ----------------- // // ----------------- //
onMounted(() => { onMounted(() => {
setupWizards(props.context, props.classUri, configVarsMain, allPrefixes) setupWizards(props.context, props.classUri, configVarsMain, allPrefixes, shapesDS)
if (props.context == '_record') { if (props.context == '_record') {
registerHandler('cancel', cancelWizard) registerHandler('cancel', cancelWizardForm)
registerHandler('save', saveWizardForm)
} }
}); });
@ -71,10 +74,14 @@ onMounted(() => {
// Functions // // Functions //
// --------- // // --------- //
function cancelWizard() { function cancelWizardForm() {
onFormWithWizardCancel(savedNodes, nodesToSubmit, rdfDS) onFormWithWizardCancel(savedNodes, nodesToSubmit, rdfDS)
} }
function saveWizardForm() {
onFormWithWizardSave(props.classUri, props.recordUri, formData, rdfDS, configVarsMain)
}
function saveWizard(wizardData) { function saveWizard(wizardData) {
handleWizardSave( handleWizardSave(
props.context, props.context,

View file

@ -91,6 +91,7 @@ const mainVarsToLoad = {
class_name_display: 'name', class_name_display: 'name',
footer_links: [], footer_links: [],
gitannex_p2phttp_config: {}, gitannex_p2phttp_config: {},
gitannex_p2phttp_config_wizard: {},
update_shapes: {}, update_shapes: {},
update_shapes_default: {}, update_shapes_default: {},
wizard_editors: {}, wizard_editors: {},

View file

@ -1,13 +1,31 @@
import { ref, reactive } from "vue"; import { ref, reactive } from "vue";
import { getContent, fillStringTemplate, findObjectByKey, findObjectIndexByKey} from "@/modules/utils"; import { getContent, fillStringTemplate, findObjectByKey, findObjectIndexByKey, nodeShapeHasProperty} from "@/modules/utils";
import { toCURIE } from "shacl-tulip"; import { toCURIE, toIRI } from "shacl-tulip";
import { RDF } from "@/modules/namespaces"; import { RDF } from "@/modules/namespaces";
import { DataFactory } from 'n3';
const { namedNode, quad } = DataFactory;
export function showWizardGroup(configVarsMain, context, classUri, allPrefixes, shapesDS) {
export function showWizardGroup(configVarsMain, context, classUri, allPrefixes) { console.log("Checking if wizard group should be shown")
const classCurie = toCURIE(classUri, allPrefixes); const classCurie = toCURIE(classUri, allPrefixes);
const selection = configVarsMain.wizardEditorSelection?.[classCurie]?.[context]; // class-based wizards ?
const rval = selection && Array.isArray(selection) && selection.length > 0; const selection = configVarsMain.wizardEditorSelection?.[classCurie]?.[context]
// slot-based wizards ?
let slot_selection = false;
if (configVarsMain.wizardEditorSelection?._slots) {
for (const slot of Object.keys(configVarsMain.wizardEditorSelection._slots)) {
let slotIRI = toIRI(slot, allPrefixes)
if (nodeShapeHasProperty(toIRI(classUri, allPrefixes), shapesDS, slotIRI, allPrefixes)
&& configVarsMain.wizardEditorSelection._slots[slot][context]
&& Array.isArray(configVarsMain.wizardEditorSelection._slots[slot][context])
&& configVarsMain.wizardEditorSelection._slots[slot][context].length > 0
) {
slot_selection = true;
break;
}
}
}
const rval = slot_selection || selection && Array.isArray(selection) && selection.length > 0;
return rval return rval
} }
@ -28,14 +46,32 @@ export function useWizard() {
// --------- // // --------- //
// Functions // // Functions //
// --------- // // --------- //
function setupWizards(context, class_IRI, configVarsMain, allPrefixes) { function setupWizards(context, class_IRI, configVarsMain, allPrefixes, shapesDS) {
let classCurie = toCURIE(class_IRI, allPrefixes) let classCurie = toCURIE(class_IRI, allPrefixes)
// Load wizard editors if any, also load template content // Load wizard editors if any, also load icon/template content
// first class-based wizards
if (configVarsMain.wizardEditorSelection?.[classCurie]?.[context]){
for (const wizard of configVarsMain.wizardEditorSelection?.[classCurie]?.[context]) { for (const wizard of configVarsMain.wizardEditorSelection?.[classCurie]?.[context]) {
console.log(`adding wizard '${wizard}' for class '${classCurie}' and context '${context}'`)
wizardEditors[wizard] = configVarsMain.wizardEditors[wizard] wizardEditors[wizard] = configVarsMain.wizardEditors[wizard]
wizardEditors[wizard].template = getContent(configVarsMain.content, wizardEditors[wizard].template) wizardEditors[wizard].template = getContent(configVarsMain.content, wizardEditors[wizard].template)
wizardEditors[wizard].iconFig = getIcon(wizardEditors[wizard], configVarsMain) wizardEditors[wizard].iconFig = getIcon(wizardEditors[wizard], configVarsMain)
} }
}
// then slot-based wizards
if (configVarsMain.wizardEditorSelection?._slots) {
for (const slot of Object.keys(configVarsMain.wizardEditorSelection._slots)) {
let slotIRI = toIRI(slot, allPrefixes)
if (nodeShapeHasProperty(toIRI(class_IRI, allPrefixes), shapesDS, slotIRI, allPrefixes)) {
for (const wizard of configVarsMain.wizardEditorSelection?._slots[slot][context]) {
if (wizard in wizardEditors) continue;
wizardEditors[wizard] = configVarsMain.wizardEditors[wizard]
wizardEditors[wizard].template = getContent(configVarsMain.content, wizardEditors[wizard].template)
wizardEditors[wizard].iconFig = getIcon(wizardEditors[wizard], configVarsMain)
}
}
}
}
if (Object.keys(wizardEditors).length > 0) { if (Object.keys(wizardEditors).length > 0) {
showWizards.value = true showWizards.value = true
} }
@ -52,17 +88,6 @@ 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) {
console.log("handleWizardSave")
console.log("context:")
console.log(context)
console.log("class_uri:")
console.log(class_uri)
console.log("wizardData:")
console.log(wizardData)
console.log("subject_uri:")
console.log(subject_uri)
wizardDialog.value = false; wizardDialog.value = false;
selectedWizard.value = null; selectedWizard.value = null;
// 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"
@ -71,19 +96,14 @@ export function useWizard() {
} }
// Now we fill string template // Now we fill string template
let newTTL = fillStringTemplate(wizardData._template, wizardData) let newTTL = fillStringTemplate(wizardData._template, wizardData)
console.log("filled string template:")
console.log(newTTL)
// 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(); 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
console.log("All added quads after wizard save:")
if (context == '_record') { if (context == '_record') {
for (const q of newQuads) { for (const q of newQuads) {
console.log(`${q.subject.value} - ${q.predicate.value} - ${q.object.value}`)
// 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) {
@ -106,7 +126,6 @@ export function useWizard() {
} }
// now we remove the record quad from graph because it was added prematurely; // now we remove the record quad from graph because it was added prematurely;
// this will be re-added, (importantly: with the correct PID), when the main form is saved // this will be re-added, (importantly: with the correct PID), when the main form is saved
console.log("going to delete this quad ^^")
rdfDS.data.graph.delete(q) rdfDS.data.graph.delete(q)
} else { } else {
// We keep track of all other quads added to the graph, in case they need to be removed on form cancel // We keep track of all other quads added to the graph, in case they need to be removed on form cancel
@ -167,20 +186,38 @@ export function useWizard() {
} }
function onFormWithWizardCancel(savedNodes, nodesToSubmit, rdfDS) { function onFormWithWizardCancel(savedNodes, nodesToSubmit, rdfDS) {
console.log("Running onFormWithWizardCancel")
for (const q of wizardAddedQuads.value) { for (const q of wizardAddedQuads.value) {
// remove named nodes from savedNodes and nodesToSubmit // remove named nodes from savedNodes and nodesToSubmit
if (q.subject.termType == 'NamedNode' && q.predicate.value == RDF.type.value) { if (q.subject.termType == 'NamedNode' && q.predicate.value == RDF.type.value) {
console.log(`Removing named node from savedNodes and nodesToSubmit: ${q.subject.value}`)
savedNodes.value.splice(findObjectIndexByKey(savedNodes.value, 'node_iri', q.subject.value), 1) savedNodes.value.splice(findObjectIndexByKey(savedNodes.value, 'node_iri', q.subject.value), 1)
nodesToSubmit.value.splice(findObjectIndexByKey(nodesToSubmit.value, 'node_iri', q.subject.value), 1) nodesToSubmit.value.splice(findObjectIndexByKey(nodesToSubmit.value, 'node_iri', q.subject.value), 1)
} }
// remove quad from graph store
console.log(`Removing quad from graph store: ${q.subject.value} - ${q.predicate.value} - ${q.object.value}`)
rdfDS.data.graph.delete(q) rdfDS.data.graph.delete(q)
} }
} }
function onFormWithWizardSave(classIRI, recordID, formData, rdfDS, configVarsMain) {
// This will run when the user hits the form save button, at which time the
// PID of the record will be known inside formData. We need to access this.
// We need to loop through all quads that the wizard saved and run a check:
// if the quad has the current record ID as object, it will already be in the graph
// while the record ID (i.e. the quad object) might be the wrong one.
// So we check it against the correct PID. If the same, we do nothing. If they differ
// we need to replace the quad with one that references the correct object
let recordPID = formData.content[classIRI]?.[recordID]?.[configVarsMain.idIri]?.[0].value
if (recordID === recordPID) {
return;
}
for (const q of wizardAddedQuads.value) {
if (q.object.value == recordID) {
// remove quad from graph store, add one with correct object
rdfDS.data.graph.delete(q)
let newQuad = quad(q.subject, q.predicate, namedNode(recordPID), null)
rdfDS.data.graph.add(newQuad)
}
}
}
// ------- // // ------- //
// Returns // // Returns //
// ------- // // ------- //
@ -195,5 +232,6 @@ export function useWizard() {
handleWizardCancel, handleWizardCancel,
handleWizardSave, handleWizardSave,
onFormWithWizardCancel, onFormWithWizardCancel,
onFormWithWizardSave,
}; };
} }

View file

@ -489,6 +489,16 @@ export function nodeShapeHasPID(nodeshapeIRI, shapesDS, pidIRI) {
return ps ? true : false return ps ? true : false
} }
export function nodeShapeHasProperty(nodeshapeIRI, shapesDS, inputURI, allPrefixes) {
// True if the nodeshape has a propertyshape with sh:path being equal to input URI,
var nodeShape = shapesDS.data.nodeShapes[nodeshapeIRI];
if (!nodeShape) return undefined
var ps = nodeShape.properties.find(
(prop) => prop[SHACL.path.value] == toIRI(inputURI, allPrefixes)
);
return ps ? true : false
}
export function getNodeShapePropertyWithAnnotations(nodeshapeIRI, shapesDS, annotations = {}, prefixes) { export function getNodeShapePropertyWithAnnotations(nodeshapeIRI, shapesDS, annotations = {}, prefixes) {
// For the given SHACL NodeShape, check if it has a property shape that is annotated // For the given SHACL NodeShape, check if it has a property shape that is annotated
// with a set of provided annotations // with a set of provided annotations
@ -629,16 +639,22 @@ export function getContent(content, key) {
} }
export function fillStringTemplate(template, params) { export function fillStringTemplate(template, params) {
return template.replace(/\{([a-zA-Z0-9_]+)\}/g, (match, key) => { return template.replace(/\{([a-zA-Z0-9_.]+)\}/g, (match, keyPath) => {
if (!(key in params)) { if (keyPath === '_randomUUID') {
if (key == '_randomUUID') { return crypto.randomUUID();
return crypto.randomUUID() }
} else { // Resolve dot notation
console.error(`Error: No value provided for placeholder {${key}}`); const value = keyPath.split('.').reduce((acc, key) => {
if (acc && Object.prototype.hasOwnProperty.call(acc, key)) {
return acc[key];
}
return undefined;
}, params);
if (value === undefined) {
console.error(`Error: No value provided for placeholder {${keyPath}}`);
return match; return match;
} }
} return value;
return params[key];
}); });
} }