Commit fbec28b2 authored by Petar Hristov's avatar Petar Hristov 💬
Browse files

New: Include multi-field feature

parent c6a9062a
module.exports = {
root: true,
env: {
node: true,
},
extends: [
'plugin:vue/essential',
'eslint:recommended',
'@vue/typescript/recommended',
'@vue/prettier',
'@vue/prettier/@typescript-eslint',
],
parserOptions: {
ecmaVersion: 2020,
},
rules: {
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
},
};
packageExtensions:
"@vue/cli-service@*":
peerDependencies:
"@vue/cli-plugin-babel": "*"
"@vue/cli-plugin-eslint": "*"
"@vue/cli-plugin-typescript": "*"
"@vue/cli-plugin-typescript@*":
peerDependencies:
"babel-loader": "*"
"fork-ts-checker-webpack-plugin@*":
dependencies:
"vue-template-compiler": "*"
peerDependencies:
"typescript": "*"
"vue-i18n@*":
dependencies:
"vue": "^2.6.12"
"vue": "^2.6.14"
"vue-material-design-icons@*":
dependencies:
"vue": "^2.6.12"
"vue-router@*":
dependencies:
"vue": "^2.6.12"
"vuex@*":
"vue": "^2.6.14"
"vuelidate@*":
dependencies:
"vue": "^2.6.12"
"vue": "^2.6.14"
"bootstrap-vue@*":
dependencies:
"vue": "^2.6.12"
"vue": "^2.6.14"
"jquery": "*"
......@@ -24,15 +24,12 @@
"dependencies": {
"@coscine/api-connection": "^1.30.0",
"@coscine/app-util": "^1.9.0",
"@types/jquery": "^3.5.2",
"@types/node": "^14.14.12",
"@types/rdf-js": "^4.0.0",
"@types/vuelidate": "^0.7.13",
"bootstrap-vue": "^2.20.1",
"rdf-ext": "^1.3.0",
"rdf-ext": "^1.3.4",
"rdf-parse": "^1.5.0",
"rdf-validate-shacl": "^0.2.5",
"vue": "^2.6.12",
"uuid": "^8.3.2",
"vue": "^2.6.14",
"vue-i18n": "^8.22.2",
"vue-material-design-icons": "^4.11.0",
"vue-multiselect": "^2.1.6",
......@@ -46,17 +43,27 @@
"@semantic-release/gitlab": "^6.0.5",
"@semantic-release/npm": "^7.0.6",
"@semantic-release/release-notes-generator": "^9.0.1",
"@types/jquery": "^3.5.2",
"@types/node": "^14.14.20",
"@typescript-eslint/eslint-plugin": "^4.12.0",
"@typescript-eslint/parser": "^4.12.0",
"@types/rdf-js": "^4.0.0",
"@types/uuid": "^8.3.1",
"@types/vuelidate": "^0.7.13",
"@typescript-eslint/eslint-plugin": "^4.18.0",
"@typescript-eslint/parser": "^4.18.0",
"@vue/cli-plugin-eslint": "^4.5.7",
"@vue/cli-plugin-typescript": "^4.5.7",
"@vue/cli-service": "^4.5.7",
"@vue/eslint-config-prettier": "^6.0.0",
"@vue/eslint-config-typescript": "^7.0.0",
"conventional-changelog-eslint": "3.0.9",
"core-js": "^3.8.2",
"eslint": "^7.17.0",
"eslint-plugin-prettier": "^3.3.1",
"eslint-plugin-vue": "^6.2.2",
"prettier": "^2.2.1",
"semantic-release": "^17.3.1",
"typescript": "^4.0.3",
"vue-template-compiler": "^2.6.12"
"typescript": "^4.1.3",
"vue-template-compiler": "^2.6.14"
},
"repository": {
"type": "git",
......
......@@ -27,17 +27,10 @@ import VueI18n from 'vue-i18n';
import BootstrapVue from 'bootstrap-vue';
import Vuelidate from 'vuelidate';
import { validationMixin } from 'vuelidate';
import { required, minLength, maxLength } from 'vuelidate/lib/validators';
import { Quad } from 'rdf-js';
import { LanguageUtil } from '@coscine/app-util';
import FieldReader from './util/FieldReader';
import WrapperInput from './components/WrapperInput.vue';
import LinkedDataHandler from './base/LinkedDataHandler';
Vue.use(VueI18n);
......@@ -46,7 +39,10 @@ Vue.use(Vuelidate);
const i18n = new VueI18n({
locale: 'en',
messages: (window.coscine && coscine.i18n) ? coscine.i18n['form-generator'] : {},
messages:
window.coscine && window.coscine.i18n
? window.coscine.i18n['form-generator']
: undefined,
silentFallbackWarn: true,
});
......@@ -110,8 +106,7 @@ export default LinkedDataHandler.extend({
validations: {} as any,
};
},
validations() : any {
const me = this;
validations(): any {
if (!this.fixedValueMode) {
return {
formData: this.validations,
......@@ -137,28 +132,38 @@ export default LinkedDataHandler.extend({
},
methods: {
createValidations() {
const me = this;
const validator = {} as any;
for (const nodename of Object.keys(this.formData)) {
validator[nodename] = {
$each: {
value: {
async shaclValidated(value: any) {
me.input();
shaclValidated: async (value: any) => {
this.input();
// A debounce has been implemented based on the nodename
if (me.timeouts[nodename]) clearTimeout(me.timeouts[nodename]);
if (this.timeouts[nodename])
clearTimeout(this.timeouts[nodename]);
return new Promise((resolve, reject) => {
me.timeouts[nodename] = setTimeout(async() => {
this.timeouts[nodename] = setTimeout(async () => {
// Validate the whole data and check if for the current nodename an error is logged
const report = await me.validateMetadata(me.formData, me.quads, me.applicationProfileId);
const report = await this.validateMetadata(
this.formData,
this.quads,
this.applicationProfileId
);
let resolveValue = true;
if (report.results.some((entry: any) => entry.path.value === nodename)) {
const result = report.results.find((entry: any) => entry.path.value === nodename);
me.errorMessages[nodename] = result.message;
if (
report.results.some(
(entry: any) => entry.path.value === nodename
)
) {
const result = report.results.find(
(entry: any) => entry.path.value === nodename
);
this.errorMessages[nodename] = result.message;
resolveValue = false;
}
resolve(resolveValue);
}, me.timeoutInterval);
}, this.timeoutInterval);
});
},
},
......@@ -168,17 +173,29 @@ export default LinkedDataHandler.extend({
this.validations = validator;
},
async handleApplicationProfiles() {
this.quads = await this.retrieveQuads(this.SHACLDefinition, this.mimeType, this.applicationProfileId);
this.properties = this.retrieveProperties(this.SHACLDefinition, this.quads, this.applicationProfileId);
this.quads = await this.retrieveQuads(
this.SHACLDefinition,
this.mimeType,
this.applicationProfileId
);
this.properties = this.retrieveProperties(
this.SHACLDefinition,
this.quads,
this.applicationProfileId
);
const unmappedSubjects = this.retrievePropertyOrder(this.properties);
this.setPropertySettings(this.properties);
this.setSHACLDefinition(unmappedSubjects);
this.createValidations();
await this.validateMetadata(this.formData, this.quads, this.applicationProfileId);
await this.validateMetadata(
this.formData,
this.quads,
this.applicationProfileId
);
},
input() {
this.$emit('input', this.formData);
......@@ -195,35 +212,55 @@ export default LinkedDataHandler.extend({
setSHACLDefinition(unmappedSubjects: any) {
this.fixedValueIds = [];
this.sortedSHACLDefinition = [];
let keys = Object.keys(unmappedSubjects).sort((a, b) => parseInt(a, 10) - parseInt(b, 10));
let keys = Object.keys(unmappedSubjects).sort(
(a, b) => parseInt(a, 10) - parseInt(b, 10)
);
for (let i = 0; i < keys.length; i++) {
this.fixedValueIds.push(unmappedSubjects[keys[i]][0].subject.value);
this.sortedSHACLDefinition.push(unmappedSubjects[keys[i]]);
const nodeName = FieldReader.getNodeName(unmappedSubjects[keys[i]]);
const minCount = FieldReader.getMinCount(unmappedSubjects[keys[i]], 1);
// if formData is empty intialize it with an empty value
if (!FieldReader.isDataValueAssignedToKey(this.formData, nodeName, 'value', 0)) {
// ToDo: replace last entry 0 by minCount defined in the application profile
this.$set(this.formData, nodeName, [{ value: '' }]);
if (!FieldReader.isDataValueAssigned(this.formData, nodeName)) {
this.$set(this.formData, nodeName, []);
for (let index = 0; index < minCount; index++) {
this.formData[nodeName].push({ value: '' });
}
} else {
for (let index = 0; index < minCount; index++) {
if (
!FieldReader.isDataValueAssignedToKey(
this.formData,
nodeName,
'value',
index
)
) {
this.formData[nodeName][index] = { value: '' };
}
}
}
}
},
checkField(data: any, nodename: string, property: string = 'value') {
checkField(data: any, nodename: string, property = 'value') {
return FieldReader.isValueAssignedToKey(data, nodename, property);
},
fieldDefinition(formElement: any) {
if (
this.checkField(formElement, 'http://www.w3.org/ns/shacl#datatype')
) {
let datatype = FieldReader.getObject(formElement, 'http://www.w3.org/ns/shacl#datatype');
if (this.checkField(formElement, 'http://www.w3.org/ns/shacl#datatype')) {
let datatype = FieldReader.getObject(
formElement,
'http://www.w3.org/ns/shacl#datatype'
);
if (datatype['value'] === 'http://www.w3.org/2001/XMLSchema#string') {
if (
this.checkField(
formElement,
'http://datashapes.org/dash#singleLine'
) &&
FieldReader.getObject(formElement, 'http://datashapes.org/dash#singleLine')[
'value'
] === 'false'
FieldReader.getObject(
formElement,
'http://datashapes.org/dash#singleLine'
)['value'] === 'false'
) {
return 'InputTextArea';
} else {
......@@ -233,16 +270,13 @@ export default LinkedDataHandler.extend({
datatype['value'] === 'http://www.w3.org/2001/XMLSchema#date'
) {
return 'InputDatePicker';
}
else if (
} else if (
datatype['value'] === 'http://www.w3.org/2001/XMLSchema#boolean'
) {
return 'InputBooleanCombobox';
}
} else {
if (
this.checkField(formElement, 'http://www.w3.org/ns/shacl#class')
) {
if (this.checkField(formElement, 'http://www.w3.org/ns/shacl#class')) {
return 'InputCombobox';
}
}
......@@ -258,14 +292,34 @@ export default LinkedDataHandler.extend({
}
.multiselect__input {
border: 0px !important;
height: calc(1.4em + 0.75rem + 2px);
height: auto;
padding: 0px;
}
.multiselect__tags {
border-radius: 0px;
height: calc(1.4em + 0.75rem + 2px);
min-height: calc(1.4em + 0.75rem + 2px);
padding-top: 2px;
padding-bottom: 2px;
padding-left: 5px;
}
.multiselect__tag {
background-color: #00549f !important;
}
.multiselect__select {
height: calc(1.4em + 0.75rem + 2px);
min-height: calc(1.4em + 0.75rem + 2px);
}
.multiselect,
.multiselect__input,
.multiselect__single {
font-size: 14px;
}
.multiselect__input,
.multiselect__single {
vertical-align: -webkit-baseline-middle;
margin-bottom: unset;
}
.multiselect__option--highlight {
background-color: #00549f !important;
background: #00549f !important;
......@@ -301,14 +355,7 @@ export default LinkedDataHandler.extend({
#FormGenerator .col-form-label {
font-weight: bold;
}
#FormGenerator .lockButton {
height: calc(1.4em + 0.75rem + 2px);
margin: 0px !important;
min-width: 60px !important;
width: 60px !important;
max-width: 60px !important;
}
#FormGenerator .visibilityButton {
#FormGenerator .innerButton {
height: calc(1.4em + 0.75rem + 2px);
margin: 0px !important;
min-width: 60px !important;
......
import Vue from 'vue'
import Vue from 'vue';
import factory from 'rdf-ext';
import SHACLValidator from 'rdf-validate-shacl';
......@@ -13,32 +13,22 @@ export default Vue.extend({
};
},
methods: {
async validateMetadata(formData: any, quads: Array<Quad>, applicationProfileId: string) {
async validateMetadata(
formData: any,
quads: Array<Quad>,
applicationProfileId: string
) {
// RDF/JSON => JSON-LD since the loadDataset function doesn't support RDF/JSON
const combinedDataObject = [];
const dataObject = {} as any;
dataObject['@type'] = applicationProfileId;
for (const entry of Object.keys(formData)) {
for (const metadataEntry of formData[entry]) {
if (metadataEntry['value'] !== '') {
if (metadataEntry['type'] === 'uri') {
dataObject[entry] = {
'@id': metadataEntry['value'],
'@type': metadataEntry['datatype']
};
} else {
dataObject[entry] = {
'@value': metadataEntry['value'],
'@type': metadataEntry['datatype']
};
}
}
}
}
const dataObject = this.processDataObject(formData, applicationProfileId);
combinedDataObject.push(dataObject);
// rdfs:subClassOf definitions have to be included for the validation to work with inheritance
const subClasses = quads.filter((quad) => quad.predicate.value === 'http://www.w3.org/2000/01/rdf-schema#subClassOf');
const subClasses = quads.filter(
(quad) =>
quad.predicate.value ===
'http://www.w3.org/2000/01/rdf-schema#subClassOf'
);
for (const subClassQuad of subClasses) {
combinedDataObject.push({
'@id': subClassQuad.subject.value,
......@@ -50,37 +40,50 @@ export default Vue.extend({
});
}
// The non-referenced shapes have also to be represented
const nonReferenced = subClasses.filter((quad) =>
!subClasses.some((subQuad) => subQuad.subject.value === quad.object.value));
const nonReferenced = subClasses.filter(
(quad) =>
!subClasses.some(
(subQuad) => subQuad.subject.value === quad.object.value
)
);
for (const nonReference of nonReferenced) {
combinedDataObject.push({
'@id': nonReference.object.value,
});
}
const data = await this.loadDataset(JSON.stringify(combinedDataObject), 'application/ld+json', null);
const data = await this.loadDataset(
JSON.stringify(combinedDataObject),
'application/ld+json',
null
);
const validator = new SHACLValidator(quads);
const report = validator.validate(data);
this.$emit('isValid', report.conforms);
return report;
},
async getQuads (data: string, mimeType: string, baseUri: string | null) : Promise<Array<Quad>> {
async getQuads(
data: string,
mimeType: string,
baseUri: string | null
): Promise<Array<Quad>> {
const input = new Readable({
read: () => {
input.push(data);
input.push(null);
}
},
});
const quads = [] as Array<Quad>;
return new Promise((resolve) => {
rdfParser.parse(input, { contentType: mimeType, baseIRI: baseUri })
rdfParser
.parse(input, { contentType: mimeType, baseIRI: baseUri })
.on('data', (quad: Quad) => {
quads.push(quad);
})
.on('end', () => resolve(quads));
});
},
async loadDataset (data: string, mimeType: string, baseUri: string | null) {
async loadDataset(data: string, mimeType: string, baseUri: string | null) {
const quads = await this.getQuads(data, mimeType, baseUri);
const dataSet = factory.dataset();
for (const quad of quads) {
......@@ -92,41 +95,67 @@ export default Vue.extend({
this.shapes = [];
this.fillShapesListRecursive(quads, this.shapes, currentShape);
},
fillShapesListRecursive(quads: Array<Quad>, shapes: Array<string>, currentShape: string) {
fillShapesListRecursive(
quads: Array<Quad>,
shapes: Array<string>,
currentShape: string
) {
if (!shapes.includes(currentShape)) {
shapes.push(currentShape);
const subClasses = quads.filter((quad) =>
quad.predicate.value === 'http://www.w3.org/2000/01/rdf-schema#subClassOf'
&& quad.subject.value === currentShape)
const subClasses = quads
.filter(
(quad) =>
quad.predicate.value ===
'http://www.w3.org/2000/01/rdf-schema#subClassOf' &&
quad.subject.value === currentShape
)
.map((quad) => quad.object.value);
for (const subClass of subClasses) {
this.fillShapesListRecursive(quads, shapes, subClass);
}
}
},
async retrieveQuads(SHACLDefinition: string, mimeType: string, applicationProfileId: string) : Promise<Array<Quad>> {
async retrieveQuads(
SHACLDefinition: string,
mimeType: string,
applicationProfileId: string
): Promise<Array<Quad>> {
if (SHACLDefinition !== '') {
return this.getQuads(SHACLDefinition, mimeType, applicationProfileId);
}
return [];
},
retrieveProperties(SHACLDefinition: string, quads: Array<Quad>, applicationProfileId: string) : Array<Quad> {
retrieveProperties(
SHACLDefinition: string,
quads: Array<Quad>,
applicationProfileId: string
): Array<Quad> {
if (SHACLDefinition !== '') {
this.fillShapesList(quads, applicationProfileId);
const propertySets = quads.filter((quad) =>
quad.predicate.value === 'http://www.w3.org/ns/shacl#property'
&& this.shapes.includes(quad.subject.value)
).map((quad) => quad.object.value);
return quads.filter((quad) => propertySets.includes(quad.subject.value));
const propertySets = quads
.filter(
(quad) =>
quad.predicate.value === 'http://www.w3.org/ns/shacl#property' &&
this.shapes.includes(quad.subject.value)
)
.map((quad) => quad.object.value);
return quads.filter((quad) =>
propertySets.includes(quad.subject.value)
);
}
return [];
},
retrievePropertyOrder(properties: Array<Quad>) {
const propertySubjects = this.getPropertySubjects(properties);
let highestOrder = Math.max(...properties.filter((property) =>
property.predicate.value === 'http://www.w3.org/ns/shacl#order'
).map((property) => Number(property.object.value)));
let highestOrder = Math.max(
...properties
.filter(
(property) =>
property.predicate.value === 'http://www.w3.org/ns/shacl#order'
)
.map((property) => Number(property.object.value))
);
if (highestOrder === -Infinity) {
highestOrder = 0;
}
......@@ -135,17 +164,20 @@ export default Vue.extend({
let manualOrder = highestOrder + 1;
for (const propertySubject of propertySubjects) {
let currentOrder = 0;
const orderDefinitions = properties.filter((property) =>
property.subject.value === propertySubject
&& property.predicate.value === 'http://www.w3.org/ns/shacl#order');
const orderDefinitions = properties.filter(
(property) =>
property.subject.value === propertySubject &&
property.predicate.value === 'http://www.w3.org/ns/shacl#order'
);
if (orderDefinitions.length > 0) {
currentOrder = Number(orderDefinitions[0].object.value);
} else {
currentOrder = manualOrder;
manualOrder++;
}
unmappedSubjects[currentOrder] = properties.filter((property) =>
property.subject.value === propertySubject);
unmappedSubjects[currentOrder] = properties.filter(
(property) => property.subject.value === propertySubject
);
}
return unmappedSubjects;
......@@ -153,5 +185,42 @@ export default Vue.extend({
getPropertySubjects(properties: Array<Quad>) {
return [...new Set(properties.map((property) => property.subject.value))];
},
}
processDataObject(formData: any, applicationProfileId: string) {
const dataObject = {} as any;
dataObject['@type'] = applicationProfileId;
for (const nodeName of Object.keys(formData)) {
if (
formData[nodeName].some((element: any) => element['value'] !== '')
) {
dataObject[nodeName] = [];
for (const metadataEntry of formData[nodeName]) {
// skip empty and repeating values for ['@id'] or ['@value']
if (metadataEntry['value'] !== '') {