mirror of
https://github.com/bknd-io/bknd/
synced 2026-08-02 08:06:00 +00:00
extended data canvas and init indices management
This commit is contained in:
@@ -218,7 +218,7 @@ export function objectCleanEmpty<Obj extends { [key: string]: any }>(obj: Obj):
|
|||||||
* @param object
|
* @param object
|
||||||
* @param sources
|
* @param sources
|
||||||
*/
|
*/
|
||||||
export function mergeObject(object, ...sources) {
|
export function mergeObject<R = unknown>(object, ...sources): R {
|
||||||
for (const source of sources) {
|
for (const source of sources) {
|
||||||
for (const [key, value] of Object.entries(source)) {
|
for (const [key, value] of Object.entries(source)) {
|
||||||
if (value === undefined) {
|
if (value === undefined) {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
type EntityManager,
|
type EntityManager,
|
||||||
constructEntity,
|
constructEntity,
|
||||||
constructRelation,
|
constructRelation,
|
||||||
|
constructIndex,
|
||||||
} from "data";
|
} from "data";
|
||||||
import { Module } from "modules/Module";
|
import { Module } from "modules/Module";
|
||||||
import { DataController } from "./api/DataController";
|
import { DataController } from "./api/DataController";
|
||||||
@@ -35,9 +36,7 @@ export class AppData extends Module<typeof dataConfigSchema> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const indices = transformObject(_indices, (index, name) => {
|
const indices = transformObject(_indices, (index, name) => {
|
||||||
const entity = _entity(index.entity)!;
|
return constructIndex(index, _entity, name);
|
||||||
const fields = index.fields.map((f) => entity.field(f)!);
|
|
||||||
return new EntityIndex(entity, fields, index.unique, name);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const entity of Object.values(entities)) {
|
for (const entity of Object.values(entities)) {
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ export const indicesSchema = tb.Type.Object(
|
|||||||
additionalProperties: false,
|
additionalProperties: false,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
export type TAppDataIndex = Static<(typeof indicesSchema)[number]>;
|
||||||
|
|
||||||
export const dataConfigSchema = tb.Type.Object(
|
export const dataConfigSchema = tb.Type.Object(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export {
|
|||||||
|
|
||||||
export { KyselyPluginRunner } from "./plugins/KyselyPluginRunner";
|
export { KyselyPluginRunner } from "./plugins/KyselyPluginRunner";
|
||||||
|
|
||||||
export { constructEntity, constructRelation } from "./schema/constructor";
|
export { constructEntity, constructRelation, constructIndex } from "./schema/constructor";
|
||||||
|
|
||||||
export const DatabaseEvents = {
|
export const DatabaseEvents = {
|
||||||
...MutatorEvents,
|
...MutatorEvents,
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
import { transformObject } from "core/utils";
|
import { transformObject } from "core/utils";
|
||||||
import { Entity, type Field } from "data";
|
import { Entity, EntityIndex, type Field } from "data";
|
||||||
import { FIELDS, RELATIONS, type TAppDataEntity, type TAppDataRelation } from "data/data-schema";
|
import {
|
||||||
|
FIELDS,
|
||||||
|
RELATIONS,
|
||||||
|
type TAppDataEntity,
|
||||||
|
type TAppDataField,
|
||||||
|
type TAppDataIndex,
|
||||||
|
type TAppDataRelation,
|
||||||
|
} from "data/data-schema";
|
||||||
|
|
||||||
export function constructEntity(name: string, entityConfig: TAppDataEntity) {
|
export function constructEntity(name: string, entityConfig: TAppDataEntity) {
|
||||||
const fields = transformObject(entityConfig.fields ?? {}, (fieldConfig, name) => {
|
const fields = transformObject(entityConfig.fields ?? {}, (fieldConfig, name) => {
|
||||||
@@ -32,3 +39,17 @@ export function constructRelation(
|
|||||||
relationConfig.config,
|
relationConfig.config,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function constructIndex(
|
||||||
|
indexConfig: TAppDataIndex,
|
||||||
|
resolver: (name: Entity | string) => Entity,
|
||||||
|
name: string,
|
||||||
|
) {
|
||||||
|
const entity = resolver(indexConfig.entity);
|
||||||
|
return new EntityIndex(
|
||||||
|
entity,
|
||||||
|
entity.fields.filter((f) => indexConfig.fields.includes(f.name)),
|
||||||
|
indexConfig.unique,
|
||||||
|
name,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ export function useBkndData() {
|
|||||||
};
|
};
|
||||||
const $data = {
|
const $data = {
|
||||||
entity: (name: string) => entities[name],
|
entity: (name: string) => entities[name],
|
||||||
|
indicesOf: (name: string) => app.indices.filter((i) => i.entity.name === name),
|
||||||
modals,
|
modals,
|
||||||
system: (name: string) => ({
|
system: (name: string) => ({
|
||||||
any: entities[name]?.type === "system",
|
any: entities[name]?.type === "system",
|
||||||
@@ -82,6 +83,7 @@ export function useBkndData() {
|
|||||||
$data,
|
$data,
|
||||||
entities,
|
entities,
|
||||||
relations: app.relations,
|
relations: app.relations,
|
||||||
|
indices: app.indices,
|
||||||
config: config.data,
|
config: config.data,
|
||||||
schema: schema.data,
|
schema: schema.data,
|
||||||
actions,
|
actions,
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
import type { App } from "App";
|
import type { App } from "App";
|
||||||
import { type Entity, type EntityRelation, constructEntity, constructRelation } from "data";
|
import {
|
||||||
|
type Entity,
|
||||||
|
type EntityIndex,
|
||||||
|
type EntityRelation,
|
||||||
|
constructEntity,
|
||||||
|
constructRelation,
|
||||||
|
constructIndex,
|
||||||
|
} from "data";
|
||||||
import { RelationAccessor } from "data/relations/RelationAccessor";
|
import { RelationAccessor } from "data/relations/RelationAccessor";
|
||||||
import { Flow, TaskMap } from "flows";
|
import { Flow, TaskMap } from "flows";
|
||||||
import type { BkndAdminOptions } from "ui/client/BkndProvider";
|
import type { BkndAdminOptions } from "ui/client/BkndProvider";
|
||||||
@@ -14,6 +21,7 @@ export class AppReduced {
|
|||||||
// @todo: change to record
|
// @todo: change to record
|
||||||
private _entities: Entity[] = [];
|
private _entities: Entity[] = [];
|
||||||
private _relations: EntityRelation[] = [];
|
private _relations: EntityRelation[] = [];
|
||||||
|
private _indices: EntityIndex[] = [];
|
||||||
private _flows: Flow[] = [];
|
private _flows: Flow[] = [];
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@@ -30,6 +38,10 @@ export class AppReduced {
|
|||||||
return constructRelation(relation, this.entity.bind(this));
|
return constructRelation(relation, this.entity.bind(this));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this._indices = Object.entries(this.appJson.data.indices ?? {}).map(([name, index]) => {
|
||||||
|
return constructIndex(index, this.entity.bind(this), name);
|
||||||
|
});
|
||||||
|
|
||||||
for (const [name, obj] of Object.entries(this.appJson.flows.flows ?? {})) {
|
for (const [name, obj] of Object.entries(this.appJson.flows.flows ?? {})) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
// @todo: fix constructing flow
|
// @todo: fix constructing flow
|
||||||
@@ -58,6 +70,10 @@ export class AppReduced {
|
|||||||
return new RelationAccessor(this._relations);
|
return new RelationAccessor(this._relations);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get indices(): EntityIndex[] {
|
||||||
|
return this._indices;
|
||||||
|
}
|
||||||
|
|
||||||
get flows(): Flow[] {
|
get flows(): Flow[] {
|
||||||
return this._flows;
|
return this._flows;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,13 +13,15 @@ import {
|
|||||||
import { type ReactNode, useCallback, useEffect, useState } from "react";
|
import { type ReactNode, useCallback, useEffect, useState } from "react";
|
||||||
import { useTheme } from "ui/client/use-theme";
|
import { useTheme } from "ui/client/use-theme";
|
||||||
|
|
||||||
type CanvasProps = ReactFlowProps & {
|
export type CanvasProps = Omit<ReactFlowProps, "onNodesChange" | "onEdgesChange"> & {
|
||||||
externalProvider?: boolean;
|
externalProvider?: boolean;
|
||||||
backgroundStyle?: "lines" | "dots";
|
backgroundStyle?: "lines" | "dots";
|
||||||
minimap?: boolean | MiniMapProps;
|
minimap?: boolean | MiniMapProps;
|
||||||
children?: Element | ReactNode;
|
children?: Element | ReactNode;
|
||||||
onDropNewNode?: (base: any) => any;
|
onDropNewNode?: (base: any) => any;
|
||||||
onDropNewEdge?: (base: any) => any;
|
onDropNewEdge?: (base: any) => any;
|
||||||
|
onNodesChange?: (changes: any) => void;
|
||||||
|
onEdgesChange?: (changes: any) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function Canvas({
|
export function Canvas({
|
||||||
@@ -33,8 +35,8 @@ export function Canvas({
|
|||||||
onDropNewEdge,
|
onDropNewEdge,
|
||||||
...props
|
...props
|
||||||
}: CanvasProps) {
|
}: CanvasProps) {
|
||||||
const [nodes, setNodes, onNodesChange] = useNodesState(_nodes ?? []);
|
const [nodes, setNodes, _onNodesChange] = useNodesState(_nodes ?? []);
|
||||||
const [edges, setEdges, onEdgesChange] = useEdgesState(_edges ?? []);
|
const [edges, setEdges, _onEdgesChange] = useEdgesState(_edges ?? []);
|
||||||
const { screenToFlowPosition } = useReactFlow();
|
const { screenToFlowPosition } = useReactFlow();
|
||||||
const { theme } = useTheme();
|
const { theme } = useTheme();
|
||||||
|
|
||||||
@@ -42,6 +44,22 @@ export function Canvas({
|
|||||||
const [isSpacePressed, setIsSpacePressed] = useState(false);
|
const [isSpacePressed, setIsSpacePressed] = useState(false);
|
||||||
const [isPointerPressed, setIsPointerPressed] = useState(false);
|
const [isPointerPressed, setIsPointerPressed] = useState(false);
|
||||||
|
|
||||||
|
const onNodesChange = useCallback(
|
||||||
|
(changes) => {
|
||||||
|
_onNodesChange(changes);
|
||||||
|
props.onNodesChange?.(changes);
|
||||||
|
},
|
||||||
|
[_onNodesChange, props.onNodesChange],
|
||||||
|
);
|
||||||
|
|
||||||
|
const onEdgesChange = useCallback(
|
||||||
|
(changes) => {
|
||||||
|
_onEdgesChange(changes);
|
||||||
|
props.onEdgesChange?.(changes);
|
||||||
|
},
|
||||||
|
[_onEdgesChange, props.onEdgesChange],
|
||||||
|
);
|
||||||
|
|
||||||
const handleKeyDown = (event: KeyboardEvent) => {
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
if (event.metaKey) {
|
if (event.metaKey) {
|
||||||
setIsCommandPressed(true);
|
setIsCommandPressed(true);
|
||||||
@@ -173,8 +191,6 @@ export function Canvas({
|
|||||||
snapToGrid
|
snapToGrid
|
||||||
nodes={nodes}
|
nodes={nodes}
|
||||||
edges={edges}
|
edges={edges}
|
||||||
onNodesChange={onNodesChange}
|
|
||||||
onEdgesChange={onEdgesChange}
|
|
||||||
nodesConnectable={false}
|
nodesConnectable={false}
|
||||||
/*panOnDrag={isSpacePressed}*/
|
/*panOnDrag={isSpacePressed}*/
|
||||||
panOnDrag={true}
|
panOnDrag={true}
|
||||||
@@ -183,6 +199,8 @@ export function Canvas({
|
|||||||
zoomOnDoubleClick={false}
|
zoomOnDoubleClick={false}
|
||||||
selectionOnDrag={!isSpacePressed}
|
selectionOnDrag={!isSpacePressed}
|
||||||
{...props}
|
{...props}
|
||||||
|
onNodesChange={onNodesChange}
|
||||||
|
onEdgesChange={onEdgesChange}
|
||||||
>
|
>
|
||||||
{backgroundStyle === "lines" && (
|
{backgroundStyle === "lines" && (
|
||||||
<Background
|
<Background
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export function DefaultNode({ selected, children, className, ...props }: TDefaul
|
|||||||
{...props}
|
{...props}
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
"relative w-80 shadow-lg rounded-lg bg-background",
|
"relative w-80 shadow-lg rounded-lg bg-background",
|
||||||
selected && "outline outline-blue-500/25",
|
selected && "ring-4 ring-blue-400/50",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -246,7 +246,6 @@ export const Switch = forwardRef<
|
|||||||
props.disabled && "opacity-50 !cursor-not-allowed",
|
props.disabled && "opacity-50 !cursor-not-allowed",
|
||||||
)}
|
)}
|
||||||
onCheckedChange={(bool) => {
|
onCheckedChange={(bool) => {
|
||||||
console.log("setting", bool);
|
|
||||||
props.onChange?.({ target: { value: bool } });
|
props.onChange?.({ target: { value: bool } });
|
||||||
}}
|
}}
|
||||||
{...(props as any)}
|
{...(props as any)}
|
||||||
@@ -272,7 +271,7 @@ export const Switch = forwardRef<
|
|||||||
export const Select = forwardRef<
|
export const Select = forwardRef<
|
||||||
HTMLSelectElement,
|
HTMLSelectElement,
|
||||||
React.ComponentProps<"select"> & {
|
React.ComponentProps<"select"> & {
|
||||||
options?: { value: string; label: string }[] | (string | number)[];
|
options?: { value: string; label: string; disabled?: boolean }[] | (string | number)[];
|
||||||
}
|
}
|
||||||
>(({ children, options, ...props }, ref) => (
|
>(({ children, options, ...props }, ref) => (
|
||||||
<div className="flex w-full relative">
|
<div className="flex w-full relative">
|
||||||
@@ -297,7 +296,7 @@ export const Select = forwardRef<
|
|||||||
return o;
|
return o;
|
||||||
})
|
})
|
||||||
.map((opt) => (
|
.map((opt) => (
|
||||||
<option key={opt.value} value={opt.value}>
|
<option key={opt.value} value={opt.value} disabled={opt.disabled}>
|
||||||
{opt.label}
|
{opt.label}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ const ArrayItem = memo(({ path, index, schema }: any) => {
|
|||||||
return (
|
return (
|
||||||
<div key={itemPath} className="flex flex-row gap-2">
|
<div key={itemPath} className="flex flex-row gap-2">
|
||||||
<FieldComponent
|
<FieldComponent
|
||||||
|
required={schema.minItems > 0}
|
||||||
name={itemPath}
|
name={itemPath}
|
||||||
schema={subschema!}
|
schema={subschema!}
|
||||||
value={value}
|
value={value}
|
||||||
|
|||||||
@@ -28,10 +28,10 @@ export function useRoutePathState(_path?: string, identifier?: string) {
|
|||||||
const [, navigate] = useLocation();
|
const [, navigate] = useLocation();
|
||||||
|
|
||||||
function toggle(_open?: boolean) {
|
function toggle(_open?: boolean) {
|
||||||
const open = _open ?? !localActive;
|
const open = _open ?? !active;
|
||||||
|
|
||||||
if (ctx) {
|
if (ctx) {
|
||||||
ctx.setActiveIdentifier(identifier!);
|
ctx.setActiveIdentifier(open ? identifier! : "");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (path) {
|
if (path) {
|
||||||
@@ -58,7 +58,7 @@ export function useRoutePathState(_path?: string, identifier?: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type RoutePathStateContextType = {
|
type RoutePathStateContextType = {
|
||||||
defaultIdentifier: string;
|
defaultIdentifier?: string;
|
||||||
path: string;
|
path: string;
|
||||||
activeIdentifier: string;
|
activeIdentifier: string;
|
||||||
setActiveIdentifier: (identifier: string) => void;
|
setActiveIdentifier: (identifier: string) => void;
|
||||||
@@ -72,7 +72,9 @@ export function RoutePathStateProvider({
|
|||||||
}: Pick<RoutePathStateContextType, "path" | "defaultIdentifier"> & { children: React.ReactNode }) {
|
}: Pick<RoutePathStateContextType, "path" | "defaultIdentifier"> & { children: React.ReactNode }) {
|
||||||
const segment = extractPathSegment(path);
|
const segment = extractPathSegment(path);
|
||||||
const routeIdentifier = useParams()[segment];
|
const routeIdentifier = useParams()[segment];
|
||||||
const [activeIdentifier, setActiveIdentifier] = useState(routeIdentifier ?? defaultIdentifier);
|
const [activeIdentifier, setActiveIdentifier] = useState(
|
||||||
|
routeIdentifier ?? defaultIdentifier ?? "",
|
||||||
|
);
|
||||||
return (
|
return (
|
||||||
<RoutePathStateContext.Provider
|
<RoutePathStateContext.Provider
|
||||||
value={{ defaultIdentifier, path, activeIdentifier, setActiveIdentifier }}
|
value={{ defaultIdentifier, path, activeIdentifier, setActiveIdentifier }}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { useClipboard } from "@mantine/hooks";
|
|||||||
import { ButtonLink } from "ui/components/buttons/Button";
|
import { ButtonLink } from "ui/components/buttons/Button";
|
||||||
import { routes } from "ui/lib/routes";
|
import { routes } from "ui/lib/routes";
|
||||||
import { useBkndMedia } from "ui/client/schema/media/use-bknd-media";
|
import { useBkndMedia } from "ui/client/schema/media/use-bknd-media";
|
||||||
import { JsonViewer } from "ui";
|
import { JsonViewer } from "ui/components/code/JsonViewer";
|
||||||
|
|
||||||
export type MediaInfoModalProps = {
|
export type MediaInfoModalProps = {
|
||||||
file: FileState;
|
file: FileState;
|
||||||
|
|||||||
@@ -1,17 +1,43 @@
|
|||||||
import { MarkerType, type Node, Position, ReactFlowProvider } from "@xyflow/react";
|
import { MarkerType, type Node, Position, ReactFlowProvider, useReactFlow } from "@xyflow/react";
|
||||||
import type { AppDataConfig, TAppDataEntity } from "data/data-schema";
|
import type { AppDataConfig, TAppDataEntity, TAppDataField } from "data/data-schema";
|
||||||
import { useBknd } from "ui/client/BkndProvider";
|
import { useBknd } from "ui/client/BkndProvider";
|
||||||
import { Canvas } from "ui/components/canvas/Canvas";
|
import { Canvas, type CanvasProps } from "ui/components/canvas/Canvas";
|
||||||
import { layoutWithDagre } from "ui/components/canvas/layouts";
|
import { layoutWithDagre } from "ui/components/canvas/layouts";
|
||||||
import { Panels } from "ui/components/canvas/panels";
|
import { Panels } from "ui/components/canvas/panels";
|
||||||
import { EntityTableNode } from "./EntityTableNode";
|
import { EntityTableNode } from "./EntityTableNode";
|
||||||
import { useTheme } from "ui/client/use-theme";
|
import { useTheme } from "ui/client/use-theme";
|
||||||
|
import { useCallback } from "react";
|
||||||
|
import { mergeObject, transformObject } from "core/utils";
|
||||||
|
|
||||||
|
export interface TCanvasEntityField extends TAppDataField {
|
||||||
|
name: string;
|
||||||
|
indexed?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TCanvasEntity extends TAppDataEntity {
|
||||||
|
label: string;
|
||||||
|
fields: Record<string, TCanvasEntityField>;
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
function entitiesToNodes(
|
||||||
|
entities: AppDataConfig["entities"],
|
||||||
|
indices?: AppDataConfig["indices"],
|
||||||
|
): Node<TCanvasEntity>[] {
|
||||||
|
const indexed_fields = Object.entries(indices ?? {}).flatMap(([, index]) => index.fields);
|
||||||
|
|
||||||
function entitiesToNodes(entities: AppDataConfig["entities"]): Node<TAppDataEntity>[] {
|
|
||||||
return Object.entries(entities ?? {}).map(([name, entity]) => {
|
return Object.entries(entities ?? {}).map(([name, entity]) => {
|
||||||
return {
|
return {
|
||||||
id: name,
|
id: name,
|
||||||
data: { label: name, ...entity },
|
data: {
|
||||||
|
...entity,
|
||||||
|
label: name,
|
||||||
|
fields: transformObject(entity.fields ?? {}, (f, name) => ({
|
||||||
|
...f,
|
||||||
|
name,
|
||||||
|
indexed: indexed_fields.includes(name),
|
||||||
|
})),
|
||||||
|
},
|
||||||
type: "entity",
|
type: "entity",
|
||||||
dragHandle: ".drag-handle",
|
dragHandle: ".drag-handle",
|
||||||
position: { x: 0, y: 0 },
|
position: { x: 0, y: 0 },
|
||||||
@@ -65,24 +91,29 @@ const nodeTypes = {
|
|||||||
entity: EntityTableNode.Component,
|
entity: EntityTableNode.Component,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
const getEdgeStyle = (theme: string) => ({
|
||||||
|
stroke: theme === "light" ? "#ccc" : "#666",
|
||||||
|
});
|
||||||
|
|
||||||
|
const getMarkerEndStyle = (theme: string) => ({
|
||||||
|
type: MarkerType.Arrow,
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
color: theme === "light" ? "#aaa" : "#777",
|
||||||
|
});
|
||||||
|
|
||||||
export function DataSchemaCanvas() {
|
export function DataSchemaCanvas() {
|
||||||
const {
|
const {
|
||||||
config: { data },
|
config: { data },
|
||||||
} = useBknd();
|
} = useBknd();
|
||||||
const { theme } = useTheme();
|
const { theme } = useTheme();
|
||||||
const nodes = entitiesToNodes(data.entities);
|
const nodes = entitiesToNodes(data.entities, data.indices);
|
||||||
|
console.log(nodes);
|
||||||
const edges = relationsToEdges(data.relations).map((e) => ({
|
const edges = relationsToEdges(data.relations).map((e) => ({
|
||||||
...e,
|
...e,
|
||||||
style: {
|
style: getEdgeStyle(theme),
|
||||||
stroke: theme === "light" ? "#ccc" : "#666",
|
|
||||||
},
|
|
||||||
type: "smoothstep",
|
type: "smoothstep",
|
||||||
markerEnd: {
|
markerEnd: getMarkerEndStyle(theme),
|
||||||
type: MarkerType.Arrow,
|
|
||||||
width: 20,
|
|
||||||
height: 20,
|
|
||||||
color: theme === "light" ? "#aaa" : "#777",
|
|
||||||
},
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const nodeLayout = layoutWithDagre({
|
const nodeLayout = layoutWithDagre({
|
||||||
@@ -107,7 +138,7 @@ export function DataSchemaCanvas() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ReactFlowProvider>
|
<ReactFlowProvider>
|
||||||
<Canvas
|
<ActualCanvas
|
||||||
nodes={nodes}
|
nodes={nodes}
|
||||||
edges={edges}
|
edges={edges}
|
||||||
nodeTypes={nodeTypes}
|
nodeTypes={nodeTypes}
|
||||||
@@ -119,7 +150,49 @@ export function DataSchemaCanvas() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Panels zoom minimap />
|
<Panels zoom minimap />
|
||||||
</Canvas>
|
</ActualCanvas>
|
||||||
</ReactFlowProvider>
|
</ReactFlowProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toRecord(nodes: Node<TAppDataEntity>[]): Record<string, Node<TAppDataEntity>> {
|
||||||
|
return Object.fromEntries(nodes.map((n) => [n.id, n]));
|
||||||
|
}
|
||||||
|
|
||||||
|
function ActualCanvas(props: CanvasProps) {
|
||||||
|
const flow = useReactFlow();
|
||||||
|
const { theme } = useTheme();
|
||||||
|
|
||||||
|
const onNodesChange = useCallback((changes: any) => {
|
||||||
|
const nodes = mergeObject<Record<string, Node<TAppDataEntity>>>(
|
||||||
|
toRecord(flow.getNodes()),
|
||||||
|
toRecord(changes),
|
||||||
|
);
|
||||||
|
const selected = Object.values(nodes).filter((n) => n.selected);
|
||||||
|
const selected_names = selected.map((n) => n.id);
|
||||||
|
flow.setEdges((edges) =>
|
||||||
|
edges.map((edge) => {
|
||||||
|
if (selected_names.includes(edge.source) || selected_names.includes(edge.target)) {
|
||||||
|
return {
|
||||||
|
...edge,
|
||||||
|
animated: true,
|
||||||
|
style: { stroke: "#6495c6" },
|
||||||
|
markerEnd: {
|
||||||
|
...getMarkerEndStyle(theme),
|
||||||
|
color: "#6495c6",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...edge,
|
||||||
|
style: getEdgeStyle(theme),
|
||||||
|
animated: false,
|
||||||
|
markerEnd: getMarkerEndStyle(theme),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return <Canvas {...props} onNodesChange={onNodesChange as any} />;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,26 +1,21 @@
|
|||||||
import { Handle, type Node, type NodeProps, Position } from "@xyflow/react";
|
import { Handle, type Node, type NodeProps, Position, useReactFlow } from "@xyflow/react";
|
||||||
|
|
||||||
import type { TAppDataEntity } from "data/data-schema";
|
import type { TAppDataEntity } from "data/data-schema";
|
||||||
import { useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { TbDiamonds, TbKey } from "react-icons/tb";
|
import { TbBolt, TbDiamonds, TbKey } from "react-icons/tb";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
import { DefaultNode } from "ui/components/canvas/components/nodes/DefaultNode";
|
import { DefaultNode } from "ui/components/canvas/components/nodes/DefaultNode";
|
||||||
|
import { useTheme } from "ui/client/use-theme";
|
||||||
|
import { useNavigate } from "ui/lib/routes";
|
||||||
|
import type { TCanvasEntity, TCanvasEntityField } from "./DataSchemaCanvas";
|
||||||
|
|
||||||
export type TableProps = {
|
export type TableProps = {
|
||||||
name: string;
|
name: string;
|
||||||
type?: string;
|
type?: string;
|
||||||
fields: TableField[];
|
fields: TCanvasEntityField[];
|
||||||
};
|
|
||||||
export type TableField = {
|
|
||||||
name: string;
|
|
||||||
type: string;
|
|
||||||
primary?: boolean;
|
|
||||||
foreign?: boolean;
|
|
||||||
indexed?: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function NodeComponent(props: NodeProps<Node<TAppDataEntity & { label: string }>>) {
|
function NodeComponent(props: NodeProps<Node<TCanvasEntity>>) {
|
||||||
const [hovered, setHovered] = useState(false);
|
|
||||||
const { data } = props;
|
const { data } = props;
|
||||||
const fields = props.data.fields ?? {};
|
const fields = props.data.fields ?? {};
|
||||||
const field_count = Object.keys(fields).length;
|
const field_count = Object.keys(fields).length;
|
||||||
@@ -32,10 +27,11 @@ function NodeComponent(props: NodeProps<Node<TAppDataEntity & { label: string }>
|
|||||||
{Object.entries(fields).map(([name, field], index) => (
|
{Object.entries(fields).map(([name, field], index) => (
|
||||||
<TableRow
|
<TableRow
|
||||||
key={index}
|
key={index}
|
||||||
field={{ name, ...field }}
|
field={field}
|
||||||
table={data.label}
|
table={data.label}
|
||||||
index={index}
|
index={index}
|
||||||
last={field_count === index + 1}
|
last={field_count === index + 1}
|
||||||
|
selected={props.selected && ["relation", "primary"].includes(field.type)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -53,13 +49,16 @@ const TableRow = ({
|
|||||||
index,
|
index,
|
||||||
onHover,
|
onHover,
|
||||||
last,
|
last,
|
||||||
|
selected,
|
||||||
}: {
|
}: {
|
||||||
field: TableField;
|
field: TCanvasEntityField;
|
||||||
table: string;
|
table: string;
|
||||||
index: number;
|
index: number;
|
||||||
last?: boolean;
|
last?: boolean;
|
||||||
|
selected?: boolean;
|
||||||
onHover?: (hovered: boolean) => void;
|
onHover?: (hovered: boolean) => void;
|
||||||
}) => {
|
}) => {
|
||||||
|
const [navigate] = useNavigate();
|
||||||
const handleTop = HEIGHTS.header + HEIGHTS.row * index + HEIGHTS.row / 2;
|
const handleTop = HEIGHTS.header + HEIGHTS.row * index + HEIGHTS.row / 2;
|
||||||
const handles = true;
|
const handles = true;
|
||||||
const handleId = `${table}:${field.name}`;
|
const handleId = `${table}:${field.name}`;
|
||||||
@@ -67,10 +66,12 @@ const TableRow = ({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
"flex flex-row w-full justify-between font-mono py-1.5 px-2.5 border-b border-primary/15 border-l border-r cursor-auto",
|
"flex flex-row w-full justify-between font-mono py-1.5 px-2.5 border-b border-primary/15 border-l border-r cursor-pointer",
|
||||||
last && "rounded-bl-lg rounded-br-lg",
|
last && "rounded-bl-lg rounded-br-lg",
|
||||||
|
selected && "bg-primary/5",
|
||||||
"hover:bg-primary/5",
|
"hover:bg-primary/5",
|
||||||
)}
|
)}
|
||||||
|
onClick={() => navigate(`/entity/${table}/fields/${field.name}`)}
|
||||||
>
|
>
|
||||||
{handles && (
|
{handles && (
|
||||||
<Handle
|
<Handle
|
||||||
@@ -86,7 +87,12 @@ const TableRow = ({
|
|||||||
{field.type === "primary" && <TbKey className="text-yellow-700" />}
|
{field.type === "primary" && <TbKey className="text-yellow-700" />}
|
||||||
{field.type === "relation" && <TbDiamonds className="text-sky-700" />}
|
{field.type === "relation" && <TbDiamonds className="text-sky-700" />}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-grow">{field.name}</div>
|
<div className="flex flex-grow items-center gap-1">
|
||||||
|
<span className={field.config?.required ? "font-bold" : "opacity-90"}>
|
||||||
|
{field.name}
|
||||||
|
</span>{" "}
|
||||||
|
{field.indexed && <TbBolt className="text-warning-foreground/50" />}
|
||||||
|
</div>
|
||||||
<div className="flex opacity-60">{field.type}</div>
|
<div className="flex opacity-60">{field.type}</div>
|
||||||
|
|
||||||
{handles && (
|
{handles && (
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { isDebug } from "core";
|
import { isDebug } from "core";
|
||||||
import { autoFormatString } from "core/utils";
|
import { autoFormatString } from "core/utils";
|
||||||
import { type ChangeEvent, useState } from "react";
|
import type { ChangeEvent } from "react";
|
||||||
import {
|
import {
|
||||||
TbAt,
|
TbAt,
|
||||||
TbBrandAppleFilled,
|
TbBrandAppleFilled,
|
||||||
@@ -13,7 +13,6 @@ import {
|
|||||||
TbBrandX,
|
TbBrandX,
|
||||||
TbSettings,
|
TbSettings,
|
||||||
} from "react-icons/tb";
|
} from "react-icons/tb";
|
||||||
import { twMerge } from "tailwind-merge";
|
|
||||||
import { useBknd } from "ui/client/bknd";
|
import { useBknd } from "ui/client/bknd";
|
||||||
import { useBkndAuth } from "ui/client/schema/auth/use-bknd-auth";
|
import { useBkndAuth } from "ui/client/schema/auth/use-bknd-auth";
|
||||||
import { Button } from "ui/components/buttons/Button";
|
import { Button } from "ui/components/buttons/Button";
|
||||||
|
|||||||
@@ -83,10 +83,6 @@ export function DataRoot({ children }) {
|
|||||||
</AppShell.SectionHeader>
|
</AppShell.SectionHeader>
|
||||||
<AppShell.Scrollable initialOffset={96}>
|
<AppShell.Scrollable initialOffset={96}>
|
||||||
<div className="flex flex-col flex-grow py-3 gap-3">
|
<div className="flex flex-col flex-grow py-3 gap-3">
|
||||||
{/*<div className="pt-3 px-3">
|
|
||||||
<SearchInput placeholder="Search entities" />
|
|
||||||
</div>*/}
|
|
||||||
|
|
||||||
<EntityLinkList entities={entityList.regular} context={context} suggestCreate />
|
<EntityLinkList entities={entityList.regular} context={context} suggestCreate />
|
||||||
<EntityLinkList entities={entityList.system} context={context} title="System" />
|
<EntityLinkList entities={entityList.system} context={context} title="System" />
|
||||||
<EntityLinkList
|
<EntityLinkList
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ import { fieldSpecs } from "ui/modules/data/components/fields-specs";
|
|||||||
import { extractSchema } from "../settings/utils/schema";
|
import { extractSchema } from "../settings/utils/schema";
|
||||||
import { EntityFieldsForm, type EntityFieldsFormRef } from "./forms/entity.fields.form";
|
import { EntityFieldsForm, type EntityFieldsFormRef } from "./forms/entity.fields.form";
|
||||||
import { RoutePathStateProvider } from "ui/hooks/use-route-path-state";
|
import { RoutePathStateProvider } from "ui/hooks/use-route-path-state";
|
||||||
|
import { EntityIndicesForm } from "./forms/entity.indices.form";
|
||||||
|
import type { TAppDataIndex } from "data/data-schema";
|
||||||
|
|
||||||
export function DataSchemaEntity({ params }) {
|
export function DataSchemaEntity({ params }) {
|
||||||
const { $data } = useBkndData();
|
const { $data } = useBkndData();
|
||||||
@@ -42,106 +44,100 @@ export function DataSchemaEntity({ params }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RoutePathStateProvider path={`/entity/${entity.name}/:setting?`} defaultIdentifier="fields">
|
<>
|
||||||
<AppShell.SectionHeader
|
<RoutePathStateProvider
|
||||||
right={
|
path={`/entity/${entity.name}/:setting?`}
|
||||||
<>
|
defaultIdentifier="fields"
|
||||||
<Dropdown
|
|
||||||
items={[
|
|
||||||
{
|
|
||||||
label: "Data",
|
|
||||||
onClick: () =>
|
|
||||||
navigate(routes.data.root() + routes.data.entity.list(entity.name), {
|
|
||||||
absolute: true,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Advanced Settings",
|
|
||||||
onClick: () =>
|
|
||||||
navigate(routes.settings.path(["data", "entities", entity.name]), {
|
|
||||||
absolute: true,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
position="bottom-end"
|
|
||||||
>
|
|
||||||
<IconButton Icon={TbDots} />
|
|
||||||
</Dropdown>
|
|
||||||
<Dropdown
|
|
||||||
items={[
|
|
||||||
{
|
|
||||||
icon: TbCirclesRelation,
|
|
||||||
label: "Add relation",
|
|
||||||
onClick: () => $data.modals.createRelation(entity.name),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: TbPhoto,
|
|
||||||
label: "Add media",
|
|
||||||
onClick: () => $data.modals.createMedia(entity.name),
|
|
||||||
},
|
|
||||||
() => <div className="h-px my-1 w-full bg-primary/5" />,
|
|
||||||
{
|
|
||||||
icon: TbDatabasePlus,
|
|
||||||
label: "Create Entity",
|
|
||||||
onClick: () => $data.modals.createEntity(),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
position="bottom-end"
|
|
||||||
>
|
|
||||||
<Button IconRight={TbPlus}>Add</Button>
|
|
||||||
</Dropdown>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
className="pl-3"
|
|
||||||
>
|
>
|
||||||
<div className="flex flex-row gap-4">
|
<AppShell.SectionHeader
|
||||||
<Breadcrumbs2
|
right={
|
||||||
path={[{ label: "Schema", href: "/" }, { label: entity.label }]}
|
<>
|
||||||
backTo="/"
|
<Dropdown
|
||||||
/>
|
items={[
|
||||||
<Link to="/" className="hidden md:inline">
|
{
|
||||||
<Button IconLeft={TbSitemap}>Overview</Button>
|
label: "Data",
|
||||||
</Link>
|
onClick: () =>
|
||||||
</div>
|
navigate(
|
||||||
</AppShell.SectionHeader>
|
routes.data.root() + routes.data.entity.list(entity.name),
|
||||||
<div className="flex flex-col h-full" key={entity.name}>
|
{
|
||||||
<Fields entity={entity} />
|
absolute: true,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Advanced Settings",
|
||||||
|
onClick: () =>
|
||||||
|
navigate(routes.settings.path(["data", "entities", entity.name]), {
|
||||||
|
absolute: true,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
position="bottom-end"
|
||||||
|
>
|
||||||
|
<IconButton Icon={TbDots} />
|
||||||
|
</Dropdown>
|
||||||
|
<Dropdown
|
||||||
|
items={[
|
||||||
|
{
|
||||||
|
icon: TbCirclesRelation,
|
||||||
|
label: "Add relation",
|
||||||
|
onClick: () => $data.modals.createRelation(entity.name),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: TbPhoto,
|
||||||
|
label: "Add media",
|
||||||
|
onClick: () => $data.modals.createMedia(entity.name),
|
||||||
|
},
|
||||||
|
() => <div className="h-px my-1 w-full bg-primary/5" />,
|
||||||
|
{
|
||||||
|
icon: TbDatabasePlus,
|
||||||
|
label: "Create Entity",
|
||||||
|
onClick: () => $data.modals.createEntity(),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
position="bottom-end"
|
||||||
|
>
|
||||||
|
<Button IconRight={TbPlus}>Add</Button>
|
||||||
|
</Dropdown>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
className="pl-3"
|
||||||
|
>
|
||||||
|
<div className="flex flex-row gap-4">
|
||||||
|
<Breadcrumbs2
|
||||||
|
path={[{ label: "Schema", href: "/" }, { label: entity.label }]}
|
||||||
|
backTo="/"
|
||||||
|
/>
|
||||||
|
<Link to="/" className="hidden md:inline">
|
||||||
|
<Button IconLeft={TbSitemap}>Overview</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</AppShell.SectionHeader>
|
||||||
|
<div className="flex flex-col h-full" key={entity.name}>
|
||||||
|
<Fields entity={entity} />
|
||||||
|
|
||||||
<BasicSettings entity={entity} />
|
<BasicSettings entity={entity} />
|
||||||
<AppShell.RouteAwareSectionHeaderAccordionItem
|
<AppShell.RouteAwareSectionHeaderAccordionItem
|
||||||
identifier="relations"
|
identifier="relations"
|
||||||
title="Relations"
|
|
||||||
ActiveIcon={IconCirclesRelation}
|
|
||||||
>
|
|
||||||
<Empty
|
|
||||||
title="Relations"
|
title="Relations"
|
||||||
description="This will soon be available here. Meanwhile, check advanced settings."
|
ActiveIcon={IconCirclesRelation}
|
||||||
primary={{
|
>
|
||||||
children: "Advanced Settings",
|
<Empty
|
||||||
onClick: () =>
|
title="Relations"
|
||||||
navigate(routes.settings.path(["data", "relations"]), { absolute: true }),
|
description="This will soon be available here. Meanwhile, check advanced settings."
|
||||||
}}
|
primary={{
|
||||||
/>
|
children: "Advanced Settings",
|
||||||
</AppShell.RouteAwareSectionHeaderAccordionItem>
|
onClick: () =>
|
||||||
<AppShell.RouteAwareSectionHeaderAccordionItem
|
navigate(routes.settings.path(["data", "relations"]), {
|
||||||
identifier="indices"
|
absolute: true,
|
||||||
title="Indices"
|
}),
|
||||||
ActiveIcon={IconBolt}
|
}}
|
||||||
>
|
/>
|
||||||
<Empty
|
</AppShell.RouteAwareSectionHeaderAccordionItem>
|
||||||
title="Indices"
|
<Indices entity={entity} />
|
||||||
description="This will soon be available here. Meanwhile, check advanced settings."
|
</div>
|
||||||
primary={{
|
</RoutePathStateProvider>
|
||||||
children: "Advanced Settings",
|
</>
|
||||||
onClick: () =>
|
|
||||||
navigate(routes.settings.path(["data", "indices"]), {
|
|
||||||
absolute: true,
|
|
||||||
}),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</AppShell.RouteAwareSectionHeaderAccordionItem>
|
|
||||||
</div>
|
|
||||||
</RoutePathStateProvider>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -281,3 +277,37 @@ const BasicSettings = ({ entity }: { entity: Entity }) => {
|
|||||||
</AppShell.RouteAwareSectionHeaderAccordionItem>
|
</AppShell.RouteAwareSectionHeaderAccordionItem>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const Indices = ({ entity }: { entity: Entity }) => {
|
||||||
|
const [navigate] = useNavigate();
|
||||||
|
const [data, setData] = useState<Record<string, TAppDataIndex>>({});
|
||||||
|
const d = useBkndData();
|
||||||
|
const [submitting, setSubmitting] = useState(false);
|
||||||
|
async function handleUpdate() {
|
||||||
|
console.log("update", data);
|
||||||
|
return;
|
||||||
|
/*if (submitting) return;
|
||||||
|
setSubmitting(true);
|
||||||
|
await d.actions.entity.patch(entity.name).indices.set(data);
|
||||||
|
setSubmitting(false);*/
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AppShell.RouteAwareSectionHeaderAccordionItem
|
||||||
|
identifier="indices"
|
||||||
|
title="Indices"
|
||||||
|
ActiveIcon={IconBolt}
|
||||||
|
renderHeaderRight={({ open }) =>
|
||||||
|
open ? (
|
||||||
|
<Button variant="primary" disabled={!open || submitting} onClick={handleUpdate}>
|
||||||
|
Update
|
||||||
|
</Button>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="flex flex-col flex-grow py-3 px-4 max-w-4xl gap-3 relative">
|
||||||
|
<EntityIndicesForm entity={entity} onChange={setData} />
|
||||||
|
</div>
|
||||||
|
</AppShell.RouteAwareSectionHeaderAccordionItem>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -0,0 +1,118 @@
|
|||||||
|
import { TbBolt, TbTrash } from "react-icons/tb";
|
||||||
|
import type { Entity } from "data/entities";
|
||||||
|
import { IconButton } from "ui/components/buttons/IconButton";
|
||||||
|
import { useBkndData } from "ui/client/schema/data/use-bknd-data";
|
||||||
|
import { CollapsibleList } from "ui/components/list/CollapsibleList";
|
||||||
|
import { EntityIndex } from "data";
|
||||||
|
import { Button } from "ui/components/buttons/Button";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { JsonViewer } from "ui/components/code/JsonViewer";
|
||||||
|
import type { TAppDataIndex } from "data/data-schema";
|
||||||
|
import * as Formy from "ui/components/form/Formy";
|
||||||
|
|
||||||
|
export interface EntityIndicesFormProps {
|
||||||
|
entity: Entity;
|
||||||
|
onChange?: (indices: Record<string, TAppDataIndex>) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EntityIndicesForm({ entity, onChange }: EntityIndicesFormProps) {
|
||||||
|
const { $data } = useBkndData();
|
||||||
|
const [indices, setIndices] = useState<EntityIndex[]>($data.indicesOf(entity.name));
|
||||||
|
const [create, setCreate] = useState<TAppDataIndex>({
|
||||||
|
entity: entity.name,
|
||||||
|
fields: [],
|
||||||
|
unique: false,
|
||||||
|
});
|
||||||
|
const indexed_fields = indices.flatMap((i) => i.fields).map((f) => f.name);
|
||||||
|
const required_fields = indices
|
||||||
|
.flatMap((i) => i.fields)
|
||||||
|
.filter((f) => f.isRequired())
|
||||||
|
.map((f) => f.name);
|
||||||
|
|
||||||
|
const fields = entity.fields.filter(
|
||||||
|
(f) => !["primary", "relation", "media"].includes(f.type) && !indexed_fields.includes(f.name),
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onChange?.(Object.fromEntries(indices.map((i) => [i.name, i.toJSON()])));
|
||||||
|
}, [indices]);
|
||||||
|
|
||||||
|
function handleAdd() {
|
||||||
|
setIndices((prev) => [
|
||||||
|
...prev,
|
||||||
|
new EntityIndex(
|
||||||
|
entity,
|
||||||
|
create.fields.map((f) => entity.fields.find((f2) => f2.name === f)!),
|
||||||
|
create.unique,
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
//console.log("indices", { indices, schema, config });
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
{indices.map((index) => (
|
||||||
|
<CollapsibleList.Item key={index.name} title={index.name}>
|
||||||
|
<CollapsibleList.Preview
|
||||||
|
left={<TbBolt />}
|
||||||
|
right={<IconButton size="lg" Icon={TbTrash} />}
|
||||||
|
>
|
||||||
|
<span>{index.fields.map((f) => f.name).join(", ")}</span>
|
||||||
|
<span className="opacity-50">{index.name}</span>
|
||||||
|
</CollapsibleList.Preview>
|
||||||
|
</CollapsibleList.Item>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-row gap-7 items-center">
|
||||||
|
<span className="font-bold">Add Index</span>
|
||||||
|
<div className="flex flex-row gap-2">
|
||||||
|
<Formy.Label className="opacity-70">Unique</Formy.Label>
|
||||||
|
<Formy.Switch
|
||||||
|
checked={create.unique}
|
||||||
|
size="sm"
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
setCreate((prev) => ({
|
||||||
|
...prev,
|
||||||
|
unique: checked,
|
||||||
|
fields: prev.fields.some((f) => required_fields.includes(f))
|
||||||
|
? prev.fields
|
||||||
|
: [],
|
||||||
|
}));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-row flex-wrap gap-2 items-center">
|
||||||
|
<Formy.Label className="opacity-70">Field</Formy.Label>
|
||||||
|
<div className="min-w-0">
|
||||||
|
<Formy.Select
|
||||||
|
className="h-9 py-1.5 pl-3 pr-8"
|
||||||
|
options={fields.map((f) => ({
|
||||||
|
label: f.getLabel()!,
|
||||||
|
value: f.name,
|
||||||
|
disabled: create.unique && !required_fields.includes(f.name),
|
||||||
|
}))}
|
||||||
|
value={create.fields[0]}
|
||||||
|
onChange={(e) => {
|
||||||
|
setCreate((prev) => ({ ...prev, fields: [e.target.value] }));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-grow" />
|
||||||
|
<Button variant="primary" disabled={create.fields.length === 0} onClick={handleAdd}>
|
||||||
|
Add
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<JsonViewer
|
||||||
|
json={{
|
||||||
|
create,
|
||||||
|
data: Object.fromEntries(indices.map((i) => [i.name, i.toJSON()])),
|
||||||
|
}}
|
||||||
|
expand={9}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user