Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found
Select Git revision
  • 2.11.0-privacyPolicy
  • APIv2
  • Docs/Setup
  • Experiment/fix-debugging
  • Experimental/Heinrichs-cypress
  • Feature/xxxx-turnOffDataPub
  • Fix/xxxx-ToS400Error
  • Fix/xxxx-migrateLogin
  • Fix/xxxx-tokenUploadButton
  • Hotfix/0038-correctDownload
  • Hotfix/1917-PublicFilesVisibility
  • Hotfix/1963-fixOrganizationField
  • Hotfix/2015-PublicFilesVisibility
  • Hotfix/2130-uiv2ContactChange
  • Hotfix/2144-invitationCall
  • Hotfix/2150-fixUpload
  • Hotfix/2160-userOrgsInst
  • Hotfix/2190-requiredFieldsForUserProfile
  • Hotfix/2196-RCVTableTranslation
  • Hotfix/2212-fixFiles
  • Hotfix/2226-userProfileSaveButton
  • Hotfix/2232-dependencyFix
  • Hotfix/2233-fixMe
  • Hotfix/2258-saveButtonWorksAsExpected
  • Hotfix/2296-selectedValuesNotReturned
  • Hotfix/2308-defaultLicense
  • Hotfix/2335-fixingSearchRCV
  • Hotfix/2353-dropShape
  • Hotfix/2370-fixDeleteButton
  • Hotfix/2378-linkedFix
  • Hotfix/2379-filesDragAndDrop
  • Hotfix/2382-guestStillBuggy
  • Hotfix/2384-guestsAndLinked
  • Hotfix/2427-adminTrouble
  • Hotfix/2459-EncodingPath
  • Hotfix/2465-orcidLink
  • Hotfix/2465-orcidLink-v1.25.1
  • Hotfix/2504-formGen
  • Hotfix/2541-resCreate
  • Hotfix/2601-correctMetadataIdentity
  • Hotfix/2611-feedback
  • Hotfix/2618-turtle
  • Hotfix/2681-validationErrors
  • Hotfix/2684-correctEncoding
  • Hotfix/2684-fixSubMetadata
  • Hotfix/2713-validateEntryName
  • Hotfix/2734-allowEmptyLicense
  • Hotfix/2765-encodingAgain
  • Hotfix/2852-adaptTextForToSUi
  • Hotfix/2853-optimizationV4
  • Hotfix/2943-reloadingResources
  • Hotfix/2943-searchHighlighting
  • Hotfix/2957-styleAndUpgrade
  • Hotfix/2971-fixTextInDataPub
  • Hotfix/2989-cookieLength
  • Hotfix/662-keepSidebarExpanded
  • Hotfix/xxxx-correctLinking
  • Hotfix/xxxx-folderRecursive
  • Hotfix/xxxx-fullscreenCss
  • Hotfix/xxxx-homepageDisplay
  • Hotfix/xxxx-liveReleaseFixes
  • Hotfix/xxxx-partnerProjects
  • Hotfix/xxxx-workingFileIndex
  • Issue/1782-structualDataIntegration
  • Issue/1792-newMetadataStructure
  • Issue/1822-coscineUIv2App
  • Issue/1824-componentsUIv2
  • Issue/1824-routerAdditions
  • Issue/1825-codeQualityPipelines
  • Issue/1833-newLogin
  • Issue/1843-multipleFilesValidation
  • Issue/1860-searchScoping
  • Issue/1861-searchMetadata
  • Issue/1862-searchFacets
  • Issue/1863-paginationForSearch
  • Issue/1926-userProfile
  • Issue/1927-projectAppMigration
  • Issue/1928-sidebarmenuAddition
  • Issue/1929-vuexToPinia
  • Issue/1938-internalHandling
  • Issue/1951-quotaImplementation
  • Issue/1953-owlImports
  • Issue/1957-resourceAppMigration
  • Issue/1957-resourceAppMigrationNew
  • Issue/1962-SearchAppUI2
  • Issue/1964-tokenExpiryUIv2
  • Issue/1965-userListMigration
  • Issue/1970-breadcrumbs
  • Issue/1971-projectEditCreateMigration
  • Issue/1972-homeDepot
  • Issue/1974-shibbolethLogout
  • Issue/1976-resouceCreationVaildEmail
  • Issue/1979-supportAdminUIv2Migration
  • Issue/1980-userManagement
  • Issue/1985-adaptSidebar
  • Issue/2002-migrateResourceCreate
  • Issue/2003-resourceSettings
  • Issue/2008-quotaManagement
  • Issue/2011-pathConfig
  • Issue/2016-BannerMigration
  • 1.28.0-pilot
  • v1.0.0
  • v1.1.0
  • v1.10.0
  • v1.10.1
  • v1.10.2
  • v1.10.3
  • v1.11.0
  • v1.11.1
  • v1.11.2
  • v1.11.3
  • v1.11.4
  • v1.11.5
  • v1.11.6
  • v1.11.7
  • v1.12.0
  • v1.13.0
  • v1.14.0
  • v1.14.1
  • v1.14.2
  • v1.14.3
  • v1.15.0
  • v1.15.1
  • v1.16.0
  • v1.16.1
  • v1.16.2
  • v1.16.3
  • v1.17.0
  • v1.17.1
  • v1.17.2
  • v1.18.0
  • v1.18.1
  • v1.19.0
  • v1.2.0
  • v1.20.0
  • v1.20.1
  • v1.20.2
  • v1.20.3
  • v1.20.4
  • v1.20.5
  • v1.21.0
  • v1.22.0
  • v1.22.1
  • v1.22.2
  • v1.23.0
  • v1.23.1
  • v1.23.2
  • v1.23.3
  • v1.23.4
  • v1.23.5
  • v1.23.6
  • v1.23.6-patch-2417-2427
  • v1.24.0
  • v1.24.1
  • v1.25.0
  • v1.25.1
  • v1.26.0
  • v1.26.1
  • v1.27.0
  • v1.27.1
  • v1.27.1-pilot
  • v1.28.0
  • v1.29.0
  • v1.29.1
  • v1.29.2
  • v1.3.0
  • v1.30.0
  • v1.30.1
  • v1.30.2
  • v1.31.0
  • v1.32.0
  • v1.4.0
  • v1.4.1
  • v1.5.0
  • v1.6.0
  • v1.6.1
  • v1.6.2
  • v1.7.0
  • v1.8.0
  • v1.8.1
  • v1.8.2
  • v1.9.0
  • v2.0.0
  • v2.1.0
  • v2.10.0
  • v2.10.1
  • v2.11.0
  • v2.12.0
  • v2.12.1
  • v2.12.2
  • v2.12.3
  • v2.12.4
  • v2.12.5
  • v2.13.0
  • v2.13.1
  • v2.13.2
  • v2.13.3
  • v2.13.4
  • v2.14.0
  • v2.15.0
200 results

Target

Select target project
  • coscine/frontend/apps/ui
1 result
Select Git revision
  • 2.11.0-privacyPolicy
  • APIv2
  • Docs/Setup
  • Experiment/fix-debugging
  • Experimental/Heinrichs-cypress
  • Feature/xxxx-turnOffDataPub
  • Fix/xxxx-ToS400Error
  • Fix/xxxx-migrateLogin
  • Fix/xxxx-tokenUploadButton
  • Hotfix/0038-correctDownload
  • Hotfix/1917-PublicFilesVisibility
  • Hotfix/1963-fixOrganizationField
  • Hotfix/2015-PublicFilesVisibility
  • Hotfix/2130-uiv2ContactChange
  • Hotfix/2144-invitationCall
  • Hotfix/2150-fixUpload
  • Hotfix/2160-userOrgsInst
  • Hotfix/2190-requiredFieldsForUserProfile
  • Hotfix/2196-RCVTableTranslation
  • Hotfix/2212-fixFiles
  • Hotfix/2226-userProfileSaveButton
  • Hotfix/2232-dependencyFix
  • Hotfix/2233-fixMe
  • Hotfix/2258-saveButtonWorksAsExpected
  • Hotfix/2296-selectedValuesNotReturned
  • Hotfix/2308-defaultLicense
  • Hotfix/2335-fixingSearchRCV
  • Hotfix/2353-dropShape
  • Hotfix/2370-fixDeleteButton
  • Hotfix/2378-linkedFix
  • Hotfix/2379-filesDragAndDrop
  • Hotfix/2382-guestStillBuggy
  • Hotfix/2384-guestsAndLinked
  • Hotfix/2427-adminTrouble
  • Hotfix/2459-EncodingPath
  • Hotfix/2465-orcidLink
  • Hotfix/2465-orcidLink-v1.25.1
  • Hotfix/2504-formGen
  • Hotfix/2541-resCreate
  • Hotfix/2601-correctMetadataIdentity
  • Hotfix/2611-feedback
  • Hotfix/2618-turtle
  • Hotfix/2681-validationErrors
  • Hotfix/2684-correctEncoding
  • Hotfix/2684-fixSubMetadata
  • Hotfix/2713-validateEntryName
  • Hotfix/2734-allowEmptyLicense
  • Hotfix/2765-encodingAgain
  • Hotfix/2852-adaptTextForToSUi
  • Hotfix/2853-optimizationV4
  • Hotfix/2943-reloadingResources
  • Hotfix/2943-searchHighlighting
  • Hotfix/2957-styleAndUpgrade
  • Hotfix/2971-fixTextInDataPub
  • Hotfix/2989-cookieLength
  • Hotfix/662-keepSidebarExpanded
  • Hotfix/xxxx-correctLinking
  • Hotfix/xxxx-folderRecursive
  • Hotfix/xxxx-fullscreenCss
  • Hotfix/xxxx-homepageDisplay
  • Hotfix/xxxx-liveReleaseFixes
  • Hotfix/xxxx-partnerProjects
  • Hotfix/xxxx-workingFileIndex
  • Issue/1782-structualDataIntegration
  • Issue/1792-newMetadataStructure
  • Issue/1822-coscineUIv2App
  • Issue/1824-componentsUIv2
  • Issue/1824-routerAdditions
  • Issue/1825-codeQualityPipelines
  • Issue/1833-newLogin
  • Issue/1843-multipleFilesValidation
  • Issue/1860-searchScoping
  • Issue/1861-searchMetadata
  • Issue/1862-searchFacets
  • Issue/1863-paginationForSearch
  • Issue/1926-userProfile
  • Issue/1927-projectAppMigration
  • Issue/1928-sidebarmenuAddition
  • Issue/1929-vuexToPinia
  • Issue/1938-internalHandling
  • Issue/1951-quotaImplementation
  • Issue/1953-owlImports
  • Issue/1957-resourceAppMigration
  • Issue/1957-resourceAppMigrationNew
  • Issue/1962-SearchAppUI2
  • Issue/1964-tokenExpiryUIv2
  • Issue/1965-userListMigration
  • Issue/1970-breadcrumbs
  • Issue/1971-projectEditCreateMigration
  • Issue/1972-homeDepot
  • Issue/1974-shibbolethLogout
  • Issue/1976-resouceCreationVaildEmail
  • Issue/1979-supportAdminUIv2Migration
  • Issue/1980-userManagement
  • Issue/1985-adaptSidebar
  • Issue/2002-migrateResourceCreate
  • Issue/2003-resourceSettings
  • Issue/2008-quotaManagement
  • Issue/2011-pathConfig
  • Issue/2016-BannerMigration
  • 1.28.0-pilot
  • v1.0.0
  • v1.1.0
  • v1.10.0
  • v1.10.1
  • v1.10.2
  • v1.10.3
  • v1.11.0
  • v1.11.1
  • v1.11.2
  • v1.11.3
  • v1.11.4
  • v1.11.5
  • v1.11.6
  • v1.11.7
  • v1.12.0
  • v1.13.0
  • v1.14.0
  • v1.14.1
  • v1.14.2
  • v1.14.3
  • v1.15.0
  • v1.15.1
  • v1.16.0
  • v1.16.1
  • v1.16.2
  • v1.16.3
  • v1.17.0
  • v1.17.1
  • v1.17.2
  • v1.18.0
  • v1.18.1
  • v1.19.0
  • v1.2.0
  • v1.20.0
  • v1.20.1
  • v1.20.2
  • v1.20.3
  • v1.20.4
  • v1.20.5
  • v1.21.0
  • v1.22.0
  • v1.22.1
  • v1.22.2
  • v1.23.0
  • v1.23.1
  • v1.23.2
  • v1.23.3
  • v1.23.4
  • v1.23.5
  • v1.23.6
  • v1.23.6-patch-2417-2427
  • v1.24.0
  • v1.24.1
  • v1.25.0
  • v1.25.1
  • v1.26.0
  • v1.26.1
  • v1.27.0
  • v1.27.1
  • v1.27.1-pilot
  • v1.28.0
  • v1.29.0
  • v1.29.1
  • v1.29.2
  • v1.3.0
  • v1.30.0
  • v1.30.1
  • v1.30.2
  • v1.31.0
  • v1.32.0
  • v1.4.0
  • v1.4.1
  • v1.5.0
  • v1.6.0
  • v1.6.1
  • v1.6.2
  • v1.7.0
  • v1.8.0
  • v1.8.1
  • v1.8.2
  • v1.9.0
  • v2.0.0
  • v2.1.0
  • v2.10.0
  • v2.10.1
  • v2.11.0
  • v2.12.0
  • v2.12.1
  • v2.12.2
  • v2.12.3
  • v2.12.4
  • v2.12.5
  • v2.13.0
  • v2.13.1
  • v2.13.2
  • v2.13.3
  • v2.13.4
  • v2.14.0
  • v2.15.0
200 results
Show changes

Commits on Source 3

Showing
with 1480 additions and 820 deletions
...@@ -25,6 +25,7 @@ module.exports = { ...@@ -25,6 +25,7 @@ module.exports = {
{ "allowWholeFile": true } { "allowWholeFile": true }
], ],
"@typescript-eslint/no-unused-vars": ["warn", { "argsIgnorePattern": "^_" }], // will only ignore variables that start with an underscore _ "@typescript-eslint/no-unused-vars": ["warn", { "argsIgnorePattern": "^_" }], // will only ignore variables that start with an underscore _
"@typescript-eslint/no-inferrable-types": "off", // this enforces the removal of the type declarations for variables that are initialized with a value (e.g. let foo: string = "foo")
"vue/multi-word-component-names": "off" "vue/multi-word-component-names": "off"
}, },
} }
{ {
"name": "ui", "name": "ui",
"version": "2.0.0", "version": "2.1.0",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
...@@ -13,7 +13,7 @@ ...@@ -13,7 +13,7 @@
"coverage": "vitest run --coverage" "coverage": "vitest run --coverage"
}, },
"dependencies": { "dependencies": {
"@coscine/api-client": "^3.0.0", "@coscine/api-client": "^3.1.0",
"@coscine/form-generator": "^3.2.2", "@coscine/form-generator": "^3.2.2",
"@dynamic-mapper/mapper": "^1.10.2", "@dynamic-mapper/mapper": "^1.10.2",
"@pinia/testing": "^0.1.2", "@pinia/testing": "^0.1.2",
...@@ -92,6 +92,7 @@ ...@@ -92,6 +92,7 @@
"vite": "^4.3.9", "vite": "^4.3.9",
"vite-aliases": "^0.11.2", "vite-aliases": "^0.11.2",
"vite-plugin-node-polyfills": "^0.9.0", "vite-plugin-node-polyfills": "^0.9.0",
"vite-plugin-rewrite-all": "^1.0.1",
"vite-plugin-windicss": "^1.9.0", "vite-plugin-windicss": "^1.9.0",
"vitest": "^0.32.2", "vitest": "^0.32.2",
"vue-template-compiler": "^2.7.14" "vue-template-compiler": "^2.7.14"
......
<template> <template>
<b-container class="mb-3 p-0 w-100">
<b-row no-gutters>
<b-form-group <b-form-group
class="w-100 m-0"
:class="mandatory === true ? 'mandatory' : ''" :class="mandatory === true ? 'mandatory' : ''"
:label-for="labelFor" :label-for="labelFor"
:label-cols-sm="labelColsSm" :label-cols-sm="labelColsSm"
...@@ -10,7 +13,10 @@ ...@@ -10,7 +13,10 @@
<template #loading> <template #loading>
<b-skeleton :type="type" animation="fade"></b-skeleton> <b-skeleton :type="type" animation="fade"></b-skeleton>
</template> </template>
<div class="flex-center">
<slot /> <slot />
</div>
</b-skeleton-wrapper> </b-skeleton-wrapper>
<!-- Label Template --> <!-- Label Template -->
...@@ -34,12 +40,16 @@ ...@@ -34,12 +40,16 @@
</b-popover> </b-popover>
</div> </div>
</template> </template>
</b-form-group>
</b-row>
<b-row no-gutters>
<b-col :sm="labelColsSm" />
<!-- Hint Text --> <!-- Hint Text -->
<div id="hint" class="small text-muted"> <b-col id="hint" class="small text-muted pl-1" align-self="end">
<slot name="hint" /> <slot name="hint" />
</div> </b-col>
</b-form-group> </b-row>
</b-container>
</template> </template>
<script lang="ts"> <script lang="ts">
...@@ -90,4 +100,18 @@ export default defineComponent({ ...@@ -90,4 +100,18 @@ export default defineComponent({
content: " *"; content: " *";
color: #a70619; color: #a70619;
} }
.flex-center {
/* Needed to center the content vertically */
display: flex;
align-items: center;
height: 100%;
width: 100%;
}
#hint {
position: relative;
}
.form-row :deep(.col) {
/* Removes the right padding from the inner col, making it go until the end */
padding: 0 0 0 5px;
}
</style> </style>
<template> <template>
<b-breadcrumb class="breadcrumbs"> <b-breadcrumb class="breadcrumbs">
<!-- Home -->
<b-breadcrumb-item :to="{ name: 'home' }" :active="crumbs.length === 0"> <b-breadcrumb-item :to="{ name: 'home' }" :active="crumbs.length === 0">
<b-icon v-if="crumbs.length === 0" icon="house-fill" aria-hidden="true" /> <b-icon v-if="crumbs.length === 0" icon="house-fill" aria-hidden="true" />
<b-icon v-else icon="house" aria-hidden="true" /> <b-icon v-else icon="house" aria-hidden="true" />
{{ $t(`breadcrumbs.home`) }} {{ $t(`breadcrumbs.home`) }}
</b-breadcrumb-item> </b-breadcrumb-item>
<!-- Rest -->
<b-breadcrumb-item <b-breadcrumb-item
v-for="(crumb, id) in crumbs" v-for="(crumb, id) in crumbs"
:key="id" :key="id"
...@@ -29,6 +32,7 @@ import type { ...@@ -29,6 +32,7 @@ import type {
ProjectDto, ProjectDto,
ResourceDto, ResourceDto,
} from "@coscine/api-client/dist/types/Coscine.Api/api"; } from "@coscine/api-client/dist/types/Coscine.Api/api";
import type { RouteRecord } from "vue-router";
interface RouteLink { interface RouteLink {
to: RawLocation; to: RawLocation;
...@@ -46,72 +50,14 @@ export default defineComponent({ ...@@ -46,72 +50,14 @@ export default defineComponent({
computed: { computed: {
crumbs(): RouteLink[] { crumbs(): RouteLink[] {
let pathArray = this.$route.path.split("/"); // Get the relevant path from the route (ignores everything after "/-")
pathArray = pathArray.filter( const relevantPath = this.$route.path.split("/-")[0];
(path) => // Filter out empty paths and the project and resource path
path.trim() !== "" && path.trim() !== "p" && path.trim() !== "r" let pathArray = this.filterPaths(relevantPath.split("/"));
); pathArray = this.includeRootPath(pathArray);
// Deal with Root Path inclusion let breadcrumbs = this.generateRouteLinks(pathArray);
if (pathArray.length === 1) { breadcrumbs = this.addParentProjects(breadcrumbs);
pathArray.unshift(""); return this.markLastActive(breadcrumbs);
}
const breadcrumbs = pathArray.reduce(
(breadcrumbArray: RouteLink[], path, idx) => {
// Deal with Root Path inclusion
if (path === "") {
return breadcrumbArray;
}
breadcrumbArray.push({
to: {
name: this.$route.matched[idx].name
? this.$route.matched[idx].name
: this.$route.matched[idx].meta.default,
},
text:
this.$route.matched[idx] &&
this.$route.matched[idx].meta &&
this.$route.matched[idx].meta.breadCrumb
? this.$t(
`breadcrumbs.${this.$route.matched[idx].meta.breadCrumb}`,
{
path: path,
projectName: this.project
? this.project.displayName
: path,
resourceName:
this.resource && this.resource.type
? `${this.$t(
"resourceTypes." +
this.resource.type.specificType +
".displayName"
)}: ${this.resource.displayName}`
: path,
}
).toString()
: path,
});
return breadcrumbArray;
},
[]
);
if (this.project && this.parentProjects) {
const parentBreadCrumbs = this.parentProjects.map((parentProject) => {
return {
to: {
name: "project-page",
params: { slug: parentProject.slug },
},
text: this.$t(`breadcrumbs.project.page`, {
projectName: parentProject.displayName,
}).toString(),
} as RouteLink;
});
breadcrumbs.unshift(...parentBreadCrumbs);
}
if (breadcrumbs.length > 0) {
breadcrumbs[breadcrumbs.length - 1].active = true;
}
return breadcrumbs;
}, },
parentProjects(): ProjectDto[] | null { parentProjects(): ProjectDto[] | null {
...@@ -155,7 +101,11 @@ export default defineComponent({ ...@@ -155,7 +101,11 @@ export default defineComponent({
}, },
methods: { methods: {
setDocumentTitle(title = "") { /**
* Set the document title.
* @param {string} [title=""] - The title to set. If empty, the default title will be used.
*/
setDocumentTitle(title: string = "") {
if (title.trim() !== "") { if (title.trim() !== "") {
document.title = this.$t("title.modified", { document.title = this.$t("title.modified", {
title: title, title: title,
...@@ -164,6 +114,126 @@ export default defineComponent({ ...@@ -164,6 +114,126 @@ export default defineComponent({
document.title = this.$t("title.default").toString(); document.title = this.$t("title.default").toString();
} }
}, },
/**
* Filters out unwanted paths from the given array.
* @param {string[]} paths - The array of paths.
* @returns {string[]} - The filtered array of paths.
*/
filterPaths(paths: string[]): string[] {
return paths.filter(
(path) =>
path.trim() !== "" &&
path.trim() !== "p" &&
path.trim() !== "r" &&
path.trim() !== "-"
);
},
/**
* Adds a root path if the path array contains only one element.
* @param {string[]} pathArray - The array of paths.
* @returns {string[]} - The array of paths possibly including the root.
*/
includeRootPath(pathArray: string[]): string[] {
if (pathArray.length === 1) {
pathArray.unshift("");
}
return pathArray;
},
/**
* Generates RouteLink objects based on the path array.
* @param {string[]} pathArray - The array of paths.
* @returns {RouteLink[]} - The array of RouteLink objects.
*/
generateRouteLinks(pathArray: string[]): RouteLink[] {
return pathArray.reduce((breadcrumbArray: RouteLink[], path, idx) => {
if (path === "") return breadcrumbArray;
const routeMatched = this.$route.matched[idx];
if (routeMatched) {
breadcrumbArray.push(this.generateRouteLink(routeMatched, path));
}
return breadcrumbArray;
}, []);
},
/**
* Generates a single RouteLink object.
* @param {RouteRecord} routeMatched - The matched route record.
* @param {string} path - The path segment for this breadcrumb.
* @returns {RouteLink} - The RouteLink object.
*/
generateRouteLink(routeMatched: RouteRecord, path: string): RouteLink {
return {
to: {
name: routeMatched.name
? routeMatched.name
: routeMatched.meta.default,
},
text: routeMatched?.meta?.breadCrumb
? this.$t(`breadcrumbs.${routeMatched.meta.breadCrumb}`, {
path: path,
projectName: this.project ? this.project.displayName : path,
resourceName: this.resource ? this.resourceDisplayName() : path,
}).toString()
: path,
} as RouteLink;
},
/**
* Retrieves the display name of the resource if available.
* @returns {string} - The display name of the resource or an empty string.
*/
resourceDisplayName(): string {
if (this.resource?.displayName) {
if (this.resource.type?.specificType) {
// e.g. "RWTH-RDS-S3: My Resource Name"
return `${this.$t(
"resourceTypes." + this.resource.type.specificType + ".displayName"
)}: ${this.resource.displayName}`;
}
return this.resource.displayName;
}
return "";
},
/**
* Adds parent projects to the breadcrumb array.
* @param {RouteLink[]} breadcrumbs - The existing array of RouteLink objects.
* @returns {RouteLink[]} - The updated array of RouteLink objects.
*/
addParentProjects(breadcrumbs: RouteLink[]): RouteLink[] {
if (this.project && this.parentProjects) {
const parentBreadCrumbs: RouteLink[] = this.parentProjects.map(
(parentProject) => {
return {
to: {
name: "project-page",
params: { slug: parentProject.slug },
},
text: this.$t(`breadcrumbs.project.page`, {
projectName: parentProject.displayName,
}).toString(),
} as RouteLink;
}
);
breadcrumbs.unshift(...parentBreadCrumbs);
}
return breadcrumbs;
},
/**
* Marks the last breadcrumb as active.
* @param {RouteLink[]} breadcrumbs - The existing array of RouteLink objects.
* @returns {RouteLink[]} - The updated array of RouteLink objects.
*/
markLastActive(breadcrumbs: RouteLink[]): RouteLink[] {
if (breadcrumbs.length > 0) {
breadcrumbs[breadcrumbs.length - 1].active = true;
}
return breadcrumbs;
},
}, },
}); });
</script> </script>
......
...@@ -166,6 +166,7 @@ export default defineComponent({ ...@@ -166,6 +166,7 @@ export default defineComponent({
params: { params: {
slug: this.project ? this.project.slug : undefined, slug: this.project ? this.project.slug : undefined,
guid: resource.id, guid: resource.id,
dirTrail: "",
}, },
}, },
icon: "bi bi-archive", icon: "bi bi-archive",
......
export const radarApplicationProfile = `[{"@id":"https://purl.org/coscine/ap/radar/","@graph":[{"@id":"https://purl.org/coscine/ap/radar#subject","http://www.w3.org/ns/shacl#path":[{"@id":"http://purl.org/dc/terms/subject"}],"http://www.w3.org/ns/shacl#order":[{"@value":"3","@type":"http://www.w3.org/2001/XMLSchema#integer"}],"http://www.w3.org/ns/shacl#maxCount":[{"@value":"1","@type":"http://www.w3.org/2001/XMLSchema#integer"}],"http://www.w3.org/ns/shacl#class":[{"@id":"http://www.dfg.de/dfg_profil/gremien/fachkollegien/faecher/"}],"http://www.w3.org/ns/shacl#name":[{"@language":"de","@value":"Fachrichtung"},{"@language":"en","@value":"Subject Area"}]},{"@id":"https://purl.org/coscine/ap/radar#created","http://www.w3.org/ns/shacl#path":[{"@id":"http://purl.org/dc/terms/created"}],"http://www.w3.org/ns/shacl#order":[{"@value":"2","@type":"http://www.w3.org/2001/XMLSchema#integer"}],"http://www.w3.org/ns/shacl#minCount":[{"@value":"1","@type":"http://www.w3.org/2001/XMLSchema#integer"}],"http://www.w3.org/ns/shacl#maxCount":[{"@value":"1","@type":"http://www.w3.org/2001/XMLSchema#integer"}],"http://www.w3.org/ns/shacl#name":[{"@language":"en","@value":"Production Date"},{"@language":"de","@value":"Erstelldatum"}],"http://www.w3.org/ns/shacl#datatype":[{"@id":"http://www.w3.org/2001/XMLSchema#date"}],"http://www.w3.org/ns/shacl#defaultValue":[{"@value":"{TODAY}"}]},{"@id":"https://purl.org/coscine/ap/radar#creator","http://www.w3.org/ns/shacl#path":[{"@id":"http://purl.org/dc/terms/creator"}],"http://www.w3.org/ns/shacl#order":[{"@value":"0","@type":"http://www.w3.org/2001/XMLSchema#integer"}],"http://www.w3.org/ns/shacl#minCount":[{"@value":"1","@type":"http://www.w3.org/2001/XMLSchema#integer"}],"http://www.w3.org/ns/shacl#maxCount":[{"@value":"1","@type":"http://www.w3.org/2001/XMLSchema#integer"}],"http://www.w3.org/ns/shacl#name":[{"@language":"de","@value":"Ersteller"},{"@language":"en","@value":"Creator"}],"http://www.w3.org/ns/shacl#minLength":[{"@value":"1","@type":"http://www.w3.org/2001/XMLSchema#integer"}],"http://www.w3.org/ns/shacl#datatype":[{"@id":"http://www.w3.org/2001/XMLSchema#string"}],"http://www.w3.org/ns/shacl#defaultValue":[{"@value":"{ME}"}]},{"@id":"https://purl.org/coscine/ap/radar#rights","http://www.w3.org/ns/shacl#path":[{"@id":"http://purl.org/dc/terms/rights"}],"http://www.w3.org/ns/shacl#order":[{"@value":"5","@type":"http://www.w3.org/2001/XMLSchema#integer"}],"http://www.w3.org/ns/shacl#maxCount":[{"@value":"1","@type":"http://www.w3.org/2001/XMLSchema#integer"}],"http://www.w3.org/ns/shacl#name":[{"@language":"en","@value":"Rights"},{"@language":"de","@value":"Berechtigung"}],"http://www.w3.org/ns/shacl#datatype":[{"@id":"http://www.w3.org/2001/XMLSchema#string"}]},{"@id":"https://purl.org/coscine/ap/radar#rightsHolder","http://www.w3.org/ns/shacl#path":[{"@id":"http://purl.org/dc/terms/rightsHolder"}],"http://www.w3.org/ns/shacl#order":[{"@value":"6","@type":"http://www.w3.org/2001/XMLSchema#integer"}],"http://www.w3.org/ns/shacl#maxCount":[{"@value":"1","@type":"http://www.w3.org/2001/XMLSchema#integer"}],"http://www.w3.org/ns/shacl#name":[{"@language":"en","@value":"Rightsholder"},{"@language":"de","@value":"Rechteinhaber"}],"http://www.w3.org/ns/shacl#datatype":[{"@id":"http://www.w3.org/2001/XMLSchema#string"}]},{"@id":"https://purl.org/coscine/ap/radar#title","http://www.w3.org/ns/shacl#path":[{"@id":"http://purl.org/dc/terms/title"}],"http://www.w3.org/ns/shacl#order":[{"@value":"1","@type":"http://www.w3.org/2001/XMLSchema#integer"}],"http://www.w3.org/ns/shacl#minCount":[{"@value":"1","@type":"http://www.w3.org/2001/XMLSchema#integer"}],"http://www.w3.org/ns/shacl#maxCount":[{"@value":"1","@type":"http://www.w3.org/2001/XMLSchema#integer"}],"http://www.w3.org/ns/shacl#name":[{"@language":"de","@value":"Titel"},{"@language":"en","@value":"Title"}],"http://www.w3.org/ns/shacl#minLength":[{"@value":"1","@type":"http://www.w3.org/2001/XMLSchema#integer"}],"http://www.w3.org/ns/shacl#datatype":[{"@id":"http://www.w3.org/2001/XMLSchema#string"}]},{"@id":"https://purl.org/coscine/ap/radar#type","http://www.w3.org/ns/shacl#path":[{"@id":"http://purl.org/dc/terms/type"}],"http://www.w3.org/ns/shacl#order":[{"@value":"4","@type":"http://www.w3.org/2001/XMLSchema#integer"}],"http://www.w3.org/ns/shacl#maxCount":[{"@value":"1","@type":"http://www.w3.org/2001/XMLSchema#integer"}],"http://www.w3.org/ns/shacl#class":[{"@id":"http://purl.org/dc/dcmitype/"}],"http://www.w3.org/ns/shacl#name":[{"@language":"en","@value":"Resource"},{"@language":"de","@value":"Ressource"}]},{"@id":"https://purl.org/coscine/ap/radar/","@type":["http://www.w3.org/ns/shacl#NodeShape"],"http://purl.org/dc/terms/license":[{"@id":"http://spdx.org/licenses/MIT"}],"http://purl.org/dc/terms/rights":[{"@value":"Copyright © 2020 IT Center, RWTH Aachen University"}],"http://www.w3.org/ns/shacl#targetClass":[{"@id":"https://purl.org/coscine/ap/radar/"}],"http://www.w3.org/ns/shacl#closed":[{"@value":"true","@type":"http://www.w3.org/2001/XMLSchema#boolean"}],"http://www.w3.org/ns/shacl#property":[{"@id":"https://purl.org/coscine/ap/radar#creator"},{"@id":"https://purl.org/coscine/ap/radar#rights"},{"@id":"https://purl.org/coscine/ap/radar#subject"},{"@id":"https://purl.org/coscine/ap/radar#created"},{"@id":"https://purl.org/coscine/ap/radar#type"},{"@id":"_:b8922734"},{"@id":"https://purl.org/coscine/ap/radar#rightsHolder"},{"@id":"https://purl.org/coscine/ap/radar#title"}],"http://purl.org/dc/terms/title":[{"@language":"en","@value":"radar application profile"}],"http://purl.org/dc/terms/publisher":[{"@id":"https://itc.rwth-aachen.de/"}],"http://purl.org/dc/terms/description":[{"@value":"The RADAR metadata schema v09 was created by the 2018 FIZ Karlsruhe - Leibniz-Institut fuer Informationsinfrastruktur GmbH and has been published under CC BY 4.0."}]},{"@id":"_:b8922734","http://www.w3.org/ns/shacl#path":[{"@id":"http://www.w3.org/1999/02/22-rdf-syntax-ns#type"}]}]}]`; export const baseApplicationProfile = `[{"@id":"_:b6482494","http://www.w3.org/ns/shacl#datatype":[{"@id":"http://www.w3.org/2001/XMLSchema#string"}],"http://www.w3.org/ns/shacl#minCount":[{"@value":"1","@type":"http://www.w3.org/2001/XMLSchema#integer"}],"http://www.w3.org/ns/shacl#order":[{"@value":"1","@type":"http://www.w3.org/2001/XMLSchema#integer"}],"http://www.w3.org/ns/shacl#name":[{"@language":"en","@value":"Creator"},{"@language":"de","@value":"Ersteller"}],"http://www.w3.org/ns/shacl#description":[{"@language":"de","@value":"Wer ist der Ersteller der Daten?"},{"@language":"en","@value":"Who is the creator of the data?"}],"http://www.w3.org/ns/shacl#defaultValue":[{"@value":"{ME}"}],"http://www.w3.org/ns/shacl#minLength":[{"@value":"1","@type":"http://www.w3.org/2001/XMLSchema#integer"}],"http://www.w3.org/ns/shacl#path":[{"@id":"http://purl.org/dc/terms/creator"}]},{"@id":"_:b6482495","http://www.w3.org/ns/shacl#description":[{"@language":"de","@value":"Von welchem Datentyp sind Ihre Daten?"},{"@language":"en","@value":"What type of data is your data?"}],"http://www.w3.org/ns/shacl#order":[{"@value":"4","@type":"http://www.w3.org/2001/XMLSchema#integer"}],"http://www.w3.org/ns/shacl#class":[{"@id":"http://purl.org/dc/dcmitype/"}],"http://www.w3.org/ns/shacl#path":[{"@id":"http://purl.org/dc/terms/type"}],"http://www.w3.org/ns/shacl#name":[{"@language":"de","@value":"Typ"},{"@language":"en","@value":"Type"}]},{"@id":"https://purl.org/coscine/ap/base/","http://purl.org/dc/terms/title":[{"@language":"en","@value":"Base Profile"},{"@language":"de","@value":"Basisprofil"}],"http://purl.org/dc/terms/creator":[{"@value":"Hanna Führ"}],"http://www.w3.org/ns/shacl#closed":[{"@value":"false","@type":"http://www.w3.org/2001/XMLSchema#boolean"}],"@type":["http://www.w3.org/ns/shacl#NodeShape"],"http://www.w3.org/ns/shacl#targetClass":[{"@id":"https://purl.org/coscine/ap/base/"}],"http://www.w3.org/ns/shacl#property":[{"@id":"_:b6482493"},{"@id":"_:b6482496"},{"@id":"_:b6482494"},{"@id":"_:b6482497"},{"@id":"_:b6482495"}],"http://purl.org/dc/terms/description":[{"@language":"de","@value":"Basisprofil, das Grundbausteine enthält"},{"@language":"en","@value":"Base application profile, which contains the basic building blocks"}],"http://purl.org/dc/terms/created":[{"@value":"2023-02-06","@type":"http://www.w3.org/2001/XMLSchema#date"}],"http://purl.org/dc/terms/license":[{"@value":"https://spdx.org/licenses/CC-BY-4.0.html"}]},{"@id":"_:b6482497","http://www.w3.org/ns/shacl#name":[{"@language":"en","@value":"Creation Date"},{"@language":"de","@value":"Erstellungsdatum"}],"http://www.w3.org/ns/shacl#path":[{"@id":"http://purl.org/dc/terms/created"}],"http://www.w3.org/ns/shacl#defaultValue":[{"@value":"{TODAY}"}],"http://www.w3.org/ns/shacl#minCount":[{"@value":"1","@type":"http://www.w3.org/2001/XMLSchema#integer"}],"http://www.w3.org/ns/shacl#datatype":[{"@id":"http://www.w3.org/2001/XMLSchema#date"}],"http://www.w3.org/ns/shacl#description":[{"@language":"en","@value":"When was the data created?"},{"@language":"de","@value":"Wann wurden die Daten erstellt?"}],"http://www.w3.org/ns/shacl#order":[{"@value":"2","@type":"http://www.w3.org/2001/XMLSchema#integer"}],"http://www.w3.org/ns/shacl#maxCount":[{"@value":"1","@type":"http://www.w3.org/2001/XMLSchema#integer"}]},{"@id":"_:b6482493","http://www.w3.org/ns/shacl#description":[{"@language":"en","@value":"Which subject does your data belong to?"},{"@language":"de","@value":"Welcher Fachrichtung gehören Ihre Daten an?"}],"http://www.w3.org/ns/shacl#class":[{"@id":"http://www.dfg.de/dfg_profil/gremien/fachkollegien/faecher/"}],"http://www.w3.org/ns/shacl#order":[{"@value":"3","@type":"http://www.w3.org/2001/XMLSchema#integer"}],"http://www.w3.org/ns/shacl#name":[{"@language":"de","@value":"Fachrichtung"},{"@language":"en","@value":"Subject Area"}],"http://www.w3.org/ns/shacl#path":[{"@id":"http://purl.org/dc/terms/subject"}]},{"@id":"_:b6482496","http://www.w3.org/ns/shacl#name":[{"@language":"de","@value":"Titel"},{"@language":"en","@value":"Title"}],"http://www.w3.org/ns/shacl#datatype":[{"@id":"http://www.w3.org/2001/XMLSchema#string"}],"http://www.w3.org/ns/shacl#minLength":[{"@value":"1","@type":"http://www.w3.org/2001/XMLSchema#integer"}],"http://www.w3.org/ns/shacl#order":[{"@value":"0","@type":"http://www.w3.org/2001/XMLSchema#integer"}],"http://www.w3.org/ns/shacl#minCount":[{"@value":"1","@type":"http://www.w3.org/2001/XMLSchema#integer"}],"http://www.w3.org/ns/shacl#description":[{"@language":"en","@value":"Enter a title to describe your data."},{"@language":"de","@value":"Geben Sie einen Titel an, um Ihre Daten zu beschreiben."}],"http://www.w3.org/ns/shacl#path":[{"@id":"http://purl.org/dc/terms/title"}]}]`;
export const baseApplicationProfileFormat = "application/ld+json";
export const getMetadataResponse = { import type {
data: { FileDto,
metadataStorage: [ MetadataDto,
TreeDataType,
} from "@coscine/api-client/dist/types/Coscine.Api";
export const getMetadataTreeResponse: MetadataDto[] = [
{ {
"https://hdl.handle.net/21.11102/eeb8d803-46a1-49ba-a47c-81cd4f49cd65@path=%2FTesta": version: "1693212042",
'<https://hdl.handle.net/21.11102/eeb8d803-46a1-49ba-a47c-81cd4f49cd65@path=%2FTesta> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <https://purl.org/coscine/ap/radar/> .\r\n<https://hdl.handle.net/21.11102/eeb8d803-46a1-49ba-a47c-81cd4f49cd65@path=%2FTesta> <http://purl.org/dc/terms/created> "2022-02-25"^^<http://www.w3.org/2001/XMLSchema#date> .\r\n<https://hdl.handle.net/21.11102/eeb8d803-46a1-49ba-a47c-81cd4f49cd65@path=%2FTesta> <http://purl.org/dc/terms/creator> "Benedikt Heinrichs"^^<http://www.w3.org/2001/XMLSchema#string> .\r\n<https://hdl.handle.net/21.11102/eeb8d803-46a1-49ba-a47c-81cd4f49cd65@path=%2FTesta> <http://purl.org/dc/terms/title> "Test"^^<http://www.w3.org/2001/XMLSchema#string> .\r\n', availableVersions: ["1693212042"],
definition:
'@base <https://purl.org/coscine/resources/4103cbea-ffa3-40a5-9e5c-b99cc16f0007/folder_1/folder_2/A.txt/@type=metadata&version=1693212042>.\r\n\r\n@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>.\r\n@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#>.\r\n@prefix xsd: <http://www.w3.org/2001/XMLSchema#>.\r\n@prefix ns2: <http://purl.org/dc/terms/>.\r\n\r\n_:b8477997 ns2:created "2023-08-15"^^xsd:date;\r\n ns2:creator "Petar Hristov";\r\n ns2:title "Title inside Form Generator";\r\n a <https://purl.org/coscine/ap/base/>.\r\n',
format: "text/turtle",
path: "folder_1/folder_2/A.txt",
type: "Leaf" as TreeDataType.Leaf,
}, },
{ {
"https://hdl.handle.net/21.11102/eeb8d803-46a1-49ba-a47c-81cd4f49cd65@path=%2FTest": version: "1692777419",
'<https://hdl.handle.net/21.11102/eeb8d803-46a1-49ba-a47c-81cd4f49cd65@path=%2FTest> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <https://purl.org/coscine/ap/radar/> .\r\n<https://hdl.handle.net/21.11102/eeb8d803-46a1-49ba-a47c-81cd4f49cd65@path=%2FTest> <http://purl.org/dc/terms/created> "2021-12-09"^^<http://www.w3.org/2001/XMLSchema#date> .\r\n<https://hdl.handle.net/21.11102/eeb8d803-46a1-49ba-a47c-81cd4f49cd65@path=%2FTest> <http://purl.org/dc/terms/creator> "Benedikt Heinrichs"^^<http://www.w3.org/2001/XMLSchema#string> .\r\n<https://hdl.handle.net/21.11102/eeb8d803-46a1-49ba-a47c-81cd4f49cd65@path=%2FTest> <http://purl.org/dc/terms/title> "Test"^^<http://www.w3.org/2001/XMLSchema#string> .\r\n', availableVersions: ["1692777419"],
definition:
'@base <https://purl.org/coscine/resources/4103cbea-ffa3-40a5-9e5c-b99cc16f0007/file_1.txt/@type=metadata&version=1692777419>.\r\n\r\n@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>.\r\n@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#>.\r\n@prefix xsd: <http://www.w3.org/2001/XMLSchema#>.\r\n@prefix ns2: <http://purl.org/dc/terms/>.\r\n\r\n_:b8425359 ns2:created "2023-08-18"^^xsd:date;\r\n ns2:creator "Petar Hristov";\r\n ns2:title "file_1";\r\n a <https://purl.org/coscine/ap/base/>.\r\n',
format: "text/turtle",
path: "file_1.txt",
type: "Leaf" as TreeDataType.Leaf,
}, },
{ {
"https://hdl.handle.net/21.11102/eeb8d803-46a1-49ba-a47c-81cd4f49cd65@path=%2FcoolerEintrag": version: "1692779210",
'<https://hdl.handle.net/21.11102/eeb8d803-46a1-49ba-a47c-81cd4f49cd65@path=%2FcoolerEintrag> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <https://purl.org/coscine/ap/radar/> .\r\n<https://hdl.handle.net/21.11102/eeb8d803-46a1-49ba-a47c-81cd4f49cd65@path=%2FcoolerEintrag> <http://purl.org/dc/terms/created> "2022-03-07"^^<http://www.w3.org/2001/XMLSchema#date> .\r\n<https://hdl.handle.net/21.11102/eeb8d803-46a1-49ba-a47c-81cd4f49cd65@path=%2FcoolerEintrag> <http://purl.org/dc/terms/creator> "Benedikt Heinrichs"^^<http://www.w3.org/2001/XMLSchema#string> .\r\n<https://hdl.handle.net/21.11102/eeb8d803-46a1-49ba-a47c-81cd4f49cd65@path=%2FcoolerEintrag> <http://purl.org/dc/terms/title> "test"^^<http://www.w3.org/2001/XMLSchema#string> .\r\n', availableVersions: [
}, "1692340745",
{ "1692340906",
"https://hdl.handle.net/21.11102/eeb8d803-46a1-49ba-a47c-81cd4f49cd65@path=%2Freview": "1692340959",
'<https://hdl.handle.net/21.11102/eeb8d803-46a1-49ba-a47c-81cd4f49cd65@path=%2Freview> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <https://purl.org/coscine/ap/radar/> .\r\n<https://hdl.handle.net/21.11102/eeb8d803-46a1-49ba-a47c-81cd4f49cd65@path=%2Freview> <http://purl.org/dc/terms/created> "2022-03-10"^^<http://www.w3.org/2001/XMLSchema#date> .\r\n<https://hdl.handle.net/21.11102/eeb8d803-46a1-49ba-a47c-81cd4f49cd65@path=%2Freview> <http://purl.org/dc/terms/creator> "Benedikt Heinrichs"^^<http://www.w3.org/2001/XMLSchema#string> .\r\n<https://hdl.handle.net/21.11102/eeb8d803-46a1-49ba-a47c-81cd4f49cd65@path=%2Freview> <http://purl.org/dc/terms/title> "MyTest2"^^<http://www.w3.org/2001/XMLSchema#string> .\r\n', "1692341031",
}, "1692779210",
{
"https://hdl.handle.net/21.11102/eeb8d803-46a1-49ba-a47c-81cd4f49cd65@path=%2Feawfaewf":
'<https://hdl.handle.net/21.11102/eeb8d803-46a1-49ba-a47c-81cd4f49cd65@path=%2Feawfaewf> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <https://purl.org/coscine/ap/radar/> .\r\n<https://hdl.handle.net/21.11102/eeb8d803-46a1-49ba-a47c-81cd4f49cd65@path=%2Feawfaewf> <http://purl.org/dc/terms/created> "2022-03-10"^^<http://www.w3.org/2001/XMLSchema#date> .\r\n<https://hdl.handle.net/21.11102/eeb8d803-46a1-49ba-a47c-81cd4f49cd65@path=%2Feawfaewf> <http://purl.org/dc/terms/creator> "Benedikt Heinrichs"^^<http://www.w3.org/2001/XMLSchema#string> .\r\n<https://hdl.handle.net/21.11102/eeb8d803-46a1-49ba-a47c-81cd4f49cd65@path=%2Feawfaewf> <http://purl.org/dc/terms/rights> "awer"^^<http://www.w3.org/2001/XMLSchema#string> .\r\n<https://hdl.handle.net/21.11102/eeb8d803-46a1-49ba-a47c-81cd4f49cd65@path=%2Feawfaewf> <http://purl.org/dc/terms/title> "awerawe"^^<http://www.w3.org/2001/XMLSchema#string> .\r\n',
},
{
"https://hdl.handle.net/21.11102/eeb8d803-46a1-49ba-a47c-81cd4f49cd65@path=%2Fw":
'<https://hdl.handle.net/21.11102/eeb8d803-46a1-49ba-a47c-81cd4f49cd65@path=%2Fw> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <https://purl.org/coscine/ap/radar/> .\r\n<https://hdl.handle.net/21.11102/eeb8d803-46a1-49ba-a47c-81cd4f49cd65@path=%2Fw> <http://purl.org/dc/terms/created> "2022-04-04"^^<http://www.w3.org/2001/XMLSchema#date> .\r\n<https://hdl.handle.net/21.11102/eeb8d803-46a1-49ba-a47c-81cd4f49cd65@path=%2Fw> <http://purl.org/dc/terms/creator> "Benedikt Heinrichs"^^<http://www.w3.org/2001/XMLSchema#string> .\r\n<https://hdl.handle.net/21.11102/eeb8d803-46a1-49ba-a47c-81cd4f49cd65@path=%2Fw> <http://purl.org/dc/terms/title> "a"^^<http://www.w3.org/2001/XMLSchema#string> .\r\n',
},
], ],
fileStorage: [ definition:
{ '@base <https://purl.org/coscine/resources/4103cbea-ffa3-40a5-9e5c-b99cc16f0007/file_0.txt/@type=metadata&version=1692779210>.\r\n\r\n@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>.\r\n@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#>.\r\n@prefix xsd: <http://www.w3.org/2001/XMLSchema#>.\r\n@prefix ns2: <http://purl.org/dc/terms/>.\r\n\r\n_:b8425361 ns2:created "2023-08-18"^^xsd:date;\r\n ns2:creator "Petar Hristov";\r\n ns2:title "Revised";\r\n a <https://purl.org/coscine/ap/base/>.\r\n',
Name: "Testa", format: "text/turtle",
Path: "/Testa", path: "file_0.txt",
Size: 7, type: "Leaf" as TreeDataType.Leaf,
Kind: "file",
Modified: null,
Created: null,
Provider: "linked",
IsFolder: false,
IsFile: true,
Action: {},
},
{
Name: "Test",
Path: "/Test",
Size: 18,
Kind: "file",
Modified: null,
Created: null,
Provider: "linked",
IsFolder: false,
IsFile: true,
Action: {},
}, },
{ {
Name: "coolerEintrag", version: "1693209938",
Path: "/coolerEintrag", availableVersions: ["1693209938"],
Size: 14, definition:
Kind: "file", '@base <https://purl.org/coscine/resources/4103cbea-ffa3-40a5-9e5c-b99cc16f0007/my_folder/file_of_folder.txt/@type=metadata&version=1693209938>.\r\n\r\n@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>.\r\n@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#>.\r\n@prefix xsd: <http://www.w3.org/2001/XMLSchema#>.\r\n@prefix ns2: <http://purl.org/dc/terms/>.\r\n\r\n_:b8477996 ns2:created "2023-08-15"^^xsd:date;\r\n ns2:creator "Petar Hristov";\r\n ns2:title "From Insomnia";\r\n a <https://purl.org/coscine/ap/base/>.\r\n',
Modified: null, format: "text/turtle",
Created: null, path: "my_folder/file_of_folder.txt",
Provider: "linked", type: "Leaf" as TreeDataType.Leaf,
IsFolder: false,
IsFile: true,
Action: {},
}, },
];
export const getFileTreeResponse: FileDto[] = [
{ {
Name: "review", parentDirectory: "",
Path: "/review", name: "folder_1",
Size: 14, size: 0,
Kind: "file", creationDate: "2023-09-01T16:57:48.2525218+02:00",
Modified: null, changeDate: "2023-09-01T16:57:48.2525225+02:00",
Created: null, path: "folder_1/",
Provider: "linked", type: "Tree" as TreeDataType.Tree,
IsFolder: false,
IsFile: true,
Action: {},
}, },
{ {
Name: "eawfaewf", parentDirectory: "",
Path: "/eawfaewf", name: "my_folder",
Size: 14, size: 0,
Kind: "file", creationDate: "2023-09-01T16:57:48.2525245+02:00",
Modified: null, changeDate: "2023-09-01T16:57:48.2525249+02:00",
Created: null, path: "my_folder/",
Provider: "linked", type: "Tree" as TreeDataType.Tree,
IsFolder: false,
IsFile: true,
Action: {},
}, },
{ {
Name: "w", parentDirectory: "",
Path: "/w", name: "file_0.txt",
Size: 9, extension: "txt",
Kind: "file", size: 2513352,
Modified: null, creationDate: "2023-08-23T10:27:20.062+02:00",
Created: null, changeDate: "2023-08-23T10:27:20.062+02:00",
Provider: "linked", path: "file_0.txt",
IsFolder: false, type: "Leaf" as TreeDataType.Leaf,
IsFile: true,
Action: {},
},
],
}, },
}; ];
...@@ -3,7 +3,10 @@ import type { ...@@ -3,7 +3,10 @@ import type {
ResourceState, ResourceState,
} from "@/modules/resource/types"; } from "@/modules/resource/types";
import { parseRDFDefinition } from "@/modules/resource/utils/linkedData"; import { parseRDFDefinition } from "@/modules/resource/utils/linkedData";
import { radarApplicationProfile } from "./metadata/applicationProfile"; import {
baseApplicationProfile,
baseApplicationProfileFormat,
} from "./metadata/applicationProfile";
import { radarFixedValues } from "./metadata/fixedValues"; import { radarFixedValues } from "./metadata/fixedValues";
import { testDiscipline, getTestUser } from "./testUser"; import { testDiscipline, getTestUser } from "./testUser";
import type { import type {
...@@ -40,10 +43,10 @@ export const testResourceType: ResourceTypeInformationDto = { ...@@ -40,10 +43,10 @@ export const testResourceType: ResourceTypeInformationDto = {
export const getTestResource: () => Promise<VisitedResourceObject> = export const getTestResource: () => Promise<VisitedResourceObject> =
async () => { async () => {
const apUrl = "https://purl.org/coscine/ap/radar/"; const apUrl = "https://purl.org/coscine/ap/base/";
const ap = await parseRDFDefinition( const ap = await parseRDFDefinition(
radarApplicationProfile, baseApplicationProfile,
"application/ld+json", baseApplicationProfileFormat,
apUrl apUrl
); );
const resourceObject: VisitedResourceObject = { const resourceObject: VisitedResourceObject = {
......
import { MapperConfiguration, MappingPair } from "@dynamic-mapper/mapper";
import { v4 as uuidv4 } from "uuid";
import type {
TreeDataType,
FileDto,
} from "@coscine/api-client/dist/types/Coscine.Api";
import type {
FileInformation,
FolderInformation,
} from "@/modules/resource/types";
export const TreeDto2FileInformation = new MappingPair<
FileDto,
FileInformation
>();
export const TreeDto2FolderInformation = new MappingPair<
FileDto,
FolderInformation
>();
const configuration = new MapperConfiguration((cfg) => {
cfg.createMap(TreeDto2FileInformation, {
id: (opt) => opt.mapFrom((_) => uuidv4()),
name: (opt) => opt.mapFrom((dto) => dto.name ?? ""),
parentDirectory: (opt) => opt.mapFrom((dto) => dto.parentDirectory ?? ""),
path: (opt) => opt.mapFrom((dto) => dto.path ?? ""),
type: (opt) => opt.mapFrom((_) => "Leaf" as TreeDataType.Leaf),
isFolder: (opt) => opt.mapFrom((_) => false),
createdAt: (opt) => opt.mapFrom((dto) => dto.creationDate ?? undefined),
lastModified: (opt) => opt.mapFrom((dto) => dto.changeDate),
size: (opt) => opt.mapFrom((dto) => dto.size ?? 0),
version: (opt) => opt.mapFrom((_) => `${+new Date()}`),
metadata: (opt) => opt.mapFrom((_) => null), // Set outside of the mapper
});
cfg.createMap(TreeDto2FolderInformation, {
id: (opt) => opt.mapFrom((_) => uuidv4()),
name: (opt) => opt.mapFrom((dto) => dto.name ?? ""),
parentDirectory: (opt) => opt.mapFrom((dto) => dto.parentDirectory ?? ""),
path: (opt) => opt.mapFrom((dto) => dto.path ?? ""),
type: (opt) => opt.mapFrom((_) => "Tree" as TreeDataType.Tree),
isFolder: (opt) => opt.mapFrom((_) => true),
createdAt: (opt) => opt.mapFrom((dto) => dto.creationDate ?? undefined),
lastModified: (opt) => opt.mapFrom((dto) => dto.changeDate),
metadata: (opt) => opt.mapFrom((_) => null), // Set outside of the mapper
});
});
export const treeMapper = configuration.createMapper();
...@@ -173,7 +173,7 @@ export default defineComponent({ ...@@ -173,7 +173,7 @@ export default defineComponent({
toResource(resource: ResourceDto): RawLocation { toResource(resource: ResourceDto): RawLocation {
const route = { const route = {
name: "resource-page", name: "resource-page",
params: { guid: resource.id }, params: { guid: resource.id, dirTrail: "" },
} as RawLocation; } as RawLocation;
return route; return route;
}, },
......
...@@ -70,6 +70,11 @@ export default defineComponent({ ...@@ -70,6 +70,11 @@ export default defineComponent({
this.initialize(); this.initialize();
}, },
beforeDestroy() {
// Set current resource ID to null
this.resourceStore.currentId = null;
},
methods: { methods: {
async initialize() { async initialize() {
await this.apiFetch(this.$router.currentRoute); await this.apiFetch(this.$router.currentRoute);
......
...@@ -28,7 +28,7 @@ ...@@ -28,7 +28,7 @@
</template> </template>
<!-- Dropdown --> <!-- Dropdown -->
<div class="d-flex align-items-center gap-2"> <div class="d-inline-flex align-items-center gap-2 w-100">
<multiselect <multiselect
id="applicationProfiles" id="applicationProfiles"
v-model="selectedApplicationProfile" v-model="selectedApplicationProfile"
...@@ -69,7 +69,7 @@ ...@@ -69,7 +69,7 @@
<b-button <b-button
variant="outline-primary" variant="outline-primary"
name="createAP" name="createAP"
class="d-flex justify-content-between align-items-center" class="d-flex gap-2 justify-content-between align-items-center w-fit"
@click="$bvModal.show('createAPModal')" @click="$bvModal.show('createAPModal')"
> >
<b-icon icon="file-earmark-plus" aria-hidden="true" /> <b-icon icon="file-earmark-plus" aria-hidden="true" />
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
:mandatory="true" :mandatory="true"
:label="$t('page.createResource.configuration.labels.size')" :label="$t('page.createResource.configuration.labels.size')"
> >
<div class="w-100">
<!-- Slider --> <!-- Slider -->
<b-form-input <b-form-input
id="slider" id="slider"
...@@ -22,6 +23,7 @@ ...@@ -22,6 +23,7 @@
{{ $t("page.createResource.configuration.needMore") }} {{ $t("page.createResource.configuration.needMore") }}
</router-link> </router-link>
</div> </div>
</div>
</CoscineFormGroup> </CoscineFormGroup>
</div> </div>
</template> </template>
......
/* Testing imports */ /* Testing imports */
import { createLocalVue, mount } from "@vue/test-utils"; import { type Wrapper, createLocalVue, mount } from "@vue/test-utils";
import { createTestingPinia } from "@pinia/testing"; import { createTestingPinia } from "@pinia/testing";
/* Vue i18n */ /* Vue i18n */
...@@ -15,6 +15,8 @@ import { PiniaVuePlugin } from "pinia"; ...@@ -15,6 +15,8 @@ import { PiniaVuePlugin } from "pinia";
/* Tested Component */ /* Tested Component */
import FilesView from "./FilesView.vue"; import FilesView from "./FilesView.vue";
import VueRouter from "vue-router";
import { routes } from "@/router";
import type Vue from "vue"; import type Vue from "vue";
...@@ -22,7 +24,10 @@ import { getTestUserState } from "@/data/mockup/testUser"; ...@@ -22,7 +24,10 @@ import { getTestUserState } from "@/data/mockup/testUser";
import { getTestResourceState } from "@/data/mockup/testResource"; import { getTestResourceState } from "@/data/mockup/testResource";
import { testProjectState } from "@/data/mockup/testProject"; import { testProjectState } from "@/data/mockup/testProject";
import useResourceStore from "../../store"; import useResourceStore from "../../store";
import { getMetadataResponse } from "@/data/mockup/responses/getMetadata"; import {
getFileTreeResponse,
getMetadataTreeResponse,
} from "@/data/mockup/responses/getMetadata";
function sleep(ms: number) { function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms)); return new Promise((resolve) => setTimeout(resolve, ms));
...@@ -31,10 +36,18 @@ function sleep(ms: number) { ...@@ -31,10 +36,18 @@ function sleep(ms: number) {
/* Create a local Vue instance */ /* Create a local Vue instance */
const localVue = createLocalVue(); const localVue = createLocalVue();
localVue.use(PiniaVuePlugin); localVue.use(PiniaVuePlugin);
localVue.use(VueRouter);
const router = new VueRouter({ routes: routes });
describe("FilesView.vue", () => { // Define the Vue instance type (computed properties)
/* Checks for local storage setting of columns */ interface ResourcePageComponent extends Vue {
test("testLocalStorageSetting", async () => { dirTrail: string;
}
describe("FilesView.vue", async () => {
let wrapper: Wrapper<ResourcePageComponent>;
// Create a mocked pinia instance with initial state
const testingPinia = createTestingPinia({ const testingPinia = createTestingPinia({
createSpy: vitest.fn, createSpy: vitest.fn,
initialState: { initialState: {
...@@ -44,24 +57,35 @@ describe("FilesView.vue", () => { ...@@ -44,24 +57,35 @@ describe("FilesView.vue", () => {
}, },
}); });
// Mock the API calls
const resourceStore = useResourceStore(testingPinia); const resourceStore = useResourceStore(testingPinia);
vi.mocked(resourceStore.getMetadata).mockReturnValue( vi.mocked(resourceStore.getMetadataTree).mockReturnValue(
Promise.resolve(getMetadataResponse) Promise.resolve(getMetadataTreeResponse)
);
vi.mocked(resourceStore.getFileTree).mockReturnValue(
Promise.resolve(getFileTreeResponse)
); );
vi.mocked(resourceStore.getVocabularyInstances).mockReturnValue( vi.mocked(resourceStore.getVocabularyInstances).mockReturnValue(
Promise.resolve({ en: [], de: [] }) Promise.resolve({ en: [], de: [] })
); );
const wrapper = mount(FilesView as unknown as typeof Vue, { beforeEach(() => {
// shallowMount does not work here!
wrapper = mount(FilesView as unknown as typeof Vue, {
pinia: testingPinia, pinia: testingPinia,
router,
i18n, i18n,
localVue, localVue,
}) as Wrapper<ResourcePageComponent>;
}); });
test(
"Should toggle column visibility and persist to local storage",
async () => {
await wrapper.vm.$nextTick(); await wrapper.vm.$nextTick();
// Wait for 1 second until everything is set up // Wait for 1 second until everything is set up
await sleep(1000); await sleep(1000); // Don't remove!
expect(resourceStore.setStoredColumns).toBeCalledTimes(1); expect(resourceStore.setStoredColumns).toBeCalledTimes(1);
...@@ -78,11 +102,16 @@ describe("FilesView.vue", () => { ...@@ -78,11 +102,16 @@ describe("FilesView.vue", () => {
await wrapper.vm.$nextTick(); await wrapper.vm.$nextTick();
// Wait for 1 second until everything is set up // Wait for 1 second until everything is set up
await sleep(1000); await sleep(1000); // Don't remove!
const additionalColumnHeader = wrapper.find(".additionalColumnHeader"); const additionalColumnHeader = wrapper.find(".additionalColumnHeader");
expect(additionalColumnHeader.exists()).toBeTruthy(); expect(additionalColumnHeader.exists()).toBeTruthy();
expect(resourceStore.setStoredColumns).toBeCalledTimes(2); expect(resourceStore.setStoredColumns).toBeCalledTimes(2);
}); },
{
// Override the maximum run time for this test (10 sec), due to the sleep() calls
timeout: 10000,
}
);
}); });
<template> <template>
<div class="DataSource"> <div class="DataSource">
<!-- Files View Header -->
<FilesViewHeader <FilesViewHeader
v-model="filter" v-model="filter"
:is-uploading="isUploading" :is-uploading="isUploading"
@clickFileSelect="clickFileSelect" @clickFileSelect="clickFileSelect"
/> />
<b-row> <b-row>
<!-- Files View Table -->
<b-table <b-table
id="resourceViewTable" id="resourceViewTable"
ref="adaptTable" ref="adaptTable"
...@@ -31,25 +34,31 @@ ...@@ -31,25 +34,31 @@
:empty-filtered-text="$t('page.resource.emptyFilterText')" :empty-filtered-text="$t('page.resource.emptyFilterText')"
@row-selected="onRowSelected" @row-selected="onRowSelected"
> >
<!-- Loading Spinner for Busy State -->
<div slot="table-busy" class="text-center text-danger my-2"> <div slot="table-busy" class="text-center text-danger my-2">
<b-spinner class="align-middle"></b-spinner> <b-spinner class="align-middle" />
<strong style="margin-left: 1%">{{ <strong style="margin-left: 1%">
$t("page.resource.loading") {{ $t("page.resource.loading") }}
}}</strong> </strong>
</div> </div>
<!-- Column "Name" -->
<template #head(name)="row"> <template #head(name)="row">
<b-form-checkbox <b-form-checkbox v-model="selectAll" @change="allSelect(row)" />
v-model="selectAll"
@change="allSelect(row)"
></b-form-checkbox>
<span>{{ $t("page.resource.fileName") }}</span> <span>{{ $t("page.resource.fileName") }}</span>
</template> </template>
<!-- Column "Last Modified" -->
<template #head(lastModified)="row"> <template #head(lastModified)="row">
<span>{{ row.label }}</span> <span>{{ row.label }}</span>
</template> </template>
<!-- Column "Size" -->
<template #head(size)="row"> <template #head(size)="row">
<span>{{ row.label }}</span> <span>{{ row.label }}</span>
</template> </template>
<!-- -->
<template #head()="row"> <template #head()="row">
<template v-if="visibleColumns.includes(row.field.key)"> <template v-if="visibleColumns.includes(row.field.key)">
<span>{{ row.label }}</span> <span>{{ row.label }}</span>
...@@ -61,11 +70,16 @@ ...@@ -61,11 +70,16 @@
</span> </span>
</template> </template>
</template> </template>
<!-- Column "Add Column"/Filter -->
<template #head(addColumn)> <template #head(addColumn)>
<b-dropdown id="addColumnDropDown" size="sm" right :no-caret="true"> <b-dropdown id="addColumnDropDown" size="sm" right :no-caret="true">
<!-- Button Template + Icon -->
<template #button-content> <template #button-content>
<b-icon icon="arrow-down" /> <b-icon icon="funnel" />
</template> </template>
<!-- Checkbox Options -->
<b-form-checkbox <b-form-checkbox
v-for="column in columns" v-for="column in columns"
:key="column.key" :key="column.key"
...@@ -76,27 +90,36 @@ ...@@ -76,27 +90,36 @@
</b-form-checkbox> </b-form-checkbox>
</b-dropdown> </b-dropdown>
</template> </template>
<!-- Row "Name" -->
<template #cell(name)="row"> <template #cell(name)="row">
<span class="d-flex justify-between fileViewEntryWrapper">
<span class="d-inline-flex gap-2">
<!-- File Icon and Checkbox -->
<span class="checkFile"> <span class="checkFile">
<!-- Show a checkbox for elements that are not read-only -->
<b-form-checkbox <b-form-checkbox
v-if="!row.item.readOnly"
v-model="row.rowSelected" v-model="row.rowSelected"
class="tableCheck" class="tableCheck"
@change="select(row)" @change="select(row)"
/> />
<!-- Icon -->
<b-icon <b-icon
:icon="row.item.isFolder ? 'folder' : 'file-earmark'" :icon="row.item.isFolder ? 'folder' : 'file-earmark'"
></b-icon> class="ml-1"
/>
</span> </span>
<a <a
v-if="!editableDataUrl" v-if="!editableDataUrl"
class="dataSourceItem" class="fileViewEntry"
:href="'#' + row.item.absolutePath" @click="triggerNavigation(row.item)"
@click="
row.item.isFolder ? openFolder(row.item) : openFile(row.item)
"
>{{ row.item.name }}</a
> >
<a v-else>{{ row.item.name }}</a> {{ row.item.name }}
</a>
<span v-else class="fileViewEntry"> {{ row.item.name }} </span>
</span>
<!-- Row File "..." Menu -->
<b-dropdown <b-dropdown
class="dotMenu" class="dotMenu"
left left
...@@ -106,26 +129,33 @@ ...@@ -106,26 +129,33 @@
:no-caret="true" :no-caret="true"
:disabled="editableDataUrl && resource && resource.archived" :disabled="editableDataUrl && resource && resource.archived"
> >
<template #button-content> ... </template> <!-- Row File "..." Button Template + Icon -->
<template #button-content>
<b-icon icon="three-dots-vertical" />
</template>
<!-- Option "Download" -->
<b-dropdown-item <b-dropdown-item
v-if="!editableDataUrl" v-if="!editableDataUrl && !row.item.isFolder"
@click=" @click="openFile(row.item)"
row.item.isFolder ? openFolder(row.item) : openFile(row.item)
"
> >
{{ $t("page.resource.metadataManagerBtnDownload") }} {{ $t("page.resource.metadataManagerBtnDownload") }}
</b-dropdown-item> </b-dropdown-item>
<!-- Option "Delete" -->
<b-dropdown-item <b-dropdown-item
v-if="!isGuest" v-if="!isGuest"
:disabled=" :disabled="
(resource && resource.archived) || (resource && resource.archived) ||
(resourceTypeInformation && !resourceTypeInformation.canDelete) (resourceTypeInformation &&
!resourceTypeInformation.canDelete)
" "
@click="showModalDeleteFile(row.item)" @click="showModalDeleteFile(row.item)"
> >
{{ $t("buttons.delete") }} {{ $t("buttons.delete") }}
</b-dropdown-item> </b-dropdown-item>
</b-dropdown> </b-dropdown>
</span>
</template> </template>
</b-table> </b-table>
</b-row> </b-row>
...@@ -134,42 +164,45 @@ ...@@ -134,42 +164,45 @@
<script lang="ts"> <script lang="ts">
import { defineComponent, type PropType, reactive } from "vue"; import { defineComponent, type PropType, reactive } from "vue";
// import the store for current module // import the store for current module
import useResourceStore from "../../store"; import useResourceStore from "../../store";
import useProjectStore from "@/modules/project/store"; import useProjectStore from "@/modules/project/store";
import FilesViewHeader from "./FilesViewHeader.vue"; import FilesViewHeader from "./FilesViewHeader.vue";
import type {
FileInformation,
FolderContent,
FolderInformation,
ReadOnlyFolderInformation,
} from "../../utils/EntryDefinition";
import MetadataManagerUtil from "../../utils/MetadataManagerUtil"; import MetadataManagerUtil from "../../utils/MetadataManagerUtil";
import fileSaver from "file-saver"; import fileSaver from "file-saver";
import { FileUtil } from "../../utils/FileUtil";
import { v4 as uuidv4 } from "uuid";
import { parseRDFDefinition } from "../../utils/linkedData";
import factory from "rdf-ext";
import {
TreeDto2FileInformation,
TreeDto2FolderInformation,
treeMapper,
} from "@/mapping/tree";
import type { BFormRow, BTable } from "bootstrap-vue";
import type { Dataset, Literal, Quad } from "@rdfjs/types";
import type {
TreeDataType,
ProjectDto,
ResourceTypeInformationDto,
FileDto,
MetadataDto,
} from "@coscine/api-client/dist/types/Coscine.Api";
import type { import type {
BilingualLabels, BilingualLabels,
CustomTableField, CustomTableField,
FileInformation,
FolderContent,
FolderInformation,
ReadOnlyFolderInformation,
VisitedResourceObject, VisitedResourceObject,
} from "../../types"; } from "../../types";
import type { BFormRow, BTable } from "bootstrap-vue";
import { FileUtil } from "../../utils/FileUtil";
import { v4 as uuidv4 } from "uuid";
import { parseRDFDefinition } from "../../utils/linkedData";
import factory from "rdf-ext";
import type { Dataset, Literal } from "@rdfjs/types";
import type { ResourceTypeInformationDto } from "@coscine/api-client/dist/types/Coscine.Api";
export default defineComponent({ export default defineComponent({
components: { components: {
FilesViewHeader, FilesViewHeader,
}, },
props: { props: {
folderContents: { folderContents: {
default: () => { default: () => {
...@@ -177,10 +210,6 @@ export default defineComponent({ ...@@ -177,10 +210,6 @@ export default defineComponent({
}, },
type: Array as PropType<FolderContent[]>, type: Array as PropType<FolderContent[]>,
}, },
currentFolder: {
default: "/",
type: String,
},
fileListEdit: { fileListEdit: {
default: () => { default: () => {
return []; return [];
...@@ -191,21 +220,37 @@ export default defineComponent({ ...@@ -191,21 +220,37 @@ export default defineComponent({
default: false, default: false,
type: Boolean, type: Boolean,
}, },
dirTrail: {
required: true,
type: String,
},
dirCrumbs: {
required: true,
type: Array as PropType<string[]>,
}, },
},
emits: {
clickFileSelect: () => true,
fileListEdit: (_: FolderContent[]) => true,
folderContents: (_: FolderContent[]) => true,
showDetail: (_: boolean) => true,
showModalDelete: (_: FolderContent[]) => true,
},
setup() { setup() {
const resourceStore = useResourceStore(); const resourceStore = useResourceStore();
const projectStore = useProjectStore(); const projectStore = useProjectStore();
return { resourceStore, projectStore }; return { resourceStore, projectStore };
}, },
data() { data() {
return { return {
columns: [] as CustomTableField[], columns: [] as CustomTableField[],
isBusy: true, isBusy: true,
selectAll: false, selectAll: false,
filter: "", filter: "",
folderPath: [] as string[],
selectableFiles: [] as FolderContent[],
sortBy: "", sortBy: "",
sortDesc: false, sortDesc: false,
}; };
...@@ -218,10 +263,12 @@ export default defineComponent({ ...@@ -218,10 +263,12 @@ export default defineComponent({
applicationProfile(): Dataset | null { applicationProfile(): Dataset | null {
return this.resourceStore.currentFullApplicationProfile; return this.resourceStore.currentFullApplicationProfile;
}, },
project(): null | ProjectDto {
return this.projectStore.currentProject;
},
resource(): null | VisitedResourceObject { resource(): null | VisitedResourceObject {
return this.resourceStore.currentResource; return this.resourceStore.currentResource;
}, },
classes(): { [className: string]: BilingualLabels } { classes(): { [className: string]: BilingualLabels } {
return this.resourceStore.classes; return this.resourceStore.classes;
}, },
...@@ -332,8 +379,8 @@ export default defineComponent({ ...@@ -332,8 +379,8 @@ export default defineComponent({
locale() { locale() {
this.getColumns(); this.getColumns();
}, },
resource() { async resource() {
this.getData(); await this.navigateTree();
}, },
sortBy() { sortBy() {
this.saveInLocalStorage(); this.saveInLocalStorage();
...@@ -341,31 +388,65 @@ export default defineComponent({ ...@@ -341,31 +388,65 @@ export default defineComponent({
sortDesc() { sortDesc() {
this.saveInLocalStorage(); this.saveInLocalStorage();
}, },
async dirTrail() {
// Reacts to route changes from 'triggerNavigation()'
await this.navigateTree();
}, },
created() { },
async created() {
this.isBusy = true; this.isBusy = true;
this.getColumns(); this.getColumns();
if (this.resource) { if (this.resource) {
this.getData(); await this.navigateTree();
} }
this.isBusy = false;
}, },
methods: { methods: {
/**
* Generates the table columns based on the application profile.
* This method reads paths from the application profile and checks the local storage to see if
* certain columns should be active or not. The resulting columns are reactive objects.
*/
getColumns() { getColumns() {
this.columns.length = 0; this.columns.length = 0;
if (!this.applicationProfile) return;
// keys a, b, name, active // keys a, b, name, active
if (this.applicationProfile) {
const oldColumns = this.loadFromLocalStorage(); const oldColumns = this.loadFromLocalStorage();
const paths = this.applicationProfile.match( const shaclPath = factory.namedNode("http://www.w3.org/ns/shacl#path");
undefined, const shaclName = factory.namedNode("http://www.w3.org/ns/shacl#name");
factory.namedNode("http://www.w3.org/ns/shacl#path")
); const paths = this.applicationProfile.match(undefined, shaclPath);
for (const path of paths) { for (const path of paths) {
// read the active value from the localStorage // Read the active value from the localStorage
const identifier = path.object.value; const identifier = path.object.value;
const names = this.applicationProfile.match(
path.subject, // Find the name of the column in the preferred language or a default if not available.
factory.namedNode("http://www.w3.org/ns/shacl#name") const names = this.applicationProfile.match(path.subject, shaclName);
const name = this.getColumnName(names);
// Determine if the column should be active based on stored settings.
const activeColumn = this.isColumnActive(identifier, oldColumns);
if (name) {
this.columns.push(
this.createColumn(name, identifier, path, activeColumn)
); );
}
}
},
/**
* Retrieves the column name from the provided names list.
* It prioritizes names in the current locale.
*
* @param {Dataset<Quad, Quad>} names - The list of potential names.
* @returns {string | null} - The retrieved name or null.
*/
getColumnName(names: Dataset<Quad, Quad>): string | null {
let name: string | null = null; let name: string | null = null;
for (const nameEntry of names) { for (const nameEntry of names) {
name = nameEntry.object.value; name = nameEntry.object.value;
...@@ -375,119 +456,188 @@ export default defineComponent({ ...@@ -375,119 +456,188 @@ export default defineComponent({
break; break;
} }
} }
return name;
},
let activeColumn = false; /**
* Determines if a column should be active based on the provided identifier and the stored columns.
* @param {string} identifier - The identifier of the column to check.
* @param {CustomTableField[]} oldColumns - The collection of stored old columns.
* @returns {boolean} - True if the column should be active, false otherwise.
*/
isColumnActive(
identifier: string,
oldColumns: CustomTableField[]
): boolean {
for (const oldColumn of oldColumns) { for (const oldColumn of oldColumns) {
if (oldColumn.key === identifier) { if (oldColumn.key === identifier) {
activeColumn = oldColumn.active; return oldColumn.active;
} }
} }
if (name) { return false;
this.columns.push( },
reactive({
/**
* Creates a column based on the provided name, identifier and path.
* @param {string} name - The name of the column.
* @param {string} identifier - The identifier of the column.
* @param {Quad} path - The path of the column.
* @param {boolean} activeColumn - True if the column should be active, false otherwise.
* @returns {CustomTableField} - The created column.
*/
createColumn(
name: string,
identifier: string,
path: Quad,
activeColumn: boolean
): CustomTableField {
return reactive({
label: name, label: name,
key: identifier, key: identifier,
sortable: true, sortable: true,
active: activeColumn, active: activeColumn,
formatter: (value, key, item: FolderContent) => { formatter: (value, key, item: FolderContent) => {
const metadata = item.metadata; return this.formatColumnValue(item.metadata, path);
const entries = metadata.match(undefined, path.object); },
if (entries.size) { sortByFormatted: true,
filterByFormatted: true,
});
},
/**
* Formats the column value based on the provided metadata and path.
* @param {Dataset<Quad, Quad>} metadata - The metadata containing information to format.
* @param {Quad} path - The RDF quad defining the property path for the column.
* @returns {string} - The formatted column value, where multiple values are separated by a line break ("<br>").
*/
formatColumnValue(
metadata: Dataset<Quad, Quad> | null,
path: Quad
): string {
// Extract all matching entries from the metadata using the specified path.
const entries = metadata?.match(undefined, path.object);
// If there are no matching entries, return an empty string.
if (!entries?.size) return "";
const returnList: string[] = []; const returnList: string[] = [];
for (const entry of entries) { for (const entry of entries) {
const entryObject = entry.object; const entryObject = entry.object;
let entryValue = entryObject.value; let entryValue = entryObject.value;
// If the entry object is a literal, further formatting might be needed.
if (entryObject.termType === "Literal") { if (entryObject.termType === "Literal") {
// If the datatype of the literal indicates a date, format it accordingly.
if (entryObject.datatype.value.endsWith("date")) { if (entryObject.datatype.value.endsWith("date")) {
const date = entryObject.value; const date = entryObject.value;
const dateObject = new Date(date); const dateObject = new Date(date);
entryValue = dateObject.toLocaleDateString( entryValue = dateObject.toLocaleDateString(this.$i18n.locale);
this.$i18n.locale }
);
} }
} else if (entryObject.termType === "NamedNode") { // If the entry object is a named node, it might need to be translated into a more readable form.
else if (entryObject.termType === "NamedNode") {
// Loop through all available classes to find a match for the named node's value.
for (const classValue of Object.values(this.classes)) { for (const classValue of Object.values(this.classes)) {
// Depending on the user's locale, choose the appropriate language list.
const langClassList = const langClassList =
this.$i18n.locale === "de" this.$i18n.locale === "de" ? classValue.de : classValue.en;
? classValue.de
: classValue.en; // If a language-specific list is available, search for the named node's value in it.
if (langClassList) { if (langClassList) {
const foundEntry = langClassList.find( const foundEntry = langClassList.find(
(langClassEntry) => (langClassEntry) => langClassEntry.value === entryValue
langClassEntry.value === entryValue
); );
// If a match is found, replace the entryValue with the more readable name.
if (foundEntry && foundEntry.name) { if (foundEntry && foundEntry.name) {
entryValue = foundEntry.name; entryValue = foundEntry.name;
break; break; // Exit the loop once a match is found.
} }
} }
} }
} }
// Add the processed entry value to the list of results.
returnList.push(entryValue); returnList.push(entryValue);
} }
// Join the list of results with line breaks and return the final formatted value.
return returnList.join("<br>"); return returnList.join("<br>");
}
return "";
}, },
sortByFormatted: true,
filterByFormatted: true, /**
}) * Navigate the folder structure based on the current folder's path.
); * This method serves to identify whether the current folder is actually a file,
} * opens it accordingly, and then navigates the directory structure.
} */
} async navigateTree() {
}, // Start with the current folder as the reference
getData() { let currentFolder = this.dirTrail;
// Doesn't end with '/' => Probably a file
let currentFolder = this.currentFolder; // Check if the current folder might actually be a file and handle it
if (!currentFolder.endsWith("/") && currentFolder !== "") { if (this.isProbablyFile(currentFolder)) {
// TODO: Change to open modal // TODO: Change to open modal
this.openFile({ await this.openFile({
id: uuidv4(),
isFolder: false,
name: currentFolder.substring(currentFolder.lastIndexOf("/") + 1), name: currentFolder.substring(currentFolder.lastIndexOf("/") + 1),
path: currentFolder, path: currentFolder,
absolutePath: currentFolder, } as FileInformation);
version: `${+new Date()}`, // Remove the file name from the path to get the folder path, then navigate to it
size: 0, currentFolder = this.dirCrumbs.join("");
metadata: factory.dataset() as unknown as Dataset, this.triggerNavigation({ path: currentFolder } as FolderInformation);
});
currentFolder = currentFolder.substring(
0,
currentFolder.lastIndexOf("/") + 1
);
}
if (currentFolder !== "/") {
let combine = "";
const folderSplit = currentFolder.split("/");
for (const folderKey in folderSplit) {
if (Object.prototype.hasOwnProperty.call(folderSplit, folderKey)) {
const folder = folderSplit[folderKey];
if (combine !== "" && folder === "") {
continue;
} }
combine += folder + "/";
this.folderPath.push(combine); // Finally, open the current folder to display its contents
} await this.openFolder({ path: currentFolder } as FolderInformation);
} },
this.folderPath.pop();
if (this.folderPath.length > 0) { /**
this.$emit("currentFolder", this.folderPath.pop()); * Navigates to the specified folder or resource based on the given `entry`.
} else { * If the provided `entry.path` is different from the current `dirTrail`,
this.$emit("currentFolder", ""); * a new route will be pushed to the Vue router.
} *
} else { * @param {FolderContent} entry - The folder or file information object to navigate to.
this.$emit("currentFolder", ""); */
} triggerNavigation(entry: FolderContent) {
this.openFolder({ this.$router.push({
id: uuidv4(), name: "resource-page",
isFolder: true, params: {
name: currentFolder.substring(currentFolder.lastIndexOf("/") + 1), dirTrail: entry.path,
path: currentFolder, },
absolutePath: currentFolder,
metadata: factory.dataset() as unknown as Dataset,
}); });
}, },
/**
* Determines if a given path represents a file based on its structure.
* Files typically do not end with a slash `/`.
*
* @param {string} path - The path to evaluate.
* @returns {boolean} - True if the path might represent a file, false otherwise.
*/
isProbablyFile(path: string | undefined): boolean | undefined {
return path !== "" && !path?.endsWith("/");
},
/**
* Splits a path into its constituent directories.
*
* @param {string} path - The path to split.
* @returns {string[]} - An array representing the folder's hierarchical structure.
*/
splitPathIntoDirectories(path: string): string[] {
const directories = [];
let combinedPath = "";
const splitFolders = path.split("/");
for (const folder of splitFolders) {
if (combinedPath && !folder) continue;
combinedPath += folder + "/";
directories.push(combinedPath);
}
return directories;
},
/**
* Saves table preferences in local storage.
*/
async saveInLocalStorage() { async saveInLocalStorage() {
await this.resourceStore.setStoredColumns({ await this.resourceStore.setStoredColumns({
columns: this.columns, columns: this.columns,
...@@ -496,20 +646,34 @@ export default defineComponent({ ...@@ -496,20 +646,34 @@ export default defineComponent({
sortDesc: this.sortDesc, sortDesc: this.sortDesc,
}); });
}, },
/**
* Loads table preferences from local storage.
* @returns {Array<CustomTableField>} - Array of table fields.
*/
loadFromLocalStorage(): Array<CustomTableField> { loadFromLocalStorage(): Array<CustomTableField> {
let element: Array<CustomTableField> = []; let storedColumns: Array<CustomTableField> = [];
if (this.resourceStore.currentStoredColumns) { const currentStoredColumns = this.resourceStore.currentStoredColumns;
// Deal with old values
if (Array.isArray(this.resourceStore.currentStoredColumns)) { if (currentStoredColumns) {
return this.resourceStore.currentStoredColumns; // If the stored columns are just an array (legacy format)
if (Array.isArray(currentStoredColumns)) {
return currentStoredColumns;
} }
element = this.resourceStore.currentStoredColumns.columns;
this.filter = this.resourceStore.currentStoredColumns.filter; // Retrieve and set stored preferences
this.sortBy = this.resourceStore.currentStoredColumns.sortBy; storedColumns = currentStoredColumns.columns;
this.sortDesc = this.resourceStore.currentStoredColumns.sortDesc; this.filter = currentStoredColumns.filter;
this.sortBy = currentStoredColumns.sortBy;
this.sortDesc = currentStoredColumns.sortDesc;
} }
return element; return storedColumns;
}, },
/**
* Deactivates a column based on the provided row.
* @param {BFormRow} row - Row which represents a column.
*/
removeColumn(row: BFormRow) { removeColumn(row: BFormRow) {
for (const column of this.columns) { for (const column of this.columns) {
if (column.key === row.column) { if (column.key === row.column) {
...@@ -517,109 +681,192 @@ export default defineComponent({ ...@@ -517,109 +681,192 @@ export default defineComponent({
} }
} }
}, },
/**
* Emits an event to show the delete modal for a specific file.
* @param {FolderContent} file - File to delete.
*/
showModalDeleteFile(file: FolderContent) { showModalDeleteFile(file: FolderContent) {
this.$emit("showModalDelete", [file]); this.$emit("showModalDelete", [file]);
}, },
/**
* Asynchronously opens the specified file, fetching its blob content and triggering a download.
* @async
* @param {FileInformation} file - Information about the file to be opened.
*/
async openFile(file: FileInformation) {
if (this.project?.id && this.resource?.id) {
// Download the file as a blob (binary data)
const asBlob = true;
// Fetch the blob content of the file
const response = await this.resourceStore.getBlob(
this.project.id,
this.resource.id,
file.path,
asBlob
);
if (response) {
// Trigger file download
fileSaver.saveAs(
new Blob([response.data], {
type: response.headers["content-type"],
}),
file.name
);
// TODO: Consider adding a visual indicator that the file is being downloaded
}
}
},
/**
* Asynchronously opens the given folder, fetching its file tree and metadata tree.
* @async
* @param {FolderInformation} folder - Information about the folder to be opened.
*/
async openFolder(folder: FolderInformation) { async openFolder(folder: FolderInformation) {
if (this.resource?.id) { if (!this.project?.id || !this.resource?.id) return;
this.isBusy = true; this.isBusy = true;
// Empty file list, since the context changes // Empty file list, since the context changes
// (Other workaround would be to keep the editing files open) // (Other workaround would be to keep the editing files open)
this.emitEmptyFileList();
// Determine the path of the folder and fetch its file and metadata trees
const path = this.determinePath(folder);
const [fileTree, metadataTree] = await Promise.all([
this.resourceStore.getFileTree(this.project.id, this.resource.id, path),
this.resourceStore.getMetadataTree(
this.project.id,
this.resource.id,
path
),
]);
// Create a temporary folder to store the entries
const folderContents = await this.constructFolderContents(
fileTree,
metadataTree
);
this.isBusy = false;
this.$emit("folderContents", folderContents);
},
/**
* Emits an event to notify of an empty file list.
*/
emitEmptyFileList() {
this.$emit("fileListEdit", []); this.$emit("fileListEdit", []);
},
const absolutePath = /**
folder.absolutePath !== "" ? folder.absolutePath : "/"; * Determines the correct path for the folder.
* @param {FolderInformation} folder - Information about the folder for which path needs to be determined.
* @returns {string} The determined path.
*/
determinePath(folder: FolderInformation): string {
return folder.path; // !== "" ? folder.path : "/";
},
// Show location change /**
window.location.hash = absolutePath; * Constructs the folder content based on the provided file and metadata trees.
* @async
* @param {FileDto[] | null | undefined} fileTree - File tree of the folder.
* @param {MetadataDto[] | null | undefined} metadataTree - Metadata tree of the folder.
* @returns {FolderContent[]} An array containing folder content.
*/
async constructFolderContents(
fileTree: FileDto[] | null | undefined,
metadataTree: MetadataDto[] | null | undefined
): Promise<FolderContent[]> {
const folderContents = [];
const response = await this.resourceStore.getMetadata( if (this.dirCrumbs.length > 0) {
this.resource.id, // Add navigate up folder entry (..)
absolutePath folderContents.push(this.createNavigateUpFolder());
); }
const tmpFolder: FolderContent[] = []; // Iterate over the file tree and map the entries for the files view table
if (folder.name === "..") { for (const fileEntry of fileTree || []) {
this.$emit("currentFolder", this.folderPath.pop()); const content = this.mapFileEntryToContent(fileEntry);
} else if (this.currentFolder !== "") { if (content) {
this.folderPath.push(this.currentFolder); const metadataEntry = metadataTree?.find(
this.$emit("currentFolder", absolutePath); (md) => md.path === fileEntry.path
} else { );
this.$emit("currentFolder", absolutePath); if (metadataEntry?.definition && metadataEntry?.format) {
content.metadata = await parseRDFDefinition(
metadataEntry.definition,
metadataEntry.format
);
}
folderContents.push(content);
} }
}
return folderContents;
},
if (this.folderPath.length > 0) { /**
const path = this.folderPath[this.folderPath.length - 1]; * Creates a representation for the "navigate up" folder.
const navigateUpFolder: ReadOnlyFolderInformation = { * @returns {FolderContent} A representation of the "navigate up" folder.
*/
createNavigateUpFolder(): FolderContent {
// Make a shallow copy of the array and copy all but the last element as it is the current folder itself
const currentDirCrumbs = this.dirCrumbs.slice(0, -1);
const path = currentDirCrumbs.join("");
return {
id: uuidv4(), id: uuidv4(),
isFolder: true, isFolder: true,
name: "..", name: "..",
absolutePath: path,
path: path, path: path,
readOnly: true, readOnly: true,
type: "Tree" as TreeDataType.Tree,
parentDirectory: "",
metadata: factory.dataset() as unknown as Dataset, metadata: factory.dataset() as unknown as Dataset,
}; } as ReadOnlyFolderInformation;
tmpFolder.push(navigateUpFolder);
}
for (const obj of response.data.fileStorage) {
const newEntry: FileInformation = {
id: uuidv4(),
isFolder: obj.IsFolder,
name: obj.Name,
path: obj.Path,
absolutePath: obj.Path,
lastModified: obj.Modified,
created: obj.Created,
size: obj.Size,
version: `${+new Date()}`,
metadata: factory.dataset() as unknown as Dataset,
};
const resultArray = MetadataManagerUtil.filterMetadataStorage(
response.data.metadataStorage,
newEntry.absolutePath
);
if (resultArray.length > 0) {
const result = resultArray[0][Object.keys(resultArray[0])[0]];
if (typeof result === "string") {
newEntry.metadata = await parseRDFDefinition(
result,
"application/n-triples"
);
}
}
tmpFolder.push(newEntry);
}
this.isBusy = false;
this.$emit("folderContents", tmpFolder);
}
}, },
async openFile(file: FileInformation) { /**
if (this.resource?.id) { * Maps a file entry to its corresponding folder content.
const response = await this.resourceStore.getFile( * @param {FileDto} fileEntry - The file data transfer object to map.
this.resource.id, * @returns {FolderContent | undefined} The corresponding folder content or undefined if the type is unknown.
file.absolutePath, */
true mapFileEntryToContent(fileEntry: FileDto): FolderContent | undefined {
); let content: FolderContent | undefined = undefined;
if (response !== null) { switch (fileEntry.type) {
fileSaver.saveAs(response, file.name); case "Tree" as TreeDataType.Tree:
} content = treeMapper.map(TreeDto2FolderInformation, fileEntry);
window.location.hash = this.currentFolder; break;
case "Leaf" as TreeDataType.Leaf:
content = treeMapper.map(TreeDto2FileInformation, fileEntry);
break;
default:
console.error("Unknown content type: ", fileEntry.type);
break;
} }
return content;
}, },
/**
* Handles when a row is selected, updating the UI and emitting necessary events.
* @param {FolderContent[]} items - Selected items.
*/
onRowSelected(items: FolderContent[]) { onRowSelected(items: FolderContent[]) {
this.$emit("showDetail", items.length > 0); this.$emit("showDetail", items.length > 0);
this.selectAll = items.length === this.folderContents.length; this.selectAll = items.length === this.folderContents.length;
// Filter out folders like ".." // Filter out read-only entires like the navigation up folder ".."
this.selectableFiles = items.filter((item) => !item.readOnly); const selectedFiles = items.filter((item) => !item.readOnly);
this.updateFileListEdit(); this.updateFileListEdit(selectedFiles);
}, },
/**
* Selects or deselects all rows.
* @param {BTable} row - Reference to the table.
*/
allSelect(row: BTable) { allSelect(row: BTable) {
if (this.selectAll) { if (this.selectAll) {
row.selectAllRows(); row.selectAllRows();
...@@ -627,77 +874,84 @@ export default defineComponent({ ...@@ -627,77 +874,84 @@ export default defineComponent({
row.clearSelected(); row.clearSelected();
} }
}, },
/**
* Toggles the selection of a specific row.
* @param {{ rowSelected: boolean; index: number }} row - Info about the row's status and its index.
*/
select(row: { rowSelected: boolean; index: number }) { select(row: { rowSelected: boolean; index: number }) {
if (row.rowSelected) { const tableRef = this.$refs.adaptTable as BTable;
(this.$refs.adaptTable as BTable).selectRow(row.index); row.rowSelected
} else { ? tableRef.selectRow(row.index)
(this.$refs.adaptTable as BTable).unselectRow(row.index); : tableRef.unselectRow(row.index);
}
}, },
/**
* Emits an event for file selection.
*/
clickFileSelect() { clickFileSelect() {
this.$emit("clickFileSelect"); this.$emit("clickFileSelect");
}, },
async updateFileListEdit() {
const newFileListEdit = [] as FolderContent[]; /**
for (const currentFileEdit of this.fileListEdit) { * Updates the file list for editing based on current selections.
const selectedFile = this.selectableFiles.find( */
(f) => async updateFileListEdit(selectedFiles: FolderContent[]) {
this.currentFolder === currentFileEdit.path && const existingFilesSet = new Set(this.fileListEdit.map((f) => f.name));
f.name === currentFileEdit.name const newFileListEdit: FileInformation[] = [];
for (const file of selectedFiles) {
// Check if file already exists in the list
if (existingFilesSet.has(file.name) && this.dirTrail === file.path) {
const existingFile = this.fileListEdit.find(
(f) => f.name === file.name
); );
if (selectedFile) { if (existingFile) {
newFileListEdit.push(currentFileEdit); newFileListEdit.push(existingFile as FileInformation);
} }
continue;
} }
for (const currentSelectableFile of this.selectableFiles) {
const selectableFile = currentSelectableFile; // If file doesn't exist, fetch metadata and add
const fileEdit = newFileListEdit.find( const loadedMetadata = await MetadataManagerUtil.loadMetadataForFile(
(f) => f.path === this.currentFolder && f.name === selectableFile.name file,
); this.project,
if (!fileEdit) {
const loadedMetadata = await MetadataManagerUtil.loadMetadata(
selectableFile,
this.resource this.resource
); );
newFileListEdit.push({ newFileListEdit.push({
...file,
id: uuidv4(), id: uuidv4(),
lastModified: selectableFile.lastModified,
created: selectableFile.created,
path: this.currentFolder,
version: `${+new Date()}`, version: `${+new Date()}`,
uploading: false, uploading: false,
name: selectableFile.name,
metadata: loadedMetadata, metadata: loadedMetadata,
isFolder: selectableFile.isFolder, info: file.isFolder ? undefined : file.info,
absolutePath: selectableFile.absolutePath, size: file.isFolder ? 0 : file.size,
info: } as FileInformation);
selectableFile.isFolder === false
? selectableFile.info
: undefined,
size: selectableFile.isFolder === false ? selectableFile.size : 0,
});
}
} }
this.$emit("fileListEdit", newFileListEdit); this.$emit("fileListEdit", newFileListEdit);
if (this.selectableFiles.length > 0) { this.$emit("showDetail", selectedFiles.length > 0);
this.$emit("showDetail", true);
} else {
this.$emit("showDetail", false);
}
}, },
/**
* Converts last modified date into a locale string format.
* @param {FolderContent} item - Item to retrieve the date from.
* @returns {string} - Formatted date string.
*/
renderDate(item: FolderContent): string { renderDate(item: FolderContent): string {
if (!item.lastModified) { return item.lastModified
return ""; ? new Date(item.lastModified).toLocaleDateString(this.$i18n.locale)
} : "";
const date = item.lastModified;
const dateObject = new Date(date);
return dateObject.toLocaleDateString(this.$i18n.locale);
}, },
/**
* Converts size into a readable string format.
* @param {FolderContent} item - Item to retrieve the size from.
* @returns {string} - Formatted size string.
*/
renderSize(item: FolderContent): string { renderSize(item: FolderContent): string {
if (!item.isFolder) { return !item.isFolder ? FileUtil.formatBytes(item.size) : "";
return FileUtil.formatBytes(item.size);
}
return "";
}, },
}, },
}); });
...@@ -763,22 +1017,6 @@ export default defineComponent({ ...@@ -763,22 +1017,6 @@ export default defineComponent({
#resourceViewTable div.tableCheck.custom-checkbox label, #resourceViewTable div.tableCheck.custom-checkbox label,
.checkFile { .checkFile {
vertical-align: top; vertical-align: top;
cursor: pointer;
}
#resourceViewTable tr:hover .dotMenu button {
font-weight: bold;
margin: 0;
}
#resourceViewTable .dotMenu {
position: absolute;
right: 0px;
margin-top: -5px;
}
#resourceViewTable .dotMenu .dropdown-item {
margin: 1px;
} }
</style> </style>
...@@ -810,10 +1048,6 @@ export default defineComponent({ ...@@ -810,10 +1048,6 @@ export default defineComponent({
margin-right: 20px; margin-right: 20px;
} }
#resourceViewTable th .additionalColumnHeader svg {
cursor: pointer;
}
.adaptTable { .adaptTable {
overflow: auto; overflow: auto;
position: absolute; position: absolute;
...@@ -824,12 +1058,16 @@ export default defineComponent({ ...@@ -824,12 +1058,16 @@ export default defineComponent({
width: auto; width: auto;
} }
.rightFloating { .fileViewEntryWrapper {
float: right; align-items: center;
} }
.dataSourceItem { .fileViewEntry {
vertical-align: sub; padding: 0.05rem 0.5rem;
}
a.fileViewEntry {
cursor: pointer;
} }
.DataSource { .DataSource {
......
...@@ -34,6 +34,7 @@ ...@@ -34,6 +34,7 @@
:disabled="isUploading || readOnly || (resource && resource.archived)" :disabled="isUploading || readOnly || (resource && resource.archived)"
@click="upload" @click="upload"
> >
<!-- TODO: Add GitLab branch disabled logic as in MetadataManagerHeader -->
<b-icon icon="plus" :title="$t('page.resource.upload')" /> <b-icon icon="plus" :title="$t('page.resource.upload')" />
</b-button> </b-button>
......
<template> <template>
<b-container id="detail-view"> <b-container id="detail-view">
<!-- Header Buttons -->
<MetadataManagerHeader <MetadataManagerHeader
v-if="resource" v-if="resource"
:editable-data-url="editableDataUrl" :editable-data-url="editableDataUrl"
...@@ -12,6 +13,8 @@ ...@@ -12,6 +13,8 @@
@selectFiles="selectFiles" @selectFiles="selectFiles"
@showModalDeleteFolderContents="showModalDeleteFolderContents" @showModalDeleteFolderContents="showModalDeleteFolderContents"
/> />
<!-- Selected Files Table -->
<MetadataManagerTable <MetadataManagerTable
:current-file-id="currentFileId" :current-file-id="currentFileId"
:current-folder-content="currentFolderContent" :current-folder-content="currentFolderContent"
...@@ -23,8 +26,11 @@ ...@@ -23,8 +26,11 @@
@loadAllFilesTab="changeMetadata(-1)" @loadAllFilesTab="changeMetadata(-1)"
@removeElement="removeElement" @removeElement="removeElement"
/> />
<!-- Metadata Section -->
<b-row id="metadataManagerMetadataSection"> <b-row id="metadataManagerMetadataSection">
<b-col> <b-col>
<!-- File Information -->
<MetadataManagerFileInformation <MetadataManagerFileInformation
v-if=" v-if="
showDetail && showDetail &&
...@@ -35,6 +41,7 @@ ...@@ -35,6 +41,7 @@
:current-file-id="currentFileId" :current-file-id="currentFileId"
:file-list-edit="fileListEdit" :file-list-edit="fileListEdit"
/> />
<!-- Special Properties (see Linked Data) -->
<MetadataManagerSpecialProperties <MetadataManagerSpecialProperties
v-if=" v-if="
shownFiles.length > 0 && shownFiles.length > 0 &&
...@@ -51,6 +58,7 @@ ...@@ -51,6 +58,7 @@
@updateAbsolutePath="updateAbsolutePath" @updateAbsolutePath="updateAbsolutePath"
@updateDataUrl="updateDataUrl" @updateDataUrl="updateDataUrl"
/> />
<!-- Form Generator -->
<span <span
v-if=" v-if="
resource && resource.applicationProfile !== '' && applicationProfile resource && resource.applicationProfile !== '' && applicationProfile
...@@ -77,6 +85,7 @@ ...@@ -77,6 +85,7 @@
</b-col> </b-col>
</b-row> </b-row>
<!-- Footer -->
<MetadataManagerFooter <MetadataManagerFooter
:is-uploading="isUploading" :is-uploading="isUploading"
:number-of-currently-processed-files="numberOfCurrentlyProcessedFiles" :number-of-currently-processed-files="numberOfCurrentlyProcessedFiles"
...@@ -89,11 +98,15 @@ ...@@ -89,11 +98,15 @@
@update="update" @update="update"
@uploadPreparation="uploadPreparation" @uploadPreparation="uploadPreparation"
/> />
<!-- Validation Popover -->
<ValidationPopover <ValidationPopover
v-if="validationResults[currentFileId + 1]" v-if="validationResults[currentFileId + 1]"
:valid="valid" :valid="valid"
:validation-results="validationResults[currentFileId + 1].results" :validation-results="validationResults[currentFileId + 1].results"
/> />
<!-- Duplicate Files Modal -->
<SaveDuplicateFilesModal <SaveDuplicateFilesModal
:visible="saveDuplicateFilesModalVisible" :visible="saveDuplicateFilesModalVisible"
:upload-file-list-replace-files="uploadFileListReplaceFiles" :upload-file-list-replace-files="uploadFileListReplaceFiles"
...@@ -111,40 +124,36 @@ import { defineComponent, reactive, type PropType } from "vue"; ...@@ -111,40 +124,36 @@ import { defineComponent, reactive, type PropType } from "vue";
import useResourceStore from "../../store"; import useResourceStore from "../../store";
import useProjectStore from "@/modules/project/store"; import useProjectStore from "@/modules/project/store";
import useUserStore from "@/modules/user/store"; import useUserStore from "@/modules/user/store";
import useNotificationStore from "@/store/notification";
import fileSaver from "file-saver"; import fileSaver from "file-saver";
import MetadataManagerUtil from "../../utils/MetadataManagerUtil"; import MetadataManagerUtil from "../../utils/MetadataManagerUtil";
import MetadataManagerHeader from "./metadata/MetadataManagerHeader.vue"; import MetadataManagerHeader from "./metadata/MetadataManagerHeader.vue";
import MetadataManagerTable from "./metadata/MetadataManagerTable.vue"; import MetadataManagerTable from "./metadata/MetadataManagerTable.vue";
import MetadataManagerFileInformation from "./metadata/MetadataManagerFileInformation.vue"; import MetadataManagerFileInformation from "./metadata/MetadataManagerFileInformation.vue";
import MetadataManagerSpecialProperties from "./metadata/MetadataManagerSpecialProperties.vue"; import MetadataManagerSpecialProperties from "./metadata/MetadataManagerSpecialProperties.vue";
import MetadataManagerFooter from "./metadata/MetadataManagerFooter.vue"; import MetadataManagerFooter from "./metadata/MetadataManagerFooter.vue";
import SaveDuplicateFilesModal from "./modals/SaveDuplicateFilesModal.vue"; import SaveDuplicateFilesModal from "./modals/SaveDuplicateFilesModal.vue";
import ValidationPopover from "./popovers/ValidationPopover.vue"; import ValidationPopover from "./popovers/ValidationPopover.vue";
import type {
FileInformation,
FolderContent,
} from "../../utils/EntryDefinition";
import "@/plugins/form-generator"; import "@/plugins/form-generator";
import type { Dataset } from "@rdfjs/types";
import factory from "rdf-ext"; import factory from "rdf-ext";
import type ValidationReport from "rdf-validate-shacl/src/validation-report";
import { v4 as uuidv4 } from "uuid"; import { v4 as uuidv4 } from "uuid";
import useNotificationStore from "@/store/notification"; import { serializeRDFDefinition } from "../../utils/linkedData";
import type ValidationReport from "rdf-validate-shacl/src/validation-report";
import type { Dataset } from "@rdfjs/types";
import type { import type {
MetadataTreeForCreationDto,
MetadataTreeForUpdateDto,
ProjectDto,
RdfFormat,
ResourceDto, ResourceDto,
ResourceTypeInformationDto, ResourceTypeInformationDto,
UserDto, UserDto,
} from "@coscine/api-client/dist/types/Coscine.Api"; } from "@coscine/api-client/dist/types/Coscine.Api";
import type { BilingualLabels } from "../../types"; import type {
BilingualLabels,
FileInformation,
FolderContent,
} from "../../types";
export default defineComponent({ export default defineComponent({
components: { components: {
...@@ -157,6 +166,7 @@ export default defineComponent({ ...@@ -157,6 +166,7 @@ export default defineComponent({
SaveDuplicateFilesModal, SaveDuplicateFilesModal,
ValidationPopover, ValidationPopover,
}, },
props: { props: {
showDetail: { showDetail: {
default: false, default: false,
...@@ -184,11 +194,26 @@ export default defineComponent({ ...@@ -184,11 +194,26 @@ export default defineComponent({
}, },
type: Array as PropType<FolderContent[]>, type: Array as PropType<FolderContent[]>,
}, },
currentFolder: { dirTrail: {
default: "/", required: true,
type: String, type: String,
}, },
dirCrumbs: {
required: true,
type: Array as PropType<string[]>,
},
}, },
emits: {
clickFileSelect: () => true,
emptyFileLists: (_: boolean) => true,
isUploading: (_: boolean) => true,
navigateTree: () => true,
removeElement: (_index: number, _count: number) => true,
removeSelection: (_index: number, _count: number) => true,
showModalDelete: (_: FolderContent[]) => true,
},
setup() { setup() {
const resourceStore = useResourceStore(); const resourceStore = useResourceStore();
const projectStore = useProjectStore(); const projectStore = useProjectStore();
...@@ -202,9 +227,10 @@ export default defineComponent({ ...@@ -202,9 +227,10 @@ export default defineComponent({
notificationStore, notificationStore,
}; };
}, },
data() { data() {
return { return {
currentUsedMetadata: factory.dataset() as unknown as Dataset, currentUsedMetadata: factory.dataset() as unknown as Dataset | null,
numberOfCurrentlyProcessedFiles: 0, numberOfCurrentlyProcessedFiles: 0,
totalNumberOfCurrentlyProcessedFiles: 0, totalNumberOfCurrentlyProcessedFiles: 0,
uploadDuplicates: true, uploadDuplicates: true,
...@@ -227,6 +253,7 @@ export default defineComponent({ ...@@ -227,6 +253,7 @@ export default defineComponent({
saveDuplicateFilesModalVisible: false, saveDuplicateFilesModalVisible: false,
}; };
}, },
computed: { computed: {
isGuest(): boolean | undefined { isGuest(): boolean | undefined {
return this.projectStore.currentUserRoleIsGuest; return this.projectStore.currentUserRoleIsGuest;
...@@ -234,6 +261,9 @@ export default defineComponent({ ...@@ -234,6 +261,9 @@ export default defineComponent({
applicationProfile(): Dataset | null { applicationProfile(): Dataset | null {
return this.resourceStore.currentFullApplicationProfile; return this.resourceStore.currentFullApplicationProfile;
}, },
project(): null | ProjectDto {
return this.projectStore.currentProject;
},
resource(): null | ResourceDto { resource(): null | ResourceDto {
return this.resourceStore.currentResource; return this.resourceStore.currentResource;
}, },
...@@ -264,7 +294,8 @@ export default defineComponent({ ...@@ -264,7 +294,8 @@ export default defineComponent({
currentFolderContent(): FolderContent | undefined { currentFolderContent(): FolderContent | undefined {
return this.shownFiles[this.currentFileId]; return this.shownFiles[this.currentFileId];
}, },
currentMetadata(): Dataset {
currentMetadata(): Dataset | null {
if ( if (
this.currentFileId >= 0 && this.currentFileId >= 0 &&
this.currentFileId < this.shownFiles.length this.currentFileId < this.shownFiles.length
...@@ -350,8 +381,11 @@ export default defineComponent({ ...@@ -350,8 +381,11 @@ export default defineComponent({
fileListUpload() { fileListUpload() {
this.getOptions(); this.getOptions();
}, },
fileListEdit() { fileListEdit(newVal, oldVal) {
// Trigger only the value really changed. Otherwise double API calls are triggered.
if (newVal !== oldVal) {
this.getOptions(); this.getOptions();
}
}, },
isValidating() { isValidating() {
if (!this.isValidating && this.storedSwitch !== null) { if (!this.isValidating && this.storedSwitch !== null) {
...@@ -370,15 +404,28 @@ export default defineComponent({ ...@@ -370,15 +404,28 @@ export default defineComponent({
); );
}, },
}, },
created() { created() {
this.getOptions(); this.getOptions();
}, },
methods: { methods: {
/**
* Fetches vocabulary instances for a given class name from the store.
*
* @async
* @param {string} className - The class name for which vocabulary instances need to be fetched.
* @returns {Promise<BilingualLabels>} Returns a promise resolving to bilingual labels.
*/
async getVocabularyInstances(className: string): Promise<BilingualLabels> { async getVocabularyInstances(className: string): Promise<BilingualLabels> {
return await this.resourceStore.getVocabularyInstances(className); return await this.resourceStore.getVocabularyInstances(className);
}, },
/**
* Applies metadata templates to the shown files.
*/
applyMetadataTemplate() { applyMetadataTemplate() {
if (this.currentUsedMetadata.size > 0) { if (this.currentUsedMetadata && this.currentUsedMetadata.size > 0) {
for (const currentFile of this.shownFiles) { for (const currentFile of this.shownFiles) {
currentFile.metadata = factory.dataset( currentFile.metadata = factory.dataset(
Array.from(this.currentUsedMetadata) Array.from(this.currentUsedMetadata)
...@@ -386,6 +433,13 @@ export default defineComponent({ ...@@ -386,6 +433,13 @@ export default defineComponent({
} }
} }
}, },
/**
* Validates a file based on provided validation results.
*
* @param {ValidationReport} valid - The validation report.
* @param {string} fileString - The file string which will be converted to a number to identify the file.
*/
isValid(valid: ValidationReport, fileString: string) { isValid(valid: ValidationReport, fileString: string) {
let fileId = Number(fileString); let fileId = Number(fileString);
if (isNaN(fileId)) { if (isNaN(fileId)) {
...@@ -404,9 +458,22 @@ export default defineComponent({ ...@@ -404,9 +458,22 @@ export default defineComponent({
); );
} }
}, },
/**
* Updates the validation status.
*
* @param {boolean} isValidating - The current validation status.
* @param {string} _ - Placeholder for an unused parameter.
*/
isValidatingChange(isValidating: boolean, _: string) { isValidatingChange(isValidating: boolean, _: string) {
this.isValidating = isValidating; this.isValidating = isValidating;
}, },
/**
* Changes the currently viewed metadata based on the provided index.
*
* @param {number} index - The index of the file whose metadata is to be viewed.
*/
changeMetadata(index: number) { changeMetadata(index: number) {
if (this.isValidating) { if (this.isValidating) {
this.storedSwitch = index; this.storedSwitch = index;
...@@ -414,6 +481,13 @@ export default defineComponent({ ...@@ -414,6 +481,13 @@ export default defineComponent({
this.currentFileId = index; this.currentFileId = index;
} }
}, },
/**
* Removes elements from the list based on the provided index and count.
*
* @param {number} index - The starting index for the removal.
* @param {number} count - The number of elements to be removed.
*/
removeElement(index: number, count = 1) { removeElement(index: number, count = 1) {
if (this.showDetail) { if (this.showDetail) {
this.$emit("removeSelection", index, count); this.$emit("removeSelection", index, count);
...@@ -421,6 +495,14 @@ export default defineComponent({ ...@@ -421,6 +495,14 @@ export default defineComponent({
this.$emit("removeElement", index, count); this.$emit("removeElement", index, count);
} }
}, },
/**
* Adjusts the status of processed files. Sends notifications if necessary.
*
* @param {FolderContent} file - The file whose status needs adjustment.
* @param {boolean} error - A flag indicating if there was an error processing the file.
* @param {boolean} reload - A flag indicating if the page should reload.
*/
adjustRemainingFiles(file: FolderContent, error = false, reload = false) { adjustRemainingFiles(file: FolderContent, error = false, reload = false) {
this.numberOfCurrentlyProcessedFiles -= 1; this.numberOfCurrentlyProcessedFiles -= 1;
this.progressStatus = 0; this.progressStatus = 0;
...@@ -480,6 +562,12 @@ export default defineComponent({ ...@@ -480,6 +562,12 @@ export default defineComponent({
} }
} }
}, },
/**
* Initializes the count of remaining files to be processed.
*
* @param {number} numberOfFiles - The initial number of files to be processed.
*/
initRemainingFiles(numberOfFiles: number) { initRemainingFiles(numberOfFiles: number) {
this.fileListError = []; this.fileListError = [];
this.numberOfCurrentlyProcessedFiles = numberOfFiles; this.numberOfCurrentlyProcessedFiles = numberOfFiles;
...@@ -498,12 +586,24 @@ export default defineComponent({ ...@@ -498,12 +586,24 @@ export default defineComponent({
this.$emit("isUploading", true); this.$emit("isUploading", true);
} }
}, },
/**
* Updates the metadata for the current folder content and sets the current metadata.
*
* @param {Dataset} metadata - The metadata to be set.
*/
inputMetadata(metadata: Dataset) { inputMetadata(metadata: Dataset) {
if (this.currentFolderContent) { if (this.currentFolderContent) {
this.currentFolderContent.metadata = metadata; this.currentFolderContent.metadata = metadata;
} }
this.currentUsedMetadata = metadata; this.currentUsedMetadata = metadata;
}, },
/**
* Prepares for file uploads by segregating new files and replacement files.
*
* @async
*/
async uploadPreparation() { async uploadPreparation() {
this.uploadFileListNewFiles = []; this.uploadFileListNewFiles = [];
this.uploadFileListReplaceFiles = []; this.uploadFileListReplaceFiles = [];
...@@ -523,6 +623,12 @@ export default defineComponent({ ...@@ -523,6 +623,12 @@ export default defineComponent({
await this.upload(); await this.upload();
} }
}, },
/**
* Starts the file upload process.
*
* @async
*/
async upload() { async upload() {
if (this.currentFileId === -1) { if (this.currentFileId === -1) {
this.applyMetadataTemplate(); this.applyMetadataTemplate();
...@@ -539,26 +645,66 @@ export default defineComponent({ ...@@ -539,26 +645,66 @@ export default defineComponent({
} }
} }
}, },
/**
* Asynchronously uploads a file and its metadata.
* It first constructs and serializes a metadata tree DTO for the file, and then
* attempts to create this metadata tree. If successful, the function uploads the file's content.
*
* @param {FileInformation} file - The file information object to be uploaded.
* @async
*/
async uploadFile(file: FileInformation) { async uploadFile(file: FileInformation) {
if (this.resource?.id) { if (!this.project?.id || !this.resource?.id) {
const result = await this.resourceStore.storeMetadata( return;
}
// Specify the target content type
const contentType = "text/turtle";
// Create the metadata tree DTO and serialize the definition
const metadataTreeForCreationDto: MetadataTreeForCreationDto = {
path: file.path,
definition: await serializeRDFDefinition(file.metadata, contentType),
format: contentType as RdfFormat,
};
// Trigger metadata tree creation. Metadata might be already present, so we try updating it if true.
const success = await this.resourceStore.addOrUpdateMetadataTree(
this.project.id,
this.resource.id, this.resource.id,
file.absolutePath, metadataTreeForCreationDto
file.metadata
); );
if (result) {
if (success) {
// Check if the file is not editable via data URL and has info, and if so, handle its content upload
if (!this.editableDataUrl && file.info) { if (!this.editableDataUrl && file.info) {
await this.handleUploadContent(file.info, file); await this.handleUploadContent(file.info, file);
} else if (file.dataUrl !== undefined) { }
// Else, if the file has a data URL, convert it to a blob and handle its content upload
else if (file.dataUrl !== undefined) {
this.fillData(file); this.fillData(file);
const blob = new Blob([file.dataUrl], { type: "plain/text" }); const blob = new Blob([file.dataUrl], { type: "plain/text" });
await this.handleUploadContent(blob, file); await this.handleUploadContent(blob, file);
} }
} else { } else {
// On Failure, post a generic warning notification
this.notificationStore.postNotification({
title: this.$t("toast.apiError.failure.title").toString(),
body: this.$t("toast.apiError.failure.message").toString(),
variant: "warning",
});
// If metadata tree creation was not successful, adjust the count of files remaining
this.adjustRemainingFiles(file, true); this.adjustRemainingFiles(file, true);
} }
}
}, },
/**
* Converts a FileInformation's dataUrl into a File object.
* If no dataUrl exists, initializes it as an empty string.
*
* @param {FileInformation} file - The file information to process.
*/
fillData(file: FileInformation) { fillData(file: FileInformation) {
if (!file.dataUrl) { if (!file.dataUrl) {
file.dataUrl = ""; file.dataUrl = "";
...@@ -567,34 +713,63 @@ export default defineComponent({ ...@@ -567,34 +713,63 @@ export default defineComponent({
type: "text/plain", type: "text/plain",
}); });
}, },
/**
* Handles the uploading of file contents.
* Depending on whether the file already exists, either updates or creates a new blob.
* Emits updated folder contents if necessary.
*
* @param {Blob} contents - The file contents to upload.
* @param {FileInformation} file - The file information.
* @async
*/
async handleUploadContent(contents: Blob, file: FileInformation) { async handleUploadContent(contents: Blob, file: FileInformation) {
if (this.resource?.id) { if (this.project?.id && this.resource?.id) {
const fileExists = this.folderContents.some(
(x) => x.name === file.name
);
this.progressStatus = 0; this.progressStatus = 0;
const result = await this.resourceStore.storeFile(
this.resource.id, // Update progress as the file is uploaded
file.absolutePath, const onUploadProgress = (progressEvent: ProgressEvent) => {
[contents],
{
onUploadProgress: (progressEvent: ProgressEvent) => {
this.progressStatus = Math.round( this.progressStatus = Math.round(
(progressEvent.loaded * 100) / progressEvent.total (progressEvent.loaded * 100) / progressEvent.total
); );
}, };
}
// Depending on the file's existence, either update or create the blob
const uploadResult = fileExists
? await this.resourceStore.updateBlob(
this.project.id,
this.resource.id,
file.path,
contents,
{ onUploadProgress }
)
: await this.resourceStore.createBlob(
this.project.id,
this.resource.id,
file.path,
contents,
{ onUploadProgress }
); );
if (!result) {
if (!uploadResult) {
this.adjustRemainingFiles(file, true); this.adjustRemainingFiles(file, true);
} }
}
// Update or emit the updated folder contents
const entry = this.folderContents.find((x) => x.name === file.name); const entry = this.folderContents.find((x) => x.name === file.name);
if (entry === undefined) { if (entry === undefined) {
const newRow: FileInformation = { const newRow: FileInformation = {
id: uuidv4(), id: uuidv4(),
isFolder: false, isFolder: false,
name: file.name, name: file.name,
absolutePath: file.absolutePath, type: file.type,
parentDirectory: file.parentDirectory,
lastModified: new Date().toString(), lastModified: new Date().toString(),
created: new Date().toString(), createdAt: new Date().toString(),
size: file.size, size: file.size,
path: file.path, path: file.path,
info: file.info, info: file.info,
...@@ -602,74 +777,124 @@ export default defineComponent({ ...@@ -602,74 +777,124 @@ export default defineComponent({
version: `${+new Date()}`, version: `${+new Date()}`,
}; };
MetadataManagerUtil.copyMetadata(file.metadata, newRow); MetadataManagerUtil.copyMetadata(file.metadata, newRow);
this.$emit("folderContents", [...this.folderContents, newRow]); this.$emit("navigateTree");
} else { } else {
entry.lastModified = new Date().toString(); entry.lastModified = new Date().toString();
entry.created = new Date().toString(); entry.createdAt = new Date().toString();
if (!entry.isFolder) { if (!entry.isFolder) {
entry.size = file.size; entry.size = file.size;
} }
MetadataManagerUtil.copyMetadata(file.metadata, entry); MetadataManagerUtil.copyMetadata(file.metadata, entry);
} }
this.adjustRemainingFiles(file); this.adjustRemainingFiles(file);
}
}, },
/**
* Downloads selected files.
* If the detail view is active, attempts to download each shown file.
*
* @async
*/
async download() { async download() {
if (this.showDetail && this.resource?.id) { if (this.showDetail && this.project?.id && this.resource?.id) {
for (const editableFile of this.shownFiles) { for (const editableFile of this.shownFiles) {
const response = await this.resourceStore.getFile( const response = await this.resourceStore.getBlob(
this.project.id,
this.resource.id, this.resource.id,
editableFile.absolutePath, editableFile.path
true );
if (response !== null) {
fileSaver.saveAs(
new Blob([response.data], {
type: response.headers["content-type"],
}),
editableFile.name
); );
if (response) {
fileSaver.saveAs(response, editableFile.name);
} }
} }
} }
}, },
/**
* Emits an event signaling the selection of files.
*/
selectFiles() { selectFiles() {
this.$emit("clickFileSelect"); this.$emit("clickFileSelect");
}, },
/**
* Asynchronously updates the resource's metadata based on the files in the edit list.
* It applies metadata templates, initializes remaining files, and then iterates over
* the edit list to update each file's metadata. It also handles file uploads when
* necessary.
*
* @async
*/
async update() { async update() {
if (this.resource?.id) { if (!this.project?.id || !this.resource?.id) {
return;
}
// Specify the target content type
const contentType = "text/turtle";
// If this is the initial file, apply the metadata template
if (this.currentFileId === -1) { if (this.currentFileId === -1) {
this.applyMetadataTemplate(); this.applyMetadataTemplate();
} }
// Initialize files yet to be processed
this.initRemainingFiles(this.fileListEdit.length); this.initRemainingFiles(this.fileListEdit.length);
for (const editableFile of this.fileListEdit) { for (const file of this.fileListEdit) {
const result = await this.resourceStore.storeMetadata( // Create the metadata tree DTO and serialize the definition
const metadataTreeForUpdateDto: MetadataTreeForUpdateDto = {
path: file.path,
definition: await serializeRDFDefinition(file.metadata, contentType),
format: contentType as RdfFormat,
};
const success = await this.resourceStore.updateOrAddMetadataTree(
this.project.id,
this.resource.id, this.resource.id,
editableFile.absolutePath, metadataTreeForUpdateDto
editableFile.metadata
);
if (result) {
const tmp = this.folderContents.find(
(x) => x.name === editableFile.name
); );
if (success) {
// Find the corresponding file entry from folder contents
const tmp = this.folderContents.find((x) => x.name === file.name);
// If the file has a data URL and is editable, handle its content upload
if ( if (
this.editableDataUrl && this.editableDataUrl &&
!editableFile.isFolder && !file.isFolder &&
editableFile.dataUrl !== undefined file.dataUrl !== undefined
) { ) {
this.fillData(editableFile); this.fillData(file);
const blob = new Blob([editableFile.dataUrl], { const blob = new Blob([file.dataUrl], {
type: "plain/text", type: "plain/text",
}); });
await this.handleUploadContent(blob, editableFile); await this.handleUploadContent(blob, file);
} else { } else {
MetadataManagerUtil.copyMetadata(editableFile.metadata, tmp); // If not a file for upload, just copy the metadata
this.adjustRemainingFiles(editableFile); MetadataManagerUtil.copyMetadata(file.metadata, tmp);
this.adjustRemainingFiles(file);
} }
} else { } else {
this.adjustRemainingFiles(editableFile, true); // If the update was not successful, adjust the remaining files count
} this.adjustRemainingFiles(file, true);
} }
} }
}, },
/**
* Retrieves blob options for each shown file, if applicable.
* Updates the data URL for each file from the store.
*
* @async
*/
async getOptions() { async getOptions() {
if (this.resource?.id) { if (this.project?.id && this.resource?.id) {
if (this.shownFiles.length === 1) { if (this.shownFiles.length === 1) {
this.currentFileId = 0; this.currentFileId = 0;
} }
...@@ -681,51 +906,89 @@ export default defineComponent({ ...@@ -681,51 +906,89 @@ export default defineComponent({
return; return;
} }
element.requesting = true; element.requesting = true;
const response = await this.resourceStore.getFile( const response = await this.resourceStore.getBlob(
this.project.id,
this.resource.id, this.resource.id,
element.absolutePath element.path
); );
this.$set(element, "dataUrl", response); if (response !== null) {
this.$set(element, "dataUrl", response.data);
}
element.requesting = false; element.requesting = false;
} }
} }
} }
} }
}, },
/**
* Updates the absolute path of the current folder content based on a new value.
*
* @param {string} value - The new value for the folder content's name.
*/
updateAbsolutePath(value: string) { updateAbsolutePath(value: string) {
if (this.currentFolderContent) { if (this.currentFolderContent) {
this.currentFolderContent.name = value; this.currentFolderContent.name = value;
const currentFolder = this.dirCrumbs.join("");
this.currentFolderContent.path = this.currentFolderContent.path =
this.currentFolderContent.path !== "" currentFolder + this.currentFolderContent.name;
? this.currentFolderContent.path
: "/";
this.currentFolderContent.absolutePath =
this.currentFolderContent.path + this.currentFolderContent.name;
} }
}, },
/**
* Updates the data URL and size of the current folder content based on a new value.
*
* @param {string} value - The new value for the data URL.
*/
updateDataUrl(value: string) { updateDataUrl(value: string) {
if (this.currentFolderContent && !this.currentFolderContent.isFolder) { if (this.currentFolderContent && !this.currentFolderContent.isFolder) {
this.currentFolderContent.dataUrl = value; this.currentFolderContent.dataUrl = value;
this.currentFolderContent.size = value.length; this.currentFolderContent.size = value.length;
} }
}, },
/**
* Emits an event to clean up file lists.
*/
cleanup() { cleanup() {
this.$emit("emptyFileLists"); this.$emit("emptyFileLists", true);
}, },
/**
* Emits an event to display the modal for deleting folder contents.
*/
showModalDeleteFolderContents() { showModalDeleteFolderContents() {
this.$emit("showModalDelete", this.fileListEdit); this.$emit("showModalDelete", this.fileListEdit);
}, },
/**
* Displays the modal for saving duplicate files.
*/
showModalSaveDuplicateFiles() { showModalSaveDuplicateFiles() {
this.saveDuplicateFilesModalVisible = true; this.saveDuplicateFilesModalVisible = true;
}, },
/**
* Hides the modal for saving duplicate files.
*/
hideModalSaveDuplicateFiles() { hideModalSaveDuplicateFiles() {
this.saveDuplicateFilesModalVisible = false; this.saveDuplicateFilesModalVisible = false;
}, },
/**
* Handles the overwrite action in the modal for saving duplicate files.
* Initiates the upload process afterward.
*/
overwriteModalSaveDuplicateFiles() { overwriteModalSaveDuplicateFiles() {
this.uploadDuplicates = true; this.uploadDuplicates = true;
this.saveDuplicateFilesModalVisible = false; this.saveDuplicateFilesModalVisible = false;
this.upload(); this.upload();
}, },
/**
* Handles the skip action in the modal for saving duplicate files.
* Initiates the upload process afterward.
*/
skipModalSaveDuplicateFiles() { skipModalSaveDuplicateFiles() {
this.uploadDuplicates = false; this.uploadDuplicates = false;
this.saveDuplicateFilesModalVisible = false; this.saveDuplicateFilesModalVisible = false;
......
<template> <template>
<span> <span>
<!-- File Name -->
<CoscineFormGroup :label="$t('page.resource.infoFileName')"> <CoscineFormGroup :label="$t('page.resource.infoFileName')">
<div <div
id="fileInfoFieldID" id="fileInfofileName"
class="fileInfoField"
data-toggle="tooltip" data-toggle="tooltip"
:title="currentFile.name" :title="currentFile.name"
> >
...@@ -14,42 +14,39 @@ ...@@ -14,42 +14,39 @@
}} }}
</div> </div>
</CoscineFormGroup> </CoscineFormGroup>
<!-- Last Modified -->
<CoscineFormGroup :label="$t('page.resource.infoFileLastModified')"> <CoscineFormGroup :label="$t('page.resource.infoFileLastModified')">
<div class="fileInfoField">
{{ {{
!currentFile.lastModified !currentFile.lastModified
? $t("page.resource.infoFileNoInformation") ? $t("page.resource.infoFileNoInformation")
: new Date(currentFile.lastModified).toLocaleDateString( : new Date(currentFile.lastModified).toLocaleDateString($i18n.locale)
$i18n.locale
)
}} }}
</div>
</CoscineFormGroup> </CoscineFormGroup>
<!-- Uploaded -->
<CoscineFormGroup :label="$t('page.resource.infoFileCreated')"> <CoscineFormGroup :label="$t('page.resource.infoFileCreated')">
<div class="fileInfoField">
{{ {{
!currentFile.created !currentFile.createdAt
? $t("page.resource.infoFileNoInformation") ? $t("page.resource.infoFileNoInformation")
: new Date(currentFile.created).toLocaleDateString($i18n.locale) : new Date(currentFile.createdAt).toLocaleDateString($i18n.locale)
}} }}
</div>
</CoscineFormGroup> </CoscineFormGroup>
<!-- File Size -->
<CoscineFormGroup :label="$t('page.resource.infoFileSize')"> <CoscineFormGroup :label="$t('page.resource.infoFileSize')">
<div class="fileInfoField">
{{ {{
currentFile.isFolder === false currentFile.isFolder === false
? currentFile.size + " Bytes" ? currentFile.size + " Bytes"
: $t("page.resource.infoFileNoInformation") : $t("page.resource.infoFileNoInformation")
}} }}
</div>
</CoscineFormGroup> </CoscineFormGroup>
</span> </span>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, type PropType } from "vue"; import { defineComponent, type PropType } from "vue";
import type { FolderContent } from "@/modules/resource/types";
import type { FolderContent } from "../../../utils/EntryDefinition";
export default defineComponent({ export default defineComponent({
props: { props: {
...@@ -71,12 +68,7 @@ export default defineComponent({ ...@@ -71,12 +68,7 @@ export default defineComponent({
</script> </script>
<style scoped> <style scoped>
.fileInfoField { #fileInfofileName {
padding-top: calc(0.375rem + 1px);
padding-bottom: calc(0.375rem + 1px);
}
#fileInfoFieldID {
margin-left: auto;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
......
...@@ -65,20 +65,20 @@ ...@@ -65,20 +65,20 @@
<script lang="ts"> <script lang="ts">
import { defineComponent, type PropType } from "vue"; import { defineComponent, type PropType } from "vue";
import useProjectStore from "@/modules/project/store"; import useProjectStore from "@/modules/project/store";
// import the store for current module // import the store for current module
import useResourceStore from "../../../store"; import useResourceStore from "../../../store";
import type { FolderContent } from "../../../utils/EntryDefinition";
import type { TranslateResult } from "vue-i18n"; import type { TranslateResult } from "vue-i18n";
import type { import type {
GitlabBranchDto, GitlabBranchDto,
ResourceDto, ResourceDto,
ResourceTypeInformationDto, ResourceTypeInformationDto,
} from "@coscine/api-client/dist/types/Coscine.Api"; } from "@coscine/api-client/dist/types/Coscine.Api";
import { CoscineResourceTypes } from "@/modules/resource/types"; import {
CoscineResourceTypes,
type FolderContent,
} from "@/modules/resource/types";
export default defineComponent({ export default defineComponent({
props: { props: {
editableDataUrl: { editableDataUrl: {
...@@ -168,7 +168,6 @@ export default defineComponent({ ...@@ -168,7 +168,6 @@ export default defineComponent({
const accessToken = const accessToken =
this.resource.type?.options?.gitLabOptions?.accessToken; this.resource.type?.options?.gitLabOptions?.accessToken;
const currentBranch = this.resource.type?.options?.gitLabOptions?.branch; const currentBranch = this.resource.type?.options?.gitLabOptions?.branch;
if (projectId && domain && accessToken && currentBranch) { if (projectId && domain && accessToken && currentBranch) {
const gitlabBranches = const gitlabBranches =
await this.resourceStore.getGitlabBranchesForProject( await this.resourceStore.getGitlabBranchesForProject(
......
<template> <template>
<div> <div>
<!-- Data URL -->
<span v-if="editableDataUrl && !currentFolderContent.isFolder"> <span v-if="editableDataUrl && !currentFolderContent.isFolder">
<CoscineFormGroup <CoscineFormGroup
:mandatory="true" :mandatory="true"
...@@ -21,6 +22,8 @@ ...@@ -21,6 +22,8 @@
</b-input-group> </b-input-group>
</CoscineFormGroup> </CoscineFormGroup>
</span> </span>
<!-- Entry Name - Editable Key -->
<span v-if="editableKey"> <span v-if="editableKey">
<CoscineFormGroup <CoscineFormGroup
:mandatory="true" :mandatory="true"
...@@ -43,7 +46,7 @@ ...@@ -43,7 +46,7 @@
import { defineComponent, type PropType } from "vue"; import { defineComponent, type PropType } from "vue";
import useProjectStore from "@/modules/project/store"; import useProjectStore from "@/modules/project/store";
import type { FolderContent } from "../../../utils/EntryDefinition"; import type { FolderContent } from "@/modules/resource/types";
import type { ResourceDto } from "@coscine/api-client/dist/types/Coscine.Api"; import type { ResourceDto } from "@coscine/api-client/dist/types/Coscine.Api";
export default defineComponent({ export default defineComponent({
......