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

Merge branch 'Issue/1938-internalHandling' into 'dev'

Breaking: Refactoring of the metadata interface

See merge request !84
parents 6b2520f2 6d08dda4
module.exports = {
root: true,
env: {
node: true,
es2021: true,
},
ignorePatterns: ["node_modules", "build", "coverage"],
plugins: ["eslint-comments", "functional"],
extends: [
"plugin:vue/essential",
"plugin:vue/recommended",
"eslint:recommended",
"@vue/typescript/recommended",
"@vue/prettier",
......@@ -14,17 +14,14 @@ module.exports = {
],
parserOptions: {
ecmaVersion: 2020,
parser: "@typescript-eslint/parser",
},
rules: {
"no-console": process.env.NODE_ENV === "production" ? "warn" : "off",
"no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off",
"@typescript-eslint/explicit-module-boundary-types": "off",
"eslint-comments/disable-enable-pair": [
"error",
{ "allowWholeFile": true }
],
"@typescript-eslint/no-empty-interface": 1, // empty Interfaces will be only warnings for now.
"vue/multi-word-component-names": "off"
"no-console": process.env.NODE_ENV === "production" ? "warn" : "off",
"no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off",
},
}
};
......@@ -8,9 +8,6 @@ packageExtensions:
"vue-material-design-icons@*":
dependencies:
"vue": "^2.6.14"
"vuelidate@*":
dependencies:
"vue": "^2.6.14"
"bootstrap-vue@*":
dependencies:
"vue": "^2.6.14"
......
# FormGenerator
# SHACL Form Generator
This repository contains the CoScInE form generation vue component.
This repository contains the [SHACL](https://www.w3.org/TR/shacl/) form-generator [Vue](https://vuejs.org/) component used in Coscine.
### Customize configuration
See [Configuration Reference](https://cli.vuejs.org/config/).
### Usage
```html
<FormGenerator
:disabled-mode="formDisabled"
:fixed-value-mode="fixedValueMode"
:fixed-values="fixedValues"
:form-data="metadata"
:form-data-mime-type="metadataMimeType"
:locale="locale"
:selected-shape="applicationProfileUrl"
:shape-mime-type="shapeMimeType"
:shapes="applicationProfile"
:class-receiver="implementedReceiveClass"
:user-receiver="implementedReceiveUser"
@input="inputMetadata"
@inputFixedValues="inputFixedValues"
/>
```
......@@ -3,8 +3,9 @@
"version": "1.18.0",
"main": "dist/index.umd.js",
"module": "dist/index.es.js",
"browser": "dist/index.umd.js",
"browser": "dist/index.es.js",
"style": "dist/style.css",
"types": "dist/index.d.ts",
"directories": {
"doc": "docs"
},
......@@ -22,9 +23,10 @@
},
"dependencies": {
"@coscine/api-client": "^1.5.0",
"@zazuko/rdf-vocabularies": "^2021.9.22-3",
"bootstrap-vue": "^2.20.1",
"rdf-ext": "^1.3.4",
"rdf-parse": "1.8.0",
"rdf-ext": "^2.0.1",
"rdf-parse": "^1.8.0",
"rdf-validate-shacl": "^0.2.5",
"stream-browserify": "^3.0.0",
"uuid": "^8.3.2",
......@@ -32,21 +34,21 @@
"vue-i18n": "^8.22.2",
"vue-material-design-icons": "^4.11.0",
"vue-multiselect": "^2.1.6",
"vue-runtime-helpers": "^1.1.2",
"vuelidate": "^0.7.6"
"vue-runtime-helpers": "^1.1.2"
},
"devDependencies": {
"@hutson/semantic-delivery-gitlab": "^9.1.0",
"@rollup/plugin-replace": "^4.0.0",
"@semantic-release/commit-analyzer": "^8.0.1",
"@semantic-release/git": "^9.0.0",
"@semantic-release/gitlab": "^6.0.5",
"@semantic-release/npm": "^7.0.6",
"@semantic-release/release-notes-generator": "^9.0.1",
"@types/node": "^14.14.20",
"@types/rdf-ext": "^1.3.11",
"@types/rdf-js": "^4.0.0",
"@types/rdf-validate-shacl": "^0.2.4",
"@types/uuid": "^8.3.1",
"@types/vuelidate": "^0.7.13",
"@typescript-eslint/eslint-plugin": "^5.15.0",
"@typescript-eslint/parser": "^5.15.0",
"@vue/cli-plugin-eslint": "^4.5.15",
......
<template>
<div id="FormGenerator">
<div v-for="(formElement, index) in sortedSHACLDefinition" :key="index">
<div v-for="(pathToProperty, index) in pathList" :key="index">
<WrapperInput
:componentDefinition="fieldDefinition(formElement)"
:formFieldInformation="formElement"
:formData="formData"
:fixedValueMode="fixedValueMode"
:fixedValues="fixedValues"
:disabledMode="disabledMode"
:languageLocale="languageLocale"
:v="$v"
:errorMessages="errorMessages"
:classReceiver="classReceiver"
:userReceiver="userReceiver"
:dataset="dataset"
:property="pathToProperty.subject"
:metadata="metadata"
:fixed-value-mode="fixedValueMode"
:fixed-values="internalFixedValues"
:disabled-mode="disabledMode"
:language-locale="locale"
:error-messages="errorMessages"
:class-receiver="classReceiver"
:user-receiver="userReceiver"
@input="input"
@triggerValidation="
validateMetadata(formData, quads, applicationProfileId)
"
@inputFixedValues="inputFixedValues"
@triggerValidation="triggerMetadataValidation(cleanedMetadata, dataset)"
/>
</div>
</div>
......@@ -28,20 +26,26 @@ import Vue, { PropType } from 'vue';
import locale from '@/locale';
import VueI18n from 'vue-i18n';
import Vuelidate from 'vuelidate';
import { MetadataApi, UserApi } from '@coscine/api-client';
import { Quad } from 'rdf-js';
import FieldReader from './util/FieldReader';
import WrapperInput from './components/WrapperInput.vue';
import LinkedDataHandler from './base/LinkedDataHandler';
import type { Dataset, Quad, Quad_Object } from 'rdf-js';
import factory from 'rdf-ext';
import WrapperInput from '@/components/WrapperInput.vue';
import LinkedDataHandler from '@/base/LinkedDataHandler';
import { prefixes } from '@zazuko/rdf-vocabularies';
import type { UserObject } from '@coscine/api-client/dist/types/Coscine.Api.User';
import type { BilingualLabels } from '@coscine/api-client/dist/types/Coscine.Api.Metadata';
import type {
FixedValueObject,
FixedValues,
ValueType,
} from '@/types/fixedValues';
import DatasetExt from 'rdf-ext/lib/Dataset';
Vue.use(VueI18n);
Vue.use(Vuelidate);
const i18n = new VueI18n({
locale: 'en',
......@@ -56,40 +60,44 @@ export default LinkedDataHandler.extend({
WrapperInput,
},
props: {
applicationProfileId: {
default: '',
type: String,
formData: {
default: () => factory.dataset() as unknown as Dataset,
type: [String, Object] as PropType<string | Dataset>,
},
mimeType: {
formDataMimeType: {
default: 'application/ld+json',
type: String,
},
formData: {
default: () => ({}),
type: Object,
},
fixedValueMode: {
disabledMode: {
default: false,
type: Boolean,
},
disabledMode: {
fixedValueMode: {
default: false,
type: Boolean,
},
fixedValues: {
default: () => ({}),
type: Object,
type: Object as PropType<FixedValues>,
},
SHACLDefinition: {
selectedShape: {
default: 'https://purl.org/coscine/ap/',
type: String,
},
shapes: {
default: '',
type: [String, Object, null] as PropType<string | Dataset | null>,
},
shapesMimeType: {
default: 'application/ld+json',
type: String,
},
languageLocale: {
locale: {
default: 'en',
type: String,
},
classReceiver: {
default: async (classUrl: string) => {
default: () => async (classUrl: string) => {
const response = await MetadataApi.metadataGetClassInstances(classUrl);
return response.data;
},
......@@ -98,7 +106,7 @@ export default LinkedDataHandler.extend({
>,
},
userReceiver: {
default: async () => {
default: () => async () => {
const response = await UserApi.userGetUser();
return response.data;
},
......@@ -107,198 +115,245 @@ export default LinkedDataHandler.extend({
},
data() {
return {
properties: [] as Array<Quad>,
quads: [] as Array<Quad>,
sortedSHACLDefinition: [] as any[],
errorMessages: {} as any,
timeouts: {} as any,
timeoutInterval: 500,
validations: {} as any,
cleanedMetadata: factory.dataset() as unknown as Dataset,
dataset: factory.dataset() as unknown as Dataset,
internalFixedValues: {} as FixedValues,
metadata: factory.dataset() as unknown as Dataset,
};
},
validations(): any {
if (!this.fixedValueMode) {
return {
formData: this.validations,
};
}
return {
formData: {},
};
computed: {
orders(): Record<string, number> {
return Array.from(
this.dataset.match(null, factory.namedNode(prefixes.sh + 'order'))
).reduce((result, item) => {
result[item.subject.value] = parseInt(item.object.value);
if (isNaN(result[item.subject.value]))
result[item.subject.value] = Number.MAX_VALUE;
return result;
}, {} as Record<string, number>);
},
pathList(): Quad[] {
return Array.from(
this.dataset.match(null, factory.namedNode(prefixes.sh + 'path'))
)
.filter((entry) => entry.object.value !== prefixes.rdf + 'type')
.sort((entry1, entry2) => this.compareEntries(entry1, entry2));
},
targetClass(): Quad_Object {
for (const targetClass of this.dataset.match(
null,
factory.namedNode(prefixes.sh + 'targetClass')
)) {
return targetClass.object;
}
return factory.namedNode(
this.selectedShape ? this.selectedShape : 'https://purl.org/coscine/ap/'
);
},
},
beforeMount() {
i18n.locale = this.languageLocale;
this.handleApplicationProfiles();
async beforeMount() {
i18n.locale = this.locale;
await this.parseFormData();
this.metadata = this.cleanedMetadata;
this.internalFixedValues = JSON.parse(JSON.stringify(this.fixedValues));
await this.handleApplicationProfiles();
this.initMetadata();
},
watch: {
formData() {
this.input();
this.createValidations();
this.$v.$reset();
this.validateMetadata(
this.formData,
this.quads,
this.applicationProfileId
);
fixedValues() {
this.internalFixedValues = JSON.parse(JSON.stringify(this.fixedValues));
},
languageLocale() {
i18n.locale = this.languageLocale;
async formData() {
await this.parseFormData();
// Don't replace metadata if cleanedMetadata only contains the same values without the "" ones
if (
!this.cleanedMetadata.every((entry) =>
this.metadata.some(
(newEntry) =>
newEntry.predicate.value === entry.predicate.value &&
newEntry.object.value === entry.object.value
)
) ||
this.metadata.some(
(entry) =>
entry.object.value !== '' &&
!this.cleanedMetadata.some(
(newEntry) =>
newEntry.predicate.value === entry.predicate.value &&
newEntry.object.value === entry.object.value
)
)
) {
this.metadata = this.cleanedMetadata;
}
this.initMetadata();
this.triggerMetadataValidation(this.cleanedMetadata, this.dataset);
},
locale() {
i18n.locale = this.locale;
},
SHACLDefinition() {
shapes() {
this.handleApplicationProfiles();
this.initMetadata();
},
targetClass() {
this.initMetadata();
},
},
methods: {
createValidations() {
const validator = {} as any;
for (const nodename of Object.keys(this.formData)) {
validator[nodename] = {
$each: {
value: {
shaclValidated: async () => {
this.input();
// A debounce has been implemented based on the nodename
if (this.timeouts[nodename])
clearTimeout(this.timeouts[nodename]);
return await new Promise((resolve) => {
this.timeouts[nodename] = setTimeout(async () => {
// Validate the whole data and check if for the current nodename an error is logged
const report = await this.validateMetadata(
this.formData,
this.quads,
this.applicationProfileId
);
let resolveValue = true;
const result = report.results.find(
(entry) =>
entry.path !== null && entry.path.value === nodename
);
if (result !== undefined) {
this.errorMessages[nodename] = result.message;
resolveValue = false;
}
resolve(resolveValue);
}, this.timeoutInterval);
});
},
},
},
};
compareEntries(entry1: Quad, entry2: Quad) {
let entry1Order: number = Number.MAX_VALUE;
let entry2Order: number = Number.MAX_VALUE;
if (entry1.subject.value in this.orders) {
entry1Order = this.orders[entry1.subject.value];
}
if (entry2.subject.value in this.orders) {
entry2Order = this.orders[entry2.subject.value];
}
this.validations = validator;
if (entry1Order === entry2Order) {
return 0;
}
return entry1Order > entry2Order ? 1 : -1;
},
initMetadata() {
this.input(prefixes.rdf + 'type', [this.targetClass]);
},
async handleApplicationProfiles() {
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();
if (typeof this.shapes === 'string') {
this.dataset = await this.retrieveDataset(
this.shapes,
this.shapesMimeType,
this.selectedShape
);
} else if (this.shapes !== null) {
this.dataset = factory.dataset(
Array.from(this.shapes)
) as unknown as Dataset;
}
await this.validateMetadata(
this.formData,
this.quads,
this.applicationProfileId
);
},
input() {
this.$emit('input', this.formData);
this.triggerMetadataValidation(this.cleanedMetadata, this.dataset);
},
setPropertySettings(properties: Array<Quad>) {
const propertySubjects = this.getPropertySubjects(properties);
this.errorMessages = {} as any;
this.timeouts = {} as any;
for (const propertySubject of propertySubjects) {
this.errorMessages[propertySubject] = '';
this.timeouts[propertySubject] = null;
input(nodeName: string, values: Quad_Object[]) {
const subjectNode = this.getMetadataSubject(this.metadata);
const node = factory.namedNode(nodeName);
const oldValues = Array.from(this.metadata.match(undefined, node));
if (
oldValues.length !== values.length ||
!oldValues.every((entry) =>
values.some((newEntry) => newEntry.value === entry.object.value)
) ||
!values.every((entry) =>
oldValues.some((newEntry) => newEntry.object.value === entry.value)
)
) {
this.metadata.deleteMatches(undefined, node);
this.metadata.addAll(
values.map((value) => factory.quad(subjectNode, node, value))
);
// Reactivity Trigger
this.metadata = (
this.metadata as unknown as DatasetExt
).clone() as unknown as Dataset;
const newCleanedMetadata = this.metadata.filter(
(entry) => entry.object.value !== ''
);
if (
!newCleanedMetadata.every((entry) =>
this.cleanedMetadata.some(
(oldEntry) =>
entry.predicate.value === oldEntry.predicate.value &&
entry.object.value === oldEntry.object.value
)
) ||
!this.cleanedMetadata.every((entry) =>
newCleanedMetadata.some(
(oldEntry) =>
entry.predicate.value === oldEntry.predicate.value &&
entry.object.value === oldEntry.object.value
)
)
) {
this.cleanedMetadata = newCleanedMetadata;
this.$emit('input', this.cleanedMetadata);
this.triggerMetadataValidation(this.cleanedMetadata, this.dataset);
}
}
},
setSHACLDefinition(unmappedSubjects: any) {
this.sortedSHACLDefinition = [];
let keys = Object.keys(unmappedSubjects).sort(
(a, b) => parseInt(a, 10) - parseInt(b, 10)
);
for (let i = 0; i < keys.length; i++) {
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.isDataValueAssigned(this.formData, nodeName)) {
this.$set(this.formData, nodeName, []);
for (let index = 0; index < minCount; index++) {
this.formData[nodeName].push({});
this.$set(this.formData[nodeName][index], 'value', '');
}
} else {
for (let index = 0; index < minCount; index++) {
if (
!FieldReader.isDataValueAssignedToKey(
this.formData,
nodeName,
'value',
index
inputFixedValues(nodeName: string, object: FixedValueObject) {
let different = false;
if (this.internalFixedValues[nodeName]) {
const previousObject = this.internalFixedValues[nodeName];
if (
previousObject['https://purl.org/coscine/defaultValue']?.length ===
object['https://purl.org/coscine/defaultValue']?.length &&
previousObject['https://purl.org/coscine/invisible']?.length ===
object['https://purl.org/coscine/invisible']?.length &&
previousObject['https://purl.org/coscine/fixedValue']?.length ===
object['https://purl.org/coscine/fixedValue']?.length
) {
// If some property is different between the old and new object
if (
!(
this.checkPropertyExistsInOtherList(
previousObject['https://purl.org/coscine/defaultValue'],
object['https://purl.org/coscine/defaultValue']
) &&
this.checkPropertyExistsInOtherList(
object['https://purl.org/coscine/defaultValue'],
previousObject['https://purl.org/coscine/defaultValue']
) &&
this.checkPropertyExistsInOtherList(
previousObject['https://purl.org/coscine/invisible'],
object['https://purl.org/coscine/invisible']
) &&
this.checkPropertyExistsInOtherList(
object['https://purl.org/coscine/invisible'],
previousObject['https://purl.org/coscine/invisible']
) &&
this.checkPropertyExistsInOtherList(
previousObject['https://purl.org/coscine/fixedValue'],
object['https://purl.org/coscine/fixedValue']
) &&
this.checkPropertyExistsInOtherList(
object['https://purl.org/coscine/fixedValue'],
previousObject['https://purl.org/coscine/fixedValue']
)
) {
this.formData[nodeName][index] = { value: '' };
}
)
) {
different = true;
}
} else {
different = true;
}
} else {
different = true;
}