import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Stack, NumberInput, Select, Switch, Text, Button, Group, Divider, TextInput, Textarea, } from "@mantine/core"; import { IBaseProperty, SelectTypeOptions, NumberTypeOptions, DateTypeOptions, PersonTypeOptions, Choice, } from "@/ee/base/types/base.types"; import { ChoiceEditor } from "./choice-editor"; import { FilterPersonInput } from "@/ee/base/components/views/filter-person-input"; import { CURRENCIES, DEFAULT_CURRENCY_CODE, } from "@/ee/base/constants/currencies"; import { useTranslation } from "react-i18next"; type PropertyOptionsProps = { property: IBaseProperty; onUpdate: (typeOptions: Record) => void; onClose: () => void; onDirtyChange?: (dirty: boolean) => void; hideButtons?: boolean; // Portal target for nested Select dropdowns; must be inside the host popover, outside ScrollArea. dropdownPortalTarget?: HTMLElement | null; }; export function PropertyOptions({ property, onUpdate, onClose, onDirtyChange, hideButtons, dropdownPortalTarget, }: PropertyOptionsProps) { const { t } = useTranslation(); switch (property.type) { case "select": case "multiSelect": return ( ); case "status": return ( ); case "number": return ( ); case "date": return ( ); case "person": return ( ); case "text": case "longText": case "url": case "email": return ( ); case "checkbox": return ( ); default: return ( {t("No options for this property type")} ); } } type OptionEditorProps = { property: IBaseProperty; onUpdate: (typeOptions: Record) => void; onClose: () => void; onDirtyChange?: (dirty: boolean) => void; hideButtons?: boolean; dropdownPortalTarget?: HTMLElement | null; }; const EMPTY_OPTIONS: Record = {}; function optionsEqual( a: Record, b: Record, ): boolean { const keys = new Set([...Object.keys(a), ...Object.keys(b)]); for (const key of keys) { const av = a[key]; const bv = b[key]; if (Array.isArray(av) && Array.isArray(bv)) { if (av.length !== bv.length || av.some((v, i) => v !== bv[i])) { return false; } } else if (av !== bv) { return false; } } return true; } // Draft hook for non-choice option editors: live in create flow, staged in edit menu. function useEditableTypeOptions( initialRaw: Record | undefined, { onUpdate, onClose, onDirtyChange, hideButtons, }: { onUpdate: (opts: Record) => void; onClose: () => void; onDirtyChange?: (dirty: boolean) => void; hideButtons?: boolean; }, ) { const initial = initialRaw ?? EMPTY_OPTIONS; const [draft, setDraft] = useState>(initial); useEffect(() => { if (!hideButtons) setDraft(initial); }, [initial, hideButtons]); const onUpdateRef = useRef(onUpdate); onUpdateRef.current = onUpdate; useEffect(() => { if (hideButtons) onUpdateRef.current(draft); }, [hideButtons, draft]); const isDirty = useMemo(() => !optionsEqual(draft, initial), [draft, initial]); useEffect(() => { onDirtyChange?.(isDirty); }, [isDirty, onDirtyChange]); const update = useCallback( (patch: Record) => setDraft((prev) => ({ ...prev, ...patch })), [], ); const save = useCallback(() => { onUpdate(draft); onClose(); }, [draft, onUpdate, onClose]); const cancel = useCallback(() => { setDraft(initial); onDirtyChange?.(false); onClose(); }, [initial, onClose, onDirtyChange]); return { draft, update, isDirty, save, cancel }; } function OptionsFooter({ isDirty, onCancel, onSave, }: { isDirty: boolean; onCancel: () => void; onSave: () => void; }) { const { t } = useTranslation(); return ( <> ); } function SelectOptions({ property, onUpdate, onClose, onDirtyChange, hideButtons, dropdownPortalTarget, }: OptionEditorProps) { const options = property.typeOptions as SelectTypeOptions | undefined; const choices = useMemo(() => options?.choices ?? [], [options?.choices]); const handleSave = useCallback( (newChoices: Choice[], defaultValue: string | string[] | null) => { onUpdate({ ...property.typeOptions, choices: newChoices, choiceOrder: newChoices.map((c) => c.id), defaultValue, }); }, [property.typeOptions, onUpdate], ); return ( ); } function StatusOptions({ property, onUpdate, onClose, onDirtyChange, hideButtons, dropdownPortalTarget, }: OptionEditorProps) { const options = property.typeOptions as SelectTypeOptions | undefined; const choices = useMemo(() => options?.choices ?? [], [options?.choices]); const handleSave = useCallback( (newChoices: Choice[], defaultValue: string | string[] | null) => { onUpdate({ ...property.typeOptions, choices: newChoices, choiceOrder: newChoices.map((c) => c.id), defaultValue, }); }, [property.typeOptions, onUpdate], ); return ( ); } function NumberOptions({ property, onUpdate, onClose, onDirtyChange, hideButtons, dropdownPortalTarget, }: OptionEditorProps) { const { t } = useTranslation(); const { draft, update, isDirty, save, cancel } = useEditableTypeOptions( property.typeOptions as Record | undefined, { onUpdate, onClose, onDirtyChange, hideButtons }, ); const options = draft as NumberTypeOptions; return ( ({ value: c.code, label: `${c.name} (${c.code})`, }))} value={options.currencyCode ?? DEFAULT_CURRENCY_CODE} onChange={(val) => update({ currencyCode: val ?? DEFAULT_CURRENCY_CODE }) } /> )} ({ value: String(i), label: String(i), })), ]} value={options.precision == null ? "default" : String(options.precision)} onChange={(val) => update({ precision: val == null || val === "default" ? undefined : Number(val) }) } /> update({ defaultValue: typeof val === "number" ? val : undefined }) } /> {!hideButtons && ( )} ); } function DateOptions({ property, onUpdate, onClose, onDirtyChange, hideButtons, dropdownPortalTarget, }: OptionEditorProps) { const { t } = useTranslation(); const { draft, update, isDirty, save, cancel } = useEditableTypeOptions( property.typeOptions as Record | undefined, { onUpdate, onClose, onDirtyChange, hideButtons }, ); const options = draft as DateTypeOptions; return ( update({ includeTime: e.currentTarget.checked })} /> {options.includeTime && (