mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-16 12:37:20 +00:00
245 lines
8.6 KiB
TypeScript
245 lines
8.6 KiB
TypeScript
import { getHotkeyHandler, useHotkeys } from "@mantine/hooks";
|
|
import { ucFirst } from "core/utils";
|
|
import type { EntityData } from "bknd";
|
|
import type { RelationField } from "data/relations";
|
|
import { useEffect, useRef, useState } from "react";
|
|
import { TbEye } from "react-icons/tb";
|
|
import { useEntityQuery } from "bknd/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 { Link } from "ui/components/wouter/Link";
|
|
import { routes } from "ui/lib/routes";
|
|
import { useLocation } from "wouter";
|
|
import type { EntityTableProps } from "../EntityTable";
|
|
import type { ResponseObject } from "modules/ModuleApi";
|
|
import ErrorBoundary from "ui/components/display/ErrorBoundary";
|
|
import { EntityTable2 } from "ui/modules/data/components/EntityTable2";
|
|
import type { TFieldApi } from "ui/modules/data/components/EntityForm";
|
|
|
|
// @todo: allow clear if not required
|
|
export function EntityRelationalFormField({
|
|
fieldApi,
|
|
field,
|
|
data,
|
|
disabled,
|
|
tabIndex,
|
|
}: {
|
|
fieldApi: TFieldApi;
|
|
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,
|
|
select: entity.getSelect(undefined, "table"),
|
|
});
|
|
const [, navigate] = useLocation();
|
|
const ref = useRef<any>(null);
|
|
const $q = useEntityQuery(field.target(), undefined, {
|
|
select: query.select,
|
|
limit: query.limit + 1 /* overfetch for softscan=false */,
|
|
offset: (query.page - 1) * query.limit,
|
|
});
|
|
const [_value, _setValue] = useState<{ id: number | undefined; [key: string]: any }>();
|
|
|
|
const referenceField = data?.[field.reference()];
|
|
const relationalField = data?.[field.name];
|
|
|
|
useEffect(() => {
|
|
const value = data?.[field.reference()];
|
|
_setValue(value);
|
|
}, [referenceField]);
|
|
|
|
useEffect(() => {
|
|
(async () => {
|
|
const rel_value = field.target();
|
|
if (!rel_value || !relationalField) return;
|
|
|
|
const fetched = await $q.api.readOne(field.target(), relationalField);
|
|
if (fetched.ok && fetched.data) {
|
|
_setValue(fetched.data as any);
|
|
}
|
|
})();
|
|
}, [relationalField]);
|
|
|
|
function handleViewItem(e: React.MouseEvent<HTMLButtonElement>) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
if (_value) {
|
|
navigate(routes.data.entity.edit(entity.name, _value.id as any));
|
|
}
|
|
}
|
|
|
|
// fix missing value on fields that are required
|
|
useEffect(() => {
|
|
if (field.isRequired() && !fieldApi.state.value) {
|
|
const firstValue = $q.data?.[0];
|
|
if (!firstValue) return;
|
|
|
|
console.warn("setting first value because field is required", field.name, firstValue.id);
|
|
fieldApi.setValue(firstValue.id);
|
|
_setValue(firstValue as any);
|
|
}
|
|
}, [$q.data]);
|
|
|
|
const fetching = $q.isLoading || $q.isValidating;
|
|
|
|
return (
|
|
<Formy.Group>
|
|
<Formy.FieldLabel
|
|
htmlFor={fieldApi.name}
|
|
field={field}
|
|
label={field.getLabel({ fallback: false }) ?? entity.label}
|
|
/>
|
|
<div
|
|
data-disabled={fetching || disabled ? 1 : undefined}
|
|
className="data-[disabled]:opacity-70 data-[disabled]:pointer-events-none"
|
|
>
|
|
<Popover
|
|
backdrop
|
|
className=""
|
|
target={({ toggle }) => (
|
|
<PopoverTable
|
|
container={$q.data}
|
|
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" ? (
|
|
<ErrorBoundary
|
|
fallback={JSON.stringify(_value)}
|
|
suppressError
|
|
>
|
|
<span className="text-nowrap truncate">{_value}</span>
|
|
</ErrorBoundary>
|
|
) : (
|
|
<span className="opacity-30 text-nowrap font-mono mt-0.5">
|
|
null
|
|
</span>
|
|
)}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
<Link to={routes.data.entity.edit(entity.name, _value.id as any)}>
|
|
<Button IconLeft={TbEye} size="small">
|
|
View
|
|
</Button>
|
|
</Link>
|
|
</>
|
|
) : (
|
|
<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.Group>
|
|
);
|
|
}
|
|
|
|
type PropoverTableProps = Omit<EntityTableProps, "data"> & {
|
|
container: ResponseObject;
|
|
query: any;
|
|
toggle: () => void;
|
|
};
|
|
const PopoverTable = ({
|
|
container,
|
|
entity,
|
|
query,
|
|
toggle,
|
|
onClickRow,
|
|
onClickPage,
|
|
}: PropoverTableProps) => {
|
|
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>
|
|
<EntityTable2
|
|
classNames={{ value: "line-clamp-1 truncate max-w-52 text-nowrap" }}
|
|
data={container ?? []}
|
|
entity={entity}
|
|
select={query.select}
|
|
total={container.body.meta?.count}
|
|
page={query.page}
|
|
onClickRow={onClickRow}
|
|
onClickPage={onClickPage}
|
|
/>
|
|
</div>
|
|
);
|
|
};
|