Skip to content

feat: Post-Job Actions — PUT endpoint + full frontend CRUD#23

Merged
MaximumTrainer merged 4 commits intomainfrom
copilot/add-post-job-action-service
Apr 1, 2026
Merged

feat: Post-Job Actions — PUT endpoint + full frontend CRUD#23
MaximumTrainer merged 4 commits intomainfrom
copilot/add-post-job-action-service

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 1, 2026

  • Explore repo structure and understand existing code
  • Backend: Add updateAction to PostJobActionService + PUT /{actionId} endpoint to PostJobActionController
  • Backend: Introduce PostJobActionRequest DTO — controller POST/PUT now accept { actionType, config, enabled } (no id/workspaceId/createdAt required in body)
  • Backend: updateAction validates workspace ownership before mutating; throws if action belongs to a different workspace
  • Backend: Update PostJobActionServiceTestupdateAction test uses new signature + new test for cross-workspace rejection
  • Backend: Update PostJobActionControllerTest — PUT test sends DTO body (no workspaceId) and asserts response fields ($.id, $.workspaceId, $.actionType, $.config)
  • Frontend: Add ActionType enum and PostJobAction/PostJobActionRequest types to types/index.ts
  • Frontend: Create api/actions.ts with CRUD API functions
  • Frontend: Create views/ActionsView.vue with JSON config validation that rejects arrays (Array.isArray check)
  • Frontend: Add route /workspaces/:id/actions to router/index.ts
  • Frontend: Add "⚡ Actions" tab to WorkspaceDetailView.vue
  • Frontend: Add views/__tests__/ActionsView.test.ts with ActionType enum coverage tests
  • Fix CI build failure: Modal.vue renamed to AppModal.vue in main — added AppModal.vue to branch and updated ActionsView.vue import/template accordingly
  • All backend and frontend tests pass

Copilot AI changed the title [WIP] Add PostJobActionService, controller, and execution trigger feat: Post-Job Actions — PUT endpoint + full frontend CRUD Apr 1, 2026
Copilot AI requested a review from MaximumTrainer April 1, 2026 16:47
@MaximumTrainer MaximumTrainer requested review from Copilot and removed request for MaximumTrainer April 1, 2026 17:31
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 updateAction service method and PUT /api/workspaces/{workspaceId}/actions/{actionId} controller route.
  • Frontend: introduced ActionType + post-job action types, an actions API client, an ActionsView CRUD 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.

Comment on lines +27 to +33
@PutMapping("/{actionId}")
fun updateAction(
@PathVariable workspaceId: Long,
@PathVariable actionId: Long,
@RequestBody action: PostJobAction
): ResponseEntity<PostJobAction> =
ResponseEntity.ok(service.updateAction(actionId, action.copy(workspaceId = workspaceId)))
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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).

Copilot uses AI. Check for mistakes.
Comment on lines +106 to +113
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)
}
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
formError.value = ''
try {
const parsed = JSON.parse(form.value.config)
if (typeof parsed !== 'object' || parsed === null) throw new Error()
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
if (typeof parsed !== 'object' || parsed === null) throw new Error()
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) throw new Error()

Copilot uses AI. Check for mistakes.
Comment on lines +72 to +78
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)
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
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 uses AI. Check for mistakes.
@MaximumTrainer
Copy link
Copy Markdown
Owner

@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>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 1, 2026

@copilot apply changes based on the comments in this thread

Applied all four changes in commit c6c7bba:

  1. PostJobActionRequest DTO (PostJobActionDto.kt) — controller POST and PUT now accept { actionType, config, enabled } without requiring id/workspaceId/createdAt in the request body, fixing the deserialization issue.

  2. Workspace ownership validationupdateAction signature updated to (workspaceId, id, request) and checks existing.workspaceId == workspaceId before mutating; throws NoSuchElementException on mismatch (verified by a new test updateAction throws when action belongs to different workspace).

  3. Array rejection — frontend config validation now uses Array.isArray(parsed) to reject arrays alongside the null/non-object check.

  4. Controller test — PUT test sends a PostJobActionRequest body (no workspaceId) and asserts $.id, $.workspaceId, $.actionType, $.config in the response.

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:

  • example.com
    • Triggering command: /usr/lib/jvm/temurin-17-jdk-amd64/bin/java /usr/lib/jvm/temurin-17-jdk-amd64/bin/java -Dorg.gradle.internal.worker.tmpdir=/home/REDACTED/work/OpenDataMask/OpenDataMask/backend/build/tmp/test/work -Dorg.gradle.native=false -javaagent:/home/REDACTED/work/OpenDataMask/OpenDataMask/backend/build/tmp/.cache/expanded/zip_0bd0851504e6286cb5fc6f37a81e25cc/jacocoagent.jar=destfile=build/jacoco/test.exec,append=true,inclnolocationclasses=false,dumponexit=true,output=file,jmx=false @/home/REDACTED/.gradle/.tmp/gradle-worker-classpath7813893750556571075txt -Xmx512m -Dfile.encoding=UTF-8 -Duser.country -Duser.language=en -Duser.variant -ea worker.org.gradle.process.internal.worker.GradleWorkerMain &#39;Gradle Test Executor 1&#39; (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

Copilot AI requested a review from MaximumTrainer April 1, 2026 18:20
@MaximumTrainer MaximumTrainer marked this pull request as ready for review April 1, 2026 18:29
@MaximumTrainer
Copy link
Copy Markdown
Owner

@copilot fix vue build error during build:
[vite:load-fallback] Could not load /home/runner/work/OpenDataMask/OpenDataMask/frontend/src/components/Modal.vue (imported by src/views/ActionsView.vue?vue&type=script&setup=true&lang.ts): ENOENT: no such file or directory, open '/home/runner/work/OpenDataMask/OpenDataMask/frontend/src/components/Modal.vue'

Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 1, 2026

@copilot fix vue build error during build:
[vite:load-fallback] Could not load /home/runner/work/OpenDataMask/OpenDataMask/frontend/src/components/Modal.vue (imported by src/views/ActionsView.vue?vue...

Fixed in commit b4fd81a. The root cause was that main renamed Modal.vueAppModal.vue after our PR was opened. The CI build runs against the PR merge commit (our branch merged into current main), where git drops Modal.vue (deleted in main, unmodified in our branch) but AppModal.vue isn't included from our side.

Changes:

  • Added frontend/src/components/AppModal.vue to the branch (identical content, correct name)
  • Updated ActionsView.vue to import AppModal from '@/components/AppModal.vue' and replaced <Modal><AppModal> in the template

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:

  • https://api.github.com/repos/MaximumTrainer/OpenDataMask/actions/runs/23863992643/attempts/1
    • Triggering command: /usr/bin/curl curl -s REDACTED 53 -j ACCEPT (http block)

If you need me to access, download, or install something from one of these locations, you can either:

@MaximumTrainer MaximumTrainer merged commit 0b827e8 into main Apr 1, 2026
8 checks passed
@MaximumTrainer MaximumTrainer deleted the copilot/add-post-job-action-service branch April 1, 2026 18:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Post-Job Actions — Service, Controller & Execution

3 participants