@deephaven/iris-grid 1.22.1 → 1.22.2-alpha-pivot-builder.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 (195) hide show
  1. package/README.md +284 -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 +62 -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.js +19 -46
  20. package/dist/FilterInputField.js.map +1 -1
  21. package/dist/GotoRow.js +32 -29
  22. package/dist/GotoRow.js.map +1 -1
  23. package/dist/IrisGrid.d.ts +85 -2
  24. package/dist/IrisGrid.d.ts.map +1 -1
  25. package/dist/IrisGrid.js +709 -861
  26. package/dist/IrisGrid.js.map +1 -1
  27. package/dist/IrisGridBottomBar.js +10 -11
  28. package/dist/IrisGridBottomBar.js.map +1 -1
  29. package/dist/IrisGridCacheUtils.js +28 -8
  30. package/dist/IrisGridCacheUtils.js.map +1 -1
  31. package/dist/IrisGridCellOverflowModal.js +30 -13
  32. package/dist/IrisGridCellOverflowModal.js.map +1 -1
  33. package/dist/IrisGridCellRendererUtils.js +6 -12
  34. package/dist/IrisGridCellRendererUtils.js.map +1 -1
  35. package/dist/IrisGridCopyHandler.js +28 -47
  36. package/dist/IrisGridCopyHandler.js.map +1 -1
  37. package/dist/IrisGridMetricCalculator.js +28 -51
  38. package/dist/IrisGridMetricCalculator.js.map +1 -1
  39. package/dist/IrisGridModel.d.ts +30 -1
  40. package/dist/IrisGridModel.d.ts.map +1 -1
  41. package/dist/IrisGridModel.js +36 -1
  42. package/dist/IrisGridModel.js.map +1 -1
  43. package/dist/IrisGridModelUpdater.js +32 -30
  44. package/dist/IrisGridModelUpdater.js.map +1 -1
  45. package/dist/IrisGridModelWidgetProps.d.ts +26 -0
  46. package/dist/IrisGridModelWidgetProps.d.ts.map +1 -0
  47. package/dist/IrisGridModelWidgetProps.js +2 -0
  48. package/dist/IrisGridModelWidgetProps.js.map +1 -0
  49. package/dist/IrisGridPartitionSelector.js +28 -62
  50. package/dist/IrisGridPartitionSelector.js.map +1 -1
  51. package/dist/IrisGridProxyModel.d.ts.map +1 -1
  52. package/dist/IrisGridProxyModel.js +41 -14
  53. package/dist/IrisGridProxyModel.js.map +1 -1
  54. package/dist/IrisGridRenderer.js +162 -264
  55. package/dist/IrisGridRenderer.js.map +1 -1
  56. package/dist/IrisGridTableModel.js +23 -16
  57. package/dist/IrisGridTableModel.js.map +1 -1
  58. package/dist/IrisGridTableModelTemplate.js +49 -73
  59. package/dist/IrisGridTableModelTemplate.js.map +1 -1
  60. package/dist/IrisGridTestUtils.js +19 -19
  61. package/dist/IrisGridTestUtils.js.map +1 -1
  62. package/dist/IrisGridTextCellRenderer.d.ts.map +1 -1
  63. package/dist/IrisGridTextCellRenderer.js +36 -53
  64. package/dist/IrisGridTextCellRenderer.js.map +1 -1
  65. package/dist/IrisGridTheme.js +9 -1
  66. package/dist/IrisGridTheme.js.map +1 -1
  67. package/dist/IrisGridThemeProvider.js +5 -7
  68. package/dist/IrisGridThemeProvider.js.map +1 -1
  69. package/dist/IrisGridTreeTableModel.js +5 -9
  70. package/dist/IrisGridTreeTableModel.js.map +1 -1
  71. package/dist/IrisGridUtils.d.ts +25 -2
  72. package/dist/IrisGridUtils.d.ts.map +1 -1
  73. package/dist/IrisGridUtils.js +297 -294
  74. package/dist/IrisGridUtils.js.map +1 -1
  75. package/dist/LazyIrisGrid.d.ts +1 -1
  76. package/dist/NoPastePermissionModal.js +3 -5
  77. package/dist/NoPastePermissionModal.js.map +1 -1
  78. package/dist/PendingDataBottomBar.js +27 -16
  79. package/dist/PendingDataBottomBar.js.map +1 -1
  80. package/dist/ToastBottomBar.js +16 -8
  81. package/dist/ToastBottomBar.js.map +1 -1
  82. package/dist/format-context-menus/CustomFormatAction.js +11 -24
  83. package/dist/format-context-menus/CustomFormatAction.js.map +1 -1
  84. package/dist/index.d.ts +1 -0
  85. package/dist/index.d.ts.map +1 -1
  86. package/dist/index.js +1 -0
  87. package/dist/index.js.map +1 -1
  88. package/dist/key-handlers/CopyCellKeyHandler.js +3 -4
  89. package/dist/key-handlers/CopyCellKeyHandler.js.map +1 -1
  90. package/dist/key-handlers/CopyKeyHandler.js +1 -3
  91. package/dist/key-handlers/CopyKeyHandler.js.map +1 -1
  92. package/dist/key-handlers/ReverseKeyHandler.js +1 -3
  93. package/dist/key-handlers/ReverseKeyHandler.js.map +1 -1
  94. package/dist/mousehandlers/IrisGridCellOverflowMouseHandler.js +35 -59
  95. package/dist/mousehandlers/IrisGridCellOverflowMouseHandler.js.map +1 -1
  96. package/dist/mousehandlers/IrisGridColumnSelectMouseHandler.js +13 -29
  97. package/dist/mousehandlers/IrisGridColumnSelectMouseHandler.js.map +1 -1
  98. package/dist/mousehandlers/IrisGridColumnTooltipMouseHandler.js +8 -17
  99. package/dist/mousehandlers/IrisGridColumnTooltipMouseHandler.js.map +1 -1
  100. package/dist/mousehandlers/IrisGridContextMenuHandler.js +96 -199
  101. package/dist/mousehandlers/IrisGridContextMenuHandler.js.map +1 -1
  102. package/dist/mousehandlers/IrisGridDataSelectMouseHandler.js +2 -4
  103. package/dist/mousehandlers/IrisGridDataSelectMouseHandler.js.map +1 -1
  104. package/dist/mousehandlers/IrisGridFilterMouseHandler.js +16 -30
  105. package/dist/mousehandlers/IrisGridFilterMouseHandler.js.map +1 -1
  106. package/dist/mousehandlers/IrisGridPartitionedTableMouseHandler.js +5 -13
  107. package/dist/mousehandlers/IrisGridPartitionedTableMouseHandler.js.map +1 -1
  108. package/dist/mousehandlers/IrisGridRowTreeMouseHandler.js +20 -36
  109. package/dist/mousehandlers/IrisGridRowTreeMouseHandler.js.map +1 -1
  110. package/dist/mousehandlers/IrisGridSortMouseHandler.js +3 -5
  111. package/dist/mousehandlers/IrisGridSortMouseHandler.js.map +1 -1
  112. package/dist/mousehandlers/IrisGridTokenMouseHandler.js +9 -18
  113. package/dist/mousehandlers/IrisGridTokenMouseHandler.js.map +1 -1
  114. package/dist/mousehandlers/PendingMouseHandler.js +10 -18
  115. package/dist/mousehandlers/PendingMouseHandler.js.map +1 -1
  116. package/dist/sidebar/AdvancedSettingsMenu.js +12 -5
  117. package/dist/sidebar/AdvancedSettingsMenu.js.map +1 -1
  118. package/dist/sidebar/ChartBuilder.js +45 -102
  119. package/dist/sidebar/ChartBuilder.js.map +1 -1
  120. package/dist/sidebar/CustomColumnBuilder.js +35 -77
  121. package/dist/sidebar/CustomColumnBuilder.js.map +1 -1
  122. package/dist/sidebar/CustomColumnInput.js +9 -11
  123. package/dist/sidebar/CustomColumnInput.js.map +1 -1
  124. package/dist/sidebar/InputEditor.js +17 -24
  125. package/dist/sidebar/InputEditor.js.map +1 -1
  126. package/dist/sidebar/IrisGridTableOptionsWidgetProps.d.ts +22 -0
  127. package/dist/sidebar/IrisGridTableOptionsWidgetProps.d.ts.map +1 -0
  128. package/dist/sidebar/IrisGridTableOptionsWidgetProps.js +2 -0
  129. package/dist/sidebar/IrisGridTableOptionsWidgetProps.js.map +1 -0
  130. package/dist/sidebar/OptionType.d.ts +8 -0
  131. package/dist/sidebar/OptionType.d.ts.map +1 -1
  132. package/dist/sidebar/OptionType.js +7 -0
  133. package/dist/sidebar/OptionType.js.map +1 -1
  134. package/dist/sidebar/PluginTableOptionsErrorBoundary.d.ts +30 -0
  135. package/dist/sidebar/PluginTableOptionsErrorBoundary.d.ts.map +1 -0
  136. package/dist/sidebar/PluginTableOptionsErrorBoundary.js +55 -0
  137. package/dist/sidebar/PluginTableOptionsErrorBoundary.js.map +1 -0
  138. package/dist/sidebar/RollupRows.js +72 -113
  139. package/dist/sidebar/RollupRows.js.map +1 -1
  140. package/dist/sidebar/SelectDistinctBuilder.js +16 -33
  141. package/dist/sidebar/SelectDistinctBuilder.js.map +1 -1
  142. package/dist/sidebar/TableCsvExporter.js +50 -74
  143. package/dist/sidebar/TableCsvExporter.js.map +1 -1
  144. package/dist/sidebar/TableSaver.js +18 -42
  145. package/dist/sidebar/TableSaver.js.map +1 -1
  146. package/dist/sidebar/aggregations/AggregationEdit.js +25 -18
  147. package/dist/sidebar/aggregations/AggregationEdit.js.map +1 -1
  148. package/dist/sidebar/aggregations/AggregationUtils.js +1 -3
  149. package/dist/sidebar/aggregations/AggregationUtils.js.map +1 -1
  150. package/dist/sidebar/aggregations/Aggregations.js +45 -34
  151. package/dist/sidebar/aggregations/Aggregations.js.map +1 -1
  152. package/dist/sidebar/conditional-formatting/ColumnFormatEditor.js +35 -24
  153. package/dist/sidebar/conditional-formatting/ColumnFormatEditor.js.map +1 -1
  154. package/dist/sidebar/conditional-formatting/ConditionEditor.js +38 -21
  155. package/dist/sidebar/conditional-formatting/ConditionEditor.js.map +1 -1
  156. package/dist/sidebar/conditional-formatting/ConditionalFormatEditor.js +27 -11
  157. package/dist/sidebar/conditional-formatting/ConditionalFormatEditor.js.map +1 -1
  158. package/dist/sidebar/conditional-formatting/ConditionalFormattingMenu.js +9 -11
  159. package/dist/sidebar/conditional-formatting/ConditionalFormattingMenu.js.map +1 -1
  160. package/dist/sidebar/conditional-formatting/ConditionalFormattingUtils.js +48 -57
  161. package/dist/sidebar/conditional-formatting/ConditionalFormattingUtils.js.map +1 -1
  162. package/dist/sidebar/conditional-formatting/RowFormatEditor.js +35 -24
  163. package/dist/sidebar/conditional-formatting/RowFormatEditor.js.map +1 -1
  164. package/dist/sidebar/conditional-formatting/StyleEditor.js +26 -11
  165. package/dist/sidebar/conditional-formatting/StyleEditor.js.map +1 -1
  166. package/dist/sidebar/index.d.ts +3 -2
  167. package/dist/sidebar/index.d.ts.map +1 -1
  168. package/dist/sidebar/index.js.map +1 -1
  169. package/dist/sidebar/visibility-ordering-builder/SearchItem.js +5 -7
  170. package/dist/sidebar/visibility-ordering-builder/SearchItem.js.map +1 -1
  171. package/dist/sidebar/visibility-ordering-builder/SearchWithModal.js +24 -18
  172. package/dist/sidebar/visibility-ordering-builder/SearchWithModal.js.map +1 -1
  173. package/dist/sidebar/visibility-ordering-builder/VisibilityOrderingBuilder.js +166 -256
  174. package/dist/sidebar/visibility-ordering-builder/VisibilityOrderingBuilder.js.map +1 -1
  175. package/dist/sidebar/visibility-ordering-builder/VisibilityOrderingBuilderUtils.js +4 -11
  176. package/dist/sidebar/visibility-ordering-builder/VisibilityOrderingBuilderUtils.js.map +1 -1
  177. package/dist/sidebar/visibility-ordering-builder/VisibilityOrderingGroup.js +28 -14
  178. package/dist/sidebar/visibility-ordering-builder/VisibilityOrderingGroup.js.map +1 -1
  179. package/dist/sidebar/visibility-ordering-builder/VisibilityOrderingItem.js +16 -19
  180. package/dist/sidebar/visibility-ordering-builder/VisibilityOrderingItem.js.map +1 -1
  181. package/dist/sidebar/visibility-ordering-builder/sortable-tree/PointerSensorWithInteraction.js +1 -3
  182. package/dist/sidebar/visibility-ordering-builder/sortable-tree/PointerSensorWithInteraction.js.map +1 -1
  183. package/dist/sidebar/visibility-ordering-builder/sortable-tree/SortableTree.js +19 -29
  184. package/dist/sidebar/visibility-ordering-builder/sortable-tree/SortableTree.js.map +1 -1
  185. package/dist/sidebar/visibility-ordering-builder/sortable-tree/SortableTreeDndContext.js +39 -52
  186. package/dist/sidebar/visibility-ordering-builder/sortable-tree/SortableTreeDndContext.js.map +1 -1
  187. package/dist/sidebar/visibility-ordering-builder/sortable-tree/SortableTreeItem.js +18 -23
  188. package/dist/sidebar/visibility-ordering-builder/sortable-tree/SortableTreeItem.js.map +1 -1
  189. package/dist/sidebar/visibility-ordering-builder/sortable-tree/TreeItem.js +19 -15
  190. package/dist/sidebar/visibility-ordering-builder/sortable-tree/TreeItem.js.map +1 -1
  191. package/dist/sidebar/visibility-ordering-builder/sortable-tree/keyboardCoordinates.js +16 -30
  192. package/dist/sidebar/visibility-ordering-builder/sortable-tree/keyboardCoordinates.js.map +1 -1
  193. package/dist/sidebar/visibility-ordering-builder/sortable-tree/utilities.js +30 -39
  194. package/dist/sidebar/visibility-ordering-builder/sortable-tree/utilities.js.map +1 -1
  195. package/package.json +16 -16
package/README.md CHANGED
@@ -20,4 +20,287 @@ 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
+ onBack,
95
+ }: IrisGridTableOptionsPageProps): JSX.Element {
96
+ return (
97
+ <div>
98
+ <button type="button" onClick={onBack}>Back</button>
99
+ <pre>{model.columns.map(c => c.name).join('\n')}</pre>
100
+ </div>
101
+ );
102
+ }
103
+ ```
104
+
105
+ `IrisGrid` wraps each `configPage` render in `PluginTableOptionsErrorBoundary`,
106
+ so a throw inside your page shows a small inline fallback instead of
107
+ unmounting the whole grid.
108
+
109
+ ### Publishing from middleware
110
+
111
+ When you're a `WidgetMiddlewarePlugin` and don't render `<IrisGrid>`
112
+ yourself, you receive `transformTableOptions` as a prop and pass a
113
+ composed transform down to the `Component` (or panel) you wrap. Run the
114
+ upstream transform first so contributions compose, then layer your own
115
+ changes on top.
116
+
117
+ A `panelComponent` middleware should be built with `createPanelMiddleware`
118
+ from `@deephaven/plugin`: you supply a body hook that may `inject` props onto
119
+ the wrapped component and/or `wrap` the child in a wrapper element (both
120
+ optional), and the factory owns the `React.forwardRef` ceremony and ref
121
+ forwarding for you. That ref matters —
122
+ golden-layout binds a ref to the registered panel to persist class-panel
123
+ state (sorts, filters, column moves, etc.) into its `componentState`, and a
124
+ middleware that swallowed it would silently break that persistence for every
125
+ panel below it; the factory guarantees it can't be dropped. For the non-panel
126
+ `component` path use `createWidgetMiddleware`, which is otherwise identical but
127
+ takes no ref.
128
+
129
+ ```tsx
130
+ import { useMemo } from 'react';
131
+ import { createPanelMiddleware, type WidgetPanelProps } from '@deephaven/plugin';
132
+ import {
133
+ type IrisGridTableOptionsWidgetProps,
134
+ type TableOptionsTransform,
135
+ } from '@deephaven/iris-grid';
136
+
137
+ function makeMyTransform(
138
+ upstream: TableOptionsTransform | undefined
139
+ ): TableOptionsTransform {
140
+ return defaults => {
141
+ const base = upstream != null ? upstream(defaults) : defaults;
142
+ return [...base, myPluginItem];
143
+ };
144
+ }
145
+
146
+ const MyMiddleware = createPanelMiddleware<
147
+ unknown,
148
+ WidgetPanelProps & IrisGridTableOptionsWidgetProps
149
+ >(({ transformTableOptions }) => {
150
+ const composedTransform = useMemo(
151
+ () => makeMyTransform(transformTableOptions),
152
+ [transformTableOptions]
153
+ );
154
+ return { inject: { transformTableOptions: composedTransform } };
155
+ }, 'MyMiddleware');
156
+ ```
157
+
158
+ The body hook receives the incoming props (minus `Component`) and returns an
159
+ optional `{ inject?, wrap? }`. Every incoming prop is forwarded to the wrapped
160
+ component automatically; `inject` only adds or overrides the few props you
161
+ actually change (here `transformTableOptions`), and `wrap` optionally nests the
162
+ child (e.g. in a context provider). Both fields are optional — a pass-through
163
+ middleware can return `{}`. The factory adds the `ref` plumbing on top.
164
+
165
+
166
+ Composition rule: each middleware layer reads the `transformTableOptions`
167
+ it was handed, runs that transform first, then layers its own changes on
168
+ top — last writer wins for any given `OptionItem.type`.
169
+
170
+ #### Model-aware menus
171
+
172
+ The transform must stay **pure** — it only sees `defaults`, never the
173
+ `IrisGridModel`. To make a menu react to model state (e.g. relabel an
174
+ item once a pivot is active), take a **snapshot of the value you care
175
+ about** from model events and fold it into the transform's identity:
176
+
177
+ ```tsx
178
+ const [isPivot, setIsPivot] = useState(model.isPivot);
179
+ useEffect(() => {
180
+ const handler = () => setIsPivot(model.isPivot);
181
+ model.addEventListener(SOME_MODEL_EVENT, handler);
182
+ return () => model.removeEventListener(SOME_MODEL_EVENT, handler);
183
+ }, [model]);
184
+
185
+ const composedTransform = useMemo(
186
+ () => makeMyTransform(transformTableOptions, isPivot),
187
+ [transformTableOptions, isPivot]
188
+ );
189
+ ```
190
+
191
+ Because `composedTransform`'s identity changes when the snapshot
192
+ changes, `IrisGrid` re-runs it (its menu cache is keyed on
193
+ `[defaults, transform]`). Keeping the snapshot in the dependency array —
194
+ rather than reading `model.isPivot` inside the transform — is what keeps
195
+ that memoization honest.
196
+
197
+ To obtain the model when the host builds it for you, pass an
198
+ `onModelChanged` callback to `IrisGridPanel` (called once the panel's
199
+ model is ready).
200
+
201
+ ### Full example
202
+
203
+ See the [`@deephaven/js-plugin-pivot-builder`](https://github.com/deephaven/deephaven-plugins/tree/main/plugins/pivot-builder)
204
+ plugin for a working `WidgetMiddlewarePlugin` that replaces the default
205
+ widget renderer and adds a `configPage`-backed "Rollup, Aggregate and Pivot"
206
+ item to the Table Options sidebar.
207
+
208
+ ### Why the transform doesn't take the model
209
+
210
+ The transform signature is `(defaults) => items` — it deliberately does
211
+ **not** receive the `IrisGridModel` or grid state. State-aware menus
212
+ (e.g. "relabel an item once a pivot is active", "show *Reset filters*
213
+ only when filters exist") are implemented in the middleware: subscribe
214
+ to model events, snapshot the value you need, and fold that snapshot
215
+ into the transform's identity (see
216
+ [Model-aware menus](#model-aware-menus)). The transform itself stays
217
+ pure.
218
+
219
+ This isn't just about keeping the public surface small — it's also
220
+ what keeps menu memoization honest. `IrisGrid` caches the computed
221
+ item list on `[defaults, transform]` (see `getCachedTransformedOptionItems`),
222
+ both of which are stable values/refs. Adding a live `model` argument
223
+ would break that: `IrisGridModel` is a long-lived mutable handle whose
224
+ identity does not change when `isExpandable`, `filter`, `sorts`, or
225
+ `isRollup` flip, so any plugin that read those fields would silently
226
+ return stale items until something unrelated invalidated the cache.
227
+
228
+ By passing a **curated snapshot of values** through the transform's
229
+ closure instead, the memo key changes exactly when those values change
230
+ and re-runs are driven by actual dependencies. Passing the model itself,
231
+ or the full `IrisGrid` instance, is intentionally off the table: the
232
+ surface is too large, too volatile, and (in the model's case)
233
+ memoization-hostile.
234
+
235
+ ## Transforming the model
236
+
237
+ Some plugins need more than a custom menu — they need to change the
238
+ **model** the grid renders (e.g. wrap it in a proxy that can swap its
239
+ inner model in response to a config page). For that there is a second,
240
+ symmetric opt-in seam: the `transformModel` prop.
241
+
242
+ Like `transformTableOptions`, it lives on an iris-grid-specific
243
+ interface (`IrisGridModelWidgetProps`), not on the generic
244
+ `WidgetComponentProps` / `WidgetPanelProps`, and is threaded down the
245
+ middleware chain by the hosts that build the model for you
246
+ (`IrisGridPanel`, `GridWidgetPlugin` / `useIrisGridModel`).
247
+
248
+ ```ts
249
+ import { type IrisGridModelTransform } from '@deephaven/iris-grid';
250
+
251
+ // (model: IrisGridModel) => IrisGridModel | Promise<IrisGridModel>
252
+ const transformModel: IrisGridModelTransform = model =>
253
+ wrapInMyProxy(model);
254
+ ```
255
+
256
+ The host builds the model from `fetch` as usual, then applies
257
+ `transformModel` to whatever it built **before** handing it to
258
+ `<IrisGrid>`. The returned model must be a drop-in for the input — the
259
+ host owns its lifecycle and will `close()` whatever you return, so wrap
260
+ rather than discard the model you were given. The transform may be async
261
+ if it needs to await dependencies first.
262
+
263
+ Rules:
264
+
265
+ - `transformModel` must be **referentially stable**. It is applied when
266
+ the model is (re)built; an unstable transform would rebuild the model.
267
+ Memoize it with `useMemo` / `useCallback`.
268
+ - It runs once per model build, not per render, so it is the right place
269
+ for one-time wrapping — not for per-render state.
270
+ - This is why model construction stays in the host: the host keeps
271
+ ownership of `fetch`, error/loading state, and `close()`, while the
272
+ plugin only augments the result. A middleware using `transformModel`
273
+ can therefore render the wrapped `Component` and stay a **chained**
274
+ layer instead of taking over model construction and becoming terminal.
275
+
276
+ ### Publishing `transformModel` from middleware
277
+
278
+ A middleware that needs both seams composes them the same way — run any
279
+ upstream transform first, then layer your own — and returns both from its
280
+ body hook's `inject`. Props you don't touch (here `transformTableOptions`)
281
+ are forwarded automatically, so you only inject what you change:
282
+
283
+ ```tsx
284
+ import { useMemo } from 'react';
285
+ import { createPanelMiddleware, type WidgetPanelProps } from '@deephaven/plugin';
286
+ import {
287
+ type IrisGridModelTransform,
288
+ type IrisGridModelWidgetProps,
289
+ type IrisGridTableOptionsWidgetProps,
290
+ } from '@deephaven/iris-grid';
291
+
292
+ const MyMiddleware = createPanelMiddleware<
293
+ unknown,
294
+ WidgetPanelProps & IrisGridModelWidgetProps & IrisGridTableOptionsWidgetProps
295
+ >(({ transformModel }) => {
296
+ const composedModel = useMemo<IrisGridModelTransform>(
297
+ () => async model => {
298
+ const base = transformModel != null ? await transformModel(model) : model;
299
+ return wrapInMyProxy(base);
300
+ },
301
+ [transformModel]
302
+ );
303
+ return { inject: { transformModel: composedModel } };
304
+ }, 'MyMiddleware');
305
+ ```
306
+
@@ -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,