wenay-react2 1.0.22 → 1.0.25
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/README.md +34 -360
- package/lib/common/src/grid/agGrid4/agGrid4.d.ts +11 -1
- package/lib/common/src/grid/agGrid4/agGrid4.js +28 -5
- package/lib/common/src/grid/agGrid4/columnBuffer.d.ts +26 -0
- package/lib/common/src/grid/agGrid4/columnBuffer.js +37 -0
- package/lib/common/src/grid/agGrid4/core.d.ts +24 -5
- package/lib/common/src/grid/agGrid4/core.js +60 -6
- package/lib/common/src/grid/agGrid4/gridUtils.d.ts +25 -0
- package/lib/common/src/grid/agGrid4/gridUtils.js +17 -0
- package/lib/common/src/grid/agGrid4/index.d.ts +6 -2
- package/lib/common/src/grid/agGrid4/index.js +3 -1
- package/lib/common/src/hooks/index.d.ts +1 -0
- package/lib/common/src/hooks/index.js +1 -0
- package/lib/common/src/hooks/useObserveStore.d.ts +105 -0
- package/lib/common/src/hooks/useObserveStore.js +224 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,384 +1,58 @@
|
|
|
1
|
-
|
|
1
|
+
# wenay-react2
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Common React UI primitives for wenay apps.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Use the new concise API guide first:
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
- [wenay-react2.md](./wenay-react2.md) - canonical everyday API.
|
|
8
|
+
- [wenay-react2-rare.md](./wenay-react2-rare.md) - old names, low-level exports, migration notes.
|
|
9
|
+
- [agGrid4 README](./src/common/src/grid/agGrid4/README.md) - table buffer/controller details.
|
|
10
|
+
- [agGrid4 wrapper boundary](./src/common/src/grid/agGrid4/WRAPPER.md) - where generic primitives end and app wrappers begin.
|
|
8
11
|
|
|
9
|
-
|
|
10
|
-
Wrapper for creating freely movable, resizable windows with state persistence.
|
|
12
|
+
## Import
|
|
11
13
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
type tDivRndBase = {
|
|
15
|
-
children: React.ReactElement | ((update: number) => React.ReactElement);
|
|
16
|
-
position?: { x: number; y: number };
|
|
17
|
-
size?: { height: number | string; width: number | string };
|
|
18
|
-
keyForSave?: string; // Key for caching the position/size in memory
|
|
19
|
-
limit?: { x?: { max?: number; min?: number }, y?: { max?: number; min?: number } };
|
|
20
|
-
onCLickClose?: () => void; // Adds a close button to the window
|
|
21
|
-
header?: React.ReactElement | boolean; // Drag handle header; otherwise the whole block is draggable
|
|
22
|
-
zIndex?: number;
|
|
23
|
-
}
|
|
14
|
+
```ts
|
|
15
|
+
import { useModal, useAgGrid, createGridBuffer, createColumnBuffer, useStoreMirror } from "wenay-react2"
|
|
24
16
|
```
|
|
25
17
|
|
|
26
|
-
|
|
27
|
-
```textmate
|
|
28
|
-
<DivRnd3
|
|
29
|
-
keyForSave="my_tool_window"
|
|
30
|
-
size={{ width: 300, height: 200 }}
|
|
31
|
-
onCLickClose={() => setOpen(false)}
|
|
32
|
-
header={<div>My tool</div>}
|
|
33
|
-
>
|
|
34
|
-
<div>Window content</div>
|
|
35
|
-
</DivRnd3>
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
#### `GetModalJSX()`
|
|
40
|
-
Factory for global modal windows. It lets business logic open windows without being tied to local component state.
|
|
41
|
-
|
|
42
|
-
**Example:**
|
|
43
|
-
```textmate
|
|
44
|
-
const myModal = GetModalJSX();
|
|
45
|
-
|
|
46
|
-
// 1. In the app root file (App.tsx):
|
|
47
|
-
<myModal.Render />
|
|
48
|
-
|
|
49
|
-
// 2. Anywhere in the logic:
|
|
50
|
-
myModal.set(<div onClick={() => myModal.set(null)}>Close me</div>);
|
|
51
|
-
// For multiple windows: myModal.addJSX(<Component/>)
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
---
|
|
56
|
-
|
|
57
|
-
### Drag-and-Drop and Interaction (Hooks & Wrappers)
|
|
58
|
-
|
|
59
|
-
#### `useOutside` / `DivOutsideClick`
|
|
60
|
-
Tracks clicks outside an element, useful for closing dropdowns and menus.
|
|
61
|
-
|
|
62
|
-
**Typing:**
|
|
63
|
-
```typescript
|
|
64
|
-
// Hook
|
|
65
|
-
function useOutside(params: { outsideClick: () => void, status?: boolean, ref?: RefObject }): RefObject;
|
|
66
|
-
|
|
67
|
-
// Component
|
|
68
|
-
type Props = { outsideClick: () => void, status?: boolean, zIndex?: number } & HTMLAttributes;
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
**Example:**
|
|
72
|
-
```textmate
|
|
73
|
-
<DivOutsideClick outsideClick={() => setDropdownOpen(false)} status={isOpen}>
|
|
74
|
-
<div className="dropdown-menu">...</div>
|
|
75
|
-
</DivOutsideClick>
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
#### `Drag22`
|
|
80
|
-
Tracks mouse/finger movement and returns new coordinates without touching CSS directly.
|
|
81
|
-
|
|
82
|
-
**Typing:**
|
|
83
|
-
```typescript
|
|
84
|
-
type Drag2Props = {
|
|
85
|
-
x?: number; y?: number; // Start/current coordinates
|
|
86
|
-
onX?: (val: number) => void; // X change callback
|
|
87
|
-
onY?: (val: number) => void; // Y change callback
|
|
88
|
-
onStart?: () => void; onStop?: () => void;
|
|
89
|
-
children: ReactNode;
|
|
90
|
-
};
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
---
|
|
95
|
-
|
|
96
|
-
### Context Menus (`MenuR` / `mouseMenuApi`)
|
|
97
|
-
|
|
98
|
-
Smart context menus. They open on desktop via right click and on mobile via long tap / double tap. They support asynchronous item generation.
|
|
99
|
-
|
|
100
|
-
**Menu item typing (`tMenuReactStrictly`):**
|
|
101
|
-
```typescript
|
|
102
|
-
type tMenuReactStrictly = {
|
|
103
|
-
name: string | ((status?: any) => string);
|
|
104
|
-
onClick?: (e: any) => void | Promise<any> | Promise<any>[]; // Promise support with ok/error counters
|
|
105
|
-
status?: boolean; // Whether the submenu is expanded
|
|
106
|
-
next?: () => tMenuReact[] | Promise<tMenuReact[]>; // Nested menu
|
|
107
|
-
func?: () => React.ReactElement | Promise<React.ReactElement>; // Custom hover render
|
|
108
|
-
};
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
**Examples:**
|
|
112
|
-
```textmate
|
|
113
|
-
// 1. Using the wrapper component
|
|
114
|
-
const menuItems = () => [
|
|
115
|
-
{ name: "Copy", onClick: () => copy() },
|
|
116
|
-
{ name: "Options", next: () => [{ name: "Option 1", onClick: () => {} }] }
|
|
117
|
-
];
|
|
118
|
-
|
|
119
|
-
<MenuR other={menuItems}>
|
|
120
|
-
<div className="item">Right-click me</div>
|
|
121
|
-
</MenuR>
|
|
122
|
-
|
|
123
|
-
// 2. Global call through the API
|
|
124
|
-
mouseMenuApi.map.set("global_action", [{ name: "Global action", onClick: () => {} }]);
|
|
125
|
-
```
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
---
|
|
129
|
-
|
|
130
|
-
### Ag-Grid Table Updates (`applyTransactionAsyncUpdate2`)
|
|
131
|
-
|
|
132
|
-
Function for safe batched asynchronous table data updates synchronized with a local buffer/cache.
|
|
133
|
-
|
|
134
|
-
**Typing:**
|
|
135
|
-
```typescript
|
|
136
|
-
type Params<T> = {
|
|
137
|
-
gridRef?: React.RefObject<GridReadyEvent<T, any>>;
|
|
138
|
-
newData: Partial<T>[]; // New data array
|
|
139
|
-
getId: (row: Partial<T>) => string; // ID getter
|
|
140
|
-
bufTable: { [id: string]: Partial<T> }; // Local cache dictionary
|
|
141
|
-
option?: { update?: boolean, add?: boolean, updateBuffer?: boolean, sync?: boolean };
|
|
142
|
-
};
|
|
143
|
-
```
|
|
144
|
-
|
|
145
|
-
**Example:**
|
|
146
|
-
```textmate
|
|
147
|
-
// Immediately updates the cache and sends a transaction to Ag-Grid
|
|
148
|
-
applyTransactionAsyncUpdate2({
|
|
149
|
-
gridRef: apiGrid,
|
|
150
|
-
newData: [{ id: "user_1", balance: 500 }],
|
|
151
|
-
getId: (e) => e.id,
|
|
152
|
-
bufTable: myLocalBuffer,
|
|
153
|
-
option: { update: true, add: true }
|
|
154
|
-
});
|
|
155
|
-
```
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
---
|
|
159
|
-
|
|
160
|
-
### UI Settings Autogeneration (`ParametersReact`)
|
|
161
|
-
|
|
162
|
-
Engine that accepts an object with a data schema and renders a ready-to-use control form with inputs, sliders, and selects.
|
|
163
|
-
|
|
164
|
-
**Typing:**
|
|
165
|
-
The schema is built from `Params.IParamsExpandableReadonly` types.
|
|
166
|
-
Supported types: `number`, `string`, `boolean`, `Date`, arrays, and nested objects.
|
|
167
|
-
|
|
168
|
-
**Example:**
|
|
169
|
-
```textmate
|
|
170
|
-
const mySettings = {
|
|
171
|
-
showGrid: true, // Generates a Checkbox
|
|
172
|
-
opacity: {
|
|
173
|
-
value: 0.5,
|
|
174
|
-
range: { min: 0, max: 1, step: 0.1 },
|
|
175
|
-
name: "Opacity"
|
|
176
|
-
}, // Generates a Range Slider + Number Input
|
|
177
|
-
theme: {
|
|
178
|
-
value: "dark",
|
|
179
|
-
range: ["dark", "light", "system"]
|
|
180
|
-
} // Generates a Select
|
|
181
|
-
};
|
|
18
|
+
The root export is still flat. A grouped namespace is also exported for large files:
|
|
182
19
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
onChange={(newParams) => {
|
|
186
|
-
console.log("New value:", newParams.opacity.value);
|
|
187
|
-
}}
|
|
188
|
-
/>
|
|
189
|
-
```
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
---
|
|
193
|
-
|
|
194
|
-
### Logging System (`logsApi`)
|
|
195
|
-
|
|
196
|
-
Log registration, log viewer table, and toast notification system.
|
|
197
|
-
|
|
198
|
-
**API:**
|
|
199
|
-
```typescript
|
|
200
|
-
// 1. Adding a log
|
|
201
|
-
logsApi.addLogs({
|
|
202
|
-
id: "system",
|
|
203
|
-
var: 10, // Severity, important for notification filtering
|
|
204
|
-
time: new Date(),
|
|
205
|
-
txt: "Connection lost"
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
// 2. Rendering components, usually somewhere near the root:
|
|
209
|
-
<logsApi.React.Message zIndex={9999} /> // Toast notifications in the top-right corner
|
|
210
|
-
<logsApi.React.PageLogs /> // Ag-Grid table with all logs
|
|
211
|
-
```
|
|
212
|
-
|
|
213
|
-
There are several other important architectural patterns and UI components actively used in the codebase. They should be documented so another developer or AI assistant can understand how interfaces are built in this project.
|
|
214
|
-
|
|
215
|
-
Here is the second part of the `README.md` additions:
|
|
216
|
-
|
|
217
|
-
---
|
|
218
|
-
|
|
219
|
-
### Sidebar Navigation
|
|
220
|
-
|
|
221
|
-
#### `ApiLeftMenu`
|
|
222
|
-
Ready-made API for controlling the left slide-out sidebar menu. It supports swipes, smooth snap scrolling, and imperative tab control.
|
|
223
|
-
|
|
224
|
-
**API and Typing:**
|
|
225
|
-
```typescript
|
|
226
|
-
type MenuItem = {
|
|
227
|
-
el: () => React.JSX.Element; // Tab component
|
|
228
|
-
button?: React.JSX.Element; // Custom button, optional
|
|
229
|
-
color?: ColorString; // Background color
|
|
230
|
-
textB?: string; // Default button text
|
|
231
|
-
};
|
|
232
|
-
|
|
233
|
-
// Register menu items:
|
|
234
|
-
ApiLeftMenu.setMenu(items: MenuItem[], key?: string);
|
|
235
|
-
```
|
|
236
|
-
|
|
237
|
-
**Usage example:**
|
|
238
|
-
```textmate
|
|
239
|
-
// 1. Register tabs, can be done anywhere in the logic:
|
|
240
|
-
ApiLeftMenu.setMenu([
|
|
241
|
-
{ textB: "Dashboard", el: () => <Dashboard />, color: "rgb(92,50,213)" },
|
|
242
|
-
{ textB: "Settings", el: () => <Settings /> }
|
|
243
|
-
], "main_menu");
|
|
244
|
-
|
|
245
|
-
// 2. Render the menu itself in the root Layout:
|
|
246
|
-
export function AppLayout() {
|
|
247
|
-
return <ApiLeftMenu.Modal2 zIndex={20} />;
|
|
248
|
-
}
|
|
249
|
-
```
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
---
|
|
253
|
-
|
|
254
|
-
### Interactive Elements and Buttons
|
|
20
|
+
```ts
|
|
21
|
+
import { v2 } from "wenay-react2"
|
|
255
22
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
**`Button` typing:**
|
|
260
|
-
```typescript
|
|
261
|
-
type tButton = {
|
|
262
|
-
button: ReactElement | ((status: boolean) => ReactElement); // The button itself; can change by status
|
|
263
|
-
children: ReactNode | ((api: {onClose: () => void}) => ReactNode); // Dropdown content
|
|
264
|
-
outClick?: boolean | (() => void); // Whether to close on outside click; true by default
|
|
265
|
-
statusDef?: boolean; // Initial state, open/closed
|
|
266
|
-
};
|
|
267
|
-
```
|
|
268
|
-
|
|
269
|
-
**Example:**
|
|
270
|
-
```textmate
|
|
271
|
-
<Button
|
|
272
|
-
outClick={true}
|
|
273
|
-
button={(isOpen) => <div className={isOpen ? "active" : ""}>Options</div>}
|
|
274
|
-
>
|
|
275
|
-
{({ onClose }) => (
|
|
276
|
-
<div className="dropdown-panel">
|
|
277
|
-
<div onClick={() => { doSomething(); onClose(); }}>Action 1</div>
|
|
278
|
-
</div>
|
|
279
|
-
)}
|
|
280
|
-
</Button>
|
|
23
|
+
v2.grid.useAgGrid(...)
|
|
24
|
+
v2.modal.useModal()
|
|
281
25
|
```
|
|
282
26
|
|
|
27
|
+
## API Standard
|
|
283
28
|
|
|
284
|
-
|
|
285
|
-
Wrapper over `re-resizable`. It creates resizable panels, for example columns or lower log panes, and stores their size in cache.
|
|
286
|
-
|
|
287
|
-
**Example:**
|
|
288
|
-
```textmate
|
|
289
|
-
<FResizableReact
|
|
290
|
-
keyForSave="bottom_panel_size"
|
|
291
|
-
size={{ height: 200, width: "100%" }}
|
|
292
|
-
moveWith={false} // Allow resize only by height
|
|
293
|
-
>
|
|
294
|
-
<LogsTable />
|
|
295
|
-
</FResizableReact>
|
|
296
|
-
```
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
---
|
|
300
|
-
|
|
301
|
-
### Global Reactivity (`renderBy` / `updateBy` Pattern)
|
|
302
|
-
|
|
303
|
-
*(Note: this is a key project concept used instead of classic `useState` / `Redux` for complex business logic.)*
|
|
304
|
-
|
|
305
|
-
Many places in the project use a mutable state approach: you mutate properties of a plain JS object and call `renderBy(obj)` to force an update of all components subscribed to that object through `updateBy(obj)`.
|
|
306
|
-
|
|
307
|
-
**Pattern example:**
|
|
308
|
-
```textmate
|
|
309
|
-
import { renderBy, updateBy } from "./updateBy";
|
|
29
|
+
New code should use the short controller-style surface:
|
|
310
30
|
|
|
311
|
-
|
|
312
|
-
const
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
};
|
|
31
|
+
```tsx
|
|
32
|
+
const modal = useModal()
|
|
33
|
+
modal.open(<Dialog />)
|
|
34
|
+
modal.close()
|
|
316
35
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
36
|
+
const grid = useAgGrid<Row>({getId: row => row.id})
|
|
37
|
+
grid.update({newData})
|
|
38
|
+
grid.clean()
|
|
39
|
+
grid.flush()
|
|
321
40
|
|
|
322
|
-
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
// 3. State change, anywhere, even outside React
|
|
326
|
-
function increment() {
|
|
327
|
-
myState.count += 1;
|
|
328
|
-
renderBy(myState); // Triggers a CounterViewer rerender
|
|
329
|
-
}
|
|
41
|
+
const mirror = useStoreMirror(remoteStore, initialState, {mask, current: true})
|
|
330
42
|
```
|
|
331
43
|
|
|
44
|
+
Old `Get*`, `*2/*3`, `FuncJSX`, and legacy grid helper names remain documented in `wenay-react2-rare.md` for existing code, but they are not the teaching path for new code.
|
|
332
45
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
### Useful Ag-Grid Utilities
|
|
336
|
-
|
|
337
|
-
#### `GridStyleDefault` and `StyleCSSHeadGrid`
|
|
338
|
-
Quickly apply the common corporate style to all application tables.
|
|
46
|
+
## QA Stand
|
|
339
47
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
import { GridStyleDefault, StyleCSSHeadGrid } from "./styleGrid";
|
|
343
|
-
|
|
344
|
-
// Applies the dark theme, compresses spacing, and centers headers
|
|
345
|
-
GridStyleDefault();
|
|
346
|
-
StyleCSSHeadGrid();
|
|
48
|
+
```sh
|
|
49
|
+
npm run testReact -- --host 127.0.0.1 --port 3002
|
|
347
50
|
```
|
|
348
51
|
|
|
52
|
+
The stand source is `src/common/testUseReact/qa.tsx`. Use it for visual checks after changing UI primitives.
|
|
349
53
|
|
|
350
|
-
|
|
351
|
-
Factory for creating custom Ag-Grid column sort functions that correctly handle `undefined`, `NaN`, and inversion.
|
|
352
|
-
**Example:**
|
|
353
|
-
```textmate
|
|
354
|
-
const columnDefs = [
|
|
355
|
-
{
|
|
356
|
-
field: "price",
|
|
357
|
-
comparator: getComparatorGrid() // Safe numeric sorting
|
|
358
|
-
}
|
|
359
|
-
];
|
|
360
|
-
```
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
---
|
|
364
|
-
|
|
365
|
-
### Global Hooks
|
|
366
|
-
|
|
367
|
-
#### `useAddDownAnyKey`
|
|
368
|
-
Registers a global keyboard listener and stores the last pressed key in the exported reactive object `KeyDown`.
|
|
369
|
-
|
|
370
|
-
**Usage example:**
|
|
371
|
-
```textmate
|
|
372
|
-
import { KeyDown, useAddDownAnyKey } from "./useAddDownAnyKey";
|
|
373
|
-
import { updateBy } from "./updateBy";
|
|
374
|
-
|
|
375
|
-
function HotkeyListener() {
|
|
376
|
-
useAddDownAnyKey(); // Hook initialization
|
|
377
|
-
updateBy(KeyDown); // Subscribe to key presses
|
|
54
|
+
## Build
|
|
378
55
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
}
|
|
382
|
-
return null;
|
|
383
|
-
}
|
|
56
|
+
```sh
|
|
57
|
+
npm run build
|
|
384
58
|
```
|
|
@@ -1,25 +1,34 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { AgGridReactProps } from 'ag-grid-react';
|
|
3
3
|
import type { GridApi, GetRowIdParams, GridPreDestroyedEvent, GridReadyEvent } from 'ag-grid-community';
|
|
4
|
-
import { type BufferTable, type GetId } from './core';
|
|
4
|
+
import { type BufferTable, type GetId, type GridBufferCore, type GridBufferMode, type PushOptions } from './core';
|
|
5
5
|
export type UseAgGridOptions<T> = {
|
|
6
6
|
/** How to get a row id. Defaults to the `id` field. Captured once (first render). */
|
|
7
7
|
getId?: GetId<T>;
|
|
8
8
|
/** External buffer: a plain object above the component (like datum.tableArr) that survives route remounts. */
|
|
9
9
|
externalBuffer?: BufferTable<T>;
|
|
10
|
+
/** mirror (default): buffer owns rows. overlay: rowData owns rows and sync only refreshes intersection. */
|
|
11
|
+
mode?: GridBufferMode;
|
|
12
|
+
/** Default delivery policy for updateData. Useful for overlay streams: { add: false }. */
|
|
13
|
+
pushDefaults?: PushOptions;
|
|
14
|
+
/** Ready-made module-level core/store; getId/externalBuffer/mode are already captured inside it. */
|
|
15
|
+
core?: GridBufferCore<T>;
|
|
10
16
|
};
|
|
11
17
|
/**
|
|
12
18
|
* Headless table hook. All race handling lives in the core; the hook only binds it
|
|
13
19
|
* to the React and grid lifecycle:
|
|
14
20
|
*
|
|
15
21
|
* const grid = useAgGrid<Row>({ getId })
|
|
22
|
+
* const grid = useAgGrid({ core: mainTable })
|
|
16
23
|
* <AgGridMy<Row> controller={grid} columnDefs={cols} />
|
|
17
24
|
* grid.update({ newData })
|
|
18
25
|
*/
|
|
19
26
|
export declare function useAgGrid<T>(options?: UseAgGridOptions<T>): {
|
|
20
27
|
update: (args: import("./core").UpdateArgs<T>) => void;
|
|
21
28
|
remove(rows: Partial<T>[]): void;
|
|
29
|
+
clean: (args?: import("./core").CleanArgs) => void;
|
|
22
30
|
fit: () => boolean;
|
|
31
|
+
flush: () => boolean;
|
|
23
32
|
updateData: (args: import("./core").UpdateArgs<T>) => void;
|
|
24
33
|
sync: () => void;
|
|
25
34
|
getId: GetId<T>;
|
|
@@ -38,6 +47,7 @@ export declare function useAgGrid<T>(options?: UseAgGridOptions<T>): {
|
|
|
38
47
|
getApi(): GridApi<T> | null;
|
|
39
48
|
withApi<R>(fn: (api: GridApi<T>) => R): R | undefined;
|
|
40
49
|
sizeColumnsToFit: () => boolean;
|
|
50
|
+
flushAsyncTransactions: () => boolean;
|
|
41
51
|
};
|
|
42
52
|
export type AgGridController<T> = ReturnType<typeof useAgGrid<T>>;
|
|
43
53
|
export type AgGridMyProps<T> = AgGridReactProps<T> & {
|
|
@@ -23,6 +23,7 @@ function ensureAgGridModules() {
|
|
|
23
23
|
* to the React and grid lifecycle:
|
|
24
24
|
*
|
|
25
25
|
* const grid = useAgGrid<Row>({ getId })
|
|
26
|
+
* const grid = useAgGrid({ core: mainTable })
|
|
26
27
|
* <AgGridMy<Row> controller={grid} columnDefs={cols} />
|
|
27
28
|
* grid.update({ newData })
|
|
28
29
|
*/
|
|
@@ -31,8 +32,15 @@ export function useAgGrid(options) {
|
|
|
31
32
|
const apiRef = useRef(null);
|
|
32
33
|
// The core is created once; dependencies live in its closure, with no useCallback chains.
|
|
33
34
|
const [core] = useState(function createCore() {
|
|
35
|
+
if (options?.core)
|
|
36
|
+
return options.core;
|
|
34
37
|
const getId = options?.getId ?? (data => String(data?.id));
|
|
35
|
-
return createGridBuffer({
|
|
38
|
+
return createGridBuffer({
|
|
39
|
+
getId,
|
|
40
|
+
externalBuffer: options?.externalBuffer,
|
|
41
|
+
mode: options?.mode,
|
|
42
|
+
pushDefaults: options?.pushDefaults,
|
|
43
|
+
});
|
|
36
44
|
});
|
|
37
45
|
const gridProps = useMemo(() => ({
|
|
38
46
|
getRowId(params) {
|
|
@@ -55,12 +63,21 @@ export function useAgGrid(options) {
|
|
|
55
63
|
api.sizeColumnsToFit();
|
|
56
64
|
return true;
|
|
57
65
|
};
|
|
66
|
+
const flush = () => {
|
|
67
|
+
const api = apiRef.current;
|
|
68
|
+
if (!api)
|
|
69
|
+
return false;
|
|
70
|
+
api.flushAsyncTransactions();
|
|
71
|
+
return true;
|
|
72
|
+
};
|
|
58
73
|
return {
|
|
59
74
|
update: core.api.updateData,
|
|
60
75
|
remove(rows) {
|
|
61
76
|
core.api.updateData({ removeData: rows });
|
|
62
77
|
},
|
|
78
|
+
clean: core.api.clean,
|
|
63
79
|
fit,
|
|
80
|
+
flush,
|
|
64
81
|
updateData: core.api.updateData,
|
|
65
82
|
sync: core.api.sync,
|
|
66
83
|
getId: core.api.getId,
|
|
@@ -76,6 +93,7 @@ export function useAgGrid(options) {
|
|
|
76
93
|
return api ? fn(api) : undefined;
|
|
77
94
|
},
|
|
78
95
|
sizeColumnsToFit: fit,
|
|
96
|
+
flushAsyncTransactions: flush,
|
|
79
97
|
};
|
|
80
98
|
}, [core, gridProps]);
|
|
81
99
|
}
|
|
@@ -85,14 +103,18 @@ export function useAgGrid(options) {
|
|
|
85
103
|
const ROW_SELECTION = { mode: 'multiRow' };
|
|
86
104
|
function AgGridMyInner(props) {
|
|
87
105
|
const { controller, data, autoSizeColumns = true, defaultColDef, theme, getRowId, onGridReady, onGridPreDestroyed, ...rest } = props;
|
|
88
|
-
// Own controller
|
|
89
|
-
const own = useAgGrid(
|
|
106
|
+
// Own controller is overlay-safe: rowData grids without a controller must not be cleared on attach.
|
|
107
|
+
const own = useAgGrid({
|
|
108
|
+
getId: getRowId ? data => getRowId({ data: data }) : undefined,
|
|
109
|
+
mode: 'overlay',
|
|
110
|
+
pushDefaults: { add: false, remove: false },
|
|
111
|
+
});
|
|
90
112
|
const grid = controller ?? own;
|
|
91
113
|
const defaultTheme = useAgGridTheme();
|
|
92
114
|
const containerRef = useRef(null);
|
|
93
115
|
useEffect(function pushData() {
|
|
94
116
|
if (data)
|
|
95
|
-
grid.update({ newData: data });
|
|
117
|
+
grid.update({ newData: data, option: { add: true } });
|
|
96
118
|
}, [data, grid]);
|
|
97
119
|
useEffect(function observeResize() {
|
|
98
120
|
if (!containerRef.current || !autoSizeColumns)
|
|
@@ -114,10 +136,11 @@ function AgGridMyInner(props) {
|
|
|
114
136
|
};
|
|
115
137
|
}, [autoSizeColumns, grid]);
|
|
116
138
|
const mergedColDef = useMemo(() => ({ sortable: true, resizable: true, filter: true, ...defaultColDef }), [defaultColDef]);
|
|
139
|
+
const wiredGetRowId = getRowId ?? (controller || data ? grid.props.getRowId : undefined);
|
|
117
140
|
return (_jsx("div", { ref: containerRef, style: { height: '100%', width: '100%', overflow: 'hidden' }, children: _jsx(AgGridReact, { theme: theme ?? defaultTheme, asyncTransactionWaitMillis: 50, suppressCellFocus: true, rowSelection: ROW_SELECTION, ...rest, defaultColDef: mergedColDef,
|
|
118
141
|
// getRowId after the spread: undefined from rest must not erase the wiring,
|
|
119
142
|
// otherwise the grid falls back to index-based row identity.
|
|
120
|
-
getRowId:
|
|
143
|
+
getRowId: wiredGetRowId, onGridReady: function ready(event) {
|
|
121
144
|
grid.props.onGridReady(event);
|
|
122
145
|
if (autoSizeColumns)
|
|
123
146
|
event.api.sizeColumnsToFit();
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { GridApi } from 'ag-grid-community';
|
|
2
|
+
export type ColumnApplyContext<TData = any> = {
|
|
3
|
+
api: GridApi<TData>;
|
|
4
|
+
names: readonly string[];
|
|
5
|
+
};
|
|
6
|
+
export type ColumnAttach<TData = any> = {
|
|
7
|
+
/** Project/page wrapper decides how names affect the grid. */
|
|
8
|
+
apply: (context: ColumnApplyContext<TData>) => void;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Buffer for the exact set of dynamic column names.
|
|
12
|
+
* setNames() means exactly this dynamic set; attach() applies the saved set; detach() keeps names.
|
|
13
|
+
* This utility deliberately knows nothing about groups, columnDefs shape, or business names.
|
|
14
|
+
*/
|
|
15
|
+
export declare function createColumnBuffer<TData = any>(): {
|
|
16
|
+
control: {
|
|
17
|
+
attach: (gridApi: GridApi<TData>, opts: ColumnAttach<TData>) => void;
|
|
18
|
+
detach: () => void;
|
|
19
|
+
};
|
|
20
|
+
api: {
|
|
21
|
+
setNames: (list: string[]) => void;
|
|
22
|
+
apply: () => void;
|
|
23
|
+
readonly names: string[];
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
export type ColumnBuffer<TData = any> = ReturnType<typeof createColumnBuffer<TData>>;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Buffer for the exact set of dynamic column names.
|
|
3
|
+
* setNames() means exactly this dynamic set; attach() applies the saved set; detach() keeps names.
|
|
4
|
+
* This utility deliberately knows nothing about groups, columnDefs shape, or business names.
|
|
5
|
+
*/
|
|
6
|
+
export function createColumnBuffer() {
|
|
7
|
+
let names = [];
|
|
8
|
+
let attachOptions = null;
|
|
9
|
+
let api = null;
|
|
10
|
+
function apply() {
|
|
11
|
+
if (api && attachOptions)
|
|
12
|
+
attachOptions.apply({ api, names });
|
|
13
|
+
}
|
|
14
|
+
function attach(gridApi, opts) {
|
|
15
|
+
api = gridApi;
|
|
16
|
+
attachOptions = opts;
|
|
17
|
+
apply();
|
|
18
|
+
}
|
|
19
|
+
function detach() {
|
|
20
|
+
api = null;
|
|
21
|
+
attachOptions = null;
|
|
22
|
+
}
|
|
23
|
+
function setNames(list) {
|
|
24
|
+
names = [...new Set(list)];
|
|
25
|
+
apply();
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
control: { attach, detach },
|
|
29
|
+
api: {
|
|
30
|
+
setNames,
|
|
31
|
+
apply,
|
|
32
|
+
get names() {
|
|
33
|
+
return names;
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
}
|
|
@@ -9,15 +9,21 @@ export type GridTransaction<T> = {
|
|
|
9
9
|
};
|
|
10
10
|
/** Minimal grid api contract (the real GridApi is structurally compatible). */
|
|
11
11
|
export type GridApiLike<T> = {
|
|
12
|
+
/** Used by overlay mode: row ownership belongs to rowData, not to this buffer. */
|
|
13
|
+
getRowNode?(id: RowId): {
|
|
14
|
+
data?: T;
|
|
15
|
+
} | undefined;
|
|
12
16
|
forEachNode(callback: (node: {
|
|
13
17
|
data?: T;
|
|
14
18
|
}) => void): void;
|
|
15
19
|
applyTransaction(tx: GridTransaction<T>): unknown;
|
|
16
20
|
applyTransactionAsync(tx: GridTransaction<T>): void;
|
|
17
21
|
};
|
|
22
|
+
export type GridBufferMode = 'mirror' | 'overlay';
|
|
18
23
|
export type PushOptions = {
|
|
19
24
|
add?: boolean;
|
|
20
25
|
update?: boolean;
|
|
26
|
+
remove?: boolean;
|
|
21
27
|
/** add+update in one SYNCHRONOUS transaction. Default: add is sync, update is async (batched). */
|
|
22
28
|
sync?: boolean;
|
|
23
29
|
};
|
|
@@ -28,23 +34,36 @@ export type UpdateArgs<T> = {
|
|
|
28
34
|
onlyMemo?: boolean;
|
|
29
35
|
option?: PushOptions;
|
|
30
36
|
};
|
|
37
|
+
export type CleanArgs = {
|
|
38
|
+
/** Only clear the buffer; do not remove rows from a mirror-owned grid. */
|
|
39
|
+
onlyMemo?: boolean;
|
|
40
|
+
};
|
|
41
|
+
export type CreateGridBufferOptions<T> = {
|
|
42
|
+
getId: GetId<T>;
|
|
43
|
+
/** External buffer (object above the component) survives route remounts. Otherwise use an internal one. */
|
|
44
|
+
externalBuffer?: BufferTable<T>;
|
|
45
|
+
/**
|
|
46
|
+
* mirror (default): the buffer is the source of truth; sync adds, updates and removes grid rows.
|
|
47
|
+
* overlay: external rowData owns rows; sync only refreshes rows that already exist in the grid.
|
|
48
|
+
*/
|
|
49
|
+
mode?: GridBufferMode;
|
|
50
|
+
/** Delivery defaults for updateData, e.g. { add: false } for rowData-owned streaming overlays. */
|
|
51
|
+
pushDefaults?: PushOptions;
|
|
52
|
+
};
|
|
31
53
|
/**
|
|
32
54
|
* Table core: buffer + delivery to the grid. Lifecycle:
|
|
33
55
|
* 1. updateData at any time: the buffer is always updated, the grid only if attached;
|
|
34
56
|
* 2. attach(api) on onGridReady: sync catches up with everything that arrived earlier;
|
|
35
57
|
* 3. detach() on grid destroy: the core accumulates in the buffer again until the next attach.
|
|
36
58
|
*/
|
|
37
|
-
export declare function createGridBuffer<T>(deps: {
|
|
38
|
-
getId: GetId<T>;
|
|
39
|
-
/** External buffer (object above the component) survives route remounts. Otherwise use an internal one. */
|
|
40
|
-
externalBuffer?: BufferTable<T>;
|
|
41
|
-
}): {
|
|
59
|
+
export declare function createGridBuffer<T>(deps: CreateGridBufferOptions<T>): {
|
|
42
60
|
control: {
|
|
43
61
|
attach: (gridApi: GridApiLike<T>) => void;
|
|
44
62
|
detach: () => void;
|
|
45
63
|
};
|
|
46
64
|
api: {
|
|
47
65
|
updateData: (args: UpdateArgs<T>) => void;
|
|
66
|
+
clean: (args?: CleanArgs) => void;
|
|
48
67
|
sync: () => void;
|
|
49
68
|
getId: GetId<T>;
|
|
50
69
|
buffer: BufferTable<T>;
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// updateData, instead of duplicating buffer/grid branching in the hook;
|
|
7
7
|
// - in-place row merge (Object.assign), with no new object allocated for each update
|
|
8
8
|
// (important on streams: less garbage for GC);
|
|
9
|
-
// - add vs update is decided by a Set of delivered ids
|
|
9
|
+
// - mirror add vs update is decided by a Set of delivered ids; overlay checks the grid by row id.
|
|
10
10
|
/**
|
|
11
11
|
* Table core: buffer + delivery to the grid. Lifecycle:
|
|
12
12
|
* 1. updateData at any time: the buffer is always updated, the grid only if attached;
|
|
@@ -14,10 +14,25 @@
|
|
|
14
14
|
* 3. detach() on grid destroy: the core accumulates in the buffer again until the next attach.
|
|
15
15
|
*/
|
|
16
16
|
export function createGridBuffer(deps) {
|
|
17
|
-
const { getId } = deps;
|
|
17
|
+
const { getId, mode = 'mirror', pushDefaults } = deps;
|
|
18
18
|
const buf = deps.externalBuffer ?? {};
|
|
19
19
|
const inGrid = new Set(); // ids already delivered to the grid
|
|
20
20
|
let api = null;
|
|
21
|
+
function gridHas(id) {
|
|
22
|
+
if (!api)
|
|
23
|
+
return false;
|
|
24
|
+
if (mode == 'mirror')
|
|
25
|
+
return inGrid.has(id);
|
|
26
|
+
const node = api.getRowNode?.(id);
|
|
27
|
+
if (node)
|
|
28
|
+
return !!node.data;
|
|
29
|
+
let found = false;
|
|
30
|
+
api.forEachNode(rowNode => {
|
|
31
|
+
if (!found && rowNode.data && getId(rowNode.data) == id)
|
|
32
|
+
found = true;
|
|
33
|
+
});
|
|
34
|
+
return found;
|
|
35
|
+
}
|
|
21
36
|
// --- Buffer ----------------------------------------------------------------
|
|
22
37
|
function upsert(rows) {
|
|
23
38
|
for (const row of rows) {
|
|
@@ -36,13 +51,17 @@ export function createGridBuffer(deps) {
|
|
|
36
51
|
delete buf[getId(row)];
|
|
37
52
|
if (onlyMemo || !api)
|
|
38
53
|
return;
|
|
39
|
-
const opt = { add: true, update: true, sync: false, ...option };
|
|
54
|
+
const opt = { add: true, update: true, remove: true, sync: false, ...pushDefaults, ...option };
|
|
40
55
|
const toAdd = [];
|
|
41
56
|
const toUpdate = [];
|
|
57
|
+
const seen = new Set();
|
|
42
58
|
for (const row of newData ?? []) {
|
|
43
59
|
const id = getId(row);
|
|
60
|
+
if (seen.has(id))
|
|
61
|
+
continue;
|
|
62
|
+
seen.add(id);
|
|
44
63
|
const merged = buf[id];
|
|
45
|
-
if (
|
|
64
|
+
if (gridHas(id))
|
|
46
65
|
toUpdate.push(merged);
|
|
47
66
|
else
|
|
48
67
|
toAdd.push(merged);
|
|
@@ -53,7 +72,13 @@ export function createGridBuffer(deps) {
|
|
|
53
72
|
for (const row of add)
|
|
54
73
|
inGrid.add(getId(row));
|
|
55
74
|
// ag-grid finds rows for remove through getRowId, so partial data is enough.
|
|
56
|
-
const remove =
|
|
75
|
+
const remove = opt.remove
|
|
76
|
+
? removeData?.filter(row => {
|
|
77
|
+
const id = getId(row);
|
|
78
|
+
const delivered = inGrid.delete(id);
|
|
79
|
+
return mode == 'overlay' ? gridHas(id) : delivered;
|
|
80
|
+
})
|
|
81
|
+
: undefined;
|
|
57
82
|
if (opt.sync) {
|
|
58
83
|
api.applyTransaction({ add, update, remove });
|
|
59
84
|
return;
|
|
@@ -67,10 +92,24 @@ export function createGridBuffer(deps) {
|
|
|
67
92
|
/**
|
|
68
93
|
* Bring the whole grid in line with the buffer (the buffer is the source of truth).
|
|
69
94
|
* Missing from the buffer -> remove; missing from the grid -> add; everything else -> update.
|
|
95
|
+
* In overlay mode, rowData is the source of truth, so sync only updates row intersection.
|
|
70
96
|
*/
|
|
71
97
|
function sync() {
|
|
72
98
|
if (!api)
|
|
73
99
|
return;
|
|
100
|
+
if (mode == 'overlay') {
|
|
101
|
+
const update = [];
|
|
102
|
+
api.forEachNode(function refresh(node) {
|
|
103
|
+
if (!node.data)
|
|
104
|
+
return;
|
|
105
|
+
const id = getId(node.data);
|
|
106
|
+
if (buf[id])
|
|
107
|
+
update.push(buf[id]);
|
|
108
|
+
});
|
|
109
|
+
if (update.length)
|
|
110
|
+
api.applyTransaction({ update });
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
74
113
|
const add = [];
|
|
75
114
|
const update = [];
|
|
76
115
|
const remove = [];
|
|
@@ -94,6 +133,21 @@ export function createGridBuffer(deps) {
|
|
|
94
133
|
if (add.length || update.length || remove.length)
|
|
95
134
|
api.applyTransaction({ add, update, remove });
|
|
96
135
|
}
|
|
136
|
+
/** Clear the buffer. In mirror mode, also remove all current grid rows unless onlyMemo is set. */
|
|
137
|
+
function clean(args = {}) {
|
|
138
|
+
const remove = [];
|
|
139
|
+
if (!args.onlyMemo && api && mode == 'mirror') {
|
|
140
|
+
api.forEachNode(node => {
|
|
141
|
+
if (node.data)
|
|
142
|
+
remove.push(node.data);
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
for (const id in buf)
|
|
146
|
+
delete buf[id];
|
|
147
|
+
inGrid.clear();
|
|
148
|
+
if (remove.length)
|
|
149
|
+
api?.applyTransaction({ remove });
|
|
150
|
+
}
|
|
97
151
|
// --- Lifecycle -------------------------------------------------------------
|
|
98
152
|
function attach(gridApi) {
|
|
99
153
|
api = gridApi;
|
|
@@ -107,7 +161,7 @@ export function createGridBuffer(deps) {
|
|
|
107
161
|
// for the React binding (or another grid owner)
|
|
108
162
|
control: { attach, detach },
|
|
109
163
|
// public consumer api
|
|
110
|
-
api: { updateData, sync, getId, buffer: buf },
|
|
164
|
+
api: { updateData, clean, sync, getId, buffer: buf },
|
|
111
165
|
};
|
|
112
166
|
}
|
|
113
167
|
/** Column comparator: numbers as numbers, empty/NaN values last (respecting inversion). */
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export { numericComparator } from './core';
|
|
2
|
+
export type { NumberPair } from './core';
|
|
3
|
+
/** Centered cells + sort/filter defaults for dense data tables. */
|
|
4
|
+
export declare const colDefCentered: {
|
|
5
|
+
headerClass: () => string;
|
|
6
|
+
resizable: true;
|
|
7
|
+
cellStyle: {
|
|
8
|
+
textAlign: string;
|
|
9
|
+
};
|
|
10
|
+
sortable: true;
|
|
11
|
+
filter: boolean;
|
|
12
|
+
};
|
|
13
|
+
/** Wrapped text cells for list/selection grids. */
|
|
14
|
+
export declare const colDefWrap: {
|
|
15
|
+
headerClass: () => string;
|
|
16
|
+
resizable: true;
|
|
17
|
+
cellClass: string;
|
|
18
|
+
cellStyle: {
|
|
19
|
+
fontFamily: string;
|
|
20
|
+
fontStyle: string;
|
|
21
|
+
fontWeight: string;
|
|
22
|
+
fontSize: string;
|
|
23
|
+
textAlign: string;
|
|
24
|
+
};
|
|
25
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { StyleGridDefault } from '../../styles/styleGrid';
|
|
2
|
+
export { numericComparator } from './core';
|
|
3
|
+
/** Centered cells + sort/filter defaults for dense data tables. */
|
|
4
|
+
export const colDefCentered = {
|
|
5
|
+
headerClass: () => 'gridTable-header',
|
|
6
|
+
resizable: true,
|
|
7
|
+
cellStyle: { textAlign: 'center' },
|
|
8
|
+
sortable: true,
|
|
9
|
+
filter: true,
|
|
10
|
+
};
|
|
11
|
+
/** Wrapped text cells for list/selection grids. */
|
|
12
|
+
export const colDefWrap = {
|
|
13
|
+
headerClass: () => 'gridTable-header',
|
|
14
|
+
resizable: true,
|
|
15
|
+
cellClass: 'cell-wrap-text',
|
|
16
|
+
cellStyle: { ...StyleGridDefault },
|
|
17
|
+
};
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
export { createGridBuffer
|
|
2
|
-
export type { BufferTable, GetId, GridApiLike, GridBufferCore,
|
|
1
|
+
export { createGridBuffer } from './core';
|
|
2
|
+
export type { BufferTable, CleanArgs, CreateGridBufferOptions, GetId, GridApiLike, GridBufferCore, GridBufferMode, GridTransaction, PushOptions, RowId, UpdateArgs, } from './core';
|
|
3
|
+
export { createColumnBuffer } from './columnBuffer';
|
|
4
|
+
export type { ColumnApplyContext, ColumnAttach, ColumnBuffer } from './columnBuffer';
|
|
3
5
|
export { useAgGrid, AgGridMy } from './agGrid4';
|
|
4
6
|
export type { AgGridController, AgGridMyProps, UseAgGridOptions } from './agGrid4';
|
|
7
|
+
export { numericComparator, colDefCentered, colDefWrap } from './gridUtils';
|
|
8
|
+
export type { NumberPair } from './gridUtils';
|
|
5
9
|
export { useAgGridTheme, buildAgTheme } from './theme';
|
|
6
10
|
export type { tThemeMode } from './theme';
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
-
export { createGridBuffer
|
|
1
|
+
export { createGridBuffer } from './core';
|
|
2
|
+
export { createColumnBuffer } from './columnBuffer';
|
|
2
3
|
export { useAgGrid, AgGridMy } from './agGrid4';
|
|
4
|
+
export { numericComparator, colDefCentered, colDefWrap } from './gridUtils';
|
|
3
5
|
export { useAgGridTheme, buildAgTheme } from './theme';
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { ObserveAll2 } from "wenay-common2";
|
|
2
|
+
type StoreChange = ObserveAll2.StoreChange;
|
|
3
|
+
type StoreSubOpts = ObserveAll2.StoreSubOpts;
|
|
4
|
+
type StoreSyncOpts = ObserveAll2.StoreSyncOpts;
|
|
5
|
+
type StoreDrain = ObserveAll2.StoreDrain;
|
|
6
|
+
type StoreMask<T> = ObserveAll2.StoreMask<T>;
|
|
7
|
+
type StorePick<T, M> = ObserveAll2.StorePick<T, M>;
|
|
8
|
+
type StoreNode<T> = ObserveAll2.StoreNode<T>;
|
|
9
|
+
type StoreSelection<T, M> = ObserveAll2.StoreSelection<T, M>;
|
|
10
|
+
export type ListenLike<TArgs extends readonly unknown[] = readonly unknown[]> = {
|
|
11
|
+
on(cb: (...args: TArgs) => void, opts?: {
|
|
12
|
+
key?: string | symbol;
|
|
13
|
+
current?: boolean | (() => TArgs | undefined);
|
|
14
|
+
}): () => void;
|
|
15
|
+
};
|
|
16
|
+
export type UseStoreNodeOptions<T> = StoreSubOpts & {
|
|
17
|
+
mode?: "get" | "snapshot";
|
|
18
|
+
fallback?: T;
|
|
19
|
+
};
|
|
20
|
+
export type StoreNodeController<T> = {
|
|
21
|
+
readonly node: StoreNode<T>;
|
|
22
|
+
readonly value: T;
|
|
23
|
+
readonly exists: boolean;
|
|
24
|
+
readonly path: PropertyKey[];
|
|
25
|
+
readonly pathString: string;
|
|
26
|
+
refresh(): void;
|
|
27
|
+
get(): T;
|
|
28
|
+
snapshot(): T;
|
|
29
|
+
has(): boolean;
|
|
30
|
+
set(value: T): void;
|
|
31
|
+
replace(value: T): void;
|
|
32
|
+
update<M extends StoreMask<T>>(mask: M, opts?: StoreSubOpts): StoreSelection<T, M>;
|
|
33
|
+
};
|
|
34
|
+
export declare function useStoreNode<T>(node: StoreNode<T>, options?: UseStoreNodeOptions<T>): StoreNodeController<T>;
|
|
35
|
+
export type UseStoreSelectOptions<TValue> = StoreSubOpts & {
|
|
36
|
+
fallback?: TValue;
|
|
37
|
+
};
|
|
38
|
+
export type StoreSelectionController<T, M extends StoreMask<T>> = {
|
|
39
|
+
readonly selection: StoreSelection<T, M>;
|
|
40
|
+
readonly value: StorePick<T, M>;
|
|
41
|
+
readonly mask: M;
|
|
42
|
+
readonly paths: PropertyKey[][];
|
|
43
|
+
refresh(): void;
|
|
44
|
+
get(): StorePick<T, M>;
|
|
45
|
+
};
|
|
46
|
+
export type StoreKeysController<T extends object = any> = {
|
|
47
|
+
readonly node: StoreNode<T>;
|
|
48
|
+
readonly keys: PropertyKey[];
|
|
49
|
+
readonly stringKeys: string[];
|
|
50
|
+
readonly value: T;
|
|
51
|
+
readonly exists: boolean;
|
|
52
|
+
refresh(): void;
|
|
53
|
+
has(key: PropertyKey): boolean;
|
|
54
|
+
get<K extends keyof T>(key: K): T[K];
|
|
55
|
+
};
|
|
56
|
+
export declare function useStoreKeys<T extends object>(node: StoreNode<T>, options?: Omit<UseStoreNodeOptions<T>, "mode">): StoreKeysController<T>;
|
|
57
|
+
export declare function useStoreSelect<T, M extends StoreMask<T>>(selection: StoreSelection<T, M>, options?: UseStoreSelectOptions<StorePick<T, M>>): StoreSelectionController<T, M>;
|
|
58
|
+
export type RemoteStoreLike<T extends object> = {
|
|
59
|
+
get(mask?: any): T | Promise<T>;
|
|
60
|
+
changed: ListenLike<readonly []>;
|
|
61
|
+
changedPaths?: ListenLike<readonly [StoreChange]>;
|
|
62
|
+
};
|
|
63
|
+
export type UseStoreMirrorOptions<T extends object, M extends StoreMask<T>> = StoreSyncOpts & {
|
|
64
|
+
mask: M;
|
|
65
|
+
auto?: boolean;
|
|
66
|
+
};
|
|
67
|
+
export type StoreMirrorController<T extends object, M extends StoreMask<T>> = {
|
|
68
|
+
readonly store: ObserveAll2.Store<T> & {
|
|
69
|
+
sync(mask: M, subOpts?: StoreSyncOpts): Promise<() => void>;
|
|
70
|
+
};
|
|
71
|
+
readonly value: StorePick<T, M>;
|
|
72
|
+
readonly selection: StoreSelectionController<T, M>;
|
|
73
|
+
readonly ready: boolean;
|
|
74
|
+
readonly syncing: boolean;
|
|
75
|
+
readonly error: unknown;
|
|
76
|
+
sync(mask?: M, opts?: StoreSyncOpts): Promise<() => void>;
|
|
77
|
+
stop(): void;
|
|
78
|
+
};
|
|
79
|
+
export declare function useStoreMirror<T extends object, M extends StoreMask<T>>(remote: RemoteStoreLike<T>, initial: T, options: UseStoreMirrorOptions<T, M>): StoreMirrorController<T, M>;
|
|
80
|
+
export declare function useListenEffect<TArgs extends readonly unknown[]>(listen: ListenLike<TArgs> | null | undefined, cb: (...args: TArgs) => void, opts?: {
|
|
81
|
+
key?: string | symbol;
|
|
82
|
+
current?: boolean | (() => TArgs | undefined);
|
|
83
|
+
}): void;
|
|
84
|
+
export type StoreChangedPathsController = {
|
|
85
|
+
readonly change: StoreChange | undefined;
|
|
86
|
+
readonly paths: PropertyKey[][];
|
|
87
|
+
readonly count: number;
|
|
88
|
+
reset(): void;
|
|
89
|
+
};
|
|
90
|
+
export declare function useStoreChangedPaths(listen: ListenLike<readonly [StoreChange]> | null | undefined, opts?: {
|
|
91
|
+
initial?: StoreChange;
|
|
92
|
+
key?: string | symbol;
|
|
93
|
+
}): StoreChangedPathsController;
|
|
94
|
+
export declare function useListenArgs<TArgs extends readonly unknown[]>(listen: ListenLike<TArgs> | null | undefined, opts?: {
|
|
95
|
+
initial?: TArgs;
|
|
96
|
+
key?: string | symbol;
|
|
97
|
+
current?: boolean | (() => TArgs | undefined);
|
|
98
|
+
}): TArgs | undefined;
|
|
99
|
+
export declare function useListenValue<T, TArgs extends readonly unknown[] = readonly [T]>(listen: ListenLike<TArgs> | null | undefined, opts?: {
|
|
100
|
+
initial?: T;
|
|
101
|
+
key?: string | symbol;
|
|
102
|
+
current?: boolean | (() => TArgs | undefined);
|
|
103
|
+
map?: (...args: TArgs) => T;
|
|
104
|
+
}): T | undefined;
|
|
105
|
+
export type { StoreChange, StoreDrain, StoreMask, StoreNode, StorePick, StoreSelection, StoreSubOpts, StoreSyncOpts };
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
+
import { ObserveAll2 } from "wenay-common2";
|
|
3
|
+
function readNode(node, options) {
|
|
4
|
+
if (!node.has())
|
|
5
|
+
return options?.fallback;
|
|
6
|
+
return (options?.mode == "snapshot" ? node.snapshot() : node.get());
|
|
7
|
+
}
|
|
8
|
+
function isSameStoreMask(a, b) {
|
|
9
|
+
if (a === b)
|
|
10
|
+
return true;
|
|
11
|
+
if (a === true || b === true)
|
|
12
|
+
return false;
|
|
13
|
+
if (!a || !b || typeof a != "object" || typeof b != "object")
|
|
14
|
+
return false;
|
|
15
|
+
const keysA = Reflect.ownKeys(a);
|
|
16
|
+
const keysB = Reflect.ownKeys(b);
|
|
17
|
+
if (keysA.length != keysB.length)
|
|
18
|
+
return false;
|
|
19
|
+
for (const key of keysA) {
|
|
20
|
+
if (!keysB.includes(key))
|
|
21
|
+
return false;
|
|
22
|
+
if (!isSameStoreMask(a[key], b[key]))
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
function useStableStoreMask(mask) {
|
|
28
|
+
const ref = useRef(mask);
|
|
29
|
+
if (!isSameStoreMask(ref.current, mask))
|
|
30
|
+
ref.current = mask;
|
|
31
|
+
return ref.current;
|
|
32
|
+
}
|
|
33
|
+
function useLatestRef(value) {
|
|
34
|
+
const ref = useRef(value);
|
|
35
|
+
ref.current = value;
|
|
36
|
+
return ref;
|
|
37
|
+
}
|
|
38
|
+
export function useStoreNode(node, options = {}) {
|
|
39
|
+
const { current, drain, key, mode = "get", fallback } = options;
|
|
40
|
+
const [version, setVersion] = useState(0);
|
|
41
|
+
const refresh = useCallback(() => setVersion(v => v + 1), []);
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
return node.on(() => refresh(), { current, drain, key });
|
|
44
|
+
}, [node, current, drain, key, refresh]);
|
|
45
|
+
const value = useMemo(() => readNode(node, { mode, fallback }), [node, version, mode, fallback]);
|
|
46
|
+
const exists = useMemo(() => node.has(), [node, version]);
|
|
47
|
+
return useMemo(() => ({
|
|
48
|
+
node,
|
|
49
|
+
value,
|
|
50
|
+
exists,
|
|
51
|
+
path: node.path,
|
|
52
|
+
pathString: node.pathString,
|
|
53
|
+
refresh,
|
|
54
|
+
get: () => node.get(),
|
|
55
|
+
snapshot: () => node.snapshot(),
|
|
56
|
+
has: () => node.has(),
|
|
57
|
+
set: (next) => node.set(next),
|
|
58
|
+
replace: (next) => node.replace(next),
|
|
59
|
+
update: (mask, opts) => node.update(mask, opts),
|
|
60
|
+
}), [node, value, exists, refresh]);
|
|
61
|
+
}
|
|
62
|
+
export function useStoreKeys(node, options = {}) {
|
|
63
|
+
const state = useStoreNode(node, { ...options, mode: "snapshot" });
|
|
64
|
+
const keys = useMemo(() => {
|
|
65
|
+
const value = state.value;
|
|
66
|
+
return value != null && typeof value == "object" ? Reflect.ownKeys(value) : [];
|
|
67
|
+
}, [state.value]);
|
|
68
|
+
return useMemo(() => ({
|
|
69
|
+
node,
|
|
70
|
+
keys,
|
|
71
|
+
stringKeys: keys.map(String),
|
|
72
|
+
value: state.value,
|
|
73
|
+
exists: state.exists,
|
|
74
|
+
refresh: state.refresh,
|
|
75
|
+
has: (key) => state.value != null && typeof state.value == "object" && key in state.value,
|
|
76
|
+
get: (key) => state.value[key],
|
|
77
|
+
}), [node, keys, state]);
|
|
78
|
+
}
|
|
79
|
+
export function useStoreSelect(selection, options = {}) {
|
|
80
|
+
const { current, drain, key, fallback } = options;
|
|
81
|
+
const [version, setVersion] = useState(0);
|
|
82
|
+
const refresh = useCallback(() => setVersion(v => v + 1), []);
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
return selection.on(() => refresh(), { current, drain, key });
|
|
85
|
+
}, [selection, current, drain, key, refresh]);
|
|
86
|
+
const value = useMemo(() => {
|
|
87
|
+
const next = selection.get();
|
|
88
|
+
return next === undefined ? fallback : next;
|
|
89
|
+
}, [selection, version, fallback]);
|
|
90
|
+
return useMemo(() => ({
|
|
91
|
+
selection,
|
|
92
|
+
value,
|
|
93
|
+
mask: selection.mask,
|
|
94
|
+
paths: selection.paths,
|
|
95
|
+
refresh,
|
|
96
|
+
get: () => selection.get(),
|
|
97
|
+
}), [selection, value, refresh]);
|
|
98
|
+
}
|
|
99
|
+
export function useStoreMirror(remote, initial, options) {
|
|
100
|
+
const { mask, auto = true, current = true, drain, key, partial, onError } = options;
|
|
101
|
+
const stableMask = useStableStoreMask(mask);
|
|
102
|
+
const syncOptionsRef = useLatestRef({ current, drain, key, partial, onError });
|
|
103
|
+
const mountedRef = useRef(false);
|
|
104
|
+
const stopRef = useRef(null);
|
|
105
|
+
const [ready, setReady] = useState(false);
|
|
106
|
+
const [syncing, setSyncing] = useState(false);
|
|
107
|
+
const [error, setError] = useState(null);
|
|
108
|
+
const storeRef = useRef(null);
|
|
109
|
+
if (!storeRef.current || storeRef.current.remote !== remote) {
|
|
110
|
+
storeRef.current = {
|
|
111
|
+
remote,
|
|
112
|
+
store: ObserveAll2.createStoreMirror(remote, initial),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
const store = storeRef.current.store;
|
|
116
|
+
const selectionRaw = useMemo(() => store.update(stableMask), [store, stableMask]);
|
|
117
|
+
const selection = useStoreSelect(selectionRaw, { drain });
|
|
118
|
+
useEffect(() => {
|
|
119
|
+
mountedRef.current = true;
|
|
120
|
+
return () => {
|
|
121
|
+
mountedRef.current = false;
|
|
122
|
+
stopRef.current?.();
|
|
123
|
+
stopRef.current = null;
|
|
124
|
+
};
|
|
125
|
+
}, []);
|
|
126
|
+
const stop = useCallback(() => {
|
|
127
|
+
stopRef.current?.();
|
|
128
|
+
stopRef.current = null;
|
|
129
|
+
if (mountedRef.current)
|
|
130
|
+
setReady(false);
|
|
131
|
+
}, []);
|
|
132
|
+
const sync = useCallback(async (nextMask = stableMask, opts) => {
|
|
133
|
+
stopRef.current?.();
|
|
134
|
+
stopRef.current = null;
|
|
135
|
+
if (mountedRef.current) {
|
|
136
|
+
setSyncing(true);
|
|
137
|
+
setError(null);
|
|
138
|
+
}
|
|
139
|
+
try {
|
|
140
|
+
const latest = syncOptionsRef.current;
|
|
141
|
+
const overrideOnError = opts?.onError;
|
|
142
|
+
const off = await store.sync(nextMask, {
|
|
143
|
+
current: latest.current,
|
|
144
|
+
drain: latest.drain,
|
|
145
|
+
key: latest.key,
|
|
146
|
+
partial: latest.partial,
|
|
147
|
+
...opts,
|
|
148
|
+
onError(error) {
|
|
149
|
+
if (mountedRef.current)
|
|
150
|
+
setError(error);
|
|
151
|
+
(overrideOnError ?? syncOptionsRef.current.onError)?.(error);
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
stopRef.current = off;
|
|
155
|
+
if (mountedRef.current)
|
|
156
|
+
setReady(true);
|
|
157
|
+
return off;
|
|
158
|
+
}
|
|
159
|
+
catch (e) {
|
|
160
|
+
if (mountedRef.current)
|
|
161
|
+
setError(e);
|
|
162
|
+
throw e;
|
|
163
|
+
}
|
|
164
|
+
finally {
|
|
165
|
+
if (mountedRef.current)
|
|
166
|
+
setSyncing(false);
|
|
167
|
+
}
|
|
168
|
+
}, [store, stableMask, syncOptionsRef]);
|
|
169
|
+
useEffect(() => {
|
|
170
|
+
if (!auto)
|
|
171
|
+
return;
|
|
172
|
+
sync().catch(() => { });
|
|
173
|
+
return () => {
|
|
174
|
+
stopRef.current?.();
|
|
175
|
+
stopRef.current = null;
|
|
176
|
+
};
|
|
177
|
+
}, [auto, sync, current, drain, key, partial]);
|
|
178
|
+
return useMemo(() => ({
|
|
179
|
+
store,
|
|
180
|
+
value: selection.value,
|
|
181
|
+
selection,
|
|
182
|
+
ready,
|
|
183
|
+
syncing,
|
|
184
|
+
error,
|
|
185
|
+
sync,
|
|
186
|
+
stop,
|
|
187
|
+
}), [store, selection, ready, syncing, error, sync, stop]);
|
|
188
|
+
}
|
|
189
|
+
export function useListenEffect(listen, cb, opts) {
|
|
190
|
+
const cbRef = useRef(cb);
|
|
191
|
+
cbRef.current = cb;
|
|
192
|
+
const key = opts?.key;
|
|
193
|
+
const current = opts?.current;
|
|
194
|
+
useEffect(() => {
|
|
195
|
+
if (!listen)
|
|
196
|
+
return;
|
|
197
|
+
return listen.on((...args) => cbRef.current(...args), { key, current });
|
|
198
|
+
}, [listen, key, current]);
|
|
199
|
+
}
|
|
200
|
+
export function useStoreChangedPaths(listen, opts) {
|
|
201
|
+
const [state, setState] = useState({ change: opts?.initial, count: 0 });
|
|
202
|
+
useListenEffect(listen, (change) => {
|
|
203
|
+
setState(prev => ({ change, count: prev.count + 1 }));
|
|
204
|
+
}, { key: opts?.key });
|
|
205
|
+
return useMemo(() => ({
|
|
206
|
+
change: state.change,
|
|
207
|
+
paths: state.change?.paths ?? [],
|
|
208
|
+
count: state.count,
|
|
209
|
+
reset: () => setState({ change: opts?.initial, count: 0 }),
|
|
210
|
+
}), [state, opts?.initial]);
|
|
211
|
+
}
|
|
212
|
+
export function useListenArgs(listen, opts) {
|
|
213
|
+
const [value, setValue] = useState(opts?.initial);
|
|
214
|
+
useListenEffect(listen, (...args) => setValue(args), { key: opts?.key, current: opts?.current });
|
|
215
|
+
return value;
|
|
216
|
+
}
|
|
217
|
+
export function useListenValue(listen, opts) {
|
|
218
|
+
const [value, setValue] = useState(opts?.initial);
|
|
219
|
+
const map = opts?.map;
|
|
220
|
+
useListenEffect(listen, (...args) => {
|
|
221
|
+
setValue(map ? map(...args) : args[0]);
|
|
222
|
+
}, { key: opts?.key, current: opts?.current });
|
|
223
|
+
return value;
|
|
224
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wenay-react2",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.25",
|
|
4
4
|
"description": "Common react",
|
|
5
5
|
"strict": true,
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"react": "^19.2.0",
|
|
24
24
|
"react-dom": "^19.2.0",
|
|
25
25
|
"react-rnd": "^10.5.3",
|
|
26
|
-
"wenay-common2": "^1.0.
|
|
26
|
+
"wenay-common2": "^1.0.53"
|
|
27
27
|
},
|
|
28
28
|
"peerDependencies": {
|
|
29
29
|
"react": "^19.2.0",
|