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 loading state #7006

Merged
merged 2 commits into from
Nov 15, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
46 changes: 35 additions & 11 deletions apps/dashboard/src/components/primitives/form/avatar-picker.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
import { useState, forwardRef, useMemo } from 'react';
import { liquid } from '@codemirror/lang-liquid';
import { EditorView } from '@uiw/react-codemirror';
import { RiEdit2Line, RiErrorWarningFill, RiImageEditFill } from 'react-icons/ri';

import { Avatar, AvatarImage } from '@/components/primitives/avatar';
import { Button } from '@/components/primitives/button';
import { FormMessage } from '@/components/primitives/form/form';
import { Input, InputField } from '@/components/primitives/input';
import { 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, RiErrorWarningFill, RiImageEditFill } from 'react-icons/ri';
import { useFormField } from './form-context';
import { Editor } from '../editor';
import { useStepEditorContext } from '@/components/workflow-editor/steps/hooks';
import { parseStepVariablesToLiquidVariables } from '@/utils/parseStepVariablesToLiquidVariables';

const predefinedAvatars = [
`${window.location.origin}/images/avatar.svg`,
Expand All @@ -25,25 +31,31 @@ const predefinedAvatars = [
`${window.location.origin}/images/error-warning.svg`,
];

type AvatarPickerProps = React.InputHTMLAttributes<HTMLInputElement>;
type AvatarPickerProps = {
name: string;
value: string;
onChange?: (value: string) => void;
};

export const AvatarPicker = forwardRef<HTMLInputElement, AvatarPickerProps>(({ id, ...props }, ref) => {
export const AvatarPicker = forwardRef<HTMLInputElement, AvatarPickerProps>(({ name, value, onChange }, ref) => {
const { step } = useStepEditorContext();
const variables = useMemo(() => (step ? parseStepVariablesToLiquidVariables(step.variables) : []), [step]);
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 component was missing variables and the CodeMirror editor field for the avatar URL

const [isOpen, setIsOpen] = useState(false);
const { error } = useFormField();

const handlePredefinedAvatarClick = (url: string) => {
props.onChange?.({ target: { value: url } } as React.ChangeEvent<HTMLInputElement>);
onChange?.(url);
setIsOpen(false);
};

return (
<div className="space-y-2">
<div className="size-10 space-y-2">
<Popover modal={true} open={isOpen} onOpenChange={setIsOpen}>
<PopoverTrigger asChild>
<Button variant="outline" size="icon" className="text-foreground-600 relative size-10">
{props.value ? (
{value ? (
<Avatar className="p-px">
<AvatarImage src={props.value as string} />
<AvatarImage src={value as string} />
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
<AvatarImage src={value as string} />
<AvatarImage src={value} />

It's a string already so this isn't needed

</Avatar>
) : (
<RiImageEditFill className="size-5" />
Expand All @@ -62,8 +74,20 @@ export const AvatarPicker = forwardRef<HTMLInputElement, AvatarPickerProps>(({ i
<Separator />
<div className="space-y-1">
<Label>Avatar URL</Label>
<InputField>
<Input type="url" id={id} placeholder="Enter avatar URL" ref={ref} {...props} />
<InputField className="px-1" state={error ? 'error' : 'default'}>
<Editor
ref={ref}
placeholder="Enter avatar URL"
id={name}
extensions={[
liquid({
variables,
}),
EditorView.lineWrapping,
]}
value={`${value}`}
onChange={(newValue) => onChange?.(newValue)}
/>
</InputField>
<FormMessage />
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export const ConfigureStepContent = () => {
</SidebarContent>
<Separator />
<SidebarContent>
<Link to={'./edit'} relative="path">
<Link to={'./edit'} relative="path" state={{ stepType: step.type }}>
<Button variant="outline" className="flex w-full justify-start gap-1.5 text-xs font-medium" type="button">
<RiPencilRuler2Fill className="h-4 w-4 text-neutral-600" />
Configure in-app template <RiArrowRightSLine className="ml-auto h-4 w-4 text-neutral-600" />
Expand All @@ -47,7 +47,7 @@ export const ConfigureStepContent = () => {
<span>Help?</span>
</Link>
</div>
<Link to={'./edit'} relative="path">
<Link to={'./edit'} relative="path" state={{ stepType: step.type }}>
<Button variant="outline" className="flex w-full justify-start gap-1.5 text-xs font-medium" type="button">
<span className="bg-destructive h-4 min-w-1 rounded-full" />
<span className="overflow-hidden text-ellipsis">{firstError}</span>
Expand All @@ -69,7 +69,7 @@ export const ConfigureStepContent = () => {
{!EXCLUDED_EDITOR_TYPES.includes(step?.type ?? '') && (
<>
<SidebarContent>
<Link to={'./edit'} relative="path">
<Link to={'./edit'} relative="path" state={{ stepType: step?.type }}>
<Button variant="outline" className="flex w-full justify-start gap-1.5 text-xs font-medium" type="button">
<RiPencilRuler2Fill className="h-4 w-4 text-neutral-600" />
Configure {step?.type} template <RiArrowRightSLine className="ml-auto h-4 w-4 text-neutral-600" />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { useEffect, useMemo } from 'react';
import { motion } from 'framer-motion';
import { useNavigate, useParams } from 'react-router-dom';
import { useNavigate } from 'react-router-dom';

import {
Sheet,
Expand All @@ -10,45 +9,25 @@ import {
SheetPortal,
SheetTitle,
} from '@/components/primitives/sheet';
import { useFetchWorkflow } from '@/hooks/use-fetch-workflow';
import { StepEditor } from '@/components/workflow-editor/steps/step-editor';
import { useFetchStep } from '@/hooks/use-fetch-step';
import { VisuallyHidden } from '@/components/primitives/visually-hidden';
import { PageMeta } from '@/components/page-meta';
import { getStepBase62Id } from '@/utils/step';
import { EXCLUDED_EDITOR_TYPES } from '@/utils/constants';
import { StepSkeleton } from './step-skeleton';
import { StepEditorProvider } from './step-editor-provider';
import { useStepEditorContext } from './hooks';
import { useWorkflowEditorContext } from '../hooks';

const transitionSetting = { ease: [0.29, 0.83, 0.57, 0.99], duration: 0.4 };

export const EditStepSidebar = () => {
const { workflowSlug = '', stepSlug = '' } = useParams<{ workflowSlug: string; stepSlug: string }>();
const EditStepSidebarInternal = () => {
const navigate = useNavigate();

const { workflow } = useFetchWorkflow({
workflowSlug,
});

const { step } = useFetchStep({ workflowSlug, stepSlug });
const stepType = useMemo(
() => workflow?.steps.find((el) => getStepBase62Id(el.slug) === getStepBase62Id(stepSlug))?.type,
[stepSlug, workflow]
);

Comment on lines -24 to -35
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Moved to the StepEditorProvider component which shares the step across the components.
I decided to do that because, in a few places, we were fetching the step with the useFetchStep hook, which on mount fetches the data from API, triggering the loading skeleton to be shown, and it was infinite because the skeleton was in the parent.

const { workflow, isPendingWorkflow } = useWorkflowEditorContext();
const { step, stepType, isPendingStep, isRefetchingStep } = useStepEditorContext();
const handleCloseSidebar = () => {
navigate('..', { relative: 'path' });
};

const isNotSupportedEditorType = EXCLUDED_EDITOR_TYPES.includes(stepType ?? '');

useEffect(() => {
if (isNotSupportedEditorType) {
navigate('..', { relative: 'path' });
}
}, [isNotSupportedEditorType, navigate]);

if (isNotSupportedEditorType) {
return null;
}
const isPending = isPendingWorkflow || isPendingStep || isRefetchingStep;

return (
<>
Expand Down Expand Up @@ -89,12 +68,25 @@ export const EditStepSidebar = () => {
<SheetTitle />
<SheetDescription />
</VisuallyHidden>
{/* TODO: show loading indicator */}
{workflow && step && stepType && <StepEditor workflow={workflow} step={step} stepType={stepType} />}
{isPending ? (
<StepSkeleton stepType={stepType} workflowOrigin={workflow?.origin} />
) : (
<>
{workflow && step && stepType && <StepEditor workflow={workflow} step={step} stepType={stepType} />}
</>
)}
</motion.div>
</SheetContentBase>
</SheetPortal>
</Sheet>
</>
);
};

export const EditStepSidebar = () => {
return (
<StepEditorProvider>
<EditStepSidebarInternal />
</StepEditorProvider>
);
};
4 changes: 4 additions & 0 deletions apps/dashboard/src/components/workflow-editor/steps/hooks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { createContextHook } from '@/utils/context';
import { StepEditorContext } from './step-editor-context';

export const useStepEditorContext = createContextHook(StepEditorContext);
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ComponentProps } from 'react';
import { ComponentProps, useMemo } from 'react';
import { useFormContext, useWatch } from 'react-hook-form';
import { RiEdit2Line, RiExpandUpDownLine, RiForbid2Line } from 'react-icons/ri';
import { liquid } from '@codemirror/lang-liquid';
Expand Down Expand Up @@ -26,6 +26,8 @@ import { URLInput } from '@/components/workflow-editor/url-input';
import { cn } from '@/utils/ui';
import { urlTargetTypes } from '@/utils/url';
import { Editor } from '@/components/primitives/editor';
import { parseStepVariablesToLiquidVariables } from '@/utils/parseStepVariablesToLiquidVariables';
import { useStepEditorContext } from '../hooks';

const primaryActionKey = 'primaryAction';
const secondaryActionKey = 'secondaryAction';
Expand Down Expand Up @@ -146,6 +148,8 @@ const ConfigureActionPopover = (props: ComponentProps<typeof PopoverTrigger> & {
...rest
} = props;
const { control } = useFormContext();
const { step } = useStepEditorContext();
const variables = useMemo(() => (step ? parseStepVariablesToLiquidVariables(step.variables) : []), [step]);

return (
<Popover modal={false}>
Expand Down Expand Up @@ -174,7 +178,7 @@ const ConfigureActionPopover = (props: ComponentProps<typeof PopoverTrigger> & {
height="30px"
extensions={[
liquid({
variables: [{ type: 'variable', label: 'asdf' }],
variables,
}),
EditorView.lineWrapping,
]}
Expand All @@ -195,6 +199,7 @@ const ConfigureActionPopover = (props: ComponentProps<typeof PopoverTrigger> & {
targetKey: `${actionKey}.redirect.target`,
}}
withHint={false}
variables={variables}
/>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { useMemo } from 'react';
import { liquid } from '@codemirror/lang-liquid';
import { EditorView } from '@uiw/react-codemirror';
import { useFormContext } from 'react-hook-form';

import { Editor } from '@/components/primitives/editor';
import { FormControl, FormField, FormItem, FormMessage } from '@/components/primitives/form/form';
import { InputField } from '@/components/primitives/input';
import { useFetchStep } from '@/hooks/use-fetch-step';
import { parseStepVariablesToLiquidVariables } from '@/utils/parseStepVariablesToLiquidVariables';
import { capitalize } from '@/utils/string';
import { useParams } from 'react-router-dom';
import { useStepEditorContext } from '../hooks';

const bodyKey = 'body';

Expand All @@ -17,10 +17,8 @@ export const InAppBody = () => {
control,
formState: { errors },
} = useFormContext();

const { workflowSlug = '', stepSlug = '' } = useParams<{ workflowSlug: string; stepSlug: string }>();

const { step } = useFetchStep({ workflowSlug, stepSlug });
const { step } = useStepEditorContext();
const variables = useMemo(() => (step ? parseStepVariablesToLiquidVariables(step.variables) : []), [step]);

return (
<FormField
Expand All @@ -36,7 +34,7 @@ export const InAppBody = () => {
id={field.name}
extensions={[
liquid({
variables: step ? parseStepVariablesToLiquidVariables(step.variables) : [],
variables,
}),
EditorView.lineWrapping,
]}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
import { useMemo } from 'react';

import { FormLabel } from '@/components/primitives/form/form';
import { URLInput } from '../../url-input';
import { urlTargetTypes } from '@/utils/url';
import { useStepEditorContext } from '../hooks';
import { parseStepVariablesToLiquidVariables } from '@/utils/parseStepVariablesToLiquidVariables';

export const InAppRedirect = () => {
const { step } = useStepEditorContext();
const variables = useMemo(() => (step ? parseStepVariablesToLiquidVariables(step.variables) : []), [step]);

return (
<div className="flex flex-col gap-1">
<FormLabel hint="">Redirect URL</FormLabel>
Expand All @@ -15,6 +22,7 @@ export const InAppRedirect = () => {
urlKey: 'redirect.url',
targetKey: 'redirect.target',
}}
variables={variables}
/>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { useMemo } from 'react';
import { useFormContext } from 'react-hook-form';
import { liquid } from '@codemirror/lang-liquid';
import { EditorView } from '@uiw/react-codemirror';
Expand All @@ -6,9 +7,8 @@ import { FormControl, FormField, FormItem, FormMessage } from '@/components/prim
import { InputField } from '@/components/primitives/input';
import { Editor } from '@/components/primitives/editor';
import { capitalize } from '@/utils/string';
import { useParams } from 'react-router-dom';
import { useFetchStep } from '@/hooks/use-fetch-step';
import { parseStepVariablesToLiquidVariables } from '@/utils/parseStepVariablesToLiquidVariables';
import { useStepEditorContext } from '../hooks';

const subjectKey = 'subject';

Expand All @@ -17,9 +17,8 @@ export const InAppSubject = () => {
control,
formState: { errors },
} = useFormContext();
const { workflowSlug = '', stepSlug = '' } = useParams<{ workflowSlug: string; stepSlug: string }>();

const { step } = useFetchStep({ workflowSlug, stepSlug });
const { step } = useStepEditorContext();
const variables = useMemo(() => (step ? parseStepVariablesToLiquidVariables(step.variables) : []), [step]);

return (
<FormField
Expand All @@ -35,7 +34,7 @@ export const InAppSubject = () => {
id={field.name}
extensions={[
liquid({
variables: step ? parseStepVariablesToLiquidVariables(step.variables) : [],
variables,
}),
EditorView.lineWrapping,
]}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { createContext } from 'react';
import { type StepDataDto, StepTypeEnum } from '@novu/shared';

export type StepEditorContextType = {
isPendingStep: boolean;
isRefetchingStep: boolean;
step?: StepDataDto;
stepType?: StepTypeEnum;
};

export const StepEditorContext = createContext<StepEditorContextType>({} as StepEditorContextType);
Loading
Loading