- Externalize all @tamagui/* and tamagui subpaths so dist no longer vendors Tamagui. - Emit TypeScript declarations with vite-plugin-dts; fix package exports types for ui/*. - Align initEnv with profiles: displayName, brandLogo, api.baseURL, themeColor, uiShell. - Stabilize tests with Node localStorage file; env tests pass. - Update README and component docs for services, menus, API client, and development.
146 lines
3.6 KiB
JavaScript
146 lines
3.6 KiB
JavaScript
import React, { useMemo } from 'react';
|
|
import { Button, XStack, YStack } from 'tamagui';
|
|
import { SidePanelShell } from './SidePanelShell.jsx';
|
|
import { FormField } from './FormField.jsx';
|
|
import { getIcon } from './IconMapper.jsx';
|
|
|
|
function defaultExpressionEvaluator(template, form) {
|
|
return template.replace(/\{\{(\w+)\}\}/g, (_match, fieldName) => form[fieldName] || '');
|
|
}
|
|
|
|
function renderAction(action, index, fallbackHandler) {
|
|
const IconComponent = action?.icon ? getIcon(action.icon) : null;
|
|
return {
|
|
id: action?.id || action?.label || `action-${index}`,
|
|
label: action?.label,
|
|
icon: action?.icon,
|
|
disabled: action?.disabled,
|
|
theme: action?.theme,
|
|
chromeless: action?.chromeless,
|
|
onPress: action?.onPress || fallbackHandler,
|
|
iconComponent: IconComponent
|
|
};
|
|
}
|
|
|
|
export function FormView({
|
|
open = false,
|
|
onClose = null,
|
|
title = 'Edit Record',
|
|
toolbar = [],
|
|
fields = [],
|
|
values = {},
|
|
onChange = () => {},
|
|
onSubmit = () => {},
|
|
onReset = () => {},
|
|
buttons = [],
|
|
loading = false,
|
|
errors = {},
|
|
children = null,
|
|
hideButtons = false,
|
|
width = 460
|
|
}) {
|
|
const processedFields = useMemo(() => {
|
|
return fields.map((field) => {
|
|
if (!field.expression) {
|
|
return field;
|
|
}
|
|
|
|
const expressionFunction = typeof field.expression === 'string'
|
|
? (form) => defaultExpressionEvaluator(field.expression, form)
|
|
: field.expression;
|
|
|
|
return {
|
|
...field,
|
|
expressionFunction,
|
|
readOnly: true
|
|
};
|
|
});
|
|
}, [fields]);
|
|
|
|
const computedValues = useMemo(() => {
|
|
return processedFields.reduce((accumulator, field) => {
|
|
if (field.expressionFunction) {
|
|
accumulator[field.id] = field.expressionFunction(values);
|
|
}
|
|
return accumulator;
|
|
}, {});
|
|
}, [processedFields, values]);
|
|
|
|
const mergedValues = {
|
|
...values,
|
|
...computedValues
|
|
};
|
|
|
|
const footerActions = useMemo(() => {
|
|
if (hideButtons) {
|
|
return [];
|
|
}
|
|
|
|
const sourceButtons = buttons.length > 0
|
|
? buttons
|
|
: [
|
|
{ label: 'Reset', chromeless: true, onPress: onReset },
|
|
{ label: 'Save', theme: 'active', onPress: onSubmit }
|
|
];
|
|
|
|
return sourceButtons.map((button, index) => renderAction(button, index, index === 0 ? onReset : onSubmit));
|
|
}, [buttons, hideButtons, onReset, onSubmit]);
|
|
|
|
const renderField = (field, index) => (
|
|
<FormField
|
|
key={field.id || `${field.type || 'field'}-${index}`}
|
|
{...field}
|
|
value={mergedValues[field.id]}
|
|
onChange={onChange}
|
|
error={errors[field.id]}
|
|
disabled={field.disabled || loading}
|
|
/>
|
|
);
|
|
|
|
const renderChildren = () => {
|
|
if (!children) {
|
|
return null;
|
|
}
|
|
|
|
return React.Children.map(children, (child) => {
|
|
if (React.isValidElement(child) && child.type === FormField) {
|
|
const fieldId = child.props.id;
|
|
return React.cloneElement(child, {
|
|
value: mergedValues[fieldId],
|
|
onChange,
|
|
error: errors[fieldId],
|
|
disabled: child.props.disabled || loading
|
|
});
|
|
}
|
|
return child;
|
|
});
|
|
};
|
|
|
|
return (
|
|
<SidePanelShell
|
|
open={open}
|
|
onClose={onClose}
|
|
title={title}
|
|
toolbar={toolbar}
|
|
footerActions={footerActions}
|
|
width={width}
|
|
>
|
|
<YStack gap="$4">
|
|
{processedFields.map(renderField)}
|
|
{children ? (
|
|
<YStack gap="$4">
|
|
{renderChildren()}
|
|
</YStack>
|
|
) : null}
|
|
{loading ? (
|
|
<XStack justifyContent="flex-end">
|
|
<Button disabled>Saving...</Button>
|
|
</XStack>
|
|
) : null}
|
|
</YStack>
|
|
</SidePanelShell>
|
|
);
|
|
}
|
|
|
|
export default FormView;
|