@carto/ps-react-ui 4.4.3 → 4.5.1
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/dist/{download-config-Dqu78h2a.js → download-config-DemuQ3Jm.js} +9 -10
- package/dist/{download-config-Dqu78h2a.js.map → download-config-DemuQ3Jm.js.map} +1 -1
- package/dist/error-Cj8eUMrl.js +40 -0
- package/dist/error-Cj8eUMrl.js.map +1 -0
- package/dist/no-data-DkIt7Qt1.js +61 -0
- package/dist/no-data-DkIt7Qt1.js.map +1 -0
- package/dist/row-D4VOhcNI.js +34 -0
- package/dist/row-D4VOhcNI.js.map +1 -0
- package/dist/series-Bola3CmD.js +90 -0
- package/dist/series-Bola3CmD.js.map +1 -0
- package/dist/types/widgets/category/style.d.ts +1 -0
- package/dist/types/widgets/echart/shared-resize-observer.d.ts +12 -0
- package/dist/types/widgets/stores/index.d.ts +2 -1
- package/dist/types/widgets/stores/use-widget-selector.d.ts +35 -0
- package/dist/types/widgets/stores/widget-store-performance.test.d.ts +1 -0
- package/dist/types/widgets/stores/widget-store.d.ts +49 -27
- package/dist/types/widgets/table/types.d.ts +1 -1
- package/dist/use-widget-ref-BFazQvJK.js +22 -0
- package/dist/use-widget-ref-BFazQvJK.js.map +1 -0
- package/dist/use-widget-selector-DqRmWQ1K.js +12 -0
- package/dist/use-widget-selector-DqRmWQ1K.js.map +1 -0
- package/dist/widget-store-CIrb9RKP.js +263 -0
- package/dist/widget-store-CIrb9RKP.js.map +1 -0
- package/dist/widgets/actions.js +783 -817
- package/dist/widgets/actions.js.map +1 -1
- package/dist/widgets/bar.js +2 -2
- package/dist/widgets/category.js +259 -258
- package/dist/widgets/category.js.map +1 -1
- package/dist/widgets/echart.js +109 -99
- package/dist/widgets/echart.js.map +1 -1
- package/dist/widgets/error.js +1 -1
- package/dist/widgets/formula.js +71 -63
- package/dist/widgets/formula.js.map +1 -1
- package/dist/widgets/histogram.js +7 -8
- package/dist/widgets/histogram.js.map +1 -1
- package/dist/widgets/loader.js +53 -60
- package/dist/widgets/loader.js.map +1 -1
- package/dist/widgets/markdown.js +51 -50
- package/dist/widgets/markdown.js.map +1 -1
- package/dist/widgets/no-data.js +1 -1
- package/dist/widgets/pie.js +2 -2
- package/dist/widgets/range.js +146 -144
- package/dist/widgets/range.js.map +1 -1
- package/dist/widgets/scatterplot.js +2 -2
- package/dist/widgets/skeleton-loader.js +18 -17
- package/dist/widgets/skeleton-loader.js.map +1 -1
- package/dist/widgets/spread.js +110 -94
- package/dist/widgets/spread.js.map +1 -1
- package/dist/widgets/stores.js +5 -2
- package/dist/widgets/stores.js.map +1 -1
- package/dist/widgets/subheader.js +29 -29
- package/dist/widgets/subheader.js.map +1 -1
- package/dist/widgets/table.js +422 -436
- package/dist/widgets/table.js.map +1 -1
- package/dist/widgets/timeseries.js +2 -2
- package/dist/widgets/utils.js +1 -1
- package/dist/widgets/wrapper.js +156 -158
- package/dist/widgets/wrapper.js.map +1 -1
- package/dist/widgets.js +4 -4
- package/package.json +1 -1
- package/src/hooks/use-widget-ref.ts +3 -4
- package/src/widgets/actions/brush-toggle/brush-toggle.tsx +18 -32
- package/src/widgets/actions/change-column/change-column.tsx +15 -15
- package/src/widgets/actions/change-column/sortable-column-item.tsx +3 -1
- package/src/widgets/actions/download/download.tsx +4 -3
- package/src/widgets/actions/fullscreen/fullscreen.tsx +7 -11
- package/src/widgets/actions/lock-selection/lock-selection.tsx +12 -15
- package/src/widgets/actions/relative-data/relative-data.tsx +22 -26
- package/src/widgets/actions/searcher/searcher-toggle.tsx +11 -12
- package/src/widgets/actions/searcher/searcher.tsx +20 -21
- package/src/widgets/actions/stack-toggle/stack-toggle.tsx +15 -21
- package/src/widgets/actions/zoom-toggle/zoom-toggle.tsx +27 -43
- package/src/widgets/category/category-ui.tsx +30 -31
- package/src/widgets/category/style.ts +1 -0
- package/src/widgets/echart/echart-ui.test.tsx +20 -16
- package/src/widgets/echart/echart-ui.tsx +6 -12
- package/src/widgets/echart/echart.tsx +13 -27
- package/src/widgets/echart/shared-resize-observer.ts +45 -0
- package/src/widgets/error/error.tsx +7 -9
- package/src/widgets/formula/components/prefix.tsx +4 -6
- package/src/widgets/formula/components/row.tsx +4 -4
- package/src/widgets/formula/components/series.tsx +4 -6
- package/src/widgets/formula/components/suffix.tsx +4 -6
- package/src/widgets/formula/components/value.tsx +9 -16
- package/src/widgets/loader/loader.tsx +31 -44
- package/src/widgets/markdown/markdown.tsx +4 -7
- package/src/widgets/no-data/no-data.tsx +7 -10
- package/src/widgets/range/components/range-item.tsx +20 -18
- package/src/widgets/skeleton-loader/skeleton-loader.tsx +2 -5
- package/src/widgets/spread/components/max-value.tsx +14 -16
- package/src/widgets/spread/components/min-value.tsx +14 -16
- package/src/widgets/stores/index.ts +2 -1
- package/src/widgets/stores/use-widget-selector.ts +47 -0
- package/src/widgets/stores/widget-store-performance.test.ts +750 -0
- package/src/widgets/stores/widget-store.test.ts +81 -0
- package/src/widgets/stores/widget-store.ts +225 -44
- package/src/widgets/subheader/subheader.tsx +11 -3
- package/src/widgets/table/config.ts +0 -1
- package/src/widgets/table/hooks/use-pagination.ts +28 -52
- package/src/widgets/table/hooks/use-selection.ts +20 -24
- package/src/widgets/table/hooks/use-sort.ts +22 -39
- package/src/widgets/table/types.ts +1 -1
- package/src/widgets/wrapper/wrapper-ui.tsx +12 -13
- package/src/widgets/wrapper/wrapper.tsx +4 -6
- package/dist/error-CEkRPccv.js +0 -39
- package/dist/error-CEkRPccv.js.map +0 -1
- package/dist/no-data-hR3KcJ-_.js +0 -60
- package/dist/no-data-hR3KcJ-_.js.map +0 -1
- package/dist/row-DTCV0Ocm.js +0 -35
- package/dist/row-DTCV0Ocm.js.map +0 -1
- package/dist/series-CYNOu2Ju.js +0 -91
- package/dist/series-CYNOu2Ju.js.map +0 -1
- package/dist/use-widget-ref-wtFLDFCD.js +0 -25
- package/dist/use-widget-ref-wtFLDFCD.js.map +0 -1
- package/dist/widget-store-CzDt8oSK.js +0 -163
- package/dist/widget-store-CzDt8oSK.js.map +0 -1
|
@@ -604,6 +604,87 @@ describe('WidgetStore', () => {
|
|
|
604
604
|
|
|
605
605
|
expect(executionOrder).not.toContain('config-tool')
|
|
606
606
|
})
|
|
607
|
+
|
|
608
|
+
it('should not overwrite user-modified state when config lacks that property', async () => {
|
|
609
|
+
useWidgetStore.getState().setWidget(widgetId, {
|
|
610
|
+
type: 'table',
|
|
611
|
+
isLoading: false,
|
|
612
|
+
})
|
|
613
|
+
|
|
614
|
+
// Apply initial config (without selected — clean config pattern)
|
|
615
|
+
await useWidgetStore.getState().executeConfigPipeline(widgetId, {
|
|
616
|
+
columns: [],
|
|
617
|
+
selectable: true,
|
|
618
|
+
mode: 'local',
|
|
619
|
+
})
|
|
620
|
+
|
|
621
|
+
// Simulate user selecting rows via UI action
|
|
622
|
+
useWidgetStore.getState().setWidget(widgetId, {
|
|
623
|
+
selected: [1, 2, 3],
|
|
624
|
+
})
|
|
625
|
+
|
|
626
|
+
// Re-run config pipeline (e.g., triggered by tool registration or data change)
|
|
627
|
+
await useWidgetStore.getState().executeConfigPipeline(widgetId, {
|
|
628
|
+
columns: [],
|
|
629
|
+
selectable: true,
|
|
630
|
+
mode: 'local',
|
|
631
|
+
})
|
|
632
|
+
|
|
633
|
+
// Selection should be preserved — config doesn't include 'selected'
|
|
634
|
+
const widget = useWidgetStore.getState().getWidget(widgetId)
|
|
635
|
+
expect((widget as { selected?: (string | number)[] }).selected).toEqual([
|
|
636
|
+
1, 2, 3,
|
|
637
|
+
])
|
|
638
|
+
})
|
|
639
|
+
|
|
640
|
+
it('should not overwrite user-modified state when a tool passes base config through unchanged', async () => {
|
|
641
|
+
const baseConfig = {
|
|
642
|
+
option: { series: [{ name: 'A' }] },
|
|
643
|
+
selected: [] as number[],
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
useWidgetStore.getState().setWidget(widgetId, {
|
|
647
|
+
type: 'bar',
|
|
648
|
+
isLoading: false,
|
|
649
|
+
})
|
|
650
|
+
|
|
651
|
+
// Register a config tool that only changes `option` but spreads
|
|
652
|
+
// the entire base config (the typical { ...config, option: modified } pattern)
|
|
653
|
+
useWidgetStore.getState().registerTool(widgetId, {
|
|
654
|
+
id: 'stack-tool',
|
|
655
|
+
type: 'config',
|
|
656
|
+
order: 10,
|
|
657
|
+
enabled: true,
|
|
658
|
+
fn: (config: unknown) => {
|
|
659
|
+
const c = config as typeof baseConfig
|
|
660
|
+
return { ...c, option: { series: [{ name: 'A', stack: 'group' }] } }
|
|
661
|
+
},
|
|
662
|
+
})
|
|
663
|
+
|
|
664
|
+
// Initial pipeline sets option and selected
|
|
665
|
+
await useWidgetStore
|
|
666
|
+
.getState()
|
|
667
|
+
.executeConfigPipeline(widgetId, baseConfig)
|
|
668
|
+
|
|
669
|
+
// Simulate user modifying selected via UI
|
|
670
|
+
useWidgetStore.getState().setWidget(widgetId, { selected: [1, 2] })
|
|
671
|
+
|
|
672
|
+
// Re-run pipeline — tool still only changes `option`,
|
|
673
|
+
// but spreads { ...config, option: modified } which includes selected: []
|
|
674
|
+
await useWidgetStore
|
|
675
|
+
.getState()
|
|
676
|
+
.executeConfigPipeline(widgetId, baseConfig)
|
|
677
|
+
|
|
678
|
+
const widget = useWidgetStore.getState().getWidget(widgetId) as {
|
|
679
|
+
selected?: number[]
|
|
680
|
+
option?: { series?: { stack?: string }[] }
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
// Tool's change should be applied
|
|
684
|
+
expect(widget.option?.series?.[0]?.stack).toBe('group')
|
|
685
|
+
// User's selection should be preserved — the tool didn't change `selected`
|
|
686
|
+
expect(widget.selected).toEqual([1, 2])
|
|
687
|
+
})
|
|
607
688
|
})
|
|
608
689
|
|
|
609
690
|
describe('Tool Dependency Management', () => {
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { create } from 'zustand'
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
WidgetState,
|
|
4
|
+
WidgetStore,
|
|
5
|
+
ToolRegistration,
|
|
6
|
+
WidgetStoreActions,
|
|
7
|
+
} from './types'
|
|
3
8
|
|
|
4
9
|
// Track active pipeline executions for cancellation
|
|
5
10
|
const activePipelines = new Map<string, number>()
|
|
@@ -8,38 +13,40 @@ const activeConfigPipelines = new Map<string, number>()
|
|
|
8
13
|
/**
|
|
9
14
|
* Zustand store for managing widget state across the application.
|
|
10
15
|
*
|
|
11
|
-
*
|
|
16
|
+
* Provides centralized state management for all widget UI components, including
|
|
17
|
+
* data/config transformation pipelines via registered tools.
|
|
12
18
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
19
|
+
* **Performance optimizations:**
|
|
20
|
+
* - `registerTool` skips the store update when structural properties (order, enabled,
|
|
21
|
+
* type, disables) haven't changed — only `fn` and `config` are updated via direct
|
|
22
|
+
* mutation, avoiding a new `registeredTools` array reference and WidgetLoader
|
|
23
|
+
* pipeline cascades.
|
|
24
|
+
* - `setToolEnabled` skips the store update when the enabled state is already the
|
|
25
|
+
* requested value.
|
|
26
|
+
* - `executeToolPipeline` / `executeConfigPipeline` skip the final `set()` when
|
|
27
|
+
* the transformed data/config is referentially identical to what's already in the store.
|
|
28
|
+
* - Both pipelines support cancellation — newer executions for the same widget
|
|
29
|
+
* automatically cancel in-progress ones.
|
|
17
30
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* const widget = useWidgetStore((state) => state.widgets['my-widget'])
|
|
22
|
-
*
|
|
23
|
-
* useEffect(() => {
|
|
24
|
-
* setWidget({
|
|
25
|
-
* id: 'my-widget',
|
|
26
|
-
* type: 'formula',
|
|
27
|
-
* title: 'Total Sales',
|
|
28
|
-
* isLoading: false,
|
|
29
|
-
* visible: true,
|
|
30
|
-
* data: { value: 1000, prefix: '$' }
|
|
31
|
-
* })
|
|
32
|
-
* }, [setWidget])
|
|
31
|
+
* @example Reading widget state (prefer useWidgetSelector for performance)
|
|
32
|
+
* ```tsx
|
|
33
|
+
* import { useWidgetSelector } from '@carto/ps-react-ui/widgets'
|
|
33
34
|
*
|
|
34
|
-
*
|
|
35
|
+
* function MyWidget({ id }: { id: string }) {
|
|
36
|
+
* const { title, data } = useWidgetSelector(id, (w) => ({
|
|
37
|
+
* title: w?.title,
|
|
38
|
+
* data: w?.data,
|
|
39
|
+
* }))
|
|
40
|
+
* return <div>{title}: {JSON.stringify(data)}</div>
|
|
35
41
|
* }
|
|
36
42
|
* ```
|
|
37
43
|
*
|
|
38
|
-
* @example
|
|
44
|
+
* @example Writing widget state
|
|
39
45
|
* ```tsx
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
* const
|
|
46
|
+
* import { widgetStoreActions } from '@carto/ps-react-ui/widgets'
|
|
47
|
+
*
|
|
48
|
+
* const { setWidget } = widgetStoreActions
|
|
49
|
+
* setWidget('my-widget', { type: 'formula', isLoading: false, data: { value: 1000 } })
|
|
43
50
|
* ```
|
|
44
51
|
*/
|
|
45
52
|
export const useWidgetStore = create<WidgetStore>()((set, get) => ({
|
|
@@ -47,21 +54,23 @@ export const useWidgetStore = create<WidgetStore>()((set, get) => ({
|
|
|
47
54
|
widgets: {},
|
|
48
55
|
|
|
49
56
|
// Actions
|
|
50
|
-
|
|
57
|
+
|
|
58
|
+
/** Merges partial state into the widget entry. Creates the widget if it doesn't exist. */
|
|
59
|
+
setWidget: (id, widget) => {
|
|
51
60
|
set((state) => {
|
|
52
|
-
const
|
|
53
|
-
state.widgets[id] ?? ({} as WidgetStore['widgets'][string])
|
|
61
|
+
const prev = state.widgets[id] ?? ({} as WidgetStore['widgets'][string])
|
|
54
62
|
return {
|
|
55
63
|
widgets: {
|
|
56
64
|
...state.widgets,
|
|
57
65
|
[id]: {
|
|
58
|
-
...
|
|
66
|
+
...prev,
|
|
59
67
|
...widget,
|
|
60
68
|
id,
|
|
61
69
|
},
|
|
62
70
|
},
|
|
63
71
|
}
|
|
64
|
-
})
|
|
72
|
+
})
|
|
73
|
+
},
|
|
65
74
|
|
|
66
75
|
removeWidget: (id) =>
|
|
67
76
|
set((state) => {
|
|
@@ -81,10 +90,37 @@ export const useWidgetStore = create<WidgetStore>()((set, get) => ({
|
|
|
81
90
|
return get().widgets[id] as T | undefined
|
|
82
91
|
},
|
|
83
92
|
|
|
84
|
-
|
|
93
|
+
/**
|
|
94
|
+
* Registers a transformation tool for a widget's data or config pipeline.
|
|
95
|
+
*
|
|
96
|
+
* **No-op optimization:** When a tool with the same `id` already exists and its
|
|
97
|
+
* structural properties (`order`, `enabled`, `type`, `disables`) are unchanged,
|
|
98
|
+
* only `fn` and `config` are updated via direct mutation — no store update is
|
|
99
|
+
* triggered. This allows action components to include all reactive dependencies
|
|
100
|
+
* in their `useEffect` arrays without causing WidgetLoader pipeline cascades.
|
|
101
|
+
*/
|
|
102
|
+
registerTool: (widgetId: string, tool: ToolRegistration) => {
|
|
103
|
+
const current = get().widgets[widgetId]
|
|
104
|
+
const existingTool = current?.registeredTools?.find(
|
|
105
|
+
(t: ToolRegistration) => t.id === tool.id,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
// No-op: structural properties unchanged — update fn/config via direct mutation.
|
|
109
|
+
// Safe because fn and config are only consumed imperatively during pipeline execution.
|
|
110
|
+
if (
|
|
111
|
+
existingTool?.order === tool.order &&
|
|
112
|
+
existingTool.enabled === tool.enabled &&
|
|
113
|
+
existingTool.type === tool.type &&
|
|
114
|
+
existingTool.disables === tool.disables
|
|
115
|
+
) {
|
|
116
|
+
existingTool.fn = tool.fn
|
|
117
|
+
if (tool.config) existingTool.config = tool.config
|
|
118
|
+
return
|
|
119
|
+
}
|
|
120
|
+
|
|
85
121
|
set((state) => {
|
|
86
|
-
const
|
|
87
|
-
const registeredTools =
|
|
122
|
+
const widget = state.widgets[widgetId] ?? ({} as WidgetState)
|
|
123
|
+
const registeredTools = widget.registeredTools ?? []
|
|
88
124
|
|
|
89
125
|
// Remove existing tool with same id if present
|
|
90
126
|
const filteredTools = registeredTools.filter(
|
|
@@ -95,13 +131,14 @@ export const useWidgetStore = create<WidgetStore>()((set, get) => ({
|
|
|
95
131
|
widgets: {
|
|
96
132
|
...state.widgets,
|
|
97
133
|
[widgetId]: {
|
|
98
|
-
...
|
|
134
|
+
...widget,
|
|
99
135
|
id: widgetId,
|
|
100
136
|
registeredTools: [...filteredTools, tool],
|
|
101
137
|
},
|
|
102
138
|
},
|
|
103
139
|
}
|
|
104
|
-
})
|
|
140
|
+
})
|
|
141
|
+
},
|
|
105
142
|
|
|
106
143
|
unregisterTool: (widgetId: string, toolId: string) =>
|
|
107
144
|
set((state) => {
|
|
@@ -154,12 +191,26 @@ export const useWidgetStore = create<WidgetStore>()((set, get) => ({
|
|
|
154
191
|
}
|
|
155
192
|
}),
|
|
156
193
|
|
|
157
|
-
|
|
194
|
+
/**
|
|
195
|
+
* Updates a tool's enabled state.
|
|
196
|
+
*
|
|
197
|
+
* **No-op optimization:** Skips the store update if the tool already has the
|
|
198
|
+
* requested enabled value.
|
|
199
|
+
*/
|
|
200
|
+
setToolEnabled: (widgetId: string, toolId: string, enabled: boolean) => {
|
|
201
|
+
const current = get().widgets[widgetId]
|
|
202
|
+
if (current) {
|
|
203
|
+
const tool = current.registeredTools?.find(
|
|
204
|
+
(t: ToolRegistration) => t.id === toolId,
|
|
205
|
+
)
|
|
206
|
+
if (tool?.enabled === enabled) return
|
|
207
|
+
}
|
|
208
|
+
|
|
158
209
|
set((state) => {
|
|
159
|
-
const
|
|
160
|
-
if (!
|
|
210
|
+
const widget = state.widgets[widgetId]
|
|
211
|
+
if (!widget) return state
|
|
161
212
|
|
|
162
|
-
const registeredTools =
|
|
213
|
+
const registeredTools = widget.registeredTools ?? []
|
|
163
214
|
const updatedTools = registeredTools.map((tool: ToolRegistration) =>
|
|
164
215
|
tool.id === toolId ? { ...tool, enabled } : tool,
|
|
165
216
|
)
|
|
@@ -168,13 +219,26 @@ export const useWidgetStore = create<WidgetStore>()((set, get) => ({
|
|
|
168
219
|
widgets: {
|
|
169
220
|
...state.widgets,
|
|
170
221
|
[widgetId]: {
|
|
171
|
-
...
|
|
222
|
+
...widget,
|
|
172
223
|
registeredTools: updatedTools,
|
|
173
224
|
},
|
|
174
225
|
},
|
|
175
226
|
}
|
|
176
|
-
})
|
|
227
|
+
})
|
|
228
|
+
},
|
|
177
229
|
|
|
230
|
+
/**
|
|
231
|
+
* Executes the data transformation pipeline for a widget.
|
|
232
|
+
*
|
|
233
|
+
* Filters to enabled data-type tools (respecting `disables`), sorts by `order`,
|
|
234
|
+
* and chains their `fn` calls. Supports async tools.
|
|
235
|
+
*
|
|
236
|
+
* **Cancellation:** Newer executions for the same widget automatically cancel
|
|
237
|
+
* in-progress ones via a version counter.
|
|
238
|
+
*
|
|
239
|
+
* **No-op optimization:** Skips the final `set()` if the transformed data is
|
|
240
|
+
* referentially identical (`Object.is`) to what's already in the store.
|
|
241
|
+
*/
|
|
178
242
|
executeToolPipeline: async (widgetId: string, sourceData: unknown) => {
|
|
179
243
|
const widget = get().widgets[widgetId]
|
|
180
244
|
if (!widget) return
|
|
@@ -221,6 +285,15 @@ export const useWidgetStore = create<WidgetStore>()((set, get) => ({
|
|
|
221
285
|
}
|
|
222
286
|
}
|
|
223
287
|
|
|
288
|
+
// Skip store update if data hasn't changed (e.g., passthrough pipeline with no tools)
|
|
289
|
+
const widgetAfter = get().widgets[widgetId]
|
|
290
|
+
if (widgetAfter && Object.is(widgetAfter.data, transformedData)) {
|
|
291
|
+
if (activePipelines.get(widgetId) === currentExecution) {
|
|
292
|
+
activePipelines.delete(widgetId)
|
|
293
|
+
}
|
|
294
|
+
return
|
|
295
|
+
}
|
|
296
|
+
|
|
224
297
|
// Single store update with final transformed data
|
|
225
298
|
set((state) => {
|
|
226
299
|
const currentWidget = state.widgets[widgetId]
|
|
@@ -243,6 +316,18 @@ export const useWidgetStore = create<WidgetStore>()((set, get) => ({
|
|
|
243
316
|
}
|
|
244
317
|
},
|
|
245
318
|
|
|
319
|
+
/**
|
|
320
|
+
* Executes the config transformation pipeline for a widget.
|
|
321
|
+
*
|
|
322
|
+
* Filters to enabled config-type tools (respecting `disables`), sorts by `order`,
|
|
323
|
+
* and chains their `fn` calls. The transformed config is spread into the widget state.
|
|
324
|
+
*
|
|
325
|
+
* **Cancellation:** Newer executions for the same widget automatically cancel
|
|
326
|
+
* in-progress ones via a version counter.
|
|
327
|
+
*
|
|
328
|
+
* **No-op optimization:** Skips the final `set()` when the config object is unchanged
|
|
329
|
+
* and all its properties already match what's in the store.
|
|
330
|
+
*/
|
|
246
331
|
executeConfigPipeline: async (widgetId: string, baseConfig: object) => {
|
|
247
332
|
const widget = get().widgets[widgetId]
|
|
248
333
|
if (!widget) return
|
|
@@ -287,7 +372,50 @@ export const useWidgetStore = create<WidgetStore>()((set, get) => ({
|
|
|
287
372
|
}
|
|
288
373
|
}
|
|
289
374
|
|
|
290
|
-
//
|
|
375
|
+
// Build a patch that only includes properties the pipeline should set.
|
|
376
|
+
// A property is applied when:
|
|
377
|
+
// 1. A config tool explicitly changed it (transformedConfig[k] !== baseConfig[k])
|
|
378
|
+
// 2. First pipeline run for this widget (no _lastConfig yet)
|
|
379
|
+
// 3. The widget value still matches what the pipeline last set — meaning the
|
|
380
|
+
// user hasn't modified it via setWidget, so it's safe to overwrite.
|
|
381
|
+
// A property is SKIPPED when the widget value differs from _lastConfig,
|
|
382
|
+
// meaning the user (or a hook) changed it since the last pipeline run.
|
|
383
|
+
const base = baseConfig as Record<string, unknown>
|
|
384
|
+
const result = transformedConfig as Record<string, unknown>
|
|
385
|
+
const widgetNow = get().widgets[widgetId] as unknown as Record<
|
|
386
|
+
string,
|
|
387
|
+
unknown
|
|
388
|
+
>
|
|
389
|
+
const lastConfig = widgetNow?._lastConfig as
|
|
390
|
+
| Record<string, unknown>
|
|
391
|
+
| undefined
|
|
392
|
+
|
|
393
|
+
const patch: Record<string, unknown> = {}
|
|
394
|
+
for (const key of Object.keys(result)) {
|
|
395
|
+
const toolChanged = !Object.is(result[key], base[key])
|
|
396
|
+
const isFirstRun = !lastConfig || !(key in lastConfig)
|
|
397
|
+
const userUnmodified =
|
|
398
|
+
!isFirstRun && Object.is(widgetNow[key], lastConfig[key])
|
|
399
|
+
|
|
400
|
+
if (toolChanged || isFirstRun || userUnmodified) {
|
|
401
|
+
patch[key] = result[key]
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Skip store update if every patch value already matches the widget
|
|
406
|
+
const hasChanges = Object.keys(patch).some(
|
|
407
|
+
(k) => !Object.is(widgetNow?.[k], patch[k]),
|
|
408
|
+
)
|
|
409
|
+
const configRefChanged = !Object.is(transformedConfig, lastConfig)
|
|
410
|
+
|
|
411
|
+
if (!hasChanges && !configRefChanged) {
|
|
412
|
+
if (activeConfigPipelines.get(widgetId) === currentExecution) {
|
|
413
|
+
activeConfigPipelines.delete(widgetId)
|
|
414
|
+
}
|
|
415
|
+
return
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Apply the patch and store the pipeline output for next run's comparison
|
|
291
419
|
set((state) => {
|
|
292
420
|
const currentWidget = state.widgets[widgetId]
|
|
293
421
|
if (!currentWidget) return state
|
|
@@ -297,7 +425,8 @@ export const useWidgetStore = create<WidgetStore>()((set, get) => ({
|
|
|
297
425
|
...state.widgets,
|
|
298
426
|
[widgetId]: {
|
|
299
427
|
...currentWidget,
|
|
300
|
-
...
|
|
428
|
+
...patch,
|
|
429
|
+
_lastConfig: transformedConfig,
|
|
301
430
|
},
|
|
302
431
|
},
|
|
303
432
|
}
|
|
@@ -308,3 +437,55 @@ export const useWidgetStore = create<WidgetStore>()((set, get) => ({
|
|
|
308
437
|
}
|
|
309
438
|
},
|
|
310
439
|
}))
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Stable references to store actions, accessible without creating a subscription.
|
|
443
|
+
*
|
|
444
|
+
* Use this instead of `useWidgetStore((state) => state.setWidget)` to avoid
|
|
445
|
+
* unnecessary subscriber evaluations. Actions are stable functions that never
|
|
446
|
+
* change, so subscribing to them wastes cycles on every store update.
|
|
447
|
+
*
|
|
448
|
+
* @example
|
|
449
|
+
* ```tsx
|
|
450
|
+
* import { widgetStoreActions } from '@carto/ps-react-ui/widgets'
|
|
451
|
+
*
|
|
452
|
+
* const { setWidget, registerTool } = widgetStoreActions
|
|
453
|
+
*
|
|
454
|
+
* useEffect(() => {
|
|
455
|
+
* registerTool(id, { id: 'my-tool', order: 10, enabled: true, fn: (d) => d })
|
|
456
|
+
* return () => widgetStoreActions.unregisterTool(id, 'my-tool')
|
|
457
|
+
* }, [id])
|
|
458
|
+
* ```
|
|
459
|
+
*/
|
|
460
|
+
export const widgetStoreActions: WidgetStoreActions = {
|
|
461
|
+
get setWidget() {
|
|
462
|
+
return useWidgetStore.getState().setWidget
|
|
463
|
+
},
|
|
464
|
+
get removeWidget() {
|
|
465
|
+
return useWidgetStore.getState().removeWidget
|
|
466
|
+
},
|
|
467
|
+
get clearWidgets() {
|
|
468
|
+
return useWidgetStore.getState().clearWidgets
|
|
469
|
+
},
|
|
470
|
+
get getWidget() {
|
|
471
|
+
return useWidgetStore.getState().getWidget
|
|
472
|
+
},
|
|
473
|
+
get registerTool() {
|
|
474
|
+
return useWidgetStore.getState().registerTool
|
|
475
|
+
},
|
|
476
|
+
get unregisterTool() {
|
|
477
|
+
return useWidgetStore.getState().unregisterTool
|
|
478
|
+
},
|
|
479
|
+
get updateToolConfig() {
|
|
480
|
+
return useWidgetStore.getState().updateToolConfig
|
|
481
|
+
},
|
|
482
|
+
get setToolEnabled() {
|
|
483
|
+
return useWidgetStore.getState().setToolEnabled
|
|
484
|
+
},
|
|
485
|
+
get executeToolPipeline() {
|
|
486
|
+
return useWidgetStore.getState().executeToolPipeline
|
|
487
|
+
},
|
|
488
|
+
get executeConfigPipeline() {
|
|
489
|
+
return useWidgetStore.getState().executeConfigPipeline
|
|
490
|
+
},
|
|
491
|
+
} as WidgetStoreActions
|
|
@@ -19,9 +19,17 @@ export function WidgetSubHeader({
|
|
|
19
19
|
sx,
|
|
20
20
|
}: WidgetSubHeaderProps) {
|
|
21
21
|
return (
|
|
22
|
-
<Box sx={{ ...styles.root, ...sx }}>
|
|
23
|
-
{slotLeft &&
|
|
24
|
-
|
|
22
|
+
<Box sx={{ ...styles.root, ...sx }} className='widget-subheader'>
|
|
23
|
+
{slotLeft && (
|
|
24
|
+
<Box sx={styles.slotLeft} className='widget-subheader-slot-left'>
|
|
25
|
+
{slotLeft}
|
|
26
|
+
</Box>
|
|
27
|
+
)}
|
|
28
|
+
{slotRight && (
|
|
29
|
+
<Box sx={styles.slotRight} className='widget-subheader-slot-right'>
|
|
30
|
+
{slotRight}
|
|
31
|
+
</Box>
|
|
32
|
+
)}
|
|
25
33
|
</Box>
|
|
26
34
|
)
|
|
27
35
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { useCallback, useMemo } from 'react'
|
|
2
2
|
import type { TableRow, TableWidgetState } from '../types'
|
|
3
3
|
import { paginateData } from '../helpers'
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { widgetStoreActions } from '../../stores/widget-store'
|
|
5
|
+
import { useWidgetSelector } from '../../stores/use-widget-selector'
|
|
6
6
|
import {
|
|
7
7
|
DEFAULT_PAGE,
|
|
8
8
|
DEFAULT_ROWS_PER_PAGE,
|
|
@@ -53,50 +53,26 @@ export function usePagination<T extends TableRow>(
|
|
|
53
53
|
widgetId: string,
|
|
54
54
|
data: T[],
|
|
55
55
|
): UsePaginationResult<T> {
|
|
56
|
-
// Get store
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
)
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
?.rowsPerPage ?? DEFAULT_ROWS_PER_PAGE,
|
|
77
|
-
),
|
|
78
|
-
)
|
|
79
|
-
const rowsPerPageOptions = useWidgetStore(
|
|
80
|
-
useShallow(
|
|
81
|
-
(state) =>
|
|
82
|
-
(state.widgets[widgetId] as TableWidgetState | undefined)?.pagination
|
|
83
|
-
?.rowsPerPageOptions ?? DEFAULT_ROWS_PER_PAGE_OPTIONS,
|
|
84
|
-
),
|
|
85
|
-
)
|
|
86
|
-
const total = useWidgetStore(
|
|
87
|
-
useShallow(
|
|
88
|
-
(state) =>
|
|
89
|
-
(state.widgets[widgetId] as TableWidgetState | undefined)?.pagination
|
|
90
|
-
?.total ?? data.length,
|
|
91
|
-
),
|
|
92
|
-
)
|
|
93
|
-
|
|
94
|
-
const mode = useWidgetStore(
|
|
95
|
-
useShallow((state) => {
|
|
96
|
-
const widget = state.widgets[widgetId] as TableWidgetState | undefined
|
|
97
|
-
return widget?.mode
|
|
98
|
-
}),
|
|
99
|
-
)
|
|
56
|
+
// Get store state
|
|
57
|
+
const {
|
|
58
|
+
paginationEnabled,
|
|
59
|
+
page,
|
|
60
|
+
rowsPerPage,
|
|
61
|
+
rowsPerPageOptions,
|
|
62
|
+
total,
|
|
63
|
+
mode,
|
|
64
|
+
} = useWidgetSelector(widgetId, (w) => {
|
|
65
|
+
const widget = w as TableWidgetState | undefined
|
|
66
|
+
return {
|
|
67
|
+
paginationEnabled: !!widget?.pagination,
|
|
68
|
+
page: widget?.pagination?.page ?? DEFAULT_PAGE,
|
|
69
|
+
rowsPerPage: widget?.pagination?.rowsPerPage ?? DEFAULT_ROWS_PER_PAGE,
|
|
70
|
+
rowsPerPageOptions:
|
|
71
|
+
widget?.pagination?.rowsPerPageOptions ?? DEFAULT_ROWS_PER_PAGE_OPTIONS,
|
|
72
|
+
total: widget?.pagination?.total ?? data.length,
|
|
73
|
+
mode: widget?.mode,
|
|
74
|
+
}
|
|
75
|
+
})
|
|
100
76
|
|
|
101
77
|
// Calculate max page
|
|
102
78
|
const maxPage = Math.max(0, Math.ceil(total / (rowsPerPage ?? 1)) - 1)
|
|
@@ -105,7 +81,7 @@ export function usePagination<T extends TableRow>(
|
|
|
105
81
|
const setPage = useCallback(
|
|
106
82
|
(newPage: number) => {
|
|
107
83
|
// Read current state directly from store to avoid stale closures
|
|
108
|
-
const widget = getWidget<TableWidgetState>(widgetId)
|
|
84
|
+
const widget = widgetStoreActions.getWidget<TableWidgetState>(widgetId)
|
|
109
85
|
const currentTotal = widget?.pagination?.total ?? data.length
|
|
110
86
|
const currentRowsPerPage =
|
|
111
87
|
widget?.pagination?.rowsPerPage ?? DEFAULT_ROWS_PER_PAGE
|
|
@@ -115,21 +91,21 @@ export function usePagination<T extends TableRow>(
|
|
|
115
91
|
)
|
|
116
92
|
const boundedPage = Math.max(0, Math.min(newPage, currentMaxPage))
|
|
117
93
|
|
|
118
|
-
setWidget<TableWidgetState>(widgetId, {
|
|
94
|
+
widgetStoreActions.setWidget<TableWidgetState>(widgetId, {
|
|
119
95
|
pagination: {
|
|
120
96
|
...widget?.pagination,
|
|
121
97
|
page: boundedPage,
|
|
122
98
|
},
|
|
123
99
|
})
|
|
124
100
|
},
|
|
125
|
-
[data.length,
|
|
101
|
+
[data.length, widgetId],
|
|
126
102
|
)
|
|
127
103
|
|
|
128
104
|
// Set rows per page and reset to first page
|
|
129
105
|
const setRowsPerPage = useCallback(
|
|
130
106
|
(newRowsPerPage: number) => {
|
|
131
|
-
const widget = getWidget<TableWidgetState>(widgetId)
|
|
132
|
-
setWidget<TableWidgetState>(widgetId, {
|
|
107
|
+
const widget = widgetStoreActions.getWidget<TableWidgetState>(widgetId)
|
|
108
|
+
widgetStoreActions.setWidget<TableWidgetState>(widgetId, {
|
|
133
109
|
pagination: {
|
|
134
110
|
...widget?.pagination,
|
|
135
111
|
rowsPerPage: newRowsPerPage,
|
|
@@ -137,7 +113,7 @@ export function usePagination<T extends TableRow>(
|
|
|
137
113
|
},
|
|
138
114
|
})
|
|
139
115
|
},
|
|
140
|
-
[
|
|
116
|
+
[widgetId],
|
|
141
117
|
)
|
|
142
118
|
|
|
143
119
|
// Navigation helpers
|