Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion formulus-formplayer/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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"
Expand Down
95 changes: 36 additions & 59 deletions formulus-formplayer/src/AudioQuestionRenderer.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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;
Expand Down Expand Up @@ -210,34 +202,19 @@ const AudioQuestionRenderer: React.FC<AudioQuestionRendererProps> = ({

const progress = duration > 0 ? (currentTime / duration) * 100 : 0;

return (
<Box sx={{ mb: 2 }}>
{/* Label */}
{schema.title && (
<Typography variant="subtitle1" sx={{ mb: 1, fontWeight: 500 }}>
{schema.title}
{schema.description && (
<Typography variant="body2" color="text.secondary" sx={{ mt: 0.5 }}>
{schema.description}
</Typography>
)}
</Typography>
)}

{/* Error Display */}
{error && (
<Alert severity="error" sx={{ mb: 2 }} onClose={() => setError(null)}>
{error}
</Alert>
)}

{/* Validation Errors */}
{errors && Array.isArray(errors) && errors.length > 0 && (
<Alert severity="error" sx={{ mb: 2 }}>
{errors.map((error: any) => error.message).join(', ')}
</Alert>
)}
const validationError =
errors && Array.isArray(errors) && errors.length > 0
? errors.map((error: any) => error.message || String(error)).join(', ')
: null;

return (
<QuestionShell
title={schema.title}
description={schema.description}
required={Boolean((uischema as any)?.options?.required ?? (schema as any)?.options?.required)}
error={error || validationError}
helperText="Record clear audio. You can re-record or delete as needed."
>
<Paper
variant="outlined"
sx={{
Expand Down Expand Up @@ -297,6 +274,27 @@ const AudioQuestionRenderer: React.FC<AudioQuestionRendererProps> = ({
) : (
// Playback State
<Box>
<Box sx={{ display: 'flex', gap: 1, mb: 2, justifyContent: 'center' }}>
<Button
variant="outlined"
startIcon={<RefreshIcon />}
onClick={handleRecord}
disabled={isLoading}
size="small"
>
Re-record
</Button>
<Button
variant="outlined"
color="error"
startIcon={<DeleteIcon />}
onClick={handleDelete}
size="small"
>
Delete
</Button>
</Box>

{/* Audio element (hidden) */}
<audio ref={audioRef} src={audioData.uri} preload="metadata" />

Expand Down Expand Up @@ -369,27 +367,6 @@ const AudioQuestionRenderer: React.FC<AudioQuestionRendererProps> = ({
</IconButton>
</Box>

{/* Action Buttons */}
<Box sx={{ display: 'flex', gap: 1, justifyContent: 'center' }}>
<Button
variant="outlined"
startIcon={<RefreshIcon />}
onClick={handleRecord}
disabled={isLoading}
>
Re-record
</Button>

<Button
variant="outlined"
color="error"
startIcon={<DeleteIcon />}
onClick={handleDelete}
>
Delete
</Button>
</Box>

{/* Development Info */}
{process.env.NODE_ENV === 'development' && (
<Box sx={{ mt: 2, p: 1, backgroundColor: 'grey.100', borderRadius: 1 }}>
Expand All @@ -401,7 +378,7 @@ const AudioQuestionRenderer: React.FC<AudioQuestionRendererProps> = ({
</Box>
)}
</Paper>
</Box>
</QuestionShell>
);
};

Expand Down
174 changes: 81 additions & 93 deletions formulus-formplayer/src/FileQuestionRenderer.tsx
Original file line number Diff line number Diff line change
@@ -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, Chip } from '@mui/material';
import {
AttachFile as FileIcon,
Delete as DeleteIcon,
Expand All @@ -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(
Expand Down Expand Up @@ -136,35 +128,29 @@ const FileQuestionRenderer: React.FC<ControlProps> = ({

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 (
<Box sx={{ mb: 2 }}>
{/* Title and Description */}
{schema.title && (
<Typography variant="subtitle1" sx={{ mb: 1, fontWeight: 500 }}>
{schema.title}
</Typography>
)}
{schema.description && (
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
{schema.description}
</Typography>
)}

{/* Error Display */}
{error && (
<Alert severity="error" sx={{ mb: 2 }}>
{error}
</Alert>
)}

{/* Validation Errors */}
{hasError && (
<Alert severity="error" sx={{ mb: 2 }}>
{Array.isArray(errors) ? errors.join(', ') : errors}
</Alert>
)}

<QuestionShell
title={schema.title}
description={schema.description}
required={Boolean((uischema as any)?.options?.required ?? (schema as any)?.options?.required)}
error={error || validationError}
helperText="Attach a file. Images, PDFs, and documents are supported."
metadata={
process.env.NODE_ENV === 'development' ? (
<Box sx={{ mt: 1, p: 1, bgcolor: 'info.light', borderRadius: 1 }}>
<Typography variant="caption" sx={{ fontFamily: 'monospace' }}>
Debug: fieldId="{fieldId}", path="{path}", format="select_file"
</Typography>
</Box>
) : undefined
}
>
{/* File Selection Button */}
{!hasData && (
<Button
Expand All @@ -182,76 +168,78 @@ const FileQuestionRenderer: React.FC<ControlProps> = ({
{/* File Display */}
{hasData && (
<Paper sx={{ p: 2, bgcolor: 'grey.50' }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
<Box sx={{ flex: 1 }}>
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
{getFileIcon(data.mimeType)}
<Box sx={{ ml: 2, flex: 1 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
{data.filename}
</Typography>
<Box sx={{ display: 'flex', gap: 1, mt: 1, flexWrap: 'wrap' }}>
<Chip
label={getFileTypeLabel(data.mimeType)}
size="small"
color="primary"
variant="outlined"
/>
<Chip label={formatFileSize(data.size)} size="small" variant="outlined" />
{data.metadata.extension && (
<Chip
label={`.${data.metadata.extension.toUpperCase()}`}
size="small"
variant="outlined"
/>
)}
</Box>
</Box>
</Box>

<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mb: 1 }}>
<strong>URI:</strong> {data.uri}
</Typography>

<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mb: 1 }}>
<strong>MIME Type:</strong> {data.mimeType}
</Typography>

{data.metadata.originalPath && (
<Typography variant="caption" color="text.secondary" sx={{ display: 'block' }}>
<strong>Original Path:</strong> {data.metadata.originalPath}
</Typography>
)}

{/* Replace File Button */}
<Box
sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
mb: 2,
}}
>
<Box sx={{ display: 'flex', gap: 1 }}>
<Button
variant="outlined"
startIcon={<FileIcon />}
onClick={handleFileSelection}
disabled={!enabled || isSelecting}
size="small"
sx={{ mt: 2 }}
>
Replace File
Replace
</Button>
<Button
variant="outlined"
color="error"
startIcon={<DeleteIcon />}
onClick={handleDelete}
disabled={!enabled}
size="small"
>
Delete
</Button>
</Box>
</Box>

<IconButton onClick={handleDelete} disabled={!enabled} size="small" sx={{ ml: 1 }}>
<DeleteIcon />
</IconButton>
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
{getFileIcon(data.mimeType)}
<Box sx={{ ml: 2, flex: 1 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
{data.filename}
</Typography>
<Box sx={{ display: 'flex', gap: 1, mt: 1, flexWrap: 'wrap' }}>
<Chip
label={getFileTypeLabel(data.mimeType)}
size="small"
color="primary"
variant="outlined"
/>
<Chip label={formatFileSize(data.size)} size="small" variant="outlined" />
{data.metadata.extension && (
<Chip
label={`.${data.metadata.extension.toUpperCase()}`}
size="small"
variant="outlined"
/>
)}
</Box>
</Box>
</Box>
</Paper>
)}

{/* Development Debug Info */}
{process.env.NODE_ENV === 'development' && (
<Box sx={{ mt: 2, p: 1, bgcolor: 'info.light', borderRadius: 1 }}>
<Typography variant="caption" sx={{ fontFamily: 'monospace' }}>
Debug: fieldId="{fieldId}", path="{path}", format="select_file"
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mb: 1 }}>
<strong>URI:</strong> {data.uri}
</Typography>
</Box>

<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mb: 1 }}>
<strong>MIME Type:</strong> {data.mimeType}
</Typography>

{data.metadata.originalPath && (
<Typography variant="caption" color="text.secondary" sx={{ display: 'block' }}>
<strong>Original Path:</strong> {data.metadata.originalPath}
</Typography>
)}
</Paper>
)}
</Box>
</QuestionShell>
);
};

Expand Down
Loading
Loading