Select Git revision
-
Andrew Cornell authoredAndrew Cornell authored
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
WrapperInput.vue 20.40 KiB
<template>
<div
v-if="componentDefinition !== null && values"
:class="componentDefinition"
:style="cssProps"
>
<b-form-group
:class="{
invisible: !fixedValueMode && invisible,
}"
label-cols-sm="3"
label-align-sm="right"
:label="label"
:label-class="
'application-profile-label' + (required ? ' mandatory' : '')
"
>
<b-button-toolbar v-for="(entry, entryIndex) in values" :key="entryIndex">
<component
:is="componentDefinition"
class="wrapper-input"
:locked="locked"
:required="required"
:fixed-value-mode="fixedValueMode"
:fixed-values="fixedValues"
:disabled-mode="disabledMode"
:language-code="languageLocale"
:entry="entry"
:data-type="dataType"
:class-object="classObject"
:list="list"
:single-line="singleLine"
:state="state"
:attached-metadata="attachedMetadata[entryIndex]"
:dataset="dataset"
:node-import="nodeImport"
:class-receiver="classReceiver"
:user-receiver="userReceiver"
@input="inputValue(entryIndex, $event)"
@inputDataset="inputDataset"
@triggerValidation="triggerValidation"
/>
<b-button-group class="wrapper-input-button">
<b-button
v-if="fixedValueMode"
:disabled="fixedValueDisabled[entryIndex]"
class="innerButton"
variant="outline-secondary"
@click.prevent="changeLockable"
>
<LockIcon v-if="locked" />
<LockOpenIcon v-else />
</b-button>
<b-button
v-if="fixedValueMode"
v-b-tooltip.hover.top="$t('invisibilityInfo')"
:disabled="disabledMode || !fieldCanBeInvisible"
class="innerButton"
variant="outline-secondary"
@click.prevent="changeVisibility"
>
<Invisible v-if="invisible" />
<Visible v-else />
</b-button>
<b-button
v-if="caseInsertField(entryIndex)"
:disabled="disabledMode || insertNewFieldButtonDisabled"
class="innerButton"
variant="outline-secondary"
@click.prevent="insertNewFields(1)"
>
<PlusIcon />
</b-button>
<b-button
v-if="!caseInsertField(entryIndex)"
:disabled="disabledMode || removeFieldButtonDisabled"
class="innerButton"
variant="outline-secondary"
@click.prevent="removeField(entryIndex)"
>
<MinusIcon />
</b-button>
</b-button-group>
</b-button-toolbar>
<b-button
v-if="values.length === 0"
:disabled="disabledMode || insertNewFieldButtonDisabled"
class="innerButton float-right"
variant="outline-secondary"
@click.prevent="insertNewFields(1)"
>
<PlusIcon />
</b-button>
<b-form-invalid-feedback :state="state">
<div v-for="(message, index) in errorMessages[nodeName]" :key="index">
{{ message.value }}
</div>
</b-form-invalid-feedback>
</b-form-group>
</div>
</template>
<script lang="ts">
import { defineComponent, Vue2 as Vue } from 'vue-demi';
import type { PropType } from 'vue-demi';
import VueI18n from 'vue-i18n';
import locale from '@/locale';
import LockIcon from 'vue-material-design-icons/Lock.vue';
import LockOpenIcon from 'vue-material-design-icons/LockOpen.vue';
import MinusIcon from 'vue-material-design-icons/Minus.vue';
import PlusIcon from 'vue-material-design-icons/Plus.vue';
import Visible from 'vue-material-design-icons/Eye.vue';
import Invisible from 'vue-material-design-icons/EyeOff.vue';
import {
BButton,
BButtonGroup,
BButtonToolbar,
BFormInvalidFeedback,
BFormGroup,
} from 'bootstrap-vue';
import InputCombobox from './InputCombobox.vue';
import InputTextField from './InputTextField.vue';
import InputDatePicker from './InputDatePicker.vue';
import InputBooleanCombobox from './InputBooleanCombobox.vue';
import InputList from './InputList.vue';
import InputShape from './InputShape.vue';
if (Vue) {
Vue.use(VueI18n);
} else {
// TODO: Deal with Vue 3
}
const i18n = new VueI18n({
locale: 'en',
messages: locale,
silentFallbackWarn: true,
});
import type { BilingualLabels } from '@coscine/api-client/dist/types/Coscine.Api.Metadata';
import type { UserObject } from '@coscine/api-client/dist/types/Coscine.Api.User';
import type { Dataset, NamedNode, Quad_Object, Quad_Subject } from 'rdf-js';
import { prefixes } from '@zazuko/rdf-vocabularies';
import factory from 'rdf-ext';
import { getObject, getObjectStringList } from '@/util/linkedData';
import type {
FixedValueObject,
FixedValues,
ValueType,
} from '@/types/fixedValues';
export default defineComponent({
i18n,
name: 'WrapperInput',
components: {
BButton,
BButtonGroup,
BButtonToolbar,
BFormInvalidFeedback,
BFormGroup,
LockIcon,
LockOpenIcon,
MinusIcon,
PlusIcon,
Visible,
Invisible,
InputTextField,
InputCombobox,
InputDatePicker,
InputBooleanCombobox,
InputList,
InputShape,
},
props: {
metadata: {
required: true,
type: Object as PropType<Dataset>,
},
metadataSubject: {
default: () => factory.blankNode(),
type: Object as PropType<Quad_Subject>,
},
fixedValues: {
default: () => ({}),
type: Object as PropType<FixedValues>,
},
fixedValueMode: {
default: false,
type: Boolean,
},
disabledMode: {
default: false,
type: Boolean,
},
languageLocale: {
default: 'en',
type: String,
},
errorMessages: {
default: () => ({}),
type: Object as PropType<{ [nodeName: string]: Quad_Object[] }>,
},
classReceiver: {
default: () => {
return async () => ({} as BilingualLabels);
},
type: Function as PropType<
(classUrl: string) => Promise<BilingualLabels>
>,
},
userReceiver: {
default: () => {
return async () => ({} as UserObject);
},
type: Function as PropType<() => Promise<UserObject>>,
},
dataset: {
required: true,
type: Object as PropType<Dataset>,
},
property: {
required: true,
type: Object as PropType<Quad_Subject>,
},
},
data() {
return {
locked: false,
invisible: false,
values: [] as Quad_Object[],
};
},
computed: {
attachedMetadata(): Dataset[] {
return this.values.map((entry) => {
const collectedNodes = [entry];
const toLookAtNodes = [entry];
const attachedMetadata = factory.dataset() as unknown as Dataset;
while (toLookAtNodes.length > 0) {
const currentNode = toLookAtNodes.pop();
const currentAttachedMetadata = Array.from(
this.metadata.match(currentNode)
);
attachedMetadata.addAll(currentAttachedMetadata);
toLookAtNodes.push(
...currentAttachedMetadata
.filter(
(currentOldValue) =>
(currentOldValue.object.termType === 'NamedNode' ||
currentOldValue.object.termType === 'BlankNode') &&
!collectedNodes.some(
(collectedNode) =>
collectedNode.value === currentOldValue.object.value
)
)
.map((quad) => quad.object)
);
collectedNodes.push(
...currentAttachedMetadata.map((quad) => quad.object)
);
}
return attachedMetadata;
});
},
classObject(): NamedNode<string> | null {
const classes = this.dataset.match(
this.property,
factory.namedNode(prefixes.sh + 'class')
);
if (classes.size) {
for (const classEntry of classes) {
return classEntry.object as NamedNode<string>;
}
}
return null;
},
componentDefinition(): string | null {
if (this.dataType) {
if (this.dataType.value === prefixes.xsd + 'date') {
return 'InputDatePicker';
} else if (this.dataType.value === prefixes.xsd + 'boolean') {
return 'InputBooleanCombobox';
}
return 'InputTextField';
} else {
if (this.classObject) {
return 'InputCombobox';
}
if (this.isList) {
return 'InputList';
}
if (this.isNode) {
return 'InputShape';
}
}
return null;
},
cssProps(): {
'--wrapperInputWidth': string;
'--wrapperInputRightMargin': string;
} {
return {
// Formula: 100% - (n.Buttons)*(Button Width) - (--wrapperInputRightMargin)
'--wrapperInputWidth': this.fixedValueMode
? 'calc(100% - 181px - 1rem)'
: 'calc(100% - 61px - 5px)',
'--wrapperInputRightMargin': this.fixedValueMode ? '1rem' : '5px',
};
},
dataType(): NamedNode | undefined {
const dataTypes = this.dataset.match(
this.property,
factory.namedNode(prefixes.sh + 'datatype')
);
for (const dataType of dataTypes) {
return dataType.object as NamedNode;
}
return undefined;
},
fieldCanBeInvisible(): boolean {
return (
(this.required &&
this.values.some((entry) => entry.value !== '') &&
this.locked) ||
!this.required
);
},
fixedValueDisabled(): Array<boolean> {
return this.values.map(
(entry) =>
this.disabledMode || !entry || (this.required && this.invisible)
);
},
insertNewFieldButtonDisabled(): boolean {
if (
this.values.length === 0 ||
((!this.maxCount || this.values.length < this.maxCount) &&
this.values.every((entry) => entry.value !== ''))
) {
return false;
} else {
return true;
}
},
isList(): boolean {
return (
this.dataset.match(this.property, factory.namedNode(prefixes.sh + 'in'))
.size > 0
);
},
isNode(): boolean {
return (
this.dataset.match(
this.property,
factory.namedNode(prefixes.sh + 'node')
).size > 0
);
},
label(): string {
const labels = getObject(
this.property,
factory.namedNode(prefixes.sh + 'name'),
this.dataset,
this.languageLocale
);
if (!labels.length) {
return this.nodeName;
}
return labels[0].object.value;
},
list(): string[] {
if (this.isList) {
return getObjectStringList(
this.property,
factory.namedNode(prefixes.sh + 'in'),
this.dataset
);
}
return [];
},
maxCount(): number | undefined {
const maxCounts = getObject(
this.property,
factory.namedNode(prefixes.sh + 'maxCount'),
this.dataset
);
if (!maxCounts.length) {
return undefined;
}
return Number(maxCounts[0].object.value);
},
minCount(): number {
const minCounts = getObject(
this.property,
factory.namedNode(prefixes.sh + 'minCount'),
this.dataset
);
if (!minCounts.length) {
return 0;
}
return Number(minCounts[0].object.value);
},
minLength(): number {
const minLengths = getObject(
this.property,
factory.namedNode(prefixes.sh + 'minLength'),
this.dataset
);
if (!minLengths.length) {
return 0;
}
return Number(minLengths[0].object.value);
},
nodeName(): string {
const names = getObject(
this.property,
factory.namedNode(prefixes.sh + 'path'),
this.dataset
);
if (!names.length) {
return '';
}
return names[0].object.value;
},
nodeImport(): string {
const names = getObject(
this.property,
factory.namedNode(prefixes.sh + 'node'),
this.dataset
);
if (!names.length) {
return '';
}
return names[0].object.value;
},
removeFieldButtonDisabled(): boolean {
if (this.values.length > this.minCount) {
return false;
} else {
return true;
}
},
required(): boolean {
return this.minCount >= 1;
},
singleLine(): boolean {
return !this.dataset.some(
(quad) =>
quad.subject === this.property &&
quad.predicate.value === 'http://datashapes.org/dash#singleLine' &&
quad.object.value === 'false'
);
},
state(): boolean | null {
if (
this.errorMessages[this.nodeName] &&
this.errorMessages[this.nodeName].length
) {
return false;
} else if (this.values.some((entry) => entry.value !== '')) {
return true;
}
return null;
},
// TODO: Currently, then handling of multiple values is a bit weird
// e.g. if you enter the same value, one disappears
// This is due to a limitation that a dataset can only contain one ?s ?p ?o triple
// Try to think how to deal with this limitation and resolve the weirdness
metadataValues(): Quad_Object[] {
return Array.from(
this.metadata.match(
this.metadataSubject,
factory.namedNode(this.nodeName)
)
).map((quad) => quad.object);
},
},
watch: {
fixedValues() {
this.initFixedValues();
},
metadata() {
this.initMetadata();
this.setValuesFromMetadataValues();
},
},
beforeMount() {
i18n.locale = this.languageLocale;
this.initMetadata();
this.initFixedValues();
this.initDefaultValues();
},
methods: {
changeLockable() {
this.locked = !this.locked;
this.setFixedValues(this.values);
},
changeVisibility() {
if (!this.invisible && this.fieldCanBeInvisible) {
this.invisible = true;
} else {
this.invisible = false;
}
this.setFixedValues(this.values);
},
initDefaultValues() {
if (
!this.values.length ||
this.values.every((entry) => entry.value === '')
) {
const fixedValueObject = this.fixedValues[this.nodeName];
const datasetMatches = this.dataset.match(
this.property,
factory.namedNode(prefixes.sh + 'defaultValue')
);
if (
fixedValueObject &&
fixedValueObject['https://purl.org/coscine/defaultValue'] &&
fixedValueObject['https://purl.org/coscine/defaultValue'].some(
(entry) => entry.value !== ''
)
) {
this.input(
this.valueTypesToQuadObjects(
fixedValueObject['https://purl.org/coscine/defaultValue']
)
);
} else if (datasetMatches.size) {
this.input(Array.from(datasetMatches).map((quad) => quad.object));
}
}
},
initFixedValues() {
const fixedValueObject = this.fixedValues[this.nodeName];
if (fixedValueObject) {
const fixedValueList =
fixedValueObject['https://purl.org/coscine/fixedValue'];
if (
fixedValueList &&
fixedValueList.some((entry) => entry.value !== '')
) {
if (
!this.values.length ||
this.values.every((entry) => entry.value === '')
) {
this.input(this.valueTypesToQuadObjects(fixedValueList));
}
this.locked = true;
}
const invisibleList =
fixedValueObject['https://purl.org/coscine/invisible'];
this.invisible =
invisibleList !== undefined &&
invisibleList.every((entry) => entry.value === '1');
}
},
initMetadata() {
this.values = [...this.metadataValues];
// Set values if they don't exist
if (!this.values.length) {
const numberOfNewFields = this.isNode ? 0 : 1;
// Setting more than one will not work since a dataset does not support multiple triples
// which are the same ?s ?p ?o
this.insertNewFields(numberOfNewFields);
}
},
input(values: Quad_Object[], fixedValueTransfer = true) {
this.$emit('input', this.nodeName, values);
if (fixedValueTransfer) {
this.updateFixedValues(values);
}
},
inputFixedValues(object: FixedValueObject) {
this.$emit('inputFixedValues', this.nodeName, object);
},
inputDataset(value: Quad_Object, dataset: Dataset) {
this.$emit('inputDataset', value, dataset);
},
inputValue(entryKey: number, value: Quad_Object) {
const newValues = [...this.values];
// Clone for breaking reactivity
if (value.termType === 'Literal') {
newValues[entryKey] = factory.literal(value.value, value.datatype);
} else if (value.termType === 'NamedNode') {
newValues[entryKey] = factory.namedNode(value.value);
} else {
newValues[entryKey] = value;
}
this.input(newValues);
},
insertNewFields(numberOfNewFields = 1) {
const newValues = [...this.values];
for (let i = 0; i < numberOfNewFields; i++) {
let entry: Quad_Object = factory.literal('', this.dataType);
if (this.isNode) {
entry = factory.blankNode();
} else if (!this.dataType) {
entry = factory.namedNode('');
}
newValues.push(entry);
}
this.input(newValues, false);
},
caseInsertField(entryKey: number) {
return entryKey === 0 ? true : false;
},
removeField(entryKey: number) {
const newValues = [...this.values];
const removedEntries = newValues.splice(entryKey, 1);
if (this.isNode) {
for (const removedEntry of removedEntries) {
this.inputDataset(
removedEntry,
factory.dataset() as unknown as Dataset
);
}
}
this.input(newValues);
},
setFixedValues(values: Quad_Object[]) {
const currentFixedValueObject: FixedValueObject = {};
const extractedFixedValue = this.extractForFixedValues(values);
currentFixedValueObject['https://purl.org/coscine/invisible'] = [
{ value: this.invisible ? '1' : '0', type: 'literal' },
];
if (this.locked) {
currentFixedValueObject['https://purl.org/coscine/fixedValue'] =
extractedFixedValue;
} else {
currentFixedValueObject['https://purl.org/coscine/defaultValue'] =
extractedFixedValue;
}
this.inputFixedValues(currentFixedValueObject);
},
setValuesFromMetadataValues() {
this.values = this.values.filter((entry) =>
this.metadataValues.some(
(metadataEntry) => entry.value === metadataEntry.value
)
);
const newValues = this.metadataValues.filter(
(metadataEntry) =>
!this.values.some((entry) => entry.value === metadataEntry.value)
);
this.values = [...this.values, ...newValues];
},
triggerValidation() {
this.$emit('triggerValidation');
},
updateFixedValues(values: Quad_Object[]) {
if (this.fixedValueMode) {
if (
this.fixedValues[this.nodeName] &&
this.fixedValues[this.nodeName]['https://purl.org/coscine/fixedValue']
) {
this.locked = true;
} else {
this.locked = false;
}
this.setFixedValues(values);
}
},
extractForFixedValues(values: Quad_Object[]) {
const extractedValues: ValueType[] = [];
for (const entry of values.filter((entry) => entry.value !== '')) {
const valueObject: ValueType = {
value: entry.value,
type: entry.termType === 'Literal' ? 'literal' : 'uri',
datatype:
entry.termType === 'Literal' ? entry.datatype.value : undefined,
};
extractedValues.push(valueObject);
}
return extractedValues;
},
valueTypesToQuadObjects(valueTypes: ValueType[]) {
return valueTypes.map((fixedValue) =>
fixedValue.type === 'literal'
? factory.literal(
fixedValue.value,
fixedValue.datatype
? factory.namedNode(fixedValue.datatype)
: undefined
)
: factory.namedNode(fixedValue.value)
);
},
},
});
</script>
<style scoped>
.wrapper-input {
width: var(--wrapperInputWidth);
margin-right: var(--wrapperInputRightMargin);
}
.wrapper-input-button {
width: 21px;
}
</style>