Skip to content

Commit

Permalink
feat(dashboard): in-app editor loading state (#7006)
Browse files Browse the repository at this point in the history
  • Loading branch information
LetItRock authored Nov 15, 2024
1 parent e5b5119 commit 60511c2
Show file tree
Hide file tree
Showing 16 changed files with 284 additions and 71 deletions.
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]);
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} />
</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]
);

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={true}>
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 tooltip="The redirect object defines the URL to visit when the notification is clicked. Alternatively, use an onNotificationClick handler in the <Inbox /> component.">
Expand All @@ -17,6 +24,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

0 comments on commit 60511c2

Please sign in to comment.