@deephaven/iris-grid 1.22.1 → 1.22.2-alpha-pivot-builder.0730ba6.0

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.
Files changed (201) hide show
  1. package/README.md +296 -1
  2. package/dist/AdvancedFilterCreator.js +65 -102
  3. package/dist/AdvancedFilterCreator.js.map +1 -1
  4. package/dist/AdvancedFilterCreatorFilterItem.js +22 -37
  5. package/dist/AdvancedFilterCreatorFilterItem.js.map +1 -1
  6. package/dist/AdvancedFilterCreatorSelectValue.js +37 -63
  7. package/dist/AdvancedFilterCreatorSelectValue.js.map +1 -1
  8. package/dist/AdvancedFilterCreatorSelectValueList.js +29 -59
  9. package/dist/AdvancedFilterCreatorSelectValueList.js.map +1 -1
  10. package/dist/ColumnHeaderGroup.js +6 -8
  11. package/dist/ColumnHeaderGroup.js.map +1 -1
  12. package/dist/ColumnStatistics.js +22 -28
  13. package/dist/ColumnStatistics.js.map +1 -1
  14. package/dist/CommonTypes.d.ts +84 -2
  15. package/dist/CommonTypes.d.ts.map +1 -1
  16. package/dist/CommonTypes.js.map +1 -1
  17. package/dist/CrossColumnSearch.js +20 -29
  18. package/dist/CrossColumnSearch.js.map +1 -1
  19. package/dist/FilterInputField.css +3 -0
  20. package/dist/FilterInputField.css.map +1 -1
  21. package/dist/FilterInputField.js +19 -46
  22. package/dist/FilterInputField.js.map +1 -1
  23. package/dist/GotoRow.js +32 -29
  24. package/dist/GotoRow.js.map +1 -1
  25. package/dist/IrisGrid.css +3 -0
  26. package/dist/IrisGrid.css.map +1 -1
  27. package/dist/IrisGrid.d.ts +99 -4
  28. package/dist/IrisGrid.d.ts.map +1 -1
  29. package/dist/IrisGrid.js +732 -860
  30. package/dist/IrisGrid.js.map +1 -1
  31. package/dist/IrisGridBottomBar.js +10 -11
  32. package/dist/IrisGridBottomBar.js.map +1 -1
  33. package/dist/IrisGridCacheUtils.js +28 -8
  34. package/dist/IrisGridCacheUtils.js.map +1 -1
  35. package/dist/IrisGridCellOverflowModal.js +30 -13
  36. package/dist/IrisGridCellOverflowModal.js.map +1 -1
  37. package/dist/IrisGridCellRendererUtils.js +6 -12
  38. package/dist/IrisGridCellRendererUtils.js.map +1 -1
  39. package/dist/IrisGridCopyHandler.js +28 -47
  40. package/dist/IrisGridCopyHandler.js.map +1 -1
  41. package/dist/IrisGridMetricCalculator.js +28 -51
  42. package/dist/IrisGridMetricCalculator.js.map +1 -1
  43. package/dist/IrisGridModel.d.ts +36 -1
  44. package/dist/IrisGridModel.d.ts.map +1 -1
  45. package/dist/IrisGridModel.js +42 -1
  46. package/dist/IrisGridModel.js.map +1 -1
  47. package/dist/IrisGridModelUpdater.js +32 -30
  48. package/dist/IrisGridModelUpdater.js.map +1 -1
  49. package/dist/IrisGridModelWidgetProps.d.ts +26 -0
  50. package/dist/IrisGridModelWidgetProps.d.ts.map +1 -0
  51. package/dist/IrisGridModelWidgetProps.js +2 -0
  52. package/dist/IrisGridModelWidgetProps.js.map +1 -0
  53. package/dist/IrisGridPartitionSelector.js +28 -62
  54. package/dist/IrisGridPartitionSelector.js.map +1 -1
  55. package/dist/IrisGridProxyModel.d.ts.map +1 -1
  56. package/dist/IrisGridProxyModel.js +41 -14
  57. package/dist/IrisGridProxyModel.js.map +1 -1
  58. package/dist/IrisGridRenderer.js +162 -264
  59. package/dist/IrisGridRenderer.js.map +1 -1
  60. package/dist/IrisGridTableModel.js +23 -16
  61. package/dist/IrisGridTableModel.js.map +1 -1
  62. package/dist/IrisGridTableModelTemplate.js +49 -73
  63. package/dist/IrisGridTableModelTemplate.js.map +1 -1
  64. package/dist/IrisGridTestUtils.js +19 -19
  65. package/dist/IrisGridTestUtils.js.map +1 -1
  66. package/dist/IrisGridTextCellRenderer.d.ts.map +1 -1
  67. package/dist/IrisGridTextCellRenderer.js +36 -53
  68. package/dist/IrisGridTextCellRenderer.js.map +1 -1
  69. package/dist/IrisGridTheme.js +9 -1
  70. package/dist/IrisGridTheme.js.map +1 -1
  71. package/dist/IrisGridThemeProvider.js +5 -7
  72. package/dist/IrisGridThemeProvider.js.map +1 -1
  73. package/dist/IrisGridTreeTableModel.js +5 -9
  74. package/dist/IrisGridTreeTableModel.js.map +1 -1
  75. package/dist/IrisGridUtils.d.ts +25 -2
  76. package/dist/IrisGridUtils.d.ts.map +1 -1
  77. package/dist/IrisGridUtils.js +297 -294
  78. package/dist/IrisGridUtils.js.map +1 -1
  79. package/dist/LazyIrisGrid.d.ts +1 -1
  80. package/dist/NoPastePermissionModal.js +3 -5
  81. package/dist/NoPastePermissionModal.js.map +1 -1
  82. package/dist/PendingDataBottomBar.js +27 -16
  83. package/dist/PendingDataBottomBar.js.map +1 -1
  84. package/dist/ToastBottomBar.js +16 -8
  85. package/dist/ToastBottomBar.js.map +1 -1
  86. package/dist/format-context-menus/CustomFormatAction.js +11 -24
  87. package/dist/format-context-menus/CustomFormatAction.js.map +1 -1
  88. package/dist/index.d.ts +1 -0
  89. package/dist/index.d.ts.map +1 -1
  90. package/dist/index.js +1 -0
  91. package/dist/index.js.map +1 -1
  92. package/dist/key-handlers/CopyCellKeyHandler.js +3 -4
  93. package/dist/key-handlers/CopyCellKeyHandler.js.map +1 -1
  94. package/dist/key-handlers/CopyKeyHandler.js +1 -3
  95. package/dist/key-handlers/CopyKeyHandler.js.map +1 -1
  96. package/dist/key-handlers/ReverseKeyHandler.js +1 -3
  97. package/dist/key-handlers/ReverseKeyHandler.js.map +1 -1
  98. package/dist/mousehandlers/IrisGridCellOverflowMouseHandler.js +35 -59
  99. package/dist/mousehandlers/IrisGridCellOverflowMouseHandler.js.map +1 -1
  100. package/dist/mousehandlers/IrisGridColumnSelectMouseHandler.js +13 -29
  101. package/dist/mousehandlers/IrisGridColumnSelectMouseHandler.js.map +1 -1
  102. package/dist/mousehandlers/IrisGridColumnTooltipMouseHandler.js +8 -17
  103. package/dist/mousehandlers/IrisGridColumnTooltipMouseHandler.js.map +1 -1
  104. package/dist/mousehandlers/IrisGridContextMenuHandler.js +96 -199
  105. package/dist/mousehandlers/IrisGridContextMenuHandler.js.map +1 -1
  106. package/dist/mousehandlers/IrisGridDataSelectMouseHandler.js +2 -4
  107. package/dist/mousehandlers/IrisGridDataSelectMouseHandler.js.map +1 -1
  108. package/dist/mousehandlers/IrisGridFilterMouseHandler.js +16 -30
  109. package/dist/mousehandlers/IrisGridFilterMouseHandler.js.map +1 -1
  110. package/dist/mousehandlers/IrisGridPartitionedTableMouseHandler.js +5 -13
  111. package/dist/mousehandlers/IrisGridPartitionedTableMouseHandler.js.map +1 -1
  112. package/dist/mousehandlers/IrisGridRowTreeMouseHandler.js +20 -36
  113. package/dist/mousehandlers/IrisGridRowTreeMouseHandler.js.map +1 -1
  114. package/dist/mousehandlers/IrisGridSortMouseHandler.js +3 -5
  115. package/dist/mousehandlers/IrisGridSortMouseHandler.js.map +1 -1
  116. package/dist/mousehandlers/IrisGridTokenMouseHandler.js +9 -18
  117. package/dist/mousehandlers/IrisGridTokenMouseHandler.js.map +1 -1
  118. package/dist/mousehandlers/PendingMouseHandler.js +10 -18
  119. package/dist/mousehandlers/PendingMouseHandler.js.map +1 -1
  120. package/dist/sidebar/AdvancedSettingsMenu.js +12 -5
  121. package/dist/sidebar/AdvancedSettingsMenu.js.map +1 -1
  122. package/dist/sidebar/ChartBuilder.js +45 -102
  123. package/dist/sidebar/ChartBuilder.js.map +1 -1
  124. package/dist/sidebar/CustomColumnBuilder.js +35 -77
  125. package/dist/sidebar/CustomColumnBuilder.js.map +1 -1
  126. package/dist/sidebar/CustomColumnInput.js +9 -11
  127. package/dist/sidebar/CustomColumnInput.js.map +1 -1
  128. package/dist/sidebar/InputEditor.js +17 -24
  129. package/dist/sidebar/InputEditor.js.map +1 -1
  130. package/dist/sidebar/IrisGridTableOptionsWidgetProps.d.ts +22 -0
  131. package/dist/sidebar/IrisGridTableOptionsWidgetProps.d.ts.map +1 -0
  132. package/dist/sidebar/IrisGridTableOptionsWidgetProps.js +2 -0
  133. package/dist/sidebar/IrisGridTableOptionsWidgetProps.js.map +1 -0
  134. package/dist/sidebar/OptionType.d.ts +8 -0
  135. package/dist/sidebar/OptionType.d.ts.map +1 -1
  136. package/dist/sidebar/OptionType.js +7 -0
  137. package/dist/sidebar/OptionType.js.map +1 -1
  138. package/dist/sidebar/PluginTableOptionsErrorBoundary.d.ts +25 -0
  139. package/dist/sidebar/PluginTableOptionsErrorBoundary.d.ts.map +1 -0
  140. package/dist/sidebar/PluginTableOptionsErrorBoundary.js +49 -0
  141. package/dist/sidebar/PluginTableOptionsErrorBoundary.js.map +1 -0
  142. package/dist/sidebar/RollupRows.js +72 -113
  143. package/dist/sidebar/RollupRows.js.map +1 -1
  144. package/dist/sidebar/SelectDistinctBuilder.js +16 -33
  145. package/dist/sidebar/SelectDistinctBuilder.js.map +1 -1
  146. package/dist/sidebar/TableCsvExporter.js +50 -74
  147. package/dist/sidebar/TableCsvExporter.js.map +1 -1
  148. package/dist/sidebar/TableSaver.js +18 -42
  149. package/dist/sidebar/TableSaver.js.map +1 -1
  150. package/dist/sidebar/aggregations/AggregationEdit.js +25 -18
  151. package/dist/sidebar/aggregations/AggregationEdit.js.map +1 -1
  152. package/dist/sidebar/aggregations/AggregationUtils.js +1 -3
  153. package/dist/sidebar/aggregations/AggregationUtils.js.map +1 -1
  154. package/dist/sidebar/aggregations/Aggregations.js +45 -34
  155. package/dist/sidebar/aggregations/Aggregations.js.map +1 -1
  156. package/dist/sidebar/conditional-formatting/ColumnFormatEditor.js +35 -24
  157. package/dist/sidebar/conditional-formatting/ColumnFormatEditor.js.map +1 -1
  158. package/dist/sidebar/conditional-formatting/ConditionEditor.js +38 -21
  159. package/dist/sidebar/conditional-formatting/ConditionEditor.js.map +1 -1
  160. package/dist/sidebar/conditional-formatting/ConditionalFormatEditor.js +27 -11
  161. package/dist/sidebar/conditional-formatting/ConditionalFormatEditor.js.map +1 -1
  162. package/dist/sidebar/conditional-formatting/ConditionalFormattingMenu.js +9 -11
  163. package/dist/sidebar/conditional-formatting/ConditionalFormattingMenu.js.map +1 -1
  164. package/dist/sidebar/conditional-formatting/ConditionalFormattingUtils.js +48 -57
  165. package/dist/sidebar/conditional-formatting/ConditionalFormattingUtils.js.map +1 -1
  166. package/dist/sidebar/conditional-formatting/RowFormatEditor.js +35 -24
  167. package/dist/sidebar/conditional-formatting/RowFormatEditor.js.map +1 -1
  168. package/dist/sidebar/conditional-formatting/StyleEditor.js +26 -11
  169. package/dist/sidebar/conditional-formatting/StyleEditor.js.map +1 -1
  170. package/dist/sidebar/index.d.ts +3 -2
  171. package/dist/sidebar/index.d.ts.map +1 -1
  172. package/dist/sidebar/index.js.map +1 -1
  173. package/dist/sidebar/visibility-ordering-builder/SearchItem.js +5 -7
  174. package/dist/sidebar/visibility-ordering-builder/SearchItem.js.map +1 -1
  175. package/dist/sidebar/visibility-ordering-builder/SearchWithModal.css +3 -0
  176. package/dist/sidebar/visibility-ordering-builder/SearchWithModal.css.map +1 -1
  177. package/dist/sidebar/visibility-ordering-builder/SearchWithModal.js +24 -18
  178. package/dist/sidebar/visibility-ordering-builder/SearchWithModal.js.map +1 -1
  179. package/dist/sidebar/visibility-ordering-builder/VisibilityOrderingBuilder.js +166 -256
  180. package/dist/sidebar/visibility-ordering-builder/VisibilityOrderingBuilder.js.map +1 -1
  181. package/dist/sidebar/visibility-ordering-builder/VisibilityOrderingBuilderUtils.js +4 -11
  182. package/dist/sidebar/visibility-ordering-builder/VisibilityOrderingBuilderUtils.js.map +1 -1
  183. package/dist/sidebar/visibility-ordering-builder/VisibilityOrderingGroup.js +28 -14
  184. package/dist/sidebar/visibility-ordering-builder/VisibilityOrderingGroup.js.map +1 -1
  185. package/dist/sidebar/visibility-ordering-builder/VisibilityOrderingItem.js +16 -19
  186. package/dist/sidebar/visibility-ordering-builder/VisibilityOrderingItem.js.map +1 -1
  187. package/dist/sidebar/visibility-ordering-builder/sortable-tree/PointerSensorWithInteraction.js +1 -3
  188. package/dist/sidebar/visibility-ordering-builder/sortable-tree/PointerSensorWithInteraction.js.map +1 -1
  189. package/dist/sidebar/visibility-ordering-builder/sortable-tree/SortableTree.js +19 -29
  190. package/dist/sidebar/visibility-ordering-builder/sortable-tree/SortableTree.js.map +1 -1
  191. package/dist/sidebar/visibility-ordering-builder/sortable-tree/SortableTreeDndContext.js +39 -52
  192. package/dist/sidebar/visibility-ordering-builder/sortable-tree/SortableTreeDndContext.js.map +1 -1
  193. package/dist/sidebar/visibility-ordering-builder/sortable-tree/SortableTreeItem.js +18 -23
  194. package/dist/sidebar/visibility-ordering-builder/sortable-tree/SortableTreeItem.js.map +1 -1
  195. package/dist/sidebar/visibility-ordering-builder/sortable-tree/TreeItem.js +19 -15
  196. package/dist/sidebar/visibility-ordering-builder/sortable-tree/TreeItem.js.map +1 -1
  197. package/dist/sidebar/visibility-ordering-builder/sortable-tree/keyboardCoordinates.js +16 -30
  198. package/dist/sidebar/visibility-ordering-builder/sortable-tree/keyboardCoordinates.js.map +1 -1
  199. package/dist/sidebar/visibility-ordering-builder/sortable-tree/utilities.js +30 -39
  200. package/dist/sidebar/visibility-ordering-builder/sortable-tree/utilities.js.map +1 -1
  201. package/package.json +16 -16
package/README.md CHANGED
@@ -20,4 +20,299 @@ const model = await IrisGridModelFactory.makeModel(dh, table);
20
20
 
21
21
  // In your render function
22
22
  <IrisGrid dh={dh} model={model} />
23
- ```
23
+ ```
24
+
25
+ ## Customizing the Table Options menu
26
+
27
+ The Table Options sidebar (the gear menu on the right edge of the grid) is
28
+ extensible. Plugin authors can hide built-in items, relabel or reorder
29
+ them, and add their own items that open a custom configuration page —
30
+ without forking `IrisGrid`.
31
+
32
+ There is a single entry point: the `transformTableOptions` prop on
33
+ `<IrisGrid>`. It is **opt-in** — it lives on the iris-grid-specific
34
+ `IrisGridTableOptionsWidgetProps`, not on the generic
35
+ `WidgetComponentProps` / `WidgetPanelProps`, so widgets that don't care
36
+ about the Table Options menu never see it.
37
+
38
+ - **Own the render site?** Pass `transformTableOptions` straight to
39
+ `<IrisGrid>`.
40
+ - **A `WidgetMiddlewarePlugin` that doesn't render `<IrisGrid>`
41
+ yourself?** Thread the prop down the middleware chain via the
42
+ `Component` you wrap, composing your own transform on top of the one
43
+ you received (see [Publishing from middleware](#publishing-from-middleware)).
44
+ The panel hosts that ship with Deephaven (`IrisGridPanel`,
45
+ `GridWidgetPlugin`) accept `transformTableOptions` as a prop and
46
+ forward it to `<IrisGrid>`.
47
+
48
+ ### Writing a transform
49
+
50
+ `transformTableOptions(defaults)` is a pure function that receives the
51
+ built-in items (already filtered by what the current model supports) and
52
+ returns the items to actually render. Use it to add, hide, relabel,
53
+ reorder, or replace entries.
54
+
55
+ ```tsx
56
+ import { OptionType, type OptionItem } from '@deephaven/iris-grid';
57
+
58
+ const transformTableOptions = (defaults: readonly OptionItem[]) => [
59
+ // hide a built-in
60
+ ...defaults.filter(o => o.type !== OptionType.SELECT_DISTINCT),
61
+ // add a plugin item with its own page
62
+ {
63
+ type: 'plugin:my-plugin:column-inspector',
64
+ title: 'Column Inspector',
65
+ configPage: ColumnInspectorPage,
66
+ },
67
+ ];
68
+ ```
69
+
70
+ Rules:
71
+
72
+ - The transform should be referentially stable and side-effect-free
73
+ (it's called inside memoization). Memoize it with `useMemo` /
74
+ `useCallback` rather than rebuilding per render.
75
+ - A throwing transform is logged once and treated as identity for that
76
+ render, so the menu degrades gracefully.
77
+ - Items **without** a `configPage` MUST have a `type` matching an
78
+ existing `OptionType` enum value — those are rendered by the built-in
79
+ page switch.
80
+ - Items **with** a `configPage` SHOULD use a namespaced key,
81
+ conventionally `plugin:<name>:<id>`, to avoid colliding with built-ins
82
+ or other plugins.
83
+
84
+ ### Implementing a `configPage`
85
+
86
+ A `configPage` is a regular React component that receives
87
+ `IrisGridTableOptionsPageProps`:
88
+
89
+ ```tsx
90
+ import { type IrisGridTableOptionsPageProps } from '@deephaven/iris-grid';
91
+
92
+ export function ColumnInspectorPage({
93
+ model,
94
+ viewState,
95
+ onBack,
96
+ }: IrisGridTableOptionsPageProps): JSX.Element {
97
+ const hidden = new Set(viewState.hiddenColumns);
98
+ return (
99
+ <div>
100
+ <button type="button" onClick={onBack}>Back</button>
101
+ <pre>
102
+ {model.columns
103
+ .filter(c => !hidden.has(c.name))
104
+ .map(c => c.name)
105
+ .join('\n')}
106
+ </pre>
107
+ </div>
108
+ );
109
+ }
110
+ ```
111
+
112
+ `IrisGrid` wraps each `configPage` render in `PluginTableOptionsErrorBoundary`,
113
+ so a throw inside your page shows a small inline fallback instead of
114
+ unmounting the whole grid.
115
+
116
+ The `viewState` prop is an `IrisGridViewState` — a curated, read-only snapshot
117
+ of the grid's current view configuration (e.g. `viewState.hiddenColumns`). It's
118
+ a small, stable subset of the grid's internal state; new fields are added
119
+ additively as plugins need them.
120
+
121
+ ### Publishing from middleware
122
+
123
+ When you're a `WidgetMiddlewarePlugin` and don't render `<IrisGrid>`
124
+ yourself, you receive `transformTableOptions` as a prop and pass a
125
+ composed transform down to the `Component` (or panel) you wrap. Run the
126
+ upstream transform first so contributions compose, then layer your own
127
+ changes on top.
128
+
129
+ A `panelComponent` middleware should be built with `createPanelMiddleware`
130
+ from `@deephaven/plugin`: you supply a body hook that may `inject` props onto
131
+ the wrapped component and/or `wrap` the child in a wrapper element (both
132
+ optional), and the factory owns the `React.forwardRef` ceremony and ref
133
+ forwarding for you. That ref matters —
134
+ golden-layout binds a ref to the registered panel to persist class-panel
135
+ state (sorts, filters, column moves, etc.) into its `componentState`, and a
136
+ middleware that swallowed it would silently break that persistence for every
137
+ panel below it; the factory guarantees it can't be dropped. For the non-panel
138
+ `component` path use `createWidgetMiddleware`, which is otherwise identical but
139
+ takes no ref.
140
+
141
+ ```tsx
142
+ import { useMemo } from 'react';
143
+ import { createPanelMiddleware, type WidgetPanelProps } from '@deephaven/plugin';
144
+ import {
145
+ type IrisGridTableOptionsWidgetProps,
146
+ type TableOptionsTransform,
147
+ } from '@deephaven/iris-grid';
148
+
149
+ function makeMyTransform(
150
+ upstream: TableOptionsTransform | undefined
151
+ ): TableOptionsTransform {
152
+ return defaults => {
153
+ const base = upstream != null ? upstream(defaults) : defaults;
154
+ return [...base, myPluginItem];
155
+ };
156
+ }
157
+
158
+ const MyMiddleware = createPanelMiddleware<
159
+ unknown,
160
+ WidgetPanelProps & IrisGridTableOptionsWidgetProps
161
+ >(({ transformTableOptions }) => {
162
+ const composedTransform = useMemo(
163
+ () => makeMyTransform(transformTableOptions),
164
+ [transformTableOptions]
165
+ );
166
+ return { inject: { transformTableOptions: composedTransform } };
167
+ }, 'MyMiddleware');
168
+ ```
169
+
170
+ The body hook receives the incoming props (minus `Component`) and returns an
171
+ optional `{ inject?, wrap? }`. Every incoming prop is forwarded to the wrapped
172
+ component automatically; `inject` only adds or overrides the few props you
173
+ actually change (here `transformTableOptions`), and `wrap` optionally nests the
174
+ child (e.g. in a context provider). Both fields are optional — a pass-through
175
+ middleware can return `{}`. The factory adds the `ref` plumbing on top.
176
+
177
+
178
+ Composition rule: each middleware layer reads the `transformTableOptions`
179
+ it was handed, runs that transform first, then layers its own changes on
180
+ top — last writer wins for any given `OptionItem.type`.
181
+
182
+ #### Model-aware menus
183
+
184
+ The transform must stay **pure** — it only sees `defaults`, never the
185
+ `IrisGridModel`. To make a menu react to model state (e.g. relabel an
186
+ item once a pivot is active), take a **snapshot of the value you care
187
+ about** from model events and fold it into the transform's identity:
188
+
189
+ ```tsx
190
+ const [isPivot, setIsPivot] = useState(model.isPivot);
191
+ useEffect(() => {
192
+ const handler = () => setIsPivot(model.isPivot);
193
+ model.addEventListener(SOME_MODEL_EVENT, handler);
194
+ return () => model.removeEventListener(SOME_MODEL_EVENT, handler);
195
+ }, [model]);
196
+
197
+ const composedTransform = useMemo(
198
+ () => makeMyTransform(transformTableOptions, isPivot),
199
+ [transformTableOptions, isPivot]
200
+ );
201
+ ```
202
+
203
+ Because `composedTransform`'s identity changes when the snapshot
204
+ changes, `IrisGrid` re-runs it (its menu cache is keyed on
205
+ `[defaults, transform]`). Keeping the snapshot in the dependency array —
206
+ rather than reading `model.isPivot` inside the transform — is what keeps
207
+ that memoization honest.
208
+
209
+ To obtain the model when the host builds it for you, pass an
210
+ `onModelChanged` callback to `IrisGridPanel` (called once the panel's
211
+ model is ready).
212
+
213
+ ### Full example
214
+
215
+ See the [`@deephaven/js-plugin-pivot-builder`](https://github.com/deephaven/deephaven-plugins/tree/main/plugins/pivot-builder)
216
+ plugin for a working `WidgetMiddlewarePlugin` that replaces the default
217
+ widget renderer and adds a `configPage`-backed "Rollup, Aggregate and Pivot"
218
+ item to the Table Options sidebar.
219
+
220
+ ### Why the transform doesn't take the model
221
+
222
+ The transform signature is `(defaults) => items` — it deliberately does
223
+ **not** receive the `IrisGridModel` or grid state. State-aware menus
224
+ (e.g. "relabel an item once a pivot is active", "show *Reset filters*
225
+ only when filters exist") are implemented in the middleware: subscribe
226
+ to model events, snapshot the value you need, and fold that snapshot
227
+ into the transform's identity (see
228
+ [Model-aware menus](#model-aware-menus)). The transform itself stays
229
+ pure.
230
+
231
+ This isn't just about keeping the public surface small — it's also
232
+ what keeps menu memoization honest. `IrisGrid` caches the computed
233
+ item list on `[defaults, transform]` (see `getCachedTransformedOptionItems`),
234
+ both of which are stable values/refs. Adding a live `model` argument
235
+ would break that: `IrisGridModel` is a long-lived mutable handle whose
236
+ identity does not change when `isExpandable`, `filter`, `sorts`, or
237
+ `isRollup` flip, so any plugin that read those fields would silently
238
+ return stale items until something unrelated invalidated the cache.
239
+
240
+ By passing a **curated snapshot of values** through the transform's
241
+ closure instead, the memo key changes exactly when those values change
242
+ and re-runs are driven by actual dependencies. Passing the model itself,
243
+ or the full `IrisGrid` instance, is intentionally off the table: the
244
+ surface is too large, too volatile, and (in the model's case)
245
+ memoization-hostile.
246
+
247
+ ## Transforming the model
248
+
249
+ Some plugins need more than a custom menu — they need to change the
250
+ **model** the grid renders (e.g. wrap it in a proxy that can swap its
251
+ inner model in response to a config page). For that there is a second,
252
+ symmetric opt-in seam: the `transformModel` prop.
253
+
254
+ Like `transformTableOptions`, it lives on an iris-grid-specific
255
+ interface (`IrisGridModelWidgetProps`), not on the generic
256
+ `WidgetComponentProps` / `WidgetPanelProps`, and is threaded down the
257
+ middleware chain by the hosts that build the model for you
258
+ (`IrisGridPanel`, `GridWidgetPlugin` / `useIrisGridModel`).
259
+
260
+ ```ts
261
+ import { type IrisGridModelTransform } from '@deephaven/iris-grid';
262
+
263
+ // (model: IrisGridModel) => IrisGridModel | Promise<IrisGridModel>
264
+ const transformModel: IrisGridModelTransform = model =>
265
+ wrapInMyProxy(model);
266
+ ```
267
+
268
+ The host builds the model from `fetch` as usual, then applies
269
+ `transformModel` to whatever it built **before** handing it to
270
+ `<IrisGrid>`. The returned model must be a drop-in for the input — the
271
+ host owns its lifecycle and will `close()` whatever you return, so wrap
272
+ rather than discard the model you were given. The transform may be async
273
+ if it needs to await dependencies first.
274
+
275
+ Rules:
276
+
277
+ - `transformModel` must be **referentially stable**. It is applied when
278
+ the model is (re)built; an unstable transform would rebuild the model.
279
+ Memoize it with `useMemo` / `useCallback`.
280
+ - It runs once per model build, not per render, so it is the right place
281
+ for one-time wrapping — not for per-render state.
282
+ - This is why model construction stays in the host: the host keeps
283
+ ownership of `fetch`, error/loading state, and `close()`, while the
284
+ plugin only augments the result. A middleware using `transformModel`
285
+ can therefore render the wrapped `Component` and stay a **chained**
286
+ layer instead of taking over model construction and becoming terminal.
287
+
288
+ ### Publishing `transformModel` from middleware
289
+
290
+ A middleware that needs both seams composes them the same way — run any
291
+ upstream transform first, then layer your own — and returns both from its
292
+ body hook's `inject`. Props you don't touch (here `transformTableOptions`)
293
+ are forwarded automatically, so you only inject what you change:
294
+
295
+ ```tsx
296
+ import { useMemo } from 'react';
297
+ import { createPanelMiddleware, type WidgetPanelProps } from '@deephaven/plugin';
298
+ import {
299
+ type IrisGridModelTransform,
300
+ type IrisGridModelWidgetProps,
301
+ type IrisGridTableOptionsWidgetProps,
302
+ } from '@deephaven/iris-grid';
303
+
304
+ const MyMiddleware = createPanelMiddleware<
305
+ unknown,
306
+ WidgetPanelProps & IrisGridModelWidgetProps & IrisGridTableOptionsWidgetProps
307
+ >(({ transformModel }) => {
308
+ const composedModel = useMemo<IrisGridModelTransform>(
309
+ () => async model => {
310
+ const base = transformModel != null ? await transformModel(model) : model;
311
+ return wrapInMyProxy(base);
312
+ },
313
+ [transformModel]
314
+ );
315
+ return { inject: { transformModel: composedModel } };
316
+ }, 'MyMiddleware');
317
+ ```
318
+
@@ -48,23 +48,17 @@ class AdvancedFilterCreator extends PureComponent {
48
48
  this.handleFocusTrapEnd = this.handleFocusTrapEnd.bind(this);
49
49
  this.handleUpdateTimeout = this.handleUpdateTimeout.bind(this);
50
50
  this.focusTrapContainer = /*#__PURE__*/React.createRef();
51
- var {
52
- model,
53
- column,
54
- options
55
- } = props;
56
- var {
57
- filterOperators,
58
- invertSelection,
59
- selectedValues
60
- } = options;
51
+ var model = props.model,
52
+ column = props.column,
53
+ options = props.options;
54
+ var filterOperators = options.filterOperators,
55
+ invertSelection = options.invertSelection,
56
+ selectedValues = options.selectedValues;
61
57
 
62
58
  // can be null or an empty array
63
59
  var filterItems = (_options$filterItems$ = (_options$filterItems = options.filterItems) === null || _options$filterItems === void 0 ? void 0 : _options$filterItems.map(_ref => {
64
- var {
65
- selectedType,
66
- value
67
- } = _ref;
60
+ var selectedType = _ref.selectedType,
61
+ value = _ref.value;
68
62
  return {
69
63
  selectedType,
70
64
  value,
@@ -117,10 +111,9 @@ class AdvancedFilterCreator extends PureComponent {
117
111
  return this.handleFilterDelete.bind(this, index);
118
112
  }
119
113
  initValuesTable() {
120
- var {
121
- model,
122
- column
123
- } = this.props;
114
+ var _this$props = this.props,
115
+ model = _this$props.model,
116
+ column = _this$props.column;
124
117
  if (!model.isValuesTableAvailable) {
125
118
  log.debug('No values table for this model, just ignore');
126
119
  return;
@@ -158,10 +151,9 @@ class AdvancedFilterCreator extends PureComponent {
158
151
  }
159
152
  }
160
153
  handleAddAnd() {
161
- var {
162
- filterItems,
163
- filterOperators
164
- } = this.state;
154
+ var _this$state = this.state,
155
+ filterItems = _this$state.filterItems,
156
+ filterOperators = _this$state.filterOperators;
165
157
  filterItems = filterItems.concat(AdvancedFilterCreator.makeFilterItem());
166
158
  filterOperators = filterOperators.concat(FilterOperator.and);
167
159
  this.setState({
@@ -170,10 +162,9 @@ class AdvancedFilterCreator extends PureComponent {
170
162
  });
171
163
  }
172
164
  handleAddOr() {
173
- var {
174
- filterItems,
175
- filterOperators
176
- } = this.state;
165
+ var _this$state2 = this.state,
166
+ filterItems = _this$state2.filterItems,
167
+ filterOperators = _this$state2.filterOperators;
177
168
  filterItems = filterItems.concat(AdvancedFilterCreator.makeFilterItem());
178
169
  filterOperators = filterOperators.concat(FilterOperator.or);
179
170
  this.setState({
@@ -182,9 +173,7 @@ class AdvancedFilterCreator extends PureComponent {
182
173
  });
183
174
  }
184
175
  handleChangeFilterOperator(index, operator) {
185
- var {
186
- filterOperators
187
- } = this.state;
176
+ var filterOperators = this.state.filterOperators;
188
177
  filterOperators = [...filterOperators];
189
178
  assertFilterOperatorValue(operator);
190
179
  filterOperators[index] = operator;
@@ -194,13 +183,9 @@ class AdvancedFilterCreator extends PureComponent {
194
183
  this.startUpdateTimer();
195
184
  }
196
185
  handleFilterChange(filterIndex, selectedType, value) {
197
- var {
198
- filterItems
199
- } = this.state;
186
+ var filterItems = this.state.filterItems;
200
187
  filterItems = [...filterItems];
201
- var {
202
- key
203
- } = filterItems[filterIndex];
188
+ var key = filterItems[filterIndex].key;
204
189
  filterItems[filterIndex] = {
205
190
  key,
206
191
  selectedType,
@@ -212,10 +197,9 @@ class AdvancedFilterCreator extends PureComponent {
212
197
  this.startUpdateTimer();
213
198
  }
214
199
  handleFilterDelete(filterIndex) {
215
- var {
216
- filterItems,
217
- filterOperators
218
- } = this.state;
200
+ var _this$state3 = this.state,
201
+ filterItems = _this$state3.filterItems,
202
+ filterOperators = _this$state3.filterOperators;
219
203
  filterItems = [...filterItems];
220
204
  filterOperators = [...filterOperators];
221
205
  if (filterIndex < filterItems.length) {
@@ -271,9 +255,7 @@ class AdvancedFilterCreator extends PureComponent {
271
255
  log.debug('Submitting and Closing Advanced Filter');
272
256
  this.stopUpdateTimer();
273
257
  this.sendUpdate();
274
- var {
275
- onDone
276
- } = this.props;
258
+ var onDone = this.props.onDone;
277
259
  onDone();
278
260
  event.preventDefault();
279
261
  }
@@ -288,17 +270,13 @@ class AdvancedFilterCreator extends PureComponent {
288
270
  * @returns true If the add filter buttons should be shown, false otherwise
289
271
  */
290
272
  shouldShowAddFilter() {
291
- var {
292
- filterItems
293
- } = this.state;
273
+ var filterItems = this.state.filterItems;
294
274
  if (filterItems.length === 0) {
295
275
  return false;
296
276
  }
297
277
  var filterItem = filterItems[filterItems.length - 1];
298
- var {
299
- selectedType,
300
- value
301
- } = filterItem;
278
+ var selectedType = filterItem.selectedType,
279
+ value = filterItem.value;
302
280
  return selectedType != null && selectedType.length > 0 && value != null && value.length > 0;
303
281
  }
304
282
 
@@ -309,13 +287,10 @@ class AdvancedFilterCreator extends PureComponent {
309
287
  */
310
288
  sortTable(direction) {
311
289
  var addToExisting = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
312
- var {
313
- column,
314
- onSortChange
315
- } = this.props;
316
- var {
317
- isSortable
318
- } = this.state;
290
+ var _this$props2 = this.props,
291
+ column = _this$props2.column,
292
+ onSortChange = _this$props2.onSortChange;
293
+ var isSortable = this.state.isSortable;
319
294
  if (isSortable) {
320
295
  onSortChange(column, direction, addToExisting);
321
296
  }
@@ -331,26 +306,20 @@ class AdvancedFilterCreator extends PureComponent {
331
306
  }
332
307
  }
333
308
  sendUpdate() {
334
- var {
335
- filterItems,
336
- filterOperators,
337
- invertSelection,
338
- selectedValues
339
- } = this.state;
340
- var {
341
- column,
342
- onFilterChange,
343
- model,
344
- tableUtils
345
- } = this.props;
346
- var {
347
- formatter
348
- } = model;
309
+ var _this$state4 = this.state,
310
+ filterItems = _this$state4.filterItems,
311
+ filterOperators = _this$state4.filterOperators,
312
+ invertSelection = _this$state4.invertSelection,
313
+ selectedValues = _this$state4.selectedValues;
314
+ var _this$props3 = this.props,
315
+ column = _this$props3.column,
316
+ onFilterChange = _this$props3.onFilterChange,
317
+ model = _this$props3.model,
318
+ tableUtils = _this$props3.tableUtils;
319
+ var formatter = model.formatter;
349
320
  var items = filterItems.filter(_ref2 => {
350
- var {
351
- selectedType,
352
- value
353
- } = _ref2;
321
+ var selectedType = _ref2.selectedType,
322
+ value = _ref2.value;
354
323
  return selectedType != null && value != null && value !== '';
355
324
  });
356
325
  var operators = filterOperators.filter((operator, i) => operator != null && filterItems[i].selectedType != null && filterItems[i].value != null && filterItems[i].value !== '').slice(0, items.length - 1);
@@ -367,28 +336,24 @@ class AdvancedFilterCreator extends PureComponent {
367
336
  }
368
337
  render() {
369
338
  var _this = this;
370
- var {
371
- column,
372
- model,
373
- sortDirection,
374
- formatter,
375
- tableUtils,
376
- isMaximized,
377
- onToggleMaximize
378
- } = this.props;
379
- var {
380
- filterItems,
381
- filterOperators,
382
- invertSelection,
383
- selectedValues,
384
- valuesTable,
385
- valuesTableError,
386
- isSortable
387
- } = this.state;
388
- var {
389
- dh,
390
- isValuesTableAvailable
391
- } = model;
339
+ var _this$props4 = this.props,
340
+ column = _this$props4.column,
341
+ model = _this$props4.model,
342
+ sortDirection = _this$props4.sortDirection,
343
+ formatter = _this$props4.formatter,
344
+ tableUtils = _this$props4.tableUtils,
345
+ isMaximized = _this$props4.isMaximized,
346
+ onToggleMaximize = _this$props4.onToggleMaximize;
347
+ var _this$state5 = this.state,
348
+ filterItems = _this$state5.filterItems,
349
+ filterOperators = _this$state5.filterOperators,
350
+ invertSelection = _this$state5.invertSelection,
351
+ selectedValues = _this$state5.selectedValues,
352
+ valuesTable = _this$state5.valuesTable,
353
+ valuesTableError = _this$state5.valuesTableError,
354
+ isSortable = _this$state5.isSortable;
355
+ var dh = model.dh,
356
+ isValuesTableAvailable = model.isValuesTableAvailable;
392
357
  var isBoolean = TableUtils.isBooleanType(column.type);
393
358
  var isDateType = TableUtils.isDateType(column.type);
394
359
  var filterTypes = this.getFilterTypes(column.type);
@@ -397,11 +362,9 @@ class AdvancedFilterCreator extends PureComponent {
397
362
  if (!isBoolean && filterTypes.length) {
398
363
  var _loop = function _loop(i) {
399
364
  var filterItem = filterItems[i];
400
- var {
401
- key,
402
- selectedType,
403
- value
404
- } = filterItem;
365
+ var key = filterItem.key,
366
+ selectedType = filterItem.selectedType,
367
+ value = filterItem.value;
405
368
  var element = /*#__PURE__*/_jsx(AdvancedFilterCreatorFilterItem, {
406
369
  column: column,
407
370
  filterTypes: filterTypes,