-
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): Nv 4800 implement arrays and nested forms in custom step controls #7127
base: next
Are you sure you want to change the base?
Changes from all commits
4975230
10c8732
cda0d5f
f97d576
9eb0921
af3939b
59a19a1
74774b9
9202906
d7557d7
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 |
---|---|---|
@@ -0,0 +1,27 @@ | ||
import { ArrayFieldTemplateItemType } from '@rjsf/utils'; | ||
import { cn } from '@/utils/ui'; | ||
import { RemoveButton } from './button-templates'; | ||
|
||
export const ArrayFieldItemTemplate = (props: ArrayFieldTemplateItemType) => { | ||
const isChildObjectType = props.schema.type === 'object'; | ||
|
||
return ( | ||
<div className="relative flex items-center gap-2 *:flex-1"> | ||
{props.children} | ||
<div | ||
className={cn( | ||
'bg-background absolute right-0 top-0 z-10 mt-2 flex w-5 -translate-y-1/2 items-center justify-end', | ||
{ 'right-4 justify-start': isChildObjectType } | ||
)} | ||
> | ||
{props.hasRemove && ( | ||
<RemoveButton | ||
disabled={props.disabled || props.readonly} | ||
onClick={(e) => props.onDropIndexClick(props.index)(e)} | ||
registry={props.registry} | ||
/> | ||
)} | ||
</div> | ||
</div> | ||
); | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
import { CollapsibleContent, CollapsibleTrigger } from '@/components/primitives/collapsible'; | ||
import { Collapsible } from '@radix-ui/react-collapsible'; | ||
import { ArrayFieldTemplateProps, getTemplate, getUiOptions } from '@rjsf/utils'; | ||
import { useMemo, useState } from 'react'; | ||
import { useFieldArray, useFormContext } from 'react-hook-form'; | ||
import { RiExpandUpDownLine } from 'react-icons/ri'; | ||
import { getFieldName } from './template-utils'; | ||
|
||
export function ArrayFieldTemplate(props: ArrayFieldTemplateProps) { | ||
const { disabled, idSchema, uiSchema, items, onAddClick, readonly, registry, required, title, schema, canAdd } = | ||
props; | ||
const { | ||
ButtonTemplates: { AddButton }, | ||
} = registry.templates; | ||
const uiOptions = useMemo(() => getUiOptions(uiSchema), [uiSchema]); | ||
const ArrayFieldTitleTemplate = useMemo( | ||
() => getTemplate('ArrayFieldTitleTemplate', registry, uiOptions), | ||
[registry, uiOptions] | ||
); | ||
const ArrayFieldItemTemplate = useMemo( | ||
() => getTemplate('ArrayFieldItemTemplate', registry, uiOptions), | ||
[registry, uiOptions] | ||
); | ||
|
||
const [isEditorOpen, setIsEditorOpen] = useState(true); | ||
|
||
const handleAddClick = () => { | ||
if (!isEditorOpen) { | ||
setIsEditorOpen(true); | ||
} | ||
onAddClick(); | ||
/** | ||
* If the array field has a default value, append it to the array | ||
*/ | ||
const defaultValue = schema.default ?? undefined; | ||
const value = Array.isArray(defaultValue) ? defaultValue[0] : defaultValue; | ||
append(value); | ||
}; | ||
|
||
const { control } = useFormContext(); | ||
const extractedName = useMemo(() => getFieldName(idSchema.$id), [idSchema.$id]); | ||
|
||
const { append, remove } = useFieldArray({ | ||
control, | ||
name: extractedName, | ||
}); | ||
|
||
return ( | ||
<Collapsible | ||
open={isEditorOpen} | ||
onOpenChange={setIsEditorOpen} | ||
className="bg-background border-neutral-alpha-200 relative mt-2 flex w-full flex-col gap-2 rounded-lg border px-3 py-4 data-[state=closed]:rounded-none data-[state=closed]:border-b-0 data-[state=closed]:border-l-0 data-[state=closed]:border-r-0 data-[state=closed]:border-t data-[state=closed]:pb-0" | ||
> | ||
<div className="absolute left-0 top-0 z-10 flex w-full -translate-y-1/2 items-center justify-between p-0 px-2 text-sm"> | ||
<div className="flex w-full items-center gap-1"> | ||
<span className="bg-background px-1"> | ||
<ArrayFieldTitleTemplate | ||
idSchema={idSchema} | ||
title={uiOptions.title || title} | ||
schema={schema} | ||
uiSchema={uiSchema} | ||
required={required} | ||
registry={registry} | ||
/> | ||
</span> | ||
<div className="bg-background text-foreground-600 -mt-px ml-auto mr-4 flex items-center gap-1"> | ||
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. Could we add a hover state to the collapsible button to be consistent with the add/minus button hover states? It's generally a good UX practice for all buttons to have a hover state to provide feedback to the user that they are interacting with something (in addition to varying the pointer) |
||
{canAdd && <AddButton onClick={handleAddClick} disabled={disabled || readonly} registry={registry} />} | ||
<CollapsibleTrigger> | ||
<RiExpandUpDownLine className="text-foreground-600 size-3" /> | ||
</CollapsibleTrigger> | ||
</div> | ||
</div> | ||
</div> | ||
|
||
<CollapsibleContent className="flex flex-col gap-3"> | ||
{items.map(({ key, onDropIndexClick, ...itemProps }) => { | ||
return ( | ||
<ArrayFieldItemTemplate | ||
key={key} | ||
{...itemProps} | ||
onDropIndexClick={(index) => { | ||
remove(index); | ||
return onDropIndexClick(index); | ||
}} | ||
/> | ||
); | ||
})} | ||
</CollapsibleContent> | ||
</Collapsible> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
import { ArrayFieldTitleProps } from '@rjsf/utils'; | ||
|
||
export const ArrayFieldTitleTemplate = (props: ArrayFieldTitleProps) => { | ||
return <legend className="text-foreground-400 px-1 text-xs">{props.title}</legend>; | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
import { RiAddLine, RiSubtractFill } from 'react-icons/ri'; | ||
import { IconButtonProps } from '@rjsf/utils'; | ||
import { Button } from '@/components/primitives/button'; | ||
|
||
export const AddButton = (props: IconButtonProps) => { | ||
return ( | ||
<Button variant="ghost" className="size-4 rounded-sm p-0.5" type="button" {...props} title="Add item"> | ||
<RiAddLine className="text-foreground-600 size-3" /> | ||
</Button> | ||
); | ||
}; | ||
|
||
export const RemoveButton = (props: IconButtonProps) => { | ||
return ( | ||
<Button variant="ghost" className="size-4 rounded-sm p-0.5" type="button" {...props} title="Remove item"> | ||
<RiSubtractFill className="text-foreground-600 size-3" /> | ||
</Button> | ||
); | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
import { useMemo, useState } from 'react'; | ||
import { RiExpandUpDownLine } from 'react-icons/ri'; | ||
import { useFormContext } from 'react-hook-form'; | ||
import { getTemplate, getUiOptions, ObjectFieldTemplateProps } from '@rjsf/utils'; | ||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/primitives/collapsible'; | ||
import { getFieldName, ROOT_DELIMITER } from './template-utils'; | ||
import { FormControl, FormField, FormItem, FormMessage } from '@/components/primitives/form/form'; | ||
|
||
export function ObjectFieldTemplate(props: ObjectFieldTemplateProps) { | ||
const { idSchema, uiSchema, registry, required, title, schema, properties } = props; | ||
|
||
const uiOptions = getUiOptions(uiSchema); | ||
|
||
const ArrayFieldTitleTemplate = getTemplate('ArrayFieldTitleTemplate', registry, uiOptions); | ||
|
||
const [isEditorOpen, setIsEditorOpen] = useState(true); | ||
|
||
const sectionTitle = uiOptions.title || title; | ||
|
||
const { control } = useFormContext(); | ||
const extractedName = useMemo(() => getFieldName(idSchema.$id) + '.' + ROOT_DELIMITER, [idSchema.$id]); | ||
|
||
if (!sectionTitle) { | ||
return properties.map((element) => { | ||
return <div key={element.name}>{element.content}</div>; | ||
}); | ||
} | ||
|
||
return ( | ||
<FormField | ||
control={control} | ||
name={extractedName} | ||
render={() => ( | ||
<FormItem> | ||
<FormControl> | ||
<Collapsible | ||
open={isEditorOpen} | ||
onOpenChange={setIsEditorOpen} | ||
className="bg-background border-neutral-alpha-200 relative mt-2 flex w-full flex-col gap-2 border-t px-3 py-4 pb-0" | ||
> | ||
<div className="absolute left-0 top-0 z-10 flex w-full -translate-y-1/2 items-center justify-between p-0 text-sm"> | ||
<div className="-mt-px flex w-full items-center gap-1"> | ||
<span className="bg-background ml-3 px-1"> | ||
<ArrayFieldTitleTemplate | ||
idSchema={idSchema} | ||
title={sectionTitle} | ||
schema={schema} | ||
uiSchema={uiSchema} | ||
required={required} | ||
registry={registry} | ||
/> | ||
</span> | ||
<div className="bg-background text-foreground-600 ml-auto flex items-center gap-1"> | ||
<CollapsibleTrigger | ||
className="flex size-4 items-center justify-center rounded-sm p-0.5" | ||
title="Collapse section" | ||
> | ||
<RiExpandUpDownLine className="text-foreground-600 size-3" /> | ||
</CollapsibleTrigger> | ||
</div> | ||
</div> | ||
</div> | ||
|
||
<CollapsibleContent className="flex flex-col gap-3"> | ||
{properties.map((element) => { | ||
return ( | ||
<div key={element.name} className="ml-1"> | ||
{element.content} | ||
</div> | ||
); | ||
})} | ||
</CollapsibleContent> | ||
</Collapsible> | ||
</FormControl> | ||
<FormMessage /> | ||
</FormItem> | ||
)} | ||
/> | ||
); | ||
} |
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.
for the Array went with the UI similar to the objects instead of using the tag-input as it will make it complex to handle different scenarios and also doesn't play well with the rjsf form