Loading client/src/components/History/CurrentHistory/HistoryOperations/OperationErrorDialog.vue +8 −2 Original line number Diff line number Diff line <template> <b-modal v-model="show" title="Something went wrong..." header-text-variant="danger" :title="title" :header-text-variant="titleVariant" title-tag="h2" scrollable ok-only Loading Loading @@ -60,6 +60,12 @@ export default { } return []; }, title() { return this.isPartialSuccess ? "Some items could not be processed" : "Something went wrong..."; }, titleVariant() { return this.isPartialSuccess ? "warning" : "danger"; }, errorMessage() { return this.operationError?.errorMessage?.message; }, Loading client/src/components/History/CurrentHistory/HistoryOperations/SelectionOperations.test.js +36 −2 Original line number Diff line number Diff line Loading @@ -24,6 +24,9 @@ const TASKS_CONFIG = { enable_celery_tasks: true, }; const getPurgedContentSelection = () => new Map([["FAKE_ID", { purged: true }]]); const getNonPurgedContentSelection = () => new Map([["FAKE_ID", { purged: false }]]); async function mountSelectionOperationsWrapper(config) { const wrapper = shallowMount( SelectionOperations, Loading Loading @@ -93,10 +96,41 @@ describe("History Selection Operations", () => { expect(wrapper.find(option).exists()).toBe(false); }); it("should display 'undelete' option only on deleted items", async () => { it("should display 'permanently delete' option always", async () => { const option = '[data-description="purge option"]'; expect(wrapper.find(option).exists()).toBe(true); await wrapper.setProps({ filterText: "deleted:true" }); expect(wrapper.find(option).exists()).toBe(true); }); it("should display 'undelete' option only on deleted and non-purged items", async () => { const option = '[data-description="undelete option"]'; expect(wrapper.find(option).exists()).toBe(false); await wrapper.setProps({ filterText: "deleted:true" }); await wrapper.setProps({ filterText: "deleted:true", 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({ filterText: "deleted:true", contentSelection: getPurgedContentSelection(), isQuerySelection: false, }); expect(wrapper.find(option).exists()).toBe(false); }); it("should display 'undelete' option when is query selection mode and filtering by deleted", 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:true", contentSelection: getPurgedContentSelection(), isQuerySelection: true, }); expect(wrapper.find(option).exists()).toBe(true); }); Loading client/src/components/History/CurrentHistory/HistoryOperations/SelectionOperations.vue +14 −3 Original line number Diff line number Diff line Loading @@ -20,15 +20,15 @@ <span v-localize>Hide</span> </b-dropdown-item> <b-dropdown-item v-if="showDeleted" v-if="canUndeleteSelection" v-b-modal:restore-selected-content data-description="undelete option"> <span v-localize>Undelete</span> </b-dropdown-item> <b-dropdown-item v-else v-b-modal:delete-selected-content data-description="delete option"> <b-dropdown-item v-if="!showDeleted" v-b-modal:delete-selected-content data-description="delete option"> <span v-localize>Delete</span> </b-dropdown-item> <b-dropdown-item v-if="!showDeleted" v-b-modal:purge-selected-content data-description="purge option"> <b-dropdown-item v-b-modal:purge-selected-content data-description="purge option"> <span v-localize>Delete (permanently)</span> </b-dropdown-item> <b-dropdown-divider v-if="showBuildOptions" /> Loading Loading @@ -210,6 +210,17 @@ export default { noTagsSelected() { return this.selectedTags.length === 0; }, canUndeleteSelection() { return this.showDeleted && (this.isQuerySelection || !this.areAllSelectedPurged); }, areAllSelectedPurged() { for (const item of this.contentSelection.values()) { if (Object.prototype.hasOwnProperty.call(item, "purged") && !item["purged"]) { return false; } } return true; }, }, watch: { hasSelection(newVal) { Loading lib/galaxy/webapps/galaxy/services/history_contents.py +5 −1 Original line number Diff line number Diff line Loading @@ -1602,11 +1602,15 @@ class HistoryItemOperator: def _undelete(self, item: HistoryItemModel): if getattr(item, "purged", False): return raise exceptions.ItemDeletionException("This item has been permanently deleted and cannot be recovered.") manager = self._get_item_manager(item) manager.undelete(item, flush=self.flush) def _purge(self, item: HistoryItemModel, trans: ProvidesHistoryContext): if getattr(item, "purged", False): # TODO: remove this `update` when we can properly track the operation results to notify the history item.update() return if isinstance(item, HistoryDatasetCollectionAssociation): return self.dataset_collection_manager.delete(trans, "history", item.id, recursive=True, purge=True) self.hda_manager.purge(item, flush=self.flush) Loading lib/galaxy_test/api/test_history_contents.py +5 −2 Original line number Diff line number Diff line Loading @@ -1122,7 +1122,7 @@ class HistoryContentsApiBulkOperationTestCase(ApiTestCase): # collections don't have a `purged` attribute but they should be marked deleted on purge assert purged_collection["deleted"] is True # Un-deleting a purged dataset should not have any effect # Un-deleting a purged dataset should not have any effect and raise an error payload = { "operation": "undelete", "items": [ Loading @@ -1134,7 +1134,10 @@ class HistoryContentsApiBulkOperationTestCase(ApiTestCase): } bulk_operation_result = self._apply_bulk_operation(history_id, payload) history_contents = self._get_history_contents(history_id) self._assert_bulk_success(bulk_operation_result, 1) assert bulk_operation_result["success_count"] == 0 assert len(bulk_operation_result["errors"]) == 1 error = bulk_operation_result["errors"][0] assert error["item"]["id"] == datasets_ids[0] purged_dataset = self._get_dataset_with_id_from_history_contents(history_contents, datasets_ids[0]) assert purged_dataset["deleted"] is True assert purged_dataset["purged"] is True Loading Loading
client/src/components/History/CurrentHistory/HistoryOperations/OperationErrorDialog.vue +8 −2 Original line number Diff line number Diff line <template> <b-modal v-model="show" title="Something went wrong..." header-text-variant="danger" :title="title" :header-text-variant="titleVariant" title-tag="h2" scrollable ok-only Loading Loading @@ -60,6 +60,12 @@ export default { } return []; }, title() { return this.isPartialSuccess ? "Some items could not be processed" : "Something went wrong..."; }, titleVariant() { return this.isPartialSuccess ? "warning" : "danger"; }, errorMessage() { return this.operationError?.errorMessage?.message; }, Loading
client/src/components/History/CurrentHistory/HistoryOperations/SelectionOperations.test.js +36 −2 Original line number Diff line number Diff line Loading @@ -24,6 +24,9 @@ const TASKS_CONFIG = { enable_celery_tasks: true, }; const getPurgedContentSelection = () => new Map([["FAKE_ID", { purged: true }]]); const getNonPurgedContentSelection = () => new Map([["FAKE_ID", { purged: false }]]); async function mountSelectionOperationsWrapper(config) { const wrapper = shallowMount( SelectionOperations, Loading Loading @@ -93,10 +96,41 @@ describe("History Selection Operations", () => { expect(wrapper.find(option).exists()).toBe(false); }); it("should display 'undelete' option only on deleted items", async () => { it("should display 'permanently delete' option always", async () => { const option = '[data-description="purge option"]'; expect(wrapper.find(option).exists()).toBe(true); await wrapper.setProps({ filterText: "deleted:true" }); expect(wrapper.find(option).exists()).toBe(true); }); it("should display 'undelete' option only on deleted and non-purged items", async () => { const option = '[data-description="undelete option"]'; expect(wrapper.find(option).exists()).toBe(false); await wrapper.setProps({ filterText: "deleted:true" }); await wrapper.setProps({ filterText: "deleted:true", 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({ filterText: "deleted:true", contentSelection: getPurgedContentSelection(), isQuerySelection: false, }); expect(wrapper.find(option).exists()).toBe(false); }); it("should display 'undelete' option when is query selection mode and filtering by deleted", 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:true", contentSelection: getPurgedContentSelection(), isQuerySelection: true, }); expect(wrapper.find(option).exists()).toBe(true); }); Loading
client/src/components/History/CurrentHistory/HistoryOperations/SelectionOperations.vue +14 −3 Original line number Diff line number Diff line Loading @@ -20,15 +20,15 @@ <span v-localize>Hide</span> </b-dropdown-item> <b-dropdown-item v-if="showDeleted" v-if="canUndeleteSelection" v-b-modal:restore-selected-content data-description="undelete option"> <span v-localize>Undelete</span> </b-dropdown-item> <b-dropdown-item v-else v-b-modal:delete-selected-content data-description="delete option"> <b-dropdown-item v-if="!showDeleted" v-b-modal:delete-selected-content data-description="delete option"> <span v-localize>Delete</span> </b-dropdown-item> <b-dropdown-item v-if="!showDeleted" v-b-modal:purge-selected-content data-description="purge option"> <b-dropdown-item v-b-modal:purge-selected-content data-description="purge option"> <span v-localize>Delete (permanently)</span> </b-dropdown-item> <b-dropdown-divider v-if="showBuildOptions" /> Loading Loading @@ -210,6 +210,17 @@ export default { noTagsSelected() { return this.selectedTags.length === 0; }, canUndeleteSelection() { return this.showDeleted && (this.isQuerySelection || !this.areAllSelectedPurged); }, areAllSelectedPurged() { for (const item of this.contentSelection.values()) { if (Object.prototype.hasOwnProperty.call(item, "purged") && !item["purged"]) { return false; } } return true; }, }, watch: { hasSelection(newVal) { Loading
lib/galaxy/webapps/galaxy/services/history_contents.py +5 −1 Original line number Diff line number Diff line Loading @@ -1602,11 +1602,15 @@ class HistoryItemOperator: def _undelete(self, item: HistoryItemModel): if getattr(item, "purged", False): return raise exceptions.ItemDeletionException("This item has been permanently deleted and cannot be recovered.") manager = self._get_item_manager(item) manager.undelete(item, flush=self.flush) def _purge(self, item: HistoryItemModel, trans: ProvidesHistoryContext): if getattr(item, "purged", False): # TODO: remove this `update` when we can properly track the operation results to notify the history item.update() return if isinstance(item, HistoryDatasetCollectionAssociation): return self.dataset_collection_manager.delete(trans, "history", item.id, recursive=True, purge=True) self.hda_manager.purge(item, flush=self.flush) Loading
lib/galaxy_test/api/test_history_contents.py +5 −2 Original line number Diff line number Diff line Loading @@ -1122,7 +1122,7 @@ class HistoryContentsApiBulkOperationTestCase(ApiTestCase): # collections don't have a `purged` attribute but they should be marked deleted on purge assert purged_collection["deleted"] is True # Un-deleting a purged dataset should not have any effect # Un-deleting a purged dataset should not have any effect and raise an error payload = { "operation": "undelete", "items": [ Loading @@ -1134,7 +1134,10 @@ class HistoryContentsApiBulkOperationTestCase(ApiTestCase): } bulk_operation_result = self._apply_bulk_operation(history_id, payload) history_contents = self._get_history_contents(history_id) self._assert_bulk_success(bulk_operation_result, 1) assert bulk_operation_result["success_count"] == 0 assert len(bulk_operation_result["errors"]) == 1 error = bulk_operation_result["errors"][0] assert error["item"]["id"] == datasets_ids[0] purged_dataset = self._get_dataset_with_id_from_history_contents(history_contents, datasets_ids[0]) assert purged_dataset["deleted"] is True assert purged_dataset["purged"] is True Loading