@wger-project/react-components 25.10.16 → 25.10.24

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.
Files changed (28) hide show
  1. package/README.md +1 -1
  2. package/build/locales/de/translation.json +334 -334
  3. package/build/locales/en/translation.json +12 -2
  4. package/build/locales/fr/translation.json +347 -337
  5. package/build/locales/hr/translation.json +344 -244
  6. package/build/locales/it/translation.json +333 -333
  7. package/build/locales/ko/translation.json +327 -327
  8. package/build/locales/pt_PT/translation.json +330 -330
  9. package/build/locales/sl/translation.json +321 -321
  10. package/build/locales/ta/translation.json +325 -325
  11. package/build/locales/uk/translation.json +344 -334
  12. package/build/locales/zh_Hans/translation.json +332 -332
  13. package/build/main.js +137 -138
  14. package/build/main.js.map +1 -1
  15. package/package.json +1 -1
  16. package/src/components/Nutrition/widgets/IngredientAutcompleter.tsx +1 -1
  17. package/src/components/Nutrition/widgets/forms/NutritionDiaryEntryForm.tsx +15 -4
  18. package/src/components/WorkoutRoutines/Detail/RoutineDetailsTable.tsx +3 -3
  19. package/src/components/WorkoutRoutines/models/Day.ts +16 -6
  20. package/src/components/WorkoutRoutines/models/SlotEntry.ts +7 -2
  21. package/src/components/WorkoutRoutines/widgets/DayDetails.tsx +1 -1
  22. package/src/components/WorkoutRoutines/widgets/RoutineDetailsCard.tsx +8 -11
  23. package/src/components/WorkoutRoutines/widgets/forms/DayForm.tsx +21 -9
  24. package/src/components/WorkoutRoutines/widgets/forms/DayTypeSelect.tsx +66 -0
  25. package/src/components/WorkoutRoutines/widgets/forms/SessionForm.test.tsx +0 -2
  26. package/src/components/WorkoutRoutines/widgets/forms/SlotEntryForm.test.tsx +12 -10
  27. package/src/components/WorkoutRoutines/widgets/forms/SlotEntryForm.tsx +2 -1
  28. package/src/components/WorkoutRoutines/widgets/forms/SlotForm.tsx +1 -1
package/package.json CHANGED
@@ -11,7 +11,7 @@
11
11
  "exports": {
12
12
  ".": "./build/index.js"
13
13
  },
14
- "version": "25.10.16",
14
+ "version": "25.10.24",
15
15
  "repository": "https://github.com/wger-project/react",
16
16
  "type": "module",
17
17
  "publishConfig": {
@@ -22,7 +22,7 @@ import { searchIngredient } from "services";
22
22
  import { LANGUAGE_SHORT_ENGLISH } from "utils/consts";
23
23
 
24
24
  type IngredientAutocompleterProps = {
25
- callback: () => void;
25
+ callback: (ingredient: Ingredient | null) => void;
26
26
  initialIngredient?: Ingredient | null;
27
27
  };
28
28
 
@@ -39,6 +39,8 @@ export const NutritionDiaryEntryForm = ({ planId, entry, mealId, meals, closeFn
39
39
  .min(1, t('forms.minValue', { value: '1' })),
40
40
  ingredient: yup
41
41
  .number()
42
+ .nullable()
43
+ .moreThan(0, t('forms.fieldRequired'))
42
44
  .required(t('forms.fieldRequired')),
43
45
  datetime: yup
44
46
  .date()
@@ -51,7 +53,7 @@ export const NutritionDiaryEntryForm = ({ planId, entry, mealId, meals, closeFn
51
53
  initialValues={{
52
54
  datetime: new Date(),
53
55
  amount: 0,
54
- ingredient: 0,
56
+ ingredient: null,
55
57
  }}
56
58
  validationSchema={validationSchema}
57
59
  onSubmit={async (values) => {
@@ -66,7 +68,7 @@ export const NutritionDiaryEntryForm = ({ planId, entry, mealId, meals, closeFn
66
68
  planId: planId,
67
69
  amount: newAmount,
68
70
  datetime: values.datetime,
69
- ingredientId: values.ingredient
71
+ ingredientId: values.ingredient!
70
72
  });
71
73
  editDiaryQuery.mutate(newDiaryEntry);
72
74
  } else {
@@ -75,7 +77,7 @@ export const NutritionDiaryEntryForm = ({ planId, entry, mealId, meals, closeFn
75
77
  planId: planId,
76
78
  amount: newAmount,
77
79
  datetime: values.datetime,
78
- ingredientId: values.ingredient,
80
+ ingredientId: values.ingredient!,
79
81
  mealId: selectedMeal,
80
82
  }));
81
83
  }
@@ -90,7 +92,16 @@ export const NutritionDiaryEntryForm = ({ planId, entry, mealId, meals, closeFn
90
92
  <Form>
91
93
  <Stack spacing={2}>
92
94
  <IngredientAutocompleter
93
- callback={(value: Ingredient | null) => formik.setFieldValue('ingredient', value?.id)} />
95
+ callback={(value: Ingredient | null) => {
96
+ formik.setFieldTouched('ingredient', true);
97
+ formik.setFieldValue('ingredient', value?.id ?? null);
98
+ }}
99
+ />
100
+ {formik.touched.ingredient && formik.errors.ingredient && (
101
+ <div style={{ color: 'crimson', fontSize: '0.7rem', marginLeft: '12px' }}>
102
+ {formik.errors.ingredient}
103
+ </div>
104
+ )}
94
105
  <TextField
95
106
  fullWidth
96
107
  id="amount"
@@ -212,7 +212,7 @@ export const RoutineTable = (props: {
212
212
  borderBottomWidth: showLogs ? 0 : null
213
213
  }}>{slotEntry.exercise?.getTranslation(language).name}</TableCell>
214
214
  {iterations.map((iteration) => {
215
- const setConfig = props.routine.getSetConfigData(day.id, iteration, slotEntry.id);
215
+ const setConfig = props.routine.getSetConfigData(day.id!, iteration, slotEntry.id!);
216
216
 
217
217
  return <React.Fragment key={iteration}>
218
218
  <TableCell align={'center'} sx={sx}>
@@ -268,7 +268,7 @@ export const RoutineTable = (props: {
268
268
  <small>{t('nutrition.logged')}</small>
269
269
  </TableCell>
270
270
  {iterations.map((iteration) => {
271
- const setConfig = props.routine.getSetConfigData(day.id, iteration, slotEntry.id);
271
+ const setConfig = props.routine.getSetConfigData(day.id!, iteration, slotEntry.id!);
272
272
  const iterationLogs = groupedLogs[iteration] ?? [];
273
273
 
274
274
  const logs = iterationLogs.filter((log) => log.slotEntryId === slotEntry.id);
@@ -330,7 +330,7 @@ export const RoutineTable = (props: {
330
330
  sx={{ backgroundColor: theme.palette.action.hover }}
331
331
  className={classes.stickyColumn}
332
332
  >
333
- <b>{day.getDisplayName()}</b>
333
+ <b>{day.displayName}</b>
334
334
  </TableCell>
335
335
  <TableCell sx={{ backgroundColor: theme.palette.action.hover }} colSpan={5 * iterations.length}>
336
336
  </TableCell>
@@ -4,6 +4,8 @@ import { Slot } from "components/WorkoutRoutines/models/Slot";
4
4
  import i18n from 'i18next';
5
5
  import { Adapter } from "utils/Adapter";
6
6
 
7
+ export type DayType = 'custom' | 'enom' | 'amrap' | 'hiit' | 'tabata' | 'edt' | 'rft' | 'afap';
8
+
7
9
  interface DayConstructorParams {
8
10
  id?: number;
9
11
  routineId: number | null;
@@ -12,7 +14,7 @@ interface DayConstructorParams {
12
14
  description?: string;
13
15
  isRest?: boolean;
14
16
  needLogsToAdvance?: boolean;
15
- type?: 'custom' | 'enom' | 'amrap' | 'hiit' | 'tabata' | 'edt' | 'rft' | 'afap';
17
+ type?: DayType;
16
18
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
17
19
  config?: any | null;
18
20
  slots?: Slot[];
@@ -30,7 +32,7 @@ export class Day {
30
32
  description: string;
31
33
  isRest: boolean;
32
34
  needLogsToAdvance: boolean;
33
- type: 'custom' | 'enom' | 'amrap' | 'hiit' | 'tabata' | 'edt' | 'rft' | 'afap';
35
+ type: DayType;
34
36
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
35
37
  config: any | null;
36
38
 
@@ -53,6 +55,14 @@ export class Day {
53
55
  return this.type !== 'custom';
54
56
  }
55
57
 
58
+ public get displayNameWithType(): string {
59
+ return this.isSpecialType ? `${this.type.toUpperCase()} - ${this.displayName}` : this.displayName;
60
+ }
61
+
62
+ public get displayName(): string {
63
+ return this.isRest ? i18n.t('routines.restDay') : this.name;
64
+ }
65
+
56
66
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
57
67
  static fromJson(json: any): Day {
58
68
  return adapter.fromJson(json);
@@ -73,9 +83,6 @@ export class Day {
73
83
  });
74
84
  }
75
85
 
76
- public getDisplayName(): string {
77
- return this.isRest ? i18n.t('routines.restDay') : this.name;
78
- }
79
86
 
80
87
  toJson() {
81
88
  return adapter.toJson(this);
@@ -83,7 +90,10 @@ export class Day {
83
90
 
84
91
  }
85
92
 
86
- export const getDayName = (day: Day | null): string => day === null || day.isRest ? i18n.t('routines.restDay') : day.getDisplayName();
93
+ /*
94
+ * Returns the display name of the day, or "Rest day" if it's a rest day or null
95
+ */
96
+ export const getDayName = (day: Day | null): string => day === null || day.isRest ? i18n.t('routines.restDay') : day.displayNameWithType;
87
97
 
88
98
 
89
99
  class DayAdapter implements Adapter<Day> {
@@ -133,11 +133,16 @@ export class SlotEntry {
133
133
  order: overrides?.order ?? other.order,
134
134
  comment: overrides?.comment ?? other.comment,
135
135
 
136
- repetitionUnit: overrides?.repetitionUnit ?? (other.repetitionUnit ?? undefined),
136
+ // NOTE: temporarily commented out to avoid issues. The constructor will see the
137
+ // unit objects and set the ids accordingly, ignoring the explicit IDs. This is
138
+ // probably OK since we always load the whole routine again after an edit, but
139
+ // it is a bit ugly.
140
+
141
+ // repetitionUnit: overrides?.repetitionUnit ?? (other.repetitionUnit ?? undefined),
137
142
  repetitionUnitId: overrides?.repetitionUnitId ?? other.repetitionUnitId,
138
143
  repetitionRounding: overrides?.repetitionRounding ?? other.repetitionRounding,
139
144
 
140
- weightUnit: overrides?.weightUnit ?? (other.weightUnit ?? undefined),
145
+ // weightUnit: overrides?.weightUnit ?? (other.weightUnit ?? undefined),
141
146
  weightUnitId: overrides?.weightUnitId ?? other.weightUnitId,
142
147
  weightRounding: overrides?.weightRounding ?? other.weightRounding,
143
148
  });
@@ -135,7 +135,7 @@ export const DayDragAndDropGrid = (props: {
135
135
  provided.draggableProps.style ?? {}
136
136
  )}
137
137
 
138
- label={day.getDisplayName()}
138
+ label={day.displayName}
139
139
  value={day.id}
140
140
  icon={<DragIndicatorIcon />}
141
141
  iconPosition="start"
@@ -111,10 +111,7 @@ export function SetConfigDataDetails(props: {
111
111
  }
112
112
 
113
113
 
114
- function SlotDataList(props: {
115
- slotData: SlotData,
116
- index: number,
117
- }) {
114
+ function SlotDataList(props: { slotData: SlotData }) {
118
115
  return (
119
116
  <Grid
120
117
  container
@@ -163,6 +160,9 @@ export const DayDetailsCard = (props: { dayData: RoutineDayData, routineId: numb
163
160
  const theme = useTheme();
164
161
  const [t, i18n] = useTranslation();
165
162
 
163
+ const isToday = isSameDay(props.dayData.date, new Date());
164
+ const subheader = <Typography sx={{ whiteSpace: 'pre-line' }}>{props.dayData.day?.description}</Typography>;
165
+
166
166
  return (
167
167
  <Card sx={{ minWidth: 275 }}>
168
168
  <CardHeader
@@ -178,19 +178,16 @@ export const DayDetailsCard = (props: { dayData: RoutineDayData, routineId: numb
178
178
  <Addchart />
179
179
  </IconButton>
180
180
  </Tooltip>}
181
- title={getDayName(props.dayData.day)}
182
- avatar={isSameDay(props.dayData.date, new Date()) ? <TodayIcon /> : undefined}
183
- subheader={<Typography sx={{ whiteSpace: 'pre-line' }}>{props.dayData.day?.description}</Typography>}
181
+ title={<Typography variant={"h5"}>{getDayName(props.dayData.day)}</Typography>}
182
+ avatar={isToday ? <TodayIcon /> : null}
183
+ subheader={subheader}
184
184
  />
185
185
  {props.dayData.slots.length > 0 && <CardContent sx={{ padding: 0, marginBottom: 0 }}>
186
186
  <Stack>
187
187
  {props.dayData.slots.map((slotData, index) => (
188
188
  <div key={index}>
189
189
  <Box padding={1}>
190
- <SlotDataList
191
- slotData={slotData}
192
- index={index}
193
- />
190
+ <SlotDataList slotData={slotData} />
194
191
  </Box>
195
192
  <Divider />
196
193
  </div>
@@ -15,8 +15,9 @@ import LoadingButton from "@mui/material/Button";
15
15
  import Grid from '@mui/material/Grid';
16
16
  import { WgerTextField } from "components/Common/forms/WgerTextField";
17
17
  import { DeleteConfirmationModal } from "components/Core/Modals/DeleteConfirmationModal";
18
- import { Day } from "components/WorkoutRoutines/models/Day";
18
+ import { Day, DayType } from "components/WorkoutRoutines/models/Day";
19
19
  import { useDeleteDayQuery, useEditDayQuery } from "components/WorkoutRoutines/queries";
20
+ import { DayTypeSelect } from "components/WorkoutRoutines/widgets/forms/DayTypeSelect";
20
21
  import { DefaultRoundingMenu } from "components/WorkoutRoutines/widgets/forms/RoutineForm";
21
22
  import { Form, Formik } from "formik";
22
23
  import React, { useState } from "react";
@@ -74,21 +75,25 @@ export const DayForm = (props: {
74
75
  description: Yup.string()
75
76
  .max(descriptionMaxLength, t('forms.maxLength', { chars: descriptionMaxLength })),
76
77
  isRest: Yup.boolean(),
77
- needsLogsToAdvance: Yup.boolean()
78
+ needsLogsToAdvance: Yup.boolean(),
79
+ type: Yup.string(),
78
80
  });
79
81
 
80
82
  const handleSubmit = (values: Partial<{
81
83
  name: string,
82
84
  description: string,
83
85
  isRest: boolean,
84
- needsLogsToAdvance: boolean
85
- }>) => editDayQuery.mutate(Day.clone(
86
+ needsLogsToAdvance: boolean,
87
+ type: string
88
+ }>) =>
89
+ editDayQuery.mutate(Day.clone(
86
90
  props.day,
87
91
  {
88
92
  ...(values.name !== undefined && { name: values.name }),
89
93
  ...(values.description !== undefined && { description: values.description }),
90
94
  ...({ isRest: values.isRest }),
91
95
  ...(values.needsLogsToAdvance !== undefined && { needLogsToAdvance: values.needsLogsToAdvance }),
96
+ ...(values.type !== undefined && { type: values.type as DayType }),
92
97
  })
93
98
  );
94
99
 
@@ -98,7 +103,8 @@ export const DayForm = (props: {
98
103
  name: props.day.name,
99
104
  description: props.day.description,
100
105
  isRest: props.day.isRest,
101
- needsLogsToAdvance: props.day.needLogsToAdvance
106
+ needsLogsToAdvance: props.day.needLogsToAdvance,
107
+ type: props.day.type,
102
108
  }}
103
109
  validationSchema={validationSchema}
104
110
  onSubmit={(values, { setSubmitting }) => {
@@ -110,19 +116,25 @@ export const DayForm = (props: {
110
116
  {(formik) => (
111
117
  <Form>
112
118
  <Grid container spacing={2}>
113
- <Grid size={{ xs: 12, sm: 6 }}>
119
+ <Grid size={{ xs: 12, sm: 6, md: 4 }}>
114
120
  <WgerTextField
115
121
  fieldName="name"
116
122
  title="Name"
117
123
  fieldProps={{ disabled: isRestDay }}
118
124
  />
119
125
  </Grid>
120
- <Grid size={{ xs: 6, sm: 2 }}>
126
+ <Grid size={{ xs: 12, sm: 6, md: 3 }}>
127
+ <DayTypeSelect
128
+ fieldName="type"
129
+ title="Type"
130
+ />
131
+ </Grid>
132
+ <Grid size={{ xs: 12, sm: 6, md: 2 }}>
121
133
  <FormControlLabel
122
134
  control={<Switch checked={isRestDay} onChange={handleRestDayChange} />}
123
135
  label={t('routines.restDay')} />
124
136
  </Grid>
125
- <Grid size={{ xs: 6, sm: 4 }}>
137
+ <Grid size={{ xs: 12, sm: 4, md: 3 }}>
126
138
  <FormControlLabel
127
139
  disabled={isRestDay}
128
140
  control={<Switch
@@ -188,7 +200,7 @@ export const DayForm = (props: {
188
200
  </Dialog>
189
201
 
190
202
  <DeleteConfirmationModal
191
- title={t('deleteConfirmation', { name: props.day.getDisplayName() })}
203
+ title={t('deleteConfirmation', { name: props.day.displayName })}
192
204
  message={t('routines.deleteDayConfirmation')}
193
205
  isOpen={openDeleteDialog}
194
206
  closeFn={handleCancelDeleteDay}
@@ -0,0 +1,66 @@
1
+ import MenuItem from "@mui/material/MenuItem";
2
+ import TextField from "@mui/material/TextField";
3
+ import { useField } from "formik";
4
+ import { useTranslation } from "react-i18next";
5
+
6
+ interface DayTypeSelectProps {
7
+ fieldName: string,
8
+ title: string,
9
+ }
10
+
11
+ export const DayTypeSelect = (props: DayTypeSelectProps) => {
12
+ const { t } = useTranslation();
13
+ const [field, meta] = useField(props.fieldName);
14
+
15
+ const options = [
16
+ {
17
+ value: 'custom',
18
+ label: t('routines.day.custom'),
19
+ },
20
+ {
21
+ value: 'enom',
22
+ label: t('routines.day.enom'),
23
+ },
24
+ {
25
+ value: 'amrap',
26
+ label: t('routines.day.amrap'),
27
+ },
28
+ {
29
+ value: 'hiit',
30
+ label: t('routines.day.hiit'),
31
+ },
32
+ {
33
+ value: 'tabata',
34
+ label: t('routines.day.tabata'),
35
+ },
36
+ {
37
+ value: 'edt',
38
+ label: t('routines.day.edt'),
39
+ },
40
+ {
41
+ value: 'rft',
42
+ label: t('routines.day.rft'),
43
+ },
44
+ {
45
+ value: 'afap',
46
+ label: t('routines.day.afap'),
47
+ }
48
+ ] as const;
49
+
50
+
51
+ return <>
52
+ <TextField
53
+ fullWidth
54
+ select
55
+ label={t('routines.set.type')}
56
+ variant="standard"
57
+ {...field}
58
+ >
59
+ {options!.map((option) => (
60
+ <MenuItem key={option.value} value={option.value}>
61
+ {option.value.toUpperCase()} - <small>{option.label}</small>
62
+ </MenuItem>
63
+ ))}
64
+ </TextField>
65
+ </>;
66
+ };
@@ -120,8 +120,6 @@ describe('SessionForm', () => {
120
120
  </BrowserRouter>
121
121
  );
122
122
 
123
- screen.logTestingPlaygroundURL();
124
-
125
123
  // Assert
126
124
  await waitFor(() => {
127
125
  // screen.logTestingPlaygroundURL();
@@ -40,16 +40,18 @@ describe('SlotEntryTypeField', () => {
40
40
  const dropdown = screen.getByRole('combobox', { name: 'routines.set.type' });
41
41
  await user.click(dropdown);
42
42
 
43
- expect(screen.queryAllByText('routines.set.normalSet')).toHaveLength(2); // One in the options menu, one in the selected value
44
- expect(screen.getByText('routines.set.dropSet')).toBeInTheDocument();
45
- expect(screen.getByText('routines.set.myo')).toBeInTheDocument();
46
- expect(screen.getByText('routines.set.partial')).toBeInTheDocument();
47
- expect(screen.getByText('routines.set.forced')).toBeInTheDocument();
48
- expect(screen.getByText('routines.set.tut')).toBeInTheDocument();
49
- expect(screen.getByText('routines.set.iso')).toBeInTheDocument();
50
- expect(screen.getByText('routines.set.jump')).toBeInTheDocument();
51
-
52
- const myoOption = screen.getByRole('option', { name: 'routines.set.myo' });
43
+ // One in the options menu, one in the selected value
44
+ expect(screen.queryAllByText(/routines\.set\.normalSet/)).toHaveLength(2);
45
+ expect(screen.getByText(/routines\.set\.dropSet/)).toBeInTheDocument();
46
+ expect(screen.getByText(/routines\.set\.myo/)).toBeInTheDocument();
47
+ expect(screen.getByText(/routines\.set\.partial/)).toBeInTheDocument();
48
+ expect(screen.getByText(/routines\.set\.forced/)).toBeInTheDocument();
49
+ expect(screen.getByText(/routines\.set\.tut/)).toBeInTheDocument();
50
+ expect(screen.getByText(/routines\.set\.iso/)).toBeInTheDocument();
51
+ expect(screen.getByText(/routines\.set\.jump/)).toBeInTheDocument();
52
+
53
+
54
+ const myoOption = screen.getByRole('option', { name: /routines\.set\.myo/ });
53
55
  await user.click(myoOption);
54
56
  expect(mockEditSlotEntry).toHaveBeenCalledWith(SlotEntry.clone(testDayLegs.slots[0].entries[0], { type: 'myo' }));
55
57
  });
@@ -55,6 +55,7 @@ export const SlotEntryTypeField = (props: { slotEntry: SlotEntry, routineId: num
55
55
  editQuery.mutate(SlotEntry.clone(props.slotEntry, { type: newValue as SlotEntryType }));
56
56
  };
57
57
 
58
+
58
59
  return <>
59
60
  <TextField
60
61
  fullWidth
@@ -67,7 +68,7 @@ export const SlotEntryTypeField = (props: { slotEntry: SlotEntry, routineId: num
67
68
  >
68
69
  {options!.map((option) => (
69
70
  <MenuItem key={option.value} value={option.value}>
70
- {option.label}
71
+ {option.value.toUpperCase()} - {option.label}
71
72
  </MenuItem>
72
73
  ))}
73
74
  </TextField>
@@ -26,7 +26,7 @@ export const SlotForm = (props: { slot: Slot, routineId: number }) => {
26
26
  size={"small"}
27
27
  value={slotComment}
28
28
  disabled={editSlotQuery.isPending}
29
- onChange={(e) => handleChange(e.target.value)}
29
+ onChange={(e) => setSlotComment(e.target.value)}
30
30
  onBlur={handleBlur}
31
31
  slotProps={{
32
32
  input: { endAdornment: editSlotQuery.isPending && <LoadingProgressIcon /> }