UNPKG

@adaptabletools/adaptable

Version:

Powerful AG Grid extension which provides advanced, cutting-edge functionality to meet all DataGrid requirements

354 lines (353 loc) 22.1 kB
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime"; import * as React from 'react'; import { flattenAdaptableFormFields, isAdaptableFormFieldDisabled, isAdaptableFormFieldGroup, isAdaptableFormFieldGroupHidden, isAdaptableFormFieldHidden, resolveSelectPlaceholder, resolveSelectValueAfterClear, validateAdaptableForm, } from '../../AdaptableState/Common/AdaptableForm'; import FormLayout, { FormRow } from '../FormLayout'; import Input from '../Input'; import { AdaptableButtonView, } from '../../View/Components/AdaptableButton'; import AdaptableInput from '../../View/Components/AdaptableInput'; import { useEffect, useId, useMemo, useState } from 'react'; import { useAdaptable } from '../../View/AdaptableContext'; import { Box, Flex } from '../Flex'; import { twMerge } from '../../twMerge'; import { MultiCombobox, SingleCombobox } from '../Combobox'; export function AdaptableFormComponentButtons({ formDef, onClick, defaultTone, disabledButtons, api, context, focusFirstButton = true, }) { const formContext = context ?? { ...api.internalApi.buildBaseContext(), }; return (_jsx(_Fragment, { children: formDef.buttons.map((button, index) => { const buttonLabel = api.internalApi.getLabelForButton(button, formContext) ?? ''; const buttonStyle = api.internalApi.getStyleForButton(button, formContext); return (_jsx(AdaptableButtonView, { button: button, context: formContext, api: api, defaults: { tone: defaultTone, variant: buttonStyle?.variant, }, rerenderOnClick: false, iconSize: { height: 15, width: 15 }, autoFocus: focusFirstButton && index === 0, disabled: disabledButtons?.[index], "data-text": buttonLabel, className: twMerge(index ? 'twa:ml-2' : '', buttonStyle?.className), onClick: () => onClick(button) }, index)); }) })); } export function AdaptableFormComponent({ formDef, data, onChange, onButtonClick, displayTitle, api, context, focusFirstButton, }) { const getFieldValue = (key) => data[key]; const setFieldValue = (key, value) => { const newData = { ...data, [key]: value }; onChange(newData); const changedField = flatFields.find((f) => f.name === key); if (changedField?.onValueChange) { const enrichedContext = { ...(context ?? {}), formData: newData }; changedField.onValueChange(value, enrichedContext); } }; const adaptable = useAdaptable(); const formInstanceId = useId(); const fieldDomId = (field) => `${formInstanceId}-${field.name}`; const fieldLabelId = (field) => `${formInstanceId}-${field.name}-label`; const fieldHelpId = (field) => `${formInstanceId}-${field.name}-help`; const fieldErrorId = (field) => `${formInstanceId}-${field.name}-error`; const flatFields = useMemo(() => flattenAdaptableFormFields(formDef), [formDef]); const [asyncOptionsCache, setAsyncOptionsCache] = useState({}); useEffect(() => { let cancelled = false; flatFields.forEach((field) => { const usesOptions = field.fieldType === 'select' || field.fieldType === 'radio'; if (usesOptions && typeof field.options === 'function') { const result = field.options(data, context); if (result instanceof Promise) { result .then((resolved) => { if (!cancelled) { setAsyncOptionsCache((prev) => ({ ...prev, [field.name]: resolved })); } }) .catch(() => { }); } } }); return () => { cancelled = true; }; }, [flatFields, data, context]); const resolveOptions = (field) => { const opts = field.options; if (!opts) return []; if (Array.isArray(opts)) return opts; const result = opts(data, context); if (result instanceof Promise) { return asyncOptionsCache[field.name] ?? []; } return result; }; const formErrors = useMemo(() => validateAdaptableForm(formDef, data, context), [formDef, data, context]); const formIsValid = Object.keys(formErrors).length === 0; const buttonContext = useMemo(() => ({ ...(context ?? {}), formData: data, formIsValid, formErrors, }), [context, data, formIsValid, formErrors]); const disabledButtons = useMemo(() => formDef.buttons?.map((button) => { const isValid = !!button.disabled && !!buttonContext ? !button.disabled(button, buttonContext) : true; return !isValid; }) ?? [], [buttonContext, formDef.buttons]); const renderLabel = (field) => { const required = field.required; const useFor = field.fieldType !== 'radio' && field.fieldType !== 'textOutput'; const requiredMarker = required ? (_jsx("span", { className: "ab-Form_required-marker", "aria-hidden": "true", children: ' *' })) : null; if (useFor) { return (_jsxs("label", { id: fieldLabelId(field), htmlFor: fieldDomId(field), className: "ab-FormLayout_column--label", style: { textAlign: 'end' }, title: field.tooltip, children: [field.label, requiredMarker] })); } return (_jsxs(Box, { id: fieldLabelId(field), className: "ab-FormLayout_column--label", style: { textAlign: 'end' }, title: field.tooltip, children: [field.label, requiredMarker] })); }; const renderField = (field) => { const value = getFieldValue(field.name) ?? ''; const isDisabled = isAdaptableFormFieldDisabled(field, data, context); const error = formErrors[field.name]; const inputId = fieldDomId(field); const helpId = field.helpText ? fieldHelpId(field) : undefined; const errorId = error ? fieldErrorId(field) : undefined; const describedBy = [errorId, helpId].filter(Boolean).join(' ') || undefined; const a11yProps = { id: inputId, 'aria-required': field.required ? true : undefined, 'aria-invalid': error ? true : undefined, 'aria-describedby': describedBy, }; const onChange = (event) => { if (field.fieldType === 'number') { const rawValue = event.target.value; if (rawValue === '') { setFieldValue(field.name, null); return; } const numberValue = Number(rawValue); if (isNaN(numberValue)) { setFieldValue(field.name, null); } else { setFieldValue(field.name, numberValue); } } else { setFieldValue(field.name, event.target.value); } }; let control; switch (field.fieldType) { case 'text': case 'number': case 'date': control = (_jsx(AdaptableInput, { type: field.fieldType, className: "twa:w-full", name: field.name, value: value, onChange: onChange, disabled: isDisabled, placeholder: field.placeholder, min: field.min, max: field.max, step: field.step, minLength: field.fieldType === 'text' ? field.minLength : undefined, maxLength: field.fieldType === 'text' ? field.maxLength : undefined, pattern: field.fieldType === 'text' ? field.pattern : undefined, ...a11yProps })); break; case 'time': control = (_jsx(Input, { type: "time", className: "twa:w-full", name: field.name, value: value, onChange: onChange, disabled: isDisabled, placeholder: field.placeholder, min: field.min, max: field.max, step: field.step, ...a11yProps })); break; case 'datetime': control = (_jsx(Input, { type: "datetime-local", className: "twa:w-full", name: field.name, value: value, onChange: onChange, disabled: isDisabled, placeholder: field.placeholder, min: field.min, max: field.max, step: field.step, ...a11yProps })); break; case 'color': control = (_jsx(Input, { type: "color", name: field.name, value: value || '#000000', onChange: onChange, disabled: isDisabled, ...a11yProps })); break; case 'textarea': { const rows = field.rows ?? 3; control = (_jsx("textarea", { id: inputId, name: field.name, value: value, disabled: isDisabled, placeholder: field.placeholder, minLength: field.minLength, maxLength: field.maxLength, rows: rows, "aria-required": field.required ? true : undefined, "aria-invalid": error ? true : undefined, "aria-describedby": describedBy, onChange: (e) => setFieldValue(field.name, e.target.value), className: twMerge('ab-Input', 'twa:box-border', 'twa:rounded-input', 'twa:w-full', 'twa:p-2', isDisabled ? 'twa:bg-(--ab-cmp-input--disabled__background)' : 'twa:bg-input-background') })); break; } case 'slider': { const min = typeof field.min === 'number' ? field.min : 0; const max = typeof field.max === 'number' ? field.max : 100; const step = field.step ?? 1; const numericValue = typeof value === 'number' ? value : Number(value) || min; control = (_jsxs(Flex, { flexDirection: "row", alignItems: "center", style: { columnGap: 'var(--ab-base-space)', minWidth: 160 }, children: [_jsx(Input, { type: "range", name: field.name, value: numericValue, disabled: isDisabled, min: min, max: max, step: step, "aria-valuemin": min, "aria-valuemax": max, "aria-valuenow": numericValue, onChange: (e) => { const next = Number(e.target.value); setFieldValue(field.name, Number.isNaN(next) ? null : next); }, className: "twa:flex-1", ...a11yProps }), _jsx(Box, { className: "twa:text-xs twa:tabular-nums", style: { minWidth: 32, textAlign: 'right' }, "aria-hidden": "true", children: numericValue })] })); break; } case 'select': { const optionItems = resolveOptions(field); const items = optionItems.map((item) => ({ value: item.value, label: item.label, })); const selectPlaceholder = resolveSelectPlaceholder(field, optionItems); const groupProps = { id: inputId, role: 'group', 'aria-labelledby': fieldLabelId(field), 'aria-required': field.required ? true : undefined, 'aria-invalid': error ? true : undefined, 'aria-describedby': describedBy, }; if (field.multi) { const selected = Array.isArray(value) ? value : []; control = (_jsx(Box, { ...groupProps, children: _jsx(MultiCombobox, { items: items, onValueChange: (newValues) => setFieldValue(field.name, newValues), value: selected, disabled: isDisabled, placeholder: selectPlaceholder }) })); } else { control = (_jsx(Box, { ...groupProps, children: _jsx(SingleCombobox, { items: items, onValueChange: (newValue) => setFieldValue(field.name, resolveSelectValueAfterClear(field, newValue)), value: value, disabled: isDisabled, placeholder: selectPlaceholder }) })); } break; } case 'radio': { const items = resolveOptions(field); const groupName = `${formInstanceId}-radio-${field.name}`; control = (_jsx(Flex, { id: inputId, role: "radiogroup", "aria-labelledby": fieldLabelId(field), "aria-required": field.required ? true : undefined, "aria-invalid": error ? true : undefined, "aria-describedby": describedBy, flexDirection: "row", alignItems: "center", flexWrap: "wrap", style: { columnGap: 'var(--ab-base-space-2)', rowGap: 'var(--ab-base-space)' }, children: items.map((item) => { const optionId = `${groupName}-${String(item.value)}`; const checked = value === item.value; return (_jsxs("label", { htmlFor: optionId, className: "twa:flex twa:flex-row twa:items-center twa:box-border", style: { columnGap: 'var(--ab-base-space)', cursor: isDisabled ? 'not-allowed' : 'pointer', }, children: [_jsx(Input, { type: "radio", id: optionId, name: groupName, value: String(item.value), checked: checked, disabled: isDisabled, onChange: () => setFieldValue(field.name, item.value) }), _jsx(Box, { children: item.label })] }, optionId)); }) })); break; } case 'checkbox': control = (_jsx(Input, { type: "checkbox", name: field.name, checked: value, disabled: isDisabled, onChange: (event) => { setFieldValue(field.name, event.target.checked); }, ...a11yProps })); break; case 'textOutput': control = _jsx(Box, { className: "twa:pl-2 twa:text-left", children: value }); break; case 'custom': control = field.render ? (field.render({ value, setValue: (newValue) => setFieldValue(field.name, newValue), field, formData: data, context, disabled: isDisabled, error, })) : (_jsx(Box, { className: "twa:text-xs twa:opacity-60", children: "custom field requires a `render` function" })); break; default: control = _jsxs("div", { children: ["Unknown field type: ", field.fieldType] }); } if (!field.helpText && !error) { return control; } return (_jsxs(Box, { children: [control, field.helpText && !error ? (_jsx(Box, { id: helpId, className: "ab-Form_help twa:text-xs twa:opacity-70 twa:mt-1", children: field.helpText })) : null, error ? (_jsx(Box, { id: errorId, role: "alert", "aria-live": "polite", className: "ab-Form_error twa:text-xs twa:mt-1", style: { color: 'var(--ab-color-destructive, #c00)' }, children: error })) : null] })); }; const columns = formDef.config?.columns ?? [1, 2]; const handleFormKeyDown = (event) => { handleKeyDownForNumberFields(event); if (!formDef.onSubmit) { return; } if (event.key !== 'Enter') { return; } if (event.shiftKey) { return; } const target = event.target; if (target && target.tagName === 'TEXTAREA') { return; } if (!formIsValid) { return; } event.preventDefault(); formDef.onSubmit(data, buttonContext); }; const handleKeyDownForNumberFields = (event) => { const target = event.target; const targetName = target.name; const field = flatFields.find((f) => f.name === targetName); if (field && field.fieldType === 'number') { const value = event.target.value; adaptable._emit('CellEditorKeyDown', { keyDownEvent: event, cellValue: value, columnId: field.name, updateValueCallback: (updatedValue) => { setFieldValue(field.name, updatedValue); }, }); } }; const isInlineLayout = formDef.layout === 'inline'; const renderInlineEntries = () => { const items = []; for (const entry of formDef.fields ?? []) { if (isAdaptableFormFieldGroup(entry)) { if (isAdaptableFormFieldGroupHidden(entry, data, context)) continue; for (const inner of entry.fields) { if (Array.isArray(inner)) items.push(...inner); else items.push(inner); } } else if (Array.isArray(entry)) { items.push(...entry); } else { items.push(entry); } } return items .filter((field) => !isAdaptableFormFieldHidden(field, data, context)) .map((field, index) => { const useFor = field.fieldType !== 'radio' && field.fieldType !== 'textOutput'; const requiredMarker = field.required ? (_jsx("span", { className: "ab-Form_required-marker", "aria-hidden": "true", children: ' *' })) : null; return (_jsxs(Flex, { flexDirection: "row", alignItems: "center", style: { columnGap: 'var(--ab-base-space)', marginLeft: index > 0 ? 'var(--ab-base-space)' : undefined, }, children: [field.label ? (useFor ? (_jsxs("label", { id: fieldLabelId(field), htmlFor: fieldDomId(field), className: "ab-FormLayout--inline_field-label", title: field.tooltip, children: [field.label, requiredMarker] })) : (_jsxs(Box, { id: fieldLabelId(field), className: "ab-FormLayout--inline_field-label", title: field.tooltip, children: [field.label, requiredMarker] }))) : null, renderField(field)] }, field.name)); }); }; const renderRowsEntry = (entry, index) => { if (isAdaptableFormFieldGroup(entry)) { if (isAdaptableFormFieldGroupHidden(entry, data, context)) { return null; } const innerNodes = entry.fields.map((inner, innerIndex) => { if (Array.isArray(inner)) { const visible = inner.filter((f) => !isAdaptableFormFieldHidden(f, data, context)); if (visible.length === 0) return null; const rowFields = {}; visible.map((fieldItem, idx) => { const rowFieldIndex = 2 * idx + 1; rowFields[rowFieldIndex] = renderLabel(fieldItem); rowFields[rowFieldIndex + 1] = renderField(fieldItem); }); return _jsx(FormRow, { ...rowFields }, `g${index}-r${innerIndex}`); } if (isAdaptableFormFieldHidden(inner, data, context)) return null; return (_jsxs(FormRow, { children: [renderLabel(inner), renderField(inner)] }, `g${index}-${inner.name}`)); }); const hasHeader = entry.title || entry.description; return (_jsxs(React.Fragment, { children: [hasHeader ? (_jsxs(Box, { "data-name": "form-group-header", className: "ab-Form_group-header", style: { gridColumn: '1 / -1', marginTop: index === 0 ? 0 : 'var(--ab-base-space-2)', marginBottom: 'var(--ab-base-space)', paddingBottom: 'var(--ab-base-space-half)', borderBottom: '1px solid var(--ab-color-divider, rgba(0,0,0,0.08))', }, children: [entry.title ? (_jsx(Box, { className: "ab-Form_group-title twa:text-4 twa:font-bold", children: entry.title })) : null, entry.description ? (_jsx(Box, { className: "ab-Form_group-description twa:text-xs twa:opacity-70 twa:mt-1", children: entry.description })) : null] })) : null, innerNodes] }, `group-${index}`)); } if (Array.isArray(entry)) { const rowFields = {}; const visible = entry.filter((f) => !isAdaptableFormFieldHidden(f, data, context)); if (visible.length === 0) return null; visible.map((fieldItem, idx) => { const rowFieldIndex = 2 * idx + 1; rowFields[rowFieldIndex] = renderLabel(fieldItem); rowFields[rowFieldIndex + 1] = renderField(fieldItem); }); return _jsx(FormRow, { ...rowFields }, index); } if (isAdaptableFormFieldHidden(entry, data, context)) { return null; } return (_jsxs(FormRow, { children: [renderLabel(entry), renderField(entry)] }, entry.name)); }; return (_jsxs(_Fragment, { children: [displayTitle && formDef.title && (_jsx(Box, { "data-name": "form-title", className: "twa:text-5 twa:mb-2 twa:font-bold", children: formDef.title })), formDef.description && (_jsx(Box, { "data-name": "form-description", className: "twa:mb-3", children: formDef.description })), isInlineLayout ? (_jsx(Flex, { "data-name": "form-content", className: "ab-FormLayout--inline", flexDirection: "row", alignItems: "center", flexWrap: "wrap", style: { columnGap: 'var(--ab-base-space-2)', rowGap: 'var(--ab-base-space)' }, onKeyDown: handleFormKeyDown, children: renderInlineEntries() })) : (_jsx(FormLayout, { onKeyDown: handleFormKeyDown, "data-name": "form-content", columns: columns, className: "twa:pr-1 twa:overflow-auto", children: formDef.fields?.map((entry, index) => renderRowsEntry(entry, index)) })), formDef.buttons ? (_jsx(Flex, { "data-name": "form-buttons", className: isInlineLayout ? '' : 'twa:mt-3', flexDirection: "row", alignItems: "center", justifyContent: isInlineLayout ? 'space-start' : 'center', style: isInlineLayout ? { marginLeft: 'var(--ab-base-space-2)', columnGap: 'var(--ab-base-space)' } : undefined, children: _jsx(AdaptableFormComponentButtons, { focusFirstButton: focusFirstButton, onClick: onButtonClick, disabledButtons: disabledButtons, defaultTone: "success", formDef: formDef, api: api, context: buttonContext }) })) : null] })); }