import { Switch, TextInput } from "@mantine/core"; import { IconDatabase } from "@tabler/icons-react"; import { DEFAULT_RELATION_CASCADE, ManyToOneRelation, type RelationType, RelationCascades, RelationTypes, } from "data/relations"; import type { ReactNode } from "react"; import { type Control, type FieldValues, type UseFormRegister, useForm } from "react-hook-form"; import { TbRefresh } from "react-icons/tb"; import { useBknd } from "ui/client/bknd"; import { Button } from "ui/components/buttons/Button"; import { MantineNumberInput } from "ui/components/form/hook-form-mantine/MantineNumberInput"; import { MantineSelect } from "ui/components/form/hook-form-mantine/MantineSelect"; import { useStepContext } from "ui/components/steps/Steps"; import { useEvent } from "ui/hooks/use-event"; import { ModalBody, ModalFooter } from "./CreateModal"; import type { TCreateModalSchema } from "./schema"; import { s, stringIdentifier } from "bknd/utils"; import { standardSchemaResolver } from "@hookform/resolvers/standard-schema"; const Relations: { type: RelationType; label: string; component: (props: ComponentCtx) => ReactNode; }[] = [ { type: RelationTypes.ManyToOne, label: "Many to One", component: ManyToOne, }, { type: RelationTypes.OneToOne, label: "One to One", component: OneToOne, }, { type: RelationTypes.ManyToMany, label: "Many to Many", component: ManyToMany, }, { type: RelationTypes.Polymorphic, label: "Polymorphic", component: Polymorphic, }, ]; const schema = s.strictObject({ type: s.string({ enum: Relations.map((r) => r.type) }), source: stringIdentifier, target: stringIdentifier, config: s.object({ on_delete: s.string({ enum: RelationCascades }).optional(), }), }); type ComponentCtx = { register: UseFormRegister; control: Control; data: T; }; export function StepRelation() { const { config } = useBknd(); const entities = config.data.entities; const count = Object.keys(entities ?? {}).length; const { nextStep, stepBack, state, path, setState } = useStepContext(); const { register, handleSubmit, formState: { isValid }, setValue, watch, control, } = useForm({ resolver: standardSchemaResolver(schema), defaultValues: (state.relations?.create?.[0] ?? {}) as s.Static, }); const data = watch(); function handleNext() { if (isValid) { setState((prev) => { return { ...prev, relations: { create: [data], }, }; }); console.log("data", data); nextStep("create")(); } } const flip = useEvent(() => { const { source, target } = data; if (source && target) { setValue("source", target); setValue("target", source); } else { if (source) { setValue("target", source); setValue("source", null as any); } else { setValue("source", target); setValue("target", null as any); } } }); return ( <> {count < 2 && (
Not enough entities to create a relation.
)}
({ value: name, label: entity.config?.name ?? name, disabled: data.target === name, }))} />
setValue("config", {})} label="Relation Type" data={Relations.map((r) => ({ value: r.type, label: r.label }))} allowDeselect={false} /> {data.type && (
)}
({ value: name, label: entity.config?.name ?? name, disabled: data.source === name, }))} />
{data.type && Relations.find((r) => r.type === data.type)?.component({ register, control, data, })}
); } const Pre = ({ children }: { children: ReactNode }) => ( {children} ); const Callout = ({ children }: { children: ReactNode }) => (
{children}
); function OnDelete({ control, label = "On target delete", }: { control: Control; label?: string; }) { return ( ({ value: cascade, label: cascade }))} /> ); } function ManyToOne({ register, control, data: { source, target, config } }: ComponentCtx) { return ( <>
{source && target && config && (
                  
                  {source}.{config.mappedBy || target}_id {"→"} {target}
               

Many

{source}
will each have one reference to
{target}
.

A property

{config.mappedBy || target}_id
will be added to{" "}
{source}
(which references
{target}
).

When creating

{source}
, a reference to
{target}
is{" "} {config.required ? "required" : "optional"}.

If a

{target}
entry is deleted, the reference is{" "} {config.on_delete ?? DEFAULT_RELATION_CASCADE}.

{config.sourceCardinality ? (

{target}
should not have more than{" "}
{config.sourceCardinality}
referencing entr {config.sourceCardinality === 1 ? "y" : "ies"} to
{source}
.

) : null}
)} ); } function OneToOne({ register, control, data: { source, target, config: { mappedBy, required, on_delete }, }, }: ComponentCtx) { return ( <>
{source && target && (
                  
                  {source}.{mappedBy || target}_id {"↔"} {target}
               

A single entry of

{source}
will have a reference to
{target}
.

A property

{mappedBy || target}_id
will be added to
{source}
{" "} (which references
{target}
).

When creating

{source}
, a reference to
{target}
is{" "} {required ? "required" : "optional"}.

If a

{target}
entry is deleted, the reference is{" "} {on_delete ?? DEFAULT_RELATION_CASCADE}.

)} ); } function ManyToMany({ register, control, data: { source, target, config } }: ComponentCtx) { const table = config.connectionTable ? config.connectionTable : source && target ? `${source}_${target}` : ""; return ( <>
{/**/}
{/**/}
{source && target && (
                  
                  {source} {"→"} {table} {"←"} {target}
               

Many

{source}
will have many
{target}
.

A connection table

{table}
will be created to store the relations.

If an entry of

{source}
or
{target}
is deleted, the connection is {config.on_delete ?? DEFAULT_RELATION_CASCADE}.

)} ); } function Polymorphic({ register, control, data: { type, source, target, config } }: ComponentCtx) { return ( <>
{source && target && (
                  
                  {source} {"←"} {target}
               

{source}
will have many
{target}
.

{target}
will get additional properties
reference
and{" "}
entity_id
to make the (polymorphic) reference.

{config.targetCardinality ? (

{source}
should not have more than{" "}
{config.targetCardinality}
reference {config.targetCardinality === 1 ? "" : "s"} to
{target}
.

) : null}
)} ); }