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

wger-project / react / 18075690066

28 Sep 2025 02:32PM UTC coverage: 75.232% (-0.4%) from 75.672%
18075690066

push

github

web-flow
Merge pull request #1126 from wger-project/feature/exercise-submission

Use new endpoint for exercise submission

1723 of 2590 branches covered (66.53%)

Branch coverage included in aggregate %.

64 of 120 new or added lines in 14 files covered. (53.33%)

6 existing lines in 1 file now uncovered.

5500 of 7011 relevant lines covered (78.45%)

28.07 hits per line

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

88.89
/src/components/Exercises/Add/Step2Variations.tsx
1
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
2✔
2
import PhotoIcon from '@mui/icons-material/Photo';
2✔
3
import SearchIcon from '@mui/icons-material/Search';
2✔
4
import {
2✔
5
    Alert,
6
    AlertTitle,
7
    Avatar,
8
    AvatarGroup,
9
    Box,
10
    Button,
11
    InputAdornment,
12
    List,
13
    ListItem,
14
    ListItemButton,
15
    ListItemIcon,
16
    ListItemText,
17
    Paper,
18
    Switch,
19
    TextField,
20
    Typography
21
} from "@mui/material";
22
import Grid from '@mui/material/Grid';
2✔
23
import { LoadingPlaceholder } from "components/Core/LoadingWidget/LoadingWidget";
2✔
24
import { StepProps } from "components/Exercises/Add/AddExerciseStepper";
25
import { Exercise } from "components/Exercises/models/exercise";
26

27
import { useExercisesQuery } from "components/Exercises/queries";
2✔
28
import React, { useEffect, useState } from "react";
2✔
29
import { useTranslation } from "react-i18next";
2✔
30
import { useExerciseSubmissionStateValue } from "state";
2✔
31
import { setNewBaseVariationId, setVariationId } from "state/exerciseSubmissionReducer";
2✔
32

33
/*
34
 * Groups a list of objects by a property
35
 */
36
function groupBy(list: any[], keyGetter: Function) {
37
    const map = new Map();
18✔
38
    list.forEach((item: any) => {
18✔
39
        const key = keyGetter(item);
27✔
40
        const collection = map.get(key);
27✔
41
        if (!collection) {
27✔
42
            map.set(key, [item]);
16✔
43
        } else {
44
            collection.push(item);
11✔
45
        }
46
    });
47
    return map;
18✔
48
}
49

50
// New component that displays the exercise info in a ListItem
51
const ExerciseInfoListItem = ({ exercises }: { exercises: Exercise[] }) => {
2✔
52
    const MAX_EXERCISE_IMAGES = 4;
31✔
53
    const MAX_EXERCISE_NAMES = 5;
31✔
54
    const variationId = exercises[0].variationId;
31✔
55
    const exerciseId = exercises[0].id;
31✔
56

57
    const [state, dispatch] = useExerciseSubmissionStateValue();
31✔
58
    const [showMore, setShowMore] = useState<boolean>(false);
31✔
59

60
    const [stateVariationId, setStateVariationId] = useState<number | null>(state.variationId);
31✔
61
    const [stateNewVariationId, setStateNewVariationId] = useState<number | null>(state.newVariationExerciseId);
31✔
62

63
    useEffect(() => {
31✔
64
        dispatch(setVariationId(stateVariationId));
15✔
65
    }, [dispatch, stateVariationId]);
66

67
    useEffect(() => {
31✔
68
        dispatch(setNewBaseVariationId(stateNewVariationId));
15✔
69
    }, [dispatch, stateNewVariationId]);
70

71

72
    const handleToggle = (variationId: number | null, newVariationId: number | null) => () => {
31✔
73

74
        if (variationId !== null) {
6✔
75
            newVariationId = null;
3✔
76
            if (variationId === state.variationId) {
3!
77
                variationId = null;
×
78
            }
79
        } else {
80
            variationId = null;
3✔
81
            if (newVariationId === state.newVariationExerciseId) {
3!
82
                newVariationId = null;
×
83
            }
84
        }
85

86
        setStateVariationId(variationId);
6✔
87
        setStateNewVariationId(newVariationId);
6✔
88
    };
89

90
    let isChecked;
91
    if (variationId === null) {
31✔
92
        isChecked = state.newVariationExerciseId === exerciseId;
14✔
93
    } else {
94
        isChecked = variationId === state.variationId;
17✔
95
    }
96

97
    return (
31✔
98
        <ListItem disableGutters>
99
            <ListItemButton onClick={handleToggle(variationId, exerciseId)}>
100
                <ListItemIcon>
101
                    <AvatarGroup max={MAX_EXERCISE_IMAGES} spacing={"small"}>
102
                        {exercises.map((base) =>
103
                            base.mainImage
43!
104
                                ? <Avatar key={base.id} src={base.mainImage.url} />
105
                                : <Avatar key={base.id} children={<PhotoIcon />} />
106
                        )}
107
                    </AvatarGroup>
108
                </ListItemIcon>
109
                <ListItemText
110
                    primary={exercises.slice(0, showMore ? exercises.length : MAX_EXERCISE_NAMES).map((exercise) =>
31!
111
                        <p style={{ margin: 0 }} key={exercise.id}>{exercise.getTranslation().name}</p>
43✔
112
                    )} />
113

114
                <Switch
115
                    key={`variation-${variationId}`}
116
                    edge="start"
117
                    checked={isChecked}
118
                    tabIndex={-1}
119
                    disableRipple
120
                />
121

122
                {!showMore && exercises.length > MAX_EXERCISE_NAMES
93!
NEW
123
                    ? <ExpandMoreIcon onMouseEnter={() => setShowMore(true)} />
×
124
                    : null
125
                }
126

127
            </ListItemButton>
128
        </ListItem>
129
    );
130
};
131

132

133
export const Step2Variations = ({ onContinue, onBack }: StepProps) => {
2✔
134
    const [t] = useTranslation();
18✔
135
    const exercisesQuery = useExercisesQuery();
18✔
136
    const [state, dispatch] = useExerciseSubmissionStateValue();
18✔
137

138
    const [searchTerm, setSearchTerms] = useState<string>('');
18✔
139

140
    // Group exercises by variationId
141
    let exercises: Exercise[] = [];
18✔
142
    let groupedExercises = new Map<number, Exercise[]>();
18✔
143
    if (exercisesQuery.isSuccess) {
18!
144
        exercises = exercisesQuery.data;
18✔
145
        if (searchTerm !== '') {
18✔
146
            exercises = exercises.filter((base) => base.getTranslation().name.toLowerCase().includes(searchTerm.toLowerCase()));
27✔
147
        }
148
    }
149
    groupedExercises = groupBy(exercises.filter(b => b.variationId !== null), (b: Exercise) => b.variationId);
40✔
150

151
    return <>
18✔
152
        <Grid container>
153
            <Grid
154
                size={{
155
                    xs: 12,
156
                    sm: 6
157
                }}>
158
                <Typography>{t('exercises.whatVariationsExist')}</Typography>
159
            </Grid>
160
            <Grid
161
                display="flex"
162
                justifyContent={"end"}
163
                size={{
164
                    xs: 12,
165
                    sm: 6
166
                }}>
167
                <TextField
168
                    label={t('name')}
169
                    helperText={t('exercises.filterVariations')}
170
                    // defaultValue={state.nameEn}
171
                    variant="standard"
172
                    onChange={(event) => setSearchTerms(event.target.value)}
9✔
173
                    slotProps={{
174
                        input: {
175
                            startAdornment: (
176
                                <InputAdornment position="start">
177
                                    <SearchIcon />
178
                                </InputAdornment>
179
                            ),
180
                        }
181
                    }}
182
                />
183
            </Grid>
184
        </Grid>
185

186
        {exercisesQuery.isLoading
18!
187
            ? <LoadingPlaceholder />
188
            : <Paper elevation={2} sx={{ mt: 2 }}>
189
                <List style={{ height: "400px", overflowY: "scroll" }}>
190
                    {exercises.filter(b => b.variationId === null).map(exercise =>
40✔
191
                        <ExerciseInfoListItem
13✔
192
                            exercises={[exercise]}
193
                            key={'exercise-' + exercise.id}
194
                        />
195
                    )}
196
                    {[...groupedExercises.keys()].map(variationId =>
197
                        <ExerciseInfoListItem
16✔
198
                            exercises={groupedExercises.get(variationId)!}
199
                            key={'variation-' + variationId}
200
                        />
201
                    )}
202
                </List>
203
            </Paper>
204
        }
205

206
        <Alert severity="info" variant="filled" sx={{ mt: 2 }}>
207
            <AlertTitle>{t("exercises.identicalExercise")}</AlertTitle>
208
            {t('exercises.identicalExercisePleaseDiscard')}
209
        </Alert>
210

211
        <Grid container>
212
            <Grid display="flex" justifyContent={"end"} size={12}>
213
                <Box sx={{ mb: 2 }}>
214
                    <div>
215
                        <Button
216
                            disabled={false}
217
                            onClick={onBack}
218
                            sx={{ mt: 1, mr: 1 }}
219
                        >
220
                            {t('goBack')}
221
                        </Button>
222
                        <Button
223
                            variant="contained"
224
                            onClick={onContinue}
225
                            sx={{ mt: 1, mr: 1 }}
226
                        >
227
                            {t('continue')}
228
                        </Button>
229
                    </div>
230
                </Box>
231
            </Grid>
232
        </Grid>
233
    </>;
234
};
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