Commits (4)
# 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/).
{
"name": "aimsfrontend",
"version": "1.0.0",
"version": "1.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
......@@ -16,8 +16,9 @@
"bootstrap-vue": "^2.21.2",
"core-js": "^3.6.5",
"file-saver": "^2.0.5",
"rdf-ext": "^1.3.1",
"rdf-ext": "1.3.2",
"rdf-parse": "^1.8.0",
"uuid": "^8.3.2",
"vue": "2.6.12",
"vue-i18n": "^8.24.4",
"vue-router": "^3.5.2"
......@@ -31,6 +32,7 @@
"@types/file-saver": "^2.0.2",
"@types/rdf-ext": "^1.3.8",
"@types/rdf-js": "^4.0.1",
"@types/uuid": "^8.3.1",
"@typescript-eslint/eslint-plugin": "^4.18.0",
"@typescript-eslint/parser": "^4.18.0",
"@vue/cli-plugin-eslint": "~4.5.0",
......@@ -45,7 +47,8 @@
"eslint-plugin-vue": "^6.2.2",
"prettier": "^2.2.1",
"semantic-release": "^17.4.4",
"typescript": "~4.1.5",
"tslib": "^2.3.0",
"typescript": "~4.3.3",
"vue-template-compiler": "2.6.12"
}
}
......@@ -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 removeButton"
......@@ -29,6 +36,9 @@
x
</button>
</b-button-group>
<div v-if="pathList.length === 0" class="grayed">
{{ $t("emptyPathList") }}
</div>
</b-list-group>
<b-textarea
class="rdfView"
......@@ -47,7 +57,11 @@ import Vue, { PropType } from "vue";
import factory from "rdf-ext";
import { getLocaleObject } from "@/util/linkedData";
import {
getApplicationProfileLabel,
getLocaleObject,
shPrefix,
} from "@/util/linkedData";
// For representing the Code of an api, these collection of libraries might be interesting: https://github.com/zazuko/rdfjs-elements
......@@ -76,14 +90,34 @@ export default Vue.extend({
type: Object as PropType<Quad_Subject | undefined>,
},
},
computed: {
applicationProfileLabel(): string {
return getApplicationProfileLabel(
this.applicationProfile,
this.definition,
this.$root.$i18n.locale
);
},
pathList() {
return this.definition.match(null, factory.namedNode(shPrefix + "path"));
},
},
methods: {
openEditModal() {
this.$emit("openEditModal");
},
getEntryLabel(entry: Quad) {
return getLocaleObject(
const localeObject = getLocaleObject(
entry.subject,
factory.namedNode(shPrefix + "name"),
this.definition,
factory.namedNode("http://www.w3.org/ns/shacl#name"),
this.$root.$i18n.locale
);
if (localeObject.length > 0) {
return localeObject;
}
// If no name is present, return the entry to render the path url
return [entry];
},
removeProperty(entry: Quad) {
this.$emit("removeProperty", entry.subject);
......@@ -97,6 +131,9 @@ export default Vue.extend({
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
.grayed {
color: gray;
}
.removeButton {
width: 10%;
border: 1px solid rgba(0, 0, 0, 0.125);
......
This diff is collapsed.
......@@ -16,8 +16,18 @@ export default {
searchVoc: "Suche Vokabularterme",
discipline: "Disziplin",
title: "Titel",
label: "Titel",
labelDescription: "rdfs:label",
targetClass: "Zielklasse",
targetClassDescription: "sh:targetClass",
url: "URL des Applikationsprofils",
storeModalTitle: "Neues Applikationsprofil speichern: {apName}",
editModalTitle: "Applikationsprofile editieren: {apName}",
noNameSet: "{Kein Name Gesetzt}",
emptyPathList:
'Es wurde noch kein Vokabularterm zu diesem Applikationsprofil hinzugefügt. Sie können einen Vokuabularterm hinzufügen, indem Sie einen Term aus der "@:vocabulary" Spalte in dieses Applikationsprofil ziehen.',
shacl: {
maxCount: {
......
......@@ -16,8 +16,18 @@ export default {
searchVoc: "Search Vocabulary Terms",
discipline: "Discipline",
title: "Title",
label: "Title",
labelDescription: "rdfs:label",
targetClass: "Target Class",
targetClassDescription: "sh:targetClass",
url: "Application Profile URL",
storeModalTitle: "Save New Application Profile: {apName}",
editModalTitle: "Edit Application Profile: {apName}",
noNameSet: "{No Name Set}",
emptyPathList:
'No vocabulary term has been added yet to this application profile. You can add a vocabulary term by dragging it from the "@:vocabulary" column into this application profile.',
shacl: {
maxCount: {
......
declare interface ApplicationProfile {
name: string;
base_url: string;
definition: Array<unknown>;
definition: string;
mimeType: string = "application/ld+json";
}
export = ApplicationProfile;
declare interface NodeShapeProperty {
literal: boolean;
locale: boolean;
name: string;
url: string;
value: string;
}
export = NodeShapeProperty;
......@@ -2,8 +2,8 @@ import { Quad_Predicate } from "rdf-js";
declare interface ShaclProperty {
predicate: Quad_Predicate;
label: string;
definition: string;
label: VueI18n.TranslateResult;
description: VueI18n.TranslateResult;
localized: boolean;
type: string;
}
......
import ApplicationProfile from "@/types/applicationProfile";
import VocabularyTerm from "@/types/vocabularyTerm";
import {
searchApplicationProfiles as mockupSearchApplicationProfiles,
searchVocabularyTerms as mockupSearchVocabularyTerms,
storeApplicationProfile as mockupStoreApplicationProfile,
} from "@/util/api-connection/mockup-api-connection";
import APs from "../data/applicationProfiles.json";
import vocabularyTerms from "../data/vocabularyTerms.json";
// TODO: Remove eslint disabling when a complete other implementation exists
// eslint-disable-next-line
let searchApplicationProfiles = mockupSearchApplicationProfiles;
// eslint-disable-next-line
let searchVocabularyTerms = mockupSearchVocabularyTerms;
// eslint-disable-next-line
let storeApplicationProfile = mockupStoreApplicationProfile;
// Dummy implementation, waiting for concrete API
export async function searchApplicationProfiles(
query: string
): Promise<Array<ApplicationProfile>> {
const queryFilter = query.toLowerCase().trim();
return APs.filter(
(ap) =>
ap.name.toLowerCase().trim().includes(queryFilter) ||
ap.base_url.toLowerCase().trim().includes(queryFilter)
);
}
const currentLocation = window.location.href;
// Dummy implementation, waiting for concrete API
export async function searchVocabularyTerms(
query: string
): Promise<Array<VocabularyTerm>> {
const queryFilter = query.toLowerCase().trim();
return vocabularyTerms.filter((vocTerm) =>
vocTerm.uri.toLowerCase().trim().includes(queryFilter)
);
if (currentLocation.includes("coscine")) {
// TODO: Use different implementation
} else if (currentLocation.includes("aims")) {
// TODO: Use different implementation
} else if (currentLocation.includes("shacl.kg")) {
// TODO: Use different implementation
}
export {
searchApplicationProfiles,
searchVocabularyTerms,
storeApplicationProfile,
};
import ApplicationProfile from "@/types/applicationProfile";
import VocabularyTerm from "@/types/vocabularyTerm";
import APs from "@/data/applicationProfiles.json";
import vocabularyTerms from "@/data/vocabularyTerms.json";
import DatasetExt from "rdf-ext/lib/Dataset";
// Dummy implementation, waiting for concrete API
export async function searchApplicationProfiles(
query: string
): Promise<Array<ApplicationProfile>> {
const queryFilter = query.toLowerCase().trim();
return APs.filter(
(ap) =>
ap.name.toLowerCase().trim().includes(queryFilter) ||
ap.base_url.toLowerCase().trim().includes(queryFilter)
);
}
// Dummy implementation, waiting for concrete API
export async function searchVocabularyTerms(
query: string
): Promise<Array<VocabularyTerm>> {
const queryFilter = query.toLowerCase().trim();
return vocabularyTerms.filter((vocTerm) =>
vocTerm.uri.toLowerCase().trim().includes(queryFilter)
);
}
// Dummy implementation, waiting for concrete API (remove eslint disabling when implemented)
export async function storeApplicationProfile(
// eslint-disable-next-line
applicationProfile: ApplicationProfile,
// eslint-disable-next-line
dataset: DatasetExt
): Promise<void> {
return;
}
import factory from "rdf-ext";
import DatasetExt from "rdf-ext/lib/Dataset";
import ApplicationProfile from "@/types/applicationProfile";
import NodeShapeProperty from "@/types/nodeShapeProperty";
import VocabularyTerm from "@/types/vocabularyTerm";
import rdfParser from "rdf-parse";
import { Readable } from "stream";
......@@ -12,8 +13,13 @@ import {
Quad_Subject,
} from "rdf-js";
const rdfPrefix = "http://www.w3.org/1999/02/22-rdf-syntax-ns#";
const shPrefix = "http://www.w3.org/ns/shacl#";
import { v4 as uuidv4 } from "uuid";
export const aimsPrefix = "https://aims.org/applicationProfiles/";
export const rdfPrefix = "http://www.w3.org/1999/02/22-rdf-syntax-ns#";
export const rdfsPrefix = "http://www.w3.org/2000/01/rdf-schema#";
export const shPrefix = "http://www.w3.org/ns/shacl#";
export const xsdPrefix = "http://www.w3.org/2001/XMLSchema#";
export async function parseRDFDefinition(
definition: string,
......@@ -38,25 +44,21 @@ export async function parseRDFDefinition(
}
export async function parseApplicationProfile(
applicationProfile: ApplicationProfile,
contentType = "application/ld+json"
applicationProfile: ApplicationProfile
): Promise<DatasetExt> {
return await parseRDFDefinition(
JSON.stringify(applicationProfile.definition),
contentType,
applicationProfile.definition,
applicationProfile.mimeType,
applicationProfile.base_url
);
}
export async function parseApplicationProfiles(
applicationProfiles: Array<ApplicationProfile>,
contentType = "application/ld+json"
applicationProfiles: Array<ApplicationProfile>
): Promise<Array<DatasetExt>> {
const datasets = [] as Array<DatasetExt>;
for (const applicationProfile of applicationProfiles) {
datasets.push(
await parseApplicationProfile(applicationProfile, contentType)
);
datasets.push(await parseApplicationProfile(applicationProfile));
}
return datasets;
}
......@@ -83,14 +85,12 @@ export async function parseVocabularyTerms(
return datasets;
}
export function addVocabularyTermToDataset(
vocabularyTerm: VocabularyTerm,
export function ensureApplicationProfileBaseUrl(
applicationProfile: ApplicationProfile,
dataset: DatasetExt
): boolean {
// Ensure Application Profile
): void {
if (applicationProfile.base_url === "") {
const rootNode = factory.blankNode();
const rootNode = factory.namedNode(aimsPrefix + uuidv4());
applicationProfile.base_url = rootNode.value;
dataset.add(
factory.quad(
......@@ -99,8 +99,19 @@ export function addVocabularyTermToDataset(
factory.namedNode(shPrefix + "NodeShape")
)
);
// Return false if the vocabulary term already exists in the ap
} else if (
}
}
export function addVocabularyTermToDataset(
vocabularyTerm: VocabularyTerm,
applicationProfile: ApplicationProfile,
dataset: DatasetExt
): boolean {
// Ensure Application Profile
ensureApplicationProfileBaseUrl(applicationProfile, dataset);
// Return false if the vocabulary term already exists in the ap
if (
dataset.some(
(quad) =>
quad.predicate.value == shPrefix + "path" &&
......@@ -151,19 +162,19 @@ export function removeVocabularyTermFromDataset(
export function getObject(
subject: Quad_Subject,
dataset: DatasetExt,
predicate: Quad_Predicate
predicate: Quad_Predicate,
dataset: DatasetExt
): Quad[] {
return dataset.match(subject, predicate).toArray();
}
export function getLocaleObject(
subject: Quad_Subject,
dataset: DatasetExt,
predicate: Quad_Predicate,
dataset: DatasetExt,
locale: string
): Quad[] {
const sortedLabelList = getObject(subject, dataset, predicate).sort((quad) =>
const sortedLabelList = getObject(subject, predicate, dataset).sort((quad) =>
(quad.object as Literal).language === locale ? -1 : 1
);
if (sortedLabelList.length > 0) {
......@@ -196,3 +207,109 @@ export function updateLocaleObject(
}
dataset.add(factory.quad(subject, predicate, object));
}
export function getApplicationProfileLabel(
applicationProfile: ApplicationProfile,
dataset: DatasetExt,
locale: string
): string {
const localeObject = getLocaleObject(
factory.namedNode(applicationProfile.base_url),
factory.namedNode(rdfsPrefix + "label"),
dataset,
locale
);
if (localeObject.length > 0) {
return localeObject[0].object.value;
}
return "";
}
export function getApplicationProfileTargetClass(
applicationProfile: ApplicationProfile,
dataset: DatasetExt
): string {
const objectQuad = getObject(
factory.namedNode(applicationProfile.base_url),
factory.namedNode(shPrefix + "targetClass"),
dataset
);
if (objectQuad.length > 0) {
return objectQuad[0].object.value;
}
if (
dataset.some(
(quad) =>
quad.subject.value == applicationProfile.base_url &&
quad.predicate.value == rdfPrefix + "type" &&
quad.object.value == rdfsPrefix + "Class"
)
) {
return applicationProfile.base_url;
}
return "";
}
export function editNodeShapeProperties(
applicationProfile: ApplicationProfile,
dataset: DatasetExt,
properties: NodeShapeProperty[],
locale: string
): void {
// Ensure Application Profile
ensureApplicationProfileBaseUrl(applicationProfile, dataset);
const baseUrlNode = factory.namedNode(applicationProfile.base_url);
for (const property of properties) {
const predicate = factory.namedNode(property.url);
let object: Quad_Object;
if (property.literal) {
object = factory.literal(
property.value,
property.locale ? locale : undefined
);
} else {
object = factory.namedNode(property.value);
}
if (property.literal && property.locale) {
for (const match of dataset.match(baseUrlNode, predicate)) {
if ((match.object as Literal).language === locale) {
dataset.delete(match);
}
}
} else {
dataset.removeMatches(baseUrlNode, predicate);
}
dataset.add(factory.quad(baseUrlNode, predicate, object));
}
}
export function changeBaseUrl(
applicationProfile: ApplicationProfile,
dataset: DatasetExt,
url: string
): void {
// Ensure Application Profile
ensureApplicationProfileBaseUrl(applicationProfile, dataset);
const oldBaseUrlNode = factory.namedNode(applicationProfile.base_url);
const newBaseUrlNode = factory.namedNode(url);
const oldMatches = dataset.match(oldBaseUrlNode);
dataset.removeMatches(oldBaseUrlNode);
for (const oldMatch of oldMatches) {
dataset.add(
factory.quad(newBaseUrlNode, oldMatch.predicate, oldMatch.object)
);
}
applicationProfile.base_url = url;
}
......@@ -23,6 +23,7 @@
:definition="definition"
:interactive="interactive"
:selectedProperty="selectedProperty"
@openEditModal="openEditModal"
@removeProperty="removeProperty"
@selectProperty="selectProperty"
/>
......@@ -87,9 +88,8 @@ export default Vue.extend({
},
onDrop(event: DragEvent) {
if (event.dataTransfer !== null) {
const vocabularyTermString = event.dataTransfer.getData(
"vocabularyTerm"
);
const vocabularyTermString =
event.dataTransfer.getData("vocabularyTerm");
// TODO: Check if it is really a VocabularyTerm
const vocabularyTerm = JSON.parse(
vocabularyTermString
......@@ -97,6 +97,9 @@ export default Vue.extend({
this.$emit("addVocabularyTerm", vocabularyTerm);
}
},
openEditModal() {
this.$emit("openEditModal");
},
removeProperty(property: Quad_Subject) {
this.$emit("removeProperty", property);
},
......