metis-desk/packages/rdx-ui/src/components/field-picker-dialog.tsx
David ccb52fb534 feat: add catalog field picker dialog and list components
- Introduced `CatalogFieldPickerDialog` and `CatalogFieldPickerList` for selecting catalog fields.
- Implemented `appendTemplateFields` helper function for managing template fields.
- Added tests for catalog field selection and template field management.
- Created reusable components for field picking and reordering in the UI library.
- Enhanced drag-and-drop functionality for reordering items in lists.
2026-10-15 15:58:01 +02:00

101 lines
3.1 KiB
TypeScript

import { Button } from "@repo/shadcn-ui/components/button";
import type { ReactNode } from "react";
import { useFieldPickerController } from "../hooks/use-field-picker-controller";
import type { FieldPickerSelection } from "./field-picker-list";
import { FormActions, FormBody, FormDialog } from "./form-dialog";
/** Selección provisional y alta opcional compartidas, independientes de catálogos y endpoints. */
export function FieldPickerDialog(props: {
items: readonly { id: string }[];
canAdd: (ids: readonly string[]) => boolean;
onConfirm: (ids: readonly string[]) => boolean;
onClose: () => void;
labels: {
title: string;
cancel: string;
help: string;
destination: string;
selected: (count: number) => string;
limit: string;
invalid: string;
create: string;
confirm: string;
};
renderList: (selection: FieldPickerSelection) => ReactNode;
quickCreate?:
| {
disabled?: boolean | undefined;
render: (callbacks: {
onOpenChange: (open: boolean) => void;
onSaved: (id: string) => void;
}) => ReactNode;
}
| undefined;
}) {
const c = useFieldPickerController({
fields: props.items,
canAdd: props.canAdd,
onConfirm: props.onConfirm,
onClose: props.onClose,
});
return (
<FormDialog
cancelLabel={props.labels.cancel}
formActions
layout="picker"
onOpenChange={(open) => {
if (!open) {
props.onClose();
}
}}
open
title={props.labels.title}
wide
>
<FormBody>
<p className="text-sm text-muted-foreground">{props.labels.help}</p>
<p className="text-sm font-medium">{props.labels.destination}</p>
{props.renderList({ disabled: c.disabled, onToggle: c.toggle, selectedIds: c.selectedIds })}
<p aria-live="polite" className="text-sm text-muted-foreground">
{props.labels.selected(c.selectedIds.length)}
</p>
{props.items.some((field) => !c.selectedIds.includes(field.id) && c.disabled(field.id)) && (
<p className="text-sm text-muted-foreground" role="status">
{props.labels.limit}
</p>
)}
{!c.canConfirm && c.selectedIds.length > 0 && (
<p className="text-sm text-destructive" role="alert">
{props.labels.invalid}
</p>
)}
</FormBody>
<FormActions
leadingActions={
props.quickCreate && (
<Button
aria-haspopup="dialog"
disabled={props.quickCreate.disabled}
onClick={() => c.setCreatingField(true)}
type="button"
variant="outline"
>
{props.labels.create}
</Button>
)
}
>
<Button disabled={!c.canConfirm} onClick={c.confirm} type="button">
{props.labels.confirm}
</Button>
</FormActions>
{props.quickCreate &&
c.creatingField &&
props.quickCreate.render({ onOpenChange: c.setCreatingField, onSaved: c.selectCreated })}
</FormDialog>
);
}