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

wger-project / react / 25459639259

06 May 2026 08:31PM UTC coverage: 78.195% (+0.1%) from 78.09%
25459639259

push

github

rolandgeider
Mock missing services

2224 of 3134 branches covered (70.96%)

Branch coverage included in aggregate %.

4819 of 5873 relevant lines covered (82.05%)

31.53 hits per line

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

87.18
/src/components/Exercises/screens/Add/Step2Variations.tsx
1
import { Exercise } from "@/components/Exercises/models/exercise";
2

3
import { useExercisesQuery } from "@/components/Exercises/queries";
4
import { StepProps } from "@/components/Exercises/screens/Add/AddExerciseStepper";
5
import { useExerciseSubmissionStateValue } from "@/components/Exercises/screens/Add/state";
6
import {
7
    setNewBaseVariationId,
8
    setVariationId
9
} from "@/components/Exercises/screens/Add/state/exerciseSubmissionReducer";
10
import { LoadingPlaceholder } from "@/core/ui/LoadingWidget/LoadingWidget";
11
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
12
import PhotoIcon from '@mui/icons-material/Photo';
13
import SearchIcon from '@mui/icons-material/Search';
14
import {
15
    Alert,
16
    AlertTitle,
17
    Avatar,
18
    AvatarGroup,
19
    Box,
20
    Button,
21
    InputAdornment,
22
    List,
23
    ListItem,
24
    ListItemButton,
25
    ListItemIcon,
26
    ListItemText,
27
    Paper,
28
    Switch,
29
    TextField,
30
    Typography
31
} from "@mui/material";
32
import Grid from '@mui/material/Grid';
33
import React, { useEffect, useState } from "react";
34
import { useTranslation } from "react-i18next";
35

36
/*
37
 * Groups a list of objects by a property
38
 */
39
function groupBy<T, K>(list: T[], keyGetter: (item: T) => K): Map<K, T[]> {
40
    const map = new Map<K, T[]>();
15✔
41
    list.forEach((item) => {
15✔
42
        const key = keyGetter(item);
21✔
43
        const collection = map.get(key);
21✔
44
        if (!collection) {
21✔
45
            map.set(key, [item]);
13✔
46
        } else {
47
            collection.push(item);
8✔
48
        }
49
    });
50
    return map;
15✔
51
}
52

53
// New component that displays the exercise info in a ListItem
54
const ExerciseInfoListItem = ({ exercises }: { exercises: Exercise[] }) => {
17✔
55
    const MAX_EXERCISE_IMAGES = 4;
31✔
56
    const MAX_EXERCISE_NAMES = 5;
31✔
57
    const variationGroup = exercises[0].variationGroup;
31✔
58
    const exerciseId = exercises[0].id;
31✔
59

60
    const [state, dispatch] = useExerciseSubmissionStateValue();
31✔
61
    const [showMore, setShowMore] = useState<boolean>(false);
31✔
62

63
    const [stateVariationGroup, setStateVariationGroup] = useState<string | null>(state.variationGroup);
31✔
64
    const [stateNewVariationId, setStateNewVariationId] = useState<number | null>(state.newVariationExerciseId);
31✔
65

66
    useEffect(() => {
31✔
67
        dispatch(setVariationId(stateVariationGroup));
15✔
68
    }, [dispatch, stateVariationGroup]);
69

70
    useEffect(() => {
31✔
71
        dispatch(setNewBaseVariationId(stateNewVariationId));
15✔
72
    }, [dispatch, stateNewVariationId]);
73

74

75
    const handleToggle = (variationGroup: string | null, newVariationId: number | null) => () => {
31✔
76

77
        if (variationGroup !== null) {
6✔
78
            newVariationId = null;
3✔
79
            if (variationGroup === state.variationGroup) {
3!
80
                variationGroup = null;
×
81
            }
82
        } else {
83
            variationGroup = null;
3✔
84
            if (newVariationId === state.newVariationExerciseId) {
3!
85
                newVariationId = null;
×
86
            }
87
        }
88

89
        setStateVariationGroup(variationGroup);
6✔
90
        setStateNewVariationId(newVariationId);
6✔
91
    };
92

93
    let isChecked;
94
    if (variationGroup === null) {
31✔
95
        isChecked = state.newVariationExerciseId === exerciseId;
14✔
96
    } else {
97
        isChecked = variationGroup === state.variationGroup;
17✔
98
    }
99

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

117
                <Switch
118
                    key={`variation-${variationGroup}`}
119
                    edge="start"
120
                    checked={isChecked}
121
                    tabIndex={-1}
122
                    disableRipple
123
                />
124

125
                {!showMore && exercises.length > MAX_EXERCISE_NAMES
93!
126
                    ? <ExpandMoreIcon onMouseEnter={() => setShowMore(true)} />
×
127
                    : null
128
                }
129

130
            </ListItemButton>
131
        </ListItem>
132
    );
133
};
134

135

136
export const Step2Variations = ({ onContinue, onBack }: StepProps) => {
17✔
137
    const [t] = useTranslation();
15✔
138
    const exercisesQuery = useExercisesQuery();
15✔
139

140
    const [searchTerm, setSearchTerms] = useState<string>('');
15✔
141

142
    // Group exercises by variationGroup
143
    let exercises: Exercise[] = [];
15✔
144
    let groupedExercises = new Map<string, Exercise[]>();
15✔
145
    if (exercisesQuery.isSuccess) {
15!
146
        exercises = exercisesQuery.data;
15✔
147
        if (searchTerm !== '') {
15✔
148
            exercises = exercises.filter((base) => base.getTranslation().name.toLowerCase().includes(searchTerm.toLowerCase()));
24✔
149
        }
150
    }
151
    groupedExercises = groupBy(exercises.filter(b => b.variationGroup !== null), (b: Exercise) => b.variationGroup!);
31✔
152

153
    return <>
15✔
154
        <Grid container>
155
            <Grid
156
                size={{
157
                    xs: 12,
158
                    sm: 6
159
                }}>
160
                <Typography>{t('exercises.whatVariationsExist')}</Typography>
161
            </Grid>
162
            <Grid
163
                sx={{ display: "flex", justifyContent: "end" }}
164
                size={{
165
                    xs: 12,
166
                    sm: 6
167
                }}>
168
                <TextField
169
                    label={t('name')}
170
                    helperText={t('exercises.filterVariations')}
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
15!
187
            ? <LoadingPlaceholder />
188
            : <Paper elevation={2} sx={{ mt: 2 }}>
189
                <List style={{ height: "400px", overflowY: "scroll" }}>
190
                    {exercises.filter(b => b.variationGroup === null).map(exercise =>
31✔
191
                        <ExerciseInfoListItem
10✔
192
                            exercises={[exercise]}
193
                            key={'exercise-' + exercise.id}
194
                        />
195
                    )}
196
                    {[...groupedExercises.keys()].map(variationGroup =>
197
                        <ExerciseInfoListItem
13✔
198
                            exercises={groupedExercises.get(variationGroup)!}
199
                            key={'variation-' + variationGroup}
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 sx={{ 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