Files
bknd/app/src/ui/modules/data/components/fields/EntityRelationalFormField.tsx

260 lines
8.7 KiB
TypeScript

import { getHotkeyHandler, useHotkeys } from "@mantine/hooks";
import type { FieldApi } from "@tanstack/react-form";
import { ucFirst } from "core/utils";
import type { EntityData, RelationField } from "data";
import { useEffect, useRef, useState } from "react";
import { TbEye } from "react-icons/tb";
import { useClient } from "ui/client";
import { useBknd } from "ui/client/bknd";
import { Button } from "ui/components/buttons/Button";
import * as Formy from "ui/components/form/Formy";
import { Popover } from "ui/components/overlay/Popover";
import { useEntities } from "ui/container";
import { routes } from "ui/lib/routes";
import { useLocation } from "wouter";
import { EntityTable } from "../EntityTable";
// @todo: allow clear if not required
export function EntityRelationalFormField({
fieldApi,
field,
data,
disabled,
tabIndex
}: {
fieldApi: FieldApi<any, any>;
field: RelationField;
data?: EntityData;
disabled?: boolean;
tabIndex?: number;
}) {
const { app } = useBknd();
const entity = app.entity(field.target())!;
const [query, setQuery] = useState<any>({ limit: 10, page: 1, perPage: 10 });
const [location, navigate] = useLocation();
const ref = useRef<any>(null);
const client = useClient();
const container = useEntities(
field.target(),
{
limit: query.limit,
offset: (query.page - 1) * query.limit
//select: entity.getSelect(undefined, "form")
},
{ enabled: true }
);
const [_value, _setValue] = useState<{ id: number | undefined; [key: string]: any }>();
const referenceField = data?.[field.reference()];
const relationalField = data?.[field.name];
useEffect(() => {
_setValue(data?.[field.reference()]);
}, [referenceField]);
useEffect(() => {
(async () => {
const rel_value = field.target();
if (!rel_value || !relationalField) return;
console.log("-- need to fetch", field.target(), relationalField);
const fetched = await client.api.data.readOne(field.target(), relationalField);
if (fetched.res.ok && fetched.data) {
_setValue(fetched.data as any);
}
console.log("-- fetched", fetched);
console.log("relation", {
referenceField,
relationalField,
data,
field,
entity
});
})();
}, [relationalField]);
/*const initialValue: { id: number | undefined; [key: string]: any } = data?.[
field.reference()
] ?? {
id: data?.[field.name],
};*/
function handleViewItem(e: React.MouseEvent<HTMLButtonElement>) {
e.preventDefault();
e.stopPropagation();
console.log("yo");
if (_value) {
navigate(routes.data.entity.edit(entity.name, _value.id as any));
}
}
/*console.log(
"relationfield:data",
{ _value, initialValue },
data,
field.reference(),
entity,
//container.entity,
//data[field.reference()],
data?.[field.name],
field,
);*/
// fix missing value on fields that are required
useEffect(() => {
if (field.isRequired() && !fieldApi.state.value) {
fieldApi.setValue(container.data?.[0]?.id);
}
}, [container.data]);
return (
<Formy.Group>
<Formy.Label htmlFor={fieldApi.name}>{field.getLabel()}</Formy.Label>
<div
data-disabled={!Array.isArray(container.data) || disabled ? 1 : undefined}
className="data-[disabled]:opacity-70 data-[disabled]:pointer-events-none"
>
<Popover
backdrop
className=""
target={({ toggle }) => (
<PopoverTable
container={container}
entity={entity}
query={query}
toggle={toggle}
onClickRow={(row) => {
fieldApi.setValue(row.id);
_setValue(row as any);
toggle();
}}
onClickPage={(page) => {
console.log("setting page", page);
setQuery((prev) => ({ ...prev, page }));
}}
/>
)}
>
<div
ref={ref}
className="bg-muted/40 w-full h-11 focus:bg-muted rounded-md items-center px-2.5 outline-none focus:outline-none focus:ring-2 focus:ring-zinc-500 focus:border-transparent transition-all disabled:bg-muted/50 disabled:text-primary/50 cursor-pointer active:bg-muted/80 hover:bg-muted/60 flex flex-row gap-2"
tabIndex={tabIndex}
onKeyDown={getHotkeyHandler([
[
"Enter",
() => {
ref.current?.click();
}
]
])}
>
{_value ? (
<>
<div className="flex bg-primary/10 px-2 leading-none py-1.5 rounded-lg font-mono text-sm">
{ucFirst(entity.name)}
</div>
<div className="flex flex-row gap-3 overflow-clip w-1 flex-grow">
{_value &&
Object.entries(_value).map(([key, value]) => {
const field = entity.getField(key)!;
if (field.isHidden("table")) return null;
const _value = field.getValue(value, "table");
return (
<div
key={key}
className="flex flex-row gap-1 items-center flex-nowrap"
>
<span className="opacity-60 text-nowrap">
{field.getLabel()}:
</span>{" "}
{_value !== null && typeof value !== "undefined" ? (
<span className="text-nowrap truncate">{_value}</span>
) : (
<span className="opacity-30 text-nowrap font-mono mt-0.5">
null
</span>
)}
</div>
);
})}
</div>
<Button IconLeft={TbEye} onClick={handleViewItem} size="small">
View
</Button>
</>
) : (
<div className="pl-2">- Select -</div>
)}
</div>
</Popover>
</div>
<Formy.Input
type="hidden"
name={fieldApi.name}
id={fieldApi.name}
value={fieldApi.state.value ?? ""}
onChange={console.log}
tabIndex={-1}
/>
{/*<Formy.Select
ref={ref}
name={fieldApi.name}
id={fieldApi.name}
value={fieldApi.state.value}
data-value={fieldApi.state.value}
onBlur={fieldApi.handleBlur}
onChange={handleUpdate}
disabled={!Array.isArray(container.data)}
>
{container.data ? (
<>
{emptyOption}
{!field.isRequired() && emptyOption}
{container.data?.map(renderRow)}
</>
) : (
<option value={undefined} disabled>
Loading...
</option>
)}
</Formy.Select>*/}
</Formy.Group>
);
}
const PopoverTable = ({ container, entity, query, toggle, onClickRow, onClickPage }) => {
function handleNext() {
if (query.limit * query.page < container.meta?.count) {
onClickPage(query.page + 1);
}
}
function handlePrev() {
if (query.page > 1) {
onClickPage(query.page - 1);
}
}
useHotkeys([
["ArrowRight", handleNext],
["ArrowLeft", handlePrev],
["Escape", toggle]
]);
return (
<div>
<EntityTable
classNames={{ value: "line-clamp-1 truncate max-w-52 text-nowrap" }}
data={container.data ?? []}
entity={entity}
total={container.meta?.count}
page={query.page}
onClickRow={onClickRow}
onClickPage={onClickPage}
/>
</div>
);
};