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

wger-project / react / 24400238877

14 Apr 2026 12:58PM UTC coverage: 74.753% (-0.7%) from 75.488%
24400238877

Pull #1234

github

web-flow
Merge b70152dcd into 47a6ff2da
Pull Request #1234: version 2.5

2018 of 3062 branches covered (65.9%)

Branch coverage included in aggregate %.

125 of 241 new or added lines in 29 files covered. (51.87%)

7 existing lines in 2 files now uncovered.

6077 of 7767 relevant lines covered (78.24%)

28.27 hits per line

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

11.46
/src/components/Exercises/forms/VariationSelect.tsx
1
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
6✔
2
import PhotoIcon from '@mui/icons-material/Photo';
6✔
3
import SearchIcon from '@mui/icons-material/Search';
6✔
4
import {
6✔
5
    Avatar,
6
    AvatarGroup,
7
    InputAdornment,
8
    List,
9
    ListItem,
10
    ListItemButton,
11
    ListItemIcon,
12
    ListItemText,
13
    Paper,
14
    Switch,
15
    TextField,
16
} from "@mui/material";
17
import Grid from '@mui/material/Grid';
6✔
18
import { LoadingPlaceholder } from "components/Core/LoadingWidget/LoadingWidget";
6✔
19
import { Exercise } from "components/Exercises/models/exercise";
20
import { useExercisesQuery } from "components/Exercises/queries";
6✔
21
import React, { useState } from "react";
6✔
22
import { useTranslation } from "react-i18next";
6✔
23

24
/*
25
 * Groups a list of objects by a property
26
 */
27

28
function groupBy<T, K>(list: T[], keyGetter: (item: T) => K): Map<K, T[]> {
NEW
29
    const map = new Map<K, T[]>();
×
NEW
30
    list.forEach((item: T) => {
×
NEW
31
        const key = keyGetter(item);
×
NEW
32
        const collection = map.get(key);
×
NEW
33
        if (!collection) {
×
NEW
34
            map.set(key, [item]);
×
35
        } else {
NEW
36
            collection.push(item);
×
37
        }
38
    });
NEW
39
    return map;
×
40
}
41

42
interface ExerciseInfoListItemProps {
43
    exercises: Exercise[];
44
    selectedVariationId: string | null;
45
    selectedNewVariationExerciseId: number | null;
46
    onToggle: (variationId: string | null, newVariationId: number | null) => void;
47
    highlight?: boolean;
48
}
49

50
const ExerciseInfoListItem = ({
6✔
51
                                  exercises,
52
                                  selectedVariationId,
53
                                  selectedNewVariationExerciseId,
54
                                  onToggle,
55
                                  highlight = false,
×
56
                              }: ExerciseInfoListItemProps) => {
NEW
57
    const MAX_EXERCISE_IMAGES = 4;
×
NEW
58
    const MAX_EXERCISE_NAMES = 5;
×
NEW
59
    const variationId = exercises[0].variationGroup;
×
NEW
60
    const exerciseId = exercises[0].id;
×
61

NEW
62
    const [showMore, setShowMore] = useState<boolean>(false);
×
63

NEW
64
    const handleToggle = () => {
×
NEW
65
        let newVarId = variationId;
×
NEW
66
        let newExId: number | null = exerciseId;
×
67

NEW
68
        if (newVarId !== null) {
×
NEW
69
            newExId = null;
×
NEW
70
            if (newVarId === selectedVariationId) {
×
NEW
71
                newVarId = null;
×
72
            }
73
        } else {
NEW
74
            newVarId = null;
×
NEW
75
            if (newExId === selectedNewVariationExerciseId) {
×
NEW
76
                newExId = null;
×
77
            }
78
        }
79

NEW
80
        onToggle(newVarId, newExId);
×
81
    };
82

83
    let isChecked;
NEW
84
    if (variationId === null) {
×
NEW
85
        isChecked = selectedNewVariationExerciseId === exerciseId;
×
86
    } else {
NEW
87
        isChecked = variationId === selectedVariationId;
×
88
    }
89

NEW
90
    return (
×
91
        <ListItem disableGutters sx={highlight ? { backgroundColor: 'action.selected' } : undefined}>
×
92
            <ListItemButton onClick={handleToggle}>
93
                <ListItemIcon>
94
                    <AvatarGroup max={MAX_EXERCISE_IMAGES} spacing={"small"}>
95
                        {exercises.map((base) =>
NEW
96
                            base.mainImage
×
97
                                ? <Avatar key={base.id} src={base.mainImage.url} />
98
                                : <Avatar key={base.id} children={<PhotoIcon />} />
99
                        )}
100
                    </AvatarGroup>
101
                </ListItemIcon>
102
                <ListItemText
103
                    primary={exercises.slice(0, showMore ? exercises.length : MAX_EXERCISE_NAMES).map((exercise) =>
×
NEW
104
                        <p style={{ margin: 0 }} key={exercise.id}>{exercise.getTranslation().name}</p>
×
105
                    )} />
106

107
                <Switch
108
                    key={`variation-${variationId}`}
109
                    edge="start"
110
                    checked={isChecked}
111
                    tabIndex={-1}
112
                    disableRipple
113
                />
114

115
                {!showMore && exercises.length > MAX_EXERCISE_NAMES
×
NEW
116
                    ? <ExpandMoreIcon onMouseEnter={() => setShowMore(true)} />
×
117
                    : null
118
                }
119

120
            </ListItemButton>
121
        </ListItem>
122
    );
123
};
124

125

126
export interface VariationSelectProps {
127
    exerciseId?: number;
128
    selectedVariationId: string | null;
129
    selectedNewVariationExerciseId: number | null;
130
    onChangeVariationId: (id: string | null) => void;
131
    onChangeNewVariationExerciseId: (id: number | null) => void;
132
}
133

134
export function VariationSelect({
6✔
135
                                    exerciseId,
136
                                    selectedVariationId,
137
                                    selectedNewVariationExerciseId,
138
                                    onChangeVariationId,
139
                                    onChangeNewVariationExerciseId,
140
                                }: VariationSelectProps) {
NEW
141
    const [t] = useTranslation();
×
NEW
142
    const exercisesQuery = useExercisesQuery();
×
NEW
143
    const [searchTerm, setSearchTerms] = useState<string>('');
×
144

145
    // Group exercises by variationId
NEW
146
    let allExercises: Exercise[] = [];
×
NEW
147
    let exercises: Exercise[] = [];
×
NEW
148
    let groupedExercises = new Map<string, Exercise[]>();
×
NEW
149
    if (exercisesQuery.isSuccess) {
×
NEW
150
        allExercises = exercisesQuery.data;
×
151

NEW
152
        if (searchTerm !== '') {
×
NEW
153
            allExercises = allExercises.filter((base) => base.getTranslation().name.toLowerCase().includes(searchTerm.toLowerCase()));
×
154
        }
155

156
        // Group first (including current exercise, so groups show all members)
NEW
157
        groupedExercises = groupBy(allExercises.filter(b => b.variationGroup !== null), (b: Exercise) => b.variationGroup as string);
×
158

159
        // Filter out the current exercise only for the standalone list
NEW
160
        exercises = exerciseId
×
NEW
161
            ? allExercises.filter((e) => e.id !== exerciseId)
×
162
            : allExercises;
163
    }
164

NEW
165
    const handleToggle = (variationId: string | null, newVariationId: number | null) => {
×
NEW
166
        onChangeVariationId(variationId);
×
NEW
167
        onChangeNewVariationExerciseId(newVariationId);
×
168
    };
169

NEW
170
    return <>
×
171
        <Grid container>
172
            <Grid size={{ xs: 12, sm: 6 }}>
173
                <TextField
174
                    fullWidth
175
                    label={t('name')}
176
                    helperText={t('exercises.filterVariations')}
177
                    variant="standard"
NEW
178
                    onChange={(event) => setSearchTerms(event.target.value)}
×
179
                    slotProps={{
180
                        input: {
181
                            startAdornment: (
182
                                <InputAdornment position="start">
183
                                    <SearchIcon />
184
                                </InputAdornment>
185
                            ),
186
                        }
187
                    }}
188
                />
189
            </Grid>
190
        </Grid>
191

192
        {exercisesQuery.isLoading
×
193
            ? <LoadingPlaceholder />
194
            : <Paper elevation={2} sx={{ mt: 2 }}>
195
                <List style={{ height: "400px", overflowY: "scroll" }}>
196
                    {/* Show the current variation group first */}
197
                    {selectedVariationId !== null && groupedExercises.has(selectedVariationId) && (
×
198
                        <ExerciseInfoListItem
199
                            exercises={groupedExercises.get(selectedVariationId)!}
200
                            key={'variation-current-' + selectedVariationId}
201
                            selectedVariationId={selectedVariationId}
202
                            selectedNewVariationExerciseId={selectedNewVariationExerciseId}
203
                            onToggle={handleToggle}
204
                            highlight
205
                        />
206
                    )}
207

208
                    {/* Remaining variation groups */}
209
                    {[...groupedExercises.keys()]
NEW
210
                        .filter(id => id !== selectedVariationId)
×
211
                        .map(variationId =>
NEW
212
                            <ExerciseInfoListItem
×
213
                                exercises={groupedExercises.get(variationId)!}
214
                                key={'variation-' + variationId}
215
                                selectedVariationId={selectedVariationId}
216
                                selectedNewVariationExerciseId={selectedNewVariationExerciseId}
217
                                onToggle={handleToggle}
218
                            />
219
                        )}
220

221
                    {/* Standalone exercises (no variation group) */}
NEW
222
                    {exercises.filter(b => b.variationGroup === null).map(exercise =>
×
NEW
223
                        <ExerciseInfoListItem
×
224
                            exercises={[exercise]}
225
                            key={'exercise-' + exercise.id}
226
                            selectedVariationId={selectedVariationId}
227
                            selectedNewVariationExerciseId={selectedNewVariationExerciseId}
228
                            onToggle={handleToggle}
229
                        />
230
                    )}
231
                </List>
232
            </Paper>
233
        }
234
    </>;
235
}
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