Skip to content

Commit

Permalink
feat(dashboard): workflow editor canvas - base step nodes and edges (n…
Browse files Browse the repository at this point in the history
  • Loading branch information
LetItRock authored Oct 16, 2024
1 parent a6fddc5 commit 508c746
Show file tree
Hide file tree
Showing 28 changed files with 1,101 additions and 471 deletions.
3 changes: 2 additions & 1 deletion .cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -686,7 +686,8 @@
"xkeysib",
"zulip",
"zwnj",
"motionone"
"motionone",
"xyflow"
],
"flagWords": [],
"patterns": [
Expand Down
2 changes: 1 addition & 1 deletion apps/dashboard/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
/>
</head>
<body>
<div id="root"></div>
<div id="root" class="h-full"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
1 change: 1 addition & 0 deletions apps/dashboard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"@radix-ui/react-tooltip": "^1.1.3",
"@segment/analytics-next": "^1.73.0",
"@tanstack/react-query": "^5.59.6",
"@xyflow/react": "^12.3.2",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
Expand Down
4 changes: 2 additions & 2 deletions apps/dashboard/src/components/dashboard-layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,12 @@ export const DashboardLayout = ({
}) => {
return (
<IntercomProvider appId={INTERCOM_APP_ID}>
<div className="relative flex w-full">
<div className="relative flex h-full w-full">
<SideNavigation />
<div className="flex flex-1 flex-col overflow-y-auto overflow-x-hidden">
<HeaderNavigation startItems={headerStartItems} />

<div className="flex min-h-[calc(100dvh-4rem)] flex-col overflow-y-auto overflow-x-hidden">{children}</div>
<div className="flex flex-1 flex-col overflow-y-auto overflow-x-hidden">{children}</div>
</div>
</div>
</IntercomProvider>
Expand Down
4 changes: 2 additions & 2 deletions apps/dashboard/src/components/edit-workflow-layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@ export const EditWorkflowLayout = ({
}) => {
return (
<IntercomProvider appId={INTERCOM_APP_ID}>
<div className="relative flex w-full">
<div className="relative flex h-full w-full">
<div className="flex flex-1 flex-col overflow-y-auto overflow-x-hidden">
<HeaderNavigation startItems={headerStartItems} />

<div className="flex min-h-dvh flex-col overflow-y-auto overflow-x-hidden">{children}</div>
<div className="flex flex-1 flex-col overflow-y-auto overflow-x-hidden">{children}</div>
</div>
</div>
</IntercomProvider>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export const EditBridgeUrlButton = () => {
'relative size-1.5 animate-[pulse-shadow_1s_ease-in-out_infinite] rounded-full',
status === ConnectionStatus.DISCONNECTED || status === ConnectionStatus.LOADING
? 'bg-destructive [--pulse-color:var(--destructive)]'
: 'bg-success [--pulse-color:var(--destructive)]'
: 'bg-success [--pulse-color:var(--success)]'
)}
/>
<span>Local Studio</span>
Expand Down
3 changes: 3 additions & 0 deletions apps/dashboard/src/components/icons/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
export * from './logo-circle';
export * from './notification-5-fill';
export * from './mail-3-fill';
export * from './sms';
12 changes: 12 additions & 0 deletions apps/dashboard/src/components/icons/mail-3-fill.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type { HTMLAttributes } from 'react';

export const Mail3Fill = (props: HTMLAttributes<HTMLOrSVGElement>) => {
return (
<svg viewBox="0 0 11 10" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
<path
d="M4.70703 1.87402H3.3125H2.60547H2.375V2.0459V2.81152V3.60059V5.33887L0.503906 3.9541C0.535156 3.60059 0.716797 3.27246 1.00586 3.05957L1.4375 2.73926V1.87402C1.4375 1.35645 1.85742 0.936523 2.375 0.936523H3.87109L4.8457 0.21582C5.03516 0.0751953 5.26367 -0.000976562 5.5 -0.000976562C5.73633 -0.000976562 5.96484 0.0751953 6.1543 0.213867L7.12891 0.936523H8.625C9.14258 0.936523 9.5625 1.35645 9.5625 1.87402V2.73926L9.99414 3.05957C10.2832 3.27246 10.4648 3.60059 10.4961 3.9541L8.625 5.33887V3.60059V2.81152V2.0459V1.87402H8.39453H7.6875H6.29297H4.70508H4.70703ZM0.5 8.74902V4.72754L4.75 7.87598C4.9668 8.03613 5.23047 8.12402 5.5 8.12402C5.76953 8.12402 6.0332 8.03809 6.25 7.87598L10.5 4.72754V8.74902C10.5 9.43848 9.93945 9.99902 9.25 9.99902H1.75C1.06055 9.99902 0.5 9.43848 0.5 8.74902ZM3.9375 3.12402H7.0625C7.23438 3.12402 7.375 3.26465 7.375 3.43652C7.375 3.6084 7.23438 3.74902 7.0625 3.74902H3.9375C3.76562 3.74902 3.625 3.6084 3.625 3.43652C3.625 3.26465 3.76562 3.12402 3.9375 3.12402ZM3.9375 4.37402H7.0625C7.23438 4.37402 7.375 4.51465 7.375 4.68652C7.375 4.8584 7.23438 4.99902 7.0625 4.99902H3.9375C3.76562 4.99902 3.625 4.8584 3.625 4.68652C3.625 4.51465 3.76562 4.37402 3.9375 4.37402Z"
fill="currentColor"
/>
</svg>
);
};
12 changes: 12 additions & 0 deletions apps/dashboard/src/components/icons/notification-5-fill.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type { HTMLAttributes } from 'react';

export const Notification5Fill = (props: HTMLAttributes<HTMLOrSVGElement>) => {
return (
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 9 12" {...props}>
<path
fill="currentColor"
d="M4.5.856a.642.642 0 0 0-.643.643v.386a3.216 3.216 0 0 0-2.572 3.15v.377c0 .945-.347 1.857-.974 2.564l-.149.167a.642.642 0 0 0 .48 1.07h7.715a.644.644 0 0 0 .48-1.07l-.149-.167a3.863 3.863 0 0 1-.974-2.564v-.377a3.216 3.216 0 0 0-2.572-3.15v-.386A.642.642 0 0 0 4.5.856Zm.91 9.91c.24-.24.375-.568.375-.91H3.214a1.286 1.286 0 0 0 2.196.91Z"
/>
</svg>
);
};
12 changes: 12 additions & 0 deletions apps/dashboard/src/components/icons/sms.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type { HTMLAttributes } from 'react';

export const Sms = (props: HTMLAttributes<HTMLOrSVGElement>) => {
return (
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 17 15" {...props}>
<path
fill="currentColor"
d="M8.5 13.693c4.42 0 8-3.064 8-6.847C16.5 3.064 12.92 0 8.5 0 4.084-.001.503 3.064.503 6.846c0 1.485.553 2.858 1.49 3.98-.06.807-.356 1.524-.669 2.07a5.945 5.945 0 0 1-.628.9l-.04.046-.01.01a.542.542 0 0 0-.106.572.5.5 0 0 0 .463.326c.896 0 1.8-.293 2.55-.635a9.244 9.244 0 0 0 1.696-1.008c.994.379 2.094.59 3.253.59v-.004ZM3.502 5.951c0-.668.516-1.212 1.15-1.212h.6c.275 0 .5.237.5.527 0 .29-.225.527-.5.527h-.6a.154.154 0 0 0-.15.158c0 .052.025.102.069.131l.919.646c.321.224.512.602.512 1.01 0 .669-.516 1.212-1.15 1.212l-.85.003a.515.515 0 0 1-.5-.527c0-.29.225-.526.5-.526h.85c.085 0 .15-.073.15-.158a.158.158 0 0 0-.069-.132l-.918-.645A1.244 1.244 0 0 1 3.5 5.95Zm8.65-1.212h.6c.274 0 .5.237.5.527 0 .29-.226.527-.5.527h-.6c-.085 0-.15.072-.15.158 0 .052.024.102.068.131l.919.646c.319.224.512.602.512 1.01 0 .669-.515 1.212-1.15 1.212l-.85.003a.515.515 0 0 1-.5-.527c0-.29.226-.526.5-.526h.85c.085 0 .15-.073.15-.158a.158.158 0 0 0-.068-.132l-.919-.645a1.233 1.233 0 0 1-.512-1.01c0-.67.515-1.212 1.15-1.212v-.004ZM7.4 4.95l1.1 1.544L9.6 4.95a.486.486 0 0 1 .559-.184c.206.072.34.273.34.5v3.16c0 .29-.224.527-.5.527a.515.515 0 0 1-.5-.527v-1.58l-.6.843a.495.495 0 0 1-.4.21.495.495 0 0 1-.4-.21l-.6-.843v1.58c0 .29-.224.527-.499.527a.515.515 0 0 1-.5-.527v-3.16c0-.227.137-.428.34-.5a.488.488 0 0 1 .56.184Z"
/>
</svg>
);
};
25 changes: 25 additions & 0 deletions apps/dashboard/src/components/icons/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { StepTypeEnum } from '@novu/shared';
import {
RiCellphoneFill,
RiChatThreadFill,
RiFlashlightFill,
RiHourglassFill,
RiRocket2Fill,
RiShadowLine,
} from 'react-icons/ri';
import { Mail3Fill } from './mail-3-fill';
import { Notification5Fill } from './notification-5-fill';
import { Sms } from './sms';
import { IconType } from 'react-icons/lib';

export const STEP_TYPE_TO_ICON: Record<StepTypeEnum, IconType> = {
[StepTypeEnum.CHAT]: RiChatThreadFill,
[StepTypeEnum.CUSTOM]: RiRocket2Fill,
[StepTypeEnum.DELAY]: RiHourglassFill,
[StepTypeEnum.DIGEST]: RiShadowLine,
[StepTypeEnum.EMAIL]: Mail3Fill,
[StepTypeEnum.IN_APP]: Notification5Fill,
[StepTypeEnum.PUSH]: RiCellphoneFill,
[StepTypeEnum.SMS]: Sms,
[StepTypeEnum.TRIGGER]: RiFlashlightFill,
};
55 changes: 48 additions & 7 deletions apps/dashboard/src/components/primitives/badge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,33 @@ import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '@/utils/ui';

const badgeVariants = cva(
'inline-flex items-center h-5 border px-2 py-1 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
'inline-flex items-center h-5 border px-2 py-1 transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
{
variants: {
variant: {
default: 'border-transparent bg-neutral-alpha-100 text-neutral-300',
destructive: 'border-transparent bg-destructive/10 text-destructive',
success: 'border-transparent bg-success/10 text-success',
warning: 'border-transparent bg-warning/10 text-warning',
neutral: 'border-neutral-500 bg-neutral-500',
feature: 'border-feature bg-feature',
information: 'border-information bg-information',
highlighted: 'border-highlighted bg-highlighted',
stable: 'border-stable bg-stable',
verified: 'border-verified bg-verified',
destructive: 'border-destructive bg-destructive',
'destructive-light': 'border-transparent bg-destructive/10',
success: 'border-success bg-success',
'success-light': 'border-transparent bg-success/10',
warning: 'border-warning bg-warning',
'warning-light': 'border-transparent bg-warning/10',
alert: 'border-alert bg-alert',
soft: 'border-neutral-alpha-200 bg-neutral-alpha-200',
},
kind: {
default: 'rounded-md',
pill: 'rounded-full',
'pill-stroke': 'rounded-full bg-transparent',
},
},
defaultVariants: {
variant: 'default',
variant: 'neutral',
kind: 'default',
},
}
Expand All @@ -30,4 +41,34 @@ function Badge({ className, variant, kind, ...props }: BadgeProps) {
return <div className={cn(badgeVariants({ variant, kind }), className)} {...props} />;
}

export { Badge, badgeVariants };
const badgeContentVariants = cva('text-xs font-medium', {
variants: {
variant: {
foreground: 'text-foreground',
'neutral-foreground': 'text-neutral-foreground',
neutral: 'text-neutral-500',
feature: 'text-feature',
information: 'text-information',
highlighted: 'text-highlighted',
stable: 'text-stable',
verified: 'text-verified',
destructive: 'text-destructive',
success: 'text-success',
warning: 'text-warning',
alert: 'text-alert',
},
},
defaultVariants: {
variant: 'foreground',
},
});

export interface BadgeContentProps
extends React.HTMLAttributes<HTMLSpanElement>,
VariantProps<typeof badgeContentVariants> {}

function BadgeContent({ className, variant, ...props }: BadgeContentProps) {
return <span className={cn(badgeContentVariants({ variant }), className)} {...props} />;
}

export { Badge, BadgeContent, badgeVariants, badgeContentVariants };
20 changes: 11 additions & 9 deletions apps/dashboard/src/components/primitives/step.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,27 @@ import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '@/utils/ui';

const stepVariants = cva(
'inline-flex items-center shadow-xs rounded-full border text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
'inline-flex items-center shadow-xs rounded-full border text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 bg-neutral-50',
{
variants: {
variant: {
// use solid bg here because we usually stack these on top of each other
default: 'border-neutral-100 bg-neutral-50 text-neutral-400',
feature: 'border-feature/30 bg-neutral-50 text-feature/30',
information: 'border-information/30 bg-neutral-50 text-information/30',
highlighted: 'border-highlighted/30 bg-neutral-50 text-highlighted/30',
stable: 'border-stable/30 bg-neutral-50 text-stable/30',
verified: 'border-verified/30 bg-neutral-50 text-verified/30',
destructive: 'border-destructive/30 bg-neutral-50 text-destructive/30',
neutral: 'border-neutral-100 text-neutral-400',
feature: 'border-feature/30 text-feature/30',
information: 'border-information/30 text-information/30',
highlighted: 'border-highlighted/30 text-highlighted/30',
stable: 'border-stable/30 text-stable/30',
verified: 'border-verified/30 text-verified/30',
destructive: 'border-destructive/30 text-destructive/30',
warning: 'border-warning/30 text-warning/30',
alert: 'border-alert/30 text-alert/30',
},
size: {
default: 'p-1 [&>svg]:size-3.5',
},
},
defaultVariants: {
variant: 'default',
variant: 'neutral',
size: 'default',
},
}
Expand Down
2 changes: 1 addition & 1 deletion apps/dashboard/src/components/workflow-cloud.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { HTMLAttributes } from 'react';
import type { HTMLAttributes } from 'react';

type WorkflowCloudProps = HTMLAttributes<HTMLOrSVGElement>;
export const WorkflowCloud = (props: WorkflowCloudProps) => {
Expand Down
109 changes: 109 additions & 0 deletions apps/dashboard/src/components/workflow-editor/add-step-menu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { ReactNode, useState } from 'react';
import { RiAddLine } from 'react-icons/ri';
import { PopoverPortal } from '@radix-ui/react-popover';
import { StepTypeEnum } from '@novu/shared';
import { Node } from './base-node';
import { Popover, PopoverContent, PopoverTrigger } from '../primitives/popover';
import { STEP_TYPE_TO_ICON } from '../icons/utils';
import { STEP_TYPE_TO_COLOR } from '@/utils/color';
import { Badge, BadgeContent } from '../primitives/badge';
import { cn } from '@/utils/ui';

const MenuGroup = ({ children }: { children: ReactNode }) => {
return <div className="flex flex-col">{children}</div>;
};

const MenuTitle = ({ children }: { children: ReactNode }) => {
return (
<span className="bg-neutral-alpha-50 text-foreground-400 border-neutral-alpha-100 border-b p-1.5 text-xs uppercase">
{children}
</span>
);
};

const MenuItemsGroup = ({ children }: { children: ReactNode }) => {
return <div className="flex flex-col gap-1 p-1">{children}</div>;
};

const MenuItem = ({
children,
stepType,
disabled = true,
}: {
children: ReactNode;
stepType: StepTypeEnum;
disabled?: boolean;
}) => {
const Icon = STEP_TYPE_TO_ICON[stepType];
const color = STEP_TYPE_TO_COLOR[stepType];

return (
<span
className={cn(
'shadow-xs text-foreground-600 hover:bg-accent flex cursor-pointer items-center gap-2 rounded-lg p-1.5',
{
'text-foreground-300 cursor-not-allowed': disabled,
}
)}
>
<Icon className={`text-${color} bg-neutral-alpha-50 h-6 w-6 rounded-md p-1 opacity-40`} />
<span className="text-xs">{children}</span>
{disabled && (
<Badge kind="pill" variant="soft" className="ml-auto opacity-40">
<BadgeContent variant="neutral">soon</BadgeContent>
</Badge>
)}
</span>
);
};

export const AddStepMenu = ({ visible = false }: { visible?: boolean }) => {
const [isPopoverOpen, setIsPopoverOpen] = useState(false);

return (
<Popover
open={isPopoverOpen}
onOpenChange={(newIsOpen) => {
setIsPopoverOpen(newIsOpen);
}}
>
<PopoverTrigger asChild>
<span>
<Node
variant="sm"
className={cn('opacity-0 transition duration-300 ease-out hover:opacity-100', {
'opacity-100': isPopoverOpen || visible,
})}
>
<RiAddLine className="h-4 w-4" />
</Node>
</span>
</PopoverTrigger>
<PopoverPortal>
<PopoverContent side="right" className="flex w-[200px] flex-col rounded-lg p-0">
<div>
<MenuGroup>
<MenuTitle>Channels</MenuTitle>
<MenuItemsGroup>
<MenuItem stepType={StepTypeEnum.EMAIL}>Email</MenuItem>
<MenuItem stepType={StepTypeEnum.IN_APP} disabled={false}>
In-App
</MenuItem>
<MenuItem stepType={StepTypeEnum.PUSH}>Push</MenuItem>
<MenuItem stepType={StepTypeEnum.CHAT}>Chat</MenuItem>
<MenuItem stepType={StepTypeEnum.SMS}>SMS</MenuItem>
</MenuItemsGroup>
</MenuGroup>
<MenuGroup>
<MenuTitle>Action Steps</MenuTitle>
<MenuItemsGroup>
<MenuItem stepType={StepTypeEnum.DIGEST}>Digest</MenuItem>
<MenuItem stepType={StepTypeEnum.DELAY}>Delay</MenuItem>
</MenuItemsGroup>
</MenuGroup>
</div>
</PopoverContent>
</PopoverPortal>
</Popover>
);
};
Loading

0 comments on commit 508c746

Please sign in to comment.