Adds initial support for uploading table data #87
9 changed files with 594 additions and 186 deletions
7
package-lock.json
generated
7
package-lock.json
generated
|
|
@ -16,6 +16,7 @@
|
|||
"@rdfjs/parser-n3": "^2.0.2",
|
||||
"rdf-ext": "^2.5.2",
|
||||
"roboto-fontface": "^0.10.0",
|
||||
"tabulator-tables": "^6.3.1",
|
||||
"util": "^0.12.5",
|
||||
"uuid": "^10.0.0"
|
||||
},
|
||||
|
|
@ -5211,6 +5212,12 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tabulator-tables": {
|
||||
"version": "6.3.1",
|
||||
"resolved": "https://registry.npmjs.org/tabulator-tables/-/tabulator-tables-6.3.1.tgz",
|
||||
"integrity": "sha512-qFW7kfadtcaISQIibKAIy0f3eeIXUVi8242Vly1iJfMD79kfEGzfczNuPBN/80hDxHzQJXYbmJ8VipI40hQtfA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tinybench": {
|
||||
"version": "2.9.0",
|
||||
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@
|
|||
"@rdfjs/parser-n3": "^2.0.2",
|
||||
"rdf-ext": "^2.5.2",
|
||||
"roboto-fontface": "^0.10.0",
|
||||
"tabulator-tables": "^6.3.1",
|
||||
"util": "^0.12.5",
|
||||
"uuid": "^10.0.0"
|
||||
},
|
||||
|
|
|
|||
208
src/classes/DataTable.js
Normal file
208
src/classes/DataTable.js
Normal file
|
|
@ -0,0 +1,208 @@
|
|||
import { SHACL } from '../modules/namespaces'
|
||||
import { findObjectByKey } from '../modules/utils'
|
||||
import { toCURIE, toIRI } from 'shacl-tulip';
|
||||
import { toRaw } from 'vue';
|
||||
|
||||
const reactiveCloneFunc = ((data) => {
|
||||
return structuredClone(toRaw(data))
|
||||
});
|
||||
|
||||
export class DataTable {
|
||||
|
||||
constructor(table, class_iri, record_iri, shapesDS, formData, allPrefixes, ID_IRI) {
|
||||
this.DELIMITER = ',';
|
||||
this.table = table
|
||||
this.columns = table.getColumnDefinitions().map(col => col.title)
|
||||
this.class_iri = class_iri
|
||||
this.record_iri = record_iri
|
||||
this.shapesDS = shapesDS
|
||||
this.nodeShape = this.shapesDS.data.nodeShapes[class_iri]
|
||||
this.ID_IRI = ID_IRI
|
||||
this.allPrefixes = allPrefixes
|
||||
this.formData = formData
|
||||
this.mappedProps = this.mapProps()
|
||||
}
|
||||
|
||||
mapProps() {
|
||||
var mappedProps = {}
|
||||
mappedProps.props = {}
|
||||
mappedProps.requiredProps = []
|
||||
mappedProps.multivaluedProps = []
|
||||
mappedProps.blankNodeProps = []
|
||||
mappedProps.namedNodeProps = []
|
||||
var propertyShapes = this.nodeShape.properties // this is an array ob objects
|
||||
for (var p=0; p<propertyShapes.length; p++) {
|
||||
// Get the propert name
|
||||
var pShape = propertyShapes[p];
|
||||
var propertyName = toCURIE(pShape[SHACL.path.value], this.allPrefixes, "parts").property
|
||||
if (propertyName) {
|
||||
mappedProps.props[propertyName] = pShape
|
||||
} else {
|
||||
console.log(`\nProp: ${propertyName}`)
|
||||
console.log('- propertyname not found, not mapping it; this is the shape:')
|
||||
console.log(pShape)
|
||||
continue;
|
||||
}
|
||||
// Is property required?
|
||||
if (this.propertyIsRequired(pShape)) {
|
||||
// console.log(`- this is a required property`)
|
||||
mappedProps.requiredProps.push(propertyName)
|
||||
}
|
||||
// Is property multivalued?
|
||||
if (this.propertyIsList(pShape)) {
|
||||
// console.log(`- this is a multivalued property`)
|
||||
mappedProps.multivaluedProps.push(propertyName)
|
||||
}
|
||||
// What is the nodeKind and type (from SHACL shape):
|
||||
// - just a string/number/...
|
||||
// - list of strings/numbers/...
|
||||
// - list of pids (named nodes)
|
||||
// - list of objects (blank nodes) => cannot be handled
|
||||
const [propertyIsBN, propertyIsIRI] = this.getNodeDeets(pShape)
|
||||
if (propertyIsBN) {mappedProps.blankNodeProps.push(propertyName)}
|
||||
if (propertyIsIRI) {mappedProps.namedNodeProps.push(propertyName)}
|
||||
}
|
||||
mappedProps.allProps = Object.keys(mappedProps.props);
|
||||
return mappedProps
|
||||
}
|
||||
|
||||
propertyIsRequired(pShape) {
|
||||
// sh:minCount must be greater than zero
|
||||
return pShape[SHACL.minCount?.value] > 0 ? true : false
|
||||
}
|
||||
|
||||
propertyIsList(pShape) {
|
||||
if (pShape.hasOwnProperty(SHACL.maxCount)) {
|
||||
if (pShape[SHACL.maxCount] == 1) {
|
||||
return false
|
||||
} else if (pShape[SHACL.maxCount] > 1) {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
getNodeDeets(pShape) {
|
||||
var isBlankNode = false
|
||||
var isIRI = false
|
||||
if ( pShape.hasOwnProperty(SHACL.nodeKind.value) ) {
|
||||
if (pShape[SHACL.nodeKind.value] == SHACL.IRI.value) {
|
||||
isIRI = true
|
||||
} else if (pShape[SHACL.nodeKind.value] == SHACL.BlankNode.value) {
|
||||
isBlankNode = true
|
||||
} else if (pShape[SHACL.nodeKind.value] == SHACL.BlankNodeOrIRI.value
|
||||
&& pShape.hasOwnProperty(SHACL.class.value)) {
|
||||
var rangeNodeShape = this.shapesDS.data.nodeShapes[pShape[SHACL.class.value]]
|
||||
var foundIDpropshape = findObjectByKey(rangeNodeShape.properties, SHACL.path.value, this.ID_IRI)
|
||||
if (foundIDpropshape) {
|
||||
isIRI = true
|
||||
} else {
|
||||
isBlankNode = true
|
||||
}
|
||||
}
|
||||
}
|
||||
return [isBlankNode, isIRI]
|
||||
}
|
||||
|
||||
saveTable(rdfDS, parentShapeIRI, parentPropertyIRI) {
|
||||
// Props have already been mapped
|
||||
// We first check if the ID_IRI, if expected to be provided, is in the list of columns
|
||||
var idPropertyName = toCURIE(this.ID_IRI, this.allPrefixes, "parts").property
|
||||
if (this.mappedProps.allProps.includes(idPropertyName) && !this.columns.includes(idPropertyName) ) {
|
||||
var msg = `ID_IRI property name '${idPropertyName}' is not included in the table columns, while it is expected. Cannot save data.`
|
||||
throw Error(msg)
|
||||
}
|
||||
// TODO: handle required properties in the same way? Do requiredProperties necessarily include ID_IRI too?
|
||||
// Now we check for column headings that can't be contained/processed in CSV
|
||||
// (isBlankNode, or is not in list of properties)
|
||||
// Warn that this column will be ignored
|
||||
for (var c of this.columns) {
|
||||
if (this.mappedProps.blankNodeProps.includes(c) ) {
|
||||
console.log(`Column '${c}' cannot be processed because it expects a blank node property, ignoring...`)
|
||||
}
|
||||
if (!this.mappedProps.allProps.includes(c) ) {
|
||||
console.log(`Column '${c}' cannot be processed because it is not in the list of possible properties, ignoring...`)
|
||||
}
|
||||
}
|
||||
// Now cycle through the rows and save data
|
||||
var tableData = this.table.getData()
|
||||
for (var row of tableData) {
|
||||
var nodeID
|
||||
if (this.columns.includes(idPropertyName)) {
|
||||
nodeID = row[idPropertyName]
|
||||
} else {
|
||||
nodeID = '_:' + crypto.randomUUID()// blanknode
|
||||
}
|
||||
// First process nodeID, i.e. subject (for both named node and blank node)
|
||||
this.formData.addSubject(this.class_iri, nodeID)
|
||||
// For each row cell, we need to add the relevant record(s) to formData
|
||||
for (var p of Object.keys(row)) {
|
||||
// ID_IRI column has already been processed, but the predicate should still be added... TODO inspect
|
||||
// if (p == idPropertyName) {continue;}
|
||||
// Ignore blanknode columns
|
||||
if (this.mappedProps.blankNodeProps.includes(p)) {continue;}
|
||||
// Ignore columns that aren't in the list of possible properties
|
||||
if (!this.mappedProps.allProps.includes(p)) {continue;}
|
||||
// If the value is empty, skip
|
||||
if (!row[p]) {continue;}
|
||||
// If the value exists
|
||||
// First get the predicate uri from the column name
|
||||
var predicate_uri = this.mappedProps.props[p][SHACL.path.value]
|
||||
// First check if this is a multivalued slot and then split it
|
||||
if (this.mappedProps.multivaluedProps.includes(p) && row[p].split(this.DELIMITER).length > 1) {
|
||||
var p_parts = row[p].split(this.DELIMITER)
|
||||
// Add a addPredicate for first element, addObject for rest
|
||||
for (var i=0;i<p_parts.length;i++) {
|
||||
if (i==0) {
|
||||
this.formData.addPredicate(this.class_iri, nodeID, predicate_uri, [p_parts[i]])
|
||||
} else {
|
||||
// This pushes null into the content array, after which we still need to set the value
|
||||
this.formData.addObject(this.class_iri, nodeID, predicate_uri);
|
||||
this.formData.content[this.class_iri][nodeID][predicate_uri][i] = p_parts[i];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.formData.addPredicate(this.class_iri, nodeID, predicate_uri, [row[p]])
|
||||
}
|
||||
}
|
||||
var saved_node = this.formData.saveNode(
|
||||
this.class_iri,
|
||||
nodeID,
|
||||
this.shapesDS,
|
||||
rdfDS,
|
||||
false,
|
||||
reactiveCloneFunc
|
||||
)
|
||||
// Lastly, if uploading a table from nodeShapeEditor, the saved nodes
|
||||
// should be linked to the record via their ID (which could be named or blank nodes)
|
||||
if (this.record_iri) {
|
||||
// Get the iri of the property for which the InstancesSelectEditor was instantiated
|
||||
console.log("saved from instancesselecteditor, have to link new record to parent record")
|
||||
var pred_iri = parentPropertyIRI
|
||||
// If a predicate does not exist yet, use addPredicate else addObject
|
||||
if (Object.keys(this.formData.content[parentShapeIRI][this.record_iri]).indexOf(pred_iri) < 0) {
|
||||
this.formData.addPredicate(parentShapeIRI, this.record_iri, pred_iri, [nodeID])
|
||||
} else {
|
||||
console.log('content value before sdding object:')
|
||||
console.log(toRaw(this.formData.content[parentShapeIRI][this.record_iri][pred_iri]))
|
||||
// If the predicate exists, but it only has a single null value in the array, don't addObject and just set value
|
||||
var predVal = toRaw(this.formData.content[parentShapeIRI][this.record_iri][pred_iri])
|
||||
|
||||
if (predVal.length == 1 && predVal[0] == null) {
|
||||
console.log("not running addObject")
|
||||
this.formData.content[parentShapeIRI][this.record_iri][pred_iri][0] = nodeID;
|
||||
} else {
|
||||
this.formData.addObject(parentShapeIRI, this.record_iri, pred_iri);
|
||||
var l = this.formData.content[parentShapeIRI][this.record_iri][pred_iri].length
|
||||
this.formData.content[parentShapeIRI][this.record_iri][pred_iri][l-1] = nodeID;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1,181 +0,0 @@
|
|||
<template>
|
||||
<v-container>
|
||||
<!-- Drag and Drop area -->
|
||||
<v-card
|
||||
class="drag-drop-area"
|
||||
:class="{'dragover': isDragging}"
|
||||
@dragover.prevent="onDragOver"
|
||||
@dragleave.prevent="onDragLeave"
|
||||
@drop.prevent="onFileDrop"
|
||||
>
|
||||
<v-row>
|
||||
<!-- File input on the left -->
|
||||
<v-col cols="6">
|
||||
<v-file-input
|
||||
label="Upload RDF file"
|
||||
v-model="selectedFile"
|
||||
:rules="[rules.required, rules.validRdfFile]"
|
||||
prepend-icon="mdi-upload"
|
||||
accept=".jsonld,.rdf,.ttl,.nt,.trig,.xml"
|
||||
@change="onFileSelect"
|
||||
></v-file-input>
|
||||
</v-col>
|
||||
|
||||
<!-- URL input on the right -->
|
||||
<v-col cols="6">
|
||||
<v-text-field
|
||||
label="Or paste RDF file URL"
|
||||
v-model="fileUrl"
|
||||
prepend-icon="mdi-link"
|
||||
@change="onUrlInput"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card>
|
||||
|
||||
<!-- Uploaded file details or preview -->
|
||||
<v-card v-if="fileData" class="mt-5">
|
||||
<v-card-title>File details:</v-card-title>
|
||||
<v-card-text>
|
||||
<p><strong>Name:</strong> {{ fileData.name }}</p>
|
||||
<p><strong>Size:</strong> {{ formatBytes(fileData.size) }}</p>
|
||||
<p><strong>Type:</strong> {{ fileData.type }}</p>
|
||||
<v-img v-if="isImage" :src="fileData.url" max-width="300" />
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import rdf from 'rdf-ext'
|
||||
|
||||
// Allowed RDF file MIME types
|
||||
const allowedMimeTypes = [
|
||||
'application/ld+json', // JSON-LD
|
||||
'application/rdf+xml', // RDF/XML
|
||||
'text/turtle', // Turtle
|
||||
'application/n-triples', // N-Triples
|
||||
'application/trig', // TriG
|
||||
]
|
||||
|
||||
const selectedFile = ref(null)
|
||||
const fileUrl = ref('')
|
||||
const isDragging = ref(false)
|
||||
const fileData = ref(null)
|
||||
|
||||
// Check if the file is an image (for preview purposes)
|
||||
const isImage = computed(() => {
|
||||
return fileData.value && fileData.value.type.startsWith('image/')
|
||||
})
|
||||
|
||||
const rules = {
|
||||
required: value => !!value || 'File is required',
|
||||
validRdfFile: value => {
|
||||
if (value && value.type && allowedMimeTypes.includes(value.type)) {
|
||||
return true
|
||||
}
|
||||
return 'Invalid file type. Please upload a valid RDF file.'
|
||||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// Handle URL input for an RDF file
|
||||
const onUrlInput = () => {
|
||||
const url = fileUrl.value
|
||||
fetch(url)
|
||||
.then(response => response.blob())
|
||||
.then(blob => {
|
||||
const file = new File([blob], url.substring(url.lastIndexOf('/') + 1), {
|
||||
type: blob.type,
|
||||
})
|
||||
validateAndReadFile(file)
|
||||
})
|
||||
.catch(() => {
|
||||
alert('Failed to fetch file from URL')
|
||||
})
|
||||
}
|
||||
|
||||
// Validate file type and read it
|
||||
const validateAndReadFile = (file) => {
|
||||
if (!allowedMimeTypes.includes(file.type)) {
|
||||
alert('Invalid file type. Please upload a valid RDF file.')
|
||||
return
|
||||
}
|
||||
|
||||
// Further validate if it's an RDF file by trying to parse it
|
||||
const reader = new FileReader()
|
||||
reader.onload = (e) => {
|
||||
const content = e.target.result
|
||||
try {
|
||||
rdf.dataset() // Dummy usage of rdf-ext; actual validation logic can be added here if necessary.
|
||||
// If the file is valid, process and store it
|
||||
fileData.value = {
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
type: file.type,
|
||||
url: URL.createObjectURL(file),
|
||||
}
|
||||
} catch (error) {
|
||||
alert('The file content is not a valid RDF serialization.')
|
||||
}
|
||||
}
|
||||
reader.readAsText(file)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.drag-drop-area {
|
||||
padding: 20px;
|
||||
border: 2px solid transparent;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.drag-drop-area.dragover {
|
||||
border: 2px dashed #3f51b5;
|
||||
background-color: #f0f0f0;
|
||||
filter: grayscale(50%);
|
||||
}
|
||||
|
||||
.drag-drop-area .v-col {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
|
@ -68,6 +68,19 @@
|
|||
</div>
|
||||
</template>
|
||||
</v-autocomplete>
|
||||
<v-dialog
|
||||
v-model="uploadMultiRecord"
|
||||
transition="dialog-top-transition"
|
||||
>
|
||||
<TableLoader
|
||||
@close-multirecord="uploadMultiRecord = false"
|
||||
:shape_iri="propClass"
|
||||
:node_idx="props.node_idx"
|
||||
:propClassList="propClassList"
|
||||
:parent_shape_iri="props.node_uid"
|
||||
:parent_property_iri="props.triple_uid"
|
||||
></TableLoader>
|
||||
</v-dialog>
|
||||
</v-input>
|
||||
</template>
|
||||
|
||||
|
|
@ -145,6 +158,7 @@
|
|||
newNodeIdx.value = null
|
||||
};
|
||||
provide('saveFormHandler', saveDialogForm);
|
||||
const uploadMultiRecord = inject('uploadMultiRecord')
|
||||
|
||||
// ------------------- //
|
||||
// Computed properties //
|
||||
|
|
@ -190,6 +204,10 @@
|
|||
}
|
||||
}, {immediate: true });
|
||||
|
||||
watch(uploadMultiRecord, (newval) => {
|
||||
if (newval) {console.log("Upload button pressed")}
|
||||
}, { immediate: true });
|
||||
|
||||
|
||||
watch(rdfDS.data.graph, () => {
|
||||
console.log("CHECK: graphdata instanceselecteditor")
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
size="x-small"
|
||||
class="rounded-lg"
|
||||
@click="editInstanceItem(record)"
|
||||
:disabled="props.formOpen"
|
||||
></v-btn>
|
||||
</v-card-title>
|
||||
<v-card-subtitle>{{ toCURIE(record.subtitle, allPrefixes) }}</v-card-subtitle>
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@
|
|||
density="comfortable"
|
||||
></v-btn>
|
||||
|
||||
<!-- Add button -->
|
||||
<!-- Add single button -->
|
||||
<v-btn v-if="allowAddTriple(triple_idx)"
|
||||
rounded="0"
|
||||
elevation="1"
|
||||
|
|
@ -44,6 +44,15 @@
|
|||
@click="formData.addObject(localNodeUid, localNodeIdx, my_uid)"
|
||||
density="comfortable"
|
||||
></v-btn>
|
||||
<!-- Add multiple button -->
|
||||
|
||||
<v-btn v-if="allowAddMany(triple_idx)"
|
||||
rounded="0"
|
||||
elevation="1"
|
||||
icon="mdi-upload"
|
||||
@click="selectUploadMultiRecord()"
|
||||
density="comfortable"
|
||||
></v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</span>
|
||||
|
|
@ -52,7 +61,7 @@
|
|||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onBeforeMount, computed, inject, onBeforeUpdate, onBeforeUnmount, watch, toRaw} from 'vue'
|
||||
import { ref, onMounted, onBeforeMount, computed, inject, onBeforeUpdate, onBeforeUnmount, provide, watch, toRaw} from 'vue'
|
||||
import { SHACL } from '../modules/namespaces'
|
||||
import { useRules } from '../composables/rules'
|
||||
import { nameOrCURIE, addCodeTagsToText} from '../modules/utils';
|
||||
|
|
@ -83,6 +92,8 @@
|
|||
const shapesDS = inject('shapesDS');
|
||||
const show_all_fields = inject('show_all_fields');
|
||||
const { isRequired } = useRules(localPropertyShape.value)
|
||||
const uploadMultiRecord = ref(false)
|
||||
provide('uploadMultiRecord', uploadMultiRecord)
|
||||
|
||||
// ----------------- //
|
||||
// Lifecycle methods //
|
||||
|
|
@ -172,6 +183,17 @@
|
|||
return false
|
||||
}
|
||||
|
||||
function allowAddMany(idx) {
|
||||
if (allowAddTriple(idx) && [SHACL.IRI.value, SHACL.BlankNodeOrIRI.value, SHACL.BlankNode.value].includes(localPropertyShape.value[SHACL.nodeKind.value])) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function selectUploadMultiRecord() {
|
||||
uploadMultiRecord.value = true;
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -34,7 +34,8 @@
|
|||
<span v-if="selectedIRI">
|
||||
<h2 class="mx-4 mb-4 truncate-heading">
|
||||
{{ toCURIE(selectedIRI, allPrefixes) }}
|
||||
<v-btn icon="mdi-plus" size="x-small" variant="tonal" @click="addInstanceItem()"></v-btn>
|
||||
<v-btn icon="mdi-plus" size="x-small" variant="tonal" @click="addInstanceItem()" :disabled="formOpen"></v-btn>
|
||||
<v-btn icon="mdi-upload" size="x-small" variant="tonal" @click="selectUploadMultiRecord()" :disabled="formOpen"></v-btn>
|
||||
</h2>
|
||||
|
||||
<p class="mx-4 mb-4" v-html="formattedDescription"></p>
|
||||
|
|
@ -52,6 +53,15 @@
|
|||
<em>No items</em>
|
||||
</div>
|
||||
</span>
|
||||
<v-dialog
|
||||
v-model="uploadMultiRecord"
|
||||
transition="dialog-top-transition"
|
||||
>
|
||||
<TableLoader
|
||||
@close-multirecord="uploadMultiRecord = false"
|
||||
:shape_iri="selectedIRI"
|
||||
></TableLoader>
|
||||
</v-dialog>
|
||||
</span>
|
||||
<span v-else style="margin-top: 1em; margin-left: 1em;">
|
||||
<em>Select a data type</em>
|
||||
|
|
@ -173,6 +183,8 @@
|
|||
// - FETCH FROM SERVICE IF REQUIRED
|
||||
// - SET VIEW FROM QUERY
|
||||
// ---------------------------------------------- //
|
||||
const uploadMultiRecord = ref(false)
|
||||
const componentReady = ref(false)
|
||||
const allPrefixes = reactive({});
|
||||
const page_ready = ref(false);
|
||||
provide('allPrefixes', allPrefixes)
|
||||
|
|
@ -226,7 +238,7 @@
|
|||
provide('submitFn', submitFn)
|
||||
provide('canSubmit', canSubmit)
|
||||
const noSubmitDialog = ref(false)
|
||||
const submitDialog = ref(false)
|
||||
const submitDialog = ref(false)
|
||||
provide('submitDialog', submitDialog)
|
||||
// When user clicks the submit button
|
||||
watch(submitButtonPressed, (newValue) => {
|
||||
|
|
@ -243,7 +255,9 @@
|
|||
}, { immediate: true });
|
||||
|
||||
|
||||
|
||||
function selectUploadMultiRecord() {
|
||||
uploadMultiRecord.value = true;
|
||||
}
|
||||
|
||||
const activatedInstancesSelectEditor = ref(null)
|
||||
provide('activatedInstancesSelectEditor', activatedInstancesSelectEditor)
|
||||
|
|
|
|||
318
src/components/TableLoader.vue
Normal file
318
src/components/TableLoader.vue
Normal file
|
|
@ -0,0 +1,318 @@
|
|||
<template>
|
||||
<v-container fluid>
|
||||
<v-card class="d-flex flex-column justify-center">
|
||||
<!-- Drag and Drop area -->
|
||||
<v-card-title class="justify-center">Upload, drag and drop, or link a CSV file</v-card-title>
|
||||
|
||||
<v-card-text>
|
||||
<v-select :items="allClassIRIs" v-model="selectedShapeIRI" variant="solo" density="compact">
|
||||
</v-select>
|
||||
</v-card-text>
|
||||
<v-card
|
||||
class="drag-drop-area"
|
||||
:class="{'dragover': isDragging}"
|
||||
max-width="80%"
|
||||
variant="tonal"
|
||||
@dragover.prevent="onDragOver"
|
||||
@dragleave.prevent="onDragLeave"
|
||||
@drop.prevent="onFileDrop"
|
||||
>
|
||||
<v-row >
|
||||
<!-- File input on the left -->
|
||||
<v-col cols="6" class="justify-center align-center">
|
||||
<v-input>
|
||||
<v-icon style="margin-top: 0.25em;">mdi-upload</v-icon>
|
||||
<v-btn
|
||||
variant="outlined"
|
||||
@click="onFileSelect"
|
||||
style="margin-left: 1em;"
|
||||
> Upload a CSV/TSV file
|
||||
</v-btn>
|
||||
</v-input>
|
||||
</v-col>
|
||||
|
||||
<!-- URL input on the right -->
|
||||
<v-col cols="6">
|
||||
<v-text-field
|
||||
label="Or paste a CSV/TSV file URL"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
v-model="fileUrl"
|
||||
prepend-icon="mdi-link"
|
||||
@change="onUrlInput"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card>
|
||||
<!-- Uploaded file details or preview -->
|
||||
<v-card v-if="fileData" class="mt-5">
|
||||
<v-card-title>File details:</v-card-title>
|
||||
<v-card-text>
|
||||
<p><strong>Name:</strong> {{ fileData.name }}</p>
|
||||
<p><strong>Size:</strong> {{ formatBytes(fileData.size) }}</p>
|
||||
<p><strong>Type:</strong> {{ fileData.type }}</p>
|
||||
<v-img v-if="isImage" :src="fileData.url" max-width="300" />
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
|
||||
<v-card v-show="showTable">
|
||||
|
||||
<div id="mytable"></div>
|
||||
<div style="display: flex; margin: 1em; margin-left: auto;">
|
||||
<v-btn
|
||||
text="Cancel"
|
||||
@click="closeDialog()"
|
||||
style="margin-left: auto; margin-right: 1em;"
|
||||
prepend-icon="mdi-close-box"
|
||||
></v-btn>
|
||||
<v-btn
|
||||
text="Save"
|
||||
type="submit"
|
||||
@click="saveData()"
|
||||
prepend-icon="mdi-content-save"
|
||||
></v-btn>
|
||||
</div>
|
||||
</v-card>
|
||||
|
||||
</v-card>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, inject, toRaw} from 'vue'
|
||||
import {TabulatorFull as Tabulator} from 'tabulator-tables';
|
||||
import { DataTable } from '../classes/DataTable'
|
||||
import { toCURIE } from 'shacl-tulip';
|
||||
|
||||
// ----- //
|
||||
// Props //
|
||||
// ----- //
|
||||
const props = defineProps({
|
||||
shape_iri: String,
|
||||
node_idx: String,
|
||||
propClassList: Array,
|
||||
parent_shape_iri: String,
|
||||
parent_property_iri: String,
|
||||
})
|
||||
const localShapeIri = ref(props.shape_iri);
|
||||
console.log(localShapeIri.value)
|
||||
const localNodeIdx = ref(props.node_idx);
|
||||
console.log(localNodeIdx.value)
|
||||
console.log(props.propClassList)
|
||||
|
||||
const shapesDS = inject('shapesDS')
|
||||
const rdfDS = inject('rdfDS')
|
||||
const formData = inject('formData')
|
||||
const shape_obj = shapesDS.data.nodeShapes[localShapeIri.value]
|
||||
const allPrefixes = inject('allPrefixes');
|
||||
const ID_IRI = inject('ID_IRI')
|
||||
|
||||
const allClassIRIs = ref([])
|
||||
const selectedShapeIRI = ref(localShapeIri.value)
|
||||
|
||||
|
||||
|
||||
onMounted( () => {
|
||||
// if the node_idx is passed, it means the TableLoader component
|
||||
// was activated from within the NodeShapeEditor (via InstancesSelectEditor).
|
||||
if (props.node_idx) {
|
||||
allClassIRIs.value = props.propClassList
|
||||
} else {
|
||||
allClassIRIs.value.push(
|
||||
{
|
||||
title: toCURIE(localShapeIri.value, allPrefixes),
|
||||
value: localShapeIri.value
|
||||
}
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['close-multirecord']);
|
||||
function closeDialog() {
|
||||
emit('close-multirecord');
|
||||
};
|
||||
|
||||
// Allowed RDF file MIME types
|
||||
const allowedMimeTypes = [
|
||||
'text/csv',
|
||||
'text/tsv'
|
||||
]
|
||||
|
||||
var table
|
||||
const showTable = ref(false)
|
||||
|
||||
const selectedFile = ref(null)
|
||||
const fileUrl = ref('')
|
||||
const isDragging = ref(false)
|
||||
const fileData = ref(null)
|
||||
|
||||
// Check if the file is an image (for preview purposes)
|
||||
const isImage = computed(() => {
|
||||
return fileData.value && fileData.value.type.startsWith('image/')
|
||||
})
|
||||
|
||||
const rules = {
|
||||
// required: value => !!value || 'File is required',
|
||||
validRdfFile: value => {
|
||||
if (value && value.type && allowedMimeTypes.includes(value.type)) {
|
||||
return true
|
||||
}
|
||||
// console.log(value.type)
|
||||
return 'Invalid file type. Please upload a valid CSV/TSV file.'
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
table = new Tabulator("#mytable", {
|
||||
// height:'50vh',
|
||||
layout:"fitColumns",
|
||||
// importFormat:"csv",
|
||||
autoColumns: true
|
||||
});
|
||||
})
|
||||
|
||||
// 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 upload button
|
||||
function onFileSelect() {
|
||||
table.import("csv", ".csv,.tsv")
|
||||
.then(() => {
|
||||
showTable.value = true;
|
||||
})
|
||||
.catch(() => {
|
||||
console.error("Table importing failed!")
|
||||
})
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// Handle URL input
|
||||
const onUrlInput = () => {
|
||||
const url = fileUrl.value
|
||||
fetch(url)
|
||||
.then(response => response.blob())
|
||||
.then(blob => {
|
||||
const file = new File([blob], url.substring(url.lastIndexOf('/') + 1), {
|
||||
type: blob.type,
|
||||
})
|
||||
validateAndReadFile(file)
|
||||
})
|
||||
.catch(() => {
|
||||
alert('Failed to fetch file from URL')
|
||||
})
|
||||
}
|
||||
|
||||
// Validate file type and read it
|
||||
const validateAndReadFile = (file) => {
|
||||
if (!allowedMimeTypes.includes(file.type)) {
|
||||
alert('Invalid file type. Please upload a valid CSV/TSV file.')
|
||||
return
|
||||
}
|
||||
|
||||
// Further validate if it's an RDF file by trying to parse it
|
||||
const reader = new FileReader()
|
||||
reader.onload = (e) => {
|
||||
const content = e.target.result
|
||||
|
||||
table = new Tabulator("#mytable", {
|
||||
// height:'50vh',
|
||||
layout:"fitColumns",
|
||||
importFormat:"csv",
|
||||
autoColumns: true
|
||||
});
|
||||
|
||||
table.on("tableBuilt", function(){
|
||||
table.setData(content);
|
||||
showTable.value = true;
|
||||
});
|
||||
}
|
||||
reader.readAsText(file)
|
||||
}
|
||||
|
||||
function saveData() {
|
||||
console.log(table.getColumnDefinitions())
|
||||
console.log(table.getData())
|
||||
console.log(toRaw(shape_obj))
|
||||
console.log(toRaw(shape_obj.properties))
|
||||
console.log(table.getColumnDefinitions().map(col => col.title))
|
||||
|
||||
const dT = new DataTable(
|
||||
table,
|
||||
selectedShapeIRI.value,
|
||||
localNodeIdx.value,
|
||||
shapesDS,
|
||||
formData,
|
||||
toRaw(allPrefixes),
|
||||
ID_IRI.value
|
||||
)
|
||||
|
||||
dT.saveTable(rdfDS, props.parent_shape_iri, props.parent_property_iri)
|
||||
closeDialog()
|
||||
// console.log(toRaw(formData.content))
|
||||
|
||||
// shape_obj = shapesDS.data.nodeShapes[localShapeIri.value]
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.drag-drop-area {
|
||||
padding: 20px;
|
||||
padding-bottom: 0;
|
||||
/* border: 1px solid rgb(201, 201, 201); */
|
||||
border: 1px solid transparent;
|
||||
transition: all 0.3s ease;
|
||||
margin: auto;
|
||||
margin-bottom:1em;
|
||||
min-width: 80vw;
|
||||
}
|
||||
|
||||
.drag-drop-area.dragover {
|
||||
border: 2px dashed #3f51b5;
|
||||
background-color: #f0f0f0;
|
||||
filter: grayscale(50%);
|
||||
}
|
||||
|
||||
.drag-drop-area .v-col {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
#mytable {
|
||||
margin: 1em 1em;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
@import "/node_modules/tabulator-tables/dist/css/tabulator_site.min.css";
|
||||
</style>
|
||||
Loading…
Add table
Add a link
Reference in a new issue