All files / frontend/src/components/SukohForm FieldRenderer.tsx

4.76% Statements 1/21
0% Branches 0/12
0% Functions 0/10
5% Lines 1/20

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128                                                                                                                      2x                                                                                                                                        
import { lazy, Suspense, useMemo, ComponentType } from 'react';
import { ErrorBoundary, FallbackProps } from 'react-error-boundary';
import Alert from '@mui/material/Alert';
import AlertTitle from '@mui/material/AlertTitle';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import CircularProgress from '@mui/material/CircularProgress';
import Typography from '@mui/material/Typography';
import { getFieldComponent, FieldComponentProps } from './FieldRegistry';
import { useFormContext } from './FormContext';
 
function FieldErrorFallback({
  error,
  resetErrorBoundary,
  compositeKey,
  fieldType,
}: FallbackProps & { compositeKey: string; fieldType: string }) {
  
  return (
    <Alert
      severity="error"
      sx={{ my: 1 }}
      action={
        <Button color="inherit" size="small" onClick={resetErrorBoundary}>
          Retry
        </Button>
      }
    >
      <AlertTitle>Field Error</AlertTitle>
      <Typography variant="body2" component="div">
        Failed to render field: <strong>{compositeKey}</strong>
      </Typography>
      <Typography variant="body2" color="text.secondary">
        Type: {fieldType}
      </Typography>
      {process.env.NODE_ENV === 'development' && (
        <Typography
          variant="caption"
          component="pre"
          sx={{ mt: 1, whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}
        >
          {error instanceof Error ? error.message : String(error)}
        </Typography>
      )}
    </Alert>
  );
}
 
function FieldLoadingFallback() {
  return (
    <Box sx={{ display: 'flex', alignItems: 'center', p: 1, gap: 1 }}>
      <CircularProgress size={16} />
      <Typography variant="body2" color="text.secondary">
        Loading field...
      </Typography>
    </Box>
  );
}
 
const componentCache = new Map<string, ComponentType<FieldComponentProps>>();
 
function getLazyComponent(fieldType: string): ComponentType<FieldComponentProps> {
  if (!componentCache.has(fieldType)) {
    const importer = getFieldComponent(fieldType);
    const LazyComponent = lazy(importer);
    componentCache.set(fieldType, LazyComponent);
  }
  return componentCache.get(fieldType)!;
}
 
interface FieldRendererProps {
  compositeKey: string;
  loadingFallback?: React.ReactNode;
}
 
export function FieldRenderer({ compositeKey, loadingFallback }: FieldRendererProps) {
  const form = useFormContext();
  const fieldConfig = form.getFieldConfig(compositeKey);
  const fieldType = fieldConfig?.type || 'not-found';
  const FieldComponent = useMemo(() => getLazyComponent(fieldType), [fieldType]);
 
  return (
    <ErrorBoundary
      fallbackRender={(props) => (
        <FieldErrorFallback {...props} compositeKey={compositeKey} fieldType={fieldType} />
      )}
      resetKeys={[`${compositeKey}-${fieldType}`]}
    >
      <Suspense fallback={loadingFallback || <FieldLoadingFallback />}>
        <FieldComponent compositeKey={compositeKey} />
      </Suspense>
    </ErrorBoundary>
  );
}
 
interface FieldListProps {
  compositeKeys: string[];
  wrapper?: ComponentType<{ children: React.ReactNode }>;
}
 
export function FieldList({ compositeKeys, wrapper: Wrapper }: FieldListProps) {
  if (Wrapper) {
    return (
      <>
        {compositeKeys.map((key) => (
          <Wrapper key={key}>
            <FieldRenderer compositeKey={key} />
          </Wrapper>
        ))}
      </>
    );
  }
 
  return (
    <>
      {compositeKeys.map((key) => (
        <FieldRenderer key={key} compositeKey={key} />
      ))}
    </>
  );
}
 
export function clearFieldComponentCache(): void {
  componentCache.clear();
}
 
export default FieldRenderer;