diff --git a/packages/backend/src/apps/postman/common/parameters.ts b/packages/backend/src/apps/postman/common/parameters.ts index dbfac8a7f..192845b0e 100644 --- a/packages/backend/src/apps/postman/common/parameters.ts +++ b/packages/backend/src/apps/postman/common/parameters.ts @@ -52,6 +52,7 @@ export const transactionalEmailFields: IField[] = [ 'table', ], supportTableDisplay: true, + previewType: 'email' as const, }, { label: 'Recipient email(s)', diff --git a/packages/frontend/src/components/FlowStepTestController/index.tsx b/packages/frontend/src/components/FlowStepTestController/index.tsx index ecacc3621..e0e91bf5c 100644 --- a/packages/frontend/src/components/FlowStepTestController/index.tsx +++ b/packages/frontend/src/components/FlowStepTestController/index.tsx @@ -1,4 +1,9 @@ -import { IBaseTrigger, IStep, ITriggerInstructions } from '@plumber/types' +import { + IBaseTrigger, + IStep, + ITriggerInstructions, + TFieldPreviewType, +} from '@plumber/types' import { useContext, useEffect, useMemo, useRef, useState } from 'react' import { useFormContext } from 'react-hook-form' @@ -10,6 +15,7 @@ import { Icon, Text, Tooltip, + useDisclosure, VStack, } from '@chakra-ui/react' import { @@ -25,7 +31,9 @@ import { validateStepParams } from '@/helpers/validateStepParams' import { useStepMetadata } from '@/hooks/useStepMetadata' import { EDITOR_MARGIN_TOP_NUM } from '../Editor/constants' +import EmailPreviewModal from '../EmailPreviewModal' import ErrorResult from '../ErrorResult' +import { substituteForPreview } from '../RichTextEditor/utils' import WebhookUrlInfo from '../WebhookUrlInfo' import { CheckAgainButton } from './CheckAgainButton' @@ -38,6 +46,10 @@ import { matchParamsToDataIn, } from './utils' +const PREVIEW_BUTTON_COPY: Record = { + email: 'Preview email', +} + const defaultTriggerInstructions: ITriggerInstructions = { beforeUrlMsg: `# 1. You'll need to configure your application with this webhook URL.`, afterUrlMsg: `# 2. Send some data to the webhook URL after configuration. Then, click check step.`, @@ -122,7 +134,45 @@ export default function FlowStepTestController( lastErrorDetails, isWebhookSubstep, testVariables, - } = useTestDetails(step, currentTestExecutionStep, allApps) + previewAction, + } = useTestDetails(step, currentTestExecutionStep, allApps, substeps) + + const { + isOpen: isPreviewModalOpen, + onOpen: onPreviewModalOpen, + onClose: onPreviewModalClose, + } = useDisclosure() + + const previewModal = useMemo(() => { + if (!previewAction) { + return null + } + switch (previewAction.kind) { + case 'email': { + const previewHtml = substituteForPreview(previewAction.html, varInfoMap) + return ( + + ) + } + } + }, [previewAction, varInfoMap, isPreviewModalOpen, onPreviewModalClose]) + + const previewButton = previewAction ? ( + + ) : null const containerRef = useRef(null) const webhookUrlInfoRef = useRef(null) const [collapseDirection, setCollapseDirection] = useState<'up' | 'down'>( @@ -337,6 +387,7 @@ export default function FlowStepTestController( {!isDirty ? 'Saved' : 'Save without checking'} )} + {previewButton} )} + {previewButton} )} + {previewModal} ) } diff --git a/packages/frontend/src/components/FlowStepTestController/useTestDetails.ts b/packages/frontend/src/components/FlowStepTestController/useTestDetails.ts index 5de1d898e..b7d31a605 100644 --- a/packages/frontend/src/components/FlowStepTestController/useTestDetails.ts +++ b/packages/frontend/src/components/FlowStepTestController/useTestDetails.ts @@ -1,21 +1,77 @@ -import { IApp, IExecutionStep, IJSONObject, IStep } from '@plumber/types' +import { + IApp, + IExecutionStep, + IFieldRichText, + IJSONObject, + IStep, + ISubstep, + TFieldPreviewType, +} from '@plumber/types' + +import { useMemo } from 'react' +import { useWatch } from 'react-hook-form' import { extractVariables, Variable } from '@/helpers/variables' import { isSameAppAndAppKey } from './utils' +export interface PreviewAction { + kind: TFieldPreviewType + fieldKey: string + html: string +} + interface UseTestDetailsResult { isTestSuccessful: boolean isWebhookSubstep: boolean lastErrorDetails?: IJSONObject | null testVariables: Variable[] | null + previewAction: PreviewAction | null } +// Stable dummy field name used when no previewable arg exists in the step, so +// useWatch is still called unconditionally and the rules-of-hooks are +// respected. +const PREVIEW_WATCH_STUB = '__previewActionStub__' + export function useTestDetails( step: IStep, currentTestExecutionStep: IExecutionStep | null, allApps: IApp[], + substeps: ISubstep[], ): UseTestDetailsResult { + const previewableArg = useMemo<{ + key: string + previewType: TFieldPreviewType + } | null>(() => { + for (const substep of substeps ?? []) { + for (const arg of substep.arguments ?? []) { + if (arg.type === 'rich-text') { + const rt = arg as IFieldRichText + if (rt.previewType) { + return { key: rt.key, previewType: rt.previewType } + } + } + } + } + return null + }, [substeps]) + + const liveValue = useWatch({ + name: previewableArg + ? `parameters.${previewableArg.key}` + : PREVIEW_WATCH_STUB, + }) as unknown + + const previewAction: PreviewAction | null = + previewableArg && typeof liveValue === 'string' && liveValue.length > 0 + ? { + kind: previewableArg.previewType, + fieldKey: previewableArg.key, + html: liveValue, + } + : null + const isWebhookSubstep = (step.appKey === 'webhook' || step.appKey === 'gathersg') && Boolean(step?.webhookUrl) @@ -26,6 +82,7 @@ export function useTestDetails( isWebhookSubstep, lastErrorDetails: null, testVariables: null, + previewAction, } } @@ -45,5 +102,6 @@ export function useTestDetails( isWebhookSubstep, lastErrorDetails, testVariables, + previewAction, } }