@wger-project/react-components 25.11.17 → 25.11.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/main.js +126 -126
- package/build/main.js.map +1 -1
- package/package.json +2 -2
- package/src/components/Exercises/Add/Step1Basics.test.tsx +4 -1
- package/src/components/Exercises/Detail/ExerciseDetailEdit.tsx +43 -27
- package/src/components/Exercises/Detail/ExerciseDetailView.tsx +57 -17
- package/src/components/Exercises/Detail/OverviewCard.test.tsx +7 -11
- package/src/components/Exercises/ExerciseOverview.tsx +6 -6
- package/src/components/Exercises/Overview/ExerciseGrid.tsx +1 -2
- package/src/components/Exercises/forms/ExerciseAliases.tsx +78 -21
- package/src/components/Exercises/forms/yupValidators.ts +7 -4
- package/src/components/Exercises/models/exercise.ts +55 -43
- package/src/services/measurements.ts +15 -2
- package/src/tests/exerciseTestdata.ts +61 -55
- package/src/tests/exercises/searchResponse.ts +31 -29
- package/src/tests/responseApi.ts +25 -14
package/package.json
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"exports": {
|
|
12
12
|
".": "./build/index.js"
|
|
13
13
|
},
|
|
14
|
-
"version": "25.11.
|
|
14
|
+
"version": "25.11.22",
|
|
15
15
|
"repository": "https://github.com/wger-project/react",
|
|
16
16
|
"type": "module",
|
|
17
17
|
"publishConfig": {
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"luxon": "^3.7.2",
|
|
40
40
|
"react": "^19.2.0",
|
|
41
41
|
"react-dom": "^19.2.0",
|
|
42
|
-
"react-i18next": "^16.
|
|
42
|
+
"react-i18next": "^16.3.3",
|
|
43
43
|
"react-is": "^19.2.0",
|
|
44
44
|
"react-responsive": "^10.0.1",
|
|
45
45
|
"react-router-dom": "^7.9.4",
|
|
@@ -127,7 +127,10 @@ describe("<Step1Basics />", () => {
|
|
|
127
127
|
expect(setSecondaryMuscles).toHaveBeenNthCalledWith(2, [4]);
|
|
128
128
|
expect(setNameEn).toHaveBeenCalledWith('Biceps enlarger');
|
|
129
129
|
expect(setCategory).toHaveBeenCalledWith(1);
|
|
130
|
-
expect(setAlternativeNamesEn).toHaveBeenCalledWith([
|
|
130
|
+
expect(setAlternativeNamesEn).toHaveBeenCalledWith([
|
|
131
|
+
{ 'alias': 'Biceps enlarger 2000' },
|
|
132
|
+
{ 'alias': 'Arms exploder' }
|
|
133
|
+
]);
|
|
131
134
|
expect(setEquipment).toHaveBeenCalledWith([42]);
|
|
132
135
|
});
|
|
133
136
|
});
|
|
@@ -67,7 +67,17 @@ export const ExerciseDetailEdit = ({ exerciseId, language }: ViewProps) => {
|
|
|
67
67
|
}, [exerciseQuery.data]);
|
|
68
68
|
|
|
69
69
|
|
|
70
|
-
if (
|
|
70
|
+
if (
|
|
71
|
+
exerciseQuery.isLoading
|
|
72
|
+
|| musclesQuery.isLoading
|
|
73
|
+
|| exerciseQuery.isLoading
|
|
74
|
+
|| profileQuery.isLoading
|
|
75
|
+
|| addImagePermissionQuery.isLoading
|
|
76
|
+
|| deleteImagePermissionQuery.isLoading
|
|
77
|
+
|| addVideoPermissionQuery.isLoading
|
|
78
|
+
|| deleteVideoPermissionQuery.isLoading
|
|
79
|
+
|| editExercisePermissionQuery.isLoading
|
|
80
|
+
) {
|
|
71
81
|
return <LoadingWidget />;
|
|
72
82
|
}
|
|
73
83
|
|
|
@@ -89,7 +99,7 @@ export const ExerciseDetailEdit = ({ exerciseId, language }: ViewProps) => {
|
|
|
89
99
|
<Formik
|
|
90
100
|
initialValues={{
|
|
91
101
|
name: exerciseTranslation.name,
|
|
92
|
-
alternativeNames: exerciseTranslation.aliases.map(a => a.alias),
|
|
102
|
+
alternativeNames: exerciseTranslation.aliases.map(a => ({ id: a.id, alias: a.alias })),
|
|
93
103
|
description: exerciseTranslation.description,
|
|
94
104
|
}}
|
|
95
105
|
enableReinitialize
|
|
@@ -114,24 +124,24 @@ export const ExerciseDetailEdit = ({ exerciseId, language }: ViewProps) => {
|
|
|
114
124
|
author: profileQuery.data!.username
|
|
115
125
|
});
|
|
116
126
|
|
|
117
|
-
//
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
// can't directly do, so the old one gets deleted and a new one created)
|
|
127
|
+
// Alias handling
|
|
128
|
+
const aliasOrig = (exerciseTranslation.aliases).map(a => ({ id: a.id, alias: a.alias }));
|
|
129
|
+
const aliasNew = values.alternativeNames ?? [];
|
|
121
130
|
|
|
122
|
-
|
|
123
|
-
const
|
|
124
|
-
const aliasNew = values.alternativeNames;
|
|
125
|
-
const aliasToCreate = aliasNew.filter(x => !aliasOrig.includes(x));
|
|
126
|
-
const aliasToDelete = aliasOrig.filter(x => !aliasNew.includes(x));
|
|
131
|
+
const aliasToCreate = aliasNew.filter(n => !aliasOrig.some(o => o.alias === n.alias));
|
|
132
|
+
const aliasToDelete = aliasOrig.filter(o => !aliasNew.some(n => n.alias === o.alias));
|
|
127
133
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
134
|
+
// Create new aliases
|
|
135
|
+
for (const a of aliasToCreate) {
|
|
136
|
+
await postAlias(translation.id!, a.alias);
|
|
137
|
+
}
|
|
131
138
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
139
|
+
// Delete removed aliases
|
|
140
|
+
for (const a of aliasToDelete) {
|
|
141
|
+
if (a.id) {
|
|
142
|
+
await deleteAlias(a.id);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
135
145
|
|
|
136
146
|
// Notify the user
|
|
137
147
|
setAlertIsVisible(true);
|
|
@@ -161,11 +171,14 @@ export const ExerciseDetailEdit = ({ exerciseId, language }: ViewProps) => {
|
|
|
161
171
|
<PaddingBox />
|
|
162
172
|
</Grid>}
|
|
163
173
|
|
|
174
|
+
<Grid size={12}>
|
|
175
|
+
<Typography variant={'h5'}>{t('translation')}</Typography>
|
|
176
|
+
</Grid>
|
|
164
177
|
<Grid size={6}>
|
|
165
|
-
<Typography variant={'
|
|
178
|
+
<Typography variant={'h6'}>{t('English')}</Typography>
|
|
166
179
|
</Grid>
|
|
167
180
|
<Grid size={6}>
|
|
168
|
-
<Typography variant={'
|
|
181
|
+
<Typography variant={'h6'}>
|
|
169
182
|
{language.nameLong} ({language.nameShort})
|
|
170
183
|
</Typography>
|
|
171
184
|
</Grid>
|
|
@@ -206,6 +219,9 @@ export const ExerciseDetailEdit = ({ exerciseId, language }: ViewProps) => {
|
|
|
206
219
|
<Grid size={12}>
|
|
207
220
|
<PaddingBox />
|
|
208
221
|
</Grid>
|
|
222
|
+
<Grid size={12}>
|
|
223
|
+
<Typography variant={'h5'}>{t('nutrition.others')}</Typography>
|
|
224
|
+
</Grid>
|
|
209
225
|
<Grid size={{ xs: 12, md: 6 }}>
|
|
210
226
|
<EditExerciseCategory exerciseId={exercise.id!} initial={exercise.category.id} />
|
|
211
227
|
</Grid>
|
|
@@ -256,7 +272,7 @@ export const ExerciseDetailEdit = ({ exerciseId, language }: ViewProps) => {
|
|
|
256
272
|
|
|
257
273
|
{/* Images */}
|
|
258
274
|
<PaddingBox />
|
|
259
|
-
<Typography variant={'
|
|
275
|
+
<Typography variant={'h5'}>{t('images')}</Typography>
|
|
260
276
|
<Grid container spacing={2} mt={2}>
|
|
261
277
|
{addImagePermissionQuery.data && <Grid key={'add'} size={{ md: 3 }}>
|
|
262
278
|
<AddImageCard exerciseId={exercise.id!} />
|
|
@@ -275,7 +291,7 @@ export const ExerciseDetailEdit = ({ exerciseId, language }: ViewProps) => {
|
|
|
275
291
|
|
|
276
292
|
{/* Videos */}
|
|
277
293
|
<PaddingBox />
|
|
278
|
-
<Typography variant={'
|
|
294
|
+
<Typography variant={'h5'}>{t('videos')}</Typography>
|
|
279
295
|
<Grid container spacing={2} mt={2}>
|
|
280
296
|
{addVideoPermissionQuery.data
|
|
281
297
|
&& <Grid key={'add'} size={{ md: 3 }}>
|
|
@@ -285,7 +301,8 @@ export const ExerciseDetailEdit = ({ exerciseId, language }: ViewProps) => {
|
|
|
285
301
|
|
|
286
302
|
{exercise.videos.map(video => (
|
|
287
303
|
<Grid key={video.id} size={{ md: 3 }}>
|
|
288
|
-
<VideoEditCard exerciseId={exercise.id!} video={video}
|
|
304
|
+
<VideoEditCard exerciseId={exercise.id!} video={video}
|
|
305
|
+
canDelete={deleteVideoPermissionQuery.data!} />
|
|
289
306
|
</Grid>
|
|
290
307
|
))}
|
|
291
308
|
</Grid>
|
|
@@ -294,9 +311,8 @@ export const ExerciseDetailEdit = ({ exerciseId, language }: ViewProps) => {
|
|
|
294
311
|
{editExercisePermissionQuery.data
|
|
295
312
|
&& <>
|
|
296
313
|
<PaddingBox />
|
|
297
|
-
<Typography variant={'
|
|
314
|
+
<Typography variant={'h5'}>{t('exercises.muscles')}</Typography>
|
|
298
315
|
<Grid container spacing={1} mt={2}>
|
|
299
|
-
|
|
300
316
|
<Grid size={{ xs: 12, md: 6 }}>
|
|
301
317
|
<EditExerciseMuscle
|
|
302
318
|
exerciseId={exercise.id!}
|
|
@@ -305,8 +321,7 @@ export const ExerciseDetailEdit = ({ exerciseId, language }: ViewProps) => {
|
|
|
305
321
|
blocked={secondaryMuscles}
|
|
306
322
|
isMain
|
|
307
323
|
/>
|
|
308
|
-
|
|
309
|
-
<Grid size={{ xs: 12, md: 6 }}>
|
|
324
|
+
<Box sx={{ mt: 2 }} />
|
|
310
325
|
<EditExerciseMuscle
|
|
311
326
|
exerciseId={exercise.id!}
|
|
312
327
|
value={secondaryMuscles}
|
|
@@ -315,7 +330,7 @@ export const ExerciseDetailEdit = ({ exerciseId, language }: ViewProps) => {
|
|
|
315
330
|
isMain={false}
|
|
316
331
|
/>
|
|
317
332
|
</Grid>
|
|
318
|
-
<Grid size={{
|
|
333
|
+
<Grid size={{ xs: 12, md: 6 }}>
|
|
319
334
|
<Grid container>
|
|
320
335
|
<Grid display="flex" justifyContent={"center"} size={6}>
|
|
321
336
|
<MuscleOverview
|
|
@@ -333,6 +348,7 @@ export const ExerciseDetailEdit = ({ exerciseId, language }: ViewProps) => {
|
|
|
333
348
|
</Grid>
|
|
334
349
|
</Grid>
|
|
335
350
|
</Grid>
|
|
351
|
+
|
|
336
352
|
</Grid>
|
|
337
353
|
</>
|
|
338
354
|
}
|
|
@@ -11,6 +11,7 @@ import { MuscleOverview } from "components/Muscles/MuscleOverview";
|
|
|
11
11
|
import { useCanContributeExercises } from "components/User/queries/contribute";
|
|
12
12
|
import React from "react";
|
|
13
13
|
import { useTranslation } from "react-i18next";
|
|
14
|
+
import { dateTimeToLocale } from "utils/date";
|
|
14
15
|
|
|
15
16
|
|
|
16
17
|
const TranslateExerciseBanner = ({ setEditMode }: { setEditMode: (mode: boolean) => void }) => {
|
|
@@ -60,6 +61,15 @@ export const ExerciseDetailView = ({
|
|
|
60
61
|
const currentTranslation = exercise.getTranslation(language);
|
|
61
62
|
const isNewTranslation = language && language.id !== currentTranslation.language;
|
|
62
63
|
|
|
64
|
+
const muscleLegendStyle = {
|
|
65
|
+
height: '17px',
|
|
66
|
+
width: '17px',
|
|
67
|
+
display: 'inline-block',
|
|
68
|
+
verticalAlign: 'middle',
|
|
69
|
+
borderRadius: 4,
|
|
70
|
+
opacity: 0.8,
|
|
71
|
+
};
|
|
72
|
+
|
|
63
73
|
return (
|
|
64
74
|
<Grid container>
|
|
65
75
|
{isNewTranslation
|
|
@@ -92,8 +102,7 @@ export const ExerciseDetailView = ({
|
|
|
92
102
|
</>}
|
|
93
103
|
|
|
94
104
|
<Typography variant="h5">{t("exercises.description")}</Typography>
|
|
95
|
-
<div
|
|
96
|
-
dangerouslySetInnerHTML={{ __html: currentTranslation?.description! }} />
|
|
105
|
+
<div dangerouslySetInnerHTML={{ __html: currentTranslation?.description! }} />
|
|
97
106
|
<PaddingBox />
|
|
98
107
|
|
|
99
108
|
{currentTranslation?.notes.length > 0 && <Typography variant="h5">{t("exercises.notes")}</Typography>}
|
|
@@ -124,7 +133,12 @@ export const ExerciseDetailView = ({
|
|
|
124
133
|
xs: 6,
|
|
125
134
|
md: 3
|
|
126
135
|
}}>
|
|
127
|
-
<
|
|
136
|
+
<div>
|
|
137
|
+
<div style={{ ...muscleLegendStyle, backgroundColor: '#fc0000' }}></div>
|
|
138
|
+
<b style={{ marginLeft: 8, verticalAlign: 'middle' }}>
|
|
139
|
+
{t("exercises.primaryMuscles")}
|
|
140
|
+
</b>
|
|
141
|
+
</div>
|
|
128
142
|
<ul>
|
|
129
143
|
{exercise.muscles.map((m: Muscle) => (
|
|
130
144
|
<li key={m.id}>{m.getName()}</li>
|
|
@@ -152,7 +166,13 @@ export const ExerciseDetailView = ({
|
|
|
152
166
|
xs: 6,
|
|
153
167
|
md: 3
|
|
154
168
|
}}>
|
|
155
|
-
<
|
|
169
|
+
<div>
|
|
170
|
+
<div style={{ ...muscleLegendStyle, backgroundColor: '#f57900' }}></div>
|
|
171
|
+
<b style={{ marginLeft: 8, verticalAlign: 'middle' }}>
|
|
172
|
+
{t("exercises.secondaryMuscles")}
|
|
173
|
+
</b>
|
|
174
|
+
</div>
|
|
175
|
+
|
|
156
176
|
<ul>
|
|
157
177
|
{exercise.musclesSecondary.map((m: Muscle) => (
|
|
158
178
|
<li key={m.id}>{m.getName()}</li>
|
|
@@ -197,19 +217,19 @@ export const ExerciseDetailView = ({
|
|
|
197
217
|
*/}
|
|
198
218
|
|
|
199
219
|
{/* This gallery only displays on medium screens upwards */}
|
|
200
|
-
<
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
220
|
+
<Box sx={{ pl: 1 }}>
|
|
221
|
+
<SideGallery
|
|
222
|
+
mainImage={exercise.mainImage}
|
|
223
|
+
sideImages={exercise.sideImages}
|
|
224
|
+
/>
|
|
225
|
+
<PaddingBox />
|
|
226
|
+
<SideVideoGallery videos={exercise.videos} />
|
|
227
|
+
</Box>
|
|
207
228
|
</Grid>
|
|
208
229
|
|
|
209
230
|
|
|
210
231
|
<Grid order={{ xs: 3 }} size={12}>
|
|
211
232
|
|
|
212
|
-
<Divider />
|
|
213
233
|
<PaddingBox />
|
|
214
234
|
|
|
215
235
|
{variations.length > 0 && <Typography variant={"h5"}>{t('exercises.variations')}</Typography>}
|
|
@@ -230,11 +250,31 @@ export const ExerciseDetailView = ({
|
|
|
230
250
|
)}
|
|
231
251
|
</Grid>
|
|
232
252
|
</Grid>
|
|
233
|
-
<Grid order={{ xs: 4 }} size={12}>
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
253
|
+
<Grid order={{ xs: 4, }} sx={{ mt: 3 }} size={12}>
|
|
254
|
+
|
|
255
|
+
<Divider />
|
|
256
|
+
<Typography
|
|
257
|
+
variant="caption"
|
|
258
|
+
display="block"
|
|
259
|
+
mt={1}
|
|
260
|
+
sx={{ display: "flex", alignItems: "center", gap: 1 }}
|
|
261
|
+
>
|
|
262
|
+
<i className="fa-brands fa-creative-commons" style={{ fontSize: 20 }}></i>
|
|
263
|
+
The content on this page is available under the
|
|
264
|
+
<a href="https://creativecommons.org/licenses/by-sa/4.0/deed">
|
|
265
|
+
CC BY-SA 4 License
|
|
266
|
+
</a>.
|
|
267
|
+
</Typography>
|
|
268
|
+
|
|
269
|
+
<Typography
|
|
270
|
+
variant="caption"
|
|
271
|
+
display="block" mt={1}
|
|
272
|
+
sx={{ display: "flex", alignItems: "center", gap: 1 }}
|
|
273
|
+
>
|
|
274
|
+
<>
|
|
275
|
+
Authors: {exercise.authors.join(", ")}<br />
|
|
276
|
+
Last update: {dateTimeToLocale(exercise.lastUpdateGlobal)}
|
|
277
|
+
</>
|
|
238
278
|
</Typography>
|
|
239
279
|
</Grid>
|
|
240
280
|
</Grid>
|
|
@@ -27,17 +27,13 @@ describe("Test the exercise overview card component", () => {
|
|
|
27
27
|
const category = new Category(10, "Abs");
|
|
28
28
|
const equipment1 = new Equipment(10, "Kettlebell");
|
|
29
29
|
const equipment2 = new Equipment(1, "Test 123");
|
|
30
|
-
const exercise = new Exercise(
|
|
31
|
-
345,
|
|
32
|
-
"c788d643-150a-4ac7-97ef-84643c6419bf",
|
|
33
|
-
category,
|
|
34
|
-
[equipment1, equipment2],
|
|
35
|
-
[]
|
|
36
|
-
|
|
37
|
-
[],
|
|
38
|
-
null,
|
|
39
|
-
[exerciseTranslation1, exerciseTranslation2]
|
|
40
|
-
);
|
|
30
|
+
const exercise = new Exercise({
|
|
31
|
+
id: 345,
|
|
32
|
+
uuid: "c788d643-150a-4ac7-97ef-84643c6419bf",
|
|
33
|
+
category: category,
|
|
34
|
+
equipment: [equipment1, equipment2],
|
|
35
|
+
translations: [exerciseTranslation1, exerciseTranslation2]
|
|
36
|
+
});
|
|
41
37
|
|
|
42
38
|
test("Render the overview card, no language selected -> use english", () => {
|
|
43
39
|
// Act
|
|
@@ -7,6 +7,7 @@ import { MuscleFilter, MuscleFilterDropdown } from "components/Exercises/Filter/
|
|
|
7
7
|
import { NameAutocompleter } from "components/Exercises/Filter/NameAutcompleter";
|
|
8
8
|
import { Category } from "components/Exercises/models/category";
|
|
9
9
|
import { Equipment } from "components/Exercises/models/equipment";
|
|
10
|
+
import { Exercise } from "components/Exercises/models/exercise";
|
|
10
11
|
import { Muscle } from "components/Exercises/models/muscle";
|
|
11
12
|
import { ExerciseGrid } from "components/Exercises/Overview/ExerciseGrid";
|
|
12
13
|
import { ExerciseGridSkeleton } from "components/Exercises/Overview/ExerciseGridLoadingSkeleton";
|
|
@@ -15,7 +16,6 @@ import { useExercisesQuery } from "components/Exercises/queries";
|
|
|
15
16
|
import React, { useContext, useMemo, useState } from "react";
|
|
16
17
|
import { useTranslation } from "react-i18next";
|
|
17
18
|
import { Link, useNavigate } from "react-router-dom";
|
|
18
|
-
import { Exercise } from "components/Exercises/models/exercise";
|
|
19
19
|
import { makeLink, WgerLink } from "utils/url";
|
|
20
20
|
import { ExerciseFiltersContext } from './Filter/ExerciseFiltersContext';
|
|
21
21
|
import { FilterDrawer } from './Filter/FilterDrawer';
|
|
@@ -73,7 +73,7 @@ const NoResultsBanner = () => {
|
|
|
73
73
|
};
|
|
74
74
|
|
|
75
75
|
export const ExerciseOverviewList = () => {
|
|
76
|
-
const
|
|
76
|
+
const exerciseQuery = useExercisesQuery();
|
|
77
77
|
const [t, i18n] = useTranslation();
|
|
78
78
|
const navigate = useNavigate();
|
|
79
79
|
const { selectedCategories, selectedEquipment, selectedMuscles } = useContext(ExerciseFiltersContext);
|
|
@@ -91,7 +91,7 @@ export const ExerciseOverviewList = () => {
|
|
|
91
91
|
};
|
|
92
92
|
|
|
93
93
|
const filteredExercises = useMemo(() => {
|
|
94
|
-
let filteredExercises =
|
|
94
|
+
let filteredExercises = exerciseQuery.data || [];
|
|
95
95
|
|
|
96
96
|
// Filter exercise bases by categories
|
|
97
97
|
if (selectedCategories.length > 0) {
|
|
@@ -123,7 +123,7 @@ export const ExerciseOverviewList = () => {
|
|
|
123
123
|
}
|
|
124
124
|
|
|
125
125
|
return filteredExercises;
|
|
126
|
-
}, [
|
|
126
|
+
}, [exerciseQuery.data, selectedCategories, selectedEquipment, selectedMuscles]);
|
|
127
127
|
|
|
128
128
|
// Should be a multiple of three, since there are three columns in the grid
|
|
129
129
|
const ITEMS_PER_PAGE = 21;
|
|
@@ -140,7 +140,7 @@ export const ExerciseOverviewList = () => {
|
|
|
140
140
|
return;
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
-
navigate(makeLink(WgerLink.EXERCISE_DETAIL, i18n.language, { id: exercise.id }));
|
|
143
|
+
navigate(makeLink(WgerLink.EXERCISE_DETAIL, i18n.language, { id: exercise.id! }));
|
|
144
144
|
};
|
|
145
145
|
|
|
146
146
|
return (
|
|
@@ -251,7 +251,7 @@ export const ExerciseOverviewList = () => {
|
|
|
251
251
|
xs: 12,
|
|
252
252
|
sm: 9
|
|
253
253
|
}}>
|
|
254
|
-
{
|
|
254
|
+
{exerciseQuery.isLoading
|
|
255
255
|
? <ExerciseGridSkeleton />
|
|
256
256
|
: <>
|
|
257
257
|
<ExerciseGrid exercises={paginatedExercises} />
|
|
@@ -15,8 +15,7 @@ export const ExerciseGrid = ({ exercises }: ExerciseGridProps) => {
|
|
|
15
15
|
|
|
16
16
|
const languageQuery = useLanguageQuery();
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
const [t, i18n] = useTranslation();
|
|
18
|
+
const { i18n } = useTranslation();
|
|
20
19
|
|
|
21
20
|
let currentUserLanguage: Language | undefined;
|
|
22
21
|
if (languageQuery.isSuccess) {
|
|
@@ -1,37 +1,94 @@
|
|
|
1
|
-
import { Autocomplete, Chip, TextField } from "@mui/material";
|
|
1
|
+
import { Autocomplete, Chip, InputAdornment, TextField } from "@mui/material";
|
|
2
2
|
import { useField } from "formik";
|
|
3
3
|
import React from "react";
|
|
4
4
|
import { useTranslation } from "react-i18next";
|
|
5
5
|
|
|
6
|
+
type AliasItem = { id?: number; alias: string };
|
|
7
|
+
|
|
6
8
|
export function ExerciseAliases(props: { fieldName: string }) {
|
|
7
9
|
const [t] = useTranslation();
|
|
8
|
-
const [field, meta, helpers] = useField(props.fieldName);
|
|
10
|
+
const [field, meta, helpers] = useField<AliasItem[]>(props.fieldName);
|
|
11
|
+
|
|
12
|
+
const normalize = (items: (AliasItem | string)[] | null | undefined): AliasItem[] =>
|
|
13
|
+
(items || []).map(item =>
|
|
14
|
+
typeof item === "string" ? { alias: item } : ("alias" in item ? (item as AliasItem) : { alias: String(item) })
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Extract a human-readable error string from the Yup alias validator, which
|
|
19
|
+
* returns a list of errors.
|
|
20
|
+
*/
|
|
21
|
+
const formatError = (err: unknown): string | undefined => {
|
|
22
|
+
if (!err) return undefined;
|
|
23
|
+
if (typeof err === "string") return err;
|
|
24
|
+
|
|
25
|
+
if (typeof err === "object") {
|
|
26
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
27
|
+
const o = err as any;
|
|
28
|
+
if (typeof o.alias === "string") return o.alias;
|
|
29
|
+
if (typeof o.message === "string") return o.message;
|
|
30
|
+
|
|
31
|
+
for (const k of Object.keys(o)) {
|
|
32
|
+
const v = o[k];
|
|
33
|
+
if (typeof v === "string") return v;
|
|
34
|
+
if (v && typeof v === "object") {
|
|
35
|
+
if (typeof v.alias === "string") return v.alias;
|
|
36
|
+
if (typeof v.message === "string") return v.message;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return String(err);
|
|
42
|
+
};
|
|
9
43
|
|
|
10
44
|
return <Autocomplete
|
|
11
45
|
multiple
|
|
12
46
|
freeSolo
|
|
13
47
|
id={props.fieldName}
|
|
14
|
-
value={field.value}
|
|
15
|
-
options={
|
|
16
|
-
|
|
17
|
-
|
|
48
|
+
value={field.value || []}
|
|
49
|
+
options={[]}
|
|
50
|
+
getOptionLabel={(opt) => (typeof opt === "string" ? opt : opt.alias)}
|
|
51
|
+
isOptionEqualToValue={(option, value) => option.alias === value.alias && (option.id === value.id || option.id === undefined || value.id === undefined)}
|
|
52
|
+
onChange={(_, newValue) => {
|
|
53
|
+
helpers.setValue(normalize(newValue));
|
|
18
54
|
}}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
55
|
+
onBlur={field.onBlur}
|
|
56
|
+
renderInput={(params) => {
|
|
57
|
+
const chips = (field.value || []).map((option, index) => (
|
|
58
|
+
<Chip
|
|
59
|
+
label={option.alias}
|
|
60
|
+
onDelete={() => {
|
|
61
|
+
const newVal = [...(field.value || [])];
|
|
62
|
+
newVal.splice(index, 1);
|
|
63
|
+
helpers.setValue(newVal);
|
|
64
|
+
}}
|
|
65
|
+
key={option.id ?? `${option.alias}-${index}`}
|
|
66
|
+
/>
|
|
22
67
|
));
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<TextField
|
|
71
|
+
{...params}
|
|
72
|
+
id="exerciseAliases"
|
|
73
|
+
variant="standard"
|
|
74
|
+
label={t("exercises.alternativeNames")}
|
|
75
|
+
error={meta.touched && Boolean(meta.error)}
|
|
76
|
+
helperText={meta.touched ? formatError(meta.error) : undefined}
|
|
77
|
+
slotProps={{
|
|
78
|
+
input: {
|
|
79
|
+
startAdornment: (
|
|
80
|
+
<InputAdornment
|
|
81
|
+
position="start"
|
|
82
|
+
sx={{ display: "flex", gap: 0.5, alignItems: "center" }}
|
|
83
|
+
>
|
|
84
|
+
{chips}
|
|
85
|
+
{/*{params.InputProps?.startAdornment}*/}
|
|
86
|
+
</InputAdornment>
|
|
87
|
+
),
|
|
88
|
+
},
|
|
89
|
+
}}
|
|
90
|
+
/>
|
|
91
|
+
);
|
|
23
92
|
}}
|
|
24
|
-
onBlur={field.onBlur}
|
|
25
|
-
renderInput={params => (
|
|
26
|
-
<TextField
|
|
27
|
-
{...params}
|
|
28
|
-
id="exerciseAliases"
|
|
29
|
-
variant="standard"
|
|
30
|
-
label={t("exercises.alternativeNames")}
|
|
31
|
-
error={meta.touched && Boolean(meta.error)}
|
|
32
|
-
helperText={meta.touched && meta.error}
|
|
33
|
-
value={field.value}
|
|
34
|
-
/>
|
|
35
|
-
)}
|
|
36
93
|
/>;
|
|
37
94
|
}
|
|
@@ -20,10 +20,13 @@ export const alternativeNameValidator = () => yup
|
|
|
20
20
|
.ensure()
|
|
21
21
|
.compact()
|
|
22
22
|
.of(
|
|
23
|
-
yup
|
|
24
|
-
.
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
yup.object({
|
|
24
|
+
id: yup.number().nullable(),
|
|
25
|
+
alias: yup.string()
|
|
26
|
+
.min(MIN_CHAR_NAME, i18n.t("forms.minLength", { 'chars': MIN_CHAR_NAME }))
|
|
27
|
+
.max(MAX_CHAR_NAME, i18n.t("forms.maxLength", { 'chars': MAX_CHAR_NAME }))
|
|
28
|
+
.required()
|
|
29
|
+
})
|
|
27
30
|
);
|
|
28
31
|
|
|
29
32
|
export const descriptionValidator = () => yup
|