From 1f00229cfa5449b49ead07f7f728476bf9483b41 Mon Sep 17 00:00:00 2001 From: Najuna Brian Date: Sat, 20 Dec 2025 04:47:08 +0300 Subject: [PATCH 1/3] feat: add question shell and card selects --- formulus-formplayer/src/App.tsx | 7 +- .../src/AudioQuestionRenderer.tsx | 53 +++----- .../src/FileQuestionRenderer.tsx | 71 ++++------ .../src/GPSQuestionRenderer.tsx | 66 ++++----- .../src/PhotoQuestionRenderer.tsx | 119 +++++++--------- .../src/QrcodeQuestionRenderer.tsx | 103 ++++++-------- formulus-formplayer/src/QuestionShell.tsx | 102 ++++++++++++++ .../src/SignatureQuestionRenderer.tsx | 63 +++------ .../src/VideoQuestionRenderer.tsx | 76 ++++------- formulus-formplayer/src/material-wrappers.tsx | 127 ++++++++++++++++++ formulus-formplayer/src/theme.ts | 10 ++ 11 files changed, 450 insertions(+), 347 deletions(-) create mode 100644 formulus-formplayer/src/QuestionShell.tsx create mode 100644 formulus-formplayer/src/material-wrappers.tsx diff --git a/formulus-formplayer/src/App.tsx b/formulus-formplayer/src/App.tsx index b4b87577f..78ae04f0e 100644 --- a/formulus-formplayer/src/App.tsx +++ b/formulus-formplayer/src/App.tsx @@ -25,6 +25,7 @@ import FileQuestionRenderer, { fileQuestionTester } from './FileQuestionRenderer import AudioQuestionRenderer, { audioQuestionTester } from './AudioQuestionRenderer'; import GPSQuestionRenderer, { gpsQuestionTester } from './GPSQuestionRenderer'; import VideoQuestionRenderer, { videoQuestionTester } from './VideoQuestionRenderer'; +import { shellMaterialRenderers } from './material-wrappers'; import ErrorBoundary from './ErrorBoundary'; import { draftService } from './DraftService'; @@ -671,7 +672,11 @@ function App() { schema={schema} uischema={uischema} data={data} - renderers={[...materialRenderers, ...customRenderers]} + renderers={[ + ...shellMaterialRenderers, + ...materialRenderers, + ...customRenderers, + ]} cells={materialCells} onChange={handleDataChange} validationMode="ValidateAndShow" diff --git a/formulus-formplayer/src/AudioQuestionRenderer.tsx b/formulus-formplayer/src/AudioQuestionRenderer.tsx index d13aeacc4..bf6d59746 100644 --- a/formulus-formplayer/src/AudioQuestionRenderer.tsx +++ b/formulus-formplayer/src/AudioQuestionRenderer.tsx @@ -1,16 +1,7 @@ import React, { useState, useRef, useEffect } from 'react'; import { withJsonFormsControlProps } from '@jsonforms/react'; import { ControlProps, rankWith, formatIs } from '@jsonforms/core'; -import { - Box, - Button, - Typography, - Paper, - IconButton, - LinearProgress, - Alert, - Chip, -} from '@mui/material'; +import { Box, Button, Typography, Paper, IconButton, LinearProgress, Chip } from '@mui/material'; import { Mic as MicIcon, Stop as StopIcon, @@ -21,6 +12,7 @@ import { } from '@mui/icons-material'; import FormulusClient from './FormulusInterface'; import { AudioResult } from './FormulusInterfaceDefinition'; +import QuestionShell from './QuestionShell'; interface AudioQuestionRendererProps extends ControlProps { data: any; @@ -210,34 +202,19 @@ const AudioQuestionRenderer: React.FC = ({ const progress = duration > 0 ? (currentTime / duration) * 100 : 0; - return ( - - {/* Label */} - {schema.title && ( - - {schema.title} - {schema.description && ( - - {schema.description} - - )} - - )} - - {/* Error Display */} - {error && ( - setError(null)}> - {error} - - )} - - {/* Validation Errors */} - {errors && Array.isArray(errors) && errors.length > 0 && ( - - {errors.map((error: any) => error.message).join(', ')} - - )} + const validationError = + errors && Array.isArray(errors) && errors.length > 0 + ? errors.map((error: any) => error.message || String(error)).join(', ') + : null; + return ( + = ({ )} - + ); }; diff --git a/formulus-formplayer/src/FileQuestionRenderer.tsx b/formulus-formplayer/src/FileQuestionRenderer.tsx index 3bde4f3b5..32a0b20d0 100644 --- a/formulus-formplayer/src/FileQuestionRenderer.tsx +++ b/formulus-formplayer/src/FileQuestionRenderer.tsx @@ -1,14 +1,5 @@ import React, { useState, useCallback, useRef } from 'react'; -import { - Button, - Typography, - Box, - Alert, - CircularProgress, - Paper, - IconButton, - Chip, -} from '@mui/material'; +import { Button, Typography, Box, CircularProgress, Paper, IconButton, Chip } from '@mui/material'; import { AttachFile as FileIcon, Delete as DeleteIcon, @@ -21,6 +12,7 @@ import { withJsonFormsControlProps } from '@jsonforms/react'; import { ControlProps, rankWith, schemaTypeIs, and, schemaMatches } from '@jsonforms/core'; import FormulusClient from './FormulusInterface'; import { FileResult } from './FormulusInterfaceDefinition'; +import QuestionShell from './QuestionShell'; // Tester function - determines when this renderer should be used export const fileQuestionTester = rankWith( @@ -136,35 +128,29 @@ const FileQuestionRenderer: React.FC = ({ const hasData = data && typeof data === 'object' && data.type === 'file'; const hasError = errors && (Array.isArray(errors) ? errors.length > 0 : errors.length > 0); + const validationError = hasError + ? Array.isArray(errors) + ? errors.join(', ') + : (errors as any) + : null; return ( - - {/* Title and Description */} - {schema.title && ( - - {schema.title} - - )} - {schema.description && ( - - {schema.description} - - )} - - {/* Error Display */} - {error && ( - - {error} - - )} - - {/* Validation Errors */} - {hasError && ( - - {Array.isArray(errors) ? errors.join(', ') : errors} - - )} - + + + Debug: fieldId="{fieldId}", path="{path}", format="select_file" + + + ) : undefined + } + > {/* File Selection Button */} {!hasData && ( )} - - {/* Debug info in development */} - {process.env.NODE_ENV === 'development' && ( - - - Debug Info: - - - {JSON.stringify( - { - fieldId, - path, - currentPhotoData, - hasPhotoData: !!currentPhotoData, - hasFilename: !!currentPhotoData?.filename, - hasUri: !!currentPhotoData?.uri, - photoUrl, - hasPhotoUrl: !!photoUrl, - shouldShowThumbnail: !!(currentPhotoData && currentPhotoData.filename && photoUrl), - isLoading, - error, - }, - null, - 2, - )} - - - )} - + ); }; diff --git a/formulus-formplayer/src/QrcodeQuestionRenderer.tsx b/formulus-formplayer/src/QrcodeQuestionRenderer.tsx index 637aebd54..5e69bd55e 100644 --- a/formulus-formplayer/src/QrcodeQuestionRenderer.tsx +++ b/formulus-formplayer/src/QrcodeQuestionRenderer.tsx @@ -1,19 +1,11 @@ import React, { useState, useRef, useCallback } from 'react'; import { withJsonFormsControlProps } from '@jsonforms/react'; import { ControlProps, rankWith, schemaTypeIs, and, schemaMatches } from '@jsonforms/core'; -import { - Button, - Box, - Typography, - Card, - CardContent, - IconButton, - Alert, - TextField, -} from '@mui/material'; +import { Button, Box, Typography, Card, CardContent, IconButton, TextField } from '@mui/material'; import { QrCodeScanner, Delete, Refresh } from '@mui/icons-material'; import FormulusClient from './FormulusInterface'; import { QrcodeResult } from './FormulusInterfaceDefinition'; +import QuestionShell from './QuestionShell'; // Tester function to identify QR code question types export const qrcodeQuestionTester = rankWith( @@ -149,39 +141,45 @@ const QrcodeQuestionRenderer: React.FC = ({ // Get display label from schema or uischema const label = (uischema as any)?.label || schema.title || 'QR Code'; const description = schema.description; - const isRequired = schema.required || false; - - return ( - - {/* Label and description */} - - {label} - {isRequired && *} - - - {description && ( - - {description} - - )} - - {/* Error display - full width, pushes content down */} - {error && ( - - {error} - - )} + const isRequired = Boolean( + (uischema as any)?.options?.required ?? (schema as any)?.options?.required ?? false, + ); - {/* Form validation errors */} - {errors && errors.length > 0 && ( - - {String(errors[0])} - - )} + const validationError = errors && errors.length > 0 ? String(errors[0]) : null; - {/* QR code value display or scanner button */} + return ( + + + Debug Info: + + + {JSON.stringify( + { + fieldId, + path, + currentQrcodeValue, + hasQrcodeValue: !!currentQrcodeValue, + isLoading, + error, + }, + null, + 2, + )} + + + ) : undefined + } + > {currentQrcodeValue ? ( - + @@ -263,30 +261,7 @@ const QrcodeQuestionRenderer: React.FC = ({ )} - - {/* Debug info in development */} - {process.env.NODE_ENV === 'development' && ( - - - Debug Info: - - - {JSON.stringify( - { - fieldId, - path, - currentQrcodeValue, - hasQrcodeValue: !!currentQrcodeValue, - isLoading, - error, - }, - null, - 2, - )} - - - )} - + ); }; diff --git a/formulus-formplayer/src/QuestionShell.tsx b/formulus-formplayer/src/QuestionShell.tsx new file mode 100644 index 000000000..bffc3fed4 --- /dev/null +++ b/formulus-formplayer/src/QuestionShell.tsx @@ -0,0 +1,102 @@ +import React, { ReactNode } from 'react'; +import { Box, Typography, Alert, Stack, Divider } from '@mui/material'; + +export interface QuestionShellProps { + title?: string; + description?: string; + required?: boolean; + error?: string | string[] | null; + helperText?: ReactNode; + actions?: ReactNode; + metadata?: ReactNode; + children: ReactNode; +} + +const normalizeError = (error?: string | string[] | null): string | null => { + if (!error) return null; + if (Array.isArray(error)) { + return error.filter(Boolean).join(', ') || null; + } + return error; +}; + +const QuestionShell: React.FC = ({ + title, + description, + required = false, + error, + helperText, + actions, + metadata, + children, +}) => { + const normalizedError = normalizeError(error); + + return ( + + {(title || description) && ( + + {title && ( + + {title} + {required && ( + + * + + )} + + )} + {description && ( + + {description} + + )} + + )} + + {normalizedError && ( + + {normalizedError} + + )} + + + {children} + + + {(helperText || actions) && ( + + {helperText && ( + + {helperText} + + )} + {actions} + + )} + + {metadata && ( + + + {metadata} + + )} + + ); +}; + +export default QuestionShell; diff --git a/formulus-formplayer/src/SignatureQuestionRenderer.tsx b/formulus-formplayer/src/SignatureQuestionRenderer.tsx index 974f2dba0..b023d3ee8 100644 --- a/formulus-formplayer/src/SignatureQuestionRenderer.tsx +++ b/formulus-formplayer/src/SignatureQuestionRenderer.tsx @@ -1,5 +1,5 @@ import React, { useState, useCallback, useRef, useEffect } from 'react'; -import { Button, Typography, Box, Alert, CircularProgress, Paper, IconButton } from '@mui/material'; +import { Button, Typography, Box, CircularProgress, Paper, IconButton } from '@mui/material'; import { Draw as SignatureIcon, Delete as DeleteIcon, @@ -9,6 +9,7 @@ import { withJsonFormsControlProps } from '@jsonforms/react'; import { ControlProps, rankWith, formatIs } from '@jsonforms/core'; import FormulusClient from './FormulusInterface'; import { SignatureResult } from './FormulusInterfaceDefinition'; +import QuestionShell from './QuestionShell'; // Tester function - determines when this renderer should be used export const signatureQuestionTester = rankWith( @@ -227,39 +228,28 @@ const SignatureQuestionRenderer: React.FC = ({ } const hasData = data && typeof data === 'object' && data.type === 'signature'; - const hasError = errors && (Array.isArray(errors) ? errors.length > 0 : errors.length > 0); + const validationError = errors && (Array.isArray(errors) ? errors.join(', ') : errors); return ( - - {/* Title and Description */} - {schema.title && ( - - {schema.title} - - )} - {schema.description && ( - - {schema.description} - - )} - - {/* Error Display */} - {error && ( - - {error} - - )} - - {/* Validation Errors */} - {hasError && ( - - {Array.isArray(errors) ? errors.join(', ') : errors} - - )} - + + + Debug: fieldId="{fieldId}", path="{path}", format="signature" + + + ) : undefined + } + > {/* Canvas Signature Pad */} {showCanvas && ( - + Draw your signature below: @@ -325,7 +315,7 @@ const SignatureQuestionRenderer: React.FC = ({ {/* Action Buttons */} {!showCanvas && ( - + + + + {/* Audio element (hidden) */} - {/* Action Buttons */} - - - - - - {/* Development Info */} {process.env.NODE_ENV === 'development' && ( diff --git a/formulus-formplayer/src/FileQuestionRenderer.tsx b/formulus-formplayer/src/FileQuestionRenderer.tsx index 32a0b20d0..5cd8495e2 100644 --- a/formulus-formplayer/src/FileQuestionRenderer.tsx +++ b/formulus-formplayer/src/FileQuestionRenderer.tsx @@ -1,5 +1,5 @@ import React, { useState, useCallback, useRef } from 'react'; -import { Button, Typography, Box, CircularProgress, Paper, IconButton, Chip } from '@mui/material'; +import { Button, Typography, Box, CircularProgress, Paper, Chip } from '@mui/material'; import { AttachFile as FileIcon, Delete as DeleteIcon, @@ -168,64 +168,75 @@ const FileQuestionRenderer: React.FC = ({ {/* File Display */} {hasData && ( - - - - {getFileIcon(data.mimeType)} - - - {data.filename} - - - - - {data.metadata.extension && ( - - )} - - - - - - URI: {data.uri} - - - - MIME Type: {data.mimeType} - - - {data.metadata.originalPath && ( - - Original Path: {data.metadata.originalPath} - - )} - - {/* Replace File Button */} + + + + - - - + + {getFileIcon(data.mimeType)} + + + {data.filename} + + + + + {data.metadata.extension && ( + + )} + + + + + URI: {data.uri} + + + + MIME Type: {data.mimeType} + + + {data.metadata.originalPath && ( + + Original Path: {data.metadata.originalPath} + + )} )} diff --git a/formulus-formplayer/src/GPSQuestionRenderer.tsx b/formulus-formplayer/src/GPSQuestionRenderer.tsx index 3e12ed97b..07e4e9396 100644 --- a/formulus-formplayer/src/GPSQuestionRenderer.tsx +++ b/formulus-formplayer/src/GPSQuestionRenderer.tsx @@ -120,14 +120,38 @@ const GPSQuestionRenderer: React.FC = (props) => { {locationData ? ( + + + + + + + } /> + + + Location Captured - - } /> - @@ -183,29 +207,6 @@ const GPSQuestionRenderer: React.FC = (props) => { - - {/* Action Buttons */} - - - - ) : ( diff --git a/formulus-formplayer/src/PhotoQuestionRenderer.tsx b/formulus-formplayer/src/PhotoQuestionRenderer.tsx index c37c05ff8..464b2d7b1 100644 --- a/formulus-formplayer/src/PhotoQuestionRenderer.tsx +++ b/formulus-formplayer/src/PhotoQuestionRenderer.tsx @@ -210,38 +210,36 @@ const PhotoQuestionRenderer: React.FC = ({ } > {currentPhotoData && currentPhotoData.filename && photoUrl ? ( - - - - - - File: {currentPhotoData.filename} - - - - - - - - - + + + + + + + + + + + + File: {currentPhotoData.filename} + ) : ( diff --git a/formulus-formplayer/src/VideoQuestionRenderer.tsx b/formulus-formplayer/src/VideoQuestionRenderer.tsx index 22afcc533..b39cb1007 100644 --- a/formulus-formplayer/src/VideoQuestionRenderer.tsx +++ b/formulus-formplayer/src/VideoQuestionRenderer.tsx @@ -9,7 +9,6 @@ import { Stop as StopIcon, Refresh as RefreshIcon, Delete as DeleteIcon, - VideoFile as VideoFileIcon, } from '@mui/icons-material'; import QuestionShell from './QuestionShell'; // Note: The shared Formulus interface v1.1.0 no longer exposes a @@ -150,10 +149,27 @@ const VideoQuestionRenderer: React.FC = (props) => { - - - Video Recorded - + + + + = (props) => { - - {/* Action Buttons */} - - - - ) : (