Initial commit: bface library, build fixes, and refreshed docs
- 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.
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user