Skip to content

Commit

Permalink
feat(dashboard): Tag input suggestions (#6728)
Browse files Browse the repository at this point in the history
  • Loading branch information
desiprisg authored Oct 21, 2024
1 parent 481a225 commit e8fd24e
Show file tree
Hide file tree
Showing 31 changed files with 394 additions and 109 deletions.
3 changes: 2 additions & 1 deletion .cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -690,6 +690,7 @@
"xyflow",
"Sonner",
"sonner",
"cmdk"
],
"flagWords": [],
"patterns": [
Expand Down Expand Up @@ -787,6 +788,6 @@
"apps/web/src/studio/components/workflows/step-editor/editor/files.ts",
"apps/web/src/pages/playground/web-container-configuration/sandbox-vite/*.ts",
"apps/api/src/app/analytics/usecases/hubspot-identify-form/hubspot-identify-form.usecase.ts",
"apps/dashboard/eslint.config.js",
"apps/dashboard/eslint.config.js"
]
}
1 change: 1 addition & 0 deletions apps/dashboard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"@xyflow/react": "^12.3.2",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"cmdk": "1.0.0",
"date-fns": "^4.1.0",
"lodash.debounce": "^4.0.8",
"lucide-react": "^0.439.0",
Expand Down
7 changes: 5 additions & 2 deletions apps/dashboard/src/components/create-workflow-button.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { createWorkflow } from '@/api/workflows';
import { Button } from '@/components/primitives/button';
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/primitives/form';
import { FormField, FormItem, FormLabel, FormControl, FormMessage, Form } from '@/components/primitives/form/form';
import { Input, InputField } from '@/components/primitives/input';
import { Separator } from '@/components/primitives/separator';
import {
Expand All @@ -16,6 +16,7 @@ import {
import { TagInput } from '@/components/primitives/tag-input';
import { Textarea } from '@/components/primitives/textarea';
import { useEnvironment } from '@/context/environment/hooks';
import { useTagsQuery } from '@/hooks/use-tags-query';
import { QueryKeys } from '@/utils/query-keys';
import { zodResolver } from '@hookform/resolvers/zod';
import { type CreateWorkflowDto, WorkflowCreationSourceEnum } from '@novu/shared';
Expand Down Expand Up @@ -54,6 +55,8 @@ export const CreateWorkflowButton = (props: CreateWorkflowButtonProps) => {
setIsOpen(false);
},
});
const tagsQuery = useTagsQuery();

const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: { description: '', identifier: '', name: '', tags: [] },
Expand Down Expand Up @@ -147,7 +150,7 @@ export const CreateWorkflowButton = (props: CreateWorkflowButtonProps) => {
<FormLabel hint="(max. 8)">Add tags</FormLabel>
</div>
<FormControl>
<TagInput {...field} />
<TagInput suggestions={tagsQuery.data?.data.map((tag) => tag.name) || []} {...field} />
</FormControl>
<FormMessage />
</FormItem>
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 @@ -2,9 +2,9 @@ import { ReactNode } from 'react';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import { IntercomProvider } from 'react-use-intercom';
import { SideNavigation } from './side-navigation';
import { HeaderNavigation } from './header-navigation';
import { INTERCOM_APP_ID } from '@/config';
import { SideNavigation } from '@/components/side-navigation/side-navigation';
import { HeaderNavigation } from '@/components/header-navigation/header-navigation';

export const DashboardLayout = ({
children,
Expand Down
2 changes: 1 addition & 1 deletion apps/dashboard/src/components/edit-workflow-layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import { ReactNode } from 'react';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import { IntercomProvider } from 'react-use-intercom';
import { HeaderNavigation } from './header-navigation';
import { INTERCOM_APP_ID } from '@/config';
import { HeaderNavigation } from '@/components/header-navigation/header-navigation';

export const EditWorkflowLayout = ({
children,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useBootIntercom } from '@/hooks/use-boot-intercom';
import { RiCustomerService2Line } from 'react-icons/ri';
import { useBootIntercom } from '@/hooks';

export const CustomerSupportButton = () => {
useBootIntercom();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@ import { cn } from '@/utils/ui';
import { Popover, PopoverContent, PopoverTrigger, PopoverPortal } from '../primitives/popover';
import { Button } from '../primitives/button';
import { Input, InputField } from '../primitives/input';
import { useBridgeHealthCheck, useUpdateBridgeUrl, useValidateBridgeUrl } from '@/hooks';
import { ConnectionStatus } from '@/utils/types';
import { useEnvironment } from '@/context/environment/hooks';
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '../primitives/form';
import { useBridgeHealthCheck } from '@/hooks/use-bridge-health-check';
import { useValidateBridgeUrl } from '@/hooks/use-validate-bridge-url';
import { useUpdateBridgeUrl } from '@/hooks/use-update-bridge-url';
import { FormField, FormItem, FormLabel, FormControl, FormMessage, Form } from '@/components/primitives/form/form';

const formSchema = z.object({ bridgeUrl: z.string().url() });

Expand Down
1 change: 0 additions & 1 deletion apps/dashboard/src/components/header-navigation/index.ts

This file was deleted.

1 change: 0 additions & 1 deletion apps/dashboard/src/components/primitives/form/index.ts

This file was deleted.

23 changes: 9 additions & 14 deletions apps/dashboard/src/components/primitives/input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as React from 'react';

import { cn } from '@/utils/ui';
import { cva, VariantProps } from 'class-variance-authority';
import { inputVariants } from '@/components/primitives/variants';

const inputFieldVariants = cva(
'text-foreground-950 flex w-full flex-nowrap items-center gap-1.5 rounded-md border bg-transparent shadow-sm transition-colors focus-within:outline-none focus-visible:outline-none hover:bg-neutral-50 has-[input:disabled]:cursor-not-allowed has-[input:disabled]:opacity-50 has-[input[value=""]]:text-foreground-400 has-[input:disabled]:bg-neutral-alpha-100 has-[input:disabled]:text-foreground-300',
Expand All @@ -27,26 +28,20 @@ export type InputFieldProps = { children: React.ReactNode; className?: string }
typeof inputFieldVariants
>;

const InputField = ({ children, className, size, state }: InputFieldProps) => {
return <div className={inputFieldVariants({ size, state, className })}>{children}</div>;
};
const InputField = React.forwardRef<HTMLInputElement, InputFieldProps>(({ children, className, size, state }, ref) => {
return (
<div ref={ref} className={inputFieldVariants({ size, state, className })}>
{children}
</div>
);
});

InputField.displayName = 'InputField';

export type InputProps = React.InputHTMLAttributes<HTMLInputElement>;

const Input = React.forwardRef<HTMLInputElement, InputProps>(({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
'file:text-foreground placeholder:text-foreground-400 flex h-full w-full bg-transparent text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:outline-none disabled:cursor-not-allowed',
className
)}
ref={ref}
{...props}
/>
);
return <input type={type} className={cn(inputVariants(), className)} ref={ref} {...props} />;
});
Input.displayName = 'Input';

Expand Down
12 changes: 7 additions & 5 deletions apps/dashboard/src/components/primitives/popover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ const PopoverPortal = PopoverPrimitive.Portal;

const PopoverContent = React.forwardRef<
React.ElementRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ className, align = 'center', sideOffset = 4, ...props }, ref) => (
<PopoverPrimitive.Portal>
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content> & { portal?: boolean }
>(({ className, align = 'center', portal = true, sideOffset = 4, ...props }, ref) => {
const body = (
<PopoverPrimitive.Content
ref={ref}
align={align}
Expand All @@ -26,8 +26,10 @@ const PopoverContent = React.forwardRef<
)}
{...props}
/>
</PopoverPrimitive.Portal>
));
);

return portal ? <PopoverPrimitive.Portal>{body}</PopoverPrimitive.Portal> : body;
});
PopoverContent.displayName = PopoverPrimitive.Content.displayName;

export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor, PopoverPortal };
119 changes: 76 additions & 43 deletions apps/dashboard/src/components/primitives/tag-input.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,25 @@
'use client';

import { Badge } from '@/components/primitives/badge';
import { Input, InputField } from '@/components/primitives/input';
import { Popover, PopoverAnchor, PopoverContent } from '@/components/primitives/popover';
import { inputVariants } from '@/components/primitives/variants';
import { CommandGroup, CommandInput, CommandItem, CommandList } from '@/components/ui/command';
import { cn } from '@/utils/ui';
import { Command } from 'cmdk';
import { forwardRef, useEffect, useState } from 'react';
import { RiCloseFill } from 'react-icons/ri';

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

const TagInput = forwardRef<HTMLInputElement, TagInputProps>((props, ref) => {
const { className, value, onChange, ...rest } = props;
const { className, suggestions, value, onChange, ...rest } = props;
const [tags, setTags] = useState<string[]>(value);
const [inputValue, setInputValue] = useState('');
const [isOpen, setIsOpen] = useState(false);

useEffect(() => {
setTags(value);
Expand All @@ -27,6 +32,7 @@ const TagInput = forwardRef<HTMLInputElement, TagInputProps>((props, ref) => {
}
onChange(newTags);
setInputValue('');
setIsOpen(false);
};

const removeTag = (tag: string) => {
Expand All @@ -39,48 +45,75 @@ const TagInput = forwardRef<HTMLInputElement, TagInputProps>((props, ref) => {
setInputValue('');
};

const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
e.preventDefault();

if (inputValue === '') {
return;
}

addTag(inputValue);
}
};

return (
<div className="flex flex-col space-y-2">
<InputField>
<Input
ref={ref}
type="text"
value={inputValue}
onKeyDown={handleKeyDown}
className={cn('flex-grow', className)}
placeholder="Type a tag and press Enter"
onChange={(e) => setInputValue(e.target.value)}
{...rest}
/>
</InputField>
<div className="flex flex-wrap gap-2">
{tags.map((tag, index) => (
<Badge key={index} variant="outline" kind="tag">
{tag}
<button
type="button"
onClick={() => removeTag(tag)}
className="ml-1 rounded-full outline-none focus:outline-none"
>
<RiCloseFill className="size-3" />
<span className="sr-only">Remove tag</span>
</button>
</Badge>
))}
</div>
</div>
<Popover open={isOpen}>
<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}
/>
</PopoverAnchor>
<div className="flex flex-wrap gap-2">
{tags.map((tag, index) => (
<Badge key={index} variant="outline" kind="tag" className="gap-1">
<span>{tag}</span>
<button type="button" onClick={() => removeTag(tag)}>
<RiCloseFill className="size-3" />
<span className="sr-only">Remove tag</span>
</button>
</Badge>
))}
</div>
</div>
<PopoverContent
className="p-1"
portal={false}
onOpenAutoFocus={(e) => {
e.preventDefault();
}}
>
<CommandList>
<CommandGroup>
{inputValue !== '' && (
<CommandItem
// We can't have duplicate keys in our list so adding a prefix
// here to differentiate this from a possible suggestion value
value={`input-${inputValue}`}
onSelect={() => {
addTag(inputValue);
}}
>
{inputValue}
</CommandItem>
)}
{suggestions.map((tag) => (
<CommandItem
key={tag}
value={tag}
onSelect={() => {
addTag(tag);
}}
>
{tag}
</CommandItem>
))}
</CommandGroup>
</CommandList>
</PopoverContent>
</Command>
</Popover>
);
});
TagInput.displayName = 'TagInput';
Expand Down
4 changes: 4 additions & 0 deletions apps/dashboard/src/components/primitives/variants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,3 +115,7 @@ export const stepVariants = cva(
},
}
);

export const inputVariants = cva(
'file:text-foreground placeholder:text-foreground-400 flex h-full w-full bg-transparent text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:outline-none disabled:cursor-not-allowed'
);
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { useBillingSubscription } from '@/hooks';
import { LogoCircle } from '../icons';
import { RiArrowRightDoubleLine, RiInformationFill } from 'react-icons/ri';
import { Progress } from '../primitives/progress';
import { Button } from '../primitives/button';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, TooltipArrow } from '../primitives/tooltip';
import { LEGACY_ROUTES } from '@/utils/routes';
import { useBillingSubscription } from '@/hooks/use-billing-subscription';

const transition = 'transition-all duration-300 ease-out';

Expand Down
1 change: 0 additions & 1 deletion apps/dashboard/src/components/side-navigation/index.ts

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ import { OrganizationDropdown } from './organization-dropdown';
import { FreeTrialCard } from './free-trial-card';
import { buildRoute, LEGACY_ROUTES, ROUTES } from '@/utils/routes';
import { SubscribersStayTunedModal } from './subscribers-stay-tuned-modal';
import { useTelemetry } from '@/hooks';
import { TelemetryEvent } from '@/utils/telemetry';
import { useTelemetry } from '@/hooks/use-telemetry';

const linkVariants = cva(
`flex items-center gap-2 text-sm py-1.5 px-2 rounded-lg focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring cursor-pointer`,
Expand Down
Loading

0 comments on commit e8fd24e

Please sign in to comment.