Skip to content

Commit

Permalink
fix(dashboard): align validation error messages across the application (
Browse files Browse the repository at this point in the history
  • Loading branch information
LetItRock authored Nov 20, 2024
1 parent 32b983b commit 913eef0
Show file tree
Hide file tree
Showing 13 changed files with 77 additions and 58 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export const EditBridgeUrlButton = () => {
handleSubmit,
reset,
setError,
formState: { isDirty, errors },
formState: { isDirty },
} = form;
const { currentEnvironment, setBridgeUrl } = useEnvironment();
const { status, bridgeURL: envBridgeUrl } = useBridgeHealthCheck();
Expand Down Expand Up @@ -86,7 +86,7 @@ export const EditBridgeUrlButton = () => {
<FormItem>
<FormLabel>Bridge Endpoint URL</FormLabel>
<FormControl>
<InputField state={errors.bridgeUrl?.message ? 'error' : 'default'}>
<InputField>
<RiLinkM className="size-5 min-w-5" />
<Input id="bridgeUrl" {...field} />
</InputField>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ export const AvatarPicker = forwardRef<HTMLInputElement, AvatarPickerProps>(({ n
<Separator />
<div className="space-y-1">
<Label>Avatar URL</Label>
<InputField className="px-1" state={error ? 'error' : 'default'}>
<InputField className="px-1">
<Editor
fontFamily="inherit"
ref={ref}
Expand Down
29 changes: 21 additions & 8 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 { useFormField } from './form/form-context';

export const inputVariants = cva(
'file:text-foreground placeholder:text-foreground-400 flex h-full w-full bg-transparent text-xs file:border-0 file:bg-transparent file:font-medium focus-visible:outline-none disabled:cursor-not-allowed'
Expand Down Expand Up @@ -59,16 +60,28 @@ const inputFieldVariants = cva(
}
);

export type InputFieldProps = { children: React.ReactNode; className?: string } & VariantProps<
export type InputFieldPureProps = { children: React.ReactNode; className?: string } & VariantProps<
typeof inputFieldVariants
>;

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

InputFieldPure.displayName = 'InputFieldPure';

export type InputFieldProps = Omit<InputFieldPureProps, 'state'>;

const InputField = React.forwardRef<HTMLInputElement, InputFieldProps>(({ ...props }, ref) => {
const { error } = useFormField();

return <InputFieldPure ref={ref} {...props} state={error?.message ? 'error' : 'default'} />;
});

InputField.displayName = 'InputField';
Expand All @@ -80,4 +93,4 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(({ className, type,
});
Input.displayName = 'Input';

export { Input, InputField };
export { Input, InputField, InputFieldPure };
7 changes: 5 additions & 2 deletions apps/dashboard/src/components/primitives/tag-input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@ const TagInput = forwardRef<HTMLInputElement, TagInputProps>((props, ref) => {
const [tags, setTags] = useState<string[]>(value);
const [inputValue, setInputValue] = useState('');
const [isOpen, setIsOpen] = useState(false);
const validSuggestions = useMemo(() => suggestions.filter((suggestion) => !tags.includes(suggestion)), [tags]);
const validSuggestions = useMemo(
() => suggestions.filter((suggestion) => !tags.includes(suggestion)),
[tags, suggestions]
);

useEffect(() => {
setTags(value);
Expand Down Expand Up @@ -55,7 +58,7 @@ const TagInput = forwardRef<HTMLInputElement, TagInputProps>((props, ref) => {
return (
<Popover open={isOpen}>
<Command loop>
<div className="flex flex-col gap-2">
<div className="flex flex-col gap-2 pb-0.5">
<PopoverAnchor asChild>
<CommandInput
ref={ref}
Expand Down
15 changes: 13 additions & 2 deletions apps/dashboard/src/components/primitives/textarea.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 { useFormField } from './form/form-context';

const textareaVariants = cva(
'text-foreground-950 flex text-sm w-full flex-nowrap items-center min-h-[60px] gap-1.5 rounded-md border bg-transparent transition-colors focus-within:outline-none focus-visible:outline-none hover:bg-neutral-alpha-50 disabled:cursor-not-allowed disabled:opacity-50 has-[value=""]:text-foreground-400 disabled:bg-neutral-alpha-100 disabled:text-foreground-300',
Expand All @@ -24,9 +25,10 @@ const textareaVariants = cva(
}
);

export type TextareaProps = React.TextareaHTMLAttributes<HTMLTextAreaElement> & VariantProps<typeof textareaVariants>;
export type TextareaPureProps = React.TextareaHTMLAttributes<HTMLTextAreaElement> &
VariantProps<typeof textareaVariants>;

const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
const TextareaPure = React.forwardRef<HTMLTextAreaElement, TextareaPureProps>(
({ className, state, size, maxLength, ...props }, ref) => {
return (
<>
Expand All @@ -45,6 +47,15 @@ const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
);
}
);
TextareaPure.displayName = 'TextareaPure';

export type TextareaProps = Omit<TextareaPureProps, 'state'>;

const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(({ ...props }, ref) => {
const { error } = useFormField();

return <TextareaPure ref={ref} {...props} state={error?.message ? 'error' : 'default'} />;
});
Textarea.displayName = 'Textarea';

export { Textarea };
9 changes: 4 additions & 5 deletions apps/dashboard/src/components/workflow-editor/schema.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import * as z from 'zod';
import type { JSONSchemaDefinition } from '@novu/shared';
import { StepTypeEnum } from '@/utils/enums';
import { capitalize } from '@/utils/string';

const enabledSchema = z.object({
enabled: z.boolean(),
Expand Down Expand Up @@ -90,12 +89,12 @@ export const buildDynamicFormSchema = ({
const isRequired = requiredFields.includes(key);
let zodValue: z.ZodString | z.ZodNumber | z.ZodOptional<z.ZodString | z.ZodNumber>;
if (value.type === 'string') {
zodValue = z.string().min(1, `${capitalize(key)} is required`);
zodValue = z.string().min(1);
if (value.format === 'email') {
zodValue = zodValue.email(`${capitalize(key)} must be a valid email`);
zodValue = zodValue.email();
}
} else {
zodValue = z.number().min(1, `${capitalize(key)} is required`);
zodValue = z.number().min(1);
}
if (!isRequired) {
zodValue = zodValue.optional();
Expand All @@ -114,7 +113,7 @@ export const buildDynamicFormSchema = ({
try {
return JSON.parse(str);
} catch (e) {
ctx.addIssue({ code: 'custom', message: 'Invalid payload. Payload needs to be a valid JSON.' });
ctx.addIssue({ code: 'custom', message: 'Payload must be valid JSON' });
return z.NEVER;
}
}),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,7 @@ import { useFormContext } from 'react-hook-form';

export function TextWidget(props: WidgetProps) {
const { label, readonly, name } = props;

const {
control,
formState: { errors },
} = useFormContext();
const { control } = useFormContext();

return (
<FormField
Expand All @@ -23,7 +19,7 @@ export function TextWidget(props: WidgetProps) {
<FormItem className="my-2 w-full py-1">
<FormLabel>{capitalize(label)}</FormLabel>
<FormControl>
<InputField className="px-1" state={errors[name] ? 'error' : 'default'}>
<InputField className="px-1">
<Editor
fontFamily="inherit"
placeholder={capitalize(label)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,7 @@ import { useStepEditorContext } from '../hooks';
const bodyKey = 'body';

export const InAppBody = () => {
const {
control,
formState: { errors },
} = useFormContext();
const { control } = useFormContext();
const { step } = useStepEditorContext();
const variables = useMemo(() => (step ? parseStepVariablesToLiquidVariables(step.variables) : []), [step]);

Expand All @@ -28,7 +25,7 @@ export const InAppBody = () => {
render={({ field }) => (
<FormItem className="w-full">
<FormControl>
<InputField className="h-36 px-1" state={errors[bodyKey] ? 'error' : 'default'}>
<InputField className="h-36 px-1">
<Editor
fontFamily="inherit"
placeholder={capitalize(field.name)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,7 @@ import { useStepEditorContext } from '../hooks';
const subjectKey = 'subject';

export const InAppSubject = () => {
const {
control,
formState: { errors },
} = useFormContext();
const { control } = useFormContext();
const { step } = useStepEditorContext();
const variables = useMemo(() => (step ? parseStepVariablesToLiquidVariables(step.variables) : []), [step]);

Expand All @@ -26,7 +23,7 @@ export const InAppSubject = () => {
control={control}
name={subjectKey}
render={({ field }) => (
<InputField state={errors[subjectKey] ? 'error' : 'default'} size="fit">
<InputField size="fit">
<FormItem className="w-full">
<FormControl>
<Editor
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,7 @@ const LANGUAGE_TO_SNIPPET_UTIL: Record<SnippetLanguage, (props: CodeSnippet) =>
};

export const TestWorkflowForm = ({ workflow }: { workflow?: WorkflowResponseDto }) => {
const {
control,
formState: { errors },
} = useFormContext<TestWorkflowFormType>();
const { control } = useFormContext<TestWorkflowFormType>();
const [activeSnippetTab, setActiveSnippetTab] = useState<SnippetLanguage>(() =>
workflow?.origin === WorkflowOriginEnum.EXTERNAL ? 'framework' : 'typescript'
);
Expand Down Expand Up @@ -72,7 +69,7 @@ export const TestWorkflowForm = ({ workflow }: { workflow?: WorkflowResponseDto
<FormItem>
<FormLabel htmlFor={key}>{capitalize(key)}</FormLabel>
<FormControl>
<InputField state={errors.to?.[key] ? 'error' : 'default'}>
<InputField>
<Input id={key} {...(field as any)} />
</InputField>
</FormControl>
Expand Down
6 changes: 3 additions & 3 deletions apps/dashboard/src/components/workflow-editor/url-input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { useFormContext } from 'react-hook-form';

import { Editor } from '@/components/primitives/editor';
import { FormControl, FormField, FormItem, FormMessagePure } from '@/components/primitives/form/form';
import { Input, InputField, InputFieldProps, InputProps } from '@/components/primitives/input';
import { Input, InputFieldProps, InputFieldPure, InputProps } from '@/components/primitives/input';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/primitives/select';
import { completions } from '@/utils/liquid-autocomplete';
import { LiquidVariable } from '@/utils/parseStepVariablesToLiquidVariables';
Expand Down Expand Up @@ -36,7 +36,7 @@ export const URLInput = ({
<div className="flex flex-col gap-1">
<div className="flex items-center justify-between space-x-2">
<div className="relative w-full">
<InputField className="pr-0">
<InputFieldPure className="pr-0">
<FormField
control={control}
name={urlKey}
Expand Down Expand Up @@ -80,7 +80,7 @@ export const URLInput = ({
</FormItem>
)}
/>
</InputField>
</InputFieldPure>
</div>
</div>
<FormMessagePure error={error ? String(error.message) : undefined}>
Expand Down
17 changes: 5 additions & 12 deletions apps/dashboard/src/utils/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ type ZodValue =

const handleStringFormat = ({ value, key, format }: { value: z.ZodString; key: string; format: string }) => {
if (format === 'email') {
return value.email(`${capitalize(key)} must be a valid email`);
return value.email();
} else if (format === 'uri') {
return value
.transform((val) => (val === '' ? undefined : val))
Expand All @@ -35,10 +35,6 @@ const handleStringPattern = ({ value, key, pattern }: { value: z.ZodString; key:
});
};

const handleStringEnum = ({ key, enumValues }: { key: string; enumValues: [string, ...string[]] }) => {
return z.enum(enumValues, { message: `${capitalize(key)} must be one of ${enumValues.join(', ')}` });
};

const handleStringType = ({
key,
format,
Expand Down Expand Up @@ -75,12 +71,9 @@ const handleStringType = ({
pattern,
});
} else if (enumValues) {
stringValue = handleStringEnum({
key,
enumValues: enumValues as [string, ...string[]],
});
stringValue = z.enum(enumValues as [string, ...string[]]);
} else if (isRequired) {
stringValue = stringValue.min(1, `${capitalize(key)} is missing`);
stringValue = stringValue.min(1);
}

if (defaultValue) {
Expand Down Expand Up @@ -123,9 +116,9 @@ export const buildDynamicZodSchema = (obj: JSONSchemaDto): z.AnyZodObject => {
} else if (type === 'string') {
zodValue = handleStringType({ key, requiredFields, format, pattern, enumValues, defaultValue });
} else if (type === 'boolean') {
zodValue = z.boolean(isRequired ? { message: `${capitalize(key)} is missing` } : undefined);
zodValue = z.boolean();
} else {
zodValue = z.number(isRequired ? { message: `${capitalize(key)} is missing` } : undefined);
zodValue = z.number();
if (defaultValue) {
zodValue = zodValue.default(defaultValue as number);
}
Expand Down
17 changes: 15 additions & 2 deletions apps/dashboard/src/utils/validation.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { z } from 'zod';
import { capitalize } from './string';

const getIssueField = (issue: z.ZodIssueBase) => capitalize(issue.path.join(' '));
const getIssueField = (issue: z.ZodIssueBase) => capitalize(`${issue.path[issue.path.length - 1]}`);
const pluralize = (count: number | bigint) => (count === 1 ? '' : 's');

/**
Expand All @@ -13,6 +13,7 @@ const pluralize = (count: number | bigint) => (count === 1 ? '' : 's');
*/
const customErrorMap: z.ZodErrorMap = (issue, ctx) => {
const issueField = getIssueField(issue);

if (issue.code === z.ZodIssueCode.too_big) {
if (issue.type === 'array') {
return {
Expand All @@ -21,7 +22,6 @@ const customErrorMap: z.ZodErrorMap = (issue, ctx) => {
}

if (issue.type === 'string') {
console.log({ issue });
return {
message: `${issueField} must be at most ${issue.maximum} character${pluralize(issue.maximum)}`,
};
Expand All @@ -39,6 +39,7 @@ const customErrorMap: z.ZodErrorMap = (issue, ctx) => {
};
}
}

if (issue.code === z.ZodIssueCode.too_small) {
if (issue.type === 'array') {
return {
Expand Down Expand Up @@ -70,6 +71,18 @@ const customErrorMap: z.ZodErrorMap = (issue, ctx) => {
};
}
}

if (issue.code === z.ZodIssueCode.invalid_string && issue.validation === 'email') {
return {
message: `${issueField} must be a valid email`,
};
}

if (issue.code === z.ZodIssueCode.invalid_enum_value) {
return {
message: `${issueField} must be one of ${issue.options.join(', ')}`,
};
}
return { message: ctx.defaultError };
};

Expand Down

0 comments on commit 913eef0

Please sign in to comment.