• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

wger-project / react / 14262852442

03 Apr 2025 01:43PM UTC coverage: 76.622% (+0.03%) from 76.59%
14262852442

push

github

rolandgeider
Some polishing

- Proper translated labels for forms
- Show any server side validation errors
- Consolidate calls to processBaseConfigs (this makes it easier to show the server
  side messages)

1287 of 1879 branches covered (68.49%)

Branch coverage included in aggregate %.

44 of 59 new or added lines in 5 files covered. (74.58%)

1 existing line in 1 file now uncovered.

5196 of 6582 relevant lines covered (78.94%)

25.5 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

76.43
/src/components/WorkoutRoutines/widgets/forms/BaseConfigForm.tsx
1
import { CheckBoxOutlineBlank } from "@mui/icons-material";
6✔
2
import ArrowDropUpIcon from '@mui/icons-material/ArrowDropUp';
6✔
3
import CheckBoxIcon from '@mui/icons-material/CheckBox';
6✔
4

5

6
import SettingsIcon from '@mui/icons-material/Settings';
6✔
7
import { Button, Divider, IconButton, ListItemIcon, ListItemText, Menu, MenuItem, TextField } from "@mui/material";
6✔
8
import { LoadingProgressIcon } from "components/Core/LoadingWidget/LoadingWidget";
6✔
9
import {
6✔
10
    BaseConfig,
11
    OPERATION_REPLACE,
12
    REQUIREMENTS_VALUES,
13
    RequirementsType,
14
    RIR_VALUES_SELECT
15
} from "components/WorkoutRoutines/models/BaseConfig";
16
import {
6✔
17
    useAddMaxRepsConfigQuery,
18
    useAddMaxRestConfigQuery,
19
    useAddMaxRiRConfigQuery,
20
    useAddMaxWeightConfigQuery,
21
    useAddNrOfSetsConfigQuery,
22
    useAddRepsConfigQuery,
23
    useAddRestConfigQuery,
24
    useAddRiRConfigQuery,
25
    useAddWeightConfigQuery,
26
    useDeleteMaxRepsConfigQuery,
27
    useDeleteMaxRestConfigQuery,
28
    useDeleteMaxRiRConfigQuery,
29
    useDeleteMaxWeightConfigQuery,
30
    useDeleteNrOfSetsConfigQuery,
31
    useDeleteRepsConfigQuery,
32
    useDeleteRestConfigQuery,
33
    useDeleteRiRConfigQuery,
34
    useDeleteWeightConfigQuery,
35
    useEditMaxRepsConfigQuery,
36
    useEditMaxRestConfigQuery,
37
    useEditMaxRiRConfigQuery,
38
    useEditMaxWeightConfigQuery,
39
    useEditNrOfSetsConfigQuery,
40
    useEditRepsConfigQuery,
41
    useEditRestConfigQuery,
42
    useEditRiRConfigQuery,
43
    useEditWeightConfigQuery
44
} from "components/WorkoutRoutines/queries";
45
import {
6✔
46
    useAddMaxNrOfSetsConfigQuery,
47
    useEditMaxNrOfSetsConfigQuery
48
} from "components/WorkoutRoutines/queries/configs";
49
import { useFormikContext } from "formik";
6✔
50
import React, { useState } from "react";
6✔
51
import { useTranslation } from "react-i18next";
6✔
52
import { DEBOUNCE_ROUTINE_FORMS } from "utils/consts";
6✔
53
import { errorsToString } from "utils/forms";
6✔
54

55
export const QUERY_MAP: { [key: string]: any } = {
6✔
56
    'weight': {
57
        edit: useEditWeightConfigQuery,
58
        add: useAddWeightConfigQuery,
59
        delete: useDeleteWeightConfigQuery
60
    },
61
    'max-weight': {
62
        edit: useEditMaxWeightConfigQuery,
63
        add: useAddMaxWeightConfigQuery,
64
        delete: useDeleteMaxWeightConfigQuery
65
    },
66
    'reps': {
67
        edit: useEditRepsConfigQuery,
68
        add: useAddRepsConfigQuery,
69
        delete: useDeleteRepsConfigQuery
70
    },
71
    'max-reps': {
72
        edit: useEditMaxRepsConfigQuery,
73
        add: useAddMaxRepsConfigQuery,
74
        delete: useDeleteMaxRepsConfigQuery
75
    },
76
    'sets': {
77
        edit: useEditNrOfSetsConfigQuery,
78
        add: useAddNrOfSetsConfigQuery,
79
        delete: useDeleteNrOfSetsConfigQuery
80
    },
81
    'max-sets': {
82
        edit: useEditMaxNrOfSetsConfigQuery,
83
        add: useAddMaxNrOfSetsConfigQuery,
84
        delete: useAddMaxRepsConfigQuery
85
    },
86
    'rest': {
87
        edit: useEditRestConfigQuery,
88
        add: useAddRestConfigQuery,
89
        delete: useDeleteRestConfigQuery
90
    },
91
    'max-rest': {
92
        edit: useEditMaxRestConfigQuery,
93
        add: useAddMaxRestConfigQuery,
94
        delete: useDeleteMaxRestConfigQuery
95
    },
96
    'rir': {
97
        edit: useEditRiRConfigQuery,
98
        add: useAddRiRConfigQuery,
99
        delete: useDeleteRiRConfigQuery
100
    },
101
    'max-rir': {
102
        edit: useEditMaxRiRConfigQuery,
103
        add: useAddMaxRiRConfigQuery,
104
        delete: useDeleteMaxRiRConfigQuery
105
    },
106
};
107

108

109
export type ConfigType =
110
    'weight'
111
    | 'max-weight'
112
    | 'reps'
113
    | 'max-reps'
114
    | 'sets'
115
    | 'max-sets'
116
    | 'rest'
117
    | 'max-rest'
118
    | 'rir'
119
    | 'max-rir';
120

121
export const SlotBaseConfigValueField = (props: {
6✔
122
    config?: BaseConfig,
123
    routineId: number,
124
    slotEntryId?: number,
125
    type: ConfigType,
126
}) => {
127
    const { t } = useTranslation();
107✔
128
    const { edit: editQuery, add: addQuery, delete: deleteQuery } = QUERY_MAP[props.type];
107✔
129
    const editQueryHook = editQuery(props.routineId);
107✔
130
    const addQueryHook = addQuery(props.routineId);
107✔
131
    const deleteQueryHook = deleteQuery(props.routineId);
107✔
132

133
    const [value, setValue] = useState(props.config?.value || '');
107✔
134
    const [manualValidationError, setManualValidationError] = useState<string | null>(null);
107✔
135
    const [timer, setTimer] = useState<NodeJS.Timeout | null>(null);
107✔
136

137
    const isInt = ['sets', 'max-sets', 'rest', 'max-rest'].includes(props.type);
107✔
138
    const parseFunction = isInt ? parseInt : parseFloat;
107✔
139

140
    let title = '';
107✔
141
    switch (props.type) {
107!
142
        case "weight":
143
            title = t('weight');
19✔
144
            break;
19✔
145
        case "max-weight":
146
            title = 'max ' + t('weight');
10✔
147
            break;
10✔
148
        case "reps":
149
            title = t('server.repetitions');
19✔
150
            break;
19✔
151
        case "max-reps":
152
            title = t('server.max_reps');
10✔
153
            break;
10✔
154
        case "sets":
155
            title = t('routines.sets');
19✔
156
            break;
19✔
157
        case "max-sets":
158
            title = 'max ' + t('routines.sets');
1✔
159
            break;
1✔
160
        case "rest":
161
            title = t('routines.restTime');
10✔
162
            break;
10✔
163
        case "max-rest":
164
            title = 'max ' + t('routines.restTime');
10✔
165
            break;
10✔
166
        case "rir":
167
            title = t('routines.rir');
9✔
168
            break;
9✔
169
        case "max-rir":
NEW
170
            title = 'max ' + t('routines.rir');
×
NEW
171
            break;
×
172
    }
173

174
    const handleData = (value: string) => {
107✔
175

176
        const data = {
24✔
177
            // eslint-disable-next-line camelcase
178
            slot_entry: props.slotEntryId,
179
            value: parseFunction(value),
180
        };
181

182
        if (value === '') {
24✔
183
            props.config && deleteQueryHook.mutate(props.config.id);
8✔
184
        } else if (props.config) {
16✔
185
            editQueryHook.mutate({ id: props.config.id, ...data });
8✔
186
        } else {
187
            addQueryHook.mutate({
8✔
188
                iteration: 1,
189
                operation: OPERATION_REPLACE,
190
                step: 'abs',
191
                requirements: null,
192
                ...data
193
            });
194
        }
195
    };
196

197
    const onChange = (text: string) => {
107✔
198
        setValue(text);
24✔
199

200
        if (text !== '' && Number.isNaN(parseFunction(text))) {
24!
NEW
201
            setManualValidationError(t(isInt ? 'forms.enterInteger' : 'forms.enterNumber'));
×
202
            return;
×
203
        } else {
204
            setManualValidationError(null);
24✔
205
        }
206

207
        if (timer) {
24!
UNCOV
208
            clearTimeout(timer);
×
209
        }
210
        setTimer(setTimeout(() => handleData(text), DEBOUNCE_ROUTINE_FORMS));
24✔
211
    };
212

213
    const isPending = editQueryHook.isPending || addQueryHook.isPending || deleteQueryHook.isPending;
107✔
214
    const isError = editQueryHook.isError || addQueryHook.isError || deleteQueryHook.isError;
107✔
215
    const errorMessage = errorsToString(editQueryHook.error?.response?.data) || errorsToString(addQueryHook.error?.response?.data) || errorsToString(deleteQueryHook.error?.response?.data);
107✔
216

217
    return (
107✔
218
        <TextField
219
            slotProps={{
220
                input: { endAdornment: isPending && <LoadingProgressIcon /> }
107!
221
            }}
222
            inputProps={{
223
                "data-testid": `${props.type}-field`,
224
            }}
225
            label={title}
226
            value={value}
227
            fullWidth
228
            variant="standard"
229
            disabled={isPending}
230
            onChange={e => onChange(e.target.value)}
24✔
231
            error={!!manualValidationError || isError}
214✔
232
            helperText={manualValidationError || errorMessage}
214✔
233
        />
234
    );
235
};
236

237

238
export const ConfigDetailsRequirementsField = (props: {
6✔
239
    fieldName: string,
240
    values: RequirementsType[],
241
    disabled?: boolean
242
}) => {
243

244
    const { setFieldValue } = useFormikContext();
50✔
245
    const { t } = useTranslation();
50✔
246
    const disable = props.disabled ?? false;
50!
247

248
    const [selectedElements, setSelectedElements] = useState<RequirementsType[]>(props.values);
50✔
249
    const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
50✔
250

251
    const handleSelection = (value: RequirementsType) => {
50✔
252
        // if the value is not in selectedElements, add it
253
        if (!selectedElements.includes(value)) {
×
254
            setSelectedElements([...selectedElements, value]);
×
255
        } else {
256
            setSelectedElements(selectedElements.filter((e) => e !== value));
×
257
        }
258
    };
259

260
    const handleSubmit = async () => {
50✔
261
        await setFieldValue(props.fieldName, selectedElements);
×
262
        setAnchorEl(null);
×
263
    };
264

265

266
    return <>
50✔
267
        <IconButton
268
            disabled={disable}
269
            onClick={(event) => setAnchorEl(event.currentTarget)}
×
270
        >
271
            {Boolean(anchorEl) ? <ArrowDropUpIcon fontSize="small" /> : <SettingsIcon fontSize="small" />}
50!
272
        </IconButton>
273
        <Menu
274
            anchorEl={anchorEl}
275
            open={Boolean(anchorEl)}
276
            onClose={() => setAnchorEl(null)}
×
277
        >
278
            {...REQUIREMENTS_VALUES.map((e, index) => <MenuItem
200✔
279
                onClick={() => handleSelection(e as unknown as RequirementsType)}>
×
280
                <ListItemIcon>
281
                    {selectedElements.includes(e as unknown as RequirementsType)
200!
282
                        ? <CheckBoxIcon fontSize="small" />
283
                        : <CheckBoxOutlineBlank fontSize="small" />
284
                    }
285
                </ListItemIcon>
286
                <ListItemText>
287
                    {e}
288
                </ListItemText>
289
            </MenuItem>)}
290
            <Divider />
291
            <MenuItem>
292

293
                <Button color="primary" variant="contained" type="submit" size="small" onClick={handleSubmit}>
294
                    {t('save')}
295
                </Button>
296
            </MenuItem>
297
        </Menu></>;
298
};
299

300

301
export const ConfigDetailsRiRField = (props: { config?: BaseConfig, slotEntryId?: number, routineId: number }) => {
6✔
302

303
    const editRiRQuery = useEditRiRConfigQuery(props.routineId);
1✔
304
    const deleteRiRQuery = useDeleteRiRConfigQuery(props.routineId);
1✔
305
    const addRiRQuery = useAddRiRConfigQuery(props.routineId);
1✔
306

307
    const handleData = (value: string) => {
1✔
308

309
        const data = {
×
310
            value: parseFloat(value),
311
        };
312

313
        if (value === '') {
×
314
            props.config && deleteRiRQuery.mutate(props.config.id);
×
315
        } else if (props.config !== undefined) {
×
316
            editRiRQuery.mutate({ id: props.config.id, ...data });
×
317
        } else {
318
            addRiRQuery.mutate({
×
319
                // eslint-disable-next-line camelcase
320
                slot_entry: props.slotEntryId!,
321
                iteration: 1,
322
                operation: OPERATION_REPLACE,
323
                ...data
324
            });
325
        }
326
    };
327

328
    return <TextField
1✔
329
        fullWidth
330
        select
331
        label="RiR"
332
        variant="standard"
333
        defaultValue=""
334
        value={props.config?.value}
335
        disabled={editRiRQuery.isPending}
336
        onChange={e => handleData(e.target.value)}
×
337
    >
338
        {RIR_VALUES_SELECT.map((option) => (
339
            <MenuItem key={option.value} value={option.value}>
11✔
340
                {option.label}
341
            </MenuItem>
342
        ))}
343
    </TextField>;
344
};
345

346

347
/*
348
 *  ---> These components are not needed anymore but are kept here in case we need
349
 *       to edit these fields individually in the future
350
 */
351

352
/*
353
export const AddEntryDetailsButton = (props: {
354
    iteration: number,
355
    routineId: number,
356
    slotEntryId: number,
357
    type: ConfigType
358
}) => {
359

360
    const { add: addQuery } = QUERY_MAP[props.type];
361
    const addQueryHook = addQuery(props.routineId);
362

363

364
    const handleData = () => {
365
        addQueryHook.mutate({
366
            slot_entry: props.slotEntryId!,
367
            iteration: props.iteration,
368
            value: 0,
369
            operation: OPERATION_REPLACE,
370
        });
371
    };
372

373
    return (<>
374
        <IconButton size="small" onClick={handleData} disabled={addQueryHook.isPending}>
375
            <AddIcon />
376
        </IconButton>
377
    </>);
378
};
379

380
export const DeleteEntryDetailsButton = (props: {
381
    configId: number,
382
    routineId: number,
383
    type: ConfigType,
384
    disable?: boolean
385
}) => {
386
    const disable = props.disable ?? false;
387
    const { delete: deleteQuery } = QUERY_MAP[props.type];
388
    const deleteQueryHook = deleteQuery(props.routineId);
389

390
    const handleData = () => {
391
        deleteQueryHook.mutate(props.configId);
392
    };
393

394
    return (
395
        <IconButton size="small" onClick={handleData} disabled={disable || deleteQueryHook.isPending}>
396
            <DeleteIcon />
397
        </IconButton>
398
    );
399
};
400

401

402
export const EntryDetailsOperationField = (props: {
403
    config: BaseConfig,
404
    routineId: number,
405
    slotEntryId: number,
406
    type: ConfigType,
407
    disable?: boolean
408
}) => {
409

410
    const disable = props.disable ?? false;
411
    const options = [
412
        {
413
            value: '+',
414
            label: 'Add',
415
        },
416
        {
417
            value: '-',
418
            label: 'Subtract',
419
        },
420
        {
421
            value: OPERATION_REPLACE,
422
            label: 'Replace',
423
        },
424
    ];
425

426
    const { edit: editQuery } = QUERY_MAP[props.type];
427
    const editQueryHook = editQuery(props.routineId);
428

429
    const handleData = (newValue: string) => {
430
        editQueryHook.mutate({ id: props.config.id, operation: newValue, });
431
    };
432

433
    return (<>
434
        <TextField
435
            sx={{ width: 100 }}
436
            select
437
            label="Operation"
438
            value={props.config?.operation}
439
            variant="standard"
440
            disabled={disable || editQueryHook.isPending}
441
            onChange={e => handleData(e.target.value)}
442
        >
443
            {options.map((option) => (
444
                <MenuItem key={option.value} value={option.value} selected={option.value === props.config.operation}>
445
                    {option.label}
446
                </MenuItem>
447
            ))}
448
        </TextField>
449
    </>);
450
};
451

452
export const EntryDetailsStepField = (props: {
453
    config: BaseConfig,
454
    routineId: number,
455
    slotEntryId: number,
456
    type: ConfigType,
457
    disable?: boolean
458
}) => {
459

460
    const disable = props.disable ?? false;
461

462
    const options = [
463
        {
464
            value: 'abs',
465
            label: 'absolute',
466
        },
467
        {
468
            value: 'percent',
469
            label: 'percent',
470
        },
471
    ];
472

473
    if (props.config.iteration === 1) {
474
        options.push({
475
            value: 'na',
476
            label: 'n/a',
477
        });
478
    }
479

480
    const { edit: editQuery } = QUERY_MAP[props.type];
481
    const editQueryHook = editQuery(props.routineId);
482

483
    const handleData = (newValue: string) => {
484
        editQueryHook.mutate({ id: props.config.id, step: newValue, });
485
    };
486

487
    return (<>
488
        <TextField
489
            sx={{ width: 100 }}
490
            select
491
            // label="Operation"
492
            value={props.config?.step}
493
            variant="standard"
494
            disabled={disable || editQueryHook.isPending}
495
            onChange={e => handleData(e.target.value)}
496
        >
497
            {options.map((option) => (
498
                <MenuItem key={option.value} value={option.value} selected={option.value === props.config.step}>
499
                    {option.label}
500
                </MenuItem>
501
            ))}
502
        </TextField>
503
    </>);
504
};
505

506
*/
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc