Skip to content
Merged
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
9 changes: 7 additions & 2 deletions formulus-formplayer/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ import ShellInputControl, {
} from './jsonforms/ShellInputControl';
import { applyClearOnHideToRenderers } from './jsonforms/applyClearOnHideToRenderers';
import type { KeyboardPrimaryEnterKeyHint } from './utils/keyboardEnterKeyHint';
import { additionalErrorsForDisplay } from './utils/additionalErrorsForDisplay';

import ErrorBoundary from './components/ErrorBoundary';
import { draftService } from './services/DraftService';
Expand Down Expand Up @@ -460,9 +461,10 @@ function App() {
// Deferred validation: new forms start hidden (no red errors on first paint),
// then switch to ValidateAndShow on first forward navigation / finalize. Edits
// and draft resumes start shown. Host can override via params.validationMode.
// Default Hide so pre-init flash cannot paint Show before FormInitData arrives.
const [validationMode, setValidationMode] = useState<
'ValidateAndShow' | 'ValidateAndHide' | 'NoValidation'
>('ValidateAndShow');
>('ValidateAndHide');
const [uiLocale, setUiLocale] = useState<OdeUiLocale>('en');
const uiLocaleRef = useRef(uiLocale);
uiLocaleRef.current = uiLocale;
Expand Down Expand Up @@ -1572,7 +1574,10 @@ function App() {
onChange={handleDataChange}
validationMode={validationMode}
ajv={ajv}
additionalErrors={customValidatorErrors}
additionalErrors={additionalErrorsForDisplay(
validationMode,
customValidatorErrors,
)}
/>
</FormEvaluationProvider>
{/* Success Snackbar */}
Expand Down
57 changes: 44 additions & 13 deletions formulus-formplayer/src/DynamicEnumControl.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
} from '@mui/material';
import QuestionShell from './components/QuestionShell';
import { useClearOnHide } from './jsonforms/useClearOnHide';
import { useOdeT } from './i18n/useOdeT';

/**
* Interface for x-dynamicEnum configuration
Expand Down Expand Up @@ -129,6 +130,7 @@ const DynamicEnumControl: React.FC<ControlProps> = ({
}) => {
const { functions } = useFormEvaluation();
const ctx = useJsonForms();
const t = useOdeT();

useClearOnHide({ visible, path, data, handleChange });

Expand Down Expand Up @@ -158,9 +160,11 @@ const DynamicEnumControl: React.FC<ControlProps> = ({
[handleChange, path],
);

// Find selected option based on current data value - must be before early returns
// Find selected option based on current data value - must be before early returns.
// Coerce so number/string mismatches from saved observations still resolve.
const selectedOption = useMemo(() => {
return choices.find(opt => opt.const === data) || null;
if (data == null || data === '') return null;
return choices.find(opt => String(opt.const) === String(data)) || null;
}, [choices, data]);

// Get display label from schema or uischema - computed before early returns
Expand All @@ -187,13 +191,20 @@ const DynamicEnumControl: React.FC<ControlProps> = ({
// Load choices when component mounts or params change
const loadChoices = useCallback(async () => {
if (!dynamicConfig) {
setError('x-dynamicEnum configuration is missing');
setError(
t(
'dynamicEnum.configMissing',
'x-dynamicEnum configuration is missing',
),
);
return;
}

// Validate configuration
if (!dynamicConfig.query) {
setError('x-dynamicEnum: query is required');
setError(
t('dynamicEnum.queryRequired', 'x-dynamicEnum: query is required'),
);
return;
}

Expand All @@ -203,7 +214,14 @@ const DynamicEnumControl: React.FC<ControlProps> = ({
if (!func) {
const availableFunctions = Array.from(functions.keys()).join(', ');
setError(
`Function "${functionName}" not found. Available: ${availableFunctions || 'none'}.`,
t(
'dynamicEnum.functionNotFound',
'Function "{{functionName}}" not found. Available: {{available}}.',
{
functionName,
available: availableFunctions || 'none',
},
),
);
return;
}
Expand Down Expand Up @@ -251,13 +269,15 @@ const DynamicEnumControl: React.FC<ControlProps> = ({
};
setLocalSchema(updatedSchema);
} catch (err: any) {
const errorMessage = err?.message || 'Failed to load dynamic choices';
const errorMessage =
err?.message ||
t('dynamicEnum.loadFailed', 'Failed to load dynamic choices');
setError(`${errorMessage}`);
console.error(`Error loading dynamic choices for ${path}:`, err);
} finally {
setLoading(false);
}
}, [dynamicConfig, functions, path, localSchema, currentFormData]); // Use currentFormData instead
}, [dynamicConfig, functions, path, localSchema, currentFormData, t]); // Use currentFormData instead

// Load choices on mount, when config changes, and when form data changes (for cascading filters)
// currentFormData must be in deps so fields that use {{data.field}} templates reload
Expand Down Expand Up @@ -311,7 +331,12 @@ const DynamicEnumControl: React.FC<ControlProps> = ({
{description}
</Typography>
)}
<Alert severity="error">x-dynamicEnum configuration is missing</Alert>
<Alert severity="error">
{t(
'dynamicEnum.configMissing',
'x-dynamicEnum configuration is missing',
)}
</Alert>
</Box>
);
}
Expand All @@ -326,7 +351,7 @@ const DynamicEnumControl: React.FC<ControlProps> = ({
<Box display="flex" alignItems="center" gap={2} sx={{ mt: 1 }}>
<CircularProgress size={20} />
<Typography variant="body2" color="text.secondary">
Loading choices...
{t('dynamicEnum.loading', 'Loading choices...')}
</Typography>
</Box>
) : error ? (
Expand All @@ -339,27 +364,33 @@ const DynamicEnumControl: React.FC<ControlProps> = ({
color="primary"
sx={{ cursor: 'pointer', textDecoration: 'underline' }}
onClick={loadChoices}>
Retry
{t('dynamicEnum.retry', 'Retry')}
</Typography>
</Box>
) : choices.length === 0 ? (
<Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>
No options available
{t('dynamicEnum.noOptions', 'No options available')}
</Typography>
) : (
<Autocomplete
value={selectedOption}
onChange={handleValueChange}
options={choices}
getOptionLabel={option => option.title || String(option.const)}
isOptionEqualToValue={(option, value) => option.const === value.const}
isOptionEqualToValue={(option, value) =>
String(option.const) === String(value.const)
}
disabled={!enabled}
sx={{ mt: 1 }}
renderInput={params => (
<TextField
{...params}
error={!!hasValidationErrors}
placeholder="Select an option..."
placeholder={
selectedOption
? undefined
: t('dynamicEnum.selectOption', 'Select an option...')
}
/>
)}
/>
Expand Down
17 changes: 17 additions & 0 deletions formulus-formplayer/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,24 @@
"media.qrCode": "QR Code",
"media.approximateDate": "Approximate Date",
"media.takePhoto": "Take photo",
"media.openingCamera": "Opening camera...",
"media.retakePhoto": "Retake photo",
"media.deletePhoto": "Delete photo",
"media.captureHint": "Capture a clear photo.",
"media.capturedPhotoAlt": "Captured photo",
"media.invalidFilename": "Invalid photo filename from camera.",
"media.cameraError": "Camera error occurred",
"media.unknownCameraError": "Unknown camera error",
"media.captureFailed": "Failed to capture photo. Please try again.",
"media.recording": "Recording...",
"dynamicEnum.selectOption": "Select an option...",
"dynamicEnum.loading": "Loading choices...",
"dynamicEnum.retry": "Retry",
"dynamicEnum.noOptions": "No options available",
"dynamicEnum.configMissing": "x-dynamicEnum configuration is missing",
"dynamicEnum.queryRequired": "x-dynamicEnum: query is required",
"dynamicEnum.functionNotFound": "Function \"{{functionName}}\" not found. Available: {{available}}.",
"dynamicEnum.loadFailed": "Failed to load dynamic choices",
"cqt.errorTitle": "Custom Question Type Error",
"cqt.errorBody": "The custom question type \"{{format}}\" encountered an error and could not be rendered.",
"cqt.errorDetails": "Error Details (click to expand)",
Expand Down
17 changes: 17 additions & 0 deletions formulus-formplayer/src/locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,24 @@
"media.qrCode": "Code QR",
"media.approximateDate": "Date approximative",
"media.takePhoto": "Prendre une photo",
"media.openingCamera": "Ouverture de l'appareil photo...",
"media.retakePhoto": "Reprendre la photo",
"media.deletePhoto": "Supprimer la photo",
"media.captureHint": "Prenez une photo nette.",
"media.capturedPhotoAlt": "Photo capturée",
"media.invalidFilename": "Nom de fichier photo invalide provenant de l'appareil photo.",
"media.cameraError": "Une erreur de l'appareil photo s'est produite",
"media.unknownCameraError": "Erreur inconnue de l'appareil photo",
"media.captureFailed": "Échec de la capture de la photo. Veuillez réessayer.",
"media.recording": "Enregistrement...",
"dynamicEnum.selectOption": "Sélectionnez une option...",
"dynamicEnum.loading": "Chargement des options...",
"dynamicEnum.retry": "Réessayer",
"dynamicEnum.noOptions": "Aucune option disponible",
"dynamicEnum.configMissing": "Configuration x-dynamicEnum manquante",
"dynamicEnum.queryRequired": "x-dynamicEnum : la requête est obligatoire",
"dynamicEnum.functionNotFound": "Fonction « {{functionName}} » introuvable. Disponibles : {{available}}.",
"dynamicEnum.loadFailed": "Échec du chargement des options dynamiques",
"cqt.errorTitle": "Erreur du type de question personnalisé",
"cqt.errorBody": "Le type de question personnalisé « {{format}} » a rencontré une erreur et n'a pas pu être affiché.",
"cqt.errorDetails": "Détails de l'erreur (cliquer pour développer)",
Expand Down
17 changes: 17 additions & 0 deletions formulus-formplayer/src/locales/pt.json
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,24 @@
"media.qrCode": "Código QR",
"media.approximateDate": "Data aproximada",
"media.takePhoto": "Tirar fotografia",
"media.openingCamera": "A abrir a câmara...",
"media.retakePhoto": "Repetir fotografia",
"media.deletePhoto": "Eliminar fotografia",
"media.captureHint": "Capture uma fotografia nítida.",
"media.capturedPhotoAlt": "Fotografia capturada",
"media.invalidFilename": "Nome de ficheiro de fotografia inválido da câmara.",
"media.cameraError": "Ocorreu um erro da câmara",
"media.unknownCameraError": "Erro desconhecido da câmara",
"media.captureFailed": "Falha ao capturar a fotografia. Tente novamente.",
"media.recording": "A gravar...",
"dynamicEnum.selectOption": "Selecione uma opção...",
"dynamicEnum.loading": "A carregar opções...",
"dynamicEnum.retry": "Tentar novamente",
"dynamicEnum.noOptions": "Nenhuma opção disponível",
"dynamicEnum.configMissing": "Configuração x-dynamicEnum em falta",
"dynamicEnum.queryRequired": "x-dynamicEnum: a consulta é obrigatória",
"dynamicEnum.functionNotFound": "Função \"{{functionName}}\" não encontrada. Disponíveis: {{available}}.",
"dynamicEnum.loadFailed": "Falha ao carregar opções dinâmicas",
"cqt.errorTitle": "Erro no tipo de pergunta personalizado",
"cqt.errorBody": "O tipo de pergunta personalizado \"{{format}}\" encontrou um erro e não pôde ser apresentado.",
"cqt.errorDetails": "Detalhes do erro (clique para expandir)",
Expand Down
4 changes: 2 additions & 2 deletions formulus-formplayer/src/renderers/FileQuestionRenderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
attachmentBasenameFromObservation,
} from '../utils/attachmentBasename';
import FormulusClient from '../services/FormulusInterface';
import { formatControlErrors } from '../utils/formatControlErrors';

const parsePx = (value: string): number =>
parseInt(value.replace('px', ''), 10);
Expand Down Expand Up @@ -183,8 +184,7 @@ const FileQuestionRenderer: React.FC<ControlProps> = ({
const obs = fileObservationRecord(data);
const hasData = obs !== null;
const displayName = displayFilenameForFileObservation(obs);
const validationError =
errors && errors.length > 0 ? String(errors[0]) : null;
const validationError = formatControlErrors(errors);

const label = (uischema as { label?: string }).label ?? schema.title;
const description = schema.description;
Expand Down
Loading
Loading