diff --git a/client/src/components/History/CurrentHistory/HistoryOperations/SelectionOperations.test.js b/client/src/components/History/CurrentHistory/HistoryOperations/SelectionOperations.test.js index 7cb65453957c..a0ace176ad53 100644 --- a/client/src/components/History/CurrentHistory/HistoryOperations/SelectionOperations.test.js +++ b/client/src/components/History/CurrentHistory/HistoryOperations/SelectionOperations.test.js @@ -84,20 +84,33 @@ describe("History Selection Operations", () => { expect(wrapper.find(option).exists()).toBe(false); }); - it("should display 'unhide' option only on hidden items", async () => { + it("should display 'unhide' option on hidden items", async () => { const option = '[data-description="unhide option"]'; expect(wrapper.find(option).exists()).toBe(false); await wrapper.setProps({ filterText: "visible:false" }); expect(wrapper.find(option).exists()).toBe(true); }); - it("should display 'delete' option only on non-deleted items", async () => { + it("should display 'unhide' option when hidden and visible items are mixed", async () => { + const option = '[data-description="unhide option"]'; + expect(wrapper.find(option).exists()).toBe(false); + await wrapper.setProps({ filterText: "visible:any" }); + expect(wrapper.find(option).exists()).toBe(true); + }); + + it("should display 'delete' option on non-deleted items", async () => { const option = '[data-description="delete option"]'; expect(wrapper.find(option).exists()).toBe(true); await wrapper.setProps({ filterText: "deleted:true" }); expect(wrapper.find(option).exists()).toBe(false); }); + it("should display 'delete' option when non-deleted and deleted items are mixed", async () => { + const option = '[data-description="delete option"]'; + await wrapper.setProps({ filterText: "deleted:any" }); + expect(wrapper.find(option).exists()).toBe(true); + }); + it("should display 'permanently delete' option always", async () => { const option = '[data-description="purge option"]'; expect(wrapper.find(option).exists()).toBe(true); @@ -105,7 +118,7 @@ describe("History Selection Operations", () => { expect(wrapper.find(option).exists()).toBe(true); }); - it("should display 'undelete' option only on deleted and non-purged items", async () => { + it("should display 'undelete' option on deleted and non-purged items", async () => { const option = '[data-description="undelete option"]'; expect(wrapper.find(option).exists()).toBe(false); await wrapper.setProps({ @@ -115,6 +128,15 @@ describe("History Selection Operations", () => { expect(wrapper.find(option).exists()).toBe(true); }); + it("should display 'undelete' option when non-purged items (deleted or not) are mixed", async () => { + const option = '[data-description="undelete option"]'; + await wrapper.setProps({ + filterText: "deleted:any", + contentSelection: getNonPurgedContentSelection(), + }); + expect(wrapper.find(option).exists()).toBe(true); + }); + it("should not display 'undelete' when is manual selection mode and all selected items are purged", async () => { const option = '[data-description="undelete option"]'; await wrapper.setProps({ @@ -136,6 +158,17 @@ describe("History Selection Operations", () => { expect(wrapper.find(option).exists()).toBe(true); }); + it("should display 'undelete' option when is query selection mode and filtering by any deleted state", async () => { + const option = '[data-description="undelete option"]'; + // In query selection mode we don't know if some items may not be purged, so we allow to undelete + await wrapper.setProps({ + filterText: "deleted:any", + contentSelection: getPurgedContentSelection(), + isQuerySelection: true, + }); + expect(wrapper.find(option).exists()).toBe(true); + }); + it("should display collection building options only on visible and non-deleted items", async () => { const buildListOption = '[data-description="build list"]'; const buildPairOption = '[data-description="build pair"]'; @@ -151,6 +184,14 @@ describe("History Selection Operations", () => { expect(wrapper.find(buildListOption).exists()).toBe(false); expect(wrapper.find(buildPairOption).exists()).toBe(false); expect(wrapper.find(buildListOfPairsOption).exists()).toBe(false); + await wrapper.setProps({ filterText: "visible:any" }); + expect(wrapper.find(buildListOption).exists()).toBe(false); + expect(wrapper.find(buildPairOption).exists()).toBe(false); + expect(wrapper.find(buildListOfPairsOption).exists()).toBe(false); + await wrapper.setProps({ filterText: "deleted:any" }); + expect(wrapper.find(buildListOption).exists()).toBe(false); + expect(wrapper.find(buildPairOption).exists()).toBe(false); + expect(wrapper.find(buildListOfPairsOption).exists()).toBe(false); }); it("should display list building option when all are selected", async () => { diff --git a/client/src/components/History/CurrentHistory/HistoryOperations/SelectionOperations.vue b/client/src/components/History/CurrentHistory/HistoryOperations/SelectionOperations.vue index 9b70de885019..ed42dd945bbd 100644 --- a/client/src/components/History/CurrentHistory/HistoryOperations/SelectionOperations.vue +++ b/client/src/components/History/CurrentHistory/HistoryOperations/SelectionOperations.vue @@ -24,7 +24,10 @@ data-description="undelete option"> Undelete - + Delete @@ -191,10 +194,14 @@ export default { computed: { /** @returns {Boolean} */ showHidden() { - return HistoryFilters.checkFilter(this.filterText, "visible", false); + return !HistoryFilters.checkFilter(this.filterText, "visible", true); }, /** @returns {Boolean} */ showDeleted() { + return !HistoryFilters.checkFilter(this.filterText, "deleted", false); + }, + /** @returns {Boolean} */ + showStrictDeleted() { return HistoryFilters.checkFilter(this.filterText, "deleted", true); }, /** @returns {Boolean} */ diff --git a/client/src/components/Tool/utilities.js b/client/src/components/Tool/utilities.js index 1b0a6d5a0ad2..84f9d2bd3fd2 100644 --- a/client/src/components/Tool/utilities.js +++ b/client/src/components/Tool/utilities.js @@ -2,7 +2,9 @@ import { getAppRoot } from "onload/loadConfig"; import { copy } from "utils/clipboard"; export function copyLink(toolId, message) { - copy(`${window.location.origin + getAppRoot()}root?tool_id=${toolId}`, message); + const link = `${window.location.origin + getAppRoot()}root?tool_id=${toolId}`; + // Encode the link to handle special characters in tool id + copy(encodeURI(link), message); } export function copyId(toolId, message) { diff --git a/client/src/components/Tool/utilities.test.ts b/client/src/components/Tool/utilities.test.ts new file mode 100644 index 000000000000..8295d1e2bfe6 --- /dev/null +++ b/client/src/components/Tool/utilities.test.ts @@ -0,0 +1,36 @@ +import { copyLink } from "./utilities"; + +const writeText = jest.fn(); + +Object.assign(navigator, { + clipboard: { + writeText, + }, +}); + +describe("copyLink", () => { + beforeEach(() => { + (navigator.clipboard.writeText as jest.Mock).mockResolvedValue(undefined); + }); + + it("should copy the link to the clipboard", () => { + const toolId = "MyToolId"; + copyLink(toolId); + expect(writeText).toHaveBeenCalledTimes(1); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining(toolId)); + }); + + it("should encode the tool id with spaces", () => { + const toolId = "My Tool Id"; + copyLink(toolId); + expect(writeText).toHaveBeenCalledTimes(1); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining("My%20Tool%20Id")); + }); + + it("should not encode the character '+' in the tool id", () => { + const toolId = "My Tool Id+1"; + copyLink(toolId); + expect(writeText).toHaveBeenCalledTimes(1); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining("My%20Tool%20Id+1")); + }); +}); diff --git a/lib/galaxy/webapps/galaxy/services/history_contents.py b/lib/galaxy/webapps/galaxy/services/history_contents.py index d82375f584ec..641020483d94 100644 --- a/lib/galaxy/webapps/galaxy/services/history_contents.py +++ b/lib/galaxy/webapps/galaxy/services/history_contents.py @@ -1421,8 +1421,12 @@ def _unhide(self, item: HistoryItemModel): def _delete(self, item: HistoryItemModel, trans: ProvidesHistoryContext): if isinstance(item, HistoryDatasetCollectionAssociation): - return self.dataset_collection_manager.delete(trans, "history", item.id, recursive=True, purge=False) - return self.hda_manager.delete(item, flush=self.flush) + self.dataset_collection_manager.delete(trans, "history", item.id, recursive=True, purge=False) + else: + self.hda_manager.delete(item, flush=self.flush) + # In the edge case where all selected items are already deleted we need to force an update + # otherwise the history will wait indefinitely for the items to be deleted + item.update() def _undelete(self, item: HistoryItemModel): if getattr(item, "purged", False):