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): Add workflow page action menu #7222

Merged
merged 10 commits into from
Dec 5, 2024
38 changes: 38 additions & 0 deletions apps/dashboard/src/components/delete-workflow-dialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { WorkflowListResponseDto, WorkflowResponseDto } from '@novu/shared';
import { ConfirmationModal } from './confirmation-modal';
import TruncatedText from './truncated-text';

type DeleteWorkflowDialogProps = {
workflow: WorkflowResponseDto | WorkflowListResponseDto;
open: boolean;
onOpenChange: (open: boolean) => void;
onConfirm: () => void;
isLoading?: boolean;
};

export const DeleteWorkflowDialog = ({
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Reused between workflows list and editor

workflow,
open,
onOpenChange,
onConfirm,
isLoading,
}: DeleteWorkflowDialogProps) => {
return (
<ConfirmationModal
open={open}
onOpenChange={onOpenChange}
onConfirm={onConfirm}
title="Are you sure?"
description={
<>
You're about to delete the <TruncatedText className="max-w-[32ch] font-bold">{workflow.name}</TruncatedText>{' '}
workflow, this action is permanent. <br />
<br />
You won't be able to trigger this workflow anymore.
</>
}
confirmButtonText="Delete"
isLoading={isLoading}
/>
);
};
2 changes: 1 addition & 1 deletion apps/dashboard/src/components/success-button-toast.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ interface SuccessToastProps {
export function SuccessButtonToast({ title, description, actionLabel, onAction, onClose }: SuccessToastProps) {
return (
<>
<ToastIcon variant="default" />
<ToastIcon variant="success" />
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This should be success :)

<div className="flex flex-1 flex-col items-start gap-2.5">
<div className="flex flex-col items-start justify-center gap-1 self-stretch">
<div className="text-foreground-950 text-sm font-medium">{title}</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { motion } from 'motion/react';
import { useState } from 'react';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { useNavigate } from 'react-router-dom';
import type { ExternalToast } from 'sonner';

import { PAUSE_MODAL_TITLE, PauseModalDescription } from '@/components/pause-workflow-dialog';
import { SidebarContent, SidebarHeader } from '@/components/side-navigation/sidebar';
Expand All @@ -21,17 +23,83 @@ import { TagInput } from '../primitives/tag-input';
import { Textarea } from '../primitives/textarea';
import { MAX_DESCRIPTION_LENGTH, workflowSchema } from '@/components/workflow-editor/schema';
import { useFormAutosave } from '@/hooks/use-form-autosave';
import { RiDeleteBin2Line, RiGitPullRequestFill, RiMore2Fill } from 'react-icons/ri';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/primitives/dropdown-menu';
import { Button } from '../primitives/button';
import { Tooltip, TooltipContent, TooltipPortal, TooltipTrigger } from '../primitives/tooltip';
import { useEnvironment } from '@/context/environment/hooks';
import { useSyncWorkflow } from '@/hooks/use-sync-workflow';
import { useDeleteWorkflow } from '@/hooks/use-delete-workflow';
import { showToast } from '@/components/primitives/sonner-helpers';
import { ToastIcon } from '@/components/primitives/sonner';
import { DeleteWorkflowDialog } from '../delete-workflow-dialog';
import { ROUTES } from '@/utils/routes';

type ConfigureWorkflowFormProps = {
workflow: WorkflowResponseDto;
update: (data: UpdateWorkflowDto) => void;
};

const toastOptions: ExternalToast = {
position: 'bottom-right',
classNames: {
toast: 'mb-4 right-0',
},
};

export const ConfigureWorkflowForm = (props: ConfigureWorkflowFormProps) => {
const { workflow, update } = props;
const navigate = useNavigate();
const isReadOnly = workflow.origin === WorkflowOriginEnum.EXTERNAL;
const [isPauseModalOpen, setIsPauseModalOpen] = useState(false);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
const tagsQuery = useTags();
const { currentEnvironment } = useEnvironment();
const { safeSync, isSyncable, tooltipContent, PromoteConfirmModal } = useSyncWorkflow(workflow);

const { deleteWorkflow, isPending: isDeleteWorkflowPending } = useDeleteWorkflow({
onSuccess: () => {
showToast({
children: () => (
<>
<ToastIcon variant="success" />
<span className="text-sm">
Deleted workflow <span className="font-bold">{workflow.name}</span>.
</span>
</>
),
options: toastOptions,
});
navigate(ROUTES.WORKFLOWS);
},
onError: () => {
showToast({
children: () => (
<>
<ToastIcon variant="error" />
<span className="text-sm">
Failed to delete workflow <span className="font-bold">{workflow.name}</span>.
</span>
</>
),
options: toastOptions,
});
},
});

const onDeleteWorkflow = async () => {
await deleteWorkflow({
workflowId: workflow._id,
});
};

const form = useForm<z.infer<typeof workflowSchema>>({
defaultValues: {
Expand All @@ -57,6 +125,8 @@ export const ConfigureWorkflowForm = (props: ConfigureWorkflowFormProps) => {
saveForm();
};

const syncToLabel = `Sync to ${currentEnvironment?.name === 'Production' ? 'Development' : 'Production'}`;

return (
<>
<ConfirmationModal
Expand All @@ -70,6 +140,13 @@ export const ConfigureWorkflowForm = (props: ConfigureWorkflowFormProps) => {
description={<PauseModalDescription workflowName={workflow.name} />}
confirmButtonText="Proceed"
/>
<DeleteWorkflowDialog
workflow={workflow}
open={isDeleteModalOpen}
onOpenChange={setIsDeleteModalOpen}
onConfirm={onDeleteWorkflow}
isLoading={isDeleteWorkflowPending}
/>
<PageMeta title={workflow.name} />
<motion.div
className={cn('relative flex h-full w-full flex-col')}
Expand All @@ -83,6 +160,50 @@ export const ConfigureWorkflowForm = (props: ConfigureWorkflowFormProps) => {
<RouteFill />
<span>Configure workflow</span>
</div>
<DropdownMenu open={isDropdownOpen} onOpenChange={setIsDropdownOpen}>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="ml-auto">
<RiMore2Fill />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56">
<DropdownMenuGroup>
{isSyncable ? (
<DropdownMenuItem onClick={safeSync}>
<RiGitPullRequestFill />
{syncToLabel}
</DropdownMenuItem>
) : (
<Tooltip>
<TooltipTrigger>
<DropdownMenuItem disabled>
<RiGitPullRequestFill />
{syncToLabel}
</DropdownMenuItem>
</TooltipTrigger>
<TooltipPortal>
<TooltipContent>{tooltipContent}</TooltipContent>
</TooltipPortal>
</Tooltip>
)}
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup className="*:cursor-pointer">
<DropdownMenuItem
className="text-destructive"
disabled={workflow.origin === WorkflowOriginEnum.EXTERNAL}
onClick={() => {
setIsDeleteModalOpen(true);
setIsDropdownOpen(false);
}}
>
<RiDeleteBin2Line />
Delete workflow
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
<PromoteConfirmModal />
</SidebarHeader>
<Separator />
<Form {...form}>
Expand Down
15 changes: 3 additions & 12 deletions apps/dashboard/src/components/workflow-row.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import { showToast } from './primitives/sonner-helpers';
import { ToastIcon } from './primitives/sonner';
import { usePatchWorkflow } from '@/hooks/use-patch-workflow';
import { PauseModalDescription, PAUSE_MODAL_TITLE } from '@/components/pause-workflow-dialog';
import { DeleteWorkflowDialog } from './delete-workflow-dialog';

type WorkflowRowProps = {
workflow: WorkflowListResponseDto;
Expand Down Expand Up @@ -207,21 +208,11 @@ export const WorkflowRow = ({ workflow }: WorkflowRowProps) => {
</Tooltip>

<TableCell className="w-1">
<ConfirmationModal
<DeleteWorkflowDialog
workflow={workflow}
open={isDeleteModalOpen}
onOpenChange={setIsDeleteModalOpen}
onConfirm={onDeleteWorkflow}
title="Are you sure?"
description={
<>
You're about to delete the{' '}
<TruncatedText className="max-w-[32ch] font-bold">{workflow.name}</TruncatedText> workflow, this action is
permanent. <br />
<br />
You won't be able to trigger this workflow anymore.
</>
}
confirmButtonText="Delete"
isLoading={isDeleteWorkflowPending}
/>
<ConfirmationModal
Expand Down
4 changes: 2 additions & 2 deletions apps/dashboard/src/hooks/use-sync-workflow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { useMutation } from '@tanstack/react-query';
import { useMemo, useState } from 'react';
import { toast } from 'sonner';

export function useSyncWorkflow(workflow: WorkflowListResponseDto) {
export function useSyncWorkflow(workflow: WorkflowResponseDto | WorkflowListResponseDto) {
const { oppositeEnvironment, switchEnvironment } = useEnvironment();
const [isLoading, setIsLoading] = useState(false);
const [showConfirmModal, setShowConfirmModal] = useState(false);
Expand Down Expand Up @@ -106,7 +106,7 @@ export function useSyncWorkflow(workflow: WorkflowListResponseDto) {
setIsLoading(true);
loadingToast = toast.loading(
<>
<ToastIcon variant="default" />
<ToastIcon variant="default" className="animate-spin" />
<span className="text-sm">
Syncing workflow <span className="font-bold">{workflow.name}</span> to {oppositeEnvironment?.name}...
</span>
Expand Down
Loading