diff --git a/app/src/gui/components/autoincrement/edit-form.test.tsx b/app/src/gui/components/autoincrement/edit-form.test.tsx
index 89ad9feab..6d1b18ecd 100644
--- a/app/src/gui/components/autoincrement/edit-form.test.tsx
+++ b/app/src/gui/components/autoincrement/edit-form.test.tsx
@@ -19,7 +19,7 @@
*/
import {fireEvent, render, screen, waitFor} from '@testing-library/react';
-import BasicAutoIncrementer from './edit-form';
+import {AutoIncrementEditForm} from './edit-form';
import {expect, describe, it} from 'vitest';
const props = {
@@ -27,11 +27,13 @@ const props = {
form_id: '',
field_id: '',
label: '',
+ open: true,
+ handleClose: () => {},
};
describe('Check edit-form component', () => {
it('Check add btn', async () => {
- render();
+ render();
const addRangeBtn = screen.getByTestId('addNewRangeBtn');
await waitFor(() => {
@@ -45,7 +47,7 @@ describe('Check edit-form component', () => {
});
});
it('Check remove btn', async () => {
- render();
+ render();
const addRangeBtn = screen.getByTestId('addNewRangeBtn');
await waitFor(() => {
@@ -67,7 +69,7 @@ describe('Check edit-form component', () => {
});
});
it('Check adding range start and stop fields', async () => {
- render();
+ render();
const addRangeBtn = screen.getByTestId('addNewRangeBtn');
await waitFor(() => {
diff --git a/app/src/gui/components/autoincrement/edit-form.tsx b/app/src/gui/components/autoincrement/edit-form.tsx
index be00976e1..db83ef1c6 100644
--- a/app/src/gui/components/autoincrement/edit-form.tsx
+++ b/app/src/gui/components/autoincrement/edit-form.tsx
@@ -15,31 +15,35 @@
*
* Filename: form.tsx
* Description:
- * TODO
+ * Defines a form for editing the auto-incrementer ranges for a field
*/
-import React from 'react';
-import {Formik, Form, Field, yupToFormErrors} from 'formik';
+import AddIcon from '@mui/icons-material/Add';
import {
- ButtonGroup,
- Box,
- Alert,
+ Badge,
Button,
- LinearProgress,
- Grid,
+ ButtonGroup,
+ Dialog,
+ DialogActions,
+ DialogContent,
+ DialogTitle,
Divider,
+ IconButton,
+ Stack,
+ TextField,
+ Typography,
} from '@mui/material';
-import AddIcon from '@mui/icons-material/Add';
-import {TextField} from 'formik-mui';
-import * as yup from 'yup';
+import {useContext, useState} from 'react';
+import {LocalAutoIncrementRange, ProjectID} from '@faims3/data-model';
+import CloseIcon from '@mui/icons-material/Close';
+import {useQuery, useQueryClient} from '@tanstack/react-query';
import {ActionType} from '../../../context/actions';
import {store} from '../../../context/store';
-import {ProjectID, LocalAutoIncrementRange} from '@faims3/data-model';
import {
+ createNewAutoincrementRange,
getLocalAutoincrementRangesForField,
setLocalAutoincrementRangesForField,
- createNewAutoincrementRange,
} from '../../../local-data/autoincrement';
interface Props {
@@ -47,109 +51,56 @@ interface Props {
form_id: string;
field_id: string;
label: string;
+ open: boolean;
+ handleClose: () => void;
}
-interface State {
- ranges: LocalAutoIncrementRange[] | null;
- ranges_initialised: boolean;
-}
-
-const FORM_SCHEMA = yup.object().shape({
- start: yup.number().required().positive().integer(),
- stop: yup.number().required().positive().integer(),
-});
-
-export default class BasicAutoIncrementer extends React.Component<
- Props,
- State
-> {
- constructor(props: Props) {
- super(props);
- this.state = {
- ranges: null,
- ranges_initialised: false,
- };
- }
+export const AutoIncrementEditForm = ({
+ project_id,
+ form_id,
+ field_id,
+ label,
+ open,
+ handleClose,
+}: Props) => {
+ const {dispatch} = useContext(store);
- async ensure_ranges() {
- if (this.state.ranges === null) {
+ // useQuery to get the current ranges for the field,
+ // we will invalidate the query when we update the ranges
+ // so that they get re-fetched
+ const queryClient = useQueryClient();
+ const queryKey = ['autoincrement', project_id, form_id, field_id];
+ const {data: ranges} = useQuery({
+ queryKey: queryKey,
+ queryFn: async () => {
const ranges = await getLocalAutoincrementRangesForField(
- this.props.project_id,
- this.props.form_id,
- this.props.field_id
+ project_id,
+ form_id,
+ field_id
);
- this.setState({ranges: ranges});
- }
- }
-
- async add_new_range() {
- await this.ensure_ranges();
- const ranges = [
- ...((this.state.ranges || []) as LocalAutoIncrementRange[]),
- ];
- ranges.push(createNewAutoincrementRange(0, 0));
- this.setState({ranges: ranges});
- }
-
- cloned_ranges(): LocalAutoIncrementRange[] | null {
- if (this.state.ranges === null) {
- return null;
- }
- // react requires us to make deep copied if we want to modify state...
- const ranges = [] as LocalAutoIncrementRange[];
- for (const old_range of this.state.ranges) {
- // This assumes we don't need to go another level down to deep clone
- const new_range = {...old_range};
- ranges.push(new_range);
- }
- return ranges;
- }
+ return ranges;
+ },
+ initialData: [],
+ enabled: true,
+ });
- render_ranges() {
- const ranges = this.cloned_ranges();
- if (ranges === null || ranges.length === 0) {
- return (
-
- No ranges allocated yet.
-
- );
- }
- return (
-
- {ranges.map((range, range_index) => {
- return this.render_range(range, range_index, ranges);
- })}
-
- );
- }
+ const addNewRange = async () => {
+ const updatedRanges = [...(ranges || [])];
+ updatedRanges.push(createNewAutoincrementRange(0, 0));
+ updateRanges(updatedRanges);
+ };
- async update_ranges(ranges: LocalAutoIncrementRange[]) {
- const {project_id, form_id, field_id} = this.props;
+ const updateRanges = async (newRanges: LocalAutoIncrementRange[]) => {
try {
await setLocalAutoincrementRangesForField(
project_id,
form_id,
field_id,
- ranges
- ).then(() => {
- (this.context as any).dispatch({
- type: ActionType.ADD_ALERT,
- payload: {
- message: 'Range successfully updated',
- severity: 'success',
- },
- });
- });
- let ranges_initialised = false;
- for (const range of ranges) {
- if (range.using || range.fully_used) {
- ranges_initialised = true;
- break;
- }
- }
- this.setState({ranges: ranges, ranges_initialised: ranges_initialised});
+ newRanges
+ );
+ queryClient.invalidateQueries({queryKey: queryKey});
} catch (err: any) {
- (this.context as any).dispatch({
+ dispatch({
type: ActionType.ADD_ALERT,
payload: {
message: err.toString(),
@@ -157,159 +108,199 @@ export default class BasicAutoIncrementer extends React.Component<
},
});
}
- }
+ };
- render_range(
- range: LocalAutoIncrementRange,
- range_index: number,
- ranges: LocalAutoIncrementRange[]
- ) {
- const range_count = ranges.length;
- const start_props = {
- id: 'start',
- label: 'start',
- name: 'start',
- required: true,
- type: 'number',
- readOnly: range.using || range.fully_used,
- disabled: range.using || range.fully_used,
- };
- const stop_props = {
- id: 'stop',
- label: 'stop',
- name: 'stop',
- required: true,
- type: 'number',
- readOnly: range.fully_used,
- disabled: range.fully_used,
+ const updateRange = (index: number) => {
+ return (range: LocalAutoIncrementRange) => {
+ const rangesCopy = [...ranges];
+ rangesCopy[index] = range;
+ updateRanges(rangesCopy);
};
+ };
- return (
- {
- return FORM_SCHEMA.validate(values)
- .then(v => {
- if (!(v.stop > v.start)) {
- return {stop: 'Must be greater than start'};
- }
- return {};
- })
- .catch(err => {
- return yupToFormErrors(err);
- });
- }}
- onSubmit={async (values, {setSubmitting}) => {
- range.start = values.start;
- range.stop = values.stop;
- await this.update_ranges(ranges).then(() => {
- setSubmitting(false);
- });
+ const handleRemoveRange = (index: number) => {
+ const newRanges = ranges?.filter((_, i) => i !== index);
+ if (newRanges !== undefined) {
+ updateRanges(newRanges);
+ } else {
+ updateRanges([]);
+ }
+ };
+
+ return (
+
+ );
+};
-
-
- {range.using ? (
-
- ) : (
-
- )}
-
-
-
-
- {isSubmitting && }
-
-
- )}
-
- );
- }
+ const handleStartChange = (event: any) => {
+ const newStart = parseInt(event.target.value);
+ if (newStart >= 0) {
+ setStart(newStart);
+ if (newStart >= props.range.stop) {
+ // initialise a range of 100 if they enter a start > stop
+ setStop(newStart + 100);
+ props.updateRange({
+ ...props.range,
+ start: newStart,
+ stop: newStart + 100,
+ });
+ } else {
+ props.updateRange({
+ ...props.range,
+ start: newStart,
+ });
+ }
+ }
+ };
- async componentDidMount() {
- await this.ensure_ranges();
- }
+ const handleStopChange = (event: any) => {
+ const newStop = parseInt(event.target.value);
+ if (newStop > props.range.start) {
+ setStop(newStop);
+ props.updateRange({
+ ...props.range,
+ stop: newStop,
+ });
+ }
+ };
- async componentDidUpdate() {
- await this.ensure_ranges();
- }
+ const handleDisableRange = () => {
+ props.updateRange({
+ ...props.range,
+ fully_used: true,
+ });
+ };
- render() {
- return (
-
- {this.render_ranges()}
-
-
- );
- }
-}
-BasicAutoIncrementer.contextType = store;
+ {props.range.using && (
+
+ )}
+
+ {!(props.range.using || props.range.fully_used) && (
+
+ )}
+ {props.range.fully_used && (
+
+ )}
+
+
+
+ );
+};
diff --git a/app/src/gui/components/notebook/settings/auto_incrementers.tsx b/app/src/gui/components/notebook/settings/auto_incrementers.tsx
index 797a3aaf5..d35b87b88 100644
--- a/app/src/gui/components/notebook/settings/auto_incrementers.tsx
+++ b/app/src/gui/components/notebook/settings/auto_incrementers.tsx
@@ -1,9 +1,9 @@
-import React, {useEffect} from 'react';
-import {Box, Grid, Typography, Paper, Alert} from '@mui/material';
+import React, {useEffect, useState} from 'react';
+import {Box, Grid, Typography, Paper, Button} from '@mui/material';
import {ProjectInformation, ProjectUIModel} from '@faims3/data-model';
import {getAutoincrementReferencesForProject} from '../../../../local-data/autoincrement';
import {AutoIncrementReference} from '@faims3/data-model';
-import AutoIncrementEditForm from '../../autoincrement/edit-form';
+import {AutoIncrementEditForm} from '../../autoincrement/edit-form';
import {logError} from '../../../../logging';
interface AutoIncrementerSettingsListProps {
@@ -11,21 +11,11 @@ interface AutoIncrementerSettingsListProps {
uiSpec: ProjectUIModel;
}
-function get_form(section_id: string, uiSpec: ProjectUIModel) {
- let form = '';
- uiSpec.visible_types.map(viewset =>
- uiSpec.viewsets[viewset].views.includes(section_id)
- ? (form = uiSpec.viewsets[viewset].label ?? viewset)
- : viewset
- );
- return form;
-}
export default function AutoIncrementerSettingsList(
props: AutoIncrementerSettingsListProps
) {
- const [references, setReferences] = React.useState(
- [] as AutoIncrementReference[]
- );
+ const [open, setOpen] = useState(false);
+ const [references, setReferences] = useState([] as AutoIncrementReference[]);
useEffect(() => {
getAutoincrementReferencesForProject(props.project_info.project_id)
.then(refs => {
@@ -35,62 +25,57 @@ export default function AutoIncrementerSettingsList(
}, [props.project_info.project_id]);
return (
-
+ <>
{references.length === 0 ? (
-
-
-
- Edit Allocations
-
-
- This project has no Auto-Incrementers
-
-
-
+ <>>
) : (
- ''
+
+
+ Edit auto-incrementers
+
+
+ View and modify the range settings for auto-incrementers. These are
+ used to generate unique identifiers for records within a defined
+ range.
+
+
+ {references.map(ai => {
+ const label = ai.label ?? '';
+ return (
+
+
+ setOpen(false)}
+ />
+
+
+
+ );
+ })}
+
)}
- {references.map(ai => {
- // display form section label for user to fill Auto correctly
- const section =
- props.uiSpec['views'][ai.form_id] !== undefined
- ? (props.uiSpec['views'][ai.form_id]['label'] ?? ai.form_id)
- : ai.form_id;
- const label =
- get_form(ai.form_id, props.uiSpec) +
- ' <' +
- section +
- '> ' +
- (ai.label ?? '');
- return (
-
-
-
- Edit Allocations for {label}
-
-
- The allocated range will be ≥ the start value and < the
- stop value. e.g., a range allocation of start:1, stop:5 will
- generate hrids in the range (1,2,3,4). There must always be a
- least one range to ensure that new IDs can be generated.
-
-
-
-
- );
- })}
-
+ >
);
}
diff --git a/app/src/gui/fields/BasicAutoIncrementer.tsx b/app/src/gui/fields/BasicAutoIncrementer.tsx
index 8cc0da06d..5c5724d14 100644
--- a/app/src/gui/fields/BasicAutoIncrementer.tsx
+++ b/app/src/gui/fields/BasicAutoIncrementer.tsx
@@ -15,29 +15,20 @@
*
* Filename: BasicAutoIncrementer.tsx
* Description:
- * TODO
+ * Implements an auto-incrementer field that is hidden but provides
+ * a value that can be used in templated string fields.
*/
-import React from 'react';
import Input from '@mui/material/Input';
import {FieldProps} from 'formik';
-import {ActionType} from '../../context/actions';
-import {store} from '../../context/store';
+import {useEffect, useState} from 'react';
import {
getLocalAutoincrementStateForField,
setLocalAutoincrementStateForField,
} from '../../local-data/autoincrement';
-import {
- Grid,
- Button,
- Dialog,
- DialogTitle,
- DialogContent,
- DialogContentText,
- DialogActions,
-} from '@mui/material';
-import {NOTEBOOK_NAME, NOTEBOOK_NAME_CAPITALIZED} from '../../buildconfig';
+import {AutoIncrementEditForm} from '../components/autoincrement/edit-form';
+
interface Props {
num_digits: number;
// This could be dropped depending on how multi-stage forms are configured
@@ -45,92 +36,25 @@ interface Props {
label?: string;
}
-interface State {
- has_run: boolean;
- is_ranger: boolean;
- label: string;
-}
-
-function AddRangeDialog() {
- const [open, setOpen] = React.useState(false);
- return (
-
-
-
-
- );
-}
-
-export class BasicAutoIncrementer extends React.Component<
- FieldProps & Props,
- State
-> {
- constructor(props: FieldProps & Props) {
- super(props);
- const label =
- this.props.label !== '' && this.props.label !== undefined
- ? this.props.label
- : this.props.form_id;
- this.state = {
- has_run: false,
- is_ranger: true,
- label: label ?? this.props.form_id,
- };
- }
+export const BasicAutoIncrementer = (props: FieldProps & Props) => {
+ const [showAutoIncrementEditForm, setShowAutoIncrementEditForm] =
+ useState(false);
- async get_id(): Promise {
- const project_id = this.props.form.values['_project_id'];
- const form_id = this.props.form_id;
- const field_id = this.props.field.name;
+ const get_id = async (): Promise => {
+ const project_id = props.form.values['_project_id'];
+ const form_id = props.form_id;
+ const field_id = props.field.name;
const local_state = await getLocalAutoincrementStateForField(
project_id,
form_id,
field_id
);
- console.debug(
- `local_auto_inc for ${project_id} ${form_id} ${field_id} is`,
- local_state
- );
if (local_state.last_used_id === null && local_state.ranges.length === 0) {
- // We have no range allocations, block
- // TODO: add link to range allocation
- (this.context as any).dispatch({
- type: ActionType.ADD_ALERT,
- payload: {
- message: `No ranges exist for this ${NOTEBOOK_NAME} yet. Go to the ${NOTEBOOK_NAME} Settings tab to add/edit ranges.`,
- severity: 'error',
- },
- });
- this.setState({...this.state, is_ranger: false});
+ setShowAutoIncrementEditForm(true);
return null;
}
+
if (local_state.last_used_id === null) {
- console.debug('local_auto_inc starting with clean slate');
// We've got a clean slate with ranges allocated, start allocating ids
const new_id = local_state.ranges[0].start;
local_state.ranges[0].using = true;
@@ -138,12 +62,12 @@ export class BasicAutoIncrementer extends React.Component<
await setLocalAutoincrementStateForField(local_state);
return new_id;
}
+
// We're now using the allocated ranges, find where we've up to:
// If we're using a range, find it
for (const range of local_state.ranges) {
if (range.using) {
if (local_state.last_used_id + 1 < range.stop) {
- console.debug('local_auto_inc using existing range', range);
const next_id = local_state.last_used_id + 1;
local_state.last_used_id = next_id;
await setLocalAutoincrementStateForField(local_state);
@@ -151,100 +75,82 @@ export class BasicAutoIncrementer extends React.Component<
}
range.fully_used = true;
range.using = false;
- console.debug('local_auto_inc finished with range', range);
}
}
+
// find a new range to use
for (const range of local_state.ranges) {
if (!range.fully_used) {
const next_id = range.start;
range.using = true;
local_state.last_used_id = next_id;
- console.debug('local_auto_inc staring with range', range);
await setLocalAutoincrementStateForField(local_state);
return next_id;
}
}
- // we've got no new ranges to use, either we block, or use the highest range
- // as a starting point
- // TODO: Add blocking logic
- let max_stop = local_state.last_used_id;
- for (const range of local_state.ranges) {
- if (range.stop > max_stop) {
- max_stop = range.stop;
- }
- }
- if (max_stop === local_state.last_used_id) {
- max_stop = max_stop + 1;
- }
- local_state.last_used_id = max_stop;
- await setLocalAutoincrementStateForField(local_state);
- console.debug('local_auto_inc using overrun', local_state);
- return max_stop;
- }
+ // we've got no new ranges to use, ask the user to allocate more
+
+ setShowAutoIncrementEditForm(true);
+ return null;
+ };
- async compute_id(num_digits: number): Promise {
- const new_id = await this.get_id();
+ const compute_id = async (
+ num_digits: number
+ ): Promise => {
+ const new_id = await get_id();
if (new_id === null || new_id === undefined) {
return undefined;
}
return new_id.toString().padStart(num_digits, '0');
- }
-
- async update_form() {
- const current_value = this.props.form.values[this.props.field.name];
+ };
- if (!this.state.has_run) {
- this.setState({has_run: true});
- console.debug('running autoinc');
- if (current_value === null) {
- const new_id = await this.compute_id(this.props.num_digits || 4);
- if (new_id === undefined) {
- (this.context as any).dispatch({
- type: ActionType.ADD_ALERT,
- payload: {
- message: 'Failed to get autoincremented ID',
- severity: 'error',
- },
- });
- } else {
- this.props.form.setFieldValue(this.props.field.name, new_id, true);
- if (this.props.form.errors[this.props.field.name] !== undefined)
- this.props.form.setFieldError(this.props.field.name, undefined);
- }
+ const update_form = async () => {
+ const current_value = props.form.values[props.field.name];
+ // we'll set the value in a form if the value has not already been set
+ // assume it has been set if the value is not null, empty or undefined
+ if (
+ current_value === null ||
+ current_value === '' ||
+ current_value === undefined
+ ) {
+ const new_id = await compute_id(props.num_digits || 4);
+ if (new_id === undefined) {
+ setShowAutoIncrementEditForm(true);
} else {
- if (this.props.form.errors[this.props.field.name] !== undefined)
- this.props.form.setFieldError(this.props.field.name, undefined);
+ props.form.setFieldValue(props.field.name, new_id, true);
}
}
- }
-
- async componentDidMount() {
- console.debug('did mount', this.props.form.values[this.props.field.name]);
- await this.update_form();
- }
- // remove the update for form, should only be update once
- // async componentDidUpdate() {
- // console.debug('did update',this.props.form.values[this.props.field.name])
- // await this.update_form();
- // }
+ // reset any errors, we don't want to report these to the user
+ if (props.form.errors[props.field.name] !== undefined)
+ props.form.setFieldError(props.field.name, undefined);
+ };
- render() {
- return (
- <>
-
- {this.state.is_ranger === false && }
- >
- );
- }
-}
+ useEffect(() => {
+ update_form();
+ }, [props.form.values[props.field.name]]);
-BasicAutoIncrementer.contextType = store;
+ return (
+ <>
+
+ {
+ await update_form();
+ setShowAutoIncrementEditForm(false);
+ }}
+ />
+ >
+ );
+};
// const uiSpec = {
// 'component-namespace': 'faims-custom', // this says what web component to use to render/acquire value from
diff --git a/designer/src/state/migrateNotebook.test.ts b/designer/src/state/migrateNotebook.test.ts
index e92d41dc0..5b064abad 100644
--- a/designer/src/state/migrateNotebook.test.ts
+++ b/designer/src/state/migrateNotebook.test.ts
@@ -97,6 +97,13 @@ describe('Migrate Notebook Tests', () => {
}
});
+ test('fix auto incrementer initial value', () => {
+ const migrated = migrateNotebook(sampleNotebook);
+ const fields = migrated['ui-specification'].fields;
+ const targetField = fields['Field-ID'];
+ expect(targetField.initialValue).toBe('');
+ });
+
test('update form descriptions', () => {
const migrated = migrateNotebook(sampleNotebook);
const fviews = migrated['ui-specification'].fviews;
diff --git a/designer/src/state/migrateNotebook.ts b/designer/src/state/migrateNotebook.ts
index 24a1c53fd..f588cbb62 100644
--- a/designer/src/state/migrateNotebook.ts
+++ b/designer/src/state/migrateNotebook.ts
@@ -44,6 +44,9 @@ export const migrateNotebook = (notebook: unknown) => {
// fix validation for photo fields which had a bad default
fixPhotoValidation(notebookCopy);
+ // fix bad autoincrementer initial value
+ fixAutoIncrementerInitialValue(notebookCopy);
+
return notebookCopy;
};
@@ -240,3 +243,26 @@ const fixPhotoValidation = (notebook: Notebook) => {
notebook['ui-specification'].fields = fields;
};
+
+/**
+ * In some old notebooks, the initialValue of an auto incrementer was null
+ * which conflicts with the validate schema and triggers an error
+ * message on load in some cases. Here we replace that with the empty string.
+ *
+ * @param notebook A notebook that might be out of date, modified
+ */
+const fixAutoIncrementerInitialValue = (notebook: Notebook) => {
+ const fields: {[key: string]: FieldType} = {};
+
+ for (const fieldName in notebook['ui-specification'].fields) {
+ const field = notebook['ui-specification'].fields[fieldName];
+
+ if (field['component-name'] === 'BasicAutoIncrementer') {
+ if (field.initialValue === null) field.initialValue = '';
+ }
+
+ fields[fieldName] = field;
+ }
+
+ notebook['ui-specification'].fields = fields;
+};