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",
|
"@rdfjs/parser-n3": "^2.0.2",
|
||||||
"rdf-ext": "^2.5.2",
|
"rdf-ext": "^2.5.2",
|
||||||
"roboto-fontface": "^0.10.0",
|
"roboto-fontface": "^0.10.0",
|
||||||
|
"tabulator-tables": "^6.3.1",
|
||||||
"util": "^0.12.5",
|
"util": "^0.12.5",
|
||||||
"uuid": "^10.0.0"
|
"uuid": "^10.0.0"
|
||||||
},
|
},
|
||||||
|
|
@ -5211,6 +5212,12 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/tinybench": {
|
||||||
"version": "2.9.0",
|
"version": "2.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,7 @@
|
||||||
"@rdfjs/parser-n3": "^2.0.2",
|
"@rdfjs/parser-n3": "^2.0.2",
|
||||||
"rdf-ext": "^2.5.2",
|
"rdf-ext": "^2.5.2",
|
||||||
"roboto-fontface": "^0.10.0",
|
"roboto-fontface": "^0.10.0",
|
||||||
|
"tabulator-tables": "^6.3.1",
|
||||||
"util": "^0.12.5",
|
"util": "^0.12.5",
|
||||||
"uuid": "^10.0.0"
|
"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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</v-autocomplete>
|
</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>
|
</v-input>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
@ -145,6 +158,7 @@
|
||||||
newNodeIdx.value = null
|
newNodeIdx.value = null
|
||||||
};
|
};
|
||||||
provide('saveFormHandler', saveDialogForm);
|
provide('saveFormHandler', saveDialogForm);
|
||||||
|
const uploadMultiRecord = inject('uploadMultiRecord')
|
||||||
|
|
||||||
// ------------------- //
|
// ------------------- //
|
||||||
// Computed properties //
|
// Computed properties //
|
||||||
|
|
@ -190,6 +204,10 @@
|
||||||
}
|
}
|
||||||
}, {immediate: true });
|
}, {immediate: true });
|
||||||
|
|
||||||
|
watch(uploadMultiRecord, (newval) => {
|
||||||
|
if (newval) {console.log("Upload button pressed")}
|
||||||
|
}, { immediate: true });
|
||||||
|
|
||||||
|
|
||||||
watch(rdfDS.data.graph, () => {
|
watch(rdfDS.data.graph, () => {
|
||||||
console.log("CHECK: graphdata instanceselecteditor")
|
console.log("CHECK: graphdata instanceselecteditor")
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
size="x-small"
|
size="x-small"
|
||||||
class="rounded-lg"
|
class="rounded-lg"
|
||||||
@click="editInstanceItem(record)"
|
@click="editInstanceItem(record)"
|
||||||
|
:disabled="props.formOpen"
|
||||||
></v-btn>
|
></v-btn>
|
||||||
</v-card-title>
|
</v-card-title>
|
||||||
<v-card-subtitle>{{ toCURIE(record.subtitle, allPrefixes) }}</v-card-subtitle>
|
<v-card-subtitle>{{ toCURIE(record.subtitle, allPrefixes) }}</v-card-subtitle>
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@
|
||||||
density="comfortable"
|
density="comfortable"
|
||||||
></v-btn>
|
></v-btn>
|
||||||
|
|
||||||
<!-- Add button -->
|
<!-- Add single button -->
|
||||||
<v-btn v-if="allowAddTriple(triple_idx)"
|
<v-btn v-if="allowAddTriple(triple_idx)"
|
||||||
rounded="0"
|
rounded="0"
|
||||||
elevation="1"
|
elevation="1"
|
||||||
|
|
@ -44,6 +44,15 @@
|
||||||
@click="formData.addObject(localNodeUid, localNodeIdx, my_uid)"
|
@click="formData.addObject(localNodeUid, localNodeIdx, my_uid)"
|
||||||
density="comfortable"
|
density="comfortable"
|
||||||
></v-btn>
|
></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-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -52,7 +61,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<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 { SHACL } from '../modules/namespaces'
|
||||||
import { useRules } from '../composables/rules'
|
import { useRules } from '../composables/rules'
|
||||||
import { nameOrCURIE, addCodeTagsToText} from '../modules/utils';
|
import { nameOrCURIE, addCodeTagsToText} from '../modules/utils';
|
||||||
|
|
@ -83,6 +92,8 @@
|
||||||
const shapesDS = inject('shapesDS');
|
const shapesDS = inject('shapesDS');
|
||||||
const show_all_fields = inject('show_all_fields');
|
const show_all_fields = inject('show_all_fields');
|
||||||
const { isRequired } = useRules(localPropertyShape.value)
|
const { isRequired } = useRules(localPropertyShape.value)
|
||||||
|
const uploadMultiRecord = ref(false)
|
||||||
|
provide('uploadMultiRecord', uploadMultiRecord)
|
||||||
|
|
||||||
// ----------------- //
|
// ----------------- //
|
||||||
// Lifecycle methods //
|
// Lifecycle methods //
|
||||||
|
|
@ -172,6 +183,17 @@
|
||||||
return false
|
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>
|
</script>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,8 @@
|
||||||
<span v-if="selectedIRI">
|
<span v-if="selectedIRI">
|
||||||
<h2 class="mx-4 mb-4 truncate-heading">
|
<h2 class="mx-4 mb-4 truncate-heading">
|
||||||
{{ toCURIE(selectedIRI, allPrefixes) }}
|
{{ 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>
|
</h2>
|
||||||
|
|
||||||
<p class="mx-4 mb-4" v-html="formattedDescription"></p>
|
<p class="mx-4 mb-4" v-html="formattedDescription"></p>
|
||||||
|
|
@ -52,6 +53,15 @@
|
||||||
<em>No items</em>
|
<em>No items</em>
|
||||||
</div>
|
</div>
|
||||||
</span>
|
</span>
|
||||||
|
<v-dialog
|
||||||
|
v-model="uploadMultiRecord"
|
||||||
|
transition="dialog-top-transition"
|
||||||
|
>
|
||||||
|
<TableLoader
|
||||||
|
@close-multirecord="uploadMultiRecord = false"
|
||||||
|
:shape_iri="selectedIRI"
|
||||||
|
></TableLoader>
|
||||||
|
</v-dialog>
|
||||||
</span>
|
</span>
|
||||||
<span v-else style="margin-top: 1em; margin-left: 1em;">
|
<span v-else style="margin-top: 1em; margin-left: 1em;">
|
||||||
<em>Select a data type</em>
|
<em>Select a data type</em>
|
||||||
|
|
@ -173,6 +183,8 @@
|
||||||
// - FETCH FROM SERVICE IF REQUIRED
|
// - FETCH FROM SERVICE IF REQUIRED
|
||||||
// - SET VIEW FROM QUERY
|
// - SET VIEW FROM QUERY
|
||||||
// ---------------------------------------------- //
|
// ---------------------------------------------- //
|
||||||
|
const uploadMultiRecord = ref(false)
|
||||||
|
const componentReady = ref(false)
|
||||||
const allPrefixes = reactive({});
|
const allPrefixes = reactive({});
|
||||||
const page_ready = ref(false);
|
const page_ready = ref(false);
|
||||||
provide('allPrefixes', allPrefixes)
|
provide('allPrefixes', allPrefixes)
|
||||||
|
|
@ -243,7 +255,9 @@
|
||||||
}, { immediate: true });
|
}, { immediate: true });
|
||||||
|
|
||||||
|
|
||||||
|
function selectUploadMultiRecord() {
|
||||||
|
uploadMultiRecord.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
const activatedInstancesSelectEditor = ref(null)
|
const activatedInstancesSelectEditor = ref(null)
|
||||||
provide('activatedInstancesSelectEditor', activatedInstancesSelectEditor)
|
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