Skip to content

Commit

Permalink
feat(dashboard): Nv 4479 workflow editor configure workflow drawer (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
BiswaViraj authored Oct 24, 2024
1 parent af72058 commit f8fcc82
Show file tree
Hide file tree
Showing 9 changed files with 303 additions and 32 deletions.
1 change: 1 addition & 0 deletions apps/dashboard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"@radix-ui/react-select": "^2.1.2",
"@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-switch": "^1.1.1",
"@radix-ui/react-tabs": "^1.1.1",
"@radix-ui/react-tooltip": "^1.1.3",
"@segment/analytics-next": "^1.73.0",
Expand Down
45 changes: 45 additions & 0 deletions apps/dashboard/src/components/primitives/copy-button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { useState } from 'react';
import { RiFileCopyLine } from 'react-icons/ri';
import { Button } from './button';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from './tooltip';

type CopyButtonProps = {
content: string;
className?: string;
};

export const CopyButton: React.FC<CopyButtonProps> = ({ content, className }) => {
const [isCopied, setIsCopied] = useState(false);

const copyToClipboard = async (e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
try {
await navigator.clipboard.writeText(content);
setIsCopied(true);
setTimeout(() => setIsCopied(false), 1500);
} catch (err) {
console.error('Failed to copy text: ', err);
}
};

return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="outline"
size="icon"
className={className}
onClick={copyToClipboard}
aria-label="Copy to clipboard"
>
<RiFileCopyLine className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{isCopied ? 'Copied!' : 'Click to copy'}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
};
26 changes: 26 additions & 0 deletions apps/dashboard/src/components/primitives/switch.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import * as React from 'react';
import * as SwitchPrimitives from '@radix-ui/react-switch';
import { cn } from '@/utils/ui';

const Switch = React.forwardRef<
React.ElementRef<typeof SwitchPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
>(({ className, ...props }, ref) => (
<SwitchPrimitives.Root
className={cn(
'focus-visible:ring-ring focus-visible:ring-offset-background data-[state=checked]:bg-primary data-[state=unchecked]:bg-input peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
className
)}
{...props}
ref={ref}
>
<SwitchPrimitives.Thumb
className={cn(
'bg-background before:bg-primary data-[state=unchecked]:before:bg-input pointer-events-none flex h-4 w-4 items-center justify-center rounded-full shadow-lg ring-0 transition-transform before:h-1.5 before:w-1.5 before:rounded-full data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0'
)}
/>
</SwitchPrimitives.Root>
));
Switch.displayName = SwitchPrimitives.Root.displayName;

export { Switch };
49 changes: 32 additions & 17 deletions apps/dashboard/src/components/primitives/tag-input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,17 @@ import { CommandGroup, CommandInput, CommandItem, CommandList } from '@/componen
import { cn } from '@/utils/ui';
import { Command } from 'cmdk';
import { forwardRef, useEffect, useState } from 'react';
import { RiCloseFill } from 'react-icons/ri';
import { RiAddFill, RiCloseFill } from 'react-icons/ri';

type TagInputProps = React.InputHTMLAttributes<HTMLInputElement> & {
value: string[];
suggestions: string[];
onChange: (tags: string[]) => void;
showAddButton?: boolean;
};

const TagInput = forwardRef<HTMLInputElement, TagInputProps>((props, ref) => {
const { className, suggestions, value, onChange, ...rest } = props;
const { className, suggestions, value, onChange, showAddButton, ...rest } = props;
const [tags, setTags] = useState<string[]>(value);
const [inputValue, setInputValue] = useState('');
const [isOpen, setIsOpen] = useState(false);
Expand Down Expand Up @@ -50,20 +51,22 @@ const TagInput = forwardRef<HTMLInputElement, TagInputProps>((props, ref) => {
<Command>
<div className="flex flex-col gap-2">
<PopoverAnchor asChild>
<CommandInput
ref={ref}
autoComplete="off"
value={inputValue}
className={cn(inputVariants(), 'flex-grow', className)}
placeholder="Type a tag and press Enter"
onValueChange={(value) => {
setInputValue(value);
setIsOpen(true);
}}
onFocusCapture={() => setIsOpen(true)}
onBlurCapture={() => setIsOpen(false)}
{...rest}
/>
<div className={cn({ 'hidden group-focus-within:block': showAddButton })}>
<CommandInput
ref={ref}
autoComplete="off"
value={inputValue}
className={cn(inputVariants(), 'flex-grow', className)}
placeholder="Type a tag and press Enter"
onValueChange={(value) => {
setInputValue(value);
setIsOpen(true);
}}
onFocusCapture={() => setIsOpen(true)}
onBlurCapture={() => setIsOpen(false)}
{...rest}
/>
</div>
</PopoverAnchor>
<div className="flex flex-wrap gap-2">
{tags.map((tag, index) => (
Expand All @@ -75,6 +78,19 @@ const TagInput = forwardRef<HTMLInputElement, TagInputProps>((props, ref) => {
</button>
</Badge>
))}

{showAddButton && (
<Badge
variant="outline"
kind="tag"
className="flex px-1.5 py-3 focus:hidden active:hidden group-focus-within:hidden"
>
<button type="button">
<RiAddFill />
<span className="sr-only">Add tag</span>
</button>
</Badge>
)}
</div>
</div>
<PopoverContent
Expand Down Expand Up @@ -116,6 +132,5 @@ const TagInput = forwardRef<HTMLInputElement, TagInputProps>((props, ref) => {
</Popover>
);
});
TagInput.displayName = 'TagInput';

export { TagInput };
124 changes: 124 additions & 0 deletions apps/dashboard/src/components/workflow-editor/configure-workflow.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { useFormContext } from 'react-hook-form';
import { RouteFill } from '../icons';
import { Input, InputField } from '../primitives/input';
import { RiArrowRightSLine, RiSettingsLine } from 'react-icons/ri';
import * as z from 'zod';
import { Separator } from '../primitives/separator';
import { TagInput } from '../primitives/tag-input';
import { Textarea } from '../primitives/textarea';
import { formSchema } from './schema';
import { useTagsQuery } from '@/hooks/use-tags-query';
import { Button } from '../primitives/button';
import { CopyButton } from '../primitives/copy-button';
import { FormControl, FormField, FormItem, FormLabel, FormMessage } from '../primitives/form/form';
import { Switch } from '../primitives/switch';

export function ConfigureWorkflow() {
const tagsQuery = useTagsQuery();

const { control } = useFormContext<z.infer<typeof formSchema>>();
return (
<aside className="text-foreground-950 flex h-full w-[300px] max-w-[350px] flex-col border-l pb-5 pt-3.5 [&_input]:text-xs [&_input]:text-neutral-600 [&_label]:text-xs [&_label]:font-medium [&_textarea]:text-xs [&_textarea]:text-neutral-600">
<div className="flex items-center gap-2.5 px-3 pb-3.5 text-sm font-medium">
<RouteFill />
<span>Configure workflow</span>
</div>
<Separator />
<FormField
control={control}
name="active"
render={({ field }) => (
<FormItem className="flex items-center justify-between gap-2.5 space-y-0 px-3 py-2">
<div className="flex items-center gap-4">
<div
className="bg-success/60 data-[active=false]:shadow-neutral-alpha-100 ml-2 h-1.5 w-1.5 rounded-full [--pulse-color:var(--success)] data-[active=true]:animate-[pulse-shadow_1s_ease-in-out_infinite] data-[active=false]:bg-neutral-300 data-[active=false]:shadow-[0_0px_0px_5px_var(--neutral-alpha-200),0_0px_0px_9px_var(--neutral-alpha-100)]"
data-active={field.value}
/>
<FormLabel>Active Workflow</FormLabel>
</div>
<FormControl>
<Switch checked={field.value} onCheckedChange={field.onChange} />
</FormControl>
</FormItem>
)}
/>
<Separator />
<div className="flex flex-col gap-4 p-3">
<FormField
control={control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Workflow Name</FormLabel>
<FormControl>
<InputField>
<Input placeholder="Untitled" {...field} />
</InputField>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={control}
name="workflowId"
render={({ field }) => (
<FormItem>
<FormLabel>Workflow Identifier</FormLabel>
<FormControl>
<InputField className="flex overflow-hidden pr-0">
<Input placeholder="Untitled" {...field} />
<CopyButton
content={field.value}
className="rounded-md rounded-s-none border-b-0 border-r-0 border-t-0 text-neutral-400"
/>
</InputField>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={control}
name="description"
render={({ field }) => (
<FormItem>
<FormLabel>Description</FormLabel>
<FormControl>
<Textarea placeholder="Description of what this workflow does" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={control}
name="tags"
render={({ field }) => (
<FormItem className="group" tabIndex={-1}>
<div className="flex items-center gap-1">
<FormLabel>Tags</FormLabel>
</div>
<FormControl className="text-xs text-neutral-600">
<TagInput
{...field}
value={field.value ?? []}
suggestions={tagsQuery.data?.data.map((tag) => tag.name) || []}
showAddButton
/>
</FormControl>
</FormItem>
)}
/>
</div>
<Separator />
<div className="px-3 py-4">
<Button variant="outline" className="flex w-full justify-start gap-1.5 text-xs font-medium" type="button">
<RiSettingsLine className="h-4 w-4 text-neutral-600" />
Configure channel preferences <RiArrowRightSLine className="ml-auto h-4 w-4 text-neutral-600" />
</Button>
</div>
<Separator />
</aside>
);
}
26 changes: 26 additions & 0 deletions apps/dashboard/src/components/workflow-editor/schema.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,19 @@
import * as z from 'zod';
import { StepTypeEnum } from '@/utils/enums';

const enabledSchema = z.object({
enabled: z.boolean(),
});

// Reusable schema for channels
const channelsSchema = z.object({
in_app: enabledSchema,
sms: enabledSchema,
email: enabledSchema,
push: enabledSchema,
chat: enabledSchema,
});

export const formSchema = z.object({
name: z.string(),
workflowId: z.string(),
Expand All @@ -16,4 +29,17 @@ export const formSchema = z.object({
})
.passthrough()
),
preferences: z.object({
/**
* TODO: Add user schema
*/
user: z.any().nullable(),
default: z.object({
all: z.object({
enabled: z.boolean(),
readOnly: z.boolean(),
}),
channels: channelsSchema,
}),
}),
});
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export const WorkflowEditor = () => {
});

return (
<Tabs defaultValue="workflow" className="-mt-[1px] flex h-full flex-col">
<Tabs defaultValue="workflow" className="-mt-[1px] flex h-full flex-1 flex-col">
<TabsList>
<TabsTrigger value="workflow">Workflow</TabsTrigger>
</TabsList>
Expand Down
Loading

0 comments on commit f8fcc82

Please sign in to comment.