Commits (4)
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"
/>
```
{
"name": "@coscine/form-generator",
"version": "1.18.0",
"version": "2.0.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",
......@@ -76,5 +78,5 @@
"url": "https://git.rwth-aachen.de/coscine/frontend/libraries/form-generator.git"
},
"license": "MIT",
"packageManager": "yarn@3.2.0"
"packageManager": "yarn@3.2.1"
}
<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;
}
this.internalFixedValues[nodeName] = object;
if (different) {
this.$emit('inputFixedValues', this.internalFixedValues);
}
},
checkField(data: any, nodename: string, property = 'value') {
return FieldReader.isValueAssignedToKey(data, nodename, property);
checkPropertyExistsInOtherList(first?: ValueType[], second?: ValueType[]) {
return (
!first ||
first.every(
(entry) =>
second && second.some((newEntry) => entry.value === newEntry.value)
)
);
},
fieldDefinition(formElement: any) {
if (this.checkField(formElement, 'http://www.w3.org/ns/shacl#datatype')) {
const datatype = FieldReader.getObject(
formElement,
'http://www.w3.org/ns/shacl#datatype'
async parseFormData() {
if (typeof this.formData === 'string') {
this.cleanedMetadata = await this.retrieveDataset(
this.formData,
this.formDataMimeType,
this.selectedShape
);
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'
) {
return 'InputTextArea';
} else {
return 'InputTextField';
}
} else if (
datatype['value'] === 'http://www.w3.org/2001/XMLSchema#date'
) {
return 'InputDatePicker';
} else if (
datatype['value'] === 'http://www.w3.org/2001/XMLSchema#boolean'
) {
return 'InputBooleanCombobox';
}
return 'InputTextField';
} else {
if (this.checkField(formElement, 'http://www.w3.org/ns/shacl#class')) {
return 'InputCombobox';
}
if (this.checkField(formElement, 'http://www.w3.org/ns/shacl#in')) {
return 'InputTextField';
}
this.cleanedMetadata = factory.dataset(
Array.from(this.formData)
) as unknown as Dataset;
}
return null;
},
},
});
......
......@@ -4,245 +4,148 @@ import factory from 'rdf-ext';
import SHACLValidator from 'rdf-validate-shacl';
import rdfParser from 'rdf-parse';
import { Readable } from 'stream';
import { DataFactory, DatasetCore, DatasetCoreFactory, Quad } from 'rdf-js';
import ValidationReport from 'rdf-validate-shacl/src/validation-report';
import { prefixes } from '@zazuko/rdf-vocabularies';
import type { Dataset, Quad, Quad_Object, Quad_Subject } from 'rdf-js';
export default Vue.extend({
data() {
return {
shapes: [] as string[],
errorMessages: {} as { [nodeName: string]: Quad_Object[] },
timeOutId: 0,
validationDebounce: 500,
};
},
methods: {
async validateMetadata(
formData: any,
quads: Array<Quad>,
applicationProfileId: string
): Promise<
ValidationReport<
DataFactory<Quad, Quad> &
DatasetCoreFactory<Quad, Quad, DatasetCore<Quad, Quad>>
>
> {
// RDF/JSON => JSON-LD since the loadDataset function doesn't support RDF/JSON
const combinedDataObject = [];
let targetClass: string | undefined = undefined;
const targetClassDefs = quads.filter(
(quad) =>
quad.subject.value === applicationProfileId &&
quad.predicate.value === 'http://www.w3.org/ns/shacl#targetClass'
);
if (targetClassDefs.length > 0) {
targetClass = targetClassDefs[0].object.value;
getMetadataSubject(metadata: Dataset): Quad_Subject {
let subjectNode: Quad_Subject = factory.blankNode();
const rdfType = factory.namedNode(prefixes.rdf + 'type');
if (metadata.size) {
for (const entry of metadata.match(undefined, rdfType)) {
subjectNode = entry.subject;
break;
}
}
const dataObject = this.processDataObject(
formData,
targetClass ? targetClass : 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'
);
for (const subClassQuad of subClasses) {
combinedDataObject.push({
'@id': subClassQuad.subject.value,
'http://www.w3.org/2000/01/rdf-schema#subClassOf': [
{
'@id': subClassQuad.object.value,
},
],
});
return subjectNode;
},
triggerMetadataValidation(metadata: Dataset, dataset: Dataset): void {
if (this.timeOutId) {
clearTimeout(this.timeOutId);
}
// The non-referenced shapes have also to be represented
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,
this.timeOutId = setTimeout(async () => {
const combinedMetadata = factory.dataset(Array.from(metadata));
// rdfs:subClassOf definitions have to be included for the validation to work with inheritance
const subClasses = dataset.match(
null,
factory.namedNode(prefixes.rdfs + 'subClassOf')
);
for (const subClassQuad of subClasses) {
combinedMetadata.add(subClassQuad);
}
// Describe that the selected value for a class relationship is a value of that class to satisfy validation
const pathRelations = dataset.match(
null,
factory.namedNode(prefixes.sh + 'path')
);
const classRelations = Array.from(pathRelations).map((pathRelation) => {
return {
path: pathRelation.object,
classRelations: dataset.match(
pathRelation.subject,
factory.namedNode(prefixes.sh + 'class')
),
};
});
}
for (const classRelation of classRelations.filter(
(entry) => entry.classRelations && entry.classRelations.size
)) {
const metadataRelations = metadata.match(
undefined,
classRelation.path
);
if (metadataRelations.size) {
for (const metadataRelation of metadataRelations) {
for (const classEntry of classRelation.classRelations) {
combinedMetadata.add(
factory.quad(
metadataRelation.object as Quad_Subject,
factory.namedNode(prefixes.rdf + 'type'),
classEntry.object
)
);
}
}
}
}
const data = await this.loadDataset(
JSON.stringify(combinedDataObject),
'application/ld+json',
undefined
);
const validator = new SHACLValidator(quads as any);
const report = validator.validate(data);
this.$emit('isValid', report);
return report;
const validator = new SHACLValidator(dataset);
const report = validator.validate(combinedMetadata);
this.$emit('isValid', report);
this.errorMessages = {};
for (const result of report.results) {
if (result.path) {
const path = result.path;
if (
metadata.some(
(entry) =>
entry.predicate.value === path.value &&
entry.object.value !== ''
)
) {
this.errorMessages[path.value] = result.message;
}
}
}
return report;
}, this.validationDebounce) as unknown as number;
},
async getQuads(
data: string,
mimeType: string,
baseUri: string | undefined
): Promise<Array<Quad>> {
): Promise<Dataset> {
const input = new Readable({
read: () => {
input.push(data);
input.push(null);
},
});
const quads = [] as Array<Quad>;
const dataset = factory.dataset();
return new Promise((resolve) => {
rdfParser
.parse(input, { contentType: mimeType, baseIRI: baseUri })
.on('data', (quad: Quad) => {
quads.push(quad);
dataset.add(quad);
})
.on('end', () => resolve(quads));
.on('end', () => resolve(dataset as unknown as Dataset));
});
},
async loadDataset(
data: string,
mimeType: string,
baseUri: string | undefined
) {
): Promise<Dataset> {
const quads = await this.getQuads(data, mimeType, baseUri);
const dataSet = factory.dataset();
for (const quad of quads) {
dataSet.add(quad);
}
return dataSet;
},
fillShapesList(quads: Array<Quad>, currentShape: string) {
this.shapes = [];
this.fillShapesListRecursive(quads, this.shapes, currentShape);
return dataSet as unknown as Dataset;
},
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
)
.map((quad) => quad.object.value);
for (const subClass of subClasses) {
this.fillShapesListRecursive(quads, shapes, subClass);
}
}
},
async retrieveQuads(
SHACLDefinition: string,
async retrieveDataset(
shapes: 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> {
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)
);
}
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))
);
if (highestOrder === -Infinity) {
highestOrder = 0;
}
const unmappedSubjects = {} as any;
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'
selectedShape: string
): Promise<Dataset> {
if (shapes !== '') {
return await this.getQuads(
shapes,
mimeType,
selectedShape ? selectedShape : 'https://purl.org/coscine/ap/'
);
if (orderDefinitions.length > 0) {
currentOrder = Number(orderDefinitions[0].object.value);
} else {
currentOrder = manualOrder;
manualOrder++;
}
unmappedSubjects[currentOrder] = properties.filter(
(property) => property.subject.value === propertySubject
);
}
return unmappedSubjects;
},
getPropertySubjects(properties: Array<Quad>) {
return [...new Set(properties.map((property) => property.subject.value))];
},
processDataObject(formData: any, typeIdentifier: string) {
const dataObject = {} as any;
dataObject['@type'] = typeIdentifier;
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'] !== '') {
if (
metadataEntry['type'] === 'uri' &&
!dataObject[nodeName].some(
(element: any) => element['@id'] === metadataEntry['value']
)
) {
dataObject[nodeName].push({
'@id': metadataEntry['value'],
'@type': metadataEntry['datatype'],
});
} else if (
!dataObject[nodeName].some(
(element: any) => element['@value'] === metadataEntry['value']
)
) {
dataObject[nodeName].push({
'@value': metadataEntry['value'],
'@type': metadataEntry['datatype'],
});
}
}
}
}
}
return dataObject;
return factory.dataset() as unknown as Dataset;
},
},
});
<template>
<multiselect
:id="cssId"
:options="selectableOptions"
<MultiSelect
v-model="selectedOption"
:options="selectableOptions"
label="name"
track-by="name"
:placeholder="$t('selectPlaceholder')"
:required="required"
:disabled="disabledMode || locked"
@input="input"
select-label=""
selected-label=""
deselect-label=""
@input="input"
/>
</template>
<script lang="ts">
import Vue from 'vue';
import Vue, { PropType } from 'vue';
import VueI18n from 'vue-i18n';
import locale from '@/locale';
import FieldReader from '@/util/FieldReader';
import Multiselect from 'vue-multiselect';
import 'vue-multiselect/dist/vue-multiselect.min.css';
Vue.component('multiselect', Multiselect);
import type { NamedNode, Quad_Object } from 'rdf-js';
import factory from 'rdf-ext';
import { prefixes } from '@zazuko/rdf-vocabularies';
import type { Label } from '@coscine/api-client/dist/types/Coscine.Api.Metadata';
Vue.component('MultiSelect', Multiselect);
Vue.use(VueI18n);
......@@ -37,84 +39,89 @@ const i18n = new VueI18n({
export default Vue.extend({
i18n,
name: 'InputBooleanCombobox',
beforeMount() {
i18n.locale = this.languageCode;
// set the datatype
this.$set(this.formData[this.nodeName][this.entryKey], 'type', 'literal');
this.$set(
this.formData[this.nodeName][this.entryKey],
'datatype',
'http://www.w3.org/2001/XMLSchema#boolean'
);
if (this.fixedValueMode) {
this.updateFixedValues(this.entryKey);
}
props: {
locked: {
default: false,
type: Boolean,
},
required: {
default: false,
type: Boolean,
},
languageCode: {
default: 'en',
type: String,
},
entry: {
required: true,
type: Object as PropType<Quad_Object>,
},
dataType: {
default: () => factory.namedNode(prefixes.xsd + 'boolean'),
type: Object as PropType<NamedNode<string>>,
},
disabledMode: {
default: false,
type: Boolean,
},
},
mounted() {
if (this.languageCode === 'en') {
this.selectableOptions = [
data() {
return {
object: factory.literal('', this.dataType),
};
},
computed: {
selectableOptions(): Label[] {
if (this.languageCode === 'de') {
return [
{ value: 'true', name: 'Ja' },
{ value: 'false', name: 'Nein' },
];
}
return [
{ value: 'true', name: 'Yes' },
{ value: 'false', name: 'No' },
];
} else {
this.selectableOptions = [
{ value: 'true', name: 'Ja' },
{ value: 'false', name: 'Nein' },
];
}
this.loadData();
if (this.fixedValueMode) {
this.updateFixedValues(this.entryKey);
}
},
selectedOption(): Label | undefined {
if (this.object.value !== '') {
const foundOption = this.selectableOptions.find(
(option) => option.value === this.object.value
);
return {
name: foundOption?.name,
value: this.object.value,
};
}
return undefined;
},
},
data() {
return {
selectableOptions: [] as Record<string, unknown>[],
selectedOption: {} as Record<string, unknown>,
};
watch: {
entry() {
this.loadData();
},
},
beforeMount() {
i18n.locale = this.languageCode;
},
mounted() {
this.loadData();
},
methods: {
input() {
FieldReader.setField(
this.formData,
this.v,
this.nodeName,
this.selectedOption !== null ? (this.selectedOption as any).value : '',
this.$set,
this.entryKey
);
this.updateFixedValues(this.entryKey);
},
loadData() {
// unnecessary logic for :multiselect="true"
const selectedData = [this.formData[this.nodeName][this.entryKey]];
for (const field of selectedData) {
const selectedElement = this.selectableOptions.filter((obj: any) => {
return obj.value === (field as any).value;
});
if (this.object.value !== this.entry.value) {
this.object.value = this.entry.value;
}
this.$emit('triggerValidation');
},
input(label: Label) {
if (label.value) {
this.object.value = label.value;
} else {
this.object.value = '';
}
this.$emit('input', this.object);
},
},
props: {
cssId: String,
invisible: Boolean,
locked: Boolean,
required: Boolean,
nodeName: String,
formFieldInformation: Array,
formData: Object,
fixedValueMode: Boolean,
disabledMode: Boolean,
checkField: Function,
updateFixedValues: Function,
languageCode: String,
v: Object,
entryKey: Number,
},
components: {},
});
</script>
......
<template>
<multiselect
:id="cssId"
<MultiSelect
:value="selectedOption"
:options="selectableOptions"
v-model="selectedOptions"
label="name"
track-by="name"
:placeholder="$t('selectPlaceholder')"
:required="required"
:disabled="disabledMode || locked"
@input="input"
select-label=""
selected-label=""
deselect-label=""
@input="input"
/>
</template>
......@@ -23,14 +22,14 @@ import locale from '@/locale';
import Multiselect from 'vue-multiselect';
import 'vue-multiselect/dist/vue-multiselect.min.css';
Vue.component('multiselect', Multiselect);
import FieldReader from '@/util/FieldReader';
Vue.component('MultiSelect', Multiselect);
import type {
BilingualLabels,
Label,
} from '@coscine/api-client/dist/types/Coscine.Api.Metadata';
import type { NamedNode, Quad_Object } from 'rdf-js';
import factory from 'rdf-ext';
Vue.use(VueI18n);
......@@ -43,82 +42,88 @@ const i18n = new VueI18n({
export default Vue.extend({
i18n,
name: 'InputCombobox',
beforeMount() {
i18n.locale = this.languageCode;
this.class = FieldReader.getObject(
this.formFieldInformation,
'http://www.w3.org/ns/shacl#class'
).value;
// set the datatype
this.$set(this.formData[this.nodeName][this.entryKey], 'type', 'uri');
this.$set(
this.formData[this.nodeName][this.entryKey],
'datatype',
this.class
);
if (this.fixedValueMode) {
this.updateFixedValues(this.entryKey);
}
},
mounted() {
this.retrieveLabels();
if (this.fixedValueMode) {
this.updateFixedValues(this.entryKey);
}
props: {
locked: {
default: false,
type: Boolean,
},
required: {
default: false,
type: Boolean,
},
entry: {
required: true,
type: Object as PropType<Quad_Object>,
},
disabledMode: {
default: false,
type: Boolean,
},
languageCode: {
default: 'en',
type: String,
},
classObject: {
required: true,
type: Object as PropType<NamedNode<string>>,
},
classReceiver: {
default: () => {
return async () => ({} as BilingualLabels);
},
type: Function as PropType<
(classUrl: string) => Promise<BilingualLabels>
>,
},
},
data() {
return {
class: '',
selectableOptions: [] as Label[],
selectedOptions: [] as Label[],
maxNumberOfSelectedObjects: 10000,
object: factory.namedNode<string>('') as NamedNode<string>,
};
},
computed: {
selectedOption(): Label | undefined {
if (this.object.value !== '') {
const foundOption = this.selectableOptions.find(
(option) => option.value === this.object.value
);
return {
name: foundOption?.name,
value: this.object.value,
};
}
return undefined;
},
},
watch: {
entry() {
this.loadData();
},
},
mounted() {
i18n.locale = this.languageCode;
this.retrieveLabels();
},
methods: {
input() {
// Unnecessary logic for :multiselect="true". Not removed for future reference.
if (Array.isArray(this.selectedOptions)) {
const array = [];
for (const field of this.selectedOptions) {
array.push({
value: (field as any).value,
type: 'uri',
datatype: this.class,
});
}
this.$set(this.formData[this.nodeName], this.entryKey, array);
input(label: Label) {
if (label.value) {
this.object.value = label.value;
} else {
// Will always enter here, since :multiselect="false" by default
this.$set(this.formData[this.nodeName], this.entryKey, {
value: (this.selectedOptions as any).value,
type: 'uri',
datatype: this.class,
});
}
if (this.v.formData[this.nodeName]) {
this.v.formData[this.nodeName].$touch();
this.object.value = '';
}
this.updateFixedValues(this.entryKey);
this.$emit('input', this.object);
},
loadData() {
// Logic for :multiselect="true".
const selectedData = [this.formData[this.nodeName][this.entryKey]];
for (const field of selectedData) {
const selectedElement = this.selectableOptions.filter((obj: any) => {
return obj.value === (field as any).value;
});
if (selectedElement[0] !== undefined) {
this.selectedOptions.push(selectedElement[0]);
}
if (this.object.value !== this.entry.value) {
this.object.value = this.entry.value;
}
this.$emit('triggerValidation');
},
async retrieveLabels() {
try {
const bilingualLabels = await this.classReceiver(this.class);
const bilingualLabels = await this.classReceiver(
this.classObject.value
);
if (this.languageCode === 'en' && bilingualLabels.en) {
this.selectableOptions = bilingualLabels.en;
} else if (this.languageCode === 'de' && bilingualLabels.de) {
......@@ -131,33 +136,12 @@ export default Vue.extend({
this.selectableOptions = [];
}
this.loadData();
} catch {
} catch (e) {
// Method not available
console.error(e);
}
},
},
props: {
cssId: String,
invisible: Boolean,
locked: Boolean,
required: Boolean,
nodeName: String,