mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-16 04:27:21 +00:00
Merge pull request #52 from bknd-io/feat/admin-entity-context-menu
Admin UI: Adding context menu to entities list
This commit is contained in:
@@ -64,7 +64,12 @@ export function useBkndData() {
|
||||
};
|
||||
const $data = {
|
||||
entity: (name: string) => entities[name],
|
||||
modals
|
||||
modals,
|
||||
system: (name: string) => ({
|
||||
any: entities[name]?.type === "system",
|
||||
users: name === config.auth.entity_name,
|
||||
media: name === config.media.entity_name
|
||||
})
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import { useClickOutside } from "@mantine/hooks";
|
||||
import { Fragment, type ReactElement, cloneElement, useState } from "react";
|
||||
import {
|
||||
type ComponentPropsWithoutRef,
|
||||
Fragment,
|
||||
type ReactElement,
|
||||
cloneElement,
|
||||
useState
|
||||
} from "react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { useEvent } from "../../hooks/use-event";
|
||||
|
||||
@@ -14,26 +20,33 @@ export type DropdownItem =
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
export type DropdownClickableChild = ReactElement<{ onClick: () => void }>;
|
||||
export type DropdownProps = {
|
||||
className?: string;
|
||||
openEvent?: "onClick" | "onContextMenu";
|
||||
defaultOpen?: boolean;
|
||||
title?: string | ReactElement;
|
||||
dropdownWrapperProps?: Omit<ComponentPropsWithoutRef<"div">, "style">;
|
||||
position?: "bottom-start" | "bottom-end" | "top-start" | "top-end";
|
||||
hideOnEmpty?: boolean;
|
||||
items: (DropdownItem | undefined | boolean)[];
|
||||
itemsClassName?: string;
|
||||
children: ReactElement<{ onClick: () => void }>;
|
||||
children: DropdownClickableChild;
|
||||
onClickItem?: (item: DropdownItem) => void;
|
||||
renderItem?: (
|
||||
item: DropdownItem,
|
||||
props: { key: number; onClick: () => void }
|
||||
) => ReactElement<{ onClick: () => void }>;
|
||||
) => DropdownClickableChild;
|
||||
};
|
||||
|
||||
export function Dropdown({
|
||||
children,
|
||||
defaultOpen = false,
|
||||
openEvent = "onClick",
|
||||
position = "bottom-start",
|
||||
dropdownWrapperProps,
|
||||
items,
|
||||
title,
|
||||
hideOnEmpty = true,
|
||||
onClickItem,
|
||||
renderItem,
|
||||
@@ -48,6 +61,11 @@ export function Dropdown({
|
||||
setTimeout(() => setOpen((prev) => !prev), typeof delay === "number" ? delay : 0)
|
||||
);
|
||||
|
||||
const openEventHandler = useEvent((e) => {
|
||||
e.preventDefault();
|
||||
toggle();
|
||||
});
|
||||
|
||||
const offset = 4;
|
||||
const dropdownStyle = {
|
||||
"bottom-start": { top: "100%", left: 0, marginTop: offset },
|
||||
@@ -94,13 +112,26 @@ export function Dropdown({
|
||||
));
|
||||
|
||||
return (
|
||||
<div role="dropdown" className={twMerge("relative flex", className)} ref={clickoutsideRef}>
|
||||
{cloneElement(children as any, { onClick: toggle })}
|
||||
<div
|
||||
role="dropdown"
|
||||
className={twMerge("relative flex", className)}
|
||||
ref={clickoutsideRef}
|
||||
onContextMenu={openEvent === "onContextMenu" ? openEventHandler : undefined}
|
||||
>
|
||||
{cloneElement(
|
||||
children as any,
|
||||
openEvent === "onClick" ? { onClick: openEventHandler } : {}
|
||||
)}
|
||||
{open && (
|
||||
<div
|
||||
className="absolute z-30 flex flex-col bg-background border border-muted px-1 py-1 rounded-lg shadow-lg min-w-full"
|
||||
{...dropdownWrapperProps}
|
||||
className={twMerge(
|
||||
"absolute z-30 flex flex-col bg-background border border-muted px-1 py-1 rounded-lg shadow-lg min-w-full",
|
||||
dropdownWrapperProps?.className
|
||||
)}
|
||||
style={dropdownStyle}
|
||||
>
|
||||
{title && <div className="text-sm font-bold px-3 mb-1 mt-1 opacity-50">{title}</div>}
|
||||
{menuItems.map((item, i) =>
|
||||
itemRenderer(item, { key: i, onClick: () => internalOnClickItem(item) })
|
||||
)}
|
||||
|
||||
@@ -191,7 +191,7 @@ export const SidebarLink = <E extends React.ElementType = "a">({
|
||||
className={twMerge(
|
||||
"flex flex-row px-4 py-2.5 items-center gap-2",
|
||||
!disabled &&
|
||||
"cursor-pointer rounded-md [&.active]:bg-primary/10 [&.active]:hover:bg-primary/15 [&.active]:font-medium hover:bg-primary/5 link",
|
||||
"cursor-pointer rounded-md [&.active]:bg-primary/10 [&.active]:hover:bg-primary/15 [&.active]:font-medium hover:bg-primary/5 focus:bg-primary/5 link",
|
||||
disabled && "opacity-50 cursor-not-allowed pointer-events-none",
|
||||
className
|
||||
)}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { PrimaryFieldType } from "core";
|
||||
import { encodeSearch } from "core/utils";
|
||||
import { useLocation } from "wouter";
|
||||
import { useLocation, useRouter } from "wouter";
|
||||
import { useBknd } from "../client/BkndProvider";
|
||||
|
||||
export const routes = {
|
||||
@@ -55,6 +55,7 @@ export function withAbsolute(url: string) {
|
||||
|
||||
export function useNavigate() {
|
||||
const [location, navigate] = useLocation();
|
||||
const router = useRouter();
|
||||
const { app } = useBknd();
|
||||
const basepath = app.getAdminConfig().basepath;
|
||||
return [
|
||||
@@ -69,6 +70,7 @@ export function useNavigate() {
|
||||
transition?: boolean;
|
||||
}
|
||||
| { reload: true }
|
||||
| { target: string }
|
||||
) => {
|
||||
const wrap = (fn: () => void) => {
|
||||
fn();
|
||||
@@ -81,9 +83,15 @@ export function useNavigate() {
|
||||
};
|
||||
|
||||
wrap(() => {
|
||||
if (options && "reload" in options) {
|
||||
window.location.href = url;
|
||||
return;
|
||||
if (options) {
|
||||
if ("reload" in options) {
|
||||
window.location.href = url;
|
||||
return;
|
||||
} else if ("target" in options) {
|
||||
const _url = window.location.origin + basepath + router.base + url;
|
||||
window.open(_url, options.target);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const _url = options?.absolute ? `~/${basepath}${url}`.replace(/\/+/g, "/") : url;
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
TbToggleLeft
|
||||
} from "react-icons/tb";
|
||||
|
||||
type TFieldSpec = {
|
||||
export type TFieldSpec = {
|
||||
type: string;
|
||||
label: string;
|
||||
icon: any;
|
||||
|
||||
@@ -1,11 +1,20 @@
|
||||
import { SegmentedControl, Tooltip } from "@mantine/core";
|
||||
import { IconDatabase } from "@tabler/icons-react";
|
||||
import {
|
||||
IconAlignJustified,
|
||||
IconCirclesRelation,
|
||||
IconDatabase,
|
||||
IconExternalLink,
|
||||
IconPhoto,
|
||||
IconPlus,
|
||||
IconSettings
|
||||
} from "@tabler/icons-react";
|
||||
import type { Entity, TEntityType } from "data";
|
||||
import { TbDatabasePlus } from "react-icons/tb";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { useBkndData } from "ui/client/schema/data/use-bknd-data";
|
||||
import { IconButton } from "ui/components/buttons/IconButton";
|
||||
import { Empty } from "ui/components/display/Empty";
|
||||
import { Dropdown, type DropdownClickableChild } from "ui/components/overlay/Dropdown";
|
||||
import { Link } from "ui/components/wouter/Link";
|
||||
import { useBrowserTitle } from "ui/hooks/use-browser-title";
|
||||
import * as AppShell from "ui/layouts/AppShell/AppShell";
|
||||
@@ -125,15 +134,92 @@ const EntityLinkList = ({
|
||||
? routes.data.entity.list(entity.name)
|
||||
: routes.data.schema.entity(entity.name);
|
||||
return (
|
||||
<AppShell.SidebarLink key={entity.name} as={Link} href={href}>
|
||||
{entity.label}
|
||||
</AppShell.SidebarLink>
|
||||
<EntityContextMenu key={entity.name} entity={entity}>
|
||||
<AppShell.SidebarLink as={Link} href={href}>
|
||||
{entity.label}
|
||||
</AppShell.SidebarLink>
|
||||
</EntityContextMenu>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
|
||||
const EntityContextMenu = ({
|
||||
entity,
|
||||
children,
|
||||
enabled = true
|
||||
}: { entity: Entity; children: DropdownClickableChild; enabled?: boolean }) => {
|
||||
if (!enabled) return children;
|
||||
const [navigate] = useNavigate();
|
||||
const { $data } = useBkndData();
|
||||
|
||||
// get href from children (single item)
|
||||
const href = (children as any).props.href;
|
||||
const separator = () => <div className="h-px my-1 w-full bg-primary/5" />;
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
className="flex flex-col w-full"
|
||||
dropdownWrapperProps={{
|
||||
className: "min-w-fit"
|
||||
}}
|
||||
title={entity.label + " Actions"}
|
||||
items={[
|
||||
href && {
|
||||
icon: IconExternalLink,
|
||||
label: "Open in new tab",
|
||||
onClick: () => navigate(href, { target: "_blank" })
|
||||
},
|
||||
separator,
|
||||
!$data.system(entity.name).any && {
|
||||
icon: IconPlus,
|
||||
label: "Create new",
|
||||
onClick: () => navigate(routes.data.entity.create(entity.name))
|
||||
},
|
||||
{
|
||||
icon: IconDatabase,
|
||||
label: "List entries",
|
||||
onClick: () => navigate(routes.data.entity.list(entity.name))
|
||||
},
|
||||
separator,
|
||||
{
|
||||
icon: IconAlignJustified,
|
||||
label: "Manage fields",
|
||||
onClick: () => navigate(routes.data.schema.entity(entity.name))
|
||||
},
|
||||
{
|
||||
icon: IconCirclesRelation,
|
||||
label: "Add relation",
|
||||
onClick: () =>
|
||||
$data.modals.createRelation({
|
||||
target: entity.name,
|
||||
type: "n:1"
|
||||
})
|
||||
},
|
||||
!$data.system(entity.name).media && {
|
||||
icon: IconPhoto,
|
||||
label: "Add media",
|
||||
onClick: () => $data.modals.createMedia(entity.name)
|
||||
},
|
||||
separator,
|
||||
{
|
||||
icon: IconSettings,
|
||||
label: "Advanced settings",
|
||||
onClick: () =>
|
||||
navigate(routes.settings.path(["data", "entities", entity.name]), {
|
||||
absolute: true
|
||||
})
|
||||
}
|
||||
]}
|
||||
openEvent="onContextMenu"
|
||||
position="bottom-start"
|
||||
>
|
||||
{children}
|
||||
</Dropdown>
|
||||
);
|
||||
};
|
||||
|
||||
export function DataEmpty() {
|
||||
useBrowserTitle(["Data"]);
|
||||
const [navigate] = useNavigate();
|
||||
|
||||
@@ -8,7 +8,7 @@ import { isDebug } from "core";
|
||||
import type { Entity } from "data";
|
||||
import { cloneDeep } from "lodash-es";
|
||||
import { useRef, useState } from "react";
|
||||
import { TbCirclesRelation, TbDots, TbPhoto, TbPlus } from "react-icons/tb";
|
||||
import { TbCirclesRelation, TbDots, TbPhoto, TbPlus, TbSitemap } from "react-icons/tb";
|
||||
import { useBkndData } from "ui/client/schema/data/use-bknd-data";
|
||||
import { Button } from "ui/components/buttons/Button";
|
||||
import { IconButton } from "ui/components/buttons/IconButton";
|
||||
@@ -16,9 +16,11 @@ import { Empty } from "ui/components/display/Empty";
|
||||
import { Message } from "ui/components/display/Message";
|
||||
import { JsonSchemaForm, type JsonSchemaFormRef } from "ui/components/form/json-schema";
|
||||
import { Dropdown } from "ui/components/overlay/Dropdown";
|
||||
import { Link } from "ui/components/wouter/Link";
|
||||
import * as AppShell from "ui/layouts/AppShell/AppShell";
|
||||
import { Breadcrumbs2 } from "ui/layouts/AppShell/Breadcrumbs2";
|
||||
import { routes, useNavigate } from "ui/lib/routes";
|
||||
import { fieldSpecs } from "ui/modules/data/components/fields-specs";
|
||||
import { extractSchema } from "../settings/utils/schema";
|
||||
import { EntityFieldsForm, type EntityFieldsFormRef } from "./forms/entity.fields.form";
|
||||
|
||||
@@ -87,10 +89,15 @@ export function DataSchemaEntity({ params }) {
|
||||
}
|
||||
className="pl-3"
|
||||
>
|
||||
<Breadcrumbs2
|
||||
path={[{ label: "Schema", href: "/" }, { label: entity.label }]}
|
||||
backTo="/"
|
||||
/>
|
||||
<div className="flex flex-row gap-4">
|
||||
<Breadcrumbs2
|
||||
path={[{ label: "Schema", href: "/" }, { label: entity.label }]}
|
||||
backTo="/"
|
||||
/>
|
||||
<Link to="/" className="invisible md:visible">
|
||||
<Button IconLeft={TbSitemap}>Overview</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</AppShell.SectionHeader>
|
||||
<div className="flex flex-col h-full" key={entity.name}>
|
||||
<Fields entity={entity} open={value === "fields"} toggle={toggle("fields")} />
|
||||
@@ -142,7 +149,7 @@ const Fields = ({
|
||||
}: { entity: Entity; open: boolean; toggle: () => void }) => {
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [updates, setUpdates] = useState(0);
|
||||
const { actions } = useBkndData();
|
||||
const { actions, $data } = useBkndData();
|
||||
const [res, setRes] = useState<any>();
|
||||
const ref = useRef<EntityFieldsFormRef>(null);
|
||||
async function handleUpdate() {
|
||||
@@ -175,7 +182,30 @@ const Fields = ({
|
||||
{submitting && (
|
||||
<div className="animate-fade-in absolute w-full h-full top-0 bottom-0 left-0 right-0 bg-background/65 z-50" />
|
||||
)}
|
||||
<EntityFieldsForm fields={initialFields} ref={ref} key={String(updates)} sortable />
|
||||
<EntityFieldsForm
|
||||
fields={initialFields}
|
||||
ref={ref}
|
||||
key={String(updates)}
|
||||
sortable
|
||||
additionalFieldTypes={fieldSpecs
|
||||
.filter((f) => ["relation", "media"].includes(f.type))
|
||||
.map((i) => ({
|
||||
...i,
|
||||
onClick: () => {
|
||||
switch (i.type) {
|
||||
case "relation":
|
||||
$data.modals.createRelation({
|
||||
target: entity.name,
|
||||
type: "n:1"
|
||||
});
|
||||
break;
|
||||
case "media":
|
||||
$data.modals.createMedia(entity.name);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}))}
|
||||
/>
|
||||
|
||||
{isDebug() && (
|
||||
<div>
|
||||
|
||||
@@ -25,7 +25,7 @@ import { MantineSwitch } from "ui/components/form/hook-form-mantine/MantineSwitc
|
||||
import { JsonSchemaForm } from "ui/components/form/json-schema";
|
||||
import { type SortableItemProps, SortableList } from "ui/components/list/SortableList";
|
||||
import { Popover } from "ui/components/overlay/Popover";
|
||||
import { fieldSpecs } from "ui/modules/data/components/fields-specs";
|
||||
import { type TFieldSpec, fieldSpecs } from "ui/modules/data/components/fields-specs";
|
||||
import { dataFieldsUiSchema } from "../../settings/routes/data.settings";
|
||||
|
||||
const fieldsSchemaObject = originalFieldsSchemaObject;
|
||||
@@ -45,7 +45,6 @@ type TFieldsFormSchema = Static<typeof schema>;
|
||||
|
||||
const fieldTypes = Object.keys(fieldsSchemaObject);
|
||||
const defaultType = fieldTypes[0];
|
||||
const blank_field = { name: "", field: { type: defaultType, config: {} } } as TFieldSchema;
|
||||
const commonProps = ["label", "description", "required", "fillable", "hidden", "virtual"];
|
||||
|
||||
function specificFieldSchema(type: keyof typeof fieldsSchemaObject) {
|
||||
@@ -53,6 +52,13 @@ function specificFieldSchema(type: keyof typeof fieldsSchemaObject) {
|
||||
return Type.Omit(fieldsSchemaObject[type]?.properties.config, commonProps);
|
||||
}
|
||||
|
||||
export type EntityFieldsFormProps = {
|
||||
fields: TAppDataEntityFields;
|
||||
onChange?: (formData: TAppDataEntityFields) => void;
|
||||
sortable?: boolean;
|
||||
additionalFieldTypes?: (TFieldSpec & { onClick: () => void })[];
|
||||
};
|
||||
|
||||
export type EntityFieldsFormRef = {
|
||||
getValues: () => TFieldsFormSchema;
|
||||
getData: () => TAppDataEntityFields;
|
||||
@@ -60,146 +66,156 @@ export type EntityFieldsFormRef = {
|
||||
reset: () => void;
|
||||
};
|
||||
|
||||
export const EntityFieldsForm = forwardRef<
|
||||
EntityFieldsFormRef,
|
||||
{
|
||||
fields: TAppDataEntityFields;
|
||||
onChange?: (formData: TAppDataEntityFields) => void;
|
||||
sortable?: boolean;
|
||||
}
|
||||
>(function EntityFieldsForm({ fields: _fields, sortable, ...props }, ref) {
|
||||
const entityFields = Object.entries(_fields).map(([name, field]) => ({
|
||||
name,
|
||||
field
|
||||
}));
|
||||
export const EntityFieldsForm = forwardRef<EntityFieldsFormRef, EntityFieldsFormProps>(
|
||||
function EntityFieldsForm({ fields: _fields, sortable, additionalFieldTypes, ...props }, ref) {
|
||||
const entityFields = Object.entries(_fields).map(([name, field]) => ({
|
||||
name,
|
||||
field
|
||||
}));
|
||||
|
||||
const {
|
||||
control,
|
||||
formState: { isValid, errors },
|
||||
getValues,
|
||||
watch,
|
||||
register,
|
||||
setValue,
|
||||
setError,
|
||||
reset
|
||||
} = useForm({
|
||||
mode: "all",
|
||||
resolver: typeboxResolver(schema),
|
||||
defaultValues: {
|
||||
fields: entityFields
|
||||
} as TFieldsFormSchema
|
||||
});
|
||||
const { fields, append, remove, move } = useFieldArray({
|
||||
control,
|
||||
name: "fields"
|
||||
});
|
||||
const {
|
||||
control,
|
||||
formState: { isValid, errors },
|
||||
getValues,
|
||||
watch,
|
||||
register,
|
||||
setValue,
|
||||
setError,
|
||||
reset
|
||||
} = useForm({
|
||||
mode: "all",
|
||||
resolver: typeboxResolver(schema),
|
||||
defaultValues: {
|
||||
fields: entityFields
|
||||
} as TFieldsFormSchema
|
||||
});
|
||||
const { fields, append, remove, move } = useFieldArray({
|
||||
control,
|
||||
name: "fields"
|
||||
});
|
||||
|
||||
function toCleanValues(formData: TFieldsFormSchema): TAppDataEntityFields {
|
||||
return Object.fromEntries(
|
||||
formData.fields.map((field) => [field.name, objectCleanEmpty(field.field)])
|
||||
);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (props?.onChange) {
|
||||
console.log("----set");
|
||||
watch((data: any) => {
|
||||
console.log("---calling");
|
||||
props?.onChange?.(toCleanValues(data));
|
||||
});
|
||||
function toCleanValues(formData: TFieldsFormSchema): TAppDataEntityFields {
|
||||
return Object.fromEntries(
|
||||
formData.fields.map((field) => [field.name, objectCleanEmpty(field.field)])
|
||||
);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
reset,
|
||||
getValues: () => getValues(),
|
||||
getData: () => {
|
||||
return toCleanValues(getValues());
|
||||
},
|
||||
isValid: () => isValid
|
||||
}));
|
||||
|
||||
function handleAppend(_type: keyof typeof fieldsSchemaObject) {
|
||||
const newField = {
|
||||
name: "",
|
||||
new: true,
|
||||
field: {
|
||||
type: _type,
|
||||
config: {}
|
||||
useEffect(() => {
|
||||
if (props?.onChange) {
|
||||
console.log("----set");
|
||||
watch((data: any) => {
|
||||
console.log("---calling");
|
||||
props?.onChange?.(toCleanValues(data));
|
||||
});
|
||||
}
|
||||
};
|
||||
append(newField);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const formProps = {
|
||||
watch,
|
||||
register,
|
||||
setValue,
|
||||
getValues,
|
||||
control,
|
||||
setError
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex flex-col gap-4">
|
||||
{sortable ? (
|
||||
<SortableList
|
||||
data={fields}
|
||||
key={fields.length}
|
||||
onReordered={move}
|
||||
extractId={(item) => item.id}
|
||||
disableIndices={[0]}
|
||||
renderItem={({ dnd, ...props }, index) => (
|
||||
<EntityFieldMemo
|
||||
key={props.id}
|
||||
field={props as any}
|
||||
index={index}
|
||||
form={formProps}
|
||||
errors={errors}
|
||||
remove={remove}
|
||||
dnd={dnd}
|
||||
useImperativeHandle(ref, () => ({
|
||||
reset,
|
||||
getValues: () => getValues(),
|
||||
getData: () => {
|
||||
return toCleanValues(getValues());
|
||||
},
|
||||
isValid: () => isValid
|
||||
}));
|
||||
|
||||
function handleAppend(_type: keyof typeof fieldsSchemaObject) {
|
||||
const newField = {
|
||||
name: "",
|
||||
new: true,
|
||||
field: {
|
||||
type: _type,
|
||||
config: {}
|
||||
}
|
||||
};
|
||||
append(newField);
|
||||
}
|
||||
|
||||
const formProps = {
|
||||
watch,
|
||||
register,
|
||||
setValue,
|
||||
getValues,
|
||||
control,
|
||||
setError
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex flex-col gap-4">
|
||||
{sortable ? (
|
||||
<SortableList
|
||||
data={fields}
|
||||
key={fields.length}
|
||||
onReordered={move}
|
||||
extractId={(item) => item.id}
|
||||
disableIndices={[0]}
|
||||
renderItem={({ dnd, ...props }, index) => (
|
||||
<EntityFieldMemo
|
||||
key={props.id}
|
||||
field={props as any}
|
||||
index={index}
|
||||
form={formProps}
|
||||
errors={errors}
|
||||
remove={remove}
|
||||
dnd={dnd}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<div>
|
||||
{fields.map((field, index) => (
|
||||
<EntityField
|
||||
key={field.id}
|
||||
field={field as any}
|
||||
index={index}
|
||||
form={formProps}
|
||||
errors={errors}
|
||||
remove={remove}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Popover
|
||||
className="flex flex-col w-full"
|
||||
target={({ toggle }) => (
|
||||
<SelectType
|
||||
additionalFieldTypes={additionalFieldTypes}
|
||||
onSelected={toggle}
|
||||
onSelect={(type) => {
|
||||
handleAppend(type as any);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<div>
|
||||
{fields.map((field, index) => (
|
||||
<EntityField
|
||||
key={field.id}
|
||||
field={field as any}
|
||||
index={index}
|
||||
form={formProps}
|
||||
errors={errors}
|
||||
remove={remove}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Popover
|
||||
className="flex flex-col w-full"
|
||||
target={({ toggle }) => (
|
||||
<SelectType
|
||||
onSelect={(type) => {
|
||||
handleAppend(type as any);
|
||||
toggle();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
>
|
||||
<Button className="justify-center">Add Field</Button>
|
||||
</Popover>
|
||||
>
|
||||
<Button className="justify-center">Add Field</Button>
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
const SelectType = ({ onSelect }: { onSelect: (type: string) => void }) => {
|
||||
const types = fieldSpecs.filter((s) => s.addable !== false);
|
||||
const SelectType = ({
|
||||
onSelect,
|
||||
additionalFieldTypes = [],
|
||||
onSelected
|
||||
}: {
|
||||
onSelect: (type: string) => void;
|
||||
additionalFieldTypes?: (TFieldSpec & { onClick?: () => void })[];
|
||||
onSelected?: () => void;
|
||||
}) => {
|
||||
const types: (TFieldSpec & { onClick?: () => void })[] = fieldSpecs.filter(
|
||||
(s) => s.addable !== false
|
||||
);
|
||||
|
||||
if (additionalFieldTypes) {
|
||||
types.push(...additionalFieldTypes);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-row gap-2 justify-center flex-wrap">
|
||||
@@ -208,7 +224,14 @@ const SelectType = ({ onSelect }: { onSelect: (type: string) => void }) => {
|
||||
key={type.type}
|
||||
IconLeft={type.icon}
|
||||
variant="ghost"
|
||||
onClick={() => onSelect(type.type)}
|
||||
onClick={() => {
|
||||
if (type.addable) {
|
||||
onSelect(type.type);
|
||||
} else {
|
||||
type.onClick?.();
|
||||
}
|
||||
onSelected?.();
|
||||
}}
|
||||
>
|
||||
{type.label}
|
||||
</Button>
|
||||
|
||||
Reference in New Issue
Block a user