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

wger-project / react / 18411303152

10 Oct 2025 03:38PM UTC coverage: 75.275% (+0.04%) from 75.236%
18411303152

push

github

rolandgeider
Remove AddSlotEntryParams and use SlotEntry objects

1737 of 2608 branches covered (66.6%)

Branch coverage included in aggregate %.

11 of 13 new or added lines in 4 files covered. (84.62%)

146 existing lines in 36 files now uncovered.

5512 of 7022 relevant lines covered (78.5%)

28.14 hits per line

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

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

109

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

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

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

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

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

175
    const handleData = (value: string) => {
101✔
176

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

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

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

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

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

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

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

238

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

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

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

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

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

266

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

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

302

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

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

309
    const handleData = (value: string) => {
1✔
310

UNCOV
311
        const data = {
×
312
            value: parseFloat(value),
313
        };
314

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

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

348

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

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

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

365

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

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

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

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

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

403

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

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

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

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

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

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

462
    const disable = props.disable ?? false;
463

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

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

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

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

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

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