/* Copyright 2026 Marimo. All rights reserved. */ /* oxlint-disable typescript/no-explicit-any */ import React, { useEffect } from "react"; import { type FieldValues, type Path, type UseFormReturn, useWatch, } from "react-hook-form"; import { z } from "zod"; import { type FormRenderer, renderZodSchema } from "@/components/forms/form"; import { FieldOptions } from "@/components/forms/options"; import { ensureStringArray, SwitchableMultiSelect, TextAreaMultiSelect, } from "@/components/forms/switchable-multi-select"; import { Combobox, ComboboxItem } from "@/components/ui/combobox"; import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, FormMessageTooltip, } from "@/components/ui/form"; import { DebouncedInput } from "@/components/ui/input"; import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { useAsyncData } from "@/hooks/useAsyncData"; import { cn } from "@/utils/cn"; import { Objects } from "@/utils/objects"; import { Strings } from "@/utils/strings"; import type { ColumnId } from "../types"; import { getOperatorForDtype, getSchemaForOperator } from "../utils/operators"; import { ColumnFetchValuesContext, ColumnInfoContext, ColumnNameContext, } from "./context"; import { DataTypeIcon } from "./datatype-icon"; export const columnIdRenderer = (): FormRenderer< T, string > => ({ isMatch: (schema: z.ZodType): schema is z.ZodString => { const { special } = FieldOptions.parse(schema.description || ""); return special === "column_id"; }, Component: ({ schema, form, path }) => { const columns = React.use(ColumnInfoContext); const { label, description } = FieldOptions.parse(schema.description); return ( ( {label} {description} )} /> ); }, }); export const multiColumnIdRenderer = (): FormRenderer< T, string[] > => ({ isMatch: (schema: z.ZodType): schema is z.ZodArray => { if (schema instanceof z.ZodArray) { const childType = schema.element; const description = childType instanceof z.ZodType ? childType.description : ""; const { special } = FieldOptions.parse(description || ""); return special === "column_id"; } return false; }, Component: ({ schema, form, path }) => { const { label } = FieldOptions.parse(schema.description); return ( ); }, }); /** * Type: (string | number)[] * Special: column_ids */ const MultiColumnFormField = ({ schema, form, path, itemLabel, }: { schema: z.ZodSchema; form: UseFormReturn; path: Path; itemLabel?: string; }) => { const columns = React.use(ColumnInfoContext); const { description } = FieldOptions.parse(schema.description); const placeholder = itemLabel ? `Select ${itemLabel.toLowerCase()}` : undefined; return ( { const values = ensureStringArray(field.value); return ( {itemLabel} {description} className="min-w-[180px]" placeholder={placeholder} displayValue={String} multiple={true} chips={true} keepPopoverOpenOnSelect={true} value={values} onValueChange={(v) => { field.onChange(v); }} > {[...columns.entries()].map(([name, dtype]) => { return ( {name} ({dtype}) ); })} ); }} /> ); }; export const columnValuesRenderer = (): FormRenderer< T, string[] > => ({ isMatch: (schema: z.ZodType): schema is z.ZodArray => { if (schema instanceof z.ZodArray) { const { special } = FieldOptions.parse(schema.description || ""); return special === "column_values"; } return false; }, Component: ({ schema, form, path }) => { const { label, description, placeholder } = FieldOptions.parse( schema.description, ); const column = React.use(ColumnNameContext); const fetchValues = React.use(ColumnFetchValuesContext); const { data, isPending } = useAsyncData( () => fetchValues({ column }), [column], ); const options = data?.values || []; if (options.length === 0 && !isPending) { return ( ( {label} {description} )} /> ); } const optionsAsStrings = options.map(String); return ( ( {label} {description} option} value={ Array.isArray(field.value) ? field.value[0] : field.value } onValueChange={field.onChange} > {optionsAsStrings.map((option) => ( {option} ))} )} /> ); }, }); export const multiColumnValuesRenderer = < T extends FieldValues, >(): FormRenderer => ({ isMatch: (schema: z.ZodType): schema is z.ZodArray => { const { special } = FieldOptions.parse(schema.description || ""); return special === "column_values" && schema instanceof z.ZodArray; }, Component: ({ schema, form, path }) => { const column = React.use(ColumnNameContext); const fetchValues = React.use(ColumnFetchValuesContext); const { data, isPending } = useAsyncData( () => fetchValues({ column }), [column], ); const options = data?.values || []; if (options.length === 0 && !isPending) { return ( ( {schema.description} )} /> ); } const optionsAsStrings = options.map(String); return ( { const valueAsArray = ensureStringArray(field.value); return ( {schema.description} ); }} /> ); }, }); const StyledFormMessage = ({ className }: { className?: string }) => { return ( ); }; export const filterFormRenderer = (): FormRenderer< T, {} > => ({ isMatch: (schema: z.ZodType): schema is z.ZodObject<{}> => { if (schema instanceof z.ZodObject) { const { special } = FieldOptions.parse(schema.description || ""); return special === "column_filter"; } return false; }, Component: ({ schema, form, path }) => { return ( } form={form} path={path} /> ); }, }); const ColumnFilterForm = ({ path, form, schema, }: { schema: z.ZodObject<{}>; form: UseFormReturn; path: Path; }) => { const { description } = FieldOptions.parse(schema.description); const columns = React.use(ColumnInfoContext); const columnIdSchema = Objects.entries(schema.shape).find( ([key]) => key === "column_id", )?.[1] as unknown as z.ZodString; // existing values const { column_id: columnId, operator } = useWatch({ name: path, }) as { column_id: ColumnId; operator: string; }; const columnRenderer = columnIdRenderer(); const children = [ renderZodSchema(columnIdSchema, form, `${path}.column_id` as Path, [ columnRenderer, ]), ]; // When column ID changes, get the new dtype and reset the operator useEffect(() => { const dtype = columns.get(columnId); const operators = getOperatorForDtype(dtype); const currentOperator = form.getValues(`${path}.operator`); if (!operators.includes(currentOperator)) { form.setValue(`${path}.operator`, operators[0]); form.setValue(`${path}.value`, undefined); } }, [columnId, columns, form, path]); if (columnId != null) { const dtype = columns.get(columnId); const operators = getOperatorForDtype(dtype); if (operators.length === 0) { children.push(

This column type does not support filtering.

, ); } else { children.push( } render={({ field }) => ( {description} )} />, ); } } if (operator != null) { const dtype = columns.get(columnId); const operandSchemas = getSchemaForOperator(dtype, operator); if (operandSchemas.length === 1) { children.push( {renderZodSchema( operandSchemas[0], form, `${path}.value` as Path, [], )} , ); } } return ( (
{children}
)} /> ); }; export const DATAFRAME_FORM_RENDERERS: FormRenderer[] = [ columnIdRenderer(), multiColumnIdRenderer(), columnValuesRenderer(), multiColumnValuesRenderer(), filterFormRenderer(), ];