From 75152d5449aa55e399f52377eb0e30e0e2354c19 Mon Sep 17 00:00:00 2001 From: guerler Date: Sat, 13 Jan 2024 16:17:29 +0300 Subject: [PATCH] Allow undeleting multiple histories --- client/src/api/histories.ts | 1 + client/src/api/schema/schema.ts | 50 +++++++++++++++++++ client/src/components/Grid/GridList.vue | 2 +- .../src/components/Grid/configs/histories.ts | 26 +++++++++- lib/galaxy/managers/histories.py | 4 +- lib/galaxy/webapps/galaxy/api/histories.py | 21 +++++++- 6 files changed, 99 insertions(+), 5 deletions(-) diff --git a/client/src/api/histories.ts b/client/src/api/histories.ts index 7c7fc558b7a..dc31257c9d8 100644 --- a/client/src/api/histories.ts +++ b/client/src/api/histories.ts @@ -5,4 +5,5 @@ export const archivedHistoriesFetcher = fetcher.path("/api/histories/archived"). export const deleteHistory = fetcher.path("/api/histories/{history_id}").method("delete").create(); export const deleteHistories = fetcher.path("/api/histories/batch/delete").method("post").create(); export const undeleteHistory = fetcher.path("/api/histories/deleted/{history_id}/undelete").method("post").create(); +export const undeleteHistories = fetcher.path("/api/histories/batch/undelete").method("post").create(); export const historiesQuery = fetcher.path("/api/histories/query").method("get").create(); diff --git a/client/src/api/schema/schema.ts b/client/src/api/schema/schema.ts index b3fd26bc7e4..69a2ad3f691 100644 --- a/client/src/api/schema/schema.ts +++ b/client/src/api/schema/schema.ts @@ -497,6 +497,10 @@ export interface paths { /** Marks several histories with the given IDs as deleted. */ post: operations["batch_delete_api_histories_batch_delete_post"]; }; + "/api/histories/batch/undelete": { + /** Marks several histories with the given IDs as undeleted. */ + post: operations["batch_undelete_api_histories_batch_undelete_post"]; + }; "/api/histories/count": { /** Returns number of histories for the current user. */ get: operations["count_api_histories_count_get"]; @@ -10372,6 +10376,14 @@ export interface components { */ title?: string | null; }; + /** UndeleteHistoriesPayload */ + UndeleteHistoriesPayload: { + /** + * IDs + * @description List of history IDs to be undeleted. + */ + ids: string[]; + }; /** * UpdateCollectionAttributePayload * @description Contains attributes that can be updated for all elements in a dataset collection. @@ -13889,6 +13901,44 @@ export interface operations { }; }; }; + batch_undelete_api_histories_batch_undelete_post: { + /** Marks several histories with the given IDs as undeleted. */ + parameters?: { + /** @description View to be passed to the serializer */ + /** @description Comma-separated list of keys to be passed to the serializer */ + query?: { + view?: string | null; + keys?: string | null; + }; + /** @description The user ID that will be used to effectively make this API call. Only admins and designated users can make API calls on behalf of other users. */ + header?: { + "run-as"?: string | null; + }; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["UndeleteHistoriesPayload"] | null; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + content: { + "application/json": ( + | components["schemas"]["HistoryDetailed"] + | components["schemas"]["HistorySummary"] + | components["schemas"]["HistoryMinimal"] + )[]; + }; + }; + /** @description Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; count_api_histories_count_get: { /** Returns number of histories for the current user. */ parameters?: { diff --git a/client/src/components/Grid/GridList.vue b/client/src/components/Grid/GridList.vue index fee5999bd82..034559fa972 100644 --- a/client/src/components/Grid/GridList.vue +++ b/client/src/components/Grid/GridList.vue @@ -102,6 +102,7 @@ function displayInitialMessage() { * Request grid data */ async function getGridData() { + selected.value = new Set(); if (props.gridConfig) { try { const offset = props.limit * (currentPage.value - 1); @@ -132,7 +133,6 @@ async function onBatchOperation(operation: BatchOperation, rowDataArray: Array) => data.length > 0 && !data.some((x) => !x.deleted || x.purged), + handler: async (data: Array) => { + if (confirm(_l(`Are you sure that you want to restore the selected histories?`))) { + try { + const historyIds = data.map((x) => String(x.id)); + await undeleteHistories({ ids: historyIds }); + return { + status: "success", + message: `Restored ${data.length} histories.`, + }; + } catch (e) { + return { + status: "danger", + message: `Failed to restore histories: ${errorMessageAsString(e)}`, + }; + } + } + }, + }, { title: "Purge", icon: faTrash, - condition: (data: Array) => data.length > 0 && !data.some((x) => x.deleted), + condition: (data: Array) => data.length > 0 && !data.some((x) => x.purged), handler: async (data: Array) => { if (confirm(_l(`Are you sure that you want to permanently delete the selected histories?`))) { try { diff --git a/lib/galaxy/managers/histories.py b/lib/galaxy/managers/histories.py index 8264a6795a2..69715c06955 100644 --- a/lib/galaxy/managers/histories.py +++ b/lib/galaxy/managers/histories.py @@ -202,7 +202,9 @@ class HistoryManager(sharable.SharableModelManager, deletable.PurgableManagerMix if show_purged: stmt = stmt.where(self.model_class.purged == true()) else: - stmt = stmt.where(self.model_class.deleted == (true() if show_deleted else false())) + stmt = stmt.where(self.model_class.purged == false()).where( + self.model_class.deleted == (true() if show_deleted else false()) + ) if include_total_count: total_matches = get_count(trans.sa_session, stmt) diff --git a/lib/galaxy/webapps/galaxy/api/histories.py b/lib/galaxy/webapps/galaxy/api/histories.py index 941a67e74e6..ce62e0a3964 100644 --- a/lib/galaxy/webapps/galaxy/api/histories.py +++ b/lib/galaxy/webapps/galaxy/api/histories.py @@ -147,6 +147,10 @@ class DeleteHistoriesPayload(BaseModel): ] +class UndeleteHistoriesPayload(BaseModel): + ids: Annotated[List[HistoryIDPathParam], Field(title="IDs", description="List of history IDs to be undeleted.")] + + @as_form class CreateHistoryFormData(CreateHistoryPayload): """Uses Form data instead of JSON""" @@ -398,7 +402,6 @@ class FastAPIHistories: purge = payload.purge results = [] for history_id in payload.ids: - log.debug(history_id) result = self.service.delete(trans, history_id, serialization_params, purge) results.append(result) return results @@ -415,6 +418,22 @@ class FastAPIHistories: ) -> AnyHistoryView: return self.service.undelete(trans, history_id, serialization_params) + @router.post( + "/api/histories/batch/undelete", + summary="Marks several histories with the given IDs as undeleted.", + ) + def batch_undelete( + self, + trans: ProvidesHistoryContext = DependsOnTrans, + serialization_params: SerializationParams = Depends(query_serialization_params), + payload: Optional[UndeleteHistoriesPayload] = Body(default=None), + ) -> List[AnyHistoryView]: + results = [] + for history_id in payload.ids: + result = self.service.undelete(trans, history_id, serialization_params) + results.append(result) + return results + @router.put( "/api/histories/{history_id}", summary="Updates the values for the history with the given ID.",