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
Showing
with 729 additions and 348 deletions
...@@ -72,9 +72,9 @@ ...@@ -72,9 +72,9 @@
{{ currentFolderContent.name }} {{ currentFolderContent.name }}
</span> </span>
</span> </span>
<span v-else-if="shownFiles.length > 0">{{ <span v-else-if="shownFiles.length > 0">
$t("page.resource.allFiles") {{ $t("page.resource.allFiles") }}
}}</span> </span>
</b-col> </b-col>
</b-row> </b-row>
</div> </div>
...@@ -83,7 +83,7 @@ ...@@ -83,7 +83,7 @@
<script lang="ts"> <script lang="ts">
import { defineComponent, type PropType } from "vue"; import { defineComponent, type PropType } from "vue";
import type { FolderContent } from "../../../utils/EntryDefinition"; import type { FolderContent } from "@/modules/resource/types";
export default defineComponent({ export default defineComponent({
props: { props: {
......
...@@ -39,7 +39,7 @@ ...@@ -39,7 +39,7 @@
<script lang="ts"> <script lang="ts">
import { defineComponent, type PropType } from "vue"; import { defineComponent, type PropType } from "vue";
import type { FolderContent } from "../../../utils/EntryDefinition"; import type { FolderContent } from "@/modules/resource/types";
export default defineComponent({ export default defineComponent({
props: { props: {
......
...@@ -56,8 +56,8 @@ import { defineComponent, type PropType } from "vue"; ...@@ -56,8 +56,8 @@ import { defineComponent, 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 type { VisitedResourceObject } from "../../../types"; import type { FileInformation, VisitedResourceObject } from "../../../types";
import type { FileInformation } from "@/modules/resource/utils/EntryDefinition";
import type { ResourceTypeInformationDto } from "@coscine/api-client/dist/types/Coscine.Api"; import type { ResourceTypeInformationDto } from "@coscine/api-client/dist/types/Coscine.Api";
export default defineComponent({ export default defineComponent({
......
/* 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,14 +15,19 @@ import { PiniaVuePlugin } from "pinia"; ...@@ -15,14 +15,19 @@ import { PiniaVuePlugin } from "pinia";
/* Tested Component */ /* Tested Component */
import ResourcePage from "./ResourcePage.vue"; import ResourcePage from "./ResourcePage.vue";
import VueRouter from "vue-router";
import { routes } from "@/router";
import type Vue from "vue"; import Vue from "vue";
import { getTestUserState } from "@/data/mockup/testUser"; 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,12 +36,18 @@ function sleep(ms: number) { ...@@ -31,12 +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("ResourcePage.vue", () => { // Define the Vue instance type (computed properties)
/* Checks for correct default handling of RCV page */ interface ResourcePageComponent extends Vue {
test( dirTrail: string;
"defaultHandling", }
async () => {
describe("ResourcePage.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: {
...@@ -46,46 +57,66 @@ describe("ResourcePage.vue", () => { ...@@ -46,46 +57,66 @@ describe("ResourcePage.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(ResourcePage as unknown as typeof Vue, { beforeEach(() => {
// shallowMount does not work here!
wrapper = mount(ResourcePage as unknown as typeof Vue, {
pinia: testingPinia, pinia: testingPinia,
router,
i18n, i18n,
localVue, localVue,
}) as Wrapper<ResourcePageComponent>;
}); });
test(
"Should render form fields and file entries and reflect metadata upon selection",
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!
// Form-Generator rendered // Form-Generator rendered
let textFields = wrapper.findAllComponents({ name: "InputTextField" }); // The test resource uses the Base Application profile,
expect(textFields.length).toBeGreaterThanOrEqual(4); // which has has 5 fields (2x text, 1x date, 2x combo box)
let wrapperInputFields = wrapper.findAllComponents({
name: "WrapperInput",
});
expect(wrapperInputFields.length).toBe(5);
// Entries in table rendered // File-View rendered with as many entries as the test resource has files/folders
const tableFields = wrapper.findAll(".dataSourceItem"); const tableFields = wrapper.findAll(".fileViewEntry");
expect(tableFields.length).toBeGreaterThanOrEqual(6); expect(tableFields.length).toBeGreaterThanOrEqual(3);
// Find first entry checkbox // Find the first entry in the table and select it
const firstFound = wrapper.find(".tableCheck .custom-control-input"); const firstFound = wrapper.find(".tableCheck .custom-control-input");
await firstFound.setChecked(true); await firstFound.setChecked(true);
// Wait for 1 second until everything is updated // Wait for 1 second until everything is updated
await sleep(1000); await sleep(1000); // Don't remove!
// After selecting an entry in the table, its metadata should be rendered in the metadata manager // After selecting an entry in the table, its metadata should be rendered in the metadata manager
textFields = wrapper.findAllComponents({ name: "InputTextField" }); wrapperInputFields = wrapper.findAllComponents({
const textInput = textFields.wrappers[1]; name: "InputTextField",
expect((textInput.element as HTMLInputElement).value).toBe("Test"); });
const textInput = wrapperInputFields.wrappers[0]; // For the used AP, the first InputTextField is "Title"
expect((textInput.element as HTMLInputElement).value).toBe(
"Title inside Form Generator"
);
}, },
{ {
// Override the maximum run time for this test (10 sec), due to the sleep() calls
timeout: 10000, timeout: 10000,
} }
); );
......
...@@ -7,33 +7,46 @@ ...@@ -7,33 +7,46 @@
@dragover.prevent="" @dragover.prevent=""
@drop.prevent="uploadDrop" @drop.prevent="uploadDrop"
> >
<!-- Drop File to Upload -->
<div v-if="showDroppable && fileAddable" class="droppable"> <div v-if="showDroppable && fileAddable" class="droppable">
<p class="droppableText">{{ $t("page.resource.canDropFile") }}</p> <p class="droppableText">
{{ $t("page.resource.canDropFile") }}
</p>
</div> </div>
<!-- Headline -->
<coscine-headline <coscine-headline
v-show="!isFullscreen" v-show="!isFullscreenActive"
:headline="$t('page.resource.resources')" :headline="$t('page.resource.resources')"
/> />
<!-- Form File Window -->
<b-form-file <b-form-file
ref="fileTrigger" ref="fileTrigger"
multiple multiple
class="mt-3" class="mt-3"
plain plain
@input="fileListUploadSelected" @input="fileListUploadSelected"
></b-form-file> />
<!-- Files View Column -->
<span id="filesViewSpan"> <span id="filesViewSpan">
<div id="filesViewCard" :class="isFullscreen == false ? 'card' : ''"> <div
<div :class="isFullscreen == false ? 'card-body' : ''"> id="filesViewCard"
:class="isFullscreenActive == false ? 'card' : ''"
>
<!-- Files View -->
<div :class="isFullscreenActive == false ? 'card-body' : ''">
<FilesView <FilesView
ref="filesView" ref="filesView"
:folder-contents="folderContents" :folder-contents="folderContents"
:current-folder="currentFolder"
:is-uploading="isUploading" :is-uploading="isUploading"
:file-list-edit="fileListEdit" :file-list-edit="fileListEdit"
:dir-trail="dirTrail"
:dir-crumbs="dirCrumbs"
@showDetail="setShowDetail" @showDetail="setShowDetail"
@currentFolder="setCurrentFolder" @folderContents="updateFolderContent"
@folderContents="setFolder" @fileListEdit="updateEditFileList"
@fileListEdit="setFileListEdit"
@clickFileSelect="clickFileSelect" @clickFileSelect="clickFileSelect"
@showModalDelete="showModalDelete" @showModalDelete="showModalDelete"
@waitingForResponse="setWaitingForResponse" @waitingForResponse="setWaitingForResponse"
...@@ -41,17 +54,25 @@ ...@@ -41,17 +54,25 @@
</div> </div>
</div> </div>
</span> </span>
<!-- Metadata Manager Column -->
<div <div
id="metadataManagerDiv" id="metadataManagerDiv"
:class="isMetadataManagerHidden == true ? 'hiddenMetadataManager' : ''" :class="{ hiddenMetadataManager: isMetadataManagerHidden }"
> >
<!-- Vertical Button Bar for Fullscreen -->
<b-button <b-button
v-show="isFullscreen" v-show="isFullscreenActive"
id="metadataManagerToggleFullscreen" id="metadataManagerToggleFullscreen"
squared squared
@click="toggleMenu()" @click="toggleMetadataPanel()"
><span>{{ $t("page.resource.metadataManager") }}</span></b-button
> >
<span>
{{ $t("page.resource.metadataManager") }}
</span>
</b-button>
<!-- Metadata Manager -->
<div class="card"> <div class="card">
<div class="card-body"> <div class="card-body">
<MetadataManager <MetadataManager
...@@ -59,44 +80,37 @@ ...@@ -59,44 +80,37 @@
:file-list-edit="fileListEdit" :file-list-edit="fileListEdit"
:file-list-upload="fileListUpload" :file-list-upload="fileListUpload"
:folder-contents="folderContents" :folder-contents="folderContents"
:current-folder="currentFolder" :dir-trail="dirTrail"
:dir-crumbs="dirCrumbs"
:is-uploading="isUploading" :is-uploading="isUploading"
@emptyFileLists="emptyFileLists" @emptyFileLists="emptyFileLists"
@folderContents="setFolder" @navigateTree="navigateTreeInFilesView"
@removeElement="removeElement" @removeElement="deleteFromUploadList"
@removeSelection="removeSelection" @removeSelection="removeSelection"
@isUploading="setIsUploading" @isUploading="setIsUploading"
@showModalDelete="showModalDelete" @showModalDelete="showModalDelete"
@clickFileSelect="clickFileSelect" @clickFileSelect="clickFileSelect"
/> />
<!-- Toggle Fullscreen Button -->
<div <div
id="toggleFullscreenButton" id="toggleFullscreenButton"
:class=" :class="{ hiddenMetadataManager: !isMetadataManagerHidden }"
isMetadataManagerHidden == true ? '' : 'hiddenMetadataManager'
"
> >
<button <button class="btn btn-secondary" @click="toggleViewMode">
v-if="isFullscreen" <b-icon
class="btn btn-secondary" :icon="isFullscreenActive ? 'fullscreen-exit' : 'fullscreen'"
type="button" />
@click="toggleFullscreen"
>
<b-icon icon="fullscreen-exit" />
</button>
<button
v-else
type="button"
class="btn btn-secondary"
@click="toggleFullscreen"
>
<b-icon icon="fullscreen" />
</button> </button>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- Page Loading Spinner -->
<LoadingSpinner :is-waiting-for-response="isWaitingForResponse" /> <LoadingSpinner :is-waiting-for-response="isWaitingForResponse" />
<!-- Delete RCV File Modal -->
<!-- Delete File Modal -->
<DeleteFolderContentsModal <DeleteFolderContentsModal
:visible="deleteModalVisible" :visible="deleteModalVisible"
:shown-files="filesToBeDeleted" :shown-files="filesToBeDeleted"
...@@ -109,33 +123,25 @@ ...@@ -109,33 +123,25 @@
<script lang="ts"> <script lang="ts">
import { defineComponent } from "vue"; import { defineComponent } from "vue";
import type Vue from "vue"; import type Vue from "vue";
// import the store for current module // import the store for current module
import useResourceStore from "../store"; import useResourceStore from "../store";
// import the main store // import the main store
import useMainStore from "@/store/index"; import useMainStore from "@/store/index";
import useProjectStore from "@/modules/project/store"; import useProjectStore from "@/modules/project/store";
import FilesView from "../components/resource-page/FilesView.vue"; import FilesView from "../components/resource-page/FilesView.vue";
import MetadataManager from "../components/resource-page/MetadataManager.vue"; import MetadataManager from "../components/resource-page/MetadataManager.vue";
import DeleteFolderContentsModal from "../components/resource-page/modals/DeleteFolderContentsModal.vue"; import DeleteFolderContentsModal from "../components/resource-page/modals/DeleteFolderContentsModal.vue";
import type {
FileInformation,
FolderContent,
FolderInformation,
} from "../utils/EntryDefinition";
import { v4 as uuidv4 } from "uuid"; import { v4 as uuidv4 } from "uuid";
import factory from "rdf-ext"; import factory from "rdf-ext";
import type { BFormFile, BTable } from "bootstrap-vue"; import type { BFormFile, BTable } from "bootstrap-vue";
import type { Dataset } from "@rdfjs/types"; import type { Dataset } from "@rdfjs/types";
import type { import type {
ProjectDto,
ResourceDto, ResourceDto,
ResourceTypeInformationDto, ResourceTypeInformationDto,
TreeDataType,
} from "@coscine/api-client/dist/types/Coscine.Api"; } from "@coscine/api-client/dist/types/Coscine.Api";
import type { FolderContent, FileInformation } from "../types";
export default defineComponent({ export default defineComponent({
components: { components: {
...@@ -143,10 +149,12 @@ export default defineComponent({ ...@@ -143,10 +149,12 @@ export default defineComponent({
MetadataManager, MetadataManager,
DeleteFolderContentsModal, DeleteFolderContentsModal,
}, },
beforeRouteLeave(to, from, next) { beforeRouteLeave(to, from, next) {
this.setFullscreen(false); this.updateViewMode(false);
next(); next();
}, },
setup() { setup() {
const mainStore = useMainStore(); const mainStore = useMainStore();
const projectStore = useProjectStore(); const projectStore = useProjectStore();
...@@ -158,18 +166,14 @@ export default defineComponent({ ...@@ -158,18 +166,14 @@ export default defineComponent({
data() { data() {
return { return {
isWaitingForResponse: false, isWaitingForResponse: false,
fileListEdit: [] as FolderContent[],
showDetail: false, showDetail: false,
fileListEdit: [] as FolderContent[],
fileListUpload: [] as FileInformation[], fileListUpload: [] as FileInformation[],
folderContents: [] as FolderContent[],
filesToBeDeleted: [] as FolderContent[], filesToBeDeleted: [] as FolderContent[],
currentFolder: folderContents: [] as FolderContent[],
window.location.hash.indexOf("#") !== -1
? window.location.hash.substring(1)
: "/",
dragCounter: 0, dragCounter: 0,
isFullscreen: false, isFullscreenActive: false,
isMetadataManagerHidden: false, isMetadataManagerHidden: false,
isUploading: false, isUploading: false,
...@@ -181,6 +185,9 @@ export default defineComponent({ ...@@ -181,6 +185,9 @@ export default defineComponent({
resource(): null | ResourceDto { resource(): null | ResourceDto {
return this.resourceStore.currentResource; return this.resourceStore.currentResource;
}, },
project(): null | ProjectDto {
return this.projectStore.currentProject;
},
resourceTypeInformation(): ResourceTypeInformationDto | undefined { resourceTypeInformation(): ResourceTypeInformationDto | undefined {
return this.resourceStore.enabledResourceTypes?.find( return this.resourceStore.enabledResourceTypes?.find(
(resourceType) => resourceType.id === this.resource?.type?.id (resourceType) => resourceType.id === this.resource?.type?.id
...@@ -200,7 +207,22 @@ export default defineComponent({ ...@@ -200,7 +207,22 @@ export default defineComponent({
isGuest(): boolean | undefined { isGuest(): boolean | undefined {
return this.projectStore.currentUserRoleIsGuest; return this.projectStore.currentUserRoleIsGuest;
}, },
dirTrail(): string {
return this.$route.params.dirTrail ?? "";
}, },
dirCrumbs(): string[] {
const trail = this.dirTrail;
const pathArray = trail
.substring(0, trail.lastIndexOf("/") + 1)
.split("/")
.filter((n) => n);
return pathArray.map((f) => {
return f ? `${f}/` : "";
});
},
},
watch: { watch: {
fileListEdit() { fileListEdit() {
if (this.fileListEdit.length > 0) { if (this.fileListEdit.length > 0) {
...@@ -211,23 +233,30 @@ export default defineComponent({ ...@@ -211,23 +233,30 @@ export default defineComponent({
this.emptyFileLists(); this.emptyFileLists();
}, },
}, },
mounted() { mounted() {
this.$nextTick(() => { this.$nextTick(() => {
window.addEventListener("resize", this.getWindowWidth); window.addEventListener("resize", this.adjustViewForScreenWidth);
this.getWindowWidth(); this.adjustViewForScreenWidth();
}); });
this.emptyFileLists(); this.emptyFileLists();
}, },
methods: { methods: {
/**
* Trigger file selection, if not editable data URL is provided.
*/
clickFileSelect() { clickFileSelect() {
this.showDetail = false; this.showDetail = false;
// Check if editableDataUrl is present
if ( if (
this.resourceTypeInformation?.resourceContent?.metadataView this.resourceTypeInformation?.resourceContent?.metadataView
?.editableDataUrl ?.editableDataUrl
) { ) {
this.emptyFileLists(); this.emptyFileLists();
} else { } else {
// Trigger a click event on fileTrigger element
(this.$refs.fileTrigger as BFormFile).$el.dispatchEvent( (this.$refs.fileTrigger as BFormFile).$el.dispatchEvent(
new MouseEvent("click", { new MouseEvent("click", {
view: window, view: window,
...@@ -237,37 +266,75 @@ export default defineComponent({ ...@@ -237,37 +266,75 @@ export default defineComponent({
); );
} }
}, },
/**
* Display the modal for file deletion and set the files to be deleted.
* @param {FolderContent[]} files - Files to be deleted.
*/
showModalDelete(files: FolderContent[]) { showModalDelete(files: FolderContent[]) {
//open modal // Open modal
this.deleteModalVisible = true; this.deleteModalVisible = true;
//pass files from emit // Pass files from emit
this.filesToBeDeleted = files; this.filesToBeDeleted = files;
}, },
/**
* Close the delete modal and reset the filesToBeDeleted list.
*/
closeModalDeleteFile() {
this.deleteModalVisible = false;
this.filesToBeDeleted = [];
},
/**
* Trigger a navigation in the child component to update the view.
*/
async navigateTreeInFilesView() {
await (
this.$refs.filesView as unknown as typeof FilesView
).navigateTree();
},
/**
* Delete selected files.
*/
async deleteFiles() { async deleteFiles() {
if (this.resource?.id) { if (this.resource?.id && this.project?.id) {
for (const fileToDelete of this.filesToBeDeleted) { for (const fileToDelete of this.filesToBeDeleted) {
this.$emit("waitingForResponse", true); this.$emit("waitingForResponse", true);
await this.resourceStore.deleteFile(
await this.resourceStore.deleteBlob(
this.project.id,
this.resource.id, this.resource.id,
fileToDelete.absolutePath fileToDelete.path
); );
this.$emit("waitingForResponse", false);
} }
location.reload(); this.closeModalDeleteFile();
// Trigger a navigation in the child component to update the view after deletion
await this.navigateTreeInFilesView();
this.$emit("waitingForResponse", false);
} }
}, },
closeModalDeleteFile() { /**
this.deleteModalVisible = false; * Handle drag enter event. Increase drag counter on dragEnter.
this.filesToBeDeleted = []; */
},
dragEnter() { dragEnter() {
this.dragCounter++; this.dragCounter++;
}, },
/**
* Handle drag leave event. Decrease drag counter on dragLeave.
*/
dragLeave() { dragLeave() {
this.dragCounter--; this.dragCounter--;
}, },
emptyFileLists(emptyUpload = true) {
/**
* Clear the file lists.
* @param {boolean} emptyUpload - Indicates whether to clear upload list as well.
*/
emptyFileLists(emptyUpload: boolean = true) {
this.removeSelection(0, this.fileListEdit.length); this.removeSelection(0, this.fileListEdit.length);
this.fileListEdit.length = 0; this.fileListEdit.length = 0;
if (emptyUpload) { if (emptyUpload) {
...@@ -276,31 +343,43 @@ export default defineComponent({ ...@@ -276,31 +343,43 @@ export default defineComponent({
this.showDetail = false; this.showDetail = false;
this.initializeForResourceType(); this.initializeForResourceType();
}, },
/**
* Add selected files to the fileListUpload list.
* @param {File[] | File} selectedFiles - Selected files.
*/
fileListUploadSelected(selectedFiles: File[] | File) { fileListUploadSelected(selectedFiles: File[] | File) {
if (!Array.isArray(selectedFiles)) { if (!Array.isArray(selectedFiles)) {
selectedFiles = [selectedFiles]; selectedFiles = [selectedFiles];
} }
this.emptyFileLists(false); this.emptyFileLists(false);
// Pushing selected files into the upload list
for (const file of selectedFiles) { for (const file of selectedFiles) {
this.fileListUpload.push({ this.fileListUpload.push({
id: uuidv4(), id: uuidv4(),
path: this.currentFolder, path: this.dirTrail + file.name,
parentDirectory: this.dirTrail,
type: file.type,
version: `${+new Date()}`, version: `${+new Date()}`,
uploading: false, uploading: false,
info: file, info: file,
name: file.name, name: file.name,
size: file.size, size: file.size,
lastModified: "" + file.lastModified, lastModified: `${file.lastModified}`,
metadata: factory.dataset() as unknown as Dataset, metadata: factory.dataset() as unknown as Dataset,
isFolder: false, } as FileInformation);
absolutePath: this.currentFolder + file.name,
});
} }
// Clear selection in files view table
( (
(this.$refs.filesView as Vue).$refs.adaptTable as BTable (this.$refs.filesView as Vue).$refs.adaptTable as BTable
).clearSelected(); ).clearSelected();
this.showDetail = false; this.showDetail = false;
}, },
/**
* Initialize the file based on resource type.
*/
initializeForResourceType() { initializeForResourceType() {
if ( if (
this.resourceTypeInformation?.resourceContent?.metadataView this.resourceTypeInformation?.resourceContent?.metadataView
...@@ -313,16 +392,24 @@ export default defineComponent({ ...@@ -313,16 +392,24 @@ export default defineComponent({
uploading: false, uploading: false,
name: "", name: "",
isFolder: false, isFolder: false,
absolutePath: this.currentFolder, parentDirectory: this.dirTrail,
type: "Leaf" as TreeDataType.Leaf,
size: 0, size: 0,
metadata: factory.dataset() as unknown as Dataset, metadata: factory.dataset() as unknown as Dataset,
dataUrl: "", dataUrl: "",
}); } as FileInformation);
} }
}, },
/**
* Handle file drop action for upload.
* @param {DragEvent} ev - Drag event.
*/
uploadDrop(ev: DragEvent) { uploadDrop(ev: DragEvent) {
if (this.fileAddable) { if (this.fileAddable) {
this.dragCounter = 0; this.dragCounter = 0;
// Handling file drops
if (ev?.dataTransfer?.items) { if (ev?.dataTransfer?.items) {
for (const item of ev.dataTransfer.items) { for (const item of ev.dataTransfer.items) {
if (item.kind === "file") { if (item.kind === "file") {
...@@ -335,33 +422,75 @@ export default defineComponent({ ...@@ -335,33 +422,75 @@ export default defineComponent({
} }
} }
}, },
setShowDetail(newShowDetail: boolean) {
this.showDetail = newShowDetail; /**
}, * Update the detail visibility.
setWaitingForResponse(newIsWaitingForResponse: boolean) { *
this.isWaitingForResponse = newIsWaitingForResponse; * @param {boolean} visibility - New visibility state.
*/
setShowDetail(visibility: boolean) {
this.showDetail = visibility;
}, },
setIsUploading(newIsUploading: boolean) {
this.isUploading = newIsUploading; /**
* Update the state indicating if the system is waiting for a response.
*
* @param {boolean} waiting - Whether the system is waiting for a response or not.
*/
setWaitingForResponse(waiting: boolean) {
this.isWaitingForResponse = waiting;
}, },
setCurrentFolder(newCurrentFolder: string) {
this.currentFolder = newCurrentFolder; /**
* Update the uploading state.
*
* @param {boolean} uploading - Indicates if a file is currently uploading.
*/
setIsUploading(uploading: boolean) {
this.isUploading = uploading;
}, },
setFolder(newFolder: FolderInformation[]) {
this.folderContents = newFolder; /**
* Updates the current folder's content.
*
* @param {FolderContent[]} folderContent - Information about the items in the folder.
*/
updateFolderContent(folderContent: FolderContent[]) {
this.folderContents = folderContent;
}, },
setFileListEdit(newFileListEdit: FileInformation[]) {
this.fileListEdit = newFileListEdit; /**
* Updates the list of files being edited.
*
* @param {FolderContent[]} fileList - New list of files for editing.
*/
updateEditFileList(fileList: FolderContent[]) {
this.fileListEdit = fileList;
}, },
removeElement(index: number, count: number) {
this.fileListUpload.splice(index, count); /**
* Deletes files from the upload list.
*
* @param {number} startIndex - Where to start the deletion in the list.
* @param {number} numberOfFiles - How many files to delete from the list.
*/
deleteFromUploadList(startIndex: number, numberOfFiles: number) {
this.fileListUpload.splice(startIndex, numberOfFiles);
}, },
/**
* Remove selected items from the list.
* @param {number} index - Start index.
* @param {number} count - Number of items to remove.
*/
removeSelection(index: number, count: number) { removeSelection(index: number, count: number) {
const selectionRemovable: FolderContent[] = this.fileListEdit.splice( const selectionRemovable: FolderContent[] = this.fileListEdit.splice(
index, index,
count count
); );
const table = (this.$refs.filesView as Vue).$refs.adaptTable as BTable; const table = (this.$refs.filesView as Vue).$refs.adaptTable as BTable;
// Unselecting rows in table
for (let i = 0; i < table.items.length; i++) { for (let i = 0; i < table.items.length; i++) {
const item = (table.items as FolderContent[])[i]; const item = (table.items as FolderContent[])[i];
if ( if (
...@@ -372,28 +501,43 @@ export default defineComponent({ ...@@ -372,28 +501,43 @@ export default defineComponent({
} }
} }
}, },
setFullscreen(newIsFullscreen: boolean) {
this.isFullscreen = newIsFullscreen; /**
if (newIsFullscreen) { * Update the view mode based on the given fullscreen state.
document.body.classList.add("fullscreen"); *
* @param {boolean} activateFullscreen - Whether to activate the fullscreen mode.
*/
updateViewMode(activateFullscreen: boolean) {
this.isFullscreenActive = activateFullscreen;
if (activateFullscreen) {
document.body.classList.add("fullscreen-mode");
this.mainStore.sidebarActive = false; this.mainStore.sidebarActive = false;
} else { } else {
document.body.classList.remove("fullscreen"); document.body.classList.remove("fullscreen-mode");
this.isMetadataManagerHidden = false; this.isMetadataManagerHidden = true;
} }
}, },
toggleFullscreen() {
this.setFullscreen(!this.isFullscreen); /**
* Toggle between fullscreen and regular mode.
*/
toggleViewMode() {
this.updateViewMode(!this.isFullscreenActive);
}, },
toggleMenu() {
/**
* Toggle the visibility of the metadata panel.
*/
toggleMetadataPanel() {
this.isMetadataManagerHidden = !this.isMetadataManagerHidden; this.isMetadataManagerHidden = !this.isMetadataManagerHidden;
}, },
getWindowWidth() {
if (document.documentElement.clientWidth < 1250) { /**
this.setFullscreen(true); * Adjust view mode based on window width.
} else { */
this.setFullscreen(false); adjustViewForScreenWidth() {
} this.updateViewMode(document.documentElement.clientWidth < 1250);
}, },
}, },
}); });
......
...@@ -41,12 +41,29 @@ export const ResourceRoutes: RouteConfig[] = [ ...@@ -41,12 +41,29 @@ export const ResourceRoutes: RouteConfig[] = [
children: [ children: [
{ {
path: "/", path: "/",
redirect: (to) => {
return {
name: "resource-page", name: "resource-page",
component: ResourcePage, params: {
slug: to.params.slug,
guid: to.params.guid,
dirTrail: "",
},
meta: { meta: {
breadCrumb: "resource.page", breadCrumb: "resource.page",
requiresAuth: true, requiresAuth: true,
}, },
};
},
},
{
path: "-/:dirTrail(.*)*",
name: "resource-page",
pathToRegexpOptions: { strict: true, sensitive: true },
component: ResourcePage,
meta: {
requiresAuth: true,
},
}, },
{ {
path: "settings", path: "settings",
......
...@@ -19,13 +19,9 @@ import { ...@@ -19,13 +19,9 @@ import {
VocabularyApi, VocabularyApi,
} from "@coscine/api-client"; } from "@coscine/api-client";
import type { Route } from "vue-router/types/router"; import type { Route } from "vue-router/types/router";
import type { AxiosError } from "axios"; import axios, { AxiosError } from "axios";
import useNotificationStore from "@/store/notification"; import useNotificationStore from "@/store/notification";
import { import { parseRDFDefinition, resolveImports } from "./utils/linkedData";
parseRDFDefinition,
resolveImports,
serializeRDFDefinition,
} from "./utils/linkedData";
import factory from "rdf-ext"; import factory from "rdf-ext";
import { useLocalStorage } from "@vueuse/core"; import { useLocalStorage } from "@vueuse/core";
import type { import type {
...@@ -33,12 +29,16 @@ import type { ...@@ -33,12 +29,16 @@ import type {
ApplicationProfileDto, ApplicationProfileDto,
GitlabBranchDto, GitlabBranchDto,
GitlabProjectDto, GitlabProjectDto,
MetadataTreeForCreationDto,
MetadataTreeForUpdateDto,
ProjectDto, ProjectDto,
ResourceDto, ResourceDto,
ResourceForCreationDto, ResourceForCreationDto,
ResourceForUpdateDto, ResourceForUpdateDto,
ResourceQuotaDto, ResourceQuotaDto,
AcceptedLanguage, AcceptedLanguage,
FileDto,
MetadataDto,
} from "@coscine/api-client/dist/types/Coscine.Api/api"; } from "@coscine/api-client/dist/types/Coscine.Api/api";
import { wrapListRequest } from "@/util/wrapListRequest"; import { wrapListRequest } from "@/util/wrapListRequest";
/* /*
...@@ -159,10 +159,14 @@ export const useResourceStore = defineStore({ ...@@ -159,10 +159,14 @@ export const useResourceStore = defineStore({
"JsonLd" as RdfFormat "JsonLd" as RdfFormat
); );
const returnedData = apiResponse.data.data; const returnedData = apiResponse.data.data;
if (returnedData?.definition && returnedData?.baseUri) { if (
returnedData?.definition &&
returnedData?.format &&
returnedData?.baseUri
) {
resource.rawApplicationProfile = await parseRDFDefinition( resource.rawApplicationProfile = await parseRDFDefinition(
returnedData.definition, returnedData.definition,
"application/ld+json", returnedData.format,
returnedData.baseUri returnedData.baseUri
); );
resource.fullApplicationProfile = await resolveImports( resource.fullApplicationProfile = await resolveImports(
...@@ -192,10 +196,14 @@ export const useResourceStore = defineStore({ ...@@ -192,10 +196,14 @@ export const useResourceStore = defineStore({
"JsonLd" as RdfFormat "JsonLd" as RdfFormat
); );
const returnedData = apiResponse.data.data; const returnedData = apiResponse.data.data;
if (returnedData?.definition && returnedData?.baseUri) { if (
returnedData?.definition &&
returnedData?.format &&
returnedData?.baseUri
) {
let returnApplicationProfile = await parseRDFDefinition( let returnApplicationProfile = await parseRDFDefinition(
returnedData.definition, returnedData.definition,
"application/ld+json", returnedData.format,
returnedData.baseUri returnedData.baseUri
); );
if (doResolveImports) { if (doResolveImports) {
...@@ -409,18 +417,6 @@ export const useResourceStore = defineStore({ ...@@ -409,18 +417,6 @@ export const useResourceStore = defineStore({
} }
}, },
async deleteFile(resourceId: string, absoluteFilePath: string) {
const notificationStore = useNotificationStore();
try {
await BlobApi.blobDeleteFileWithParameter(resourceId, absoluteFilePath);
return true;
} catch (error) {
// Handle other Status Codes
notificationStore.postApiErrorNotification(error as AxiosError);
return false;
}
},
async getVocabularyInstances(className: string): Promise<BilingualLabels> { async getVocabularyInstances(className: string): Promise<BilingualLabels> {
const notificationStore = useNotificationStore(); const notificationStore = useNotificationStore();
try { try {
...@@ -431,7 +427,7 @@ export const useResourceStore = defineStore({ ...@@ -431,7 +427,7 @@ export const useResourceStore = defineStore({
undefined, undefined,
"en" as AcceptedLanguage.En, "en" as AcceptedLanguage.En,
pageNumber, pageNumber,
50 150
) )
); );
const deInstances = await wrapListRequest((pageNumber: number) => const deInstances = await wrapListRequest((pageNumber: number) =>
...@@ -440,7 +436,7 @@ export const useResourceStore = defineStore({ ...@@ -440,7 +436,7 @@ export const useResourceStore = defineStore({
undefined, undefined,
"de" as AcceptedLanguage.De, "de" as AcceptedLanguage.De,
pageNumber, pageNumber,
50 150
) )
); );
...@@ -448,13 +444,13 @@ export const useResourceStore = defineStore({ ...@@ -448,13 +444,13 @@ export const useResourceStore = defineStore({
de: deInstances.map((instance) => { de: deInstances.map((instance) => {
return { return {
name: instance.displayName, name: instance.displayName,
value: instance.graphUri, value: instance.instanceUri,
}; };
}), }),
en: enInstances.map((instance) => { en: enInstances.map((instance) => {
return { return {
name: instance.displayName, name: instance.displayName,
value: instance.graphUri, value: instance.instanceUri,
}; };
}), }),
}; };
...@@ -469,23 +465,206 @@ export const useResourceStore = defineStore({ ...@@ -469,23 +465,206 @@ export const useResourceStore = defineStore({
} }
}, },
async getFile( async createMetadataTree(
projectId: string,
resourceId: string,
metadataTreeForCreationDto: MetadataTreeForCreationDto
): Promise<boolean> {
const notificationStore = useNotificationStore();
try {
await TreeApi.createMetadataTree(
projectId,
resourceId,
metadataTreeForCreationDto
);
return true;
} catch (error) {
// Handle other Status Codes
notificationStore.postApiErrorNotification(error as AxiosError);
return false;
}
},
async updateMetadataTree(
projectId: string,
resourceId: string,
metadataTreeForUpdateDto: MetadataTreeForUpdateDto
): Promise<boolean> {
const notificationStore = useNotificationStore();
try {
await TreeApi.updateMetadataTree(
projectId,
resourceId,
metadataTreeForUpdateDto
);
return true;
} catch (error) {
// Handle other Status Codes
notificationStore.postApiErrorNotification(error as AxiosError);
return false;
}
},
/**
* Attempts to add a new metadata tree. If a conflict (409) error occurs, it tries to update the existing metadata tree.
*
* @param {string} projectId - The ID of the project.
* @param {string} resourceId - The ID of the resource.
* @param {MetadataTreeForCreationDto | MetadataTreeForUpdateDto} metadataTreeDto - The data transfer object containing metadata tree details. Can be for creation or update.
* @returns {Promise<boolean>} Returns true if the operation is successful, false otherwise.
* @throws {AxiosError} Throws an AxiosError if the API call fails.
*/
async addOrUpdateMetadataTree(
projectId: string,
resourceId: string,
metadataTreeDto: MetadataTreeForCreationDto | MetadataTreeForUpdateDto
): Promise<boolean> {
const notificationStore = useNotificationStore();
try {
// Attempt to create metadata tree
await TreeApi.createMetadataTree(
projectId,
resourceId,
metadataTreeDto
);
return true;
} catch (error: unknown) {
if (axios.isAxiosError(error)) {
if (error.response?.status === 409) {
// Metadata tree already exists, attempt to update
try {
await TreeApi.updateMetadataTree(
projectId,
resourceId,
metadataTreeDto // NOTE: Potential issue here, as the DTO might not be the same for creation and update if signature changes
);
return true;
} catch (updateError) {
// Handle other Status Codes
notificationStore.postApiErrorNotification(
updateError as AxiosError
);
return false;
}
}
}
}
// Handle notification externally
return false;
},
/**
* Attempts to update a new metadata tree. If a not found (404) error occurs, it tries to create a new metadata tree.
*
* @param {string} projectId - The ID of the project.
* @param {string} resourceId - The ID of the resource.
* @param {MetadataTreeForCreationDto | MetadataTreeForUpdateDto} metadataTreeDto - The data transfer object containing metadata tree details. Can be for creation or update.
* @returns {Promise<boolean>} Returns true if the operation is successful, false otherwise.
* @throws {AxiosError} Throws an AxiosError if the API call fails.
*/
async updateOrAddMetadataTree(
projectId: string,
resourceId: string,
metadataTreeDto: MetadataTreeForCreationDto | MetadataTreeForUpdateDto
): Promise<boolean> {
const notificationStore = useNotificationStore();
try {
// Attempt to update metadata tree
await TreeApi.updateMetadataTree(
projectId,
resourceId,
metadataTreeDto // NOTE: Potential issue here, as the DTO might not be the same for creation and update if signature changes
);
return true;
} catch (error: unknown) {
if (axios.isAxiosError(error)) {
if (error.response?.status === 404) {
// Metadata tree already exists, attempt to update
try {
await TreeApi.createMetadataTree(
projectId,
resourceId,
metadataTreeDto // NOTE: Potential issue here, as the DTO might not be the same for creation and update if signature changes
);
return true;
} catch (updateError) {
// Handle other Status Codes
notificationStore.postApiErrorNotification(
updateError as AxiosError
);
return false;
}
}
}
}
// Handle notification externally
return false;
},
async getMetadataTree(
projectId: string,
resourceId: string,
filePath: string,
format?: RdfFormat
): Promise<MetadataDto[] | null | undefined> {
const notificationStore = useNotificationStore();
try {
const response = await TreeApi.getMetadataTree(
projectId,
resourceId,
filePath,
format
);
return response.data.data;
} catch (error) {
// Handle other Status Codes
notificationStore.postApiErrorNotification(error as AxiosError);
return null;
}
},
async getFileTree(
projectId: string,
resourceId: string,
filePath: string
): Promise<FileDto[] | null | undefined> {
const notificationStore = useNotificationStore();
try {
const response = await TreeApi.getFileTree(
projectId,
resourceId,
filePath
);
return response.data.data;
} catch (error) {
// Handle other Status Codes
notificationStore.postApiErrorNotification(error as AxiosError);
return null;
}
},
async getBlob(
projectId: string,
resourceId: string, resourceId: string,
absoluteFilePath: string, absoluteFilePath: string,
asBlob = false asBlob = false
) { ) {
const notificationStore = useNotificationStore(); const notificationStore = useNotificationStore();
try { try {
const response = await BlobApi.blobGetFileWithParameter( const response = await BlobApi.getBlob(
projectId,
resourceId, resourceId,
absoluteFilePath, absoluteFilePath,
/* the following is an axios option */
asBlob asBlob
? { ? {
responseType: "blob", responseType: "blob",
} }
: undefined : undefined
); );
return response.data; return response;
} catch (error) { } catch (error) {
// Handle other Status Codes // Handle other Status Codes
notificationStore.postApiErrorNotification(error as AxiosError); notificationStore.postApiErrorNotification(error as AxiosError);
...@@ -493,37 +672,47 @@ export const useResourceStore = defineStore({ ...@@ -493,37 +672,47 @@ export const useResourceStore = defineStore({
} }
}, },
async getMetadata(resourceId: string, absoluteFilePath: string) { async createBlob(
projectId: string,
resourceId: string,
absoluteFilePath: string,
file: Blob,
options?: unknown
) {
const notificationStore = useNotificationStore(); const notificationStore = useNotificationStore();
try { try {
const response = await TreeApi.treeGetMetadataWithParameter( const response = await BlobApi.createBlob(
projectId,
resourceId, resourceId,
absoluteFilePath, absoluteFilePath,
"application/n-triples" file,
options
); );
return response.data; return response.status >= 200 && response.status < 300;
} catch (error) { } catch (error) {
// Handle other Status Codes // Handle other Status Codes
notificationStore.postApiErrorNotification(error as AxiosError); notificationStore.postApiErrorNotification(error as AxiosError);
return null; return false;
} }
}, },
async storeFile( async updateBlob(
projectId: string,
resourceId: string, resourceId: string,
absoluteFilePath: string, absoluteFilePath: string,
files: Blob[], file: Blob,
options?: unknown options?: unknown
) { ) {
const notificationStore = useNotificationStore(); const notificationStore = useNotificationStore();
try { try {
await BlobApi.blobUploadFileWithParameter( const response = await BlobApi.updateBlob(
projectId,
resourceId, resourceId,
absoluteFilePath, absoluteFilePath,
files, file,
options options
); );
return true; return response.status >= 200 && response.status < 300;
} catch (error) { } catch (error) {
// Handle other Status Codes // Handle other Status Codes
notificationStore.postApiErrorNotification(error as AxiosError); notificationStore.postApiErrorNotification(error as AxiosError);
...@@ -531,19 +720,14 @@ export const useResourceStore = defineStore({ ...@@ -531,19 +720,14 @@ export const useResourceStore = defineStore({
} }
}, },
async storeMetadata( async deleteBlob(
projectId: string,
resourceId: string, resourceId: string,
absoluteFilePath: string, absoluteFilePath: string
body: Dataset
) { ) {
const notificationStore = useNotificationStore(); const notificationStore = useNotificationStore();
try { try {
await TreeApi.treeStoreMetadataForFileWithParameter( await BlobApi.deleteBlob(projectId, resourceId, absoluteFilePath);
resourceId,
absoluteFilePath,
"text/turtle",
{ data: { metadata: await serializeRDFDefinition(body) } }
);
return true; return true;
} catch (error) { } catch (error) {
// Handle other Status Codes // Handle other Status Codes
......
...@@ -8,6 +8,7 @@ import type { ...@@ -8,6 +8,7 @@ import type {
GitlabProjectDto, GitlabProjectDto,
ResourceDto, ResourceDto,
ResourceTypeInformationDto, ResourceTypeInformationDto,
TreeDataType,
} from "@coscine/api-client/dist/types/Coscine.Api/api"; } from "@coscine/api-client/dist/types/Coscine.Api/api";
import type { Dataset } from "@rdfjs/types"; import type { Dataset } from "@rdfjs/types";
...@@ -185,3 +186,38 @@ export interface Label { ...@@ -185,3 +186,38 @@ export interface Label {
name?: string | null; name?: string | null;
value?: string | null; value?: string | null;
} }
export interface GeneralInformation {
id: string;
name: string;
path: string;
parentDirectory: string;
type: TreeDataType;
isFolder: boolean;
lastModified?: string | null;
readOnly?: boolean;
createdAt?: string;
metadata: Dataset | null;
}
export interface FileInformation extends GeneralInformation {
type: TreeDataType.Leaf;
isFolder: false;
version: string;
size: number;
dataUrl?: string;
info?: File;
uploading?: boolean;
requesting?: boolean;
}
export interface FolderInformation extends GeneralInformation {
type: TreeDataType.Tree;
isFolder: true;
}
export interface ReadOnlyFolderInformation extends FolderInformation {
readOnly: true;
}
export type FolderContent = FileInformation | FolderInformation;
import type { Dataset } from "@rdfjs/types";
export interface GeneralInformation {
id: string;
path: string;
absolutePath: string;
name: string;
isFolder: boolean;
lastModified?: string;
readOnly?: boolean;
created?: string;
metadata: Dataset;
}
export interface FileInformation extends GeneralInformation {
isFolder: false;
version: string;
size: number;
dataUrl?: string;
info?: File;
uploading?: boolean;
requesting?: boolean;
}
export interface FolderInformation extends GeneralInformation {
isFolder: true;
}
export interface ReadOnlyFolderInformation extends FolderInformation {
readOnly: true;
}
export type FolderContent = FileInformation | FolderInformation;
import useResourceStore from "../store"; import useResourceStore from "../store";
import type { FolderContent } from "./EntryDefinition";
import type { Dataset } from "@rdfjs/types"; import type { Dataset } from "@rdfjs/types";
import factory from "rdf-ext"; import factory from "rdf-ext";
import { parseRDFDefinition } from "./linkedData"; import { parseRDFDefinition } from "./linkedData";
import type { Metadata } from "../types"; import type { FolderContent } from "../types";
import type { ResourceDto } from "@coscine/api-client/dist/types/Coscine.Api"; import type {
ProjectDto,
ResourceDto,
} from "@coscine/api-client/dist/types/Coscine.Api";
export default { export default {
filterMetadataStorage( /**
metadataStorage: Metadata[], * Loads metadata for a specified file. If no metadata is found, it returns an empty dataset.
filterPath: string *
): Metadata[] { * @param {FolderContent} fileInfo - Information about the file for which metadata is to be loaded.
return metadataStorage.filter((x) => { * @param {ProjectDto | null} project - Parent project of the file.
if (Object.keys(x).length > 0) { * @param {ResourceDto | null} resource - Parent resource of the file.
const graphName = Object.keys(x)[0]; * @returns {Promise<Dataset>} A promise that resolves to the metadata for the file or an empty dataset if none is found.
const pathQueryString = "path="; */
if (graphName.indexOf(pathQueryString) !== -1) { async loadMetadataForFile(
let path = graphName.substring(
graphName.indexOf(pathQueryString) + pathQueryString.length
);
if (path.indexOf("&") !== -1) {
path = path.substring(0, path.indexOf("&"));
}
if (
decodeURIComponent(path) === filterPath ||
decodeURIComponent(path) === "/" + filterPath
) {
return true;
}
}
const typeQueryString = "/@type=metadata";
if (graphName.indexOf(typeQueryString) !== -1) {
const urlEnd = graphName.substring(
0,
graphName.indexOf(typeQueryString)
);
if (
urlEnd.endsWith(filterPath) ||
decodeURIComponent(urlEnd).endsWith(filterPath)
) {
return true;
}
}
}
return false;
});
},
async loadMetadata(
fileInfo: FolderContent, fileInfo: FolderContent,
project: ProjectDto | null,
resource: ResourceDto | null resource: ResourceDto | null
): Promise<Dataset> { ): Promise<Dataset | null> {
if ( // Return an empty dataset if no file info is available
fileInfo !== undefined && if (!fileInfo?.path || !project?.id || !resource?.id) {
fileInfo.absolutePath !== undefined && return factory.dataset() as unknown as Dataset;
resource?.id
) {
if (fileInfo.metadata) {
if (fileInfo.metadata.size > 0) {
return fileInfo.metadata;
} }
// Return existing metadata if available
if (fileInfo.metadata && fileInfo.metadata.size > 0) {
return fileInfo.metadata;
} }
const response = await useResourceStore().getMetadata( // Fetch metadata from the API
const metadataTree = await useResourceStore().getMetadataTree(
project.id,
resource.id, resource.id,
fileInfo.absolutePath fileInfo.path
); );
const metadataStorage = response.data.metadataStorage; // If the metadata exists and has a definition, parse it
if (metadataTree?.[0]?.definition && metadataTree[0].format) {
if (metadataStorage.length === 0) { return await parseRDFDefinition(
return factory.dataset() as unknown as Dataset; metadataTree[0].definition,
} metadataTree[0].format
const resultArray = this.filterMetadataStorage(
metadataStorage,
fileInfo.absolutePath
); );
if (resultArray.length === 0) {
return factory.dataset() as unknown as Dataset;
} }
const result = resultArray[0]; // Return an empty dataset if no metadata was found
const objectKeys = Object.keys(result);
if (
response.data.metadataStorage !== undefined &&
objectKeys.length === 1
) {
const entry = result[objectKeys[0]];
if (typeof entry === "string") {
return await parseRDFDefinition(entry, "application/n-triples");
}
} else if (
response.data.metadataStorage !== undefined &&
objectKeys.length > 1
) {
const entry = result;
if (typeof entry === "string") {
return await parseRDFDefinition(entry, "application/n-triples");
}
}
}
return factory.dataset() as unknown as Dataset; return factory.dataset() as unknown as Dataset;
}, },
copyMetadata(source: Dataset | undefined, target: FolderContent | undefined) {
/**
* Copies metadata from the source to the target.
*
* @param {Dataset | null | undefined} source - The source dataset from which to copy metadata.
* @param {FolderContent | undefined} target - The target where the metadata should be copied to.
*/
copyMetadata(
source: Dataset | null | undefined,
target: FolderContent | undefined
) {
if (source && target) { if (source && target) {
target.metadata = factory.dataset( target.metadata = factory.dataset(
Array.from(source) Array.from(source)
......
...@@ -30,18 +30,25 @@ export async function parseRDFDefinition( ...@@ -30,18 +30,25 @@ export async function parseRDFDefinition(
return dataset as unknown as Dataset; return dataset as unknown as Dataset;
} }
/**
* Serializes an RDF dataset into a specified format.
*
* @param {Dataset | null} dataset - The RDF dataset to be serialized.
* @param {string} [contentType="text/turtle"] - The MIME type for the serialization format.
* @returns {Promise<string>} A promise that resolves with the serialized dataset as a string.
*
* @example
* const turtleStr = await serializeRDFDefinition(myDataset, "text/turtle");
*/
export async function serializeRDFDefinition( export async function serializeRDFDefinition(
dataset: Dataset, dataset: Dataset | null,
contentType = "text/turtle" contentType: string = "text/turtle"
): Promise<string> { ): Promise<string> {
const canonical = dataset.toCanonical(); if (!dataset || !dataset.toCanonical()) return "";
if (!canonical) {
return "";
}
const output = formats.serializers.import(contentType, dataset.toStream()); const output = formats.serializers.import(contentType, dataset.toStream());
if (output === null) { if (!output) return "";
return "";
}
return await stringifyStream(output as NodeJS.ReadableStream); return await stringifyStream(output as NodeJS.ReadableStream);
} }
...@@ -80,10 +87,10 @@ export async function resolveImports( ...@@ -80,10 +87,10 @@ export async function resolveImports(
"JsonLd" as RdfFormat "JsonLd" as RdfFormat
); );
const apResponse = importedApiResponse.data.data; const apResponse = importedApiResponse.data.data;
if (apResponse?.definition) { if (apResponse?.definition && apResponse?.format) {
const importedApplicationProfile = await parseRDFDefinition( const importedApplicationProfile = await parseRDFDefinition(
apResponse.definition, apResponse.definition,
"application/ld+json", apResponse.format,
importedAP.value importedAP.value
); );
fullApplicationProfile = ( fullApplicationProfile = (
......
...@@ -73,8 +73,8 @@ ...@@ -73,8 +73,8 @@
v-html=" v-html="
$options.filters $options.filters
? $options.filters.highlight( ? $options.filters.highlight(
resourceType.displayName resourceType.specificType
? resourceTypeName(resourceType.displayName) ? resourceTypeName(resourceType.specificType)
: null, : null,
query query
) )
...@@ -113,8 +113,10 @@ import { defineComponent, type PropType } from "vue"; ...@@ -113,8 +113,10 @@ import { defineComponent, type PropType } from "vue";
import useResourceStore from "@/modules/resource/store"; import useResourceStore from "@/modules/resource/store";
import type { ItemSearchResult } from "@coscine/api-client/dist/types/Coscine.Api.Search"; import type { ItemSearchResult } from "@coscine/api-client/dist/types/Coscine.Api.Search";
import type { ResourceTypeObject } from "@coscine/api-client/dist/types/Coscine.Api.Resources"; import type {
import type { ProjectDto } from "@coscine/api-client/dist/types/Coscine.Api/api"; ProjectDto,
ResourceTypeInformationDto,
} from "@coscine/api-client/dist/types/Coscine.Api/api";
export default defineComponent({ export default defineComponent({
name: "MetadataResult", name: "MetadataResult",
...@@ -208,7 +210,7 @@ export default defineComponent({ ...@@ -208,7 +210,7 @@ export default defineComponent({
} }
return null; return null;
}, },
resourceType(): ResourceTypeObject | undefined | null { resourceType(): ResourceTypeInformationDto | undefined | null {
if ( if (
this.resourceTypes && this.resourceTypes &&
this.result.type === "Resource" && this.result.type === "Resource" &&
...@@ -223,7 +225,7 @@ export default defineComponent({ ...@@ -223,7 +225,7 @@ export default defineComponent({
} }
return null; return null;
}, },
resourceTypes(): ResourceTypeObject[] | null { resourceTypes(): ResourceTypeInformationDto[] | undefined | null {
return this.resourceStore.enabledResourceTypes; return this.resourceStore.enabledResourceTypes;
}, },
fields(): Record<string, string> { fields(): Record<string, string> {
...@@ -285,6 +287,7 @@ export default defineComponent({ ...@@ -285,6 +287,7 @@ export default defineComponent({
params: { params: {
guid: this.resourceId, guid: this.resourceId,
slug: this.project.slug, slug: this.project.slug,
dirTrail: "",
}, },
}).href; }).href;
} else if ( } else if (
...@@ -298,6 +301,7 @@ export default defineComponent({ ...@@ -298,6 +301,7 @@ export default defineComponent({
params: { params: {
guid: this.resourceId, guid: this.resourceId,
slug: this.project.slug, slug: this.project.slug,
dirTrail: "",
}, },
}).href; }).href;
} else { } else {
......
...@@ -42,7 +42,12 @@ VueRouter.prototype.push = async function (location: RawLocation) { ...@@ -42,7 +42,12 @@ VueRouter.prototype.push = async function (location: RawLocation) {
); );
} catch (err) { } catch (err) {
if (err instanceof Error) { if (err instanceof Error) {
if (err.name !== "NavigationDuplicated") { if (
// Ignore errors from navigating to the same route
err.name !== "NavigationDuplicated" &&
// Ignore errors from redirecting enforced by a navigation guard
!err.message.includes("via a navigation guard")
) {
throw err; throw err;
} }
} }
...@@ -115,6 +120,11 @@ router.beforeEach((to, _, next) => { ...@@ -115,6 +120,11 @@ router.beforeEach((to, _, next) => {
else if (to.name === "login" && loginStore.isLoggedIn) { else if (to.name === "login" && loginStore.isLoggedIn) {
next({ name: "home" }); next({ name: "home" });
} }
// Navigation Guard - Set directory trail for resource page when no dirTrail is provided
else if (to.name === "resource-page" && to.params.dirTrail === undefined) {
to.params.dirTrail = "";
next({ name: "resource-page", params: { ...to.params } });
}
// Continue navigation // Continue navigation
else { else {
next(); next();
......
import path from "node:path"; import path from "node:path";
import { defineConfig } from "vite"; import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue2"; import vue from "@vitejs/plugin-vue2";
import pluginRewriteAll from 'vite-plugin-rewrite-all';
import { nodePolyfills } from 'vite-plugin-node-polyfills'; import { nodePolyfills } from 'vite-plugin-node-polyfills';
...@@ -39,6 +40,9 @@ const config = defineConfig({ ...@@ -39,6 +40,9 @@ const config = defineConfig({
plugins: [ plugins: [
nodePolyfills(), nodePolyfills(),
vue(), vue(),
// This plugin fixes a vite bug, that breaks SPA-fallback with a dot (.) in the URL (e.g. /file_0.txt).
// TODO: Check if the issue is resolved after upgrading to vite 5.0.0
pluginRewriteAll(),
WindiCSS(), WindiCSS(),
Components({ Components({
dts: 'src/components.d.ts', dts: 'src/components.d.ts',
...@@ -47,6 +51,8 @@ const config = defineConfig({ ...@@ -47,6 +51,8 @@ const config = defineConfig({
}), }),
], ],
appType: "spa",
server: { server: {
host: true, host: true,
port: 9234, port: 9234,
......
...@@ -428,12 +428,12 @@ __metadata: ...@@ -428,12 +428,12 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@coscine/api-client@npm:^3.0.0": "@coscine/api-client@npm:^3.1.0":
version: 3.0.0 version: 3.1.0
resolution: "@coscine/api-client@npm:3.0.0" resolution: "@coscine/api-client@npm:3.1.0"
dependencies: dependencies:
axios: ^0.21.1 axios: ^0.21.1
checksum: b4722c0428ca3851f47f21bd606617d8386efc1c547ab68d55cbd8038496182684465b2fa6f27e37e9bc780160206eb3d5a9ecc26790bc4bf9df2d8c1e5304e6 checksum: 71892411a48930421486b299915a0bd7b3b3e6209953f2304cc22ab2cc3705b56d4c702b8ea4d6e7dd438cd67fa348bb86219df5e7b128ab0ae4e356501e6e82
languageName: node languageName: node
linkType: hard linkType: hard
...@@ -4243,6 +4243,13 @@ __metadata: ...@@ -4243,6 +4243,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"connect-history-api-fallback@npm:^1.6.0":
version: 1.6.0
resolution: "connect-history-api-fallback@npm:1.6.0"
checksum: 804ca2be28c999032ecd37a9f71405e5d7b7a4b3defcebbe41077bb8c5a0a150d7b59f51dcc33b2de30bc7e217a31d10f8cfad27e8e74c2fc7655eeba82d6e7e
languageName: node
linkType: hard
"consola@npm:^2.15.0": "consola@npm:^2.15.0":
version: 2.15.3 version: 2.15.3
resolution: "consola@npm:2.15.3" resolution: "consola@npm:2.15.3"
...@@ -11895,7 +11902,7 @@ __metadata: ...@@ -11895,7 +11902,7 @@ __metadata:
version: 0.0.0-use.local version: 0.0.0-use.local
resolution: "ui@workspace:." resolution: "ui@workspace:."
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
...@@ -11965,6 +11972,7 @@ __metadata: ...@@ -11965,6 +11972,7 @@ __metadata:
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: ^2.7.14 vue: ^2.7.14
...@@ -12263,6 +12271,17 @@ __metadata: ...@@ -12263,6 +12271,17 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"vite-plugin-rewrite-all@npm:^1.0.1":
version: 1.0.1
resolution: "vite-plugin-rewrite-all@npm:1.0.1"
dependencies:
connect-history-api-fallback: ^1.6.0
peerDependencies:
vite: ^2.0.0 || ^3.0.0 || ^4.0.0
checksum: 9a22e51e80fc14d58d32556208c26740baf3f02eae2d0a5f356ddc1ea478bd2f56803cc17e77261ffad33d27301017ba03736143446685c03bb0f076eef2fdd0
languageName: node
linkType: hard
"vite-plugin-windicss@npm:^1.9.0": "vite-plugin-windicss@npm:^1.9.0":
version: 1.9.0 version: 1.9.0
resolution: "vite-plugin-windicss@npm:1.9.0" resolution: "vite-plugin-windicss@npm:1.9.0"
......