Skip to content
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): workflow template store cta on list page #7540

Merged
merged 15 commits into from
Jan 22, 2025
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export const accessTokenTemplate: WorkflowTemplate = {
name: 'Access Token',
description: 'Alert users about new access token creation',
category: 'authentication',
isPopular: false,
isPopular: true,
workflowDefinition: {
name: 'Access Token',
description: 'Notify a user about a creation of a personal access token in their GitHub account',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export const paymentConfirmedTemplate: WorkflowTemplate = {
name: 'Payment Confirmed',
description: 'Send payment confirmations with receipts',
category: 'billing',
isPopular: false,
isPopular: true,
workflowDefinition: {
name: 'Payment Confirmed',
description: '',
Expand Down
21 changes: 13 additions & 8 deletions apps/dashboard/src/components/template-store/workflow-card.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { StepTypeEnum } from '@novu/shared';
import React from 'react';
import { RiAddFill } from 'react-icons/ri';
import { Card, CardContent } from '../primitives/card';
import { StepType } from '../step-preview-hover-card';
import { WorkflowStep } from '../workflow-step';
Expand All @@ -19,19 +20,23 @@ export function WorkflowCard({
}: WorkflowCardProps) {
return (
<Card
className="border-stroke-soft min-h-[138px] min-w-[250px] border shadow-none hover:cursor-pointer"
className="border-stroke-soft min-h-[120px] min-w-[250px] border shadow-none hover:cursor-pointer"
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Made the cards smaller in height

onClick={onClick}
>
<CardContent className="p-3">
<div className="overflow-hidden rounded-lg border border-neutral-100">
<div className="bg-bg-weak relative h-[114px] bg-[url(/images/dots.svg)] bg-cover">
<div className="bg-bg-weak relative h-[100px] bg-[url(/images/dots.svg)] bg-cover">
<div className="flex h-full w-full items-center justify-center">
{steps.map((step, index) => (
<React.Fragment key={index}>
<WorkflowStep step={step} />
{index < steps.length - 1 && <div className="h-px w-6 bg-gray-200" />}
</React.Fragment>
))}
{!steps?.length ? (
<RiAddFill className="text-[#D6D6D6]" />
) : (
steps.map((step, index) => (
<React.Fragment key={index}>
<WorkflowStep step={step} />
{index < steps.length - 1 && <div className="h-px w-6 bg-gray-200" />}
Copy link
Contributor

Choose a reason for hiding this comment

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

What is this div for? Is it a separator?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's the connector line between the steps

</React.Fragment>
))
)}
</div>
</div>
</div>
Expand Down
109 changes: 52 additions & 57 deletions apps/dashboard/src/components/template-store/workflow-sidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { CreateWorkflowButton } from '@/components/create-workflow-button';
import { useTelemetry } from '@/hooks/use-telemetry';
import { TelemetryEvent } from '@/utils/telemetry';
import { Calendar, Code2, ExternalLink, FileCode2, FileText, KeyRound, LayoutGrid, Users } from 'lucide-react';
import { motion } from 'motion/react';
import { ReactNode } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { buildRoute, ROUTES } from '../../utils/routes';
import { Badge } from '../primitives/badge';
import { WorkflowMode } from './types';

Expand All @@ -17,10 +20,8 @@ interface SidebarButtonProps {
onClick?: () => void;
isActive?: boolean;
bgColor?: string;
asChild?: boolean;
hasExternalLink?: boolean;
beta?: boolean;
createWorkflowButton?: React.ComponentType<any>;
}

const buttonVariants = {
Expand All @@ -40,12 +41,9 @@ function SidebarButton({
onClick,
isActive,
bgColor = 'bg-blue-50',
asChild,
beta,
hasExternalLink,
createWorkflowButton: CustomCreateWorkflowButton,
}: SidebarButtonProps) {
const ButtonWrapper = asChild && CustomCreateWorkflowButton ? CustomCreateWorkflowButton : motion.button;
const content = (
<div className="flex items-center gap-3">
<motion.div variants={iconVariants} className={`rounded-lg p-[5px] ${bgColor}`}>
Expand All @@ -61,7 +59,7 @@ function SidebarButton({
);

return (
<ButtonWrapper
<motion.button
variants={buttonVariants}
initial="initial"
whileHover="hover"
Expand All @@ -72,19 +70,15 @@ function SidebarButton({
isActive ? '!border-[#EEEFF1] bg-white' : ''
}`}
>
{asChild ? (
content
) : (
<div className="flex w-full items-center gap-2">
{content}{' '}
{beta && (
<Badge color="gray" size="sm">
BETA
</Badge>
)}
</div>
)}
</ButtonWrapper>
<div className="flex w-full items-center gap-2">
{content}{' '}
{beta && (
<Badge color="gray" size="sm">
BETA
</Badge>
)}
</div>
</motion.button>
);
}

Expand Down Expand Up @@ -115,44 +109,46 @@ const useCases = [
},
] as const;

const createOptions = [
{
icon: <FileText className="h-3 w-3 text-gray-700" />,
label: 'Blank workflow',
bgColor: 'bg-green-50',
asChild: true,
},
{
icon: <Code2 className="h-3 w-3 text-gray-700" />,
label: 'Code-based workflow',
hasExternalLink: true,
bgColor: 'bg-blue-50',
onClick: () => window.open('https://docs.novu.co/framework/overview', '_blank'),
},
];

export function WorkflowSidebar({ selectedCategory, onCategorySelect, mode }: WorkflowSidebarProps) {
const navigate = useNavigate();
const { environmentSlug } = useParams();
const track = useTelemetry();

const handleCreateWorkflow = () => {
track(TelemetryEvent.CREATE_WORKFLOW_CLICK);
navigate(buildRoute(ROUTES.WORKFLOWS_CREATE, { environmentSlug: environmentSlug || '' }));
};

const createOptions = [
{
icon: <FileText className="h-3 w-3 text-gray-700" />,
label: 'Blank workflow',
bgColor: 'bg-green-50',
onClick: handleCreateWorkflow,
},
{
icon: <Code2 className="h-3 w-3 text-gray-700" />,
label: 'Code-based workflow',
hasExternalLink: true,
bgColor: 'bg-blue-50',
onClick: () => window.open('https://docs.novu.co/framework/overview', '_blank'),
},
];

return (
<div className="flex h-full flex-col bg-gray-50">
<section className="p-2">
<div className="mb-2">
<span className="text-subheading-2xs text-gray-500">CREATE</span>
</div>
<div className="flex flex-col gap-2">
{createOptions.map((item, index) => (
<SidebarButton
key={index}
icon={item.icon}
label={item.label}
onClick={item.onClick}
bgColor={item.bgColor}
asChild={item.asChild}
hasExternalLink={item.hasExternalLink}
createWorkflowButton={CreateWorkflowButton}
/>
))}
</div>
</section>
<div className="flex h-full w-[240px] flex-col gap-4 border-r p-2">
<div className="flex flex-col gap-1">
{createOptions.map((item, index) => (
<SidebarButton
key={index}
icon={item.icon}
label={item.label}
onClick={item.onClick}
bgColor={item.bgColor}
hasExternalLink={item.hasExternalLink}
/>
))}
</div>
<section className="p-2">
<div className="mb-2">
<span className="text-subheading-2xs text-gray-500">EXPLORE</span>
Expand All @@ -167,7 +163,6 @@ export function WorkflowSidebar({ selectedCategory, onCategorySelect, mode }: Wo
onClick={() => onCategorySelect(item.id)}
isActive={mode === WorkflowMode.TEMPLATES && selectedCategory === item.id}
bgColor={item.bgColor}
createWorkflowButton={CreateWorkflowButton}
/>
))}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export type WorkflowTemplateModalProps = ComponentProps<typeof DialogTrigger> &
open?: boolean;
onOpenChange?: (open: boolean) => void;
source?: string;
selectedTemplate?: IWorkflowSuggestion;
};

export function WorkflowTemplateModal(props: WorkflowTemplateModalProps) {
Expand All @@ -41,7 +42,9 @@ export function WorkflowTemplateModal(props: WorkflowTemplateModalProps) {
const [selectedCategory, setSelectedCategory] = useState<string>('popular');
const [suggestions, setSuggestions] = useState<IWorkflowSuggestion[]>([]);
const [mode, setMode] = useState<WorkflowMode>(WorkflowMode.TEMPLATES);
const [selectedTemplate, setSelectedTemplate] = useState<IWorkflowSuggestion | null>(null);
const [internalSelectedTemplate, setInternalSelectedTemplate] = useState<IWorkflowSuggestion | null>(null);

const selectedTemplate = props.selectedTemplate ?? internalSelectedTemplate;

const filteredTemplates = WORKFLOW_TEMPLATES.filter((template) =>
selectedCategory === 'popular' ? template.isPopular : template.category === selectedCategory
Expand All @@ -56,6 +59,12 @@ export function WorkflowTemplateModal(props: WorkflowTemplateModalProps) {
}
}, [props.open, props.source, track]);

useEffect(() => {
if (props.selectedTemplate) {
setInternalSelectedTemplate(props.selectedTemplate);
}
}, [props.selectedTemplate]);

const handleCreateWorkflow = async (values: z.infer<typeof workflowSchema>) => {
if (!selectedTemplate) return;

Expand Down Expand Up @@ -88,11 +97,11 @@ export function WorkflowTemplateModal(props: WorkflowTemplateModalProps) {
};

const handleTemplateClick = (template: IWorkflowSuggestion) => {
setSelectedTemplate(template);
setInternalSelectedTemplate(template);
};

const handleBackClick = () => {
setSelectedTemplate(null);
setInternalSelectedTemplate(null);
setMode(WorkflowMode.TEMPLATES);
};

Expand Down Expand Up @@ -137,13 +146,7 @@ export function WorkflowTemplateModal(props: WorkflowTemplateModalProps) {
</DialogHeader>
<div className={`flex h-[${selectedTemplate ? '600px' : '640px'}]`}>
{!selectedTemplate && (
<div className="h-full w-[259px] border-r border-neutral-200">
<WorkflowSidebar
selectedCategory={selectedCategory}
onCategorySelect={handleCategorySelect}
mode={mode}
/>
</div>
<WorkflowSidebar selectedCategory={selectedCategory} onCategorySelect={handleCategorySelect} mode={mode} />
)}

<div className="w-full flex-1 overflow-auto">
Expand Down
56 changes: 33 additions & 23 deletions apps/dashboard/src/components/workflow-list-empty.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { CreateWorkflowButton } from '@/components/create-workflow-button';
import { VersionControlDev } from '@/components/icons/version-control-dev';
import { VersionControlProd } from '@/components/icons/version-control-prod';
import { Button } from '@/components/primitives/button';
import { useEnvironment } from '@/context/environment/hooks';
import { RiBookMarkedLine, RiRouteFill } from 'react-icons/ri';
import { Link } from 'react-router-dom';
import { Link, useNavigate, useParams } from 'react-router-dom';
import { buildRoute, ROUTES } from '../utils/routes';
import { LinkButton } from './primitives/button-link';

export const WorkflowListEmpty = () => {
Expand Down Expand Up @@ -42,29 +42,39 @@ const WorkflowListEmptyProd = ({ switchToDev }: { switchToDev: () => void }) =>
</div>
);

const WorkflowListEmptyDev = () => (
<div className="flex h-full w-full flex-col items-center justify-center gap-6">
<VersionControlDev />
<div className="flex flex-col items-center gap-2 text-center">
<span className="text-foreground-900 block font-medium">Create your first workflow to send notifications</span>
<p className="text-foreground-400 max-w-[60ch] text-sm">
Workflows handle notifications across multiple channels in a single, version-controlled flow, with the ability
to manage preference for each subscriber.
</p>
</div>
const WorkflowListEmptyDev = () => {
const navigate = useNavigate();
const { environmentSlug } = useParams();

<div className="flex items-center justify-center gap-6">
<Link to={'https://docs.novu.co/concepts/workflows'} target="_blank">
<LinkButton variant="gray" trailingIcon={RiBookMarkedLine}>
View docs
</LinkButton>
</Link>
return (
<div className="flex h-full w-full flex-col items-center justify-center gap-6">
<VersionControlDev />
<div className="flex flex-col items-center gap-2 text-center">
<span className="text-foreground-900 block font-medium">Create your first workflow to send notifications</span>
<p className="text-foreground-400 max-w-[60ch] text-sm">
Workflows handle notifications across multiple channels in a single, version-controlled flow, with the ability
to manage preference for each subscriber.
</p>
</div>

<div className="flex items-center justify-center gap-6">
<Link to={'https://docs.novu.co/concepts/workflows'} target="_blank">
<LinkButton variant="gray" trailingIcon={RiBookMarkedLine}>
View docs
</LinkButton>
</Link>

<CreateWorkflowButton asChild>
<Button variant="primary" leadingIcon={RiRouteFill} className="gap-2">
<Button
variant="primary"
leadingIcon={RiRouteFill}
className="gap-2"
onClick={() => {
navigate(buildRoute(ROUTES.WORKFLOWS_CREATE, { environmentSlug: environmentSlug || '' }));
}}
>
Create workflow
</Button>
</CreateWorkflowButton>
</div>
</div>
</div>
);
);
};
Loading
Loading