Commits (3)
......@@ -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": "*"
{
"name": "aimsfrontend",
"version": "1.1.0",
"version": "1.2.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
......@@ -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(
this.selectedProperty,
......
......@@ -20,14 +20,17 @@
"
@click="selectProperty(entry)"
:active="selectedProperty === entry.subject"
:id="entry.object.value"
:id="'ap' + 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-tooltip
:target="'ap' + entry.object.value"
triggers="hover"
boundary="window"
>{{ entry.object.value }}</b-tooltip
>
</b-list-group-item>
<button
class="list-group-item btn-danger removeButton"
......@@ -43,7 +46,14 @@
<b-textarea
class="rdfView"
v-show="!interactive"
:value="definition.toCanonical()"
:value="textRepresentation"
@input="textDefinitionUpdate"
debounce="2000"
/>
<b-form-select
v-show="!interactive"
v-model="mimeType"
:options="mimeTypes"
/>
</div>
</template>
......@@ -58,8 +68,11 @@ import Vue, { PropType } from "vue";
import factory from "rdf-ext";
import {
getApplicationProfileLabel,
getLocaleObject,
getApplicationProfileProperty,
getObject,
getRDFContentTypes,
rdfsPrefix,
serializeRDFDefinition,
shPrefix,
} from "@/util/linkedData";
......@@ -67,9 +80,19 @@ import {
export default Vue.extend({
name: "Representation",
created() {
this.getMimeTypes();
this.serializeDefinition();
},
data() {
return {
factory: factory,
mimeType: "text/turtle",
mimeTypes: [
{ value: "application/ld+json", text: "application/ld+json" },
{ value: "text/turtle", text: "text/turtle" },
],
textRepresentation: "",
};
},
props: {
......@@ -92,9 +115,10 @@ export default Vue.extend({
},
computed: {
applicationProfileLabel(): string {
return getApplicationProfileLabel(
return getApplicationProfileProperty(
this.applicationProfile,
this.definition,
rdfsPrefix + "label",
this.$root.$i18n.locale
);
},
......@@ -102,12 +126,26 @@ export default Vue.extend({
return this.definition.match(null, factory.namedNode(shPrefix + "path"));
},
},
watch: {
definition() {
this.serializeDefinition();
},
mimeType() {
this.serializeDefinition();
},
},
methods: {
async serializeDefinition() {
this.textRepresentation = await serializeRDFDefinition(
this.definition,
this.mimeType
);
},
openEditModal() {
this.$emit("openEditModal");
},
getEntryLabel(entry: Quad) {
const localeObject = getLocaleObject(
const localeObject = getObject(
entry.subject,
factory.namedNode(shPrefix + "name"),
this.definition,
......@@ -119,12 +157,22 @@ export default Vue.extend({
// If no name is present, return the entry to render the path url
return [entry];
},
async getMimeTypes() {
const contentTypes = await getRDFContentTypes();
this.mimeTypes = [];
for (const contentType of contentTypes) {
this.mimeTypes.push({ value: contentType, text: contentType });
}
},
removeProperty(entry: Quad) {
this.$emit("removeProperty", entry.subject);
},
selectProperty(entry: Quad) {
this.$emit("selectProperty", entry.subject);
},
textDefinitionUpdate(textDefinition: string) {
this.$emit("textDefinitionUpdate", textDefinition, this.mimeType);
},
},
});
</script>
......
<template>
<b-list-group>
<b-list-group-item
class="d-flex justify-content-between align-items-center draggable"
button
draggable
v-for="vocabularyTerm in vocabularyTerms"
:key="vocabularyTerm.uri"
@click="addVocabularyTerm(vocabularyTerm)"
@dragstart="startDrag($event, vocabularyTerm)"
>
{{ vocabularyTerm.prefix !== null ? vocabularyTerm.prefix + ":" : ""
}}{{ vocabularyTerm.property }}
<b-badge variant="success" pill class="add">+</b-badge>
</b-list-group-item>
</b-list-group>
<div>
<b-list-group>
<b-list-group-item
class="d-flex justify-content-between align-items-center draggable"
button
draggable
v-for="vocabularyTerm in vocabularyTerms"
:key="vocabularyTerm.uri"
:id="'vocab' + vocabularyTerm.uri"
@click="addVocabularyTerm(vocabularyTerm)"
@dragstart="startDrag($event, vocabularyTerm)"
>
{{
vocabularyTerm.prefix !== undefined
? vocabularyTerm.prefix + ":"
: ""
}}{{ vocabularyTerm.property }}
<b-tooltip
:target="'vocab' + vocabularyTerm.uri"
triggers="hover"
placement="left"
boundary="window"
>{{ vocabularyTerm.uri }}</b-tooltip
>
<b-badge variant="success" pill class="add">+</b-badge>
</b-list-group-item>
</b-list-group>
<div v-if="vocabularyTerms.length === 0" class="grayed">
{{ $t("emptyVocabList") }}
</div>
</div>
</template>
<script lang="ts">
......@@ -52,4 +68,7 @@ export default Vue.extend({
.draggable:hover {
cursor: grab;
}
.grayed {
color: gray;
}
</style>
This diff is collapsed.
......@@ -5,6 +5,7 @@ export default {
Save: "Speichern",
Cancel: "Abbrechen",
Continue: "Weiter",
Submit: "Senden",
availableapplicationprofiles: "Verfügbare Applikationsprofile",
......@@ -15,9 +16,13 @@ export default {
searchAP: "Suche Applikationsprofile",
searchVoc: "Suche Vokabularterme",
description: "Beschreibung",
descriptionDescription: "rdfs:comment",
discipline: "Disziplin",
label: "Titel",
labelDescription: "rdfs:label",
node: "Abgeleitet von",
nodeDescription: "sh:node",
targetClass: "Zielklasse",
targetClassDescription: "sh:targetClass",
url: "URL des Applikationsprofils",
......@@ -25,11 +30,21 @@ export default {
storeModalTitle: "Neues Applikationsprofil speichern: {apName}",
editModalTitle: "Applikationsprofile editieren: {apName}",
storeToastDescription: "Ihr Applikationsprofil wurde gespeichert!",
storeToastTitle: "Applikationsprofil speichern",
emptyVocabList:
"Es wurde kein passender Vokabularterm zu dieser Eingabe gefunden.",
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: {
class: {
label: "Class",
description: "SHACL sh:class",
},
maxCount: {
label: "Maximum",
description: "SHACL sh:maxCount",
......@@ -42,5 +57,9 @@ export default {
label: "Name",
description: "SHACL sh:name",
},
node: {
label: "Node",
description: "SHACL sh:node",
},
},
};
......@@ -5,6 +5,7 @@ export default {
Save: "Save",
Cancel: "Cancel",
Continue: "Continue",
Submit: "Submit",
availableapplicationprofiles: "Available Application Profiles",
......@@ -15,9 +16,13 @@ export default {
searchAP: "Search Application Profiles",
searchVoc: "Search Vocabulary Terms",
description: "Description",
descriptionDescription: "rdfs:comment",
discipline: "Discipline",
label: "Title",
labelDescription: "rdfs:label",
node: "Inherits from",
nodeDescription: "sh:node",
targetClass: "Target Class",
targetClassDescription: "sh:targetClass",
url: "Application Profile URL",
......@@ -25,11 +30,21 @@ export default {
storeModalTitle: "Save New Application Profile: {apName}",
editModalTitle: "Edit Application Profile: {apName}",
storeToastDescription: "Your application profile has been saved!",
storeToastTitle: "Saving Application Profile",
emptyVocabList:
"No fitting vocabulary term has been found for the given input.",
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: {
class: {
label: "Class",
description: "SHACL sh:class",
},
maxCount: {
label: "Maximum",
description: "SHACL sh:maxCount",
......@@ -42,5 +57,9 @@ export default {
label: "Name",
description: "SHACL sh:name",
},
node: {
label: "Node",
description: "SHACL sh:node",
},
},
};
......@@ -2,7 +2,7 @@ declare interface ApplicationProfile {
name: string;
base_url: string;
definition: string;
mimeType: string = "application/ld+json";
mimeType?: string = "application/ld+json";
}
export = ApplicationProfile;
declare interface NodeShapeProperty {
literal: boolean;
locale: boolean;
multiline: boolean;
name: string;
type: string;
url: string;
value: string;
}
......
export interface TerminologyServiceResponse {
responseHeader: ResponseHeader;
response: Response;
facet_counts: FacetCounts;
highlighting: { [key: string]: Highlighting };
expanded: { [key: string]: Response };
}
export interface Response {
numFound: number;
start: number;
docs: Doc[];
}
export interface Doc {
id: string;
iri: string;
short_form: string;
label: string;
ontology_name: string;
ontology_prefix: string;
type: Type;
is_defining_ontology: boolean;
obo_id?: string;
}
export enum Type {
Class = "class",
Property = "property",
}
export interface FacetCounts {
facet_queries: unknown;
facet_fields: FacetFields;
facet_dates: unknown;
facet_ranges: unknown;
facet_intervals: unknown;
facet_heatmaps: unknown;
}
export interface FacetFields {
ontology_name: Array<number | string>;
ontology_prefix: Array<number | string>;
type: Array<number | string>;
subset: Array<number | string>;
is_defining_ontology: Array<number | string>;
is_obsolete: Array<number | string>;
}
export interface Highlighting {
label: string[];
}
export interface ResponseHeader {
status: number;
QTime: number;
params: Params;
}
export interface Params {
"facet.field": string[];
hl: string;
fl: string;
start: string;
fq: string[];
rows: string;
"hl.simple.pre": string;
bq: string;
q: string;
defType: string;
expand: string;
"expand.rows": string;
"hl.simple.post": string;
qf: string;
"hl.fl": string[];
facet: string;
wt: string;
}
declare interface VocabularyTerm {
property: string;
vocabulary: string;
vocabulary?: string;
uri: string;
label: ?string;
prefix: ?string;
definition: ?Array<unknown>;
label?: string;
prefix?: string;
definition?: Array<unknown>;
}
export = VocabularyTerm;
import {
searchApplicationProfiles as mockupSearchApplicationProfiles,
searchVocabularyTerms as mockupSearchVocabularyTerms,
storeApplicationProfile as mockupStoreApplicationProfile,
} from "@/util/api-connection/mockup-api-connection";
import { searchVocabularyTerms as aimsSearchVocabularyTerms } from "@/util/api-connection/aims-api-connection";
// TODO: Remove eslint disabling when a complete other implementation exists
// eslint-disable-next-line
let searchApplicationProfiles = mockupSearchApplicationProfiles;
// eslint-disable-next-line
let searchVocabularyTerms = mockupSearchVocabularyTerms;
let searchVocabularyTerms = aimsSearchVocabularyTerms;
// eslint-disable-next-line
let storeApplicationProfile = mockupStoreApplicationProfile;
......@@ -18,6 +19,7 @@ if (currentLocation.includes("coscine")) {
// TODO: Use different implementation
} else if (currentLocation.includes("aims")) {
// TODO: Use different implementation
searchVocabularyTerms = aimsSearchVocabularyTerms;
} else if (currentLocation.includes("shacl.kg")) {
// TODO: Use different implementation
}
......
import { TerminologyServiceResponse } from "@/types/terminologyServiceResponse";
import VocabularyTerm from "@/types/vocabularyTerm";
import axios from "axios";
export async function searchVocabularyTerms(
query: string
): Promise<Array<VocabularyTerm>> {
const response = await axios.get(
`https://service.tib.eu/ts4tib/api/search?q=${query}&groupField=iri&start=0&type=property`
);
const terminologyServiceResponse =
response.data as TerminologyServiceResponse;
return terminologyServiceResponse.response.docs.map((doc) => {
return {
label: doc.label,
property: doc.short_form,
uri: doc.iri,
} as VocabularyTerm;
});
}
import factory from "rdf-ext";
import DatasetExt from "rdf-ext/lib/Dataset";
import ApplicationProfile from "@/types/applicationProfile";
import ApplicationProfileImpl from "@/classes/ApplicationProfileImpl";
import NodeShapeProperty from "@/types/nodeShapeProperty";
import VocabularyTerm from "@/types/vocabularyTerm";
import rdfParser from "rdf-parse";
import rdfSerializer from "rdf-serialize";
import stringifyStream from "stream-to-string";
import { Readable } from "stream";
import {
Literal,
......@@ -21,6 +24,10 @@ 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 getRDFContentTypes(): Promise<string[]> {
return await rdfSerializer.getContentTypes();
}
export async function parseRDFDefinition(
definition: string,
contentType = "application/ld+json",
......@@ -43,6 +50,16 @@ export async function parseRDFDefinition(
return dataset;
}
export async function serializeRDFDefinition(
dataset: DatasetExt,
contentType = "application/ld+json"
): Promise<string> {
const textStream = rdfSerializer.serialize(dataset.toStream(), {
contentType: contentType,
});
return await stringifyStream(textStream);
}
export async function parseApplicationProfile(
applicationProfile: ApplicationProfile
): Promise<DatasetExt> {
......@@ -139,7 +156,7 @@ export function addVocabularyTermToDataset(
)
);
if (vocabularyTerm.label !== null) {
if (vocabularyTerm.label !== undefined) {
dataset.add(
factory.quad(
blankNode,
......@@ -161,26 +178,21 @@ export function removeVocabularyTermFromDataset(
}
export function getObject(
subject: Quad_Subject,
predicate: Quad_Predicate,
dataset: DatasetExt
): Quad[] {
return dataset.match(subject, predicate).toArray();
}
export function getLocaleObject(
subject: Quad_Subject,
predicate: Quad_Predicate,
dataset: DatasetExt,
locale: string
locale?: string
): Quad[] {
const sortedLabelList = getObject(subject, predicate, dataset).sort((quad) =>
(quad.object as Literal).language === locale ? -1 : 1
);
if (sortedLabelList.length > 0) {
return [sortedLabelList[0]];
let results = dataset.match(subject, predicate).toArray();
if (locale !== undefined) {
results = results.sort((quad) =>
(quad.object as Literal).language === locale ? -1 : 1
);
if (results.length > 0) {
return [results[0]];
}
}
return sortedLabelList;
return results;
}
export function updateObject(
......@@ -208,19 +220,20 @@ export function updateLocaleObject(
dataset.add(factory.quad(subject, predicate, object));
}
export function getApplicationProfileLabel(
export function getApplicationProfileProperty(
applicationProfile: ApplicationProfile,
dataset: DatasetExt,
locale: string
property: string,
locale?: string
): string {
const localeObject = getLocaleObject(
const object = getObject(
factory.namedNode(applicationProfile.base_url),
factory.namedNode(rdfsPrefix + "label"),
factory.namedNode(property),
dataset,
locale
);
if (localeObject.length > 0) {
return localeObject[0].object.value;
if (object.length > 0) {
return object[0].object.value;
}
return "";
......@@ -268,7 +281,7 @@ export function editNodeShapeProperties(
const predicate = factory.namedNode(property.url);
let object: Quad_Object;
if (property.literal) {
if (property.type !== "url") {
object = factory.literal(
property.value,
property.locale ? locale : undefined
......@@ -277,7 +290,7 @@ export function editNodeShapeProperties(
object = factory.namedNode(property.value);
}
if (property.literal && property.locale) {
if (property.type !== "url" && property.locale) {
for (const match of dataset.match(baseUrlNode, predicate)) {
if ((match.object as Literal).language === locale) {
dataset.delete(match);
......@@ -313,3 +326,36 @@ export function changeBaseUrl(
}
applicationProfile.base_url = url;
}
export async function setApplicationProfileBasedOnDataset(
dataset: DatasetExt
): Promise<ApplicationProfile> {
const applicationProfile = new ApplicationProfileImpl(
"",
"",
await serializeRDFDefinition(dataset, "text/turtle"),
"text/turtle"
);
const baseUrls = dataset.match(
null,
factory.namedNode(rdfPrefix + "type"),
factory.namedNode(shPrefix + "NodeShape")
);
if (baseUrls.length > 0) {
for (const match of baseUrls) {
applicationProfile.base_url = match.subject.value;
break;
}
} else {
ensureApplicationProfileBaseUrl(applicationProfile, dataset);
}
const names = getObject(
factory.namedNode(applicationProfile.base_url),
factory.namedNode(rdfsPrefix + "label"),
dataset
);
if (names.length > 0) {
applicationProfile.name = names[0].object.value;
}
return applicationProfile;
}
......@@ -26,6 +26,7 @@
@openEditModal="openEditModal"
@removeProperty="removeProperty"
@selectProperty="selectProperty"
@textDefinitionUpdate="textDefinitionUpdate"
/>
</b-card>
</b-col>
......@@ -109,6 +110,12 @@ export default Vue.extend({
selectProperty(property: Quad_Subject) {
this.$emit("selectProperty", property);
},
textDefinitionUpdate(
textDefinition: string,
mimeType = "application/n-triples"
) {
this.$emit("textDefinitionUpdate", textDefinition, mimeType);
},
updateDefinition() {
this.$emit("updateDefinition");
},
......