From 56dd4a18af74bcce78e8319effe2f04f4963225c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 1 Apr 2026 16:24:37 +0000 Subject: [PATCH 1/4] Initial plan From 2d9e95aa3d8442c538e3b1f6b1abaea53c250e7f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 1 Apr 2026 16:46:24 +0000 Subject: [PATCH 2/4] feat: implement post-job actions service, controller, and frontend Agent-Logs-Url: https://github.com/MaximumTrainer/OpenDataMask/sessions/57b16640-b697-457b-9948-ba50ad6b410a Co-authored-by: MaximumTrainer <1376575+MaximumTrainer@users.noreply.github.com> --- .../controller/PostJobActionController.kt | 8 + .../service/PostJobActionService.kt | 8 + .../controller/PostJobActionControllerTest.kt | 12 + .../service/PostJobActionServiceTest.kt | 22 ++ frontend/src/api/actions.ts | 34 +++ frontend/src/router/index.ts | 7 + frontend/src/types/index.ts | 23 ++ frontend/src/views/ActionsView.vue | 285 ++++++++++++++++++ frontend/src/views/WorkspaceDetailView.vue | 13 +- .../src/views/__tests__/ActionsView.test.ts | 14 + 10 files changed, 425 insertions(+), 1 deletion(-) create mode 100644 frontend/src/api/actions.ts create mode 100644 frontend/src/views/ActionsView.vue create mode 100644 frontend/src/views/__tests__/ActionsView.test.ts diff --git a/backend/src/main/kotlin/com/opendatamask/controller/PostJobActionController.kt b/backend/src/main/kotlin/com/opendatamask/controller/PostJobActionController.kt index e752d95..d21e196 100644 --- a/backend/src/main/kotlin/com/opendatamask/controller/PostJobActionController.kt +++ b/backend/src/main/kotlin/com/opendatamask/controller/PostJobActionController.kt @@ -24,6 +24,14 @@ class PostJobActionController( return ResponseEntity.status(HttpStatus.CREATED).body(service.createAction(toSave)) } + @PutMapping("/{actionId}") + fun updateAction( + @PathVariable workspaceId: Long, + @PathVariable actionId: Long, + @RequestBody action: PostJobAction + ): ResponseEntity = + ResponseEntity.ok(service.updateAction(actionId, action.copy(workspaceId = workspaceId))) + @DeleteMapping("/{actionId}") fun deleteAction( @PathVariable workspaceId: Long, diff --git a/backend/src/main/kotlin/com/opendatamask/service/PostJobActionService.kt b/backend/src/main/kotlin/com/opendatamask/service/PostJobActionService.kt index 892341d..030a9fc 100644 --- a/backend/src/main/kotlin/com/opendatamask/service/PostJobActionService.kt +++ b/backend/src/main/kotlin/com/opendatamask/service/PostJobActionService.kt @@ -103,5 +103,13 @@ class PostJobActionService( fun createAction(action: PostJobAction): PostJobAction = repository.save(action) fun listActions(workspaceId: Long): List = repository.findByWorkspaceId(workspaceId) + fun updateAction(id: Long, updated: PostJobAction): PostJobAction { + val existing = repository.findById(id) + .orElseThrow { NoSuchElementException("PostJobAction not found: $id") } + existing.actionType = updated.actionType + existing.config = updated.config + existing.enabled = updated.enabled + return repository.save(existing) + } fun deleteAction(id: Long) = repository.deleteById(id) } diff --git a/backend/src/test/kotlin/com/opendatamask/controller/PostJobActionControllerTest.kt b/backend/src/test/kotlin/com/opendatamask/controller/PostJobActionControllerTest.kt index 2c5729a..94d2834 100644 --- a/backend/src/test/kotlin/com/opendatamask/controller/PostJobActionControllerTest.kt +++ b/backend/src/test/kotlin/com/opendatamask/controller/PostJobActionControllerTest.kt @@ -66,4 +66,16 @@ class PostJobActionControllerTest { .andExpect(status().isNoContent) verify(service).deleteAction(42L) } + + @Test + fun `PUT update action returns 200`() { + val action = PostJobAction(workspaceId = 1L, actionType = ActionType.WEBHOOK, config = """{"url":"http://example.com"}""") + whenever(service.updateAction(eq(42L), any())).thenReturn(action.copy(id = 42L)) + mockMvc.perform( + put("/api/workspaces/1/actions/42") + .contentType(MediaType.APPLICATION_JSON) + .content(mapper.writeValueAsString(action)) + ).andExpect(status().isOk) + verify(service).updateAction(eq(42L), any()) + } } diff --git a/backend/src/test/kotlin/com/opendatamask/service/PostJobActionServiceTest.kt b/backend/src/test/kotlin/com/opendatamask/service/PostJobActionServiceTest.kt index 1a290cb..ef931b7 100644 --- a/backend/src/test/kotlin/com/opendatamask/service/PostJobActionServiceTest.kt +++ b/backend/src/test/kotlin/com/opendatamask/service/PostJobActionServiceTest.kt @@ -68,4 +68,26 @@ class PostJobActionServiceTest { service.deleteAction(42L) verify(repository).deleteById(42L) } + + @Test + fun `updateAction updates fields and saves`() { + val existing = PostJobAction( + id = 1L, workspaceId = 1L, + actionType = ActionType.EMAIL, + config = """{"to":"old@example.com"}""" + ) + val updated = PostJobAction( + id = 1L, workspaceId = 1L, + actionType = ActionType.WEBHOOK, + config = """{"url":"http://new.example.com"}""", + enabled = false + ) + whenever(repository.findById(1L)).thenReturn(java.util.Optional.of(existing)) + whenever(repository.save(any())).thenReturn(existing) + service.updateAction(1L, updated) + verify(repository).save(existing) + assertEquals(ActionType.WEBHOOK, existing.actionType) + assertEquals("""{"url":"http://new.example.com"}""", existing.config) + assertEquals(false, existing.enabled) + } } diff --git a/frontend/src/api/actions.ts b/frontend/src/api/actions.ts new file mode 100644 index 0000000..bc42c43 --- /dev/null +++ b/frontend/src/api/actions.ts @@ -0,0 +1,34 @@ +import apiClient from './client' +import type { PostJobAction, PostJobActionRequest } from '@/types' + +export async function listActions(workspaceId: number): Promise { + const { data } = await apiClient.get(`/workspaces/${workspaceId}/actions`) + return data +} + +export async function createAction( + workspaceId: number, + payload: PostJobActionRequest +): Promise { + const { data } = await apiClient.post( + `/workspaces/${workspaceId}/actions`, + payload + ) + return data +} + +export async function updateAction( + workspaceId: number, + actionId: number, + payload: PostJobActionRequest +): Promise { + const { data } = await apiClient.put( + `/workspaces/${workspaceId}/actions/${actionId}`, + payload + ) + return data +} + +export async function deleteAction(workspaceId: number, actionId: number): Promise { + await apiClient.delete(`/workspaces/${workspaceId}/actions/${actionId}`) +} diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index ca8139f..9310537 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -8,6 +8,7 @@ import WorkspaceDetailView from '@/views/WorkspaceDetailView.vue' import ConnectionsView from '@/views/ConnectionsView.vue' import TablesView from '@/views/TablesView.vue' import JobsView from '@/views/JobsView.vue' +import ActionsView from '@/views/ActionsView.vue' const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), @@ -60,6 +61,12 @@ const router = createRouter({ name: 'jobs', component: JobsView, meta: { requiresAuth: true } + }, + { + path: '/workspaces/:id/actions', + name: 'actions', + component: ActionsView, + meta: { requiresAuth: true } } ] }) diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index eacf1d6..6248537 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -204,6 +204,29 @@ export interface JobLog { timestamp: string } +// ── Post-Job Action ─────────────────────────────────────────────────────── + +export enum ActionType { + WEBHOOK = 'WEBHOOK', + EMAIL = 'EMAIL', + SCRIPT = 'SCRIPT' +} + +export interface PostJobAction { + id: number + workspaceId: number + actionType: ActionType + config: string + enabled: boolean + createdAt: string +} + +export interface PostJobActionRequest { + actionType: ActionType + config: string + enabled?: boolean +} + // ── Pagination ──────────────────────────────────────────────────────────── export interface Page { diff --git a/frontend/src/views/ActionsView.vue b/frontend/src/views/ActionsView.vue new file mode 100644 index 0000000..a95dd83 --- /dev/null +++ b/frontend/src/views/ActionsView.vue @@ -0,0 +1,285 @@ + + +