From 6b6ce059fe9ced21e7f872801241c6b11e43e7f5 Mon Sep 17 00:00:00 2001 From: Assem Hafez Date: Wed, 14 May 2025 13:12:14 +0000 Subject: [PATCH 01/17] Signal workflow API --- .../[runId]/(actions)/signal/route.ts | 18 +++ .../__tests__/signal-workflow.node.ts | 127 ++++++++++++++++++ .../schemas/signal-workflow-input-schema.ts | 15 +++ .../signal-workflow-request-body-schema.ts | 9 ++ .../signal-workflow/signal-workflow.ts | 58 ++++++++ .../signal-workflow/signal-workflow.types.ts | 14 ++ 6 files changed, 241 insertions(+) create mode 100644 src/app/api/domains/[domain]/[cluster]/workflows/[workflowId]/[runId]/(actions)/signal/route.ts create mode 100644 src/route-handlers/signal-workflow/__tests__/signal-workflow.node.ts create mode 100644 src/route-handlers/signal-workflow/schemas/signal-workflow-input-schema.ts create mode 100644 src/route-handlers/signal-workflow/schemas/signal-workflow-request-body-schema.ts create mode 100644 src/route-handlers/signal-workflow/signal-workflow.ts create mode 100644 src/route-handlers/signal-workflow/signal-workflow.types.ts diff --git a/src/app/api/domains/[domain]/[cluster]/workflows/[workflowId]/[runId]/(actions)/signal/route.ts b/src/app/api/domains/[domain]/[cluster]/workflows/[workflowId]/[runId]/(actions)/signal/route.ts new file mode 100644 index 000000000..4fe2d1f5c --- /dev/null +++ b/src/app/api/domains/[domain]/[cluster]/workflows/[workflowId]/[runId]/(actions)/signal/route.ts @@ -0,0 +1,18 @@ +import { type NextRequest } from 'next/server'; + +import { signalWorkflow } from '@/route-handlers/signal-workflow/signal-workflow'; +import { type RequestParams } from '@/route-handlers/signal-workflow/signal-workflow.types'; +import { routeHandlerWithMiddlewares } from '@/utils/route-handlers-middleware'; +import routeHandlersDefaultMiddlewares from '@/utils/route-handlers-middleware/config/route-handlers-default-middlewares.config'; + +export async function POST( + request: NextRequest, + options: { params: RequestParams['params'] } +) { + return routeHandlerWithMiddlewares( + signalWorkflow, + request, + options, + routeHandlersDefaultMiddlewares + ); +} \ No newline at end of file diff --git a/src/route-handlers/signal-workflow/__tests__/signal-workflow.node.ts b/src/route-handlers/signal-workflow/__tests__/signal-workflow.node.ts new file mode 100644 index 000000000..90ebb8456 --- /dev/null +++ b/src/route-handlers/signal-workflow/__tests__/signal-workflow.node.ts @@ -0,0 +1,127 @@ +import { NextRequest } from 'next/server'; + +import { GRPCError } from '@/utils/grpc/grpc-error'; +import { mockGrpcClusterMethods } from '@/utils/route-handlers-middleware/middlewares/__mocks__/grpc-cluster-methods'; + +import { signalWorkflow } from '../signal-workflow'; +import { type Context } from '../signal-workflow.types'; +import { SignalWorkflowSubmissionData } from '@/views/workflow-actions/workflow-action-signal-form/workflow-action-signal-form.types'; + +const defaultRequestBody = { + signalName: 'test-signal', + signalInput: { data: 'test-input' }, +}; + +describe(signalWorkflow.name, () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('calls signalWorkflow and returns valid response', async () => { + const { res, mockSignalWorkflow } = await setup({ + requestBody: JSON.stringify({ + signalName: 'custom-signal', + signalInput: 'custom-input', + }), + }); + + expect(mockSignalWorkflow).toHaveBeenCalledWith({ + domain: 'mock-domain', + workflowExecution: { + workflowId: 'mock-wfid', + runId: 'mock-runid', + }, + signalName: 'test-signal', + signalInput: { data: Buffer.from('test-input') }, + }); + + const responseJson = await res.json(); + expect(responseJson).toEqual({}); + }); + + it('calls signalWorkflow without signalInput when not provided', async () => { + const { mockSignalWorkflow } = await setup({ + requestBody: JSON.stringify({ + signalName: 'signal-without-input', + }), + }); + + expect(mockSignalWorkflow).toHaveBeenCalledWith( + expect.objectContaining({ + signalName: 'signal-without-input', + signalInput: undefined, + }) + ); + }); + + it('returns an error if something went wrong in the backend', async () => { + const { res, mockSignalWorkflow } = await setup({ + error: true, + }); + + expect(mockSignalWorkflow).toHaveBeenCalled(); + + expect(res.status).toEqual(500); + const responseJson = await res.json(); + expect(responseJson).toEqual( + expect.objectContaining({ + message: 'Could not signal workflow', + }) + ); + }); + + it('returns an error if the signal input has an unexpected format', async () => { + const { res, mockSignalWorkflow } = await setup({ + requestBody: JSON.stringify({ + signalName: 'test-signal', + signalInput: 'not-an-object', // should be an object + } satisfies SignalWorkflowSubmissionData), + }); + + expect(mockSignalWorkflow).not.toHaveBeenCalled(); + + const responseJson = await res.json(); + expect(responseJson).toEqual( + expect.objectContaining({ + message: 'Invalid values provided for workflow signal', + }) + ); + }); +}); + +async function setup({ + requestBody = JSON.stringify(defaultRequestBody), + error, +}: { + requestBody?: string; + error?: true; +}) { + const mockSignalWorkflow = jest + .spyOn(mockGrpcClusterMethods, 'signalWorkflow') + .mockImplementationOnce(async () => { + if (error) { + throw new GRPCError('Could not signal workflow'); + } + return {}; + }); + + const res = await signalWorkflow( + new NextRequest('http://localhost', { + method: 'POST', + body: requestBody ?? '{}', + }), + { + params: { + domain: 'mock-domain', + cluster: 'mock-cluster', + workflowId: 'mock-wfid', + runId: 'mock-runid', + }, + }, + { + grpcClusterMethods: mockGrpcClusterMethods, + } as Context + ); + + return { res, mockSignalWorkflow }; +} \ No newline at end of file diff --git a/src/route-handlers/signal-workflow/schemas/signal-workflow-input-schema.ts b/src/route-handlers/signal-workflow/schemas/signal-workflow-input-schema.ts new file mode 100644 index 000000000..9c0629714 --- /dev/null +++ b/src/route-handlers/signal-workflow/schemas/signal-workflow-input-schema.ts @@ -0,0 +1,15 @@ +import losslessJsonParse from "@/utils/lossless-json-parse"; +import { z } from "zod"; + +const signalWorkflowInputSchema = z.string().superRefine((str, ctx) => { + if (!str) return undefined; + + try { + return losslessJsonParse(str); + } catch { + ctx.addIssue({ code: 'custom', message: 'Invalid JSON' }); + return z.NEVER; + } +}); + +export default signalWorkflowInputSchema; diff --git a/src/route-handlers/signal-workflow/schemas/signal-workflow-request-body-schema.ts b/src/route-handlers/signal-workflow/schemas/signal-workflow-request-body-schema.ts new file mode 100644 index 000000000..0f56f404b --- /dev/null +++ b/src/route-handlers/signal-workflow/schemas/signal-workflow-request-body-schema.ts @@ -0,0 +1,9 @@ +import { z } from 'zod'; +import signalWorkflowInputSchema from './signal-workflow-input-schema'; + +const signalWorkflowRequestBodySchema = z.object({ + signalName: z.string().min(1), + signalInput: signalWorkflowInputSchema.optional(), +}); + +export default signalWorkflowRequestBodySchema; \ No newline at end of file diff --git a/src/route-handlers/signal-workflow/signal-workflow.ts b/src/route-handlers/signal-workflow/signal-workflow.ts new file mode 100644 index 000000000..664ad445b --- /dev/null +++ b/src/route-handlers/signal-workflow/signal-workflow.ts @@ -0,0 +1,58 @@ +import { type NextRequest, NextResponse } from 'next/server'; + +import decodeUrlParams from '@/utils/decode-url-params'; +import { getHTTPStatusCode, GRPCError } from '@/utils/grpc/grpc-error'; +import logger, { type RouteHandlerErrorPayload } from '@/utils/logger'; + +import { type Context, type RequestParams } from './signal-workflow.types'; +import signalWorkflowRequestBodySchema from './schemas/signal-workflow-request-body-schema'; + +export async function signalWorkflow( + request: NextRequest, + requestParams: RequestParams, + ctx: Context +) { + const requestBody = await request.json(); + const { data, error } = signalWorkflowRequestBodySchema.safeParse(requestBody); + + if (error) { + return NextResponse.json( + { + message: 'Invalid values provided for workflow signal', + validationErrors: error.errors, + }, + { status: 400 } + ); + } + + const decodedParams = decodeUrlParams(requestParams.params); + + try { + const response = await ctx.grpcClusterMethods.signalWorkflow({ + domain: decodedParams.domain, + workflowExecution: { + workflowId: decodedParams.workflowId, + runId: decodedParams.runId, + }, + signalName: data.signalName, + signalInput: data.signalInput ? { data: Buffer.from(data.signalInput) } : undefined, + // TODO: add user identity + }); + + return NextResponse.json(response); + } catch (e) { + logger.error( + { requestParams: decodedParams, error: e }, + 'Error signaling workflow' + ); + + return NextResponse.json( + { + message: + e instanceof GRPCError ? e.message : 'Error signaling workflow', + cause: e, + }, + { status: getHTTPStatusCode(e) } + ); + } +} \ No newline at end of file diff --git a/src/route-handlers/signal-workflow/signal-workflow.types.ts b/src/route-handlers/signal-workflow/signal-workflow.types.ts new file mode 100644 index 000000000..51e58e258 --- /dev/null +++ b/src/route-handlers/signal-workflow/signal-workflow.types.ts @@ -0,0 +1,14 @@ +import { type GRPCClusterMethods } from '@/utils/grpc/grpc-client'; + +export type RequestParams = { + params: { + domain: string; + cluster: string; + workflowId: string; + runId: string; + }; +}; + +export type Context = { + grpcClusterMethods: GRPCClusterMethods; +}; \ No newline at end of file From 509ae9d28b528540b69d058850e7019678a3595e Mon Sep 17 00:00:00 2001 From: Assem Hafez Date: Wed, 14 May 2025 13:18:42 +0000 Subject: [PATCH 02/17] fix node test --- .../signal-workflow/__tests__/signal-workflow.node.ts | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/route-handlers/signal-workflow/__tests__/signal-workflow.node.ts b/src/route-handlers/signal-workflow/__tests__/signal-workflow.node.ts index 90ebb8456..d44df94cf 100644 --- a/src/route-handlers/signal-workflow/__tests__/signal-workflow.node.ts +++ b/src/route-handlers/signal-workflow/__tests__/signal-workflow.node.ts @@ -9,8 +9,8 @@ import { SignalWorkflowSubmissionData } from '@/views/workflow-actions/workflow- const defaultRequestBody = { signalName: 'test-signal', - signalInput: { data: 'test-input' }, -}; + signalInput: 'test-input' , +} satisfies SignalWorkflowSubmissionData; describe(signalWorkflow.name, () => { beforeEach(() => { @@ -18,12 +18,7 @@ describe(signalWorkflow.name, () => { }); it('calls signalWorkflow and returns valid response', async () => { - const { res, mockSignalWorkflow } = await setup({ - requestBody: JSON.stringify({ - signalName: 'custom-signal', - signalInput: 'custom-input', - }), - }); + const { res, mockSignalWorkflow } = await setup({}); expect(mockSignalWorkflow).toHaveBeenCalledWith({ domain: 'mock-domain', From 06edf6c7d26b7632673bdaccf47eda371ab93ecf Mon Sep 17 00:00:00 2001 From: Assem Hafez Date: Wed, 14 May 2025 13:24:13 +0000 Subject: [PATCH 03/17] fix lint --- .../signal-workflow/__tests__/signal-workflow.node.ts | 6 +++--- .../schemas/signal-workflow-input-schema.ts | 5 +++-- .../schemas/signal-workflow-request-body-schema.ts | 3 ++- src/route-handlers/signal-workflow/signal-workflow.ts | 11 +++++++---- .../signal-workflow/signal-workflow.types.ts | 2 +- 5 files changed, 16 insertions(+), 11 deletions(-) diff --git a/src/route-handlers/signal-workflow/__tests__/signal-workflow.node.ts b/src/route-handlers/signal-workflow/__tests__/signal-workflow.node.ts index d44df94cf..8fb3d6bdc 100644 --- a/src/route-handlers/signal-workflow/__tests__/signal-workflow.node.ts +++ b/src/route-handlers/signal-workflow/__tests__/signal-workflow.node.ts @@ -2,14 +2,14 @@ import { NextRequest } from 'next/server'; import { GRPCError } from '@/utils/grpc/grpc-error'; import { mockGrpcClusterMethods } from '@/utils/route-handlers-middleware/middlewares/__mocks__/grpc-cluster-methods'; +import { type SignalWorkflowSubmissionData } from '@/views/workflow-actions/workflow-action-signal-form/workflow-action-signal-form.types'; import { signalWorkflow } from '../signal-workflow'; import { type Context } from '../signal-workflow.types'; -import { SignalWorkflowSubmissionData } from '@/views/workflow-actions/workflow-action-signal-form/workflow-action-signal-form.types'; const defaultRequestBody = { signalName: 'test-signal', - signalInput: 'test-input' , + signalInput: 'test-input', } satisfies SignalWorkflowSubmissionData; describe(signalWorkflow.name, () => { @@ -119,4 +119,4 @@ async function setup({ ); return { res, mockSignalWorkflow }; -} \ No newline at end of file +} diff --git a/src/route-handlers/signal-workflow/schemas/signal-workflow-input-schema.ts b/src/route-handlers/signal-workflow/schemas/signal-workflow-input-schema.ts index 9c0629714..dd7b5bdce 100644 --- a/src/route-handlers/signal-workflow/schemas/signal-workflow-input-schema.ts +++ b/src/route-handlers/signal-workflow/schemas/signal-workflow-input-schema.ts @@ -1,5 +1,6 @@ -import losslessJsonParse from "@/utils/lossless-json-parse"; -import { z } from "zod"; +import { z } from 'zod'; + +import losslessJsonParse from '@/utils/lossless-json-parse'; const signalWorkflowInputSchema = z.string().superRefine((str, ctx) => { if (!str) return undefined; diff --git a/src/route-handlers/signal-workflow/schemas/signal-workflow-request-body-schema.ts b/src/route-handlers/signal-workflow/schemas/signal-workflow-request-body-schema.ts index 0f56f404b..35618dc93 100644 --- a/src/route-handlers/signal-workflow/schemas/signal-workflow-request-body-schema.ts +++ b/src/route-handlers/signal-workflow/schemas/signal-workflow-request-body-schema.ts @@ -1,4 +1,5 @@ import { z } from 'zod'; + import signalWorkflowInputSchema from './signal-workflow-input-schema'; const signalWorkflowRequestBodySchema = z.object({ @@ -6,4 +7,4 @@ const signalWorkflowRequestBodySchema = z.object({ signalInput: signalWorkflowInputSchema.optional(), }); -export default signalWorkflowRequestBodySchema; \ No newline at end of file +export default signalWorkflowRequestBodySchema; diff --git a/src/route-handlers/signal-workflow/signal-workflow.ts b/src/route-handlers/signal-workflow/signal-workflow.ts index 664ad445b..68e1d172e 100644 --- a/src/route-handlers/signal-workflow/signal-workflow.ts +++ b/src/route-handlers/signal-workflow/signal-workflow.ts @@ -4,8 +4,8 @@ import decodeUrlParams from '@/utils/decode-url-params'; import { getHTTPStatusCode, GRPCError } from '@/utils/grpc/grpc-error'; import logger, { type RouteHandlerErrorPayload } from '@/utils/logger'; -import { type Context, type RequestParams } from './signal-workflow.types'; import signalWorkflowRequestBodySchema from './schemas/signal-workflow-request-body-schema'; +import { type Context, type RequestParams } from './signal-workflow.types'; export async function signalWorkflow( request: NextRequest, @@ -13,7 +13,8 @@ export async function signalWorkflow( ctx: Context ) { const requestBody = await request.json(); - const { data, error } = signalWorkflowRequestBodySchema.safeParse(requestBody); + const { data, error } = + signalWorkflowRequestBodySchema.safeParse(requestBody); if (error) { return NextResponse.json( @@ -35,7 +36,9 @@ export async function signalWorkflow( runId: decodedParams.runId, }, signalName: data.signalName, - signalInput: data.signalInput ? { data: Buffer.from(data.signalInput) } : undefined, + signalInput: data.signalInput + ? { data: Buffer.from(data.signalInput) } + : undefined, // TODO: add user identity }); @@ -55,4 +58,4 @@ export async function signalWorkflow( { status: getHTTPStatusCode(e) } ); } -} \ No newline at end of file +} diff --git a/src/route-handlers/signal-workflow/signal-workflow.types.ts b/src/route-handlers/signal-workflow/signal-workflow.types.ts index 51e58e258..9b2ed29b6 100644 --- a/src/route-handlers/signal-workflow/signal-workflow.types.ts +++ b/src/route-handlers/signal-workflow/signal-workflow.types.ts @@ -11,4 +11,4 @@ export type RequestParams = { export type Context = { grpcClusterMethods: GRPCClusterMethods; -}; \ No newline at end of file +}; From ceec9e6c4fe5344cb118bd7171e6bec2e2d8dcf0 Mon Sep 17 00:00:00 2001 From: Assem Hafez Date: Wed, 14 May 2025 14:01:42 +0000 Subject: [PATCH 04/17] add new line --- .../workflows/[workflowId]/[runId]/(actions)/signal/route.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/api/domains/[domain]/[cluster]/workflows/[workflowId]/[runId]/(actions)/signal/route.ts b/src/app/api/domains/[domain]/[cluster]/workflows/[workflowId]/[runId]/(actions)/signal/route.ts index 4fe2d1f5c..4c6ae710e 100644 --- a/src/app/api/domains/[domain]/[cluster]/workflows/[workflowId]/[runId]/(actions)/signal/route.ts +++ b/src/app/api/domains/[domain]/[cluster]/workflows/[workflowId]/[runId]/(actions)/signal/route.ts @@ -15,4 +15,4 @@ export async function POST( options, routeHandlersDefaultMiddlewares ); -} \ No newline at end of file +} From b08d6b0038a556aafe9f4abed490e6013ccd3f13 Mon Sep 17 00:00:00 2001 From: Assem Hafez Date: Wed, 14 May 2025 14:07:28 +0000 Subject: [PATCH 05/17] add type for SignalWorkflowRequestBody --- .../signal-workflow/__tests__/signal-workflow.node.ts | 8 +++++--- .../signal-workflow/signal-workflow.types.ts | 8 ++++++++ 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/route-handlers/signal-workflow/__tests__/signal-workflow.node.ts b/src/route-handlers/signal-workflow/__tests__/signal-workflow.node.ts index 8fb3d6bdc..4377d63c7 100644 --- a/src/route-handlers/signal-workflow/__tests__/signal-workflow.node.ts +++ b/src/route-handlers/signal-workflow/__tests__/signal-workflow.node.ts @@ -2,15 +2,17 @@ import { NextRequest } from 'next/server'; import { GRPCError } from '@/utils/grpc/grpc-error'; import { mockGrpcClusterMethods } from '@/utils/route-handlers-middleware/middlewares/__mocks__/grpc-cluster-methods'; -import { type SignalWorkflowSubmissionData } from '@/views/workflow-actions/workflow-action-signal-form/workflow-action-signal-form.types'; import { signalWorkflow } from '../signal-workflow'; -import { type Context } from '../signal-workflow.types'; +import { + type SignalWorkflowRequestBody, + type Context, +} from '../signal-workflow.types'; const defaultRequestBody = { signalName: 'test-signal', signalInput: 'test-input', -} satisfies SignalWorkflowSubmissionData; +} satisfies SignalWorkflowRequestBody; describe(signalWorkflow.name, () => { beforeEach(() => { diff --git a/src/route-handlers/signal-workflow/signal-workflow.types.ts b/src/route-handlers/signal-workflow/signal-workflow.types.ts index 9b2ed29b6..487bb5b30 100644 --- a/src/route-handlers/signal-workflow/signal-workflow.types.ts +++ b/src/route-handlers/signal-workflow/signal-workflow.types.ts @@ -1,5 +1,9 @@ +import { type z } from 'zod'; + import { type GRPCClusterMethods } from '@/utils/grpc/grpc-client'; +import type signalWorkflowRequestBodySchema from './schemas/signal-workflow-request-body-schema'; + export type RequestParams = { params: { domain: string; @@ -9,6 +13,10 @@ export type RequestParams = { }; }; +export type SignalWorkflowRequestBody = z.infer< + typeof signalWorkflowRequestBodySchema +>; + export type Context = { grpcClusterMethods: GRPCClusterMethods; }; From 12ec77a42ac242e272a2ddc305a8c41409fc6696 Mon Sep 17 00:00:00 2001 From: Assem Hafez Date: Wed, 14 May 2025 14:14:17 +0000 Subject: [PATCH 06/17] fix typecheck --- .../signal-workflow/__tests__/signal-workflow.node.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/route-handlers/signal-workflow/__tests__/signal-workflow.node.ts b/src/route-handlers/signal-workflow/__tests__/signal-workflow.node.ts index 4377d63c7..564fb9a1b 100644 --- a/src/route-handlers/signal-workflow/__tests__/signal-workflow.node.ts +++ b/src/route-handlers/signal-workflow/__tests__/signal-workflow.node.ts @@ -72,7 +72,7 @@ describe(signalWorkflow.name, () => { requestBody: JSON.stringify({ signalName: 'test-signal', signalInput: 'not-an-object', // should be an object - } satisfies SignalWorkflowSubmissionData), + } satisfies SignalWorkflowRequestBody), }); expect(mockSignalWorkflow).not.toHaveBeenCalled(); From 9f6b690ac0d170aae2412d4bb6e0e06d0f616336 Mon Sep 17 00:00:00 2001 From: Assem Hafez Date: Wed, 14 May 2025 14:45:52 +0000 Subject: [PATCH 07/17] fix test cases --- .../signal-workflow/__tests__/signal-workflow.node.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/route-handlers/signal-workflow/__tests__/signal-workflow.node.ts b/src/route-handlers/signal-workflow/__tests__/signal-workflow.node.ts index 564fb9a1b..cc0d1df93 100644 --- a/src/route-handlers/signal-workflow/__tests__/signal-workflow.node.ts +++ b/src/route-handlers/signal-workflow/__tests__/signal-workflow.node.ts @@ -11,7 +11,7 @@ import { const defaultRequestBody = { signalName: 'test-signal', - signalInput: 'test-input', + signalInput: '"test-input"', } satisfies SignalWorkflowRequestBody; describe(signalWorkflow.name, () => { @@ -29,7 +29,7 @@ describe(signalWorkflow.name, () => { runId: 'mock-runid', }, signalName: 'test-signal', - signalInput: { data: Buffer.from('test-input') }, + signalInput: { data: Buffer.from('"test-input"') }, }); const responseJson = await res.json(); From 8dba2478450d71c74788721b8ac1e0c5b3556d9b Mon Sep 17 00:00:00 2001 From: Assem Hafez Date: Wed, 14 May 2025 15:53:49 +0000 Subject: [PATCH 08/17] Signal workflow form --- ...kflow-action-new-run-success-msg.types.tsx | 1 - .../workflow-action-signal-form.test.tsx | 111 ++++++++++++++++++ .../schemas/signal-workflow-form-schema.ts | 8 ++ .../workflow-action-signal-form.tsx | 66 +++++++++++ .../workflow-action-signal-form.types.ts | 15 +++ 5 files changed, 200 insertions(+), 1 deletion(-) create mode 100644 src/views/workflow-actions/workflow-action-signal-form/__tests__/workflow-action-signal-form.test.tsx create mode 100644 src/views/workflow-actions/workflow-action-signal-form/schemas/signal-workflow-form-schema.ts create mode 100644 src/views/workflow-actions/workflow-action-signal-form/workflow-action-signal-form.tsx create mode 100644 src/views/workflow-actions/workflow-action-signal-form/workflow-action-signal-form.types.ts diff --git a/src/views/workflow-actions/workflow-action-new-run-success-msg/workflow-action-new-run-success-msg.types.tsx b/src/views/workflow-actions/workflow-action-new-run-success-msg/workflow-action-new-run-success-msg.types.tsx index d11dcd289..b8f034647 100644 --- a/src/views/workflow-actions/workflow-action-new-run-success-msg/workflow-action-new-run-success-msg.types.tsx +++ b/src/views/workflow-actions/workflow-action-new-run-success-msg/workflow-action-new-run-success-msg.types.tsx @@ -5,5 +5,4 @@ export type Props = WorkflowActionSuccessMessageProps< { runId: string } > & { successMessage: string; - onDismissMessage: () => void; }; diff --git a/src/views/workflow-actions/workflow-action-signal-form/__tests__/workflow-action-signal-form.test.tsx b/src/views/workflow-actions/workflow-action-signal-form/__tests__/workflow-action-signal-form.test.tsx new file mode 100644 index 000000000..64da8767f --- /dev/null +++ b/src/views/workflow-actions/workflow-action-signal-form/__tests__/workflow-action-signal-form.test.tsx @@ -0,0 +1,111 @@ +import React from 'react'; + +import { type FieldErrors, useForm } from 'react-hook-form'; + +import { render, screen, fireEvent } from '@/test-utils/rtl'; + +import WorkflowActionSignalForm from '../workflow-action-signal-form'; +import { type SignalWorkflowFormData } from '../workflow-action-signal-form.types'; + +describe('WorkflowActionSignalForm', () => { + it('renders all form fields correctly', async () => { + await setup({}); + + expect( + screen.getByPlaceholderText('Enter signal name') + ).toBeInTheDocument(); + expect( + screen.getByPlaceholderText('Enter valid JSON input') + ).toBeInTheDocument(); + }); + + it('displays error when form has errors', async () => { + const formErrors = { + signalName: { + message: 'Signal name is required', + type: 'required', + }, + signalInput: { + message: 'Invalid JSON format', + type: 'invalid', + }, + }; + + await setup({ formErrors }); + + const signalNameInput = screen.getByPlaceholderText('Enter signal name'); + expect(signalNameInput).toHaveAttribute('aria-invalid', 'true'); + + const signalInputTextarea = screen.getByPlaceholderText( + 'Enter valid JSON input' + ); + expect(signalInputTextarea).toHaveAttribute('aria-invalid', 'true'); + }); + + it('handles input changes correctly', async () => { + await setup({}); + + const signalNameInput = screen.getByPlaceholderText('Enter signal name'); + fireEvent.change(signalNameInput, { target: { value: 'test-signal' } }); + expect(signalNameInput).toHaveValue('test-signal'); + + const signalInputTextarea = screen.getByPlaceholderText( + 'Enter valid JSON input' + ); + fireEvent.change(signalInputTextarea, { + target: { value: '{"key": "value"}' }, + }); + expect(signalInputTextarea).toHaveValue('{"key": "value"}'); + }); + + it('renders with default values', async () => { + await setup({ + formData: { + signalName: 'test-signal', + signalInput: '{"key": "value"}', + }, + }); + + const signalNameInput = screen.getByPlaceholderText('Enter signal name'); + expect(signalNameInput).toHaveValue('test-signal'); + + const signalInputTextarea = screen.getByPlaceholderText( + 'Enter valid JSON input' + ); + expect(signalInputTextarea).toHaveValue('{"key": "value"}'); + }); +}); + +type TestProps = { + formErrors: FieldErrors; + formData: SignalWorkflowFormData; +}; + +function TestWrapper({ formErrors, formData }: TestProps) { + const methods = useForm({ + defaultValues: formData, + }); + + return ( + + ); +} + +async function setup({ + formErrors = {}, + formData = { + signalName: '', + signalInput: '', + }, +}: Partial) { + render(); +} diff --git a/src/views/workflow-actions/workflow-action-signal-form/schemas/signal-workflow-form-schema.ts b/src/views/workflow-actions/workflow-action-signal-form/schemas/signal-workflow-form-schema.ts new file mode 100644 index 000000000..8d5c7afbf --- /dev/null +++ b/src/views/workflow-actions/workflow-action-signal-form/schemas/signal-workflow-form-schema.ts @@ -0,0 +1,8 @@ +import { z } from 'zod'; + +import signalWorkflowInputSchema from '@/route-handlers/signal-workflow/schemas/signal-workflow-input-schema'; + +export const signalWorkflowFormSchema = z.object({ + signalName: z.string().min(1), + signalInput: signalWorkflowInputSchema.optional(), +}); diff --git a/src/views/workflow-actions/workflow-action-signal-form/workflow-action-signal-form.tsx b/src/views/workflow-actions/workflow-action-signal-form/workflow-action-signal-form.tsx new file mode 100644 index 000000000..c8b2b5b73 --- /dev/null +++ b/src/views/workflow-actions/workflow-action-signal-form/workflow-action-signal-form.tsx @@ -0,0 +1,66 @@ +import { FormControl } from 'baseui/form-control'; +import { Input } from 'baseui/input'; +import { Textarea } from 'baseui/textarea'; +import { Controller } from 'react-hook-form'; + +import { type WorkflowActionFormProps } from '../workflow-actions.types'; + +import { type SignalWorkflowFormData } from './workflow-action-signal-form.types'; + +export default function WorkflowActionSignalForm({ + fieldErrors, + control, + formData, +}: WorkflowActionFormProps) { + const getErrorMessage = (field: string) => { + return field in fieldErrors + ? fieldErrors[field as keyof typeof fieldErrors]?.message + : undefined; + }; + + return ( +
+ + ( + { + field.onChange(e.target.value); + }} + onBlur={field.onBlur} + error={Boolean(getErrorMessage('signalName'))} + placeholder="Enter signal name" + /> + )} + /> + + + + ( +