File upload support for wizard feature #329
7 changed files with 491 additions and 50 deletions
369
src/components/GitAnnexUploader4Wiz.vue
Normal file
369
src/components/GitAnnexUploader4Wiz.vue
Normal 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>
|
||||||
|
{{ 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>
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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: {},
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -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];
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue