Commit 93731d9f authored by Sirieam Marie Hunke's avatar Sirieam Marie Hunke Committed by Benedikt Heinrichs
Browse files

New: Sharing Link for Files

parent f59cfeb9
Loading
Loading
Loading
Loading
+2 −2
Original line number Diff line number Diff line
@@ -16,8 +16,8 @@ import type {
} from "@coscine/api-client/dist/types/Coscine.Api";

export const testResourceType: ResourceTypeInformationDto = {
  specificType: "gitlab",
  generalType: "gitlab",
  specificType: "rds",
  generalType: "rds",
  id: "123497",
  canCreate: true,
  canCreateLinks: true,
+58 −7
Original line number Diff line number Diff line
@@ -57,6 +57,7 @@ describe("FilesView.vue", async () => {
      propsData: {
        dirTrail: "/",
        dirCrumbs: [],
        folderContents: [],
      },
    });
  };
@@ -113,12 +114,22 @@ describe("FilesView.vue", async () => {
      timeout: 10000,
    },
    async () => {
      const folderContents = await wrapper.vm.constructFolderContents(
        getFileTreeResponse,
        getMetadataTreeResponse,
      );

      await wrapper.setProps({
        dirTrail: "/",
        dirCrumbs: [],
        folderContents,
      });

      await wrapper.vm.$nextTick();

      // Wait for 1 second until everything is set up
      await sleep(1000); // Don't remove!

      /** TODO: Figure out why this doesn't work
      const entry = wrapper.find("#fileViewEntry2");
      await entry.trigger("click");

@@ -126,7 +137,47 @@ describe("FilesView.vue", async () => {
      await sleep(1000); // Don't remove!

      expect(resourceStore.download).toBeCalledTimes(1);
        */
    },
  );
  test(
    "Trigger share uses getdownloadUrl store method",
    {
      // Override the maximum run time for this test (10 sec), due to the sleep() calls
      timeout: 10000,
    },
    async () => {
      const folderContents = await wrapper.vm.constructFolderContents(
        getFileTreeResponse,
        getMetadataTreeResponse,
      );

      await wrapper.setProps({
        dirTrail: "/",
        dirCrumbs: [],
        folderContents,
      });

      await wrapper.vm.$nextTick();

      // Wait for 1 second until everything is set up
      await sleep(1000); // Don't remove!

      // Find three point menu
      const dropdown = wrapper.find(".dropdown.dotMenu");
      await dropdown.trigger("click");

      // Wait for the dropdown menu to appear
      await wrapper.vm.$nextTick();

      // Find share button
      const shareButton = wrapper.find("#dropDownItemShare");
      await shareButton.trigger("click");

      // Wait for the action to complete
      await wrapper.vm.$nextTick();

      // Verify that the store method was called
      expect(resourceStore.getDownloadUrl).toBeCalledTimes(1);
    },
  );
});
+82 −19
Original line number Diff line number Diff line
@@ -132,18 +132,34 @@
              <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("page.resource.metadataManagerBtnDownload") }}
              </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 &&
@@ -190,6 +206,7 @@
// 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";
@@ -209,15 +226,16 @@ import type {
  FileTreeDto,
  MetadataTreeDto,
} from "@coscine/api-client/dist/types/Coscine.Api";
import type {
  AdaptTable,
  BilingualLabels,
  CustomTableField,
  FileInformation,
  FolderContent,
  FolderInformation,
  ReadOnlyFolderInformation,
  VisitedResourceObject,
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 };
@@ -265,13 +283,15 @@ export default defineComponent({
  setup() {
    const resourceStore = useResourceStore();
    const projectStore = useProjectStore();
    const notificationStore = useNotificationStore();

    return { resourceStore, projectStore };
    return { resourceStore, projectStore, notificationStore };
  },

  data() {
    return {
      currentPage: 1,
      coscineResourceTypes: CoscineResourceTypes,
      filteredRows: -1,
      isBusy: true,
      isLoadingSearchResults: false,
@@ -282,11 +302,16 @@ export default defineComponent({
      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;
    },
@@ -600,13 +625,12 @@ export default defineComponent({
      // 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() {
    async navigateTree(keepEditedFiles: boolean = false) {
      // Start with the current folder as the reference
      let currentFolder = this.dirTrail;

@@ -623,7 +647,10 @@ export default defineComponent({
      }

      // Finally, open the current folder to display its contents
      await this.openFolder({ path: currentFolder } as FolderInformation);
      await this.openFolder(
        { path: currentFolder } as FolderInformation,
        keepEditedFiles,
      );
    },

    /**
@@ -747,20 +774,51 @@ export default defineComponent({
        );
      }
    },
    /**
     * 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) {
    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);
@@ -1014,7 +1072,12 @@ export default defineComponent({
      return item.type === "Leaf" ? FileUtil.formatBytes(item.size) : "";
    },
  },
  expose: ["navigateTree", "onSelection", "selectedItems"],
  expose: [
    "constructFolderContents",
    "navigateTree",
    "onSelection",
    "selectedItems",
  ],
});
</script>

+50 −11
Original line number Diff line number Diff line
@@ -3,6 +3,7 @@
    <!-- Header Buttons -->
    <MetadataManagerHeader
      v-if="resource"
      :current-folder-content="currentFolderContent"
      :editable-data-url="editableDataUrl"
      :is-uploading="isUploading"
      :read-only="readOnly"
@@ -15,6 +16,7 @@
      @selectFiles="selectFiles"
      @selectFolders="selectFolders"
      @showModalDeleteFolderContents="showModalDeleteFolderContents"
      @copyUrl="copyUrl"
    />

    <!-- Selected Files Table -->
@@ -987,21 +989,53 @@ export default defineComponent({
    },

    /**
     * Downloads selected files.
     * If the detail view is active, attempts to download each shown file.
     * TODO: Review this behavior, this seems not very intuitive.
     *
     * Asynchronously creates the download url for sharing.
     * @async
     */
    async copyUrl() {
      if (
        this.showDetail &&
        this.project?.id &&
        this.resource?.id &&
        this.currentFolderContent
      ) {
        const downloadUrl = await this.resourceStore.getDownloadUrl(
          this.project.id,
          this.resource.id,
          this.currentFolderContent.path,
          this.currentFolderContent.name,
          this.currentFolderContent.actions,
        );

        if (downloadUrl) {
          navigator.clipboard.writeText(downloadUrl);
          this.notificationStore.postNotification({
            title: this.$t("page.resource.toClipboard").toString(),
            body: this.$t("page.resource.expirationMessage").toString(),
          });
        }
      }
    },

    /**
     * Downloads selected file or all files when all files is selected.
     * @async
     */
    async download() {
      const downloadFiles: FolderContent[] = [];
      if (this.currentFolderContent) {
        downloadFiles.push(this.currentFolderContent);
      } else {
        downloadFiles.push(...this.shownFiles);
      }
      for (const downloadFile of downloadFiles) {
        if (this.showDetail && this.project?.id && this.resource?.id) {
        for (const editableFile of this.shownFiles) {
          await this.resourceStore.download(
            this.project.id,
            this.resource.id,
            editableFile.path,
            editableFile.name,
            editableFile.actions,
            downloadFile.path,
            downloadFile.name,
            downloadFile.actions,
          );
        }
      }
@@ -1287,12 +1321,17 @@ export default defineComponent({
    cleanup() {
      this.$emit("emptyFileLists", true);
    },

    /**
     * Emits an event to display the modal for deleting folder contents.
     */
    showModalDeleteFolderContents() {
      this.$emit("showModalDelete", this.fileListEdit);
      const deleteList: FolderContent[] = [];
      if (this.currentFolderContent) {
        deleteList.push(this.currentFolderContent);
      } else {
        deleteList.push(...this.shownFiles);
      }
      this.$emit("showModalDelete", deleteList);
    },

    /**
+25 −2
Original line number Diff line number Diff line
@@ -59,14 +59,33 @@
          :disabled="!showDetail || shownFiles.length === 0"
        >
          <b-dropdown-item
            :disabled="!showDetail || shownFiles.length === 0"
            v-if="!editableDataUrl && !isGuest && !isGitLab"
            :disabled="
              !showDetail ||
              shownFiles.length === 0 ||
              !currentFolderContent ||
              currentFolderContent.type !== 'Leaf'
            "
            @click="$emit('copyUrl')"
          >
            {{ $t("page.resource.metadataManagerBtnCopyLink") }}
          </b-dropdown-item>
          <b-dropdown-item
            :disabled="
              !showDetail || shownFiles.length === 0 || !currentFolderContent
            "
            @click="$emit('downloadMetadata')"
          >
            {{ $t("page.resource.metadataManagerBtnDownloadMetadata") }}
          </b-dropdown-item>
          <b-dropdown-divider
            v-if="!editableDataUrl && !isGuest"
          ></b-dropdown-divider>
          <b-dropdown-item
            v-if="resource.metadataExtraction"
            :disabled="!showDetail || shownFiles.length === 0"
            :disabled="
              !showDetail || shownFiles.length === 0 || !currentFolderContent
            "
            @click="$emit('downloadExtractedMetadata')"
          >
            {{
@@ -114,6 +133,10 @@ import {

export default defineComponent({
  props: {
    currentFolderContent: {
      default: undefined,
      type: [Object, undefined] as PropType<FolderContent | undefined>,
    },
    editableDataUrl: {
      required: true,
      type: Boolean,
Loading