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): in-app editor form driven by BE schema #6877

Merged
merged 7 commits into from
Nov 7, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,33 +9,36 @@ const redirectSchema = {
url: {
type: 'string',
pattern: ABSOLUTE_AND_RELATIVE_URL_REGEX,
default: '',
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added the default values to the dataSchema, these are used to generate the default values for the form. The default values are required for all visible text fields by the react-hook-form, ref.

},
target: {
type: 'string',
enum: ['_self', '_blank', '_parent', '_top', '_unfencedTop'],
default: '_blank', // Default value for target
default: '_self', // Default value for target
},
},
required: ['url'], // url remains required
additionalProperties: false, // No additional properties allowed
default: { url: '', target: '_self' },
} as const satisfies JSONSchema;

const actionSchema = {
type: 'object',
properties: {
label: { type: 'string' },
label: { type: 'string', default: '' },
redirect: redirectSchema,
},
required: ['label'],
additionalProperties: false,
default: null,
} as const satisfies JSONSchema;

export const inAppControlSchema = {
type: 'object',
properties: {
subject: { type: 'string' },
body: { type: 'string' },
avatar: { type: 'string', format: 'uri' },
subject: { type: 'string', default: '' },
body: { type: 'string', default: '' },
avatar: { type: 'string', format: 'uri', default: '' },
primaryAction: actionSchema, // Nested primaryAction
secondaryAction: actionSchema, // Nested secondaryAction
data: { type: 'object', additionalProperties: true },
Expand Down
1 change: 1 addition & 0 deletions apps/dashboard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@
"devDependencies": {
"@clerk/types": "^4.6.1",
"@eslint/js": "^9.9.0",
"@hookform/devtools": "^4.3.0",
Copy link
Contributor Author

Choose a reason for hiding this comment

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

A really useful tool to debug the react-hook-form state

"@playwright/test": "^1.44.0",
"@sentry/vite-plugin": "^2.22.6",
"@types/lodash.debounce": "^4.0.9",
Expand Down
14 changes: 14 additions & 0 deletions apps/dashboard/src/api/steps.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { getV2 } from './api.client';
import type { StepDataDto } from '@novu/shared';

export const fetchStep = async ({
workflowSlug,
stepSlug,
}: {
workflowSlug: string;
stepSlug: string;
}): Promise<StepDataDto> => {
const { data } = await getV2<{ data: StepDataDto }>(`/workflows/${workflowSlug}/steps/${stepSlug}`);

return data;
};
24 changes: 12 additions & 12 deletions apps/dashboard/src/components/primitives/form/avatar-picker.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
'use client';

import { Avatar, AvatarImage } from '@/components/primitives/avatar';
import { Button } from '@/components/primitives/button';
import { FormControl, FormMessage } from '@/components/primitives/form/form';
import { FormMessage } from '@/components/primitives/form/form';
import { Input, InputField } from '@/components/primitives/input';
import { Label } from '@/components/primitives/label';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/primitives/popover';
import { Separator } from '@/components/primitives/separator';
import TextSeparator from '@/components/primitives/text-separator';
import { useState, forwardRef } from 'react';
import { RiEdit2Line, RiImageEditFill } from 'react-icons/ri';
import { RiEdit2Line, RiErrorWarningFill, RiImageEditFill } from 'react-icons/ri';
import { useFormField } from './form-context';

const predefinedAvatars = [
`${window.location.origin}/images/avatar.svg`,
Expand All @@ -30,6 +29,7 @@ type AvatarPickerProps = React.InputHTMLAttributes<HTMLInputElement>;

export const AvatarPicker = forwardRef<HTMLInputElement, AvatarPickerProps>(({ id, ...props }, ref) => {
const [isOpen, setIsOpen] = useState(false);
const { error } = useFormField();

const handlePredefinedAvatarClick = (url: string) => {
props.onChange?.({ target: { value: url } } as React.ChangeEvent<HTMLInputElement>);
Expand All @@ -40,14 +40,17 @@ export const AvatarPicker = forwardRef<HTMLInputElement, AvatarPickerProps>(({ i
<div className="space-y-2">
<Popover modal={true} open={isOpen} onOpenChange={setIsOpen}>
<PopoverTrigger asChild>
<Button variant="outline" size="icon" className="text-foreground-600 size-10">
<Button variant="outline" size="icon" className="text-foreground-600 relative size-10">
{props.value ? (
<Avatar>
<Avatar className="p-[1px]">
<AvatarImage src={props.value as string} />
LetItRock marked this conversation as resolved.
Show resolved Hide resolved
</Avatar>
) : (
<RiImageEditFill className="size-5" />
)}
{error && (
<RiErrorWarningFill className="text-destructive outline-destructive absolute right-0 top-0 size-3 -translate-y-1/2 translate-x-1/2 rounded-full outline outline-1 outline-offset-1" />
)}
Copy link
Contributor

Choose a reason for hiding this comment

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

You can do m-auto inset-0 to center this a bit cleaner i think.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm not sure I get it, we want to show the error icon in the top right corner of the avatar. Can you please clarify?

Copy link
Contributor

Choose a reason for hiding this comment

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

ah i thought you were centering. I think that you can do inset: 0 0 auto auto;

</Button>
Comment on lines +51 to +53
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 was missing the error icon

</PopoverTrigger>
<PopoverContent className="w-[300px] p-4">
Expand All @@ -59,11 +62,9 @@ export const AvatarPicker = forwardRef<HTMLInputElement, AvatarPickerProps>(({ i
<Separator />
<div className="space-y-1">
<Label>Avatar URL</Label>
<FormControl>
<InputField>
<Input type="url" id={id} placeholder="Enter avatar URL" ref={ref} {...props} />
</InputField>
</FormControl>
<InputField>
<Input type="url" id={id} placeholder="Enter avatar URL" ref={ref} {...props} />
</InputField>
<FormMessage />
</div>
</div>
Expand All @@ -80,7 +81,6 @@ export const AvatarPicker = forwardRef<HTMLInputElement, AvatarPickerProps>(({ i
</div>
</PopoverContent>
</Popover>
<FormMessage />
</div>
);
});
Expand Down
49 changes: 28 additions & 21 deletions apps/dashboard/src/components/primitives/form/form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,28 +101,35 @@ const formMessageVariants = cva('flex items-center gap-1', {
},
});

const FormMessage = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
({ className, children, ...props }, ref) => {
const { error, formMessageId } = useFormField();
const body = error ? String(error?.message) : children;
const FormMessagePure = React.forwardRef<
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The FormMessagePure is a label + icon that is not dependent on the FormField, it's used in a few cases.

HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement> & { error?: string }
>(({ className, children, error, id, ...props }, ref) => {
const body = error ? error : children;

if (!body) {
return null;
}

if (!body) {
return null;
}
return (
<p
ref={ref}
id={id}
className={formMessageVariants({ variant: error ? 'error' : 'default', className })}
{...props}
>
{error ? <RiErrorWarningFill className="size-4" /> : <RiInformationFill className="size-4" />}
<span className="mt-[1px] text-xs leading-3">{body}</span>
</p>
);
});
FormMessagePure.displayName = 'FormMessagePure';

return (
<p
ref={ref}
id={formMessageId}
className={formMessageVariants({ variant: error ? 'error' : 'default', className })}
{...props}
>
{error ? <RiErrorWarningFill className="size-4" /> : <RiInformationFill className="size-4" />}
<span className="mt-[1px] text-xs leading-3">{body}</span>
</p>
);
}
);
const FormMessage = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>((props, ref) => {
const { error, formMessageId } = useFormField();

return <FormMessagePure ref={ref} id={formMessageId} error={error ? String(error?.message) : undefined} {...props} />;
});
FormMessage.displayName = 'FormMessage';

export { Form, FormItem, FormLabel, FormControl, FormDescription, FormMessage, FormField };
export { Form, FormItem, FormLabel, FormControl, FormDescription, FormMessage, FormMessagePure, FormField };
2 changes: 1 addition & 1 deletion apps/dashboard/src/components/primitives/sonner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ const Toaster = ({ ...props }: ToasterProps) => {
toastOptions={{
classNames: {
toast:
'group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg text-foreground-950',
'group toast group-[.toaster]:bg-transparent group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg text-foreground-950',
description: 'group-[.toast]:text-foreground-600',
actionButton: 'group-[.toast]:bg-primary group-[.toast]:text-primary-foreground',
cancelButton: 'group-[.toast]:bg-muted group-[.toast]:text-muted-foreground',
Expand Down
69 changes: 0 additions & 69 deletions apps/dashboard/src/components/primitives/url-input.tsx

This file was deleted.

Loading
Loading