@wger-project/react-components 26.1.18 → 26.2.26

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/package.json CHANGED
@@ -11,7 +11,7 @@
11
11
  "exports": {
12
12
  ".": "./build/index.js"
13
13
  },
14
- "version": "26.1.18",
14
+ "version": "26.2.26",
15
15
  "repository": "https://github.com/wger-project/react",
16
16
  "type": "module",
17
17
  "publishConfig": {
@@ -22,12 +22,12 @@
22
22
  "@emotion/react": "^11.14.0",
23
23
  "@emotion/styled": "^11.14.1",
24
24
  "@hello-pangea/dnd": "^18.0.1",
25
- "@mui/icons-material": "^7.3.5",
25
+ "@mui/icons-material": "^7.3.7",
26
26
  "@mui/lab": "^7.0.1-beta.18",
27
27
  "@mui/material": "^7.3.4",
28
28
  "@mui/system": "^7.3.3",
29
- "@mui/x-data-grid": "^8.18.0",
30
- "@mui/x-date-pickers": "^8.18.0",
29
+ "@mui/x-data-grid": "^8.26.0",
30
+ "@mui/x-date-pickers": "^8.26.0",
31
31
  "@tanstack/react-query": "^5.90.2",
32
32
  "@vitejs/plugin-react": "^5.1.1",
33
33
  "axios": "^1.12.2",
@@ -38,12 +38,12 @@
38
38
  "i18next-http-backend": "^3.0.2",
39
39
  "luxon": "^3.7.2",
40
40
  "react": "^19.2.0",
41
- "react-dom": "^19.2.0",
42
- "react-grid-layout": "^1.5.2",
41
+ "react-dom": "^19.2.4",
42
+ "react-grid-layout": "^2.2.2",
43
43
  "react-i18next": "^16.3.3",
44
- "react-is": "^19.2.0",
44
+ "react-is": "^19.2.4",
45
45
  "react-responsive": "^10.0.1",
46
- "react-router-dom": "^7.9.4",
46
+ "react-router-dom": "^7.12.0",
47
47
  "react-simple-wysiwyg": "^3.4.1",
48
48
  "react-slick": "^0.31.0",
49
49
  "recharts": "^3.4.1",
@@ -67,26 +67,25 @@
67
67
  "@types/node": "^22.18.9",
68
68
  "@types/react": "^19.2.5",
69
69
  "@types/react-dom": "^19.2.3",
70
- "@types/react-grid-layout": "^1.3.5",
71
70
  "@types/react-is": "^19.2.0",
72
71
  "@types/react-slick": "^0.23.13",
73
72
  "@types/slug": "^5.0.9",
74
73
  "@typescript-eslint/eslint-plugin": "^8.47.0",
75
74
  "@typescript-eslint/parser": "^8.47.0",
76
- "eslint": "^9.37.0",
75
+ "eslint": "^9.39.2",
77
76
  "eslint-plugin-import": "^2.32.0",
78
77
  "eslint-plugin-jsx-a11y": "^6.10.2",
79
78
  "eslint-plugin-react": "^7.37.5",
80
79
  "eslint-plugin-react-hooks": "^7.0.0",
81
- "i18next-parser": "^9.3.0",
80
+ "i18next-cli": "^1.46.0",
82
81
  "jest": "^30.2.0",
83
82
  "jest-environment-jsdom": "^30.2.0",
84
83
  "jsdom": "^27.2.0",
85
84
  "ts-jest": "^29.4.5",
86
85
  "typescript-eslint": "^8.46.0",
87
- "vite": "^7.2.2",
86
+ "vite": "^7.3.1",
88
87
  "vite-plugin-eslint": "^1.8.1",
89
- "vitest": "^3.2.4",
88
+ "vitest": "^4.0.18",
90
89
  "webpack-bundle-analyzer": "^4.10.2"
91
90
  },
92
91
  "scripts": {
@@ -54,8 +54,9 @@ describe('loadDashboardState migration', () => {
54
54
  expect(res).not.toBeNull();
55
55
  expect(res!.selectedWidgetIds).not.toContain('foo');
56
56
  expect(res!.selectedWidgetIds).toContain('routine');
57
- expect(res!.layouts!.lg.length).toEqual(1);
58
- expect(res!.layouts!.lg[0].i).toEqual('routine');
57
+ const lg = res?.layouts?.lg ?? [];
58
+ expect(lg.length).toEqual(1);
59
+ expect(lg[0]?.i).toEqual('routine');
59
60
  });
60
61
 
61
62
  test('migrates old top-level structure', () => {
@@ -10,13 +10,11 @@ import { RoutineCard } from "components/Dashboard/RoutineCard";
10
10
  import { TrophiesCard } from "components/Dashboard/TrophiesCard";
11
11
  import { WeightCard } from "components/Dashboard/WeightCard";
12
12
  import React, { useCallback, useEffect, useMemo, useState } from "react";
13
- import { Layout, Layouts, Responsive, WidthProvider } from "react-grid-layout";
13
+ import { Layout, LayoutItem, Responsive, ResponsiveLayouts, useContainerWidth, } from "react-grid-layout";
14
14
  import "react-grid-layout/css/styles.css";
15
15
  import "react-resizable/css/styles.css";
16
16
  import { useTranslation } from "react-i18next";
17
17
 
18
- const ResponsiveGridLayout = WidthProvider(Responsive);
19
-
20
18
  const DASHBOARD_STORAGE_KEY = "dashboard-state";
21
19
 
22
20
  const VERSION = 1;
@@ -25,7 +23,7 @@ type DashboardState = {
25
23
  version: number;
26
24
  selectedWidgetIds: string[];
27
25
  hiddenWidgetIds: string[];
28
- layouts: Layouts | null;
26
+ layouts: ResponsiveLayouts | null;
29
27
  };
30
28
 
31
29
  const BREAKPOINTS = ['lg', 'md', 'sm', 'xs'] as const;
@@ -143,9 +141,11 @@ export const loadDashboardState = (): DashboardState | null => {
143
141
 
144
142
  // -> remove unknown ids from the layout
145
143
  for (const bp of BREAKPOINTS) {
146
- const arr = (out.layouts as Layouts)[bp] as Layout[] | undefined;
144
+ const arr = (out.layouts as ResponsiveLayouts)[bp] as Layout | undefined;
147
145
  if (Array.isArray(arr)) {
148
- (out.layouts as Layouts)[bp] = arr.filter((item: Layout) => item && allowedWidgets.has(String(item.i)));
146
+ (out.layouts as ResponsiveLayouts)[bp] = arr.filter(
147
+ (item: LayoutItem) => item && allowedWidgets.has(String(item.i))
148
+ );
149
149
  }
150
150
  }
151
151
 
@@ -191,16 +191,16 @@ const migrateOldDashboardState = (parsedAny: any): DashboardState => {
191
191
  // If selectedWidgetIds missing, try to extract from layouts (or top-level breakpoints)
192
192
  if (!Array.isArray(parsed.selectedWidgetIds)) {
193
193
  const ids = new Set<string>();
194
- let layoutsSource: Layouts | undefined;
194
+ let layoutsSource: ResponsiveLayouts | undefined;
195
195
  if (parsed && parsed.layouts) {
196
- layoutsSource = parsed.layouts as Layouts;
196
+ layoutsSource = parsed.layouts as ResponsiveLayouts;
197
197
  } else if (parsedAny && (parsedAny.lg || parsedAny.md || parsedAny.sm || parsedAny.xs)) {
198
198
  layoutsSource = {
199
199
  lg: parsedAny.lg ?? [],
200
200
  md: parsedAny.md ?? [],
201
201
  sm: parsedAny.sm ?? [],
202
202
  xs: parsedAny.xs ?? [],
203
- } as Layouts;
203
+ } as ResponsiveLayouts;
204
204
  parsed.layouts = layoutsSource;
205
205
 
206
206
  delete parsedAny.lg;
@@ -211,8 +211,8 @@ const migrateOldDashboardState = (parsedAny: any): DashboardState => {
211
211
 
212
212
  if (layoutsSource) {
213
213
  for (const bp of BREAKPOINTS) {
214
- const arr = layoutsSource[bp] as Layout[] | undefined;
215
- if (Array.isArray(arr)) arr.forEach((item: Layout) => {
214
+ const arr = layoutsSource[bp] as Layout | undefined;
215
+ if (Array.isArray(arr)) arr.forEach((item: LayoutItem) => {
216
216
  if (item && item.i) ids.add(String(item.i));
217
217
  });
218
218
  }
@@ -228,14 +228,14 @@ const migrateOldDashboardState = (parsedAny: any): DashboardState => {
228
228
 
229
229
 
230
230
  // Generate default layouts for all breakpoints
231
- const generateDefaultLayouts = (widgets: WidgetConfig[] = AVAILABLE_WIDGETS): Layouts => {
232
- const lg: Layout[] = widgets.map((widget) => ({
231
+ const generateDefaultLayouts = (widgets: WidgetConfig[] = AVAILABLE_WIDGETS): ResponsiveLayouts => {
232
+ const lg: Layout = widgets.map((widget) => ({
233
233
  i: widget.id,
234
234
  ...widget.defaultLayout,
235
235
  }));
236
236
 
237
237
  // For medium screens, make widgets full width in pairs
238
- const md: Layout[] = widgets.map((widget, index) => ({
238
+ const md: Layout = widgets.map((widget, index) => ({
239
239
  i: widget.id,
240
240
  w: 6,
241
241
  h: widget.defaultLayout.h,
@@ -246,7 +246,7 @@ const generateDefaultLayouts = (widgets: WidgetConfig[] = AVAILABLE_WIDGETS): La
246
246
  }));
247
247
 
248
248
  // For small screens, stack vertically
249
- const sm: Layout[] = widgets.map((widget, index) => ({
249
+ const sm: Layout = widgets.map((widget, index) => ({
250
250
  i: widget.id,
251
251
  w: 12,
252
252
  h: widget.defaultLayout.h,
@@ -269,6 +269,8 @@ export const ConfigurableDashboard: React.FC<ConfigurableDashboardProps> = ({ en
269
269
  // Cast t to a looser signature so we can call dynamic keys like `dashboard.widgets` without TS errors
270
270
  const t = tRaw as unknown as (key: string) => string;
271
271
 
272
+ const { width, containerRef, mounted } = useContainerWidth();
273
+
272
274
  const [isEditMode, setIsEditMode] = useState(false);
273
275
 
274
276
  // Selected widgets (internal state) - initialize from prop or persisted single-state
@@ -295,7 +297,7 @@ export const ConfigurableDashboard: React.FC<ConfigurableDashboardProps> = ({ en
295
297
  return AVAILABLE_WIDGETS.filter((w) => selectedWidgetIds.includes(w.id));
296
298
  }, [selectedWidgetIds]);
297
299
 
298
- const [layouts, setLayouts] = useState<Layouts>(() => {
300
+ const [layouts, setLayouts] = useState<ResponsiveLayouts>(() => {
299
301
  const saved = loadDashboardState();
300
302
  return (saved && saved.layouts) || generateDefaultLayouts(visibleWidgets);
301
303
  });
@@ -316,7 +318,7 @@ export const ConfigurableDashboard: React.FC<ConfigurableDashboardProps> = ({ en
316
318
  setLayouts(generateDefaultLayouts(visibleWidgets));
317
319
  }, [selectedWidgetIds, visibleWidgets]);
318
320
 
319
- const handleLayoutChange = useCallback((_: Layout[], allLayouts: Layouts) => {
321
+ const handleLayoutChange = useCallback((_: Layout, allLayouts: ResponsiveLayouts) => {
320
322
  setLayouts(allLayouts);
321
323
  }, []);
322
324
 
@@ -361,19 +363,25 @@ export const ConfigurableDashboard: React.FC<ConfigurableDashboardProps> = ({ en
361
363
  }, [selectedWidgetIds, hiddenWidgetIds, layouts]);
362
364
 
363
365
  // Grid configuration
364
- const gridConfig = useMemo(
366
+ const gridProps = useMemo(
365
367
  () => ({
366
368
  className: "layout",
367
369
  layouts: layouts,
368
370
  breakpoints: { lg: 1200, md: 996, sm: 768, xs: 480 },
369
371
  cols: { lg: 12, md: 12, sm: 12, xs: 12 },
370
- rowHeight: 100,
371
- isDraggable: isEditMode,
372
- isResizable: isEditMode,
373
372
  onLayoutChange: handleLayoutChange,
374
- draggableHandle: isEditMode ? undefined : ".no-drag",
375
- margin: [16, 16] as [number, number],
376
- containerPadding: [0, 0] as [number, number],
373
+ dragConfig: {
374
+ enabled: isEditMode,
375
+ handle: isEditMode ? undefined : ".no-drag",
376
+ },
377
+ resizeConfig: {
378
+ enabled: isEditMode,
379
+ },
380
+ gridConfig: {
381
+ rowHeight: 100,
382
+ margin: [16, 16],
383
+ containerPadding: [0, 0],
384
+ },
377
385
  }),
378
386
  [layouts, isEditMode, handleLayoutChange]
379
387
  );
@@ -449,31 +457,35 @@ export const ConfigurableDashboard: React.FC<ConfigurableDashboardProps> = ({ en
449
457
  </Tooltip>
450
458
  </Box>
451
459
 
452
- <ResponsiveGridLayout {...gridConfig}>
453
- {visibleWidgets.map((widget) => {
454
- const WidgetComponent = widget.component;
455
- return (
456
- <Box
457
- key={widget.id}
458
- sx={{
459
- // Add visual feedback in edit mode
460
- border: isEditMode ? "1px dashed" : "none",
461
- borderColor: "primary.main",
462
- borderRadius: 1,
463
- transition: "border 0.2s",
464
- cursor: isEditMode ? "move" : "default",
465
- "&:hover": isEditMode
466
- ? {
467
- borderColor: "primary.dark",
468
- }
469
- : {},
470
- }}
471
- >
472
- <WidgetComponent />
473
- </Box>
474
- );
475
- })}
476
- </ResponsiveGridLayout>
460
+ <Box ref={containerRef}>
461
+ {mounted && (
462
+ <Responsive {...gridProps} width={width}>
463
+ {visibleWidgets.map((widget) => {
464
+ const WidgetComponent = widget.component;
465
+ return (
466
+ <Box
467
+ key={widget.id}
468
+ sx={{
469
+ // Add visual feedback in edit mode
470
+ border: isEditMode ? "1px dashed" : "none",
471
+ borderColor: "primary.main",
472
+ borderRadius: 1,
473
+ transition: "border 0.2s",
474
+ cursor: isEditMode ? "move" : "default",
475
+ "&:hover": isEditMode
476
+ ? {
477
+ borderColor: "primary.dark",
478
+ }
479
+ : {},
480
+ }}
481
+ >
482
+ <WidgetComponent />
483
+ </Box>
484
+ );
485
+ })}
486
+ </Responsive>
487
+ )}
488
+ </Box>
477
489
  </Box>
478
490
  );
479
491
  };
@@ -44,7 +44,7 @@ const RoutineCardContent = (props: { routine: Routine }) => {
44
44
  }
45
45
  >
46
46
  <List>
47
- {props.routine.dayDataCurrentIterationNoNulls.map((dayData) => (
47
+ {props.routine.dayDataCurrentIterationFiltered.map((dayData) => (
48
48
  <DayListItem dayData={dayData} key={`dayDetails-${dayData.date.toISOString()}`} />
49
49
  ))}
50
50
  </List>
@@ -51,7 +51,7 @@ export const RoutineDetail = () => {
51
51
  href={makeLink(WgerLink.ROUTINE_COPY, i18n.language, { id: routineId })}
52
52
  variant={"contained"}
53
53
  >{t('routines.copyAndUseTemplate')}</Button>}
54
- {routine!.dayDataCurrentIterationNoNulls.map((dayData) =>
54
+ {routine!.dayDataCurrentIterationFiltered.map((dayData) =>
55
55
  <DayDetailsCard
56
56
  routineId={routineId}
57
57
  dayData={dayData}
@@ -48,7 +48,7 @@ export const TemplateDetail = () => {
48
48
  variant={"contained"}
49
49
  >{t('routines.copyAndUseTemplate')}</Button>
50
50
 
51
- {routine!.dayDataCurrentIterationNoNulls.map((dayData) =>
51
+ {routine!.dayDataCurrentIterationFiltered.map((dayData) =>
52
52
  <DayDetailsCard
53
53
  dayData={dayData}
54
54
  routineId={routineId}
@@ -110,4 +110,21 @@ describe('Routine model tests', () => {
110
110
  // Assert
111
111
  expect(routine.dayDataCurrentIteration).toEqual(testRoutineDayData1);
112
112
  });
113
+
114
+ test('correctly filters out null days', () => {
115
+
116
+ // Arrange
117
+ routine.dayData = [
118
+ ...testRoutineDayData1,
119
+ ...testRoutineDayData1,
120
+ ...testRoutineDayData1,
121
+ ];
122
+ routine.dayData[0].date = new Date('2026-01-01');
123
+ routine.dayData[1].date = new Date('2026-01-02');
124
+ routine.dayData[2].date = new Date('2026-01-03');
125
+
126
+ // Assert
127
+ expect(routine.dayDataCurrentIteration.length).toEqual(3);
128
+ expect(routine.dayDataCurrentIterationFiltered).toEqual(testRoutineDayData1);
129
+ });
113
130
  });
@@ -62,12 +62,29 @@ export class Routine {
62
62
  }
63
63
 
64
64
  /*
65
- * Filter out dayData entries with null days
65
+ * Filter out dayData entries with null days as well as duplicated days from
66
+ * the "fixed weekly schedule" toggle.
66
67
  */
67
- get dayDataCurrentIterationNoNulls() {
68
- return this.dayDataCurrentIteration
68
+ get dayDataCurrentIterationFiltered() {
69
+ const sorted = this.dayDataCurrentIteration
69
70
  .filter((dayData) => dayData.day !== null)
70
71
  .sort((a, b) => a.day!.order - b.day!.order);
72
+
73
+ // Filter out entries where the day is the same as the previous one. This is
74
+ // necessary because if the user has the "Fixed weekly schedule" option enabled,
75
+ // there would be multiple entries for the same day.
76
+ const unique: RoutineDayData[] = [];
77
+ for (const dd of sorted) {
78
+ if (unique.length === 0 || unique[unique.length - 1].day!.id !== dd.day!.id) {
79
+ unique.push(dd);
80
+ } else {
81
+ // If the day id is the same as the previous entry, replace it with the current one
82
+ // so the last occurrence is kept.
83
+ unique[unique.length - 1] = dd;
84
+ }
85
+ }
86
+
87
+ return unique;
71
88
  }
72
89
 
73
90
  get groupedDayDataByIteration() {
@@ -53,7 +53,7 @@ export const RoutineDetailsCard = () => {
53
53
  </Typography>
54
54
  }
55
55
  <Stack spacing={2} sx={{ mt: 2 }}>
56
- {routineQuery.data!.dayDataCurrentIteration.filter((dayData) => dayData.day !== null).map((dayData, index) =>
56
+ {routineQuery.data!.dayDataCurrentIterationFiltered.map((dayData, index) =>
57
57
  <DayDetailsCard routineId={routineId} dayData={dayData} key={`dayDetails-${index}`} />
58
58
  )}
59
59
  </Stack>