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

Merge branch 'Product/1586-shapeEdit' into 'Sprint/2021-13'

Product/1586 shapeEdit

See merge request !16
parents 2d40e8f0 056c1eea
# Aims Frontend
# AIMS Frontend (SHACL Shape Editor)
This repository contains the common frontend for the AIMS project.
The main goal of this frontend application is the editing of [SHACL](https://www.w3.org/TR/shacl/) application profiles.
If you want to use this frontend against your own shapes repository and vocabulary term service, please provide your own api-connection implementation in [here](./src/util/api-connection/) and edit [this file](./src/util/api-connection.ts).
## Project setup
```
yarn install
......@@ -21,6 +25,3 @@ yarn build
```
yarn lint
```
### Customize configuration
See [Configuration Reference](https://cli.vuejs.org/config/).
......@@ -5,7 +5,7 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title><%= htmlWebpackPlugin.options.title %></title>
<title>AIMS</title>
</head>
<body>
<noscript>
......
......@@ -30,11 +30,19 @@
:interactive="interactive"
:selectedProperty="selectedProperty"
@addVocabularyTerm="addVocabularyTerm"
@openEditModal="openEditModal"
@removeProperty="removeProperty"
@selectApplicationProfile="selectApplicationProfile"
@selectProperty="selectProperty"
@updateDefinition="updateDefinition"
/>
<EditModal
:applicationProfile="selectedApplicationProfile"
:definition="definition"
:open="editModalOpen"
@close="closeEditModal"
@save="editAP"
/>
<StoreModal
:applicationProfile="selectedApplicationProfile"
:definition="definition"
......@@ -63,8 +71,11 @@ import AimsHeader from "@/components/Layout/AimsHeader.vue";
import aimsLogo from "@/assets/AIMS-Logo-2-Transparent.png";
import EditModal from "@/components/ApplicationProfiles/Modals/Edit.vue";
import StoreModal from "@/components/ApplicationProfiles/Modals/Store.vue";
import { storeApplicationProfile } from "@/util/api-connection";
import {
addVocabularyTermToDataset,
parseApplicationProfile,
......@@ -75,17 +86,20 @@ export default Vue.extend({
name: "App",
components: {
AimsHeader,
EditModal,
StoreModal,
},
data() {
return {
aimsLogo: aimsLogo,
definition: factory.dataset() as DatasetExt,
editModalOpen: false,
interactive: true,
selectedApplicationProfile: new ApplicationProfileImpl(
"",
"",
[]
"[]",
"application/ld+json"
) as ApplicationProfile,
selectedProperty: factory.blankNode() as Quad_Subject,
storeModalOpen: false,
......@@ -107,6 +121,9 @@ export default Vue.extend({
this.updateDefinition();
}
},
closeEditModal() {
this.editModalOpen = false;
},
closeStoreModal() {
this.storeModalOpen = false;
},
......@@ -124,8 +141,20 @@ export default Vue.extend({
});
saveAs(blob, filename);
},
editAP() {
this.updateDefinition();
this.editModalOpen = false;
},
newApplicationProfile() {
this.selectedApplicationProfile = new ApplicationProfileImpl("", "", []);
this.selectedApplicationProfile = new ApplicationProfileImpl(
"",
"",
"[]",
"application/ld+json"
);
},
openEditModal() {
this.editModalOpen = true;
},
openStoreModal() {
this.storeModalOpen = true;
......@@ -134,8 +163,11 @@ export default Vue.extend({
removeVocabularyTermFromDataset(property, this.definition);
this.updateDefinition();
},
saveAP() {
// TODO: Not yet implemented
async saveAP() {
await storeApplicationProfile(
this.selectedApplicationProfile,
this.definition
);
this.storeModalOpen = false;
},
selectApplicationProfile(selectedApplicationProfile: ApplicationProfile) {
......
......@@ -3,12 +3,19 @@ import ApplicationProfile from "@/types/applicationProfile";
class ApplicationProfileImpl implements ApplicationProfile {
name: string;
base_url: string;
definition: Array<unknown>;
definition: string;
mimeType = "application/ld+json";
constructor(name: string, base_url: string, definition: Array<unknown>) {
constructor(
name: string,
base_url: string,
definition: string,
mimeType = "application/ld+json"
) {
this.name = name;
this.base_url = base_url;
this.definition = definition;
this.mimeType = mimeType;
}
}
......
......@@ -5,6 +5,7 @@
class="d-flex justify-content-between align-items-center"
v-for="applicationProfile in applicationProfiles"
:key="applicationProfile.base_url"
:id="applicationProfile.base_url"
@click="selectApplicationProfile(applicationProfile)"
:active="
selectedApplicationProfile &&
......@@ -12,6 +13,9 @@
"
>
{{ applicationProfile.name }}
<b-tooltip :target="applicationProfile.base_url" triggers="hover">{{
applicationProfile.base_url
}}</b-tooltip>
</b-list-group-item>
</b-list-group>
</template>
......
<template>
<b-modal
id="editModal"
v-model="isOpen"
@ok="save"
@cancel="close"
@close="close"
@hidden="hidden"
:title="$t('editModalTitle', { apName: applicationProfileLabel })"
:cancel-title="$t('Cancel')"
:ok-title="$t('Submit')"
>
<label id="editUrl">{{ $t("url") }}</label>
<b-form-input
id="editUrl"
:placeholder="$t('url')"
:value="url"
@input="setUrl"
trim
/>
<div v-for="formEntry in form" :key="formEntry.url">
<label :for="'edit' + formEntry.name"
>{{ $t(formEntry.name) }} <b-icon icon="question-circle-fill"
/></label>
<b-form-input
:id="'edit' + formEntry.name"
:placeholder="$t(formEntry.name)"
:value="formEntry.value"
@input="setForm(formEntry, $event)"
trim
/>
<b-tooltip :target="'edit' + formEntry.name">{{
$t(formEntry.name + "Description")
}}</b-tooltip>
</div>
</b-modal>
</template>
<script lang="ts">
import DatasetExt from "rdf-ext/lib/Dataset";
import ApplicationProfile from "@/types/applicationProfile";
import NodeShapeProperty from "@/types/nodeShapeProperty";
import Vue, { PropType } from "vue";
import {
changeBaseUrl,
editNodeShapeProperties,
getApplicationProfileLabel,
getApplicationProfileTargetClass,
rdfsPrefix,
shPrefix,
} from "@/util/linkedData";
// TODO: Think if in future Store / Edit Modal can be merged
export default Vue.extend({
name: "Edit",
data() {
return {
url: "",
form: [
{
literal: true,
locale: true,
name: "label",
url: rdfsPrefix + "label",
value: "",
},
{
literal: false,
locale: false,
name: "targetClass",
url: shPrefix + "targetClass",
value: "",
},
] as NodeShapeProperty[],
isOpen: false,
};
},
props: {
applicationProfile: {
required: true,
type: Object as PropType<ApplicationProfile>,
},
definition: {
required: true,
type: Object as PropType<DatasetExt>,
},
open: {
required: true,
default: () => false,
type: Boolean,
},
},
computed: {
applicationProfileLabel(): string {
return getApplicationProfileLabel(
this.applicationProfile,
this.definition,
this.$root.$i18n.locale
);
},
applicationProfileTargetClass(): string {
return getApplicationProfileTargetClass(
this.applicationProfile,
this.definition
);
},
},
watch: {
applicationProfile() {
this.url = this.applicationProfile.base_url;
},
applicationProfileLabel() {
this.setFormProperty("label", this.applicationProfileLabel);
},
applicationProfileTargetClass() {
this.setFormProperty("targetClass", this.applicationProfileTargetClass);
},
open() {
this.isOpen = this.open;
},
},
methods: {
close() {
this.isOpen = false;
},
hidden() {
this.setValues();
this.$emit("close");
},
save() {
changeBaseUrl(this.applicationProfile, this.definition, this.url);
editNodeShapeProperties(
this.applicationProfile,
this.definition,
this.form,
this.$root.$i18n.locale
);
this.$emit("save");
},
setForm(property: NodeShapeProperty, value: string) {
property.value = value;
},
setFormProperty(name: string, value: string) {
const property = this.form.find((formEntry) => formEntry.name === name);
if (property !== undefined) {
property.value = value;
}
},
setUrl(value: string) {
this.url = value;
},
setValues() {
this.url = this.applicationProfile.base_url;
this.setFormProperty("label", this.applicationProfileLabel);
this.setFormProperty("targetClass", this.applicationProfileTargetClass);
},
},
});
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped></style>
......@@ -6,19 +6,34 @@
@cancel="close"
@close="close"
@hidden="hidden"
:title="$t('storeModalTitle', { apName: applicationProfile.name })"
:title="$t('storeModalTitle', { apName: applicationProfileLabel })"
:cancel-title="$t('Cancel')"
:ok-title="$t('Submit')"
:ok-disabled="applicationProfile.base_url === form.url"
>
<label id="storeTitle">{{ $t("title") }}:</label>
<label id="storeUrl">{{ $t("url") }}</label>
<b-form-input
id="storeUrl"
:placeholder="$t('url')"
:value="applicationProfile.base_url"
@input="setUrl"
trim
/>
<label id="storeTitle">{{ $t("label") }}</label>
<b-form-input
id="storeTitle"
:placeholder="$t('title')"
:value="applicationProfile.name"
:placeholder="$t('label')"
:value="applicationProfileLabel"
@input="setUrl"
trim
/>
<label id="storeDiscipline">{{ $t("discipline") }}</label>
<b-form-input
id="storeDiscipline"
:placeholder="$t('discipline')"
@input="setDiscipline"
trim
/>
<label id="storeDiscipline">{{ $t("discipline") }}:</label>
<b-form-input id="storeDiscipline" :placeholder="$t('discipline')" trim />
</b-modal>
</template>
......@@ -28,11 +43,19 @@ import ApplicationProfile from "@/types/applicationProfile";
import Vue, { PropType } from "vue";
import { getApplicationProfileLabel } from "@/util/linkedData";
// TODO: This modal does not update the dataset yet and still is missing a lot of the functionality, it is only for show for now
// TODO: Think if in future Store / Edit Modal can be merged
export default Vue.extend({
name: "Store",
data() {
return {
form: {
discipline: "",
title: "",
url: "",
},
isOpen: false,
};
},
......@@ -43,7 +66,6 @@ export default Vue.extend({
},
definition: {
required: true,
default: () => null,
type: Object as PropType<DatasetExt>,
},
open: {
......@@ -52,7 +74,22 @@ export default Vue.extend({
type: Boolean,
},
},
computed: {
applicationProfileLabel(): string {
return getApplicationProfileLabel(
this.applicationProfile,
this.definition,
this.$root.$i18n.locale
);
},
},
watch: {
applicationProfile() {
this.form.url = this.applicationProfile.base_url;
},
applicationProfileLabel() {
this.form.title = this.applicationProfileLabel;
},
open() {
this.isOpen = this.open;
},
......@@ -65,8 +102,18 @@ export default Vue.extend({
this.$emit("close");
},
save() {
// TODO: Implement logic for storing the relevant values to definition
this.$emit("save");
},
setDiscipline(value: string) {
this.form.discipline = value;
},
setTitle(value: string) {
this.form.title = value;
},
setUrl(value: string) {
this.form.url = value;
},
},
});
</script>
......
......@@ -15,6 +15,7 @@
:value="getValue(entry)"
trim
:type="entry.type"
:readonly="readonly"
/>
</div>
</div>
......@@ -24,7 +25,7 @@
import DatasetExt from "rdf-ext/lib/Dataset";
import ApplicationProfile from "@/types/applicationProfile";
import ShaclProperty from "@/types/shaclProperty";
import { Quad_Subject } from "rdf-js";
import { Quad_Predicate, Quad_Subject } from "rdf-js";
import Vue, { PropType } from "vue";
......@@ -33,33 +34,33 @@ import factory from "rdf-ext";
import {
getLocaleObject,
getObject,
shPrefix,
updateLocaleObject,
updateObject,
xsdPrefix,
} from "@/util/linkedData";
const xsdPrefix = "http://www.w3.org/2001/XMLSchema#";
export default Vue.extend({
name: "PropertyDefinition",
data() {
return {
mapping: [
{
predicate: factory.namedNode("http://www.w3.org/ns/shacl#name"),
predicate: factory.namedNode(shPrefix + "name") as Quad_Predicate,
label: this.$t("shacl.name.label"),
description: this.$t("shacl.name.description"),
localized: true,
type: "text",
},
{
predicate: factory.namedNode("http://www.w3.org/ns/shacl#minCount"),
predicate: factory.namedNode(shPrefix + "minCount") as Quad_Predicate,
label: this.$t("shacl.minCount.label"),
description: this.$t("shacl.minCount.description"),
localized: false,
type: "number",
},
{
predicate: factory.namedNode("http://www.w3.org/ns/shacl#maxCount"),
predicate: factory.namedNode(shPrefix + "maxCount") as Quad_Predicate,
label: this.$t("shacl.maxCount.label"),
description: this.$t("shacl.maxCount.description"),
localized: false,
......@@ -68,6 +69,11 @@ export default Vue.extend({
],
};
},
computed: {
readonly() {
return this.definition.match(this.selectedProperty).size === 0;
},
},
props: {
applicationProfile: {
required: true,
......@@ -77,10 +83,6 @@ export default Vue.extend({
required: true,
type: Object as PropType<DatasetExt>,
},
readOnly: {
type: Boolean,
default: false,
},
selectedProperty: {
required: true,
type: Object as PropType<Quad_Subject>,
......@@ -92,15 +94,15 @@ export default Vue.extend({
if (entry.localized) {
matches = getLocaleObject(
this.selectedProperty,
this.definition,
entry.predicate,
this.definition,
this.$root.$i18n.locale
);
} else {
matches = getObject(
this.selectedProperty,
this.definition,
entry.predicate
entry.predicate,
this.definition
);
}
......
<template>
<div>
<h5>{{ applicationProfile.name }}</h5>
<h5>
<div :class="{ grayed: applicationProfileLabel === '' }">
{{
applicationProfileLabel !== ""
? applicationProfileLabel
: $t("noNameSet")
}}
<b-button @click="openEditModal"><b-icon-gear-fill /></b-button>
</div>
</h5>
<b-list-group v-show="interactive">
<b-button-group
v-for="entry in definition.match(
null,
factory.namedNode('http://www.w3.org/ns/shacl#path')
)"
:key="entry.subject.value"
>
<b-button-group v-for="entry in pathList" :key="entry.subject.value">
<b-list-group-item
button
:class="
......@@ -17,10 +20,14 @@
"
@click="selectProperty(entry)"
:active="selectedProperty === entry.subject"
:id="entry.object.value"
>
<div v-for="name in getEntryLabel(entry)" :key="name.object.value">
{{ name.object.value }}
</div>
<b-tooltip :target="entry.object.value" triggers="hover">{{
entry.object.value
}}</b-tooltip>
</b-list-group-item>
<button
class="list-group-item btn-danger removeBut