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

Update: Add Storing functionality

parent 54a84991
......@@ -7,21 +7,13 @@ packageExtensions:
"fork-ts-checker-webpack-plugin@*":
dependencies:
"vue-template-compiler": "*"
peerDependencies:
"typescript": "*"
"vue-i18n@*":
dependencies:
"vue": "^2.6.12"
"vue-material-design-icons@*":
dependencies:
"vue": "^2.6.12"
"vue": "^2.6.14"
"vue-router@*":
dependencies:
"vue": "^2.6.12"
"vuex@*":
dependencies:
"vue": "^2.6.12"
"vue": "^2.6.14"
"bootstrap-vue@*":
dependencies:
"vue": "^2.6.12"
"vue": "^2.6.14"
"jquery": "*"
......@@ -10,17 +10,21 @@
"dependencies": {
"@itcenter-layout/bootstrap": "~1.5.5",
"@itcenter-layout/masterpage": "^1.3.0",
"@popperjs/core": "^2.9.2",
"@rdfjs/data-model": "^1.2.0",
"@rdfjs/dataset": "^1.1.0",
"axios": "^0.21.1",
"bootstrap": "^5.0.0",
"bootstrap-vue": "^2.21.2",
"core-js": "^3.6.5",
"file-saver": "^2.0.5",
"rdf-ext": "1.3.2",
"rdf-ext": "^1.3.4",
"rdf-parse": "^1.8.0",
"rdf-serialize": "^1.1.0",
"stream-to-string": "^1.2.0",
"uuid": "^8.3.2",
"vue": "2.6.12",
"vue-i18n": "^8.24.4",
"vue": "^2.6.14",
"vue-i18n": "^8.25.0",
"vue-router": "^3.5.2"
},
"devDependencies": {
......@@ -45,10 +49,25 @@
"eslint": "^6.7.2",
"eslint-plugin-prettier": "^3.3.1",
"eslint-plugin-vue": "^6.2.2",
"lint-staged": "^9.5.0",
"prettier": "^2.2.1",
"semantic-release": "^17.4.4",
"tslib": "^2.3.0",
"typescript": "~4.3.3",
"vue-template-compiler": "2.6.12"
}
"vue-template-compiler": "^2.6.14"
},
"repository": {
"type": "git",
"url": "https://git.rwth-aachen.de/coscine/frontend/apps/aimsfrontend.git"
},
"gitHooks": {
"pre-commit": "lint-staged"
},
"lint-staged": {
"*.{js,jsx,vue,ts,tsx}": [
"vue-cli-service lint",
"git add"
]
},
"license": "MIT"
}
......@@ -34,22 +34,35 @@
@removeProperty="removeProperty"
@selectApplicationProfile="selectApplicationProfile"
@selectProperty="selectProperty"
@textDefinitionUpdate="textDefinitionUpdate"
@updateDefinition="updateDefinition"
/>
<EditModal
:applicationProfile="selectedApplicationProfile"
:definition="definition"
labelPrefix="edit"
okLabelKey="Continue"
:open="editModalOpen"
@close="closeEditModal"
@save="editAP"
/>
<StoreModal
<EditModal
:applicationProfile="selectedApplicationProfile"
:definition="definition"
labelPrefix="store"
okLabelKey="Submit"
:open="storeModalOpen"
@close="closeStoreModal"
@save="saveAP"
/>
<b-toast
id="storeToast"
:title="$t('storeToastTitle')"
autoHideDelay="5000"
toaster="b-toaster-bottom-right"
>
{{ $t("storeToastDescription") }}
</b-toast>
</div>
</div>
</template>
......@@ -72,14 +85,16 @@ 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,
parseRDFDefinition,
removeVocabularyTermFromDataset,
serializeRDFDefinition,
setApplicationProfileBasedOnDataset,
} from "@/util/linkedData";
export default Vue.extend({
......@@ -87,7 +102,6 @@ export default Vue.extend({
components: {
AimsHeader,
EditModal,
StoreModal,
},
data() {
return {
......@@ -134,11 +148,14 @@ export default Vue.extend({
this.$root.$i18n.locale = "en";
}
},
downloadRDF() {
const filename = "applicationProfile.n3";
const blob = new Blob([this.definition.toCanonical()], {
type: "text/plain;charset=utf-8",
});
async downloadRDF() {
const filename = "applicationProfile.ttl";
const blob = new Blob(
[await serializeRDFDefinition(this.definition, "text/turtle")],
{
type: "text/plain;charset=utf-8",
}
);
saveAs(blob, filename);
},
editAP() {
......@@ -164,11 +181,13 @@ export default Vue.extend({
this.updateDefinition();
},
async saveAP() {
this.updateDefinition();
await storeApplicationProfile(
this.selectedApplicationProfile,
this.definition
);
this.storeModalOpen = false;
this.$bvToast.show("storeToast");
},
selectApplicationProfile(selectedApplicationProfile: ApplicationProfile) {
this.selectedApplicationProfile = selectedApplicationProfile;
......@@ -184,6 +203,18 @@ export default Vue.extend({
switchInteractivity() {
this.interactive = !this.interactive;
},
async textDefinitionUpdate(
textDefinition: string,
mimeType = "application/n-triples"
) {
this.definition = await parseRDFDefinition(
textDefinition,
mimeType,
this.selectedApplicationProfile.base_url
);
this.selectedApplicationProfile =
await setApplicationProfileBasedOnDataset(this.definition);
},
updateDefinition() {
// Force definition update
this.definition = this.definition.clone();
......@@ -248,4 +279,7 @@ export default Vue.extend({
.input-group {
margin-bottom: 0.5rem;
}
#storeToast.toast:not(.showing):not(.show) {
opacity: 1;
}
</style>
......@@ -13,9 +13,12 @@
"
>
{{ applicationProfile.name }}
<b-tooltip :target="applicationProfile.base_url" triggers="hover">{{
applicationProfile.base_url
}}</b-tooltip>
<b-tooltip
:target="applicationProfile.base_url"
triggers="hover"
boundary="window"
>{{ applicationProfile.base_url }}</b-tooltip
>
</b-list-group-item>
</b-list-group>
</template>
......
<template>
<b-modal
id="editModal"
:id="labelPrefix + 'Modal'"
v-model="isOpen"
@ok="save"
@cancel="close"
@close="close"
@hidden="hidden"
:title="$t('editModalTitle', { apName: applicationProfileLabel })"
:title="$t(labelPrefix + 'ModalTitle', { apName: applicationProfileLabel })"
:cancel-title="$t('Cancel')"
:ok-title="$t('Submit')"
:ok-title="$t(okLabelKey)"
>
<label id="editUrl">{{ $t("url") }}</label>
<label :id="labelPrefix + 'Url'">{{ $t("url") }}</label>
<b-form-input
id="editUrl"
:id="labelPrefix + 'Url'"
:placeholder="$t('url')"
:value="url"
@input="setUrl"
trim
/>
<div v-for="formEntry in form" :key="formEntry.url">
<label :for="'edit' + formEntry.name"
<label
:for="labelPrefix + formEntry.name"
:id="labelPrefix + formEntry.name + 'Label'"
>{{ $t(formEntry.name) }} <b-icon icon="question-circle-fill"
/></label>
<b-form-input
:id="'edit' + formEntry.name"
:id="labelPrefix + formEntry.name"
:placeholder="$t(formEntry.name)"
:value="formEntry.value"
@input="setForm(formEntry, $event)"
trim
:type="formEntry.type"
v-if="!formEntry.multiline"
/>
<b-tooltip :target="'edit' + formEntry.name">{{
$t(formEntry.name + "Description")
}}</b-tooltip>
<b-form-textarea
:id="labelPrefix + formEntry.name"
:placeholder="$t(formEntry.name)"
:value="formEntry.value"
@input="setForm(formEntry, $event)"
trim
v-else
/>
<b-tooltip
:target="labelPrefix + formEntry.name + 'Label'"
triggers="hover"
>{{ $t(formEntry.name + "Description") }}</b-tooltip
>
</div>
</b-modal>
</template>
......@@ -46,13 +60,12 @@ import Vue, { PropType } from "vue";
import {
changeBaseUrl,
editNodeShapeProperties,
getApplicationProfileLabel,
getApplicationProfileProperty,
getApplicationProfileTargetClass,
rdfsPrefix,
shPrefix,
} from "@/util/linkedData";
// TODO: Think if in future Store / Edit Modal can be merged
export default Vue.extend({
name: "Edit",
data() {
......@@ -60,16 +73,34 @@ export default Vue.extend({
url: "",
form: [
{
literal: true,
locale: true,
multiline: false,
name: "label",
type: "text",
url: rdfsPrefix + "label",
value: "",
},
{
literal: false,
locale: true,
multiline: true,
name: "description",
type: "text",
url: rdfsPrefix + "comment",
value: "",
},
{
locale: false,
multiline: false,
name: "node",
type: "url",
url: shPrefix + "node",
value: "",
},
{
locale: false,
multiline: false,
name: "targetClass",
type: "url",
url: shPrefix + "targetClass",
value: "",
},
......@@ -86,6 +117,14 @@ export default Vue.extend({
required: true,
type: Object as PropType<DatasetExt>,
},
labelPrefix: {
default: "edit",
type: String,
},
okLabelKey: {
default: "Continue",
type: String,
},
open: {
required: true,
default: () => false,
......@@ -94,10 +133,25 @@ export default Vue.extend({
},
computed: {
applicationProfileLabel(): string {
return getApplicationProfileLabel(
this.applicationProfile,
this.definition,
this.$root.$i18n.locale
const labelObject = this.form.find((entry) => entry.name === "label");
if (labelObject !== undefined) {
return labelObject.value;
}
return "";
},
applicationProfileProperties(): Map<NodeShapeProperty, string> {
return new Map(
this.form
.filter((entry) => entry.name !== "targetClass")
.map((entry) => [
entry,
getApplicationProfileProperty(
this.applicationProfile,
this.definition,
entry.url,
entry.locale ? this.$root.$i18n.locale : undefined
),
])
);
},
applicationProfileTargetClass(): string {
......@@ -108,11 +162,11 @@ export default Vue.extend({
},
},
watch: {
applicationProfile() {
"applicationProfile.base_url"() {
this.url = this.applicationProfile.base_url;
},
applicationProfileLabel() {
this.setFormProperty("label", this.applicationProfileLabel);
applicationProfileProperties() {
this.setByProperties();
},
applicationProfileTargetClass() {
this.setFormProperty("targetClass", this.applicationProfileTargetClass);
......@@ -127,7 +181,6 @@ export default Vue.extend({
},
hidden() {
this.setValues();
this.$emit("close");
},
save() {
......@@ -140,13 +193,18 @@ export default Vue.extend({
);
this.$emit("save");
},
setByProperties() {
this.applicationProfileProperties.forEach((definitionValue, property) => {
this.setFormProperty(property.name, definitionValue);
});
},
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;
this.setForm(property, value);
}
},
setUrl(value: string) {
......@@ -154,7 +212,7 @@ export default Vue.extend({
},
setValues() {
this.url = this.applicationProfile.base_url;
this.setFormProperty("label", this.applicationProfileLabel);
this.setByProperties();
this.setFormProperty("targetClass", this.applicationProfileTargetClass);
},
},
......
<template>
<b-modal
id="storeModal"
v-model="isOpen"
@ok="save"
@cancel="close"
@close="close"
@hidden="hidden"
:title="$t('storeModalTitle', { apName: applicationProfileLabel })"
:cancel-title="$t('Cancel')"
:ok-title="$t('Submit')"
:ok-disabled="applicationProfile.base_url === form.url"
>
<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('label')"
:value="applicationProfileLabel"
@input="setUrl"
trim
/>
<label id="storeDiscipline">{{ $t("discipline") }}</label>
<b-form-input
id="storeDiscipline"
:placeholder="$t('discipline')"
@input="setDiscipline"
trim
/>
</b-modal>
</template>
<script lang="ts">
import DatasetExt from "rdf-ext/lib/Dataset";
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,
};
},
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
);
},
},
watch: {
applicationProfile() {
this.form.url = this.applicationProfile.base_url;
},
applicationProfileLabel() {
this.form.title = this.applicationProfileLabel;
},
open() {
this.isOpen = this.open;
},
},
methods: {
close() {
this.isOpen = false;
},
hidden() {
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>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped></style>
......@@ -25,14 +25,13 @@
import DatasetExt from "rdf-ext/lib/Dataset";
import ApplicationProfile from "@/types/applicationProfile";
import ShaclProperty from "@/types/shaclProperty";
import { Quad_Predicate, Quad_Subject } from "rdf-js";
import { Quad_Object, Quad_Predicate, Quad_Subject } from "rdf-js";
import Vue, { PropType } from "vue";
import factory from "rdf-ext";
import {
getLocaleObject,
getObject,
shPrefix,
updateLocaleObject,
......@@ -66,6 +65,20 @@ export default Vue.extend({
localized: false,
type: "number",
},
{
predicate: factory.namedNode(shPrefix + "class") as Quad_Predicate,
label: this.$t("shacl.class.label"),
description: this.$t("shacl.class.description"),
localized: false,
type: "url",
},
{
predicate: factory.namedNode(shPrefix + "node") as Quad_Predicate,
label: this.$t("shacl.node.label"),
description: this.$t("shacl.node.description"),
localized: false,
type: "url",
},
],
};
},
......@@ -92,7 +105,7 @@ export default Vue.extend({
getValue(entry: ShaclProperty) {
let matches = [];
if (entry.localized) {
matches = getLocaleObject(
matches = getObject(
this.selectedProperty,
entry.predicate,
this.definition,
......@@ -125,9 +138,14 @@ export default Vue.extend({
this.definition
);
} else {
const object = factory.literal(value);
if (entry.type === "number") {
object.datatype = factory.namedNode(xsdPrefix + "integer");
let object: Quad_Object;
if (entry.type === "url") {
object = factory.namedNode(value);
} else {
object = factory.literal(value);
if (entry.type === "number") {
object.datatype = factory.namedNode(xsdPrefix + "integer");
}
}
updateObject(