feat: Post-Job Actions — PUT endpoint + full frontend CRUD#23
feat: Post-Job Actions — PUT endpoint + full frontend CRUD#23MaximumTrainer merged 4 commits intomainfrom
Conversation
Agent-Logs-Url: https://github.com/MaximumTrainer/OpenDataMask/sessions/57b16640-b697-457b-9948-ba50ad6b410a Co-authored-by: MaximumTrainer <1376575+MaximumTrainer@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Adds full “Post-Job Actions” CRUD support across backend and frontend, completing the previously partial implementation by introducing an update (PUT) endpoint and a dedicated UI for managing actions per workspace.
Changes:
- Backend: added
updateActionservice method andPUT /api/workspaces/{workspaceId}/actions/{actionId}controller route. - Frontend: introduced
ActionType+ post-job action types, an actions API client, anActionsViewCRUD page, and workspace routing/navigation updates. - Tests: added/updated unit tests for the new backend update flow and a small frontend enum-shape test.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| frontend/src/views/WorkspaceDetailView.vue | Adds an “Actions” tab and quick action entry to navigate to post-job actions. |
| frontend/src/views/ActionsView.vue | New CRUD UI for listing/creating/editing/enabling/deleting post-job actions. |
| frontend/src/views/tests/ActionsView.test.ts | Adds enum-shape coverage for ActionType. |
| frontend/src/types/index.ts | Introduces ActionType, PostJobAction, and request type definitions. |
| frontend/src/router/index.ts | Adds /workspaces/:id/actions route. |
| frontend/src/api/actions.ts | New API client wrapper for post-job action CRUD endpoints. |
| backend/src/test/kotlin/com/opendatamask/service/PostJobActionServiceTest.kt | Adds service-level test for updating actions. |
| backend/src/test/kotlin/com/opendatamask/controller/PostJobActionControllerTest.kt | Adds controller-level test for PUT update endpoint. |
| backend/src/main/kotlin/com/opendatamask/service/PostJobActionService.kt | Adds updateAction implementation. |
| backend/src/main/kotlin/com/opendatamask/controller/PostJobActionController.kt | Adds PUT endpoint for updating an action. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| @PutMapping("/{actionId}") | ||
| fun updateAction( | ||
| @PathVariable workspaceId: Long, | ||
| @PathVariable actionId: Long, | ||
| @RequestBody action: PostJobAction | ||
| ): ResponseEntity<PostJobAction> = | ||
| ResponseEntity.ok(service.updateAction(actionId, action.copy(workspaceId = workspaceId))) |
There was a problem hiding this comment.
@RequestBody action: PostJobAction requires workspaceId (non-null, no default in PostJobAction), but the frontend sends only { actionType, config, enabled }. This will cause PUT requests to fail deserialization with 400 before action.copy(workspaceId = workspaceId) runs. Introduce a dedicated request DTO (e.g., PostJobActionRequest) without id/workspaceId/createdAt, and map it to the entity using the workspaceId and actionId path params (similar to DataConnectionController).
| 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) | ||
| } |
There was a problem hiding this comment.
updateAction looks up actions by ID only and does not validate that the record belongs to the workspaceId from the URL (the controller currently passes it via updated, but it’s ignored). This allows cross-workspace modification if an action ID is known. Pass workspaceId into updateAction and verify existing.workspaceId == workspaceId (or query by both fields) before mutating/saving.
frontend/src/views/ActionsView.vue
Outdated
| formError.value = '' | ||
| try { | ||
| const parsed = JSON.parse(form.value.config) | ||
| if (typeof parsed !== 'object' || parsed === null) throw new Error() |
There was a problem hiding this comment.
The JSON validation accepts arrays because typeof [] === 'object'. If the intent is “JSON object”, reject arrays explicitly (e.g., Array.isArray(parsed)), otherwise users can save configs like [] while the UI/error message claims only objects are allowed.
| if (typeof parsed !== 'object' || parsed === null) throw new Error() | |
| if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) throw new Error() |
| val action = PostJobAction(workspaceId = 1L, actionType = ActionType.WEBHOOK, config = """{"url":"http://example.com"}""") | ||
| whenever(service.updateAction(eq(42L), any<PostJobAction>())).thenReturn(action.copy(id = 42L)) | ||
| mockMvc.perform( | ||
| put("/api/workspaces/1/actions/42") | ||
| .contentType(MediaType.APPLICATION_JSON) | ||
| .content(mapper.writeValueAsString(action)) | ||
| ).andExpect(status().isOk) |
There was a problem hiding this comment.
This test constructs and sends a full PostJobAction (including workspaceId) in the PUT body, which masks the current API/body mismatch with the frontend payload (which omits workspaceId). Once the controller is switched to a request DTO, update this test to send the actual PUT request shape and assert the response body fields (not just 200).
| val action = PostJobAction(workspaceId = 1L, actionType = ActionType.WEBHOOK, config = """{"url":"http://example.com"}""") | |
| whenever(service.updateAction(eq(42L), any<PostJobAction>())).thenReturn(action.copy(id = 42L)) | |
| mockMvc.perform( | |
| put("/api/workspaces/1/actions/42") | |
| .contentType(MediaType.APPLICATION_JSON) | |
| .content(mapper.writeValueAsString(action)) | |
| ).andExpect(status().isOk) | |
| val action = PostJobAction( | |
| workspaceId = 1L, | |
| actionType = ActionType.WEBHOOK, | |
| config = """{"url":"http://example.com"}""" | |
| ) | |
| val requestBody = mapOf( | |
| "actionType" to action.actionType.name, | |
| "config" to action.config | |
| ) | |
| whenever(service.updateAction(eq(42L), any<PostJobAction>())).thenReturn(action.copy(id = 42L)) | |
| mockMvc.perform( | |
| put("/api/workspaces/1/actions/42") | |
| .contentType(MediaType.APPLICATION_JSON) | |
| .content(mapper.writeValueAsString(requestBody)) | |
| ) | |
| .andExpect(status().isOk) | |
| .andExpect(jsonPath("$.id").value(42)) | |
| .andExpect(jsonPath("$.workspaceId").value(1)) | |
| .andExpect(jsonPath("$.actionType").value("WEBHOOK")) | |
| .andExpect(jsonPath("$.config").value("""{"url":"http://example.com"}""")) |
|
@copilot apply changes based on the comments in this thread |
…rray rejection in config validation Agent-Logs-Url: https://github.com/MaximumTrainer/OpenDataMask/sessions/d2b2fdb4-26f5-47f7-b1d2-085697bf7fed Co-authored-by: MaximumTrainer <1376575+MaximumTrainer@users.noreply.github.com>
Applied all four changes in commit
Warning Firewall rules blocked me from connecting to one or more addresses (expand for details)I tried to connect to the following addresses, but was blocked by firewall rules:
If you need me to access, download, or install something from one of these locations, you can either:
|
|
@copilot fix vue build error during build: |
Agent-Logs-Url: https://github.com/MaximumTrainer/OpenDataMask/sessions/80938ceb-3097-4b4f-83b7-80b77dfb2470 Co-authored-by: MaximumTrainer <1376575+MaximumTrainer@users.noreply.github.com>
Fixed in commit Changes:
Warning Firewall rules blocked me from connecting to one or more addresses (expand for details)I tried to connect to the following addresses, but was blocked by firewall rules:
If you need me to access, download, or install something from one of these locations, you can either:
|
updateActiontoPostJobActionService+PUT /{actionId}endpoint toPostJobActionControllerPostJobActionRequestDTO — controller POST/PUT now accept{ actionType, config, enabled }(noid/workspaceId/createdAtrequired in body)updateActionvalidates workspace ownership before mutating; throws if action belongs to a different workspacePostJobActionServiceTest—updateActiontest uses new signature + new test for cross-workspace rejectionPostJobActionControllerTest— PUT test sends DTO body (noworkspaceId) and asserts response fields ($.id,$.workspaceId,$.actionType,$.config)ActionTypeenum andPostJobAction/PostJobActionRequesttypes totypes/index.tsapi/actions.tswith CRUD API functionsviews/ActionsView.vuewith JSON config validation that rejects arrays (Array.isArraycheck)/workspaces/:id/actionstorouter/index.tsWorkspaceDetailView.vueviews/__tests__/ActionsView.test.tswithActionTypeenum coverage testsModal.vuerenamed toAppModal.vuein main — addedAppModal.vueto branch and updatedActionsView.vueimport/template accordingly