@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
JavaScript
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] }));
}