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

wger-project / react / 13619287146

02 Mar 2025 09:04PM UTC coverage: 76.562% (+0.6%) from 75.927%
13619287146

Pull #975

github

web-flow
Merge 045b02207 into 4d8aa218a
Pull Request #975: UI for flexible routines

1273 of 1857 branches covered (68.55%)

Branch coverage included in aggregate %.

2054 of 2529 new or added lines in 102 files covered. (81.22%)

2 existing lines in 2 files now uncovered.

5162 of 6548 relevant lines covered (78.83%)

25.2 hits per line

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

71.67
/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

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

107

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

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

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

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

139
    const handleData = (value: string) => {
107✔
140

141
        const data = {
24✔
142
            // eslint-disable-next-line camelcase
143
            slot_entry: props.slotEntryId,
144
            value: parseFunction(value),
145
        };
146

147
        if (value === '') {
24✔
148
            props.config && deleteQueryHook.mutate(props.config.id);
8✔
149
        } else if (props.config) {
16✔
150
            editQueryHook.mutate({ id: props.config.id, ...data });
8✔
151
        } else {
152
            addQueryHook.mutate({
8✔
153
                iteration: 1,
154
                operation: OPERATION_REPLACE,
155
                step: 'abs',
156
                requirements: null,
157
                ...data
158
            });
159
        }
160
    };
161

162
    const onChange = (text: string) => {
107✔
163
        setValue(text);
24✔
164

165
        if (text !== '' && Number.isNaN(parseFunction(text))) {
24!
NEW
166
            setError(t(isInt ? 'forms.enterInteger' : 'forms.enterNumber'));
×
NEW
167
            return;
×
168
        } else {
169
            setError(null);
24✔
170
        }
171

172

173
        if (timer) {
24!
NEW
174
            clearTimeout(timer);
×
175
        }
176
        setTimer(setTimeout(() => handleData(text), DEBOUNCE_ROUTINE_FORMS));
24✔
177
    };
178

179
    const isPending = editQueryHook.isPending || addQueryHook.isPending || deleteQueryHook.isPending;
107✔
180

181
    return (
107✔
182
        <TextField
183
            slotProps={{
184
                input: { endAdornment: isPending && <LoadingProgressIcon /> }
107!
185
            }}
186
            inputProps={{
187
                "data-testid": `${props.type}-field`,
188
            }}
189
            label={props.type}
190
            value={value}
191
            fullWidth
192
            variant="standard"
193
            disabled={isPending}
194
            onChange={e => onChange(e.target.value)}
24✔
195
            error={!!error || editQueryHook.isError || addQueryHook.isError || deleteQueryHook.isError}
428✔
196
            helperText={error || editQueryHook.error?.message || addQueryHook.error?.message || deleteQueryHook.error?.message}
428✔
197
        />
198
    );
199
};
200

201

202
export const ConfigDetailsRequirementsField = (props: {
6✔
203
    fieldName: string,
204
    values: RequirementsType[],
205
    disabled?: boolean
206
}) => {
207

208
    const { setFieldValue } = useFormikContext();
50✔
209
    const { t } = useTranslation();
50✔
210
    const disable = props.disabled ?? false;
50!
211

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

215
    const handleSelection = (value: RequirementsType) => {
50✔
216
        // if the value is not in selectedElements, add it
NEW
217
        if (!selectedElements.includes(value)) {
×
NEW
218
            setSelectedElements([...selectedElements, value]);
×
219
        } else {
NEW
220
            setSelectedElements(selectedElements.filter((e) => e !== value));
×
221
        }
222
    };
223

224
    const handleSubmit = async () => {
50✔
NEW
225
        await setFieldValue(props.fieldName, selectedElements);
×
NEW
226
        setAnchorEl(null);
×
227
    };
228

229

230
    return <>
50✔
231
        <IconButton
232
            disabled={disable}
NEW
233
            onClick={(event) => setAnchorEl(event.currentTarget)}
×
234
        >
235
            {Boolean(anchorEl) ? <ArrowDropUpIcon fontSize="small" /> : <SettingsIcon fontSize="small" />}
50!
236
        </IconButton>
237
        <Menu
238
            anchorEl={anchorEl}
239
            open={Boolean(anchorEl)}
NEW
240
            onClose={() => setAnchorEl(null)}
×
241
        >
242
            {...REQUIREMENTS_VALUES.map((e, index) => <MenuItem
200✔
NEW
243
                onClick={() => handleSelection(e as unknown as RequirementsType)}>
×
244
                <ListItemIcon>
245
                    {selectedElements.includes(e as unknown as RequirementsType)
200!
246
                        ? <CheckBoxIcon fontSize="small" />
247
                        : <CheckBoxOutlineBlank fontSize="small" />
248
                    }
249
                </ListItemIcon>
250
                <ListItemText>
251
                    {e}
252
                </ListItemText>
253
            </MenuItem>)}
254
            <Divider />
255
            <MenuItem>
256

257
                <Button color="primary" variant="contained" type="submit" size="small" onClick={handleSubmit}>
258
                    {t('save')}
259
                </Button>
260
            </MenuItem>
261
        </Menu></>;
262
};
263

264

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

267
    const editRiRQuery = useEditRiRConfigQuery(props.routineId);
1✔
268
    const deleteRiRQuery = useDeleteRiRConfigQuery(props.routineId);
1✔
269
    const addRiRQuery = useAddRiRConfigQuery(props.routineId);
1✔
270

271
    const handleData = (value: string) => {
1✔
272

NEW
273
        const data = {
×
274
            value: parseFloat(value),
275
        };
276

NEW
277
        if (value === '') {
×
NEW
278
            props.config && deleteRiRQuery.mutate(props.config.id);
×
NEW
279
        } else if (props.config !== undefined) {
×
NEW
280
            editRiRQuery.mutate({ id: props.config.id, ...data });
×
281
        } else {
NEW
282
            addRiRQuery.mutate({
×
283
                // eslint-disable-next-line camelcase
284
                slot_entry: props.slotEntryId!,
285
                iteration: 1,
286
                operation: OPERATION_REPLACE,
287
                ...data
288
            });
289
        }
290
    };
291

292
    return <TextField
1✔
293
        fullWidth
294
        select
295
        label="RiR"
296
        variant="standard"
297
        defaultValue=""
298
        value={props.config?.value}
299
        disabled={editRiRQuery.isPending}
NEW
300
        onChange={e => handleData(e.target.value)}
×
301
    >
302
        {RIR_VALUES_SELECT.map((option) => (
303
            <MenuItem key={option.value} value={option.value}>
11✔
304
                {option.label}
305
            </MenuItem>
306
        ))}
307
    </TextField>;
308
};
309

310

311
/*
312
 *  ---> These components are not needed anymore but are kept here in case we need
313
 *       to edit these fields individually in the future
314
 */
315

316
/*
317
export const AddEntryDetailsButton = (props: {
318
    iteration: number,
319
    routineId: number,
320
    slotEntryId: number,
321
    type: ConfigType
322
}) => {
323

324
    const { add: addQuery } = QUERY_MAP[props.type];
325
    const addQueryHook = addQuery(props.routineId);
326

327

328
    const handleData = () => {
329
        addQueryHook.mutate({
330
            slot_entry: props.slotEntryId!,
331
            iteration: props.iteration,
332
            value: 0,
333
            operation: OPERATION_REPLACE,
334
        });
335
    };
336

337
    return (<>
338
        <IconButton size="small" onClick={handleData} disabled={addQueryHook.isPending}>
339
            <AddIcon />
340
        </IconButton>
341
    </>);
342
};
343

344
export const DeleteEntryDetailsButton = (props: {
345
    configId: number,
346
    routineId: number,
347
    type: ConfigType,
348
    disable?: boolean
349
}) => {
350
    const disable = props.disable ?? false;
351
    const { delete: deleteQuery } = QUERY_MAP[props.type];
352
    const deleteQueryHook = deleteQuery(props.routineId);
353

354
    const handleData = () => {
355
        deleteQueryHook.mutate(props.configId);
356
    };
357

358
    return (
359
        <IconButton size="small" onClick={handleData} disabled={disable || deleteQueryHook.isPending}>
360
            <DeleteIcon />
361
        </IconButton>
362
    );
363
};
364

365

366
export const EntryDetailsOperationField = (props: {
367
    config: BaseConfig,
368
    routineId: number,
369
    slotEntryId: number,
370
    type: ConfigType,
371
    disable?: boolean
372
}) => {
373

374
    const disable = props.disable ?? false;
375
    const options = [
376
        {
377
            value: '+',
378
            label: 'Add',
379
        },
380
        {
381
            value: '-',
382
            label: 'Subtract',
383
        },
384
        {
385
            value: OPERATION_REPLACE,
386
            label: 'Replace',
387
        },
388
    ];
389

390
    const { edit: editQuery } = QUERY_MAP[props.type];
391
    const editQueryHook = editQuery(props.routineId);
392

393
    const handleData = (newValue: string) => {
394
        editQueryHook.mutate({ id: props.config.id, operation: newValue, });
395
    };
396

397
    return (<>
398
        <TextField
399
            sx={{ width: 100 }}
400
            select
401
            label="Operation"
402
            value={props.config?.operation}
403
            variant="standard"
404
            disabled={disable || editQueryHook.isPending}
405
            onChange={e => handleData(e.target.value)}
406
        >
407
            {options.map((option) => (
408
                <MenuItem key={option.value} value={option.value} selected={option.value === props.config.operation}>
409
                    {option.label}
410
                </MenuItem>
411
            ))}
412
        </TextField>
413
    </>);
414
};
415

416
export const EntryDetailsStepField = (props: {
417
    config: BaseConfig,
418
    routineId: number,
419
    slotEntryId: number,
420
    type: ConfigType,
421
    disable?: boolean
422
}) => {
423

424
    const disable = props.disable ?? false;
425

426
    const options = [
427
        {
428
            value: 'abs',
429
            label: 'absolute',
430
        },
431
        {
432
            value: 'percent',
433
            label: 'percent',
434
        },
435
    ];
436

437
    if (props.config.iteration === 1) {
438
        options.push({
439
            value: 'na',
440
            label: 'n/a',
441
        });
442
    }
443

444
    const { edit: editQuery } = QUERY_MAP[props.type];
445
    const editQueryHook = editQuery(props.routineId);
446

447
    const handleData = (newValue: string) => {
448
        editQueryHook.mutate({ id: props.config.id, step: newValue, });
449
    };
450

451
    return (<>
452
        <TextField
453
            sx={{ width: 100 }}
454
            select
455
            // label="Operation"
456
            value={props.config?.step}
457
            variant="standard"
458
            disabled={disable || editQueryHook.isPending}
459
            onChange={e => handleData(e.target.value)}
460
        >
461
            {options.map((option) => (
462
                <MenuItem key={option.value} value={option.value} selected={option.value === props.config.step}>
463
                    {option.label}
464
                </MenuItem>
465
            ))}
466
        </TextField>
467
    </>);
468
};
469

470
*/
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