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

wger-project / react / 14796038759

02 May 2025 01:21PM UTC coverage: 76.3%. Remained the same
14796038759

push

github

rolandgeider
Merge remote-tracking branch 'origin/master'

1337 of 1957 branches covered (68.32%)

Branch coverage included in aggregate %.

5369 of 6832 relevant lines covered (78.59%)

26.16 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();
101✔
128
    const { edit: editQuery, add: addQuery, delete: deleteQuery } = QUERY_MAP[props.type];
101✔
129
    const editQueryHook = editQuery(props.routineId);
101✔
130
    const addQueryHook = addQuery(props.routineId);
101✔
131
    const deleteQueryHook = deleteQuery(props.routineId);
101✔
132

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

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

140
    let title = '';
101✔
141
    switch (props.type) {
101!
142
        case "weight":
143
            title = t('weight');
17✔
144
            break;
17✔
145
        case "max-weight":
146
            title = 'max ' + t('weight');
10✔
147
            break;
10✔
148
        case "reps":
149
            title = t('server.repetitions');
17✔
150
            break;
17✔
151
        case "max-reps":
152
            title = t('server.max_reps');
10✔
153
            break;
10✔
154
        case "sets":
155
            title = t('routines.sets');
17✔
156
            break;
17✔
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":
170
            title = 'max ' + t('routines.rir');
×
171
            break;
×
172
    }
173

174
    const handleData = (value: string) => {
101✔
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 === '' && props.config) {
24✔
183
            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) => {
101✔
198
        setValue(text);
24✔
199

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

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

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

217
    return (
101✔
218
        <TextField
219
            slotProps={{
220
                input: { endAdornment: isPending && <LoadingProgressIcon /> }
101!
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}
202✔
232
            helperText={manualValidationError || errorMessage}
202✔
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();
48✔
245
    const { t } = useTranslation();
48✔
246
    const disable = props.disabled ?? false;
48!
247

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

251
    const handleSelection = (value: RequirementsType) => {
48✔
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 () => {
48✔
261
        await setFieldValue(props.fieldName, selectedElements);
×
262
        setAnchorEl(null);
×
263
    };
264

265

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

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

301

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

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

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

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

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

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

347

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

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

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

364

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

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

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

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

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

402

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

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

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

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

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

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

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

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

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

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

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

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

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