-
Notifications
You must be signed in to change notification settings - Fork 3.9k
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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`, | ||||||
|
@@ -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} /> | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
It's a string already so this isn't needed |
||||||
</Avatar> | ||||||
) : ( | ||||||
<RiImageEditFill className="size-5" /> | ||||||
|
@@ -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> | ||||||
|
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, | ||
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Moved to the |
||
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 ( | ||
<> | ||
|
@@ -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> | ||
); | ||
}; |
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 |
---|---|---|
@@ -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); |
There was a problem hiding this comment.
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