mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-17 04:46:05 +00:00
admin: data/auth route-driven settings and collapsible components (#168)
introduced `useRoutePathState` for managing active states via routes, added `CollapsibleList` for reusable collapsible UI, and updated various components to leverage route awareness for improved navigation state handling. Also adjusted routing for entities, strategies, and schema to support optional sub-paths.
This commit is contained in:
@@ -30,14 +30,10 @@ 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";
|
||||
import { RoutePathStateProvider } from "ui/hooks/use-route-path-state";
|
||||
|
||||
export function DataSchemaEntity({ params }) {
|
||||
const { $data } = useBkndData();
|
||||
const [value, setValue] = useState("fields");
|
||||
|
||||
function toggle(value) {
|
||||
return () => setValue(value);
|
||||
}
|
||||
|
||||
const [navigate] = useNavigate();
|
||||
const entity = $data.entity(params.entity as string)!;
|
||||
@@ -46,7 +42,7 @@ export function DataSchemaEntity({ params }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<RoutePathStateProvider path={`/entity/${entity.name}/:setting?`} defaultIdentifier="fields">
|
||||
<AppShell.SectionHeader
|
||||
right={
|
||||
<>
|
||||
@@ -109,13 +105,12 @@ export function DataSchemaEntity({ params }) {
|
||||
</div>
|
||||
</AppShell.SectionHeader>
|
||||
<div className="flex flex-col h-full" key={entity.name}>
|
||||
<Fields entity={entity} open={value === "fields"} toggle={toggle("fields")} />
|
||||
<Fields entity={entity} />
|
||||
|
||||
<BasicSettings entity={entity} open={value === "2"} toggle={toggle("2")} />
|
||||
<AppShell.SectionHeaderAccordionItem
|
||||
<BasicSettings entity={entity} />
|
||||
<AppShell.RouteAwareSectionHeaderAccordionItem
|
||||
identifier="relations"
|
||||
title="Relations"
|
||||
open={value === "3"}
|
||||
toggle={toggle("3")}
|
||||
ActiveIcon={IconCirclesRelation}
|
||||
>
|
||||
<Empty
|
||||
@@ -127,11 +122,10 @@ export function DataSchemaEntity({ params }) {
|
||||
navigate(routes.settings.path(["data", "relations"]), { absolute: true }),
|
||||
}}
|
||||
/>
|
||||
</AppShell.SectionHeaderAccordionItem>
|
||||
<AppShell.SectionHeaderAccordionItem
|
||||
</AppShell.RouteAwareSectionHeaderAccordionItem>
|
||||
<AppShell.RouteAwareSectionHeaderAccordionItem
|
||||
identifier="indices"
|
||||
title="Indices"
|
||||
open={value === "4"}
|
||||
toggle={toggle("4")}
|
||||
ActiveIcon={IconBolt}
|
||||
>
|
||||
<Empty
|
||||
@@ -145,17 +139,13 @@ export function DataSchemaEntity({ params }) {
|
||||
}),
|
||||
}}
|
||||
/>
|
||||
</AppShell.SectionHeaderAccordionItem>
|
||||
</AppShell.RouteAwareSectionHeaderAccordionItem>
|
||||
</div>
|
||||
</>
|
||||
</RoutePathStateProvider>
|
||||
);
|
||||
}
|
||||
|
||||
const Fields = ({
|
||||
entity,
|
||||
open,
|
||||
toggle,
|
||||
}: { entity: Entity; open: boolean; toggle: () => void }) => {
|
||||
const Fields = ({ entity }: { entity: Entity }) => {
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [updates, setUpdates] = useState(0);
|
||||
const { actions, $data } = useBkndData();
|
||||
@@ -174,10 +164,9 @@ const Fields = ({
|
||||
const initialFields = Object.fromEntries(entity.fields.map((f) => [f.name, f.toJSON()])) as any;
|
||||
|
||||
return (
|
||||
<AppShell.SectionHeaderAccordionItem
|
||||
<AppShell.RouteAwareSectionHeaderAccordionItem
|
||||
identifier="fields"
|
||||
title="Fields"
|
||||
open={open}
|
||||
toggle={toggle}
|
||||
ActiveIcon={IconAlignJustified}
|
||||
renderHeaderRight={({ open }) =>
|
||||
open ? (
|
||||
@@ -192,6 +181,7 @@ const Fields = ({
|
||||
<div className="animate-fade-in absolute w-full h-full top-0 bottom-0 left-0 right-0 bg-background/65 z-50" />
|
||||
)}
|
||||
<EntityFieldsForm
|
||||
routePattern={`/entity/${entity.name}/fields/:sub?`}
|
||||
fields={initialFields}
|
||||
ref={ref}
|
||||
key={String(updates)}
|
||||
@@ -237,15 +227,11 @@ const Fields = ({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</AppShell.SectionHeaderAccordionItem>
|
||||
</AppShell.RouteAwareSectionHeaderAccordionItem>
|
||||
);
|
||||
};
|
||||
|
||||
const BasicSettings = ({
|
||||
entity,
|
||||
open,
|
||||
toggle,
|
||||
}: { entity: Entity; open: boolean; toggle: () => void }) => {
|
||||
const BasicSettings = ({ entity }: { entity: Entity }) => {
|
||||
const d = useBkndData();
|
||||
const config = d.entities?.[entity.name]?.config;
|
||||
const formRef = useRef<JsonSchemaFormRef>(null);
|
||||
@@ -271,10 +257,9 @@ const BasicSettings = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<AppShell.SectionHeaderAccordionItem
|
||||
<AppShell.RouteAwareSectionHeaderAccordionItem
|
||||
identifier="settings"
|
||||
title="Settings"
|
||||
open={open}
|
||||
toggle={toggle}
|
||||
ActiveIcon={IconSettings}
|
||||
renderHeaderRight={({ open }) =>
|
||||
open ? (
|
||||
@@ -293,6 +278,6 @@ const BasicSettings = ({
|
||||
className="legacy hide-required-mark fieldset-alternative mute-root"
|
||||
/>
|
||||
</div>
|
||||
</AppShell.SectionHeaderAccordionItem>
|
||||
</AppShell.RouteAwareSectionHeaderAccordionItem>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -27,6 +27,7 @@ import { Popover } from "ui/components/overlay/Popover";
|
||||
import { type TFieldSpec, fieldSpecs } from "ui/modules/data/components/fields-specs";
|
||||
import { dataFieldsUiSchema } from "../../settings/routes/data.settings";
|
||||
import * as tbbox from "@sinclair/typebox";
|
||||
import { useRoutePathState } from "ui/hooks/use-route-path-state";
|
||||
const { Type } = tbbox;
|
||||
|
||||
const fieldsSchemaObject = originalFieldsSchemaObject;
|
||||
@@ -63,6 +64,7 @@ export type EntityFieldsFormProps = {
|
||||
onChange?: (formData: TAppDataEntityFields) => void;
|
||||
sortable?: boolean;
|
||||
additionalFieldTypes?: (TFieldSpec & { onClick: () => void })[];
|
||||
routePattern?: string;
|
||||
};
|
||||
|
||||
export type EntityFieldsFormRef = {
|
||||
@@ -74,7 +76,10 @@ export type EntityFieldsFormRef = {
|
||||
};
|
||||
|
||||
export const EntityFieldsForm = forwardRef<EntityFieldsFormRef, EntityFieldsFormProps>(
|
||||
function EntityFieldsForm({ fields: _fields, sortable, additionalFieldTypes, ...props }, ref) {
|
||||
function EntityFieldsForm(
|
||||
{ fields: _fields, sortable, additionalFieldTypes, routePattern, ...props },
|
||||
ref,
|
||||
) {
|
||||
const entityFields = Object.entries(_fields).map(([name, field]) => ({
|
||||
name,
|
||||
field,
|
||||
@@ -166,6 +171,7 @@ export const EntityFieldsForm = forwardRef<EntityFieldsFormRef, EntityFieldsForm
|
||||
errors={errors}
|
||||
remove={remove}
|
||||
dnd={dnd}
|
||||
routePattern={routePattern}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
@@ -179,6 +185,7 @@ export const EntityFieldsForm = forwardRef<EntityFieldsFormRef, EntityFieldsForm
|
||||
form={formProps}
|
||||
errors={errors}
|
||||
remove={remove}
|
||||
routePattern={routePattern}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -273,6 +280,7 @@ function EntityField({
|
||||
remove,
|
||||
errors,
|
||||
dnd,
|
||||
routePattern,
|
||||
}: {
|
||||
field: FieldArrayWithId<TFieldsFormSchema, "fields", "id">;
|
||||
index: number;
|
||||
@@ -283,11 +291,12 @@ function EntityField({
|
||||
remove: (index: number) => void;
|
||||
errors: any;
|
||||
dnd?: SortableItemProps;
|
||||
routePattern?: string;
|
||||
}) {
|
||||
const [opened, handlers] = useDisclosure(false);
|
||||
const prefix = `fields.${index}.field` as const;
|
||||
const type = field.field.type;
|
||||
const name = watch(`fields.${index}.name`);
|
||||
const { active, toggle } = useRoutePathState(routePattern ?? "", name);
|
||||
const fieldSpec = fieldSpecs.find((s) => s.type === type)!;
|
||||
const specificData = omit(field.field.config, commonProps);
|
||||
const disabled = fieldSpec.disabled || [];
|
||||
@@ -300,9 +309,11 @@ function EntityField({
|
||||
return () => {
|
||||
if (name.length === 0) {
|
||||
remove(index);
|
||||
return;
|
||||
toggle();
|
||||
} else if (window.confirm(`Sure to delete "${name}"?`)) {
|
||||
remove(index);
|
||||
toggle();
|
||||
}
|
||||
window.confirm(`Sure to delete "${name}"?`) && remove(index);
|
||||
};
|
||||
}
|
||||
//console.log("register", register(`${prefix}.config.required`));
|
||||
@@ -313,7 +324,7 @@ function EntityField({
|
||||
key={field.id}
|
||||
className={twMerge(
|
||||
"flex flex-col border border-muted rounded bg-background mb-2",
|
||||
opened && "mb-6",
|
||||
active && "mb-6",
|
||||
hasErrors && "border-red-500 ",
|
||||
)}
|
||||
{...dndProps}
|
||||
@@ -371,13 +382,13 @@ function EntityField({
|
||||
Icon={TbSettings}
|
||||
disabled={is_primary}
|
||||
iconProps={{ strokeWidth: 1.5 }}
|
||||
onClick={handlers.toggle}
|
||||
variant={opened ? "primary" : "ghost"}
|
||||
onClick={() => toggle()}
|
||||
variant={active ? "primary" : "ghost"}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{opened && (
|
||||
{active && (
|
||||
<div className="flex flex-col border-t border-t-muted px-3 py-2 bg-lightest/50">
|
||||
{/*<pre>{JSON.stringify(field, null, 2)}</pre>*/}
|
||||
<Tabs defaultValue="general">
|
||||
|
||||
@@ -17,7 +17,7 @@ export default function DataRoutes() {
|
||||
|
||||
<Route path="/schema" nest>
|
||||
<Route path="/" component={DataSchemaIndex} />
|
||||
<Route path="/entity/:entity" component={DataSchemaEntity} />
|
||||
<Route path="/entity/:entity/:setting?/:sub?" component={DataSchemaEntity} />
|
||||
</Route>
|
||||
</Switch>
|
||||
</DataRoot>
|
||||
|
||||
Reference in New Issue
Block a user