Commit 3935a497 authored by Marcel Nellesen's avatar Marcel Nellesen
Browse files

Merge branch 'Sprint/2020-19' into 'master'

Sprint/2020 19

See merge request coscine/vue/form-generator!40
parents 96f2b18c f4ad4e58
......@@ -14,6 +14,7 @@ test:
- npm test
except:
refs:
- master
- tags
variables:
- $GITLAB_USER_ID == $GIT_BOT_USER_ID
......@@ -22,6 +23,7 @@ publish:
stage: publish
script:
- npm run build
- npm test
- npx semantic-release
only:
- master
......
This diff is collapsed.
......@@ -3,14 +3,17 @@
<div v-for="(formElement, index) in sortedSHACLDefinition" :key="index">
<WrapperInput
:componentDefinition="fieldDefinition(formElement)"
:idForFixedValues="fixedValueIds[index]"
:formFieldInformation="formElement"
:formData="SHACLResponse"
:formData="formData"
:resourceId="resourceId"
:projectId="projectId"
:fixedValueMode="fixedValueMode"
:fixedValues="fixedValues"
:disabledMode="disabledMode"
:languageLocale="languageLocale"
:v="$v"
:errorMessages="errorMessages"
/>
</div>
</div>
......@@ -30,6 +33,11 @@ import { LanguageUtil } from '@coscine/app-util';
import FieldReader from './util/FieldReader';
import factory from 'rdf-ext';
import SHACLValidator from 'rdf-validate-shacl';
import rdfParser from "rdf-parse";
import { Readable } from 'stream';
import WrapperInput from './components/WrapperInput.vue';
Vue.use(BootstrapVue);
......@@ -37,10 +45,13 @@ Vue.use(VueI18n);
const i18n = new VueI18n({
locale: 'en',
messages: coscine.i18n['form-generator'],
messages: (window.coscine && coscine.i18n) ? coscine.i18n['form-generator'] : {},
silentFallbackWarn: true,
});
import Vuelidate from 'vuelidate';
Vue.use(Vuelidate);
export default Vue.extend({
i18n,
name: 'FormGenerator',
......@@ -52,8 +63,12 @@ export default Vue.extend({
default: '',
type: String,
},
SHACLResponse: {
default: {},
mimeType: {
default: 'application/ld+json',
type: String,
},
formData: {
default: () => ({}),
type: Object,
},
projectId: {
......@@ -73,41 +88,130 @@ export default Vue.extend({
type: Boolean,
},
fixedValues: {
default: {},
default: () => ({}),
type: Object,
},
SHACLDefinition: {
default: '',
type: String,
},
languageLocale: {
default: () => LanguageUtil.getLanguage(),
type: String,
},
},
data() {
return {
sortedSHACLDefinition: [] as object[],
SHACLDefinition: Object,
languageLocale: LanguageUtil.getLanguage(),
quads: [] as any[],
fixedValueIds: [] as any[],
sortedSHACLDefinition: [] as any[],
errorMessages: {} as any,
timeouts: {} as any,
timeoutInterval: 500,
};
},
beforeMount() {
i18n.locale = this.languageLocale;
this.sortedSHACLDefinition = [];
if (this.applicationProfileId !== '') {
if (
this.resourceId !== '00000000-0000-0000-0000-000000000000' &&
this.resourceId !== ''
) {
MetadataApi.getProfileComplete(
this.applicationProfileId,
this.resourceId,
(response: any) => {
this.handleApplicationProfiles(response);
});
} else {
MetadataApi.getProfile(
this.applicationProfileId,
(response: any) => {
this.handleApplicationProfiles(response);
});
validations() : any {
const me = this;
if (!this.fixedValueMode) {
const validator = {} as any;
for (const nodename of Object.keys(this.formData)) {
validator[nodename] = {
$each: {
value: {
async shaclValidated(value: any) {
// A debounce has been implemented based on the nodename
if (me.timeouts[nodename]) clearTimeout(me.timeouts[nodename]);
return new Promise((resolve, reject) => {
me.timeouts[nodename] = setTimeout(async() => {
// Validate the whole data and check if for the current nodename an error is logged
const report = await me.validatorFunction();
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;
resolveValue = false;
}
resolve(resolveValue);
}, me.timeoutInterval);
});
},
},
},
};
}
return {
formData: validator,
};
}
return {
formData: {},
};
},
beforeMount() {
i18n.locale = this.languageLocale;
this.handleApplicationProfiles();
},
watch: {
SHACLDefinition() {
this.handleApplicationProfiles();
},
formData() {
this.$v.$reset();
},
},
methods: {
async validatorFunction() {
const shapes = await this.loadDataset(this.SHACLDefinition, this.mimeType, this.applicationProfileId);
// RDF/JSON => JSON-LD since the loadDataset function doesn't support RDF/JSON
const dataObject = {} as any;
dataObject['@type'] = this.applicationProfileId;
for (const entry of Object.keys(this.formData)) {
for (const metadataEntry of this.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 data = await this.loadDataset(JSON.stringify(dataObject), 'application/ld+json', null);
const validator = new SHACLValidator(shapes);
const report = validator.validate(data);
this.$emit('isValid', report.conforms);
return report;
},
async getQuads (data: string, mimeType: string, baseUri: string | null) : Promise<any> {
const input = new Readable({
read: () => {
input.push(data);
input.push(null);
}
});
const quads = [] as any[];
return new Promise((resolve) => {
rdfParser.parse(input, { contentType: mimeType, baseIRI: baseUri })
.on('data', (quad: any) => {
quads.push(quad);
})
.on('end', () => resolve(quads));
});
},
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) {
dataSet.add(quad);
}
return dataSet;
},
checkField(data: any, nodename: string, property: string = 'value') {
return FieldReader.isValueAssignedToKey(data, nodename, property);
},
......@@ -116,14 +220,14 @@ export default Vue.extend({
if (
this.checkField(formElement, 'http://www.w3.org/ns/shacl#datatype')
) {
let datatype = formElement['http://www.w3.org/ns/shacl#datatype'][0];
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'
) &&
formElement['http://datashapes.org/dash#singleLine'][0][
FieldReader.getObject(formElement, 'http://datashapes.org/dash#singleLine')[
'value'
] === 'false'
) {
......@@ -151,22 +255,36 @@ export default Vue.extend({
}
return null;
},
handleApplicationProfiles(response: any) {
this.SHACLDefinition = response.data;
let keys = Object.keys(this.SHACLDefinition);
for (let i = 0; i < keys.length; i++) {
for (let j = 0; j < keys.length; j++) {
let tmp = (this.SHACLDefinition as any)[keys[j]];
if (
this.checkField(tmp, 'http://www.w3.org/ns/shacl#order') &&
tmp['http://www.w3.org/ns/shacl#order'][0]['value'].toString() ===
'' + i
) {
tmp['idForFixedValues'] = keys[j];
this.sortedSHACLDefinition.push(tmp);
}
async retrieveSHACLDefinition() : Promise<any> {
if (this.SHACLDefinition !== null && this.SHACLDefinition !== '') {
return this.getQuads(this.SHACLDefinition, this.mimeType, this.applicationProfileId);
}
return [];
},
async handleApplicationProfiles() {
this.quads = await this.retrieveSHACLDefinition();
const unmappedSubjects = {} as any;
this.errorMessages = {} as any;
this.timeouts = {} as any;
for (let i = 0; i < this.quads.length; i++) {
if (this.quads[i].predicate.value === 'http://www.w3.org/ns/shacl#order') {
unmappedSubjects[this.quads[i].object.value] = this.quads.filter((entry) =>
entry.subject.value === this.quads[i].subject.value);
this.errorMessages[this.quads[i].subject.value] = '';
this.timeouts[this.quads[i].subject.value] = null;
}
}
this.fixedValueIds = [];
this.sortedSHACLDefinition = [];
let keys = Object.keys(unmappedSubjects).sort((a, b) => parseInt(a) - parseInt(b));
for (let i = 0; i < keys.length; i++) {
this.fixedValueIds.push(unmappedSubjects[keys[i]][0].subject.value);
this.sortedSHACLDefinition.push(unmappedSubjects[keys[i]]);
}
await this.validatorFunction();
},
},
});
......
......@@ -15,13 +15,25 @@
<script lang="ts">
import Vue from 'vue';
import VueI18n from 'vue-i18n';
import Multiselect from 'vue-multiselect';
import 'vue-multiselect/dist/vue-multiselect.min.css';
import FieldReader from '@/util/FieldReader';
Vue.use(VueI18n);
const i18n = new VueI18n({
locale: 'en',
messages: (window.coscine && coscine.i18n) ? coscine.i18n['form-generator'] : {},
silentFallbackWarn: true,
});
export default Vue.extend({
i18n,
name: 'InputBooleanCombobox',
beforeMount() {
i18n.locale = this.languageCode;
// set the datatype
this.$set(this.formData[this.nodeName][0], 'type', 'literal');
this.$set(
......@@ -54,11 +66,13 @@ export default Vue.extend({
},
methods: {
input() {
if (this.selectedOption !== null) {
this.$set(this.formData[this.nodeName][0], 'value', (this.selectedOption as any).value);
} else {
this.$set(this.formData[this.nodeName][0], 'value', '');
}
FieldReader.setField(
this.formData,
this.v,
this.nodeName,
this.selectedOption !== null ? (this.selectedOption as any).value : '',
this.$set
);
this.updateFixedValues();
},
loadData() {
......@@ -81,9 +95,8 @@ export default Vue.extend({
locked: Boolean,
required: Boolean,
nodeName: String,
formFieldInformation: Object,
formFieldInformation: Array,
formData: Object,
fixedValues: Object,
fixedValueMode: Boolean,
disabledMode: Boolean,
checkField: Function,
......@@ -91,6 +104,7 @@ export default Vue.extend({
resourceId: String,
projectId: String,
languageCode: String,
v: Object,
},
components: {
Multiselect,
......
......@@ -23,12 +23,13 @@ import Multiselect from 'vue-multiselect';
import 'vue-multiselect/dist/vue-multiselect.min.css';
import { MetadataApi } from '@coscine/api-connection';
import FieldReader from '@/util/FieldReader';
Vue.use(VueI18n);
const i18n = new VueI18n({
locale: 'en',
messages: coscine.i18n['form-generator'],
messages: (window.coscine && coscine.i18n) ? coscine.i18n['form-generator'] : {},
silentFallbackWarn: true,
});
......@@ -42,12 +43,19 @@ export default Vue.extend({
this.formFieldInformation,
'http://www.w3.org/ns/shacl#maxCount'
) &&
parseInt(this.formFieldInformation['http://www.w3.org/ns/shacl#maxCount'][0][
parseInt(FieldReader.getObject(this.formFieldInformation, 'http://www.w3.org/ns/shacl#maxCount')[
'value'
]) === 1;
this.class = FieldReader.getObject(this.formFieldInformation, 'http://www.w3.org/ns/shacl#class').value;
// set the datatype
this.$set(this.formData[this.nodeName][0], 'type', 'uri');
this.$set(
this.formData[this.nodeName][0],
'datatype',
this.class
);
if (this.fixedValueMode) {
this.updateFixedValues();
......@@ -56,7 +64,7 @@ export default Vue.extend({
mounted() {
MetadataApi.getClassInstances(
this.projectId,
this.formFieldInformation['http://www.w3.org/ns/shacl#class'][0].value,
this.class,
(response: any) => {
if (
Array.isArray(response.data[this.languageCode]) &&
......@@ -81,6 +89,7 @@ export default Vue.extend({
},
data() {
return {
class: '',
noMaxMode: true,
selectableOptions: [] as object[],
selectedOptions: [] as object[],
......@@ -90,19 +99,16 @@ export default Vue.extend({
methods: {
getMax() {
if (!this.noMaxMode) {
return this.formFieldInformation[
const maxCountObject = FieldReader.getObject(this.formFieldInformation,
'http://www.w3.org/ns/shacl#maxCount'
] !== undefined &&
this.formFieldInformation[
'http://www.w3.org/ns/shacl#maxCount'
][0] !== undefined &&
this.formFieldInformation['http://www.w3.org/ns/shacl#maxCount'][0][
);
return
maxCountObject !== undefined &&
maxCountObject[
'value'
] !== undefined
? parseInt(
this.formFieldInformation[
'http://www.w3.org/ns/shacl#maxCount'
][0]['value']
maxCountObject['value']
)
: this.maxNumberOfSelectedObjects;
}
......@@ -112,12 +118,15 @@ export default Vue.extend({
const array = [];
if (Array.isArray(this.selectedOptions)) {
for (const field of this.selectedOptions) {
array.push({ value: (field as any).value, type: 'uri' });
array.push({ value: (field as any).value, type: 'uri', datatype: this.class });
}
} else {
array.push({ value: (this.selectedOptions as any).value, type: 'uri' });
array.push({ value: (this.selectedOptions as any).value, type: 'uri', datatype: this.class });
}
this.$set(this.formData, this.nodeName, array);
if (this.v.formData[this.nodeName]) {
this.v.formData[this.nodeName].$touch();
}
this.updateFixedValues();
},
loadData() {
......@@ -140,9 +149,8 @@ export default Vue.extend({
locked: Boolean,
required: Boolean,
nodeName: String,
formFieldInformation: Object,
formFieldInformation: Array,
formData: Object,
fixedValues: Object,
fixedValueMode: Boolean,
disabledMode: Boolean,
checkField: Function,
......@@ -150,6 +158,7 @@ export default Vue.extend({
resourceId: String,
projectId: String,
languageCode: String,
v: Object,
},
components: {
Multiselect,
......
......@@ -21,6 +21,8 @@ import Vue from 'vue';
import Datepicker from 'vuejs-datepicker';
import { de, en } from 'vuejs-datepicker/dist/locale';
import FieldReader from '@/util/FieldReader';
export default Vue.extend({
name: 'InputDatePicker',
beforeMount() {
......@@ -54,21 +56,21 @@ export default Vue.extend({
locked: Boolean,
required: Boolean,
nodeName: String,
formFieldInformation: Object,
formFieldInformation: Array,
formData: Object,
fixedValues: Object,
fixedValueMode: Boolean,
disabledMode: Boolean,
checkDataField: Function,
checkField: Function,
updateFixedValues: Function,
v: Object,
},
components: {
Datepicker,
},
methods: {
replacePlaceholder() {
if (this.checkField(this.formData, this.nodeName)) {
let adjustDate = 0;
if (this.checkDataField(this.formData, this.nodeName)) {
if (this.formData[this.nodeName][0]['value'] === '{TODAY}') {
if (this.fixedValueMode) {
return;
......@@ -77,29 +79,26 @@ export default Vue.extend({
} else if (this.formData[this.nodeName][0]['value'] === '') {
this.selectedDate = new Date();
} else {
adjustDate = 1;
this.selectedDate = new Date(
this.formData[this.nodeName][0]['value']
this.formData[this.nodeName][0]['value'] + " UTC"
);
}
this.$set(
this.formData[this.nodeName][0],
'value',
`${this.selectedDate.getUTCFullYear()}-${
this.selectedDate.getUTCMonth() + 1
}-${this.selectedDate.getUTCDate() + adjustDate}`
this.selectedDate.toISOString().slice(0, 10)
);
}
},
updateFixedValuesOverwrite() {
this.$set(
this.formData[this.nodeName][0],
'value',
`${this.selectedDate.getUTCFullYear()}-${
this.selectedDate.getUTCMonth() + 1
}-${this.selectedDate.getUTCDate() + 1}`
FieldReader.setField(
this.formData,
this.v,
this.nodeName,
this.selectedDate.toISOString().slice(0, 10),
this.$set
);
this.updateFixedValues();
},
},
......
<template>
<b-form-textarea
:id="cssId"
v-model="formData[nodeName][0]['value']"
@input="updateFixedValues"
v-model="textValue"
@input="input"
:disabled="disabledMode || locked"
:required="required"
:state="v.formData[nodeName] && v.formData[nodeName].$each[0]['value'].$dirty ? !v.formData[nodeName].$each[0]['value'].$error : null"
></b-form-textarea>
</template>
......@@ -12,6 +13,7 @@
import Vue from 'vue';
import { UserApi } from '@coscine/api-connection';
import FieldReader from '@/util/FieldReader';
export default Vue.extend({
name: 'InputTextArea',
......@@ -21,29 +23,56 @@ export default Vue.extend({
// set the datatype
this.$set(this.formData[this.nodeName][0], 'type', 'literal');
this.$set(
this.formData[this.nodeName][0],
'datatype',
'http://www.w3.org/2001/XMLSchema#string'
);
if (this.fixedValueMode) {
this.updateFixedValues();
}
},
mounted() {