Skip to content

Commit 321e517

Browse files
Merge branch 'widgets' into demo
2 parents e779232 + 2dff43d commit 321e517

8 files changed

Lines changed: 164 additions & 2801 deletions

File tree

docs/Configuration/Widgets.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
This is an example of customizing a custom widget (CustomOne) in a React application that is built using Vite.
2+
3+
### **1. Custom Widget Definition (`fixture/test-widgets/Custom.ts`)**
4+
- This is a widget class (`CustomOne`) that extends a base widget class (`CustomBase`).
5+
- It defines metadata for the widget, such as:
6+
- `id`, `name`, `description`, and `department` (e.g., `"test"`)
7+
- `icon` (using a Material icon name, `"takeout_dining"`)
8+
- Default `size` (height and width in grid units)
9+
- `backgroundCSS` (a light gray color)
10+
- It specifies JavaScript paths for **development** (direct `.tsx` file) and **production** (a compiled `.es.js` file).
11+
- The widget is initialized with a `routePrefix` (likely a base URL for asset loading).
12+
13+
### **2. React Component (`modules/test/ComponentB.tsx`)**
14+
- This is the actual UI implementation of the widget.
15+
- It displays:
16+
- A **message** (passed as a prop)
17+
- A **button** that increments a counter when clicked
18+
- The **current counter value** (managed via React’s `useState`)
19+
- The button uses basic styling (similar to a styled button component).
20+
21+
### **3. Vite Build Configuration (`modules/test/vite.config.module.ts`)**
22+
- Configures the build process for the React component.
23+
- **Key settings:**
24+
- Builds the component as an **ES module** (`lib` mode).
25+
- Outputs the file as `ComponentB.es.js`.
26+
- Treats `react` and `react-dom` as **external dependencies** (they won’t be bundled; expected to be loaded separately).
27+
- Uses path aliases (e.g., `@` points to a shared JS directory).
28+
- Supports both **development** (direct `.tsx` usage) and **production** (optimized `.es.js` file).
29+
30+
### **How It All Works Together**
31+
1. **`CustomOne`** defines the widget’s metadata and where its JavaScript lives.
32+
2. **`ComponentB`** provides the interactive UI (a counter button with a message).
33+
3. **Vite** compiles the React component into a standalone ES module (`ComponentB.es.js`).
34+
- In **development**, it loads the `.tsx` file directly.
35+
- In **production**, it loads the optimized `.es.js` file from a CDN-like path (`/assets/modules/`).
36+
37+
This setup allows the widget to be reusable, dynamically loaded, and integrated into a larger application.

src/assets/js/components/widgets/widgets-layout.tsx

Lines changed: 81 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ import WidgetItem from "@/components/widgets/widget-item.tsx";
2121
import "react-grid-layout/css/styles.css";
2222
import "react-resizable/css/styles.css";
2323
import {usePage} from "@inertiajs/react";
24+
import {WidgetsLayouts} from "@/lib/widgets-service.ts";
25+
2426

2527
interface WidgetLayoutProps extends SharedData {
2628
title: string
@@ -33,15 +35,35 @@ interface WidgetLayoutProps extends SharedData {
3335

3436
const WidgetLayout = () => {
3537
const ResponsiveGridLayout = useMemo(() => WidthProvider(Responsive), []);
36-
const [layout, setLayout] = useState<WidgetLayoutItem[]>([]);
38+
const [layout, setLayout] = useState<WidgetsLayouts>({
39+
lg: [],
40+
md: [],
41+
sm: [],
42+
xs: [],
43+
xxs: [],
44+
});
3745
const [widgets, setWidgets] = useState<Widget[]>([])
3846
const [popUpDisabled, setPopUpDisabled] = useState(false)
3947
const [keyRender, setKeyRender] = useState(0);
4048
const [loading, setIsLoading] = useState(true)
4149
const [isDraggable, setIsDraggable] = useState(false)
50+
const [gridRef, setGridRef] = useState<HTMLDivElement | null>(null);
4251

4352
const page = usePage<WidgetLayoutProps>()
4453

54+
// Get the current breakpoint
55+
const getCurrentBreakpoint = useCallback(() => {
56+
if (!gridRef) return 'lg'; // fallback
57+
58+
const width = gridRef.clientWidth;
59+
60+
if (width >= 1200) return 'lg';
61+
if (width >= 996) return 'md';
62+
if (width >= 768) return 'sm';
63+
if (width >= 480) return 'xs';
64+
return 'xxs';
65+
}, [gridRef]);
66+
4567
useEffect(() => {
4668
async function loadWidgets() {
4769
try {
@@ -70,67 +92,69 @@ const WidgetLayout = () => {
7092
}, [layout, isDraggable])
7193

7294
const handleLayoutChange = (currentLayout: WidgetLayoutItem[]) => {
95+
const currentBreakpoint = getCurrentBreakpoint();
7396

74-
const mergeCoordinates = (layout: WidgetLayoutItem[], currentLayout: WidgetLayoutItem[]) => {
75-
// Создаем копию первого массива, чтобы не изменять оригинал
76-
const result = JSON.parse(JSON.stringify(layout));
97+
setLayout(prev => {
98+
const updatedLayout = {...prev};
99+
const currentLayoutMap = new Map(currentLayout.map(item => [item.i, item]));
77100

78-
// Создаем карту для быстрого поиска элементов по полю 'i'
79-
const secondMap = new Map();
80-
currentLayout.forEach(item => secondMap.set(item.i, item));
81-
82-
// Обновляем координаты в первом массиве
83-
result.forEach((item: WidgetLayoutItem) => {
84-
const secondItem = secondMap.get(item.i);
85-
if (secondItem) {
86-
item.x = secondItem.x;
87-
item.y = secondItem.y;
88-
}
89-
});
101+
updatedLayout[currentBreakpoint as keyof WidgetsLayouts] =
102+
updatedLayout[currentBreakpoint as keyof WidgetsLayouts].map(item => {
103+
const currentItem = currentLayoutMap.get(item.i);
104+
return currentItem ? {...item, x: currentItem.x, y: currentItem.y, w: currentItem.w, h: currentItem.h} : item;
105+
});
90106

91-
return result;
92-
}
93-
setLayout(mergeCoordinates(layout, currentLayout))
107+
return updatedLayout;
108+
});
94109
};
95110

96111
const addWidgets = useCallback((id: string) => {
97112
setPopUpDisabled(true);
98113
const updatedWidgets = widgets.map(widget =>
99-
widget.id === id
100-
? {...widget, added: !widget.added}
101-
: widget
114+
widget.id === id ? {...widget, added: !widget.added} : widget
102115
);
103116

104-
const layoutItem = layout.find(e => e.id === id);
105-
let newLayout: WidgetLayoutItem[];
117+
const currentLayout = layout.lg;
118+
const layoutItem = currentLayout.find(e => e.i === id);
119+
let newLayout: WidgetsLayouts = {...layout};
106120

107121
if (layoutItem) {
108-
newLayout = layout.filter(e => e.id !== id);
122+
// Delete the layoutItem from all breakpoints
123+
for (const breakpoint in newLayout) {
124+
newLayout[breakpoint as keyof WidgetsLayouts] =
125+
newLayout[breakpoint as keyof WidgetsLayouts].filter(e => e.i !== id);
126+
}
109127
} else {
110128
const widget = widgets.find(e => e.id === id);
111129
const w = widget?.size ? widget.size.w : 1;
112130
const h = widget?.size ? widget.size.h : 1;
113131

114-
let x = layout.length === 0
115-
? 0
116-
: ((layout[layout.length - 1].x + layout[layout.length - 1].w) > 8 ||
117-
(layout[layout.length - 1].x + layout[layout.length - 1].w + w) > 8
132+
let x = currentLayout.length === 0 ? 0 :
133+
((currentLayout[currentLayout.length - 1].x + currentLayout[currentLayout.length - 1].w) > 8 ||
134+
(currentLayout[currentLayout.length - 1].x + currentLayout[currentLayout.length - 1].w + w) > 8
118135
? 0
119-
: (layout[layout.length - 1].x + layout[layout.length - 1].w));
136+
: (currentLayout[currentLayout.length - 1].x + currentLayout[currentLayout.length - 1].w));
120137

121138
const y = 0;
122139

123-
newLayout = [
124-
...layout,
125-
{
126-
x: x,
127-
y: y,
128-
w: w,
129-
h: h,
130-
i: String(layout.length + 1),
131-
id: widget?.id as string,
140+
const newItem = {
141+
x,
142+
y,
143+
w,
144+
h,
145+
i: id,
146+
id: widget?.id as string,
147+
};
148+
149+
// Add the newItem to all breakpoints
150+
for (const breakpoint in newLayout) {
151+
if (!newLayout[breakpoint as keyof WidgetsLayouts].some(item => item.i === id)) {
152+
newLayout[breakpoint as keyof WidgetsLayouts] = [
153+
...newLayout[breakpoint as keyof WidgetsLayouts],
154+
{...newItem}
155+
];
132156
}
133-
];
157+
}
134158
}
135159

136160
setWidgets(updatedWidgets);
@@ -140,12 +164,11 @@ const WidgetLayout = () => {
140164
addWidgetsDB(updatedWidgets, newLayout).then(() => {
141165
setTimeout(() => {
142166
setPopUpDisabled(false);
143-
}, 300)
144-
})
145-
167+
}, 300);
168+
});
146169
}, [layout, widgets]);
147170

148-
const addWidgetsDB = useCallback(async (updatedWidgets: Widget[], newLayout: WidgetLayoutItem[]) => {
171+
const addWidgetsDB = useCallback(async (updatedWidgets: Widget[], newLayout: WidgetsLayouts) => {
149172
try {
150173
const storeWidgets = updatedWidgets.filter(widget => widget.added === true)
151174
await axios.post(`${window.routePrefix}/widgets-get-all-db`, {
@@ -161,6 +184,7 @@ const WidgetLayout = () => {
161184
console.log(e);
162185
}
163186
}, [widgets])
187+
164188
return (
165189
<div
166190
className={`flex h-full flex-1 flex-col gap-4 rounded-xl p-4 `}>
@@ -212,28 +236,32 @@ const WidgetLayout = () => {
212236
</DialogStackTitle>
213237
<div className="overflow-auto h-[calc(100%-64px)] pr-4 pt-4">
214238
<AddWidgets initWidgets={widgets} onAddWidgets={addWidgets}
215-
disabled={popUpDisabled} searchPlaceholder={page.props.searchPlaceholder} actionsTitles={page.props.actionsTitles}/>
239+
disabled={popUpDisabled}
240+
searchPlaceholder={page.props.searchPlaceholder}
241+
actionsTitles={page.props.actionsTitles}/>
216242
</div>
217243
</div>
218244
</DialogStackContent>
219245
</DialogStackBody>
220246
</DialogStack>
221247
</div>
222248
</div>
223-
<div>
224-
{layout.length > 0 ? (
249+
<div ref={setGridRef}>
250+
{layout.lg.length > 0 ? (
225251
<ResponsiveGridLayout
226-
layouts={{lg: layout}}
227-
breakpoints={{lg: 1024, md: 768, sm: 475, xs: 320, xxs: 0}}
228-
cols={{lg: 8, md: 6, sm: 4, xs: 2, xxs: 2}}
252+
//@ts-ignore
253+
layouts={layout}
254+
breakpoints={{lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0}}
255+
cols={{lg: 8, md: 6, sm: 4, xs: 2, xxs: 1}}
229256
rowHeight={131}
230-
onLayoutChange={handleLayoutChange}
257+
//@ts-ignore
258+
onDragStop={handleLayoutChange}
231259
isResizable={false}
232260
isDraggable={isDraggable}
233261
key={keyRender}
234-
className="overflow-hidden"
262+
className="overflow-hidden max-w-[1440px]"
235263
>
236-
{layout.map((widget) => (
264+
{layout.lg.map((widget) => (
237265
<div key={widget.i}>
238266
<WidgetItem draggable={isDraggable} widgets={widgets} ID={widget.id}/>
239267
</div>
Lines changed: 34 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,62 @@
1-
import { Widget, WidgetLayoutItem } from "@/types";
1+
import {Widget, WidgetLayoutItem} from "@/types";
22
import axios from "axios";
33

4-
// Ключ для хранения данных в localStorage
4+
export interface WidgetsLayouts {
5+
lg: WidgetLayoutItem[],
6+
md: WidgetLayoutItem[],
7+
sm: WidgetLayoutItem[],
8+
xs: WidgetLayoutItem[],
9+
xxs: WidgetLayoutItem[]
10+
}
511

612
export async function initializeWidgets(): Promise<{
7-
layout: WidgetLayoutItem[];
13+
layout: WidgetsLayouts;
814
widgets: Widget[];
915
}> {
1016
try {
11-
// 1. Пытаемся получить данные из localStorage
1217
const storedData = localStorage.getItem('widgetsData');
1318
let storedWidgets: Widget[] = [];
14-
let storedLayout: WidgetLayoutItem[] = [];
19+
let storedLayout: WidgetsLayouts = {
20+
lg: [],
21+
md: [],
22+
sm: [],
23+
xs: [],
24+
xxs: []
25+
};
1526

1627
if (storedData) {
1728
try {
1829
const parsedData = JSON.parse(storedData);
1930
storedWidgets = parsedData.widgets || [];
20-
storedLayout = parsedData.layout || [];
31+
storedLayout = parsedData.layout || {
32+
lg: [],
33+
md: [],
34+
sm: [],
35+
xs: [],
36+
xxs: []
37+
};
2138
} catch (e) {
22-
console.error('Ошибка при разборе данных из localStorage', e);
39+
console.error('Error parsing data from localStorage', e);
2340
}
2441
}
2542

26-
// 2. Получаем данные с сервера
2743
const widgetsDBResponse = await axios.get(`${window.routePrefix}/widgets-get-all-db`);
2844
let widgetsDB = widgetsDBResponse.data?.widgetsDB?.widgets as Widget[] ?? [];
29-
let layoutDB = widgetsDBResponse.data?.widgetsDB?.layout ?? [];
45+
let layoutDB = widgetsDBResponse.data?.widgetsDB?.layout ?? {
46+
lg: [],
47+
md: [],
48+
sm: [],
49+
xs: [],
50+
xxs: []
51+
};
3052

3153
const widgetsResponse = await axios.get(`${window.routePrefix}/widgets-get-all`);
3254
const allWidgets = widgetsResponse.data.widgets as Widget[];
3355

34-
// 3. Сравниваем виджеты из БД и localStorage
3556
const dbWidgetIds = widgetsDB.map(w => w.id).sort();
3657
const storedWidgetIds = storedWidgets.map(w => w.id).sort();
3758

38-
// Проверяем, совпадают ли массивы ID
59+
// Check if the widget IDs in the database and localStorage match
3960
const idsMatch =
4061
dbWidgetIds.length === storedWidgetIds.length &&
4162
dbWidgetIds.every((id, index) => id === storedWidgetIds[index]);
@@ -44,30 +65,27 @@ export async function initializeWidgets(): Promise<{
4465
let finalLayoutDB = layoutDB;
4566

4667
if (idsMatch && storedWidgets.length > 0) {
47-
// Если ID совпадают - используем данные из localStorage
4868
finalWidgetsDB = storedWidgets;
4969
finalLayoutDB = storedLayout;
5070
} else {
51-
// Если данные изменились - сохраняем в localStorage
5271
const dataToStore = {
5372
widgets: widgetsDB,
5473
layout: layoutDB
5574
};
5675
localStorage.setItem('widgetsData', JSON.stringify(dataToStore));
5776
}
5877

59-
// 4. Инициализируем виджеты, отмечая добавленные
6078
const initWidgets = allWidgets.map(widget => {
6179
const findItem = finalWidgetsDB.find((e: any) => e.id === widget.id);
62-
return findItem && findItem.added ? { ...widget, added: true } : widget;
80+
return findItem && findItem.added ? {...widget, added: true} : widget;
6381
});
6482

6583
return {
6684
layout: finalLayoutDB,
6785
widgets: initWidgets
6886
};
6987
} catch (error) {
70-
console.error('Ошибка при инициализации виджетов:', error);
88+
console.error('Error initializing widgets:', error);
7189
throw error;
7290
}
7391
}

src/lib/widgets/widgetHandler.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,14 @@ export interface WidgetLayoutItem {
3838
id: string;
3939
}
4040

41+
export interface WidgetsLayouts {
42+
lg: WidgetLayoutItem[],
43+
md: WidgetLayoutItem[],
44+
sm: WidgetLayoutItem[],
45+
xs: WidgetLayoutItem[],
46+
xxs: WidgetLayoutItem[]
47+
}
48+
4149
export class WidgetHandler {
4250
private widgets: WidgetType[] = [];
4351
public adminizer: Adminizer;
@@ -158,10 +166,10 @@ export class WidgetHandler {
158166

159167
public async getWidgetsDB(id: number, auth: boolean, i18n: I18n): Promise<{
160168
widgets: WidgetConfig[],
161-
layout: WidgetLayoutItem[]
169+
layout: WidgetsLayouts
162170
}> {
163171
let user: UserAP;
164-
let result: { widgets: WidgetConfig[], layout: WidgetLayoutItem[] } = {widgets: [], layout: []};
172+
let result: { widgets: WidgetConfig[], layout: WidgetsLayouts } = {widgets: [], layout: {lg: [], md: [], sm: [], xs: [], xxs: []}};
165173

166174
if (!auth) {
167175
// TODO refactor CRUD functions for DataAccessor usage

0 commit comments

Comments
 (0)