@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/build/assets/index.css +1 -1
- package/build/locales/ar/translation.json +347 -236
- package/build/locales/cs/translation.json +257 -253
- package/build/locales/de/translation.json +1 -1
- package/build/locales/en/translation.json +1 -1
- package/build/locales/es/translation.json +5 -2
- package/build/locales/hr/translation.json +15 -3
- package/build/locales/mk/translation.json +138 -0
- package/build/locales/pt/translation.json +338 -338
- package/build/locales/pt_BR/translation.json +3 -0
- package/build/locales/ru/translation.json +6 -2
- package/build/locales/ta/translation.json +1 -1
- package/build/locales/uk/translation.json +3 -0
- package/build/locales/zh_Hans/translation.json +25 -1
- package/build/locales/zh_Hant/translation.json +255 -243
- package/build/main.js +146 -146
- package/build/main.js.map +1 -1
- package/package.json +12 -13
- package/src/components/Dashboard/ConfigurableDashboard.test.ts +3 -2
- package/src/components/Dashboard/ConfigurableDashboard.tsx +61 -49
- package/src/components/Dashboard/RoutineCard.tsx +1 -1
- package/src/components/WorkoutRoutines/Detail/RoutineDetail.tsx +1 -1
- package/src/components/WorkoutRoutines/Detail/TemplateDetail.tsx +1 -1
- package/src/components/WorkoutRoutines/models/Routine.test.ts +17 -0
- package/src/components/WorkoutRoutines/models/Routine.ts +20 -3
- package/src/components/WorkoutRoutines/widgets/RoutineDetailsCard.tsx +1 -1
package/package.json
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"exports": {
|
|
12
12
|
".": "./build/index.js"
|
|
13
13
|
},
|
|
14
|
-
"version": "26.
|
|
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.
|
|
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.
|
|
30
|
-
"@mui/x-date-pickers": "^8.
|
|
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.
|
|
42
|
-
"react-grid-layout": "^
|
|
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.
|
|
44
|
+
"react-is": "^19.2.4",
|
|
45
45
|
"react-responsive": "^10.0.1",
|
|
46
|
-
"react-router-dom": "^7.
|
|
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.
|
|
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-
|
|
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.
|
|
86
|
+
"vite": "^7.3.1",
|
|
88
87
|
"vite-plugin-eslint": "^1.8.1",
|
|
89
|
-
"vitest": "^
|
|
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
|
-
|
|
58
|
-
expect(
|
|
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,
|
|
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:
|
|
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
|
|
144
|
+
const arr = (out.layouts as ResponsiveLayouts)[bp] as Layout | undefined;
|
|
147
145
|
if (Array.isArray(arr)) {
|
|
148
|
-
(out.layouts as
|
|
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:
|
|
194
|
+
let layoutsSource: ResponsiveLayouts | undefined;
|
|
195
195
|
if (parsed && parsed.layouts) {
|
|
196
|
-
layoutsSource = parsed.layouts as
|
|
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
|
|
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
|
|
215
|
-
if (Array.isArray(arr)) arr.forEach((item:
|
|
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):
|
|
232
|
-
const lg: Layout
|
|
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
|
|
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
|
|
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<
|
|
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
|
|
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
|
|
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
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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
|
-
<
|
|
453
|
-
{
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
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.
|
|
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!.
|
|
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!.
|
|
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
|
|
68
|
-
|
|
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!.
|
|
56
|
+
{routineQuery.data!.dayDataCurrentIterationFiltered.map((dayData, index) =>
|
|
57
57
|
<DayDetailsCard routineId={routineId} dayData={dayData} key={`dayDetails-${index}`} />
|
|
58
58
|
)}
|
|
59
59
|
</Stack>
|