Skip to content

Commit 2cd9c40

Browse files
feat: record progress bar (#1171)
## JIRA Ticket [BSS-383](https://jira.csiro.au/browse/BSS-383) ## Description Add progress bar to top of record edit form to track completion of required fields. ## Proposed Changes - add progress bar component - update current form screens to include new component - pass state updater to form to update progress - create helper functions for measuring completion ## How to Test 1. Navigate to any survey 2. Click "+ NEW RECORD" button 3. Input values into required fields an check that progress bar reflects these changes 4. (optional) Publish and close record 5. (optional) Open published record and verify completion is retained ## Additional Information The survey form seems reasonably unreliable and might be worth considering a rebuild at some point. - the form re-renders around 11 times on load and another 5 times every few seconds (even without any changes) - in testing I was able to get into a broken state where I would need to kill the tab and reload to resolve - the code uses class components and is thousands of lines long across multiple files ## Checklist - [x] I have confirmed all commits have been signed. - [x] I have added JSDoc style comments to any new functions or classes. - [x] Relevant documentation such as READMEs, guides, and class comments are updated.
2 parents 3a4ff24 + 9311a5e commit 2cd9c40

File tree

7 files changed

+211
-153
lines changed

7 files changed

+211
-153
lines changed
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
interface ProgressBarProps {
2+
percentage: number;
3+
style?: React.CSSProperties;
4+
}
5+
6+
/**
7+
* ProgressBar component that visually represents the progress based on the percentage prop.
8+
*
9+
* @param {ProgressBarProps} props - The properties for the component.
10+
* @param {number} props.percentage - A value between 0 and 1 representing the completion percentage.
11+
* @param {React.CSSProperties} props.style - Additional styles for the progress bar.
12+
* @returns {JSX.Element} A visual representation of the progress in the form of a bar and percentage text.
13+
*/
14+
export default function ProgressBar({percentage, style}: ProgressBarProps) {
15+
const rounded = Math.round(percentage * 100);
16+
17+
return (
18+
<div
19+
style={{
20+
...style,
21+
display: 'flex',
22+
flexDirection: 'column',
23+
gap: '6px',
24+
}}
25+
>
26+
<div
27+
style={{
28+
backgroundColor: '#edeeeb',
29+
borderRadius: '6px',
30+
}}
31+
>
32+
<div
33+
style={{
34+
width: `${rounded}%`,
35+
height: '32px',
36+
backgroundColor: '#669911',
37+
borderRadius: '6px',
38+
}}
39+
></div>
40+
</div>
41+
<div style={{fontSize: '12px'}}>{rounded}% Completed</div>
42+
</div>
43+
);
44+
}

app/src/gui/components/record/RecordData.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ interface RecordDataTypes {
6262
handleUnlink: Function;
6363
setRevision_id?: Function;
6464
mq_above_md: boolean;
65+
setProgress: React.Dispatch<React.SetStateAction<number>>;
6566
buttonRef: React.RefObject<HTMLDivElement>;
6667
}
6768

@@ -148,6 +149,7 @@ export default function RecordData(props: RecordDataTypes) {
148149
draftLastSaved={props.draftLastSaved}
149150
mq_above_md={props.mq_above_md}
150151
navigate={navigate}
152+
setProgress={props.setProgress}
151153
buttonRef={props.buttonRef}
152154
/>
153155
) : (
@@ -206,6 +208,7 @@ export default function RecordData(props: RecordDataTypes) {
206208
handleSetDraftLastSaved={props.handleSetDraftLastSaved}
207209
handleSetDraftError={props.handleSetDraftError}
208210
navigate={navigate}
211+
setProgress={props.setProgress}
209212
buttonRef={props.buttonRef}
210213
/>
211214
</Box>

app/src/gui/components/record/form.tsx

Lines changed: 8 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ import {
6868

6969
import {generateFAIMSDataID, getFirstRecordHead} from '@faims3/data-model';
7070
import {INDIVIDUAL_NOTEBOOK_ROUTE} from '../../../constants/routes';
71+
import {percentComplete, requiredFields} from '../../../lib/form-utils';
7172
import {logError} from '../../../logging';
7273
import CircularLoading from '../ui/circular_loading';
7374
import FormButtonGroup from './formButton';
@@ -1114,16 +1115,6 @@ class RecordForm extends React.Component<
11141115
}
11151116

11161117
isReady(): boolean {
1117-
// if (DEBUG_APP) {
1118-
// if (!this.state.type_cached)
1119-
// console.debug('isReady false because type_cached is false');
1120-
// if (!this.state.initialValues)
1121-
// console.debug('isReady false because initialValues is false');
1122-
// if (!this.props.ui_specification)
1123-
// console.debug('isReady false because ui_specification is false');
1124-
// if (!this.state.view_cached)
1125-
// console.debug('isReady false because view_cached is false');
1126-
// }
11271118
return Boolean(
11281119
this.state.type_cached &&
11291120
this.state.initialValues &&
@@ -1145,39 +1136,6 @@ class RecordForm extends React.Component<
11451136
}
11461137

11471138
render() {
1148-
// we can't do this here because it changes state and forces a redraw
1149-
// if (this.state.draft_created !== null) {
1150-
// // If a draft was created, that implies this form started from
1151-
// // a non draft, so it must have been an existing record (see props
1152-
// // as it's got a type {existing record} | {draft already created}
1153-
// (this.context as any).dispatch({
1154-
// type: ActionType.ADD_CUSTOM_ALERT,
1155-
// payload: {
1156-
// severity: 'success',
1157-
// element: (
1158-
// <React.Fragment>
1159-
// <Link
1160-
// component={RouterLink}
1161-
// to={
1162-
// ROUTES.NOTEBOOK +
1163-
// this.props.project_id +
1164-
// ROUTES.RECORD_EXISTING +
1165-
// this.props.record_id! +
1166-
// ROUTES.REVISION +
1167-
// this.props.revision_id! +
1168-
// ROUTES.RECORD_DRAFT +
1169-
// this.state.draft_created
1170-
// }
1171-
// >
1172-
// Created new draft
1173-
// </Link>
1174-
// </React.Fragment>
1175-
// ),
1176-
// },
1177-
// });
1178-
// this.setState({draft_created: null});
1179-
// }
1180-
11811139
if (this.isReady()) {
11821140
const viewName = this.requireView();
11831141
const viewsetName = this.requireViewsetName();
@@ -1220,6 +1178,13 @@ class RecordForm extends React.Component<
12201178
}}
12211179
>
12221180
{formProps => {
1181+
this.props.setProgress?.(
1182+
percentComplete(
1183+
requiredFields(this.props.ui_specification.fields),
1184+
formProps.values
1185+
)
1186+
);
1187+
12231188
//ONLY update if the updated field is the controller field
12241189
fieldNames = getFieldsMatchingCondition(
12251190
this.props.ui_specification,

app/src/gui/pages/record-create.tsx

Lines changed: 101 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ import {ParentLinkProps} from '../components/record/relationships/types';
6363
import {getParentPersistenceData} from '../components/record/relationships/RelatedInformation';
6464
import InheritedDataComponent from '../components/record/inherited_data';
6565
import {NOTEBOOK_NAME_CAPITALIZED} from '../../buildconfig';
66+
import ProgressBar from '../components/progress-bar';
6667
import TransparentButton from '../components/buttons/transparent-button';
6768
import {scrollToDiv} from '../../lib/navigation';
6869
import ArrowDropDown from '@mui/icons-material/ArrowDropDown';
@@ -175,6 +176,8 @@ function DraftEdit(props: DraftEditProps) {
175176
const [parentLinks, setParentLinks] = useState([] as ParentLinkProps[]);
176177
const [is_link_ready, setIs_link_ready] = useState(false);
177178

179+
const [progress, setProgress] = useState(0);
180+
178181
const buttonRef = useRef<HTMLDivElement | null>(null);
179182

180183
useEffect(() => {
@@ -240,117 +243,113 @@ function DraftEdit(props: DraftEditProps) {
240243
});
241244
navigate(-1);
242245
return <React.Fragment />;
243-
} else if (uiSpec === null) {
244-
// Loading
245-
return <CircularProgress size={12} thickness={4} />;
246-
} else {
247-
// Loaded, variant picked, show form:
246+
}
248247

249-
return (
250-
<React.Fragment>
251-
<Box mb={2}>
252-
<Typography variant={'h2'} component={'h1'}>
253-
{uiSpec['viewsets'][type_name]['label'] ?? type_name} Record
254-
</Typography>
255-
<Typography variant={'subtitle1'} gutterBottom>
256-
Add a record for the{' '}
257-
{project_info !== null ? project_info.name : project_id} project.
258-
</Typography>
259-
<div
260-
style={{
261-
display: 'flex',
262-
justifyContent: 'flex-end',
263-
}}
264-
>
265-
<TransparentButton onClick={() => scrollToDiv(buttonRef)}>
266-
<ArrowDropDown />
267-
Jump to end
268-
</TransparentButton>
269-
</div>
270-
</Box>
271-
<Paper square>
272-
<TabContext value={value}>
273-
<AppBar position="static" color="primary">
274-
<TabList
275-
onChange={handleChange}
276-
aria-label="simple tabs example"
277-
indicatorColor={'secondary'}
278-
textColor="secondary"
279-
>
280-
<Tab label="Create" value="1" sx={{color: '#c2c2c2'}} />
281-
<Tab label="Meta" value="2" sx={{color: '#c2c2c2'}} />
282-
</TabList>
283-
</AppBar>
284-
<TabPanel value="1" sx={{p: 0}}>
285-
<Box>
286-
<UnpublishedWarning />
287-
<DraftSyncStatus
288-
last_saved={draftLastSaved}
289-
is_saving={isDraftSaving}
290-
error={draftError}
291-
/>
248+
if (uiSpec === null) return <CircularProgress size={12} thickness={4} />;
249+
250+
return (
251+
<React.Fragment>
252+
<Box mb={2}>
253+
<Typography variant={'h2'} component={'h1'}>
254+
{uiSpec['viewsets'][type_name]['label'] ?? type_name} Record
255+
</Typography>
256+
<Typography variant={'subtitle1'} gutterBottom>
257+
Add a record for the{' '}
258+
{project_info !== null ? project_info.name : project_id} project.
259+
</Typography>
260+
<div
261+
style={{
262+
display: 'flex',
263+
justifyContent: 'flex-end',
264+
}}
265+
>
266+
<TransparentButton onClick={() => scrollToDiv(buttonRef)}>
267+
<ArrowDropDown />
268+
Jump to end
269+
</TransparentButton>
270+
</div>
271+
</Box>
272+
<div style={{padding: '10px'}}>
273+
<ProgressBar percentage={progress} />
274+
</div>
275+
<div />
276+
<Paper square>
277+
<TabContext value={value}>
278+
<AppBar position="static" color="primary">
279+
<TabList
280+
onChange={handleChange}
281+
aria-label="simple tabs example"
282+
indicatorColor={'secondary'}
283+
textColor="secondary"
284+
>
285+
<Tab label="Create" value="1" sx={{color: '#c2c2c2'}} />
286+
<Tab label="Meta" value="2" sx={{color: '#c2c2c2'}} />
287+
</TabList>
288+
</AppBar>
289+
<TabPanel value="1" sx={{p: 0}}>
290+
<Box>
291+
<UnpublishedWarning />
292+
<DraftSyncStatus
293+
last_saved={draftLastSaved}
294+
is_saving={isDraftSaving}
295+
error={draftError}
296+
/>
297+
<Box sx={{backgroundColor: grey[100], p: {xs: 0, sm: 1, md: 2}}}>
292298
<Box
293-
sx={{backgroundColor: grey[100], p: {xs: 0, sm: 1, md: 2}}}
299+
component={Paper}
300+
elevation={0}
301+
p={{xs: 1, sm: 1, md: 2, lg: 2}}
302+
variant={is_mobile ? undefined : 'outlined'}
294303
>
295-
<Box
296-
component={Paper}
297-
elevation={0}
298-
p={{xs: 1, sm: 1, md: 2, lg: 2}}
299-
variant={is_mobile ? undefined : 'outlined'}
300-
>
301-
{is_link_ready ? (
302-
<InheritedDataComponent
303-
parentRecords={parentLinks}
304-
ui_specification={uiSpec}
305-
/>
306-
) : (
307-
<CircularProgress size={24} />
308-
)}
309-
<RecordForm
310-
project_id={project_id}
311-
record_id={record_id}
312-
type={type_name}
304+
{is_link_ready ? (
305+
<InheritedDataComponent
306+
parentRecords={parentLinks}
313307
ui_specification={uiSpec}
314-
draft_id={draft_id}
315-
handleSetIsDraftSaving={setIsDraftSaving}
316-
handleSetDraftLastSaved={setDraftLastSaved}
317-
handleSetDraftError={setDraftError}
318-
draftLastSaved={draftLastSaved}
319-
mq_above_md={mq_above_md}
320-
navigate={navigate}
321-
location={props.location}
322-
buttonRef={buttonRef}
323308
/>
324-
</Box>
309+
) : (
310+
<CircularProgress size={24} />
311+
)}
312+
<RecordForm
313+
project_id={project_id}
314+
record_id={record_id}
315+
type={type_name}
316+
ui_specification={uiSpec}
317+
draft_id={draft_id}
318+
handleSetIsDraftSaving={setIsDraftSaving}
319+
handleSetDraftLastSaved={setDraftLastSaved}
320+
handleSetDraftError={setDraftError}
321+
draftLastSaved={draftLastSaved}
322+
mq_above_md={mq_above_md}
323+
navigate={navigate}
324+
location={props.location}
325+
setProgress={setProgress}
326+
buttonRef={buttonRef}
327+
/>
325328
</Box>
326329
</Box>
327-
</TabPanel>
328-
<TabPanel value="2">
329-
<Box mt={2}>
330-
<Typography variant={'h5'} gutterBottom>
331-
Discard Draft
332-
</Typography>
333-
<RecordDelete
334-
project_id={project_id}
335-
record_id={record_id}
336-
revision_id={null}
337-
draft_id={draft_id}
338-
show_label={true}
339-
handleRefresh={handleRefresh}
340-
/>
341-
</Box>
342-
</TabPanel>
343-
</TabContext>
344-
</Paper>
345-
</React.Fragment>
346-
);
347-
}
330+
</Box>
331+
</TabPanel>
332+
<TabPanel value="2">
333+
<Box mt={2}>
334+
<Typography variant={'h5'} gutterBottom>
335+
Discard Draft
336+
</Typography>
337+
<RecordDelete
338+
project_id={project_id}
339+
record_id={record_id}
340+
revision_id={null}
341+
draft_id={draft_id}
342+
show_label={true}
343+
handleRefresh={handleRefresh}
344+
/>
345+
</Box>
346+
</TabPanel>
347+
</TabContext>
348+
</Paper>
349+
</React.Fragment>
350+
);
348351
}
349352

350-
// type RecordCreateProps = {
351-
// token?: null | undefined | TokenContents;
352-
// };
353-
354353
export default function RecordCreate() {
355354
const {project_id, type_name, draft_id, record_id} = useParams<{
356355
project_id: ProjectID;

0 commit comments

Comments
 (0)