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

wger-project / react / 25381278702

05 May 2026 02:05PM UTC coverage: 74.492% (-0.1%) from 74.633%
25381278702

push

github

rolandgeider
Upgrade dependencies

2050 of 3129 branches covered (65.52%)

Branch coverage included in aggregate %.

6118 of 7836 relevant lines covered (78.08%)

28.48 hits per line

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

11.11
/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[]> {
29
    const map = new Map<K, T[]>();
×
30
    list.forEach((item: T) => {
×
31
        const key = keyGetter(item);
×
32
        const collection = map.get(key);
×
33
        if (!collection) {
×
34
            map.set(key, [item]);
×
35
        } else {
36
            collection.push(item);
×
37
        }
38
    });
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) => {
57
    const MAX_EXERCISE_IMAGES = 4;
×
58
    const MAX_EXERCISE_NAMES = 5;
×
59
    const variationId = exercises[0].variationGroup;
×
60
    const exerciseId = exercises[0].id;
×
61

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

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

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

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

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

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) =>
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) =>
×
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
×
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) {
141
    const [t] = useTranslation();
×
142
    const exercisesQuery = useExercisesQuery();
×
143
    const [searchTerm, setSearchTerms] = useState<string>('');
×
144

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

152
        if (searchTerm !== '') {
×
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)
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
160
        exercises = exerciseId
×
161
            ? allExercises.filter((e) => e.id !== exerciseId)
×
162
            : allExercises;
163
    }
164

165
    const handleToggle = (variationId: string | null, newVariationId: number | null) => {
×
166
        if (newVariationId !== null) {
×
167
            onChangeNewVariationExerciseId(newVariationId);
×
168
        } else {
169
            onChangeVariationId(variationId);
×
170
        }
171
    };
172

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

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

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

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