Skip to content
Open
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
8 changes: 8 additions & 0 deletions formulus-formplayer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ The formplayer is solely responsible for
- submitting the forms to Formulus (either as draft or final)
- (soft-)deleting observations

## Development setup
This project depends on `@ode/tokens` (local `packages/tokens`). On a fresh clone or new branch, install in order:

1. From repo root: `cd packages/tokens && npm install`
2. Then: `cd formulus-formplayer && npm install && npm start`

If you run `npm install` only in formulus-formplayer, the tokens package’s `prepare` script may fail with "Cannot find module 'style-dictionary'" until tokens has its own dependencies installed.

## Building this project
Use 'npm run build:rn' to build the project. This will build the project and copy the build to the formulus app.

Expand Down
29 changes: 29 additions & 0 deletions formulus-formplayer/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions formulus-formplayer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"@mui/icons-material": "^7.3.7",
"@mui/material": "^7.3.7",
"@mui/x-date-pickers": "^8.26.0",
"@ode/components": "file:../packages/components",
"@ode/tokens": "file:../packages/tokens",
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.9.1",
Expand Down Expand Up @@ -57,6 +58,7 @@
"eslint-plugin-react-hooks": "^7.0.1",
"globals": "^17.1.0",
"prettier": "3.8.1",
"react-refresh": "^0.17.0",
"typescript-eslint": "^8.54.0",
"vite": "^6.4.1"
},
Expand Down
24 changes: 24 additions & 0 deletions formulus-formplayer/src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,27 @@
transform: rotate(360deg);
}
}

/* Reverse-style button */
.button-reverse-primary {
background-color: var(--ode-color-brand-primary-500, #4f7f4e) !important;
color: var(--ode-color-neutral-white, #ffffff) !important;
border-color: var(--ode-color-brand-primary-500, #4f7f4e) !important;
}

.button-reverse-primary:hover {
background-color: transparent !important;
color: var(--ode-color-brand-primary-500, #4f7f4e) !important;
border-color: var(--ode-color-brand-primary-500, #4f7f4e) !important;
}

/* Hide native number input spinners - replaced with custom NumberStepperRenderer */
input[type='number']::-webkit-inner-spin-button,
input[type='number']::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
}

input[type='number'] {
-moz-appearance: textfield;
}
40 changes: 31 additions & 9 deletions formulus-formplayer/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
ThemeProvider,
} from '@mui/material';
import { createTheme, getThemeOptions } from './theme/theme';
import { tokens } from './theme/tokens-adapter';
import Ajv from 'ajv';
import addErrors from 'ajv-errors';
import addFormats from 'ajv-formats';
Expand Down Expand Up @@ -65,6 +66,7 @@ import AdateQuestionRenderer, {
adateQuestionTester,
} from './renderers/AdateQuestionRenderer';
import { shellMaterialRenderers } from './theme/material-wrappers';
import { numberStepperRenderer } from './renderers/NumberStepperRenderer';
import DynamicEnumControl, { dynamicEnumTester } from './DynamicEnumControl';

import ErrorBoundary from './components/ErrorBoundary';
Expand Down Expand Up @@ -223,6 +225,8 @@ export const customRenderers = [
{ tester: adateQuestionTester, renderer: AdateQuestionRenderer },
// Dynamic choice list renderer for x-dynamicEnum fields
{ tester: dynamicEnumTester, renderer: DynamicEnumControl },
// Number/integer fields with simple +/- buttons via InputAdornment
numberStepperRenderer,
];

function App() {
Expand Down Expand Up @@ -269,8 +273,7 @@ function App() {
// Store extension functions for potential future use (e.g., validation context injection)

const [extensionFunctions, setExtensionFunctions] = useState<
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
Map<string, Function>
Map<string, (...args: any[]) => any>
>(new Map());
const [extensionDefinitions, setExtensionDefinitions] = useState<
Record<string, any>
Expand Down Expand Up @@ -333,7 +336,7 @@ function App() {

// Merge loaded functions with built-ins (loaded functions take precedence)
extensionResult.functions.forEach((func, name) => {
allFunctions.set(name, func);
allFunctions.set(name, func as (...args: any[]) => any);
});

setExtensionRenderers(extensionResult.renderers);
Expand Down Expand Up @@ -799,6 +802,25 @@ function App() {
return createTheme(getThemeOptions(darkMode ? 'dark' : 'light'));
}, [darkMode]);

// Set CSS custom properties from tokens for use in CSS files
// Must be called before any early returns to follow React Hooks rules
useEffect(() => {
const root = document.documentElement;
root.style.setProperty(
'--ode-color-brand-primary-500',
tokens.color.brand.primary[500],
);
root.style.setProperty(
'--ode-color-neutral-white',
tokens.color.neutral.white,
);
root.style.setProperty(
'--ode-color-neutral-200',
tokens.color.neutral[200],
);
root.style.setProperty('--ode-color-neutral-50', tokens.color.neutral[50]);
}, []);

// Show draft selector if we have pending form init and available drafts
if (showDraftSelector && pendingFormInit) {
return (
Expand Down Expand Up @@ -913,7 +935,7 @@ function App() {
style={{
width: process.env.NODE_ENV === 'development' ? '60%' : '100%',
overflow: 'hidden', // Prevent outer scrolling - FormLayout handles scrolling internally
padding: '4px',
padding: tokens.spacing[1],
boxSizing: 'border-box',
height: '100%', // Ensure it takes full height
backgroundColor: 'transparent', // Use theme background
Expand All @@ -922,11 +944,11 @@ function App() {
{loadError ? (
<Box
sx={{
padding: '20px',
padding: tokens.spacing[5],
backgroundColor: 'error.light',
border: '1px solid',
border: `${tokens.border.width.thin} solid`,
borderColor: 'error.main',
borderRadius: '4px',
borderRadius: tokens.border.radius.md, // Match button border radius
color: 'error.dark',
}}>
<Typography variant="h6" color="error">
Expand Down Expand Up @@ -987,8 +1009,8 @@ function App() {
<div
style={{
width: '40%',
borderLeft: '2px solid #e0e0e0',
backgroundColor: '#fafafa',
borderLeft: `${tokens.border.width.medium} solid ${tokens.color.neutral[200]}`,
backgroundColor: tokens.color.neutral[50],
}}>
<ErrorBoundary>
<DevTestbed isVisible={true} />
Expand Down
6 changes: 2 additions & 4 deletions formulus-formplayer/src/builtinExtensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -411,10 +411,8 @@ export async function getDynamicChoiceList(
* Get all built-in extension functions as a Map
* @returns Map of function name to function
*/
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
export function getBuiltinExtensions(): Map<string, Function> {
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
const functions = new Map<string, Function>();
export function getBuiltinExtensions(): Map<string, (...args: any[]) => any> {
const functions = new Map<string, (...args: any[]) => any>();
functions.set('getDynamicChoiceList', getDynamicChoiceList);
return functions;
}
31 changes: 16 additions & 15 deletions formulus-formplayer/src/components/DraftSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import {
Card,
CardContent,
CardActions,
Button,
IconButton,
Dialog,
DialogTitle,
Expand All @@ -23,9 +22,9 @@ import {
Grid,
Divider,
} from '@mui/material';
import { Button } from '@ode/components/react-web';
import {
Delete as DeleteIcon,
PlayArrow as ResumeIcon,
Schedule as ClockIcon,
Description as FormIcon,
} from '@mui/icons-material';
Expand Down Expand Up @@ -220,10 +219,9 @@ export const DraftSelector: React.FC<DraftSelectorProps> = ({

<CardActions sx={{ pt: 0 }}>
<Button
startIcon={<ResumeIcon />}
onClick={() => onResumeDraft(draft.id)}
variant="contained"
size="small">
variant="primary"
size="medium"
onPress={() => onResumeDraft(draft.id)}>
Resume Draft
</Button>
</CardActions>
Expand Down Expand Up @@ -251,10 +249,10 @@ export const DraftSelector: React.FC<DraftSelectorProps> = ({
Begin a new form without any saved data.
</Typography>
<Button
variant="outlined"
variant="secondary"
size="large"
onClick={onStartNew}
sx={{ minWidth: 200 }}>
onPress={onStartNew}
style={{ minWidth: 200 }}>
Start New Form
</Button>
</Box>
Expand All @@ -271,11 +269,10 @@ export const DraftSelector: React.FC<DraftSelectorProps> = ({
</Typography>
</DialogContent>
<DialogActions>
<Button onClick={() => setDeleteConfirmOpen(false)}>Cancel</Button>
<Button
onClick={confirmDeleteDraft}
color="error"
variant="contained">
<Button variant="neutral" onPress={() => setDeleteConfirmOpen(false)}>
Cancel
</Button>
<Button variant="danger" onPress={confirmDeleteDraft}>
Delete
</Button>
</DialogActions>
Expand Down Expand Up @@ -303,7 +300,11 @@ export const DraftSelector: React.FC<DraftSelectorProps> = ({
alignItems: 'center',
}}>
<Typography variant="h6">Select Draft</Typography>
{onClose && <Button onClick={onClose}>Close</Button>}
{onClose && (
<Button variant="secondary" onPress={onClose}>
Close
</Button>
)}
</Box>
</DialogTitle>
<DialogContent>{content}</DialogContent>
Expand Down
37 changes: 19 additions & 18 deletions formulus-formplayer/src/components/ErrorBoundary.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React, { Component, ReactNode } from 'react';
import { tokens } from '../theme/tokens-adapter';

interface Props {
children: ReactNode;
Expand Down Expand Up @@ -41,26 +42,26 @@ class ErrorBoundary extends Component<Props, State> {
return (
<div
style={{
padding: '20px',
margin: '20px',
border: '2px solid #ff6b6b',
borderRadius: '8px',
backgroundColor: '#fff5f5',
color: '#c92a2a',
padding: tokens.spacing?.[5] ?? '20px',
margin: tokens.spacing?.[5] ?? '20px',
border: `${tokens.border?.width?.medium ?? '2px'} solid ${tokens.color.semantic.error[500]}`,
borderRadius: tokens.border.radius.md,
backgroundColor: tokens.color.semantic.error[50],
color: tokens.color.semantic.error[600],
}}>
<h2>🚨 Something went wrong</h2>
<details style={{ marginTop: '10px' }}>
<details style={{ marginTop: tokens.spacing?.[3] ?? '12px' }}>
<summary style={{ cursor: 'pointer', fontWeight: 'bold' }}>
Error Details (click to expand)
</summary>
<pre
style={{
marginTop: '10px',
padding: '10px',
backgroundColor: '#f8f8f8',
border: '1px solid #ddd',
borderRadius: '4px',
fontSize: '12px',
marginTop: tokens.spacing?.[3] ?? '12px',
padding: tokens.spacing?.[3] ?? '12px',
backgroundColor: tokens.color.neutral[50],
border: `${tokens.border?.width?.thin ?? '1px'} solid ${tokens.color.neutral[300]}`,
borderRadius: tokens.border.radius.md,
fontSize: tokens.typography.fontSize.sm ?? '12px',
overflow: 'auto',
}}>
{this.state.error?.toString()}
Expand All @@ -70,12 +71,12 @@ class ErrorBoundary extends Component<Props, State> {
<button
onClick={() => this.setState({ hasError: false, error: undefined })}
style={{
marginTop: '15px',
padding: '8px 16px',
backgroundColor: '#ff6b6b',
color: 'white',
marginTop: tokens.spacing?.[4] ?? '16px',
padding: `${tokens.spacing?.[2] ?? '8px'} ${tokens.spacing?.[4] ?? '16px'}`,
backgroundColor: tokens.color.semantic.error[500],
color: tokens.color.neutral.white,
border: 'none',
borderRadius: '4px',
borderRadius: tokens.border.radius.md,
cursor: 'pointer',
}}>
Try Again
Expand Down
Loading
Loading