Skip to content

Commit 1d507ff

Browse files
axelbocmarcus-oscarsson
authored andcommitted
Validate beamline attributes and handle server errors
1 parent 64e8705 commit 1d507ff

File tree

3 files changed

+68
-40
lines changed

3 files changed

+68
-40
lines changed

ui/src/components/BeamlineAttribute/BeamlineAttribute.jsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,14 @@ import BeamlineAttributeForm from './BeamlineAttributeForm';
77

88
function BeamlineAttribute(props) {
99
const { attribute, format, precision = 1, suffix, onSave, onCancel } = props;
10-
const { state, value = 0, step = 0.1, msg, readonly = false } = attribute;
10+
const {
11+
state,
12+
value = 0,
13+
step = 0.1,
14+
limits,
15+
msg,
16+
readonly = false,
17+
} = attribute;
1118

1219
const isBusy = state === HW_STATE.BUSY;
1320

@@ -49,6 +56,7 @@ function BeamlineAttribute(props) {
4956
isBusy={isBusy}
5057
step={step}
5158
precision={precision}
59+
limits={limits}
5260
onSave={onSave}
5361
onCancel={onCancel}
5462
/>

ui/src/components/BeamlineAttribute/BeamlineAttribute.module.css

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,14 @@
2626
}
2727

2828
.input {
29-
width: 8em;
29+
width: 9em;
3030
height: 36px;
3131
appearance: auto !important;
3232
}
33+
34+
.error {
35+
color: var(--bs-form-invalid-color);
36+
line-height: 1;
37+
margin-top: 0.625rem;
38+
margin-bottom: 0;
39+
}

ui/src/components/BeamlineAttribute/BeamlineAttributeForm.jsx

Lines changed: 51 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,73 @@
1-
import { useEffect, useRef } from 'react';
1+
import { useEffect } from 'react';
22
import { Button, ButtonToolbar, Form } from 'react-bootstrap';
3+
import { useForm } from 'react-hook-form';
34
import { TiTick, TiTimes } from 'react-icons/ti';
45

56
import styles from './BeamlineAttribute.module.css';
67

78
function BeamlineAttributeForm(props) {
8-
const { value, isBusy, step, precision, onSave, onCancel } = props;
9-
const inputRef = useRef(null);
9+
const { value, isBusy, step, precision, limits, onSave, onCancel } = props;
10+
11+
const {
12+
register,
13+
setFocus,
14+
setError,
15+
handleSubmit: makeOnSubmit,
16+
formState: { isDirty, errors },
17+
} = useForm({ defaultValues: { value: value.toFixed(precision) } });
1018

1119
useEffect(() => {
1220
if (!isBusy) {
1321
setTimeout(() => {
1422
/* Focus and select text when popover opens and every time a value is applied.
1523
* Timeout ensures this works when opening a popover while another is already opened. */
16-
inputRef.current?.focus({ preventScroll: true });
17-
inputRef.current?.select();
24+
setFocus('value', { shouldSelect: true });
1825
}, 0);
1926
}
20-
}, [isBusy]);
21-
22-
function handleSubmit(evt) {
23-
evt.preventDefault();
24-
const formData = new FormData(evt.target);
25-
const strVal = formData.get('value');
27+
}, [isBusy, setFocus]);
2628

27-
const numVal =
28-
typeof strVal === 'string' ? Number.parseFloat(strVal) : Number.NaN;
29-
30-
if (!Number.isNaN(numVal)) {
31-
onSave(numVal);
29+
async function handleSubmit(data) {
30+
try {
31+
await onSave(data.value);
32+
} catch {
33+
setError('value', { message: 'Unable to set value' });
3234
}
3335
}
3436

37+
const minMaxMsg = `Allowed range: [${limits
38+
.map((v) => v.toFixed(precision))
39+
.join(', ')}]`;
40+
3541
return (
36-
<Form className="d-flex" noValidate onSubmit={handleSubmit}>
37-
<Form.Control
38-
ref={inputRef}
39-
className={styles.input}
40-
name="value"
41-
type="number"
42-
step={step}
43-
defaultValue={value.toFixed(precision)}
44-
disabled={isBusy}
45-
aria-label="Value"
46-
/>
47-
<ButtonToolbar className="ms-1">
48-
{isBusy ? (
49-
<Button variant="danger" size="sm" onClick={() => onCancel()}>
50-
<TiTimes size="1.5em" />
51-
</Button>
52-
) : (
53-
<Button type="submit" variant="success" size="sm">
54-
<TiTick size="1.5em" />
55-
</Button>
56-
)}
57-
</ButtonToolbar>
42+
<Form noValidate onSubmit={makeOnSubmit(handleSubmit)}>
43+
<div className="d-flex">
44+
<Form.Control
45+
{...register('value', {
46+
valueAsNumber: true,
47+
required: 'Please enter a valid number',
48+
min: { value: limits[0], message: minMaxMsg },
49+
max: { value: limits[1], message: minMaxMsg },
50+
disabled: isBusy,
51+
})}
52+
className={styles.input}
53+
type="number"
54+
step={step}
55+
aria-label="Value"
56+
isInvalid={isDirty && !!errors.value}
57+
/>
58+
<ButtonToolbar className="ms-1">
59+
{isBusy ? (
60+
<Button variant="danger" size="sm" onClick={() => onCancel()}>
61+
<TiTimes size="1.5em" />
62+
</Button>
63+
) : (
64+
<Button type="submit" variant="success" size="sm">
65+
<TiTick size="1.5em" />
66+
</Button>
67+
)}
68+
</ButtonToolbar>
69+
</div>
70+
{errors.value && <p className={styles.error}>{errors.value.message}</p>}
5871
</Form>
5972
);
6073
}

0 commit comments

Comments
 (0)