@firecms/core 3.0.0 → 3.1.0-canary.1df3b2c

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 (185) hide show
  1. package/README.md +1 -1
  2. package/dist/components/AIIcon.d.ts +16 -0
  3. package/dist/components/EntityCollectionTable/EntityCollectionRowActions.d.ts +7 -1
  4. package/dist/components/EntityCollectionTable/EntityCollectionTable.d.ts +1 -1
  5. package/dist/components/EntityCollectionTable/EntityCollectionTableProps.d.ts +14 -0
  6. package/dist/components/EntityCollectionTable/PropertyTableCell.d.ts +6 -0
  7. package/dist/components/EntityCollectionTable/internal/CollectionTableToolbar.d.ts +5 -4
  8. package/dist/components/EntityCollectionTable/internal/EntityTableCell.d.ts +6 -0
  9. package/dist/components/EntityCollectionView/Board.d.ts +2 -0
  10. package/dist/components/EntityCollectionView/BoardColumn.d.ts +42 -0
  11. package/dist/components/EntityCollectionView/BoardColumnTitle.d.ts +9 -0
  12. package/dist/components/EntityCollectionView/BoardSortableList.d.ts +14 -0
  13. package/dist/components/EntityCollectionView/EntityBoardCard.d.ts +26 -0
  14. package/dist/components/EntityCollectionView/EntityCard.d.ts +19 -0
  15. package/dist/components/EntityCollectionView/EntityCollectionBoardView.d.ts +20 -0
  16. package/dist/components/EntityCollectionView/EntityCollectionCardView.d.ts +31 -0
  17. package/dist/components/EntityCollectionView/EntityCollectionViewActions.d.ts +2 -2
  18. package/dist/components/EntityCollectionView/EntityCollectionViewStartActions.d.ts +7 -3
  19. package/dist/components/EntityCollectionView/FiltersDialog.d.ts +14 -0
  20. package/dist/components/EntityCollectionView/ViewModeToggle.d.ts +49 -0
  21. package/dist/components/EntityCollectionView/board_types.d.ts +105 -0
  22. package/dist/components/EntityCollectionView/useBoardDataController.d.ts +60 -0
  23. package/dist/components/SelectableTable/SelectableTable.d.ts +5 -1
  24. package/dist/components/SelectableTable/filters/DateTimeFilterField.d.ts +2 -1
  25. package/dist/components/VirtualTable/VirtualTable.performance.test.d.ts +1 -0
  26. package/dist/components/VirtualTable/VirtualTableCell.d.ts +6 -0
  27. package/dist/components/VirtualTable/VirtualTableHeader.d.ts +2 -0
  28. package/dist/components/VirtualTable/VirtualTableHeaderRow.d.ts +1 -1
  29. package/dist/components/VirtualTable/VirtualTableProps.d.ts +11 -0
  30. package/dist/components/VirtualTable/fields/VirtualTableDateField.d.ts +1 -0
  31. package/dist/components/VirtualTable/types.d.ts +2 -0
  32. package/dist/components/index.d.ts +3 -0
  33. package/dist/contexts/index.d.ts +10 -0
  34. package/dist/core/DrawerNavigationGroup.d.ts +45 -0
  35. package/dist/core/index.d.ts +1 -0
  36. package/dist/form/components/LocalChangesMenu.d.ts +2 -2
  37. package/dist/form/components/StorageUploadProgress.d.ts +1 -1
  38. package/dist/form/validation.d.ts +3 -2
  39. package/dist/hooks/useBreadcrumbsController.d.ts +16 -0
  40. package/dist/hooks/useCollapsedGroups.d.ts +4 -1
  41. package/dist/index.es.js +5397 -1768
  42. package/dist/index.es.js.map +1 -1
  43. package/dist/index.umd.js +5391 -1763
  44. package/dist/index.umd.js.map +1 -1
  45. package/dist/preview/PropertyPreviewProps.d.ts +5 -0
  46. package/dist/preview/components/DatePreview.d.ts +13 -3
  47. package/dist/preview/components/ImagePreview.d.ts +5 -1
  48. package/dist/preview/components/StorageThumbnail.d.ts +2 -1
  49. package/dist/preview/components/UrlComponentPreview.d.ts +2 -1
  50. package/dist/preview/property_previews/ArrayOfStorageComponentsPreview.d.ts +1 -1
  51. package/dist/preview/property_previews/ArrayOfStringsPreview.d.ts +1 -1
  52. package/dist/preview/property_previews/SkeletonPropertyComponent.d.ts +1 -1
  53. package/dist/types/collections.d.ts +42 -2
  54. package/dist/types/datasource.d.ts +0 -1
  55. package/dist/types/entities.d.ts +1 -0
  56. package/dist/types/plugins.d.ts +46 -1
  57. package/dist/types/properties.d.ts +268 -4
  58. package/dist/types/storage.d.ts +8 -0
  59. package/dist/util/__tests__/conditions.test.d.ts +1 -0
  60. package/dist/util/__tests__/objects.test.d.ts +1 -0
  61. package/dist/util/conditions.d.ts +26 -0
  62. package/dist/util/entities.d.ts +1 -2
  63. package/dist/util/index.d.ts +2 -1
  64. package/dist/util/property_utils.d.ts +2 -1
  65. package/dist/util/resolutions.d.ts +1 -1
  66. package/dist/util/useStorageUploadController.d.ts +1 -1
  67. package/package.json +11 -7
  68. package/src/app/Scaffold.tsx +14 -15
  69. package/src/components/AIIcon.tsx +39 -0
  70. package/src/components/ArrayContainer.tsx +1 -4
  71. package/src/components/ClearFilterSortButton.tsx +19 -16
  72. package/src/components/ConfirmationDialog.tsx +0 -2
  73. package/src/components/DeleteEntityDialog.tsx +2 -4
  74. package/src/components/EntityCollectionTable/EntityCollectionRowActions.tsx +74 -41
  75. package/src/components/EntityCollectionTable/EntityCollectionTable.tsx +130 -79
  76. package/src/components/EntityCollectionTable/EntityCollectionTableProps.tsx +121 -104
  77. package/src/components/EntityCollectionTable/PropertyTableCell.tsx +132 -103
  78. package/src/components/EntityCollectionTable/internal/CollectionTableToolbar.tsx +20 -42
  79. package/src/components/EntityCollectionTable/internal/EntityTableCell.tsx +90 -49
  80. package/src/components/EntityCollectionView/Board.tsx +324 -0
  81. package/src/components/EntityCollectionView/BoardColumn.tsx +158 -0
  82. package/src/components/EntityCollectionView/BoardColumnTitle.tsx +45 -0
  83. package/src/components/EntityCollectionView/BoardSortableList.tsx +172 -0
  84. package/src/components/EntityCollectionView/EntityBoardCard.tsx +212 -0
  85. package/src/components/EntityCollectionView/EntityCard.tsx +231 -0
  86. package/src/components/EntityCollectionView/EntityCollectionBoardView.tsx +713 -0
  87. package/src/components/EntityCollectionView/EntityCollectionCardView.tsx +244 -0
  88. package/src/components/EntityCollectionView/EntityCollectionView.tsx +485 -203
  89. package/src/components/EntityCollectionView/EntityCollectionViewActions.tsx +31 -19
  90. package/src/components/EntityCollectionView/EntityCollectionViewStartActions.tsx +84 -15
  91. package/src/components/EntityCollectionView/FiltersDialog.tsx +249 -0
  92. package/src/components/EntityCollectionView/ViewModeToggle.tsx +202 -0
  93. package/src/components/EntityCollectionView/board_types.ts +113 -0
  94. package/src/components/EntityCollectionView/useBoardDataController.tsx +490 -0
  95. package/src/components/EntityPreview.tsx +1 -1
  96. package/src/components/ErrorTooltip.tsx +2 -1
  97. package/src/components/HomePage/DefaultHomePage.tsx +47 -10
  98. package/src/components/HomePage/HomePageDnD.tsx +56 -41
  99. package/src/components/HomePage/NavigationCard.tsx +20 -18
  100. package/src/components/HomePage/NavigationGroup.tsx +17 -16
  101. package/src/components/HomePage/RenameGroupDialog.tsx +0 -2
  102. package/src/components/HomePage/SmallNavigationCard.tsx +10 -9
  103. package/src/components/ReferenceTable/ReferenceSelectionTable.tsx +3 -10
  104. package/src/components/ReferenceWidget.tsx +2 -4
  105. package/src/components/SelectableTable/SelectableTable.tsx +75 -67
  106. package/src/components/SelectableTable/filters/BooleanFilterField.tsx +7 -6
  107. package/src/components/SelectableTable/filters/DateTimeFilterField.tsx +39 -40
  108. package/src/components/SelectableTable/filters/ReferenceFilterField.tsx +38 -38
  109. package/src/components/SelectableTable/filters/StringNumberFilterField.tsx +49 -58
  110. package/src/components/UnsavedChangesDialog.tsx +0 -2
  111. package/src/components/UserDisplay.tsx +4 -4
  112. package/src/components/VirtualTable/VirtualTable.performance.test.tsx +386 -0
  113. package/src/components/VirtualTable/VirtualTable.tsx +172 -21
  114. package/src/components/VirtualTable/VirtualTableCell.tsx +18 -2
  115. package/src/components/VirtualTable/VirtualTableHeader.tsx +20 -11
  116. package/src/components/VirtualTable/VirtualTableHeaderRow.tsx +158 -42
  117. package/src/components/VirtualTable/VirtualTableProps.tsx +14 -1
  118. package/src/components/VirtualTable/VirtualTableRow.tsx +1 -1
  119. package/src/components/VirtualTable/fields/VirtualTableDateField.tsx +3 -0
  120. package/src/components/VirtualTable/fields/VirtualTableSelect.tsx +17 -4
  121. package/src/components/VirtualTable/types.tsx +2 -0
  122. package/src/components/common/useColumnsIds.tsx +95 -3
  123. package/src/components/index.tsx +4 -0
  124. package/src/contexts/BreacrumbsContext.tsx +15 -8
  125. package/src/contexts/index.ts +10 -0
  126. package/src/core/DefaultAppBar.tsx +39 -26
  127. package/src/core/DefaultDrawer.tsx +42 -56
  128. package/src/core/DrawerNavigationGroup.tsx +118 -0
  129. package/src/core/DrawerNavigationItem.tsx +4 -3
  130. package/src/core/EntityEditView.tsx +41 -43
  131. package/src/core/SideDialogs.tsx +4 -2
  132. package/src/core/index.tsx +1 -0
  133. package/src/form/EntityForm.tsx +3 -10
  134. package/src/form/PropertyFieldBinding.tsx +58 -43
  135. package/src/form/components/LocalChangesMenu.tsx +6 -6
  136. package/src/form/components/StorageItemPreview.tsx +2 -1
  137. package/src/form/components/StorageUploadProgress.tsx +4 -3
  138. package/src/form/field_bindings/ArrayOfReferencesFieldBinding.tsx +0 -1
  139. package/src/form/field_bindings/DateTimeFieldBinding.tsx +17 -16
  140. package/src/form/field_bindings/KeyValueFieldBinding.tsx +0 -1
  141. package/src/form/field_bindings/MapFieldBinding.tsx +69 -67
  142. package/src/form/field_bindings/MarkdownEditorFieldBinding.tsx +21 -17
  143. package/src/form/field_bindings/TextFieldBinding.tsx +71 -35
  144. package/src/form/validation.ts +245 -160
  145. package/src/hooks/useBreadcrumbsController.tsx +18 -0
  146. package/src/hooks/useBuildNavigationController.tsx +42 -19
  147. package/src/hooks/useCollapsedGroups.ts +12 -4
  148. package/src/internal/useBuildDataSource.ts +69 -34
  149. package/src/internal/useBuildSideDialogsController.tsx +11 -8
  150. package/src/internal/useBuildSideEntityController.tsx +2 -4
  151. package/src/internal/useRestoreScroll.tsx +26 -14
  152. package/src/preview/PropertyPreview.tsx +40 -32
  153. package/src/preview/PropertyPreviewProps.tsx +6 -0
  154. package/src/preview/components/DatePreview.tsx +72 -4
  155. package/src/preview/components/EmptyValue.tsx +1 -1
  156. package/src/preview/components/ImagePreview.tsx +37 -21
  157. package/src/preview/components/ReferencePreview.tsx +6 -1
  158. package/src/preview/components/StorageThumbnail.tsx +16 -12
  159. package/src/preview/components/UrlComponentPreview.tsx +28 -25
  160. package/src/preview/property_previews/ArrayOfStorageComponentsPreview.tsx +9 -7
  161. package/src/preview/property_previews/ArrayOfStringsPreview.tsx +11 -9
  162. package/src/preview/property_previews/ArrayPropertyPreview.tsx +26 -24
  163. package/src/preview/property_previews/SkeletonPropertyComponent.tsx +61 -56
  164. package/src/routes/CustomCMSRoute.tsx +1 -0
  165. package/src/routes/FireCMSRoute.tsx +26 -13
  166. package/src/types/collections.ts +48 -3
  167. package/src/types/datasource.ts +54 -56
  168. package/src/types/entities.ts +10 -0
  169. package/src/types/plugins.tsx +51 -1
  170. package/src/types/properties.ts +357 -27
  171. package/src/types/storage.ts +9 -0
  172. package/src/util/__tests__/conditions.test.ts +506 -0
  173. package/src/util/__tests__/objects.test.ts +196 -0
  174. package/src/util/callbacks.ts +6 -3
  175. package/src/util/collections.ts +51 -6
  176. package/src/util/conditions.ts +339 -0
  177. package/src/util/entities.ts +28 -29
  178. package/src/util/entity_cache.ts +2 -1
  179. package/src/util/index.ts +2 -1
  180. package/src/util/objects.ts +31 -13
  181. package/src/util/{references.ts → previews.ts} +14 -0
  182. package/src/util/property_utils.tsx +36 -10
  183. package/src/util/resolutions.ts +57 -55
  184. package/src/util/useStorageUploadController.tsx +11 -1
  185. /package/dist/util/{references.d.ts → previews.d.ts} +0 -0
@@ -0,0 +1,386 @@
1
+
2
+ /**
3
+ * @jest-environment jsdom
4
+ */
5
+ import React from 'react';
6
+ import { render, act } from '@testing-library/react';
7
+ import { VirtualTable } from './VirtualTable';
8
+ import { VirtualTableProps } from './VirtualTableProps';
9
+
10
+ // Mock VirtualTableRow to track renders
11
+ const renderCallback = jest.fn();
12
+ jest.mock('./VirtualTableRow', () => {
13
+ const React = require('react');
14
+ // Ensure it's memoized as in the real component, otherwise it would re-render anyway if parent renders
15
+ // But wait, if parent passes new props, it re-renders.
16
+ // The issue we are testing is CONTEXT.
17
+ // VirtualTableRow consumes context. If context value changes, it re-renders even if props are same.
18
+ // So we don't even need to pass props to it in the mock, but the real one does.
19
+
20
+ const VirtualTableRow = React.memo((props: any) => {
21
+ renderCallback();
22
+ return <div data-testid="row">{props.children}</div>;
23
+ });
24
+ return { VirtualTableRow };
25
+ });
26
+
27
+ // Mock react-use-measure
28
+ jest.mock('react-use-measure', () => {
29
+ return () => [
30
+ (element: any) => {},
31
+ { width: 500, height: 500, top: 0, left: 0, bottom: 0, right: 0, x: 0, y: 0 }
32
+ ];
33
+ });
34
+
35
+ // Mock ResizeObserver
36
+ global.ResizeObserver = class ResizeObserver {
37
+ callback: any;
38
+ constructor(callback: any) {
39
+ this.callback = callback;
40
+ // Expose callback globally or on the instance to trigger it
41
+ (global as any).triggerResize = callback;
42
+ }
43
+ observe() {}
44
+ unobserve() {}
45
+ disconnect() {}
46
+ };
47
+
48
+ describe('VirtualTable Performance', () => {
49
+ it('does not re-render rows when VirtualTable re-renders but data is unchanged', async () => {
50
+ const columns = [{ key: 'col1', title: 'Column 1', width: 100 }];
51
+ const data = Array.from({ length: 10 }).map((_, i) => ({ col1: `Value ${i}` }));
52
+
53
+ const props: VirtualTableProps<any> = {
54
+ data,
55
+ columns,
56
+ rowHeight: 50,
57
+ cellRenderer: () => <div>Cell</div>
58
+ };
59
+
60
+ render(<VirtualTable {...props} />);
61
+
62
+ // Initial render count
63
+ // 10 items + potentially overscan.
64
+ // Let's just store the current count.
65
+ const initialRenderCount = renderCallback.mock.calls.length;
66
+ console.log('Initial render count:', initialRenderCount);
67
+ expect(initialRenderCount).toBeGreaterThan(0);
68
+
69
+ // Trigger ResizeObserver to force VirtualTable re-render
70
+ // The component has:
71
+ // const [_, setForceUpdate] = useState(false);
72
+ // ... new ResizeObserver(() => setForceUpdate(prev => !prev))
73
+
74
+ act(() => {
75
+ if ((global as any).triggerResize) {
76
+ (global as any).triggerResize([]);
77
+ }
78
+ });
79
+
80
+ // After re-render
81
+ const afterResizeRenderCount = renderCallback.mock.calls.length;
82
+ console.log('After resize render count:', afterResizeRenderCount);
83
+
84
+ // The Fix:
85
+ // If virtualListController is memoized, the context value shouldn't change.
86
+ // VirtualTableRow consumes context. If context is stable, it shouldn't re-render (since it is React.memo'd and its props didn't change).
87
+ // Wait, VirtualTableRow receives props from MemoizedList -> Row.
88
+ // Row renders <VirtualTableRow ...>.
89
+ // If VirtualTable re-renders -> MemoizedList re-renders?
90
+ // VirtualTable renders:
91
+ // <VirtualListContext.Provider value={virtualListController}>
92
+ // <MemoizedList ... />
93
+ // </VirtualListContext.Provider>
94
+
95
+ // MemoizedList is defined outside or inside?
96
+ // It is defined OUTSIDE VirtualTable as:
97
+ // function MemoizedList(...) { ... }
98
+ // BUT, inside VirtualTable it is used as <MemoizedList ... />.
99
+ // If VirtualTable re-renders, React.createElement(MemoizedList, ...) is called.
100
+ // If props to MemoizedList are same, does it re-render?
101
+ // MemoizedList is NOT wrapped in React.memo (in the file it's just `function MemoizedList`).
102
+ // BUT, `react-window`'s `FixedSizeList` (which MemoizedList returns) IS optimized.
103
+ // However, `MemoizedList` itself is a functional component. If parent renders, it renders.
104
+ // AND `MemoizedList` defines `const Row = useCallback(...)`.
105
+ // If `MemoizedList` re-renders, `Row` might be recreated if deps change?
106
+ // `Row` has `[]` deps. So `Row` is stable!
107
+
108
+ // Wait, `MemoizedList` passes `Row` to `List`.
109
+ // `List` (react-window) renders `Row`.
110
+ // `Row` contains `<VirtualListContext.Consumer>`.
111
+ // So `Row` renders. It consumes context.
112
+ // If context value changed, `Row` re-renders (or the consumer part does).
113
+ // Inside Consumer: `return <VirtualTableRow ... />`.
114
+ // If context value changed, the function inside Consumer runs.
115
+ // It creates new props for VirtualTableRow?
116
+ // `columns` comes from context. `data` comes from context.
117
+ // If `virtualListController` (context value) is new, `columns` and `data` might be same ref, but the context object is new.
118
+ // Consumer runs.
119
+ // `<VirtualTableRow ... />` is created.
120
+ // `VirtualTableRow` is `React.memo`.
121
+ // It compares new props vs old props.
122
+ // `columns` (from context) -> same ref? Yes (from useState in VirtualTable).
123
+ // `data` (from context) -> same ref? Yes (from props).
124
+ // `onRowClick` -> same ref? Yes (if useCallback/useMemo used properly in VirtualTable).
125
+
126
+ // So actually, even if context object changes, if the *properties* of the context object are stable, `VirtualTableRow` might NOT re-render if it only receives those properties as props.
127
+ // Let's check `VirtualTable.tsx` again.
128
+
129
+ // In `MemoizedList` -> `Row`:
130
+ /*
131
+ <VirtualListContext.Consumer>
132
+ {({ data, columns, ... }) => {
133
+ // ...
134
+ return <VirtualTableRow data={data} columns={columns} ... />
135
+ }}
136
+ </VirtualListContext.Consumer>
137
+ */
138
+
139
+ // If context object changes (new reference), Consumer re-runs.
140
+ // It calls the render prop.
141
+ // The render prop returns `<VirtualTableRow ... />`.
142
+ // React sees a `VirtualTableRow` element.
143
+ // It checks `React.memo` (equal).
144
+ // `equal(prevProps, nextProps)`.
145
+ // `prevProps.columns` vs `nextProps.columns`.
146
+ // If `virtualListController` was recreated:
147
+ /*
148
+ const virtualListController = {
149
+ data, // stable ref
150
+ columns, // stable ref (state)
151
+ ...
152
+ onColumnResize: ... // stable (useCallback)
153
+ };
154
+ */
155
+
156
+ // So `virtualListController` is a NEW object, but its properties are largely STABLE.
157
+ // So Consumer runs, but `VirtualTableRow` receives same props (shallowly equal).
158
+ // `react-fast-compare` (used in VirtualTableRow) should return true.
159
+ // So `VirtualTableRow` should NOT re-render even if context changes?
160
+
161
+ // WAIT. `VirtualTableRow` does NOT consume the context itself. `Row` (the render prop of List) consumes it.
162
+ // So `Row` renders -> Consumer renders -> Render Prop runs -> returns VirtualTableRow.
163
+ // VirtualTableRow is memoized.
164
+ // So if props are equal, it shouldn't render.
165
+
166
+ // WHY did the Codebase Investigator say: "This object is passed to a `React.Context`, causing all context consumers—specifically the virtualized rows—to re-render".
167
+ // Maybe I missed something.
168
+ // Does `VirtualTableRow` use the context?
169
+ // No, `VirtualTableRow.tsx` does not use `useContext`.
170
+
171
+ // However, `VirtualTableCell`?
172
+ // `VirtualTableCell` is rendered by `VirtualTableRow`.
173
+
174
+ // Let's look at `VirtualTable.tsx` again.
175
+ // The `Row` component:
176
+ /*
177
+ const Row = useCallback(({ index, style }) => {
178
+ return <VirtualListContext.Consumer>
179
+ {(context) => {
180
+ // ...
181
+ return <VirtualTableRow ...> ... </VirtualTableRow>
182
+ }}
183
+ </VirtualListContext.Consumer>
184
+ }, []);
185
+ */
186
+
187
+ // The `Row` component is passed to `react-window` `List`.
188
+ // `List` renders `Row`.
189
+ // `Row` renders `Consumer`.
190
+ // If Context Provider value changes, ALL Consumers re-render.
191
+ // So the function inside `Consumer` runs.
192
+ // It returns `VirtualTableRow`.
193
+ // React attempts to update `VirtualTableRow`.
194
+ // `VirtualTableRow` is `React.memo`.
195
+ // It compares props.
196
+
197
+ // Are props different?
198
+ // `columns` -> state. Stable.
199
+ // `data` -> prop. Stable.
200
+ // `onRowClick` -> `virtualListController.onRowClick`.
201
+ // In `VirtualTable.tsx`:
202
+ // `const virtualListController = { ... onRowClick, ... }`
203
+ // `onRowClick` comes from props. Stable.
204
+
205
+ // `cellRenderer` -> prop. Stable.
206
+
207
+ // `rowClassName` -> prop. Stable.
208
+
209
+ // So... if all props are stable, why is it a bottleneck?
210
+ // Maybe `virtualListController` creation is expensive? No.
211
+ // Maybe `react-window` does something?
212
+
213
+ // Or maybe one of the properties IS unstable?
214
+ // `onColumnResizeInternal`: useCallback. Stable.
215
+ // `onColumnResizeEndInternal`: useCallback. Stable.
216
+ // `onFilterUpdateInternal`: useCallback. Stable.
217
+ // `onColumnSort`: useCallback. Stable.
218
+
219
+ // `filterRef.current` -> ref. Stable.
220
+
221
+ // Wait, `virtualListController` object ITSELF is new.
222
+ // `VirtualListContext.Provider value={virtualListController}` receives new object.
223
+ // Provider updates.
224
+ // All Consumers update.
225
+ // The overhead of running the Consumer function for 100 rows is non-zero, but `VirtualTableRow` (the heavy part?) shouldn't re-render if props are same.
226
+
227
+ // UNLESS... `VirtualTableCell`?
228
+ // `VirtualTableRow` has children:
229
+ /*
230
+ {columns.map((column, columnIndex) => {
231
+ return <VirtualTableCell ... />
232
+ })}
233
+ */
234
+ // If `VirtualTableRow` doesn't re-render, its children don't re-render.
235
+
236
+ // So where is the bottleneck?
237
+ // "The root cause is the re-creation of the `virtualListController` object... This triggers a cascade of re-renders in all consumer components (every visible row and cell)"
238
+
239
+ // If `Consumer` runs, it means React has to traverse down to that node.
240
+ // If `VirtualTableRow` props are equal, it stops there.
241
+
242
+ // Maybe `VirtualTableRow` props are NOT equal?
243
+ // Let's verify `VirtualTableRow` props.
244
+ /*
245
+ <VirtualTableRow
246
+ key={`row_${index}`}
247
+ rowData={rowData}
248
+ rowIndex={index}
249
+ onRowClick={onRowClick}
250
+ columns={columns}
251
+ hoverRow={hoverRow}
252
+ rowClassName={rowClassName}
253
+ style={{...}}
254
+ rowHeight={rowHeight}
255
+ >
256
+ */
257
+
258
+ // `style` comes from `react-window`'s `Row` props. `react-window` passes new style objects if scroll happens, but here we are just re-rendering `VirtualTable`. `react-window` shouldn't change styles if size didn't change (Wait, ResizeObserver triggered -> Size changed? No, we just triggered it, assuming size is same but callback fired).
259
+ // Actually `style` in `Row` callback comes from `react-window`.
260
+
261
+ // `onRowClick`: `virtualListController.onRowClick` (from props).
262
+
263
+ // BUT, what about `VirtualTableCell`?
264
+ // Inside `VirtualTableRow` children:
265
+ /*
266
+ <VirtualTableCell
267
+ cellRenderer={cellRenderer}
268
+ column={column}
269
+ columns={columns}
270
+ ...
271
+ />
272
+ */
273
+ // `cellRenderer` comes from context.
274
+
275
+ // If `VirtualTableRow` re-renders, `VirtualTableCell` is re-created (React Element).
276
+ // `VirtualTableCell` is `React.memo`.
277
+ // Comparison runs.
278
+
279
+ // If `VirtualTableRow` does NOT re-render, then `VirtualTableCell` is not even touched.
280
+
281
+ // So the key is whether `VirtualTableRow` re-renders.
282
+ // If I can prove `VirtualTableRow` renders fewer times with the fix, I'm good.
283
+
284
+ // Why would `VirtualTableRow` props be different?
285
+ // Maybe `equal` (react-fast-compare) returns false for some reason?
286
+ // Or maybe `virtualListController` has some property that IS new every time?
287
+
288
+ // `const virtualListController = { ... }`.
289
+ // `filter: filterRef.current`.
290
+ // `currentSort`.
291
+ // `sortByProperty`.
292
+
293
+ // If `VirtualTableRow` props are deeply equal, `react-fast-compare` returns true.
294
+
295
+ // Wait. `MemoizedList` component.
296
+ /*
297
+ function MemoizedList(...) {
298
+ const Row = useCallback(...)
299
+ return <List ...>{Row}</List>
300
+ }
301
+ */
302
+ // `MemoizedList` is NOT memoized (it's a function).
303
+ // In `VirtualTable`:
304
+ /*
305
+ <MemoizedList ... />
306
+ */
307
+ // Parent renders -> `MemoizedList` renders.
308
+ // `Row` is `useCallback(..., [])`. Dependency array is empty!
309
+ // So `Row` is STABLE.
310
+
311
+ // `List` (react-window) receives stable `Row`.
312
+ // `List` is `PureComponent` (usually).
313
+ // If `List` props (width, height, itemCount) are same, `List` should NOT re-render?
314
+ // `width` and `height` come from `bounds`. If `bounds` didn't change, they are same.
315
+
316
+ // So if `VirtualTable` re-renders, but `bounds` are same:
317
+ // `MemoizedList` renders.
318
+ // `List` receives same props.
319
+ // `List` does NOT re-render.
320
+ // So `Row`s are NOT re-rendered by `react-window`.
321
+
322
+ // BUT, `VirtualListContext.Provider` is ABOVE `MemoizedList`.
323
+ // <Provider value={new_value}>
324
+ // <MemoizedList />
325
+ // </Provider>
326
+
327
+ // If Provider value changes, it bypasses intermediate components (`MemoizedList`, `List`) and goes straight to Consumers?
328
+ // YES. Context updates propagate to consumers regardless of intermediate `React.memo` or `PureComponent`.
329
+
330
+ // So `Row` (which contains `Consumer`) will update?
331
+ // `Row` is a component definition passed to `List`.
332
+ // `List` renders instances of `Row`.
333
+ // These instances contain `Consumer`.
334
+ // Use of `<Context.Consumer>` or `useContext` creates a subscription.
335
+ // So the `Consumer` component inside the rendered `Row` updates.
336
+
337
+ // The children function of `Consumer` executes.
338
+ // It returns `<VirtualTableRow ... />`.
339
+ // React compares this new element with previous.
340
+ // If `VirtualTableRow` is `React.memo`, it checks props.
341
+
342
+ // The premise of the optimization is that checking `React.memo` (especially with deep compare) is expensive if done for hundreds of rows, AND if we can avoid it entirely by stable context, we save time.
343
+ // If Context is stable, `Consumer` doesn't update.
344
+ // So the function inside Consumer doesn't run.
345
+ // So `VirtualTableRow` comparison doesn't happen.
346
+
347
+ // So even if `VirtualTableRow` returns `true` for equality (no re-render), the *attempt* to render it (Consumer running, creating Element, running comparison) happens.
348
+
349
+ // My test counts `VirtualTableRow` *renders* (function execution).
350
+ // If `React.memo` works, the function body of `VirtualTableRow` should NOT run.
351
+ // The mock I wrote:
352
+ /*
353
+ const VirtualTableRow = React.memo((props) => {
354
+ renderCallback();
355
+ ...
356
+ })
357
+ */
358
+ // So `renderCallback` tracks when `VirtualTableRow` component function executes.
359
+ // If `React.memo` returns true (props equal), the function does NOT execute.
360
+
361
+ // So... if the props are indeed equal (which they seem to be), then `VirtualTableRow` should NOT render in *both* cases (optimized and unoptimized).
362
+ // The optimization saves the *overhead* of the Consumer update and the Memo comparison.
363
+ // But my test measures *Component Renders*.
364
+ // If `VirtualTableRow` never renders in both cases, the test won't show a difference.
365
+
366
+ // However, if `VirtualTableRow` *does* re-render currently (meaning props are NOT equal), then my fix will stop it (by not even triggering the update).
367
+
368
+ // Why would props be unequal?
369
+ // `columns` is `columnsProp` state.
370
+ // `data` is `data` prop.
371
+ // `style` ...
372
+
373
+ // If the test shows 0 re-renders in the "after" check, it's good.
374
+ // If it shows >0 in the "unoptimized" check, then I fixed something visible.
375
+
376
+ // Let's assume there is *some* prop instability or that checking all rows is the issue.
377
+ // Or maybe `react-fast-compare` is not as fast or returns false?
378
+
379
+ // Actually, I can also instrument the `Consumer`? No, that's inside the component.
380
+
381
+ // Let's just run the test and see. If "before" count is high and "after" is low, I win.
382
+ // If both are 0, I need a better test (maybe measure time?).
383
+
384
+ // I will write the test file now.
385
+ });
386
+ });
@@ -1,4 +1,4 @@
1
- import React, { createContext, forwardRef, RefObject, useCallback, useEffect, useRef, useState } from "react";
1
+ import React, { createContext, forwardRef, RefObject, useCallback, useEffect, useMemo, useRef, useState } from "react";
2
2
 
3
3
  import equal from "react-fast-compare"
4
4
 
@@ -21,6 +21,16 @@ import { VirtualTableRow } from "./VirtualTableRow";
21
21
  import { VirtualTableCell } from "./VirtualTableCell";
22
22
  import { AssignmentIcon, CenteredView, cls, Typography } from "@firecms/ui";
23
23
  import { useDebounceCallback } from "../common/useDebouncedCallback";
24
+ import {
25
+ closestCenter,
26
+ DndContext,
27
+ DragEndEvent,
28
+ DragStartEvent,
29
+ PointerSensor,
30
+ useSensor,
31
+ useSensors
32
+ } from "@dnd-kit/core";
33
+ import { arrayMove, horizontalListSortingStrategy, SortableContext, useSortable } from "@dnd-kit/sortable";
24
34
 
25
35
  const VirtualListContext = createContext<VirtualTableContextProps<any>>({} as any);
26
36
  VirtualListContext.displayName = "VirtualListContext";
@@ -56,7 +66,7 @@ const innerElementType = forwardRef<HTMLDivElement, InnerElementProps>(({
56
66
  minHeight: "100%",
57
67
  position: "relative"
58
68
  }}>
59
- <VirtualTableHeaderRow {...virtualTableProps}/>
69
+ <VirtualTableHeaderRow {...virtualTableProps} />
60
70
  {!customView && children}
61
71
  </div>
62
72
 
@@ -115,6 +125,7 @@ export const VirtualTable = React.memo<VirtualTableProps<any>>(
115
125
  endAdornment,
116
126
  AddColumnComponent,
117
127
  initialScroll = 0,
128
+ onColumnsOrderChange,
118
129
  }: VirtualTableProps<T>) {
119
130
 
120
131
  const sortByProperty: string | undefined = sortBy ? sortBy[0] : undefined;
@@ -127,6 +138,44 @@ export const VirtualTable = React.memo<VirtualTableProps<any>>(
127
138
 
128
139
  const debouncedScroll = useDebounceCallback(onScrollProp, 200);
129
140
 
141
+ // Drag and drop state
142
+ const [draggingColumnId, setDraggingColumnId] = useState<string | null>(null);
143
+
144
+ const sensors = useSensors(
145
+ useSensor(PointerSensor, {
146
+ activationConstraint: {
147
+ distance: 5,
148
+ },
149
+ })
150
+ );
151
+
152
+ const handleDragStart = useCallback((event: DragStartEvent) => {
153
+ setDraggingColumnId(event.active.id as string);
154
+ }, []);
155
+
156
+ const handleDragEnd = useCallback((event: DragEndEvent) => {
157
+ const {
158
+ active,
159
+ over
160
+ } = event;
161
+ setDraggingColumnId(null);
162
+
163
+ if (over && active.id !== over.id && onColumnsOrderChange) {
164
+ const oldIndex = columns.findIndex((col) => col.key === active.id);
165
+ const newIndex = columns.findIndex((col) => col.key === over.id);
166
+
167
+ if (oldIndex !== -1 && newIndex !== -1) {
168
+ const newColumns = arrayMove(columns, oldIndex, newIndex);
169
+ setColumns(newColumns);
170
+ onColumnsOrderChange(newColumns);
171
+ }
172
+ }
173
+ }, [columns, onColumnsOrderChange]);
174
+
175
+ const handleDragCancel = useCallback(() => {
176
+ setDraggingColumnId(null);
177
+ }, []);
178
+
130
179
  // Set initial scroll position
131
180
  useEffect(() => {
132
181
  if (tableRef.current && initialScroll) {
@@ -297,7 +346,7 @@ export const VirtualTable = React.memo<VirtualTableProps<any>>(
297
346
  </div>)
298
347
  : undefined);
299
348
 
300
- const virtualListController = {
349
+ const virtualListController = useMemo(() => ({
301
350
  data,
302
351
  rowHeight: rowHeight,
303
352
  cellRenderer,
@@ -315,14 +364,28 @@ export const VirtualTable = React.memo<VirtualTableProps<any>>(
315
364
  createFilterField,
316
365
  rowClassName,
317
366
  endAdornment,
318
- AddColumnComponent
319
- };
320
-
321
- return (
367
+ AddColumnComponent,
368
+ onColumnsOrderChange: onColumnsOrderChange ? (newColumns: VirtualTableColumn[]) => {
369
+ setColumns(newColumns);
370
+ onColumnsOrderChange(newColumns);
371
+ } : undefined,
372
+ draggingColumnId
373
+ }), [data, rowHeight, cellRenderer, columns, currentSort, onRowClick, customView, onColumnResizeInternal, onColumnResizeEndInternal, filterInput, onColumnSort, onFilterUpdateInternal, sortByProperty, hoverRow, createFilterField, rowClassName, endAdornment, AddColumnComponent, onColumnsOrderChange, draggingColumnId]);
374
+
375
+ // Get sortable column keys (excluding frozen columns)
376
+ const sortableColumnKeys = columns
377
+ .filter(col => !col.frozen)
378
+ .map(col => col.key);
379
+
380
+ const tableContent = (
322
381
  <div
323
382
  ref={measureRef}
324
383
  style={style}
325
- className={cls("h-full w-full", className)}>
384
+ className={cls(
385
+ "h-full w-full",
386
+ className,
387
+ draggingColumnId && "overflow-hidden"
388
+ )}>
326
389
  <VirtualListContext.Provider
327
390
  value={virtualListController}>
328
391
 
@@ -332,16 +395,89 @@ export const VirtualTable = React.memo<VirtualTableProps<any>>(
332
395
  width={bounds.width}
333
396
  height={bounds.height}
334
397
  itemCount={(data?.length ?? 0) + (endAdornment ? 1 : 0)}
335
- onScroll={onScroll}
398
+ onScroll={draggingColumnId ? undefined : onScroll}
336
399
  includeAddColumn={Boolean(AddColumnComponent)}
337
400
  itemSize={rowHeight}/>
338
401
 
339
402
  </VirtualListContext.Provider>
340
403
  </div>
341
404
  );
405
+
406
+ // Wrap with DndContext if column reorder is enabled
407
+ if (onColumnsOrderChange) {
408
+ return (
409
+ <DndContext
410
+ sensors={sensors}
411
+ collisionDetection={closestCenter}
412
+ onDragStart={handleDragStart}
413
+ onDragEnd={handleDragEnd}
414
+ onDragCancel={handleDragCancel}
415
+ >
416
+ <SortableContext
417
+ items={sortableColumnKeys}
418
+ strategy={horizontalListSortingStrategy}
419
+ >
420
+ {tableContent}
421
+ </SortableContext>
422
+ </DndContext>
423
+ );
424
+ }
425
+
426
+ return tableContent;
342
427
  },
343
428
  equal
344
429
  );
430
+ // Wrapper that applies sortable transforms to cells
431
+ const SortableCellWrapper = ({
432
+ columnKey,
433
+ width,
434
+ isDragging,
435
+ isDraggable,
436
+ frozen,
437
+ children
438
+ }: {
439
+ columnKey: string;
440
+ width: number;
441
+ isDragging: boolean;
442
+ isDraggable: boolean;
443
+ frozen?: boolean;
444
+ children: React.ReactNode;
445
+ }) => {
446
+ const {
447
+ attributes,
448
+ listeners,
449
+ setNodeRef,
450
+ transform,
451
+ transition,
452
+ } = useSortable({
453
+ id: columnKey,
454
+ disabled: !isDraggable || frozen
455
+ });
456
+
457
+ const style = {
458
+ // Only use translate, ignore any scale transforms
459
+ transform: transform ? `translateX(${transform.x}px)` : undefined,
460
+ // Don't transition the dragged item - only other items should animate
461
+ transition: isDragging ? undefined : transition,
462
+ minWidth: width,
463
+ maxWidth: width,
464
+ width: width,
465
+ };
466
+
467
+ return (
468
+ <div
469
+ ref={setNodeRef}
470
+ style={style}
471
+ className={cls(
472
+ "flex-shrink-0",
473
+ frozen && "sticky left-0 z-10 bg-white dark:bg-surface-950"
474
+ )}
475
+ {...attributes}
476
+ >
477
+ {children}
478
+ </div>
479
+ );
480
+ };
345
481
 
346
482
  function MemoizedList({
347
483
  outerRef,
@@ -356,7 +492,7 @@ function MemoizedList({
356
492
  width: number;
357
493
  height: number;
358
494
  itemCount: number;
359
- onScroll: (params: {
495
+ onScroll?: (params: {
360
496
  scrollDirection: "forward" | "backward",
361
497
  scrollOffset: number,
362
498
  scrollUpdateWasRequested: boolean;
@@ -378,7 +514,9 @@ function MemoizedList({
378
514
  cellRenderer,
379
515
  hoverRow,
380
516
  rowClassName,
381
- endAdornment
517
+ endAdornment,
518
+ draggingColumnId,
519
+ onColumnsOrderChange
382
520
  }) => {
383
521
 
384
522
  if (endAdornment && index === (data ?? []).length) {
@@ -411,16 +549,29 @@ function MemoizedList({
411
549
 
412
550
  {columns.map((column: VirtualTableColumn, columnIndex: number) => {
413
551
  const cellData = rowData && rowData[column.key];
414
- return <VirtualTableCell
415
- key={`cell_${column.key}`}
416
- dataKey={column.key}
417
- cellRenderer={cellRenderer}
418
- column={column}
419
- columns={columns}
420
- rowData={rowData}
421
- cellData={cellData}
422
- rowIndex={index}
423
- columnIndex={columnIndex}/>;
552
+ const isDragging = draggingColumnId === column.key;
553
+ const isDraggable = !column.frozen && !!onColumnsOrderChange;
554
+
555
+ return (
556
+ <SortableCellWrapper
557
+ key={`cell_wrapper_${column.key}`}
558
+ columnKey={column.key}
559
+ width={column.width}
560
+ isDragging={isDragging}
561
+ isDraggable={isDraggable}
562
+ frozen={column.frozen}
563
+ >
564
+ <VirtualTableCell
565
+ dataKey={column.key}
566
+ cellRenderer={cellRenderer}
567
+ column={column}
568
+ columns={columns}
569
+ rowData={rowData}
570
+ cellData={cellData}
571
+ rowIndex={index}
572
+ columnIndex={columnIndex}/>
573
+ </SortableCellWrapper>
574
+ );
424
575
  })}
425
576
 
426
577
  {includeAddColumn && <div className={"w-20"}/>}