Skip to content
Snippets Groups Projects
Select Git revision
  • ede2ac032d4d1038c97a20a8276edc50127d62cb
  • main default protected
  • Issue/3242-UserDeletionOnProfile
  • dev protected
  • Issue/3187-VersionStorage
  • Issue/3179-sortDataPublicationServiceList
  • Issue/3193-processingOfPersonalDataConsent
  • Issue/2450-AdminPage
  • Issue/3215-lifecycle
  • Issue/3133-subProjectsChanges
  • Issue/2489-addNotificationManagement
  • Issue/3085-useNewApiClient
  • Issue/3043-DataStorageNrwResource
  • Issue/3011-maintenanceMode
  • Issue/2446-addingResponsibleOrganization
  • Issue/2900-removeInsituteField
  • Issue/2981-dataPubInDb
  • Issue/2881-messageController
  • Issue/2921-changesToDataPublicationFeature
  • Issue/2926-regAppLogin
  • Issue/2672-fixSfbPidPointing
  • v3.21.0
  • v3.20.0
  • v3.19.0
  • v3.18.0
  • v3.17.0
  • v3.16.0
  • v3.15.0
  • v3.14.0
  • v3.13.0
  • v3.12.0
  • v3.11.0
  • v3.10.0
  • v3.9.0
  • v3.8.0
  • v3.7.0
  • v3.6.0
  • v3.5.0
  • v3.4.3
  • v3.4.2
  • v3.4.1
41 results

configuration.ts

Blame
  • Code owners
    Assign users and groups as approvers for specific file changes. Learn more.
    FilesView.vue 36.61 KiB
    <template>
      <div class="DataSource">
        <!-- Files View Header -->
        <FilesViewHeader
          v-model="searchTerm"
          :is-uploading="isUploading"
          @clickFileSelect="clickFileSelect"
        />
    
        <b-row>
          <!-- Files View Table -->
          <b-table
            id="resourceViewTable"
            ref="adaptTable"
            v-model:sort-by="sortBy"
            v-model:selected-items="selectedItems"
            :fields="headers"
            :items="folderContents"
            :busy="isBusy"
            :locale="$i18n.locale"
            :filter="searchTerm"
            :per-page="perPage"
            :current-page="currentPage"
            :filter-included-fields="filterFields"
            select-mode="multi"
            selectable
            striped
            bordered
            outlined
            hover
            small
            responsive
            head-variant="dark"
            class="adaptTable"
            show-empty
            sort-icon-right
            :empty-text="$t('page.resource.emptyTableText')"
            :empty-filtered-text="$t('page.resource.emptyFilterText')"
            @filtered="filtered"
          >
            <!-- Loading Spinner for Busy State -->
            <template #table-busy>
              <div class="text-center text-danger my-2">
                <b-spinner class="align-middle" />
                <strong style="margin-left: 1%">
                  {{ $t("page.resource.loading") }}
                </strong>
              </div>
            </template>
    
            <!-- Head "Icon" -->
            <template #head(icon)>
              <b-form-checkbox
                v-model="selectAll"
                class="align-top"
                @update:modelValue="allSelect"
              />
            </template>
    
            <!-- Column "Add Column"/Filter -->
            <template #head(addColumn)>
              <b-dropdown
                id="addColumnDropDown"
                menu-class="px-1"
                size="sm"
                right
                :no-caret="true"
              >
                <!-- Button Template + Icon -->
                <template #button-content>
                  <i-bi-funnel />
                </template>
    
                <!-- Checkbox Options -->
                <b-form-checkbox
                  v-for="column in columns"
                  :key="column.key"
                  v-model="column.active"
                  stacked
                >
                  {{ column.label }}
                </b-form-checkbox>
              </b-dropdown>
            </template>
    
            <!-- Row "Icon" -->
            <template #cell(icon)="row">
              <span class="checkFile">
                <!-- Show a checkbox for elements that are not read-only -->
                <b-form-checkbox
                  v-if="!row.item.readOnly"
                  :model-value="selectedItems.includes(row.item)"
                  class="tableCheck"
                  @update:modelValue="select(row.item)"
                />
    
                <span class="fileIconEntry">
                  <!-- Icon -->
                  <i-bi-file-earmark v-if="row.item.type === 'Leaf'" class="ms-1" />
                  <i-bi-folder v-else class="ms-1" />
                </span>
              </span>
            </template>
    
            <!-- Row "Name" -->
            <template #cell(name)="row">
              <span class="d-flex justify-content-between fileViewEntryWrapper">
                <span class="d-inline-flex gap-2">
                  <a
                    v-if="!editableDataUrl || row.item.type === 'Tree'"
                    :id="'fileViewEntry' + row.index"
                    class="fileViewEntry"
                    @click="triggerNavigation(row.item)"
                  >
                    {{ row.item.name }}
                  </a>
                  <span v-else class="fileViewEntry"> {{ row.item.name }} </span>
                </span>
                <!-- Row File "..." Menu -->
                <b-dropdown
                  class="dotMenu"
                  left
                  variant="link"
                  toggle-class="text-decoration-none"
                  size="sm"
                  :no-caret="true"
                  :disabled="
                    (editableDataUrl && resource && resource.archived) ?? true
                  "
                >
                  <!-- Row File "..." Button Template + Icon -->
                  <template #button-content>
                    <i-bi-three-dots-vertical />
                  </template>
                  <!-- Option "Sharing" -->
                  <b-dropdown-item
                    v-if="
                      !editableDataUrl &&
                      row.item.type === 'Leaf' &&
                      !isGuest &&
                      !isGitLab
                    "
                    :id="'dropDownItemShare'"
                    @click="copyUrl(row.item)"
                  >
                    {{ $t("page.resource.metadataManagerBtnCopyLink") }}
                  </b-dropdown-item>
                  <!-- Option "Download" -->
                  <b-dropdown-item
                    v-if="!editableDataUrl && row.item.type === 'Leaf'"
                    :id="'dropDownItemDownload'"
                    @click="openFile(row.item)"
                  >
                    {{ $t("buttons.download") }}
                  </b-dropdown-item>
                  <b-dropdown-divider
                    v-if="!editableDataUrl && row.item.type === 'Leaf' && !isGuest"
                  ></b-dropdown-divider>
                  <!-- Option "Delete" -->
                  <b-dropdown-item
                    v-if="!isGuest"
                    :id="'dropDownItemDelete'"
                    :disabled="
                      (resource && resource.archived) ||
                      (resourceTypeInformation &&
                        !resourceTypeInformation.canDelete)
                    "
                    @click="showModalDeleteFile(row.item)"
                  >
                    {{ $t("buttons.delete") }}
                  </b-dropdown-item>
                </b-dropdown>
              </span>
            </template>
            <!-- Pagination -->
            <template #table-caption>
              <b-row class="text-end" align-v="center" no-gutters>
                <b-col align-self="center" class="p-0" />
                <b-col
                  align-self="center"
                  class="p-0 d-flex justify-content-center"
                >
                  <b-pagination
                    id="pagination"
                    v-model="currentPage"
                    :total-rows="rows"
                    :per-page="perPage"
                    aria-controls="resourceViewTable"
                  />
                </b-col>
                <b-col align-self="center" class="p-0 d-flex justify-content-end">
                  <b-form-select
                    v-model="perPage"
                    :options="paginationPerPageOptions"
                    style="max-width: 5rem"
                  />
                </b-col>
              </b-row>
            </template>
          </b-table>
        </b-row>
      </div>
    </template>
    
    <script lang="ts">
    // import the store for current module
    import useResourceStore from "../../store";
    import useProjectStore from "@/modules/project/store";
    import useNotificationStore from "@/store/notification";
    import FilesViewHeader from "./FilesViewHeader.vue";
    import MetadataManagerUtil from "../../utils/MetadataManagerUtil";
    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 { BTableSortBy } from "bootstrap-vue-next";
    import type { Dataset, Literal, Quad } from "@rdfjs/types";
    import type {
      ProjectDto,
      ResourceTypeInformationDto,
      FileTreeDto,
      MetadataTreeDto,
    } from "@coscine/api-client/dist/types/Coscine.Api";
    import {
      CoscineResourceTypes,
      type AdaptTable,
      type BilingualLabels,
      type CustomTableField,
      type FileInformation,
      type FolderContent,
      type FolderInformation,
      type ReadOnlyFolderInformation,
      type VisitedResourceObject,
    } from "../../types";
    
    type RowHead = { column: string; label: string };
    
    export default defineComponent({
      components: {
        FilesViewHeader,
      },
    
      props: {
        folderContents: {
          default: () => {
            return [];
          },
          type: Array as PropType<FolderContent[]>,
        },
        fileListEdit: {
          default: () => {
            return [];
          },
          type: Array as PropType<FolderContent[]>,
        },
        isUploading: {
          default: false,
          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() {
        const resourceStore = useResourceStore();
        const projectStore = useProjectStore();
        const notificationStore = useNotificationStore();
    
        return { resourceStore, projectStore, notificationStore };
      },
    
      data() {
        return {
          currentPage: 1,
          coscineResourceTypes: CoscineResourceTypes,
          filteredRows: -1,
          isBusy: true,
          isLoadingSearchResults: false,
          perPage: 10,
          selectAll: false,
          searchTerm: "",
          selectedItems: [] as FolderContent[],
          sortBy: [{ key: "name", order: "desc" }] as BTableSortBy[] | undefined,
        };
      },
      computed: {
        isGuest(): boolean | undefined {
          return this.projectStore.currentUserRoleIsGuest;
        },
        isGitLab(): boolean {
          return (
            this.resource?.type?.generalType ===
            this.coscineResourceTypes.Gitlab.General
          );
        },
        applicationProfile(): Dataset | null {
          return this.resourceStore.currentFullApplicationProfile;
        },
        project(): null | ProjectDto {
          return this.projectStore.currentProject;
        },
        resource(): null | VisitedResourceObject {
          return this.resourceStore.currentResource;
        },
        classes(): { [className: string]: BilingualLabels } {
          return this.resourceStore.classes;
        },
        resourceTypeInformation(): ResourceTypeInformationDto | undefined {
          return this.resourceStore.enabledResourceTypes?.find(
            (resourceType) => resourceType.id === this.resource?.type?.id,
          );
        },
        editableDataUrl(): boolean {
          return (
            this.resourceTypeInformation?.resourceContent?.metadataView
              ?.editableDataUrl === true
          );
        },
        locale(): string {
          // Define as computed property to translate RCV Table
          return this.$i18n.locale;
        },
        columns(): CustomTableField[] {
          const columns: CustomTableField[] = [];
          const oldColumns = this.loadFromLocalStorage();
    
          if (!this.applicationProfile) return oldColumns;
    
          // keys a, b, name, active
          const shaclPath = factory.namedNode("http://www.w3.org/ns/shacl#path");
          const shaclName = factory.namedNode("http://www.w3.org/ns/shacl#name");
    
          const paths = this.applicationProfile.match(undefined, shaclPath);
          for (const path of paths) {
            // Read the active value from the localStorage
            const identifier = path.object.value;
    
            // Find the name of the column in the preferred language or a default if not available.
            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) {
              columns.push(this.createColumn(name, identifier, path, activeColumn));
            }
          }
    
          return columns;
        },
        defaultHeaders(): Array<CustomTableField> {
          // Define as computed property to have table
          // header text react on language changes.
          return [
            {
              key: "icon",
              sortable: false,
              active: true,
            },
            {
              label: this.$t("page.resource.fileName").toString(),
              key: "name",
              sortable: true,
              active: true,
            },
            {
              label: this.$t("page.resource.lastModified").toString(),
              key: "lastModified",
              sortable: true,
              active: true,
              formatter: (value, key, item: FolderContent) => {
                return this.renderDate(item);
              },
              filterByFormatted: true,
            },
            {
              label: this.$t("page.resource.size").toString(),
              key: "size",
              sortable: true,
              active: true,
              formatter: (value, key, item: FolderContent) => {
                return this.renderSize(item);
              },
              filterByFormatted: true,
            },
          ];
        },
        filterFields(): string[] {
          const filterFields = this.defaultHeaders.map((header) => header.key);
          filterFields.push(
            ...this.visibleColumns.filter(
              (column) => !filterFields.includes(column),
            ),
          );
          filterFields.push(
            ...this.columns
              .filter((column) => column.active)
              .map((column) => column.key),
          );
          return filterFields;
        },
        headers(): Array<CustomTableField> {
          const headers: CustomTableField[] = [...this.defaultHeaders];
          for (const column of this.visibleColumns) {
            if (!this.filterFields.includes(column)) {
              headers.push({
                label: this.$t(column).toString(),
                key: column,
                sortable: true,
                active: true,
              });
            }
          }
          headers.push(...this.columns.filter((column) => column.active));
          headers.push({
            label: "add",
            key: "addColumn",
            sortable: false,
            active: true,
          });
          return headers;
        },
        paginationPerPageOptions(): { value: number; text: string }[] {
          return [
            { value: 10, text: "10" },
            { value: 50, text: "50" },
            { value: 100, text: "100" },
            { value: this.rows, text: this.$t("page.resource.all").toString() },
          ];
        },
        rows() {
          return this.filteredRows >= 0
            ? this.filteredRows
            : this.folderContents.length;
        },
        visibleColumns(): Array<string> {
          let visibleColumns: Array<string> = [];
          const collectedColumns =
            this.resourceTypeInformation?.resourceContent?.entriesView?.columns
              ?.always;
          if (collectedColumns) {
            visibleColumns = Array.from(collectedColumns);
          }
          return visibleColumns;
        },
      },
    
      watch: {
        searchTerm() {
          this.saveInLocalStorage();
        },
        selectedItems() {
          this.onSelection(this.selectedItems);
        },
        folderContents() {
          this.filteredRows = this.folderContents.length;
        },
        headers() {
          this.saveInLocalStorage();
        },
        async resource() {
          await this.navigateTree();
        },
        sortBy() {
          this.saveInLocalStorage();
        },
        async dirTrail() {
          // Reacts to route changes from 'triggerNavigation()'
          await this.navigateTree();
        },
      },
    
      async created() {
        this.isBusy = true;
        if (this.resource) {
          await this.navigateTree();
        }
        this.isBusy = false;
      },
    
      methods: {
        /**
         * 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;
          for (const nameEntry of names) {
            name = nameEntry.object.value;
            if ((nameEntry.object as Literal).language === this.$i18n.locale) {
              break;
            }
          }
          return name;
        },
    
        /**
         * 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) {
            if (oldColumn.key === identifier) {
              return oldColumn.active;
            }
          }
          return false;
        },
    
        /**
         * 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,
            key: identifier,
            sortable: true,
            active: activeColumn,
            formatter: (value, key, item: FolderContent) => {
              return this.formatColumnValue(
                item.metadata[item.latestVersion],
                path,
              );
            },
            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 comma (", ").
         */
        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[] = [];
          for (const entry of entries) {
            const entryObject = entry.object;
            let entryValue = entryObject.value;
    
            // If the entry object is a literal, further formatting might be needed.
            if (entryObject.termType === "Literal") {
              // If the datatype of the literal indicates a date, format it accordingly.
              if (entryObject.datatype.value.endsWith("date")) {
                const date = entryObject.value;
                const dateObject = new Date(date);
                entryValue = dateObject.toLocaleDateString(this.$i18n.locale);
              }
            }
            // 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)) {
                // Depending on the user's locale, choose the appropriate language list.
                const langClassList =
                  this.$i18n.locale === "de" ? classValue.de : classValue.en;
    
                // If a language-specific list is available, search for the named node's value in it.
                if (langClassList) {
                  const foundEntry = langClassList.find(
                    (langClassEntry) => langClassEntry.value === entryValue,
                  );
    
                  // If a match is found, replace the entryValue with the more readable name.
                  if (foundEntry && foundEntry.name) {
                    entryValue = foundEntry.name;
                    break; // Exit the loop once a match is found.
                  }
                }
              }
            }
    
            // Add the processed entry value to the list of results.
            returnList.push(entryValue);
          }
    
          // Join the list of results with line breaks and return the final formatted value.
          return returnList.join(", ");
        },
        /**
         * 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(keepEditedFiles: boolean = false) {
          // Start with the current folder as the reference
          let currentFolder = this.dirTrail;
    
          // Check if the current folder might actually be a file and handle it
          if (this.isProbablyFile(currentFolder)) {
            // TODO: Change to open modal
            await this.openFile({
              name: currentFolder.substring(currentFolder.lastIndexOf("/") + 1),
              path: currentFolder,
            } as FileInformation);
            // Remove the file name from the path to get the folder path, then navigate to it
            currentFolder = this.dirCrumbs.join("");
            this.triggerNavigation({ path: currentFolder } as FolderInformation);
          }
    
          // Finally, open the current folder to display its contents
          await this.openFolder(
            { path: currentFolder } as FolderInformation,
            keepEditedFiles,
          );
        },
    
        /**
         * Navigates to the specified folder or resource based on the given `entry`.
         * If the provided `entry.path` is different from the current `dirTrail`,
         * a new route will be pushed to the Vue router.
         *
         * File triggers download, folder triggers navigation
         *
         * @param {FolderContent} entry - The folder or file information object to navigate to.
         */
        triggerNavigation(entry: FolderContent) {
          if (entry.type === "Leaf") {
            this.openFile(entry);
          } else {
            this.$router.push({
              name: "resource-page",
              params: {
                dirTrail: entry.path,
              },
            });
          }
        },
    
        /**
         * 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() {
          await this.resourceStore.setStoredColumns({
            columns: this.columns,
            filter: this.searchTerm,
            sortBy: this.sortBy,
          });
        },
    
        /**
         * Loads table preferences from local storage.
         * @returns {Array<CustomTableField>} - Array of table fields.
         */
        loadFromLocalStorage(): Array<CustomTableField> {
          let storedColumns: Array<CustomTableField> = [];
          const currentStoredColumns = this.resourceStore.currentStoredColumns;
    
          if (currentStoredColumns) {
            // If the stored columns are just an array (legacy format)
            if (Array.isArray(currentStoredColumns)) {
              return currentStoredColumns;
            }
    
            // Retrieve and set stored preferences
            storedColumns = currentStoredColumns.columns;
            this.searchTerm = currentStoredColumns.filter;
            this.sortBy = currentStoredColumns.sortBy;
          }
          return storedColumns;
        },
    
        /**
         * Deactivates a column based on the provided row.
         * @param {RowHead} row - Row which represents a column.
         */
        removeColumn(row: RowHead) {
          for (const column of this.columns) {
            if (column.key === row.column) {
              column.active = false;
            }
          }
        },
    
        /**
         * Emits an event to show the delete modal for a specific file.
         * @param {FolderContent} file - File to delete.
         */
        showModalDeleteFile(file: FolderContent) {
          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)
            await this.resourceStore.download(
              this.project.id,
              this.resource.id,
              file.path,
              file.name,
              file.actions,
            );
          }
        },
        /**
         * Asynchronously creates the download url for sharing.
         * @async
         * @param {FileInformation} file - Information about the file to be shared.
         */
        async copyUrl(file: FileInformation) {
          if (this.project?.id && this.resource?.id) {
            // Create the sharing link
            const downloadUrl = await this.resourceStore.getDownloadUrl(
              this.project.id,
              this.resource.id,
              file.path,
              file.name,
              file.actions,
              this.editableDataUrl,
            );
            if (downloadUrl) {
              navigator.clipboard.writeText(downloadUrl);
              this.notificationStore.postNotification({
                title: this.$t("page.resource.toClipboard").toString(),
                body: this.$t("page.resource.expirationMessage").toString(),
              });
            }
          }
        },
    
        /**
         * Asynchronously opens the given folder, fetching its file tree and metadata tree.
         * @async
         * @param {FolderInformation} folder - Information about the folder to be opened.
         * @param {boolean} keepEditedFiles - Keep the edited files.
         */
        async openFolder(
          folder: FolderInformation,
          keepEditedFiles: boolean = false,
        ) {
          if (!this.project?.id || !this.resource?.id) return;
    
          this.isBusy = true;
    
          // Empty file list, since the context changes
          // (Other workaround would be to keep the editing files open)
          if (!keepEditedFiles) {
            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", []);
        },
    
        /**
         * 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 : "/";
        },
    
        /**
         * Constructs the folder content based on the provided file and metadata trees.
         * @async
         * @param {FileTreeDto[] | null | undefined} fileTree - File tree of the folder.
         * @param {MetadataTreeDto[] | null | undefined} metadataTree - Metadata tree of the folder.
         * @returns {FolderContent[]} An array containing folder content.
         */
        async constructFolderContents(
          fileTree: FileTreeDto[] | null | undefined,
          metadataTree: MetadataTreeDto[] | null | undefined,
        ): Promise<FolderContent[]> {
          const folderContents = [];
    
          if (this.dirCrumbs.length > 0) {
            // Add navigate up folder entry (..)
            folderContents.push(this.createNavigateUpFolder());
          }
    
          // Iterate over the file tree and map the entries for the files view table
          for (const fileEntry of fileTree || []) {
            const content = this.mapFileEntryToContent(fileEntry);
            // Don't add hidden files/folders
            if (content && !fileEntry.hidden) {
              const metadataEntry = metadataTree?.find(
                (md) => md.path === fileEntry.path,
              );
              if (metadataEntry?.version) {
                const version = Number(metadataEntry.version);
                content.latestVersion = version;
                content.currentVersion = version;
              }
              if (
                metadataEntry?.definition?.type &&
                metadataEntry?.definition?.content
              ) {
                content.metadata[content.latestVersion] = await parseRDFDefinition(
                  metadataEntry.definition.content,
                  metadataEntry.definition.type,
                );
              }
              if (metadataEntry?.availableVersions) {
                content.versions = metadataEntry.availableVersions.map((x) =>
                  Number(x),
                );
              }
    
              folderContents.push(content);
            }
          }
          return folderContents;
        },
    
        /**
         * Creates a representation for the "navigate up" folder.
         * @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("");
          const identifier = uuidv4();
          const version = +new Date();
          return {
            id: identifier,
            name: "..",
            path: path,
            readOnly: true,
            type: "Tree",
            parentDirectory: "",
            versions: [version],
            latestVersion: version,
            currentVersion: version,
            metadata: { [version]: factory.dataset() as unknown as Dataset },
          } satisfies ReadOnlyFolderInformation;
        },
    
        /**
         * Maps a file entry to its corresponding folder content.
         * @param {FileTreeDto} fileEntry - The file data transfer object to map.
         * @returns {FolderContent | undefined} The corresponding folder content or undefined if the type is unknown.
         */
        mapFileEntryToContent(fileEntry: FileTreeDto): FolderContent | undefined {
          let content: FolderContent | undefined = undefined;
          switch (fileEntry.type) {
            case "Tree":
              content = treeMapper.map(TreeDto2FolderInformation, fileEntry);
              break;
            case "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.
         */
        onSelection(items: FolderContent[]) {
          this.$emit("showDetail", items.length > 0);
          this.selectAll =
            items.length > 0 && items.length === this.folderContents.length;
          // Filter out read-only entires like the navigation up folder ".."
          const selectedFiles = items.filter((item) => !item.readOnly);
          this.updateFileListEdit(selectedFiles);
        },
    
        /**
         * Selects or deselects all rows.
         */
        allSelect() {
          const tableRef = this.$refs.adaptTable as unknown as AdaptTable;
          if (this.selectAll) {
            tableRef.selectAllRows();
          } else {
            this.selectedItems.length = 0;
            this.onSelection(this.selectedItems);
          }
        },
    
        /**
         * Selects or deselects a row
         */
        select(item: FolderContent) {
          if (this.selectedItems.includes(item)) {
            this.selectedItems.splice(this.selectedItems.indexOf(item), 1);
          } else {
            this.selectedItems.push(item);
          }
          this.onSelection(this.selectedItems);
        },
    
        /**
         * Handler for the filtered elements
         * @param elements filtered elements
         */
        filtered(elements: FolderContent[]) {
          this.currentPage = 1;
          this.filteredRows = elements.length;
        },
    
        /**
         * Emits an event for file selection.
         */
        clickFileSelect() {
          this.$emit("clickFileSelect");
        },
    
        /**
         * Updates the file list for editing based on current selections.
         */
        async updateFileListEdit(selectedFiles: FolderContent[]) {
          const existingFilesSet = new Set(this.fileListEdit.map((f) => f.name));
          const newFileListEdit: FileInformation[] = [];
    
          for (const file of selectedFiles) {
            // Check if file already exists in the list.
            // Keep already entered metadata values, if another entry selected.
            if (
              existingFilesSet.has(file.name) &&
              this.dirTrail === file.parentDirectory
            ) {
              const existingFile = this.fileListEdit.find(
                (f) => f.name === file.name,
              );
              if (existingFile) {
                newFileListEdit.push(existingFile as FileInformation);
              }
              continue;
            }
    
            // If file doesn't exist, fetch metadata and add
            const loadedMetadata = await MetadataManagerUtil.loadMetadataForFile(
              file,
              this.project,
              this.resource,
            );
    
            const version = file.latestVersion;
            newFileListEdit.push({
              ...file,
              uploading: false,
              metadata: { [version]: loadedMetadata },
              info: file.type === "Tree" ? undefined : file.info,
              size: file.type === "Tree" ? 0 : file.size,
            } as FileInformation);
          }
    
          this.$emit("fileListEdit", newFileListEdit);
          this.$emit("showDetail", selectedFiles.length > 0);
        },
    
        /**
         * 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 {
          return item.lastModified
            ? new Date(item.lastModified).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 {
          return item.type === "Leaf" ? FileUtil.formatBytes(item.size) : "";
        },
      },
      expose: [
        "constructFolderContents",
        "navigateTree",
        "onSelection",
        "selectedItems",
      ],
    });
    </script>
    
    <style>
    #resourceViewTable thead th:first-of-type div {
      display: inline;
      position: relative;
      margin-left: 5px;
    }
    
    #resourceViewTable thead th:first-of-type div label {
      cursor: pointer;
    }
    
    #resourceViewTable thead th:last-of-type {
      text-align: center;
    }
    
    #resourceViewTable td,
    #resourceViewTable th {
      vertical-align: middle;
      cursor: initial;
      white-space: nowrap;
    }
    
    #resourceViewTable td,
    #resourceViewTable tr td:not(:first-child):not(:nth-child(2)):not(:last-child),
    #resourceViewTable tr th:not(:first-child):not(:nth-child(2)):not(:last-child) {
      position: relative;
    }
    
    #resourceViewTable td .fileViewEntry {
      padding-right: 30px;
      vertical-align: middle;
    }
    
    #resourceViewTable tr td:nth-child(3),
    #resourceViewTable tr th:nth-child(3) {
      max-width: 170px !important;
      min-width: 170px !important;
      width: 170px !important;
    }
    
    #resourceViewTable tr td:last-child,
    #resourceViewTable tr th:last-child {
      max-width: 55px !important;
      min-width: 55px !important;
      width: 55px !important;
    }
    
    #resourceViewTable tr td:not(:first-child):not(:nth-child(2)):not(:last-child),
    #resourceViewTable tr th:not(:first-child):not(:nth-child(2)):not(:last-child) {
      max-width: 200px;
      text-overflow: ellipsis;
      overflow: hidden;
    }
    
    #resourceViewTable i,
    #resourceViewTable input,
    #resourceViewTable div.tableCheck.form-check-input label,
    .checkFile {
      display: inline-flex;
      vertical-align: middle;
      align-items: center;
    }
    
    .DataSource {
      height: inherit;
      margin-top: 7px;
    }
    
    #resourceViewTable {
      overflow: visible;
    }
    
    #resourceViewTable :deep(.btn-link) {
      padding-left: 5px !important;
      padding-right: 5px !important;
      background: none !important;
      min-width: auto;
    }
    
    #resourceViewTable th:last-of-type,
    .center-align {
      text-align: center;
    }
    
    #resourceViewTable th > i {
      position: absolute;
      right: 10px;
      top: 8px;
    }
    
    #resourceViewTable th .additionalColumnHeader {
      margin-right: 20px;
    }
    
    .adaptTable {
      overflow: auto;
      position: absolute;
      top: 65px;
      bottom: 0px;
      left: 0px;
      right: 0px;
      width: auto;
    }
    
    .fileViewEntryWrapper {
      align-items: center;
    }
    
    .fileIconEntry {
      margin-top: -0.3rem;
    }
    a.fileViewEntry {
      cursor: pointer;
    }
    .tableCheck {
      text-align: center;
      margin-right: -0.5em;
      position: absolute;
      top: 15px;
      left: 17px;
      cursor: pointer;
    }
    #pagination {
      margin-top: 0.5rem;
      margin-bottom: 0.5rem;
    }
    #pagination :deep(li.page-item.active button) {
      z-index: 0;
    }
    #resourceViewTable .tableCheck,
    #resourceViewTable .dotMenu {
      visibility: hidden;
    }
    
    #resourceViewTable tr:hover .tableCheck,
    #resourceViewTable tr:hover .dotMenu,
    #resourceViewTable .selected .tableCheck {
      visibility: visible;
    }
    </style>