-
Notifications
You must be signed in to change notification settings - Fork 3.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(dashboard): test workflow functionality #6768
Changes from 6 commits
c5d5c72
5f2a977
1ba0163
3dbe3b7
da5243b
0f094da
a1f64d6
cb0de48
05cba25
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
import { EnvironmentWithUserObjectCommand } from '@novu/application-generic'; | ||
import { IsDefined, IsString } from 'class-validator'; | ||
|
||
export class WorkflowTestDataCommand extends EnvironmentWithUserObjectCommand { | ||
@IsString() | ||
@IsDefined() | ||
identifierOrInternalId: string; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
import { JSONSchema } from 'json-schema-to-ts'; | ||
import { Injectable } from '@nestjs/common'; | ||
import { NotificationTemplateEntity } from '@novu/dal'; | ||
import { UserSessionData, WorkflowTestDataResponseDto } from '@novu/shared'; | ||
|
||
import { WorkflowTestDataCommand } from './test-data.command'; | ||
import { GetWorkflowByIdsUseCase } from '../get-workflow-by-ids/get-workflow-by-ids.usecase'; | ||
import { GetWorkflowByIdsCommand } from '../get-workflow-by-ids/get-workflow-by-ids.command'; | ||
|
||
const buildToFieldSchema = ({ user }: { user: UserSessionData }) => | ||
({ | ||
type: 'object', | ||
properties: { | ||
subscriberId: { type: 'string', default: user._id }, | ||
/* | ||
* TODO: the email and phone fields should be dynamic based on the workflow steps | ||
* if the workflow has has an email step, then email is required etc | ||
*/ | ||
email: { type: 'string', default: user.email ?? '', format: 'email' }, | ||
phone: { type: 'string', default: '' }, | ||
}, | ||
required: ['subscriberId', 'email', 'phone'], | ||
additionalProperties: false, | ||
}) as const satisfies JSONSchema; | ||
|
||
const buildPayloadSchema = () => | ||
({ | ||
type: 'object', | ||
description: 'Schema representing the workflow payload', | ||
properties: { | ||
/* | ||
* TODO: the properties should be dynamic based on the workflow variables | ||
*/ | ||
example: { type: 'string', description: 'Example field', default: 'payload.example' }, | ||
}, | ||
required: ['subscriberId', 'email', 'phone'], | ||
additionalProperties: false, | ||
}) as const satisfies JSONSchema; | ||
|
||
@Injectable() | ||
export class WorkflowTestDataUseCase { | ||
constructor(private getWorkflowByIdsUseCase: GetWorkflowByIdsUseCase) {} | ||
|
||
async execute(command: WorkflowTestDataCommand): Promise<WorkflowTestDataResponseDto> { | ||
const _workflowEntity: NotificationTemplateEntity | null = await this.getWorkflowByIdsUseCase.execute( | ||
GetWorkflowByIdsCommand.create({ | ||
...command, | ||
identifierOrInternalId: command.identifierOrInternalId, | ||
}) | ||
); | ||
|
||
return { | ||
to: buildToFieldSchema({ user: command.user }), | ||
payload: buildPayloadSchema(), | ||
}; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -22,9 +22,11 @@ | |
"dependencies": { | ||
"@clerk/clerk-react": "^5.2.5", | ||
"@hookform/resolvers": "^3.9.0", | ||
"@monaco-editor/react": "^4.6.0", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The |
||
"@novu/react": "^2.3.0", | ||
"@novu/shared": "workspace:*", | ||
"@radix-ui/react-avatar": "^1.1.1", | ||
"@radix-ui/react-accordion": "^1.2.1", | ||
"@radix-ui/react-dialog": "^1.1.2", | ||
"@radix-ui/react-dropdown-menu": "^2.1.1", | ||
"@radix-ui/react-icons": "^1.3.0", | ||
|
@@ -49,6 +51,7 @@ | |
"lodash.debounce": "^4.0.8", | ||
"lucide-react": "^0.439.0", | ||
"mixpanel-browser": "^2.52.0", | ||
"monaco-editor": "^0.39.0", | ||
"next-themes": "^0.3.0", | ||
"react": "^18.3.1", | ||
"react-dom": "^18.3.1", | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
export const Code2: React.FC<React.SVGProps<SVGSVGElement>> = (props) => ( | ||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 20" {...props}> | ||
<g clipPath="url(#a)"> | ||
<g clipPath="url(#b)"> | ||
<mask id="c" width="14" height="14" x="3" y="3" maskUnits="userSpaceOnUse" style={{ maskType: 'luminance' }}> | ||
<path fill="#fff" d="M17 3H3v14h14V3Z" /> | ||
</mask> | ||
<g fill="#7D52F4" fillRule="evenodd" clipRule="evenodd" mask="url(#c)"> | ||
<path d="M6.338 4.32c.5-.28 1.045-.37 1.412-.37a.55.55 0 1 1 0 1.1c-.216 0-.57.06-.875.23-.288.161-.515.408-.582.81-.052.31-.055.678-.059 1.116v.057c-.004.438-.01.94-.1 1.404-.09.467-.274.958-.683 1.327-.415.375-.984.556-1.7.556a.55.55 0 1 1 0-1.1c.533 0 .807-.132.962-.272.162-.146.274-.374.34-.72.068-.349.077-.753.08-1.205l.002-.088c.003-.406.006-.857.073-1.255.133-.797.613-1.3 1.13-1.59Z" /> | ||
<path d="M6.338 15.68c.5.28 1.045.37 1.412.37a.55.55 0 0 0 0-1.1c-.216 0-.57-.06-.875-.23-.288-.16-.515-.407-.582-.81-.052-.31-.055-.678-.059-1.116v-.056c-.004-.439-.01-.941-.1-1.404-.09-.467-.274-.958-.683-1.328-.415-.375-.984-.556-1.7-.556a.55.55 0 0 0 0 1.1c.533 0 .807.132.962.272.162.147.274.374.34.72.068.35.077.753.08 1.205l.002.089c.003.406.006.857.073 1.255.133.797.613 1.3 1.13 1.59ZM13.662 4.32c-.499-.28-1.045-.37-1.412-.37a.55.55 0 1 0 0 1.1c.217 0 .57.06.875.23.288.161.515.408.583.81.051.31.054.678.058 1.116v.057c.005.438.011.94.1 1.404.09.467.275.958.683 1.327.415.375.985.556 1.701.556a.55.55 0 0 0 0-1.1c-.533 0-.808-.132-.963-.272-.162-.146-.274-.374-.34-.72-.068-.349-.076-.753-.08-1.205l-.001-.088c-.003-.406-.007-.857-.073-1.255-.133-.797-.614-1.3-1.13-1.59Z" /> | ||
<path d="M13.662 15.68c-.499.28-1.045.37-1.412.37a.55.55 0 0 1 0-1.1c.217 0 .57-.06.875-.23.288-.16.515-.407.583-.81.051-.31.054-.678.058-1.116v-.056c.005-.439.011-.941.1-1.404.09-.467.275-.958.683-1.328.415-.375.985-.556 1.701-.556a.55.55 0 0 1 0 1.1c-.533 0-.808.132-.963.272-.162.147-.274.374-.34.72-.068.35-.076.753-.08 1.205l-.001.089c-.003.406-.007.857-.073 1.255-.133.797-.614 1.3-1.13 1.59ZM8.111 8.111a.55.55 0 0 1 .778 0l3 3a.55.55 0 1 1-.778.778l-3-3a.55.55 0 0 1 0-.778Z" /> | ||
<path d="M11.89 8.111a.55.55 0 0 0-.779 0l-3 3a.55.55 0 1 0 .778.778l3-3a.55.55 0 0 0 0-.778Z" /> | ||
</g> | ||
</g> | ||
</g> | ||
<defs> | ||
<clipPath id="a"> | ||
<path fill="#fff" d="M3 3h14v14H3z" /> | ||
</clipPath> | ||
<clipPath id="b"> | ||
<path fill="#fff" d="M3 3h14v14H3z" /> | ||
</clipPath> | ||
</defs> | ||
</svg> | ||
); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
import * as React from 'react'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Actually, I started to create the |
||
import * as AccordionPrimitive from '@radix-ui/react-accordion'; | ||
import { ChevronDownIcon } from '@radix-ui/react-icons'; | ||
|
||
import { cn } from '@/utils/ui'; | ||
|
||
const Accordion = AccordionPrimitive.Root; | ||
|
||
const AccordionItem = React.forwardRef< | ||
React.ElementRef<typeof AccordionPrimitive.Item>, | ||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item> | ||
>(({ className, ...props }, ref) => ( | ||
<AccordionPrimitive.Item | ||
ref={ref} | ||
className={cn('bg-neutral-alpha-50 flex flex-col gap-2 rounded-lg border border-neutral-200 p-2', className)} | ||
{...props} | ||
/> | ||
)); | ||
AccordionItem.displayName = 'AccordionItem'; | ||
|
||
type AccordionTriggerProps = React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger> & { | ||
withChevron?: boolean; | ||
}; | ||
|
||
const AccordionTrigger = React.forwardRef<React.ElementRef<typeof AccordionPrimitive.Trigger>, AccordionTriggerProps>( | ||
({ className, children, withChevron = true, ...props }, ref) => ( | ||
<AccordionPrimitive.Header className="flex"> | ||
<AccordionPrimitive.Trigger | ||
ref={ref} | ||
className={cn( | ||
'flex flex-1 items-center justify-between text-sm font-medium transition-all [&[data-state=open]>svg]:rotate-180', | ||
className | ||
)} | ||
{...props} | ||
> | ||
{children} | ||
{withChevron && ( | ||
<ChevronDownIcon className="text-muted-foreground h-4 w-4 shrink-0 transition-transform duration-200" /> | ||
)} | ||
</AccordionPrimitive.Trigger> | ||
</AccordionPrimitive.Header> | ||
) | ||
); | ||
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName; | ||
|
||
const AccordionContent = React.forwardRef< | ||
React.ElementRef<typeof AccordionPrimitive.Content>, | ||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content> | ||
>(({ className, children, ...props }, ref) => ( | ||
<AccordionPrimitive.Content | ||
ref={ref} | ||
className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm" | ||
{...props} | ||
> | ||
<div className={cn('pb-4 pt-0', className)}>{children}</div> | ||
</AccordionPrimitive.Content> | ||
)); | ||
AccordionContent.displayName = AccordionPrimitive.Content.displayName; | ||
|
||
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,14 +1,25 @@ | ||
import { useState } from 'react'; | ||
import { RiFileCopyLine } from 'react-icons/ri'; | ||
import { Button } from './button'; | ||
import { RiCheckLine, RiFileCopyLine } from 'react-icons/ri'; | ||
import { Button, ButtonProps } from './button'; | ||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from './tooltip'; | ||
import { cn } from '@/utils/ui'; | ||
|
||
type CopyButtonProps = { | ||
content: string; | ||
value?: string; | ||
className?: string; | ||
variant?: ButtonProps['variant']; | ||
size?: ButtonProps['size']; | ||
}; | ||
|
||
export const CopyButton: React.FC<CopyButtonProps> = ({ content, className }) => { | ||
export const CopyButton: React.FC<CopyButtonProps> = ({ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Improved the |
||
value, | ||
content, | ||
className, | ||
variant = 'outline', | ||
size = 'icon', | ||
}) => { | ||
const [isHovered, setIsHovered] = useState(false); | ||
const [isCopied, setIsCopied] = useState(false); | ||
|
||
const copyToClipboard = async (e: React.MouseEvent<HTMLButtonElement>) => { | ||
|
@@ -24,16 +35,19 @@ export const CopyButton: React.FC<CopyButtonProps> = ({ content, className }) => | |
|
||
return ( | ||
<TooltipProvider> | ||
<Tooltip> | ||
<Tooltip open={isHovered}> | ||
<TooltipTrigger asChild> | ||
<Button | ||
variant="outline" | ||
size="icon" | ||
className={className} | ||
variant={variant} | ||
size={size} | ||
className={cn('flex items-center gap-1', className)} | ||
onClick={copyToClipboard} | ||
aria-label="Copy to clipboard" | ||
onMouseEnter={() => setIsHovered(true)} | ||
onMouseLeave={() => setIsHovered(false)} | ||
> | ||
<RiFileCopyLine className="h-4 w-4" /> | ||
{isCopied ? <RiCheckLine className="size-4" /> : <RiFileCopyLine className="size-4" />} | ||
{value && <span>{value}</span>} | ||
</Button> | ||
</TooltipTrigger> | ||
<TooltipContent> | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
import React from 'react'; | ||
import { cn } from '@/utils/ui'; | ||
|
||
const Panel = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>( | ||
({ children, className, ...restDivProps }, ref) => { | ||
return ( | ||
<div | ||
ref={ref} | ||
className={cn('bg-neutral-alpha-50 flex flex-col gap-2 rounded-lg border border-neutral-200 p-2', className)} | ||
{...restDivProps} | ||
> | ||
{children} | ||
</div> | ||
); | ||
} | ||
); | ||
|
||
const PanelHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>( | ||
({ children, ...restDivProps }, ref) => { | ||
return ( | ||
<div ref={ref} className="flex items-center gap-1 text-sm font-medium" {...restDivProps}> | ||
{children} | ||
</div> | ||
); | ||
} | ||
); | ||
|
||
const PanelContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>( | ||
({ children, className, ...restDivProps }, ref) => { | ||
return ( | ||
<div | ||
ref={ref} | ||
className={cn('bg-background border-neutral-alpha-200 h-full rounded-lg border border-dashed p-3', className)} | ||
{...restDivProps} | ||
> | ||
{children} | ||
</div> | ||
); | ||
} | ||
); | ||
|
||
export { Panel, PanelHeader, PanelContent }; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,11 +1,19 @@ | ||
import { ReactNode } from 'react'; | ||
import { ExternalToast, toast } from 'sonner'; | ||
import { SmallToast } from './sonner'; | ||
import { Toast, ToastProps } from './sonner'; | ||
import { ReactNode } from 'react'; | ||
|
||
export const smallToast = ({ children, options }: { children: ReactNode; options: ExternalToast }) => { | ||
return toast(<SmallToast>{children}</SmallToast>, { | ||
export const showToast = ({ | ||
options, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. improved the toast helper function so we can pass the |
||
children, | ||
...toastProps | ||
}: Omit<ToastProps, 'children'> & { | ||
options: ExternalToast; | ||
children: (args: { close: () => void }) => ReactNode; | ||
}) => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do you maybe need the instance of the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the |
||
return toast.custom((id) => <Toast {...toastProps}>{children({ close: () => toast.dismiss(id) })}</Toast>, { | ||
duration: 5000, | ||
unstyled: true, | ||
closeButton: false, | ||
...options, | ||
}); | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
GET /workflow/:id/test-data
endpoint returns JSONSchema for theto
andpayload
fields that are used to populate the Test Workflow form with the data on FE.We agreed on this with @tatarco, and the work will be continued later.