@dxos/react-ui-table 0.7.4 → 0.7.5-labs.401163d

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 (120) hide show
  1. package/dist/lib/browser/{chunk-IYCSJDMB.mjs → chunk-3FR6NHPD.mjs} +7 -5
  2. package/dist/lib/browser/chunk-3FR6NHPD.mjs.map +7 -0
  3. package/dist/lib/browser/index.mjs +1488 -1112
  4. package/dist/lib/browser/index.mjs.map +4 -4
  5. package/dist/lib/browser/meta.json +1 -1
  6. package/dist/lib/browser/types/index.mjs +1 -1
  7. package/dist/lib/node/{chunk-MHBMVCFZ.cjs → chunk-NB3SFHGA.cjs} +9 -7
  8. package/dist/lib/node/chunk-NB3SFHGA.cjs.map +7 -0
  9. package/dist/lib/node/index.cjs +1493 -1097
  10. package/dist/lib/node/index.cjs.map +4 -4
  11. package/dist/lib/node/meta.json +1 -1
  12. package/dist/lib/node/types/index.cjs +3 -3
  13. package/dist/lib/node/types/index.cjs.map +1 -1
  14. package/dist/lib/node-esm/{chunk-BMWGPXPL.mjs → chunk-RLVVOZW5.mjs} +7 -5
  15. package/dist/lib/node-esm/chunk-RLVVOZW5.mjs.map +7 -0
  16. package/dist/lib/node-esm/index.mjs +1488 -1112
  17. package/dist/lib/node-esm/index.mjs.map +4 -4
  18. package/dist/lib/node-esm/meta.json +1 -1
  19. package/dist/lib/node-esm/types/index.mjs +1 -1
  20. package/dist/types/src/_deprecated/hooks/useColumnSorting.d.ts +1 -1
  21. package/dist/types/src/components/Table/ColumnActionsMenu.d.ts +4 -3
  22. package/dist/types/src/components/Table/ColumnActionsMenu.d.ts.map +1 -1
  23. package/dist/types/src/components/Table/ColumnSettings.d.ts +3 -2
  24. package/dist/types/src/components/Table/ColumnSettings.d.ts.map +1 -1
  25. package/dist/types/src/components/Table/CreateRefPanel.d.ts +3 -2
  26. package/dist/types/src/components/Table/CreateRefPanel.d.ts.map +1 -1
  27. package/dist/types/src/components/Table/RefPanel.d.ts +4 -3
  28. package/dist/types/src/components/Table/RefPanel.d.ts.map +1 -1
  29. package/dist/types/src/components/Table/RowActionsMenu.d.ts +3 -2
  30. package/dist/types/src/components/Table/RowActionsMenu.d.ts.map +1 -1
  31. package/dist/types/src/components/Table/Table.d.ts +2 -1
  32. package/dist/types/src/components/Table/Table.d.ts.map +1 -1
  33. package/dist/types/src/components/Table/Table.stories.d.ts +1 -4
  34. package/dist/types/src/components/Table/Table.stories.d.ts.map +1 -1
  35. package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts +3 -2
  36. package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts.map +1 -1
  37. package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts.map +1 -1
  38. package/dist/types/src/components/TableToolbar/TableToolbar.d.ts +18 -0
  39. package/dist/types/src/components/TableToolbar/TableToolbar.d.ts.map +1 -0
  40. package/dist/types/src/components/TableToolbar/index.d.ts +2 -0
  41. package/dist/types/src/components/TableToolbar/index.d.ts.map +1 -0
  42. package/dist/types/src/components/index.d.ts +1 -1
  43. package/dist/types/src/components/index.d.ts.map +1 -1
  44. package/dist/types/src/components/testing.d.ts +31 -71
  45. package/dist/types/src/components/testing.d.ts.map +1 -1
  46. package/dist/types/src/hooks/useTableModel.d.ts.map +1 -1
  47. package/dist/types/src/index.d.ts +1 -0
  48. package/dist/types/src/index.d.ts.map +1 -1
  49. package/dist/types/src/model/index.d.ts +1 -0
  50. package/dist/types/src/model/index.d.ts.map +1 -1
  51. package/dist/types/src/model/modal-controller.d.ts +6 -2
  52. package/dist/types/src/model/modal-controller.d.ts.map +1 -1
  53. package/dist/types/src/model/selection-model.d.ts.map +1 -1
  54. package/dist/types/src/model/table-model.d.ts +19 -31
  55. package/dist/types/src/model/table-model.d.ts.map +1 -1
  56. package/dist/types/src/model/table-presentation.d.ts +22 -0
  57. package/dist/types/src/model/table-presentation.d.ts.map +1 -0
  58. package/dist/types/src/model/table-presentation.test.d.ts +2 -0
  59. package/dist/types/src/model/table-presentation.test.d.ts.map +1 -0
  60. package/dist/types/src/model/table-sorting.d.ts +53 -0
  61. package/dist/types/src/model/table-sorting.d.ts.map +1 -0
  62. package/dist/types/src/playwright/TableManager.d.ts +2 -1
  63. package/dist/types/src/playwright/TableManager.d.ts.map +1 -1
  64. package/dist/types/src/translations.d.ts +2 -1
  65. package/dist/types/src/translations.d.ts.map +1 -1
  66. package/dist/types/src/types/table.d.ts +46 -154
  67. package/dist/types/src/types/table.d.ts.map +1 -1
  68. package/dist/types/src/util/index.d.ts +1 -0
  69. package/dist/types/src/util/index.d.ts.map +1 -1
  70. package/dist/types/src/util/table-buttons.d.ts +63 -19
  71. package/dist/types/src/util/table-buttons.d.ts.map +1 -1
  72. package/dist/types/src/util/table-controls.d.ts +32 -9
  73. package/dist/types/src/util/table-controls.d.ts.map +1 -1
  74. package/dist/types/src/util/table.d.ts +3 -2
  75. package/dist/types/src/util/table.d.ts.map +1 -1
  76. package/dist/types/tsconfig.tsbuildinfo +1 -0
  77. package/package.json +35 -34
  78. package/src/_deprecated/schema/Schema.stories.tsx +1 -1
  79. package/src/components/Table/ColumnActionsMenu.tsx +12 -12
  80. package/src/components/Table/ColumnSettings.tsx +11 -11
  81. package/src/components/Table/CreateRefPanel.tsx +13 -14
  82. package/src/components/Table/RefPanel.tsx +9 -9
  83. package/src/components/Table/RowActionsMenu.tsx +6 -7
  84. package/src/components/Table/Table.stories.tsx +84 -56
  85. package/src/components/Table/Table.tsx +239 -148
  86. package/src/components/TableCellEditor/TableCellEditor.stories.tsx +13 -12
  87. package/src/components/TableCellEditor/TableCellEditor.tsx +6 -4
  88. package/src/components/TableToolbar/TableToolbar.tsx +72 -0
  89. package/src/components/{Toolbar → TableToolbar}/index.ts +1 -1
  90. package/src/components/index.ts +1 -1
  91. package/src/components/testing.ts +10 -8
  92. package/src/hooks/useTableModel.ts +19 -0
  93. package/src/index.ts +2 -0
  94. package/src/model/index.ts +1 -0
  95. package/src/model/modal-controller.ts +15 -48
  96. package/src/model/selection-model.ts +12 -1
  97. package/src/model/table-model.test.ts +4 -76
  98. package/src/model/table-model.ts +73 -304
  99. package/src/model/table-presentation.test.ts +102 -0
  100. package/src/model/table-presentation.ts +235 -0
  101. package/src/model/table-sorting.ts +215 -0
  102. package/src/playwright/TableManager.ts +5 -1
  103. package/src/playwright/smoke.spec.ts +94 -9
  104. package/src/translations.ts +2 -1
  105. package/src/types/table.ts +4 -4
  106. package/src/util/index.ts +1 -0
  107. package/src/util/table-buttons.ts +127 -48
  108. package/src/util/table-controls.ts +71 -7
  109. package/src/util/table.ts +93 -52
  110. package/dist/lib/browser/chunk-IYCSJDMB.mjs.map +0 -7
  111. package/dist/lib/node/chunk-MHBMVCFZ.cjs.map +0 -7
  112. package/dist/lib/node-esm/chunk-BMWGPXPL.mjs.map +0 -7
  113. package/dist/types/src/components/Toolbar/Toolbar.d.ts +0 -21
  114. package/dist/types/src/components/Toolbar/Toolbar.d.ts.map +0 -1
  115. package/dist/types/src/components/Toolbar/common.d.ts +0 -14
  116. package/dist/types/src/components/Toolbar/common.d.ts.map +0 -1
  117. package/dist/types/src/components/Toolbar/index.d.ts +0 -2
  118. package/dist/types/src/components/Toolbar/index.d.ts.map +0 -1
  119. package/src/components/Toolbar/Toolbar.tsx +0 -87
  120. package/src/components/Toolbar/common.tsx +0 -71
@@ -3,47 +3,32 @@
3
3
  //
4
4
 
5
5
  import { computed, effect, signal, type ReadonlySignal } from '@preact/signals-core';
6
- import orderBy from 'lodash.orderby';
7
6
 
8
7
  import { Resource } from '@dxos/context';
9
- import { getValue, setValue, FormatEnum, type JsonProp } from '@dxos/echo-schema';
8
+ import { type FieldSortType, FormatEnum, getValue, setValue, type JsonProp } from '@dxos/echo-schema';
10
9
  import { invariant } from '@dxos/invariant';
10
+ import { isReactiveObject, makeRef } from '@dxos/live-object';
11
11
  import { PublicKey } from '@dxos/react-client';
12
- import {
13
- cellClassesForFieldType,
14
- cellClassesForRowSelection,
15
- formatForDisplay,
16
- formatForEditing,
17
- parseValue,
18
- } from '@dxos/react-ui-form';
12
+ import { formatForEditing, parseValue } from '@dxos/react-ui-form';
19
13
  import {
20
14
  type DxGridAxisMeta,
21
- type DxGridCellValue,
22
- type DxGridPlane,
23
- type DxGridPlaneCells,
24
15
  type DxGridPlaneRange,
25
16
  type DxGridPlanePosition,
26
- toPlaneCellIndex,
27
17
  type DxGridPosition,
28
18
  } from '@dxos/react-ui-grid';
29
- import { mx } from '@dxos/react-ui-theme';
30
- import { VIEW_FIELD_LIMIT, type ViewProjection, type FieldType } from '@dxos/schema';
19
+ import { type ViewProjection } from '@dxos/schema';
31
20
 
32
- import { ModalController } from './modal-controller';
33
21
  import { SelectionModel } from './selection-model';
22
+ import { TableSorting } from './table-sorting';
34
23
  import { type TableType } from '../types';
35
- import { tableButtons, touch } from '../util';
36
- import { tableControls } from '../util/table-controls';
37
-
38
- export type SortDirection = 'asc' | 'desc';
39
- export type SortConfig = { fieldId: string; direction: SortDirection };
24
+ import { touch } from '../util';
40
25
 
41
26
  export type BaseTableRow = Record<JsonProp, any> & { id: string };
42
27
 
43
28
  export type TableModelProps<T extends BaseTableRow = { id: string }> = {
44
29
  table: TableType;
45
30
  projection: ViewProjection;
46
- sorting?: SortConfig[];
31
+ sorting?: FieldSortType[];
47
32
  pinnedRows?: { top: number[]; bottom: number[] };
48
33
  onInsertRow?: (index?: number) => void;
49
34
  onDeleteRows?: (index: number, obj: T[]) => void;
@@ -63,28 +48,18 @@ export class TableModel<T extends BaseTableRow = { id: string }> extends Resourc
63
48
  end: { row: 0, col: 0 },
64
49
  });
65
50
 
66
- /**
67
- * Maps display indices to data indices.
68
- * Used for translating between sorted/displayed order and original data order.
69
- * Keys are display indices, values are corresponding data indices.
70
- */
71
- private readonly _displayToDataIndex: Map<number, number> = new Map();
72
-
73
51
  private readonly _onInsertRow?: TableModelProps<T>['onInsertRow'];
74
52
  private readonly _onDeleteRows?: TableModelProps<T>['onDeleteRows'];
75
53
  private readonly _onDeleteColumn?: TableModelProps<T>['onDeleteColumn'];
76
54
  private readonly _onCellUpdate?: TableModelProps<T>['onCellUpdate'];
77
55
  private readonly _onRowOrderChanged?: TableModelProps<T>['onRowOrderChanged'];
78
56
 
79
- private readonly _sorting = signal<SortConfig | undefined>(undefined);
80
- private _sortedRows!: ReadonlySignal<T[]>;
81
- private _rows = signal<T[]>([]);
57
+ private readonly _rows = signal<T[]>([]);
58
+ private readonly _sorting: TableSorting<T>;
82
59
 
83
60
  private _pinnedRows: NonNullable<TableModelProps<T>['pinnedRows']>;
84
-
85
61
  private _selection!: SelectionModel<T>;
86
62
  private _columnMeta?: ReadonlySignal<DxGridAxisMeta>;
87
- private readonly _modalController = new ModalController();
88
63
 
89
64
  constructor({
90
65
  table,
@@ -100,8 +75,13 @@ export class TableModel<T extends BaseTableRow = { id: string }> extends Resourc
100
75
  super();
101
76
  this._table = table;
102
77
  this._projection = projection;
78
+ this._sorting = new TableSorting(this._rows, table.view?.target, projection);
79
+
80
+ if (sorting.length > 0) {
81
+ const [sort] = sorting;
82
+ this._sorting.setSort(sort.fieldId, sort.direction);
83
+ }
103
84
 
104
- this._sorting.value = sorting.at(0);
105
85
  this._pinnedRows = pinnedRows;
106
86
  this._onInsertRow = onInsertRow;
107
87
  this._onDeleteRows = onDeleteRows;
@@ -118,6 +98,17 @@ export class TableModel<T extends BaseTableRow = { id: string }> extends Resourc
118
98
  return this._projection;
119
99
  }
120
100
 
101
+ public get rows(): ReadonlySignal<T[]> {
102
+ return this._sorting.sortedRows;
103
+ }
104
+
105
+ /**
106
+ * @reactive
107
+ */
108
+ public get sorting(): TableSorting<T> | undefined {
109
+ return this._sorting;
110
+ }
111
+
121
112
  public get pinnedRows(): NonNullable<TableModelProps<T>['pinnedRows']> {
122
113
  return this._pinnedRows;
123
114
  }
@@ -127,33 +118,29 @@ export class TableModel<T extends BaseTableRow = { id: string }> extends Resourc
127
118
  return this._columnMeta;
128
119
  }
129
120
 
130
- public get sorting(): SortConfig | undefined {
131
- return this._sorting.value;
132
- }
133
-
134
- public get modalController() {
135
- return this._modalController;
136
- }
137
-
138
121
  public get selection() {
139
122
  return this._selection;
140
123
  }
141
124
 
125
+ /** @reactive */
126
+ public get isViewDirty(): boolean {
127
+ return this._sorting.isDirty;
128
+ }
129
+
142
130
  //
143
131
  // Initialisation
144
132
  //
145
133
 
146
134
  protected override async _open() {
147
135
  this.initializeColumnMeta();
148
- this.initializeSorting();
149
136
  this.initializeEffects();
150
- this._selection = new SelectionModel(this._sortedRows, () => this._onRowOrderChanged?.());
137
+ this._selection = new SelectionModel(this._sorting.sortedRows, () => this._onRowOrderChanged?.());
151
138
  await this._selection.open(this._ctx);
152
139
  }
153
140
 
154
141
  private initializeColumnMeta(): void {
155
142
  this._columnMeta = computed(() => {
156
- const fields = this._table.view?.fields ?? [];
143
+ const fields = this._table.view?.target?.fields ?? [];
157
144
  const meta = Object.fromEntries(
158
145
  fields.map((field, index: number) => [index, { size: field?.size ?? 256, resizeable: true }]),
159
146
  );
@@ -166,39 +153,9 @@ export class TableModel<T extends BaseTableRow = { id: string }> extends Resourc
166
153
  });
167
154
  }
168
155
 
169
- private initializeSorting(): void {
170
- this._sortedRows = computed(() => {
171
- this._displayToDataIndex.clear();
172
- const sort = this._sorting.value;
173
- if (!sort) {
174
- return this._rows.value;
175
- }
176
-
177
- const field = this._table.view?.fields.find((field) => field.id === sort.fieldId);
178
- if (!field) {
179
- return this._rows.value;
180
- }
181
-
182
- const dataWithIndices = this._rows.value.map((item, index) => {
183
- const value = getValue(item, field.path);
184
- return { item, index, value, isEmpty: value == null || value === '' };
185
- });
186
- const sorted = orderBy(dataWithIndices, ['isEmpty', 'value'], ['asc', sort.direction]);
187
-
188
- for (let displayIndex = 0; displayIndex < sorted.length; displayIndex++) {
189
- const { index: dataIndex } = sorted[displayIndex];
190
- if (displayIndex !== dataIndex) {
191
- this._displayToDataIndex.set(displayIndex, dataIndex);
192
- }
193
- }
194
-
195
- return sorted.map(({ item }) => item);
196
- });
197
- }
198
-
199
156
  private initializeEffects(): void {
200
157
  const rowOrderWatcher = effect(() => {
201
- touch(this._sortedRows.value);
158
+ touch(this._sorting.sortedRows.value);
202
159
  this._onRowOrderChanged?.();
203
160
  });
204
161
  this._ctx.onDispose(rowOrderWatcher);
@@ -214,8 +171,8 @@ export class TableModel<T extends BaseTableRow = { id: string }> extends Resourc
214
171
  for (let row = start.row; row <= end.row; row++) {
215
172
  rowEffects.push(
216
173
  effect(() => {
217
- const obj = this._sortedRows.value[row];
218
- this?._table?.view?.fields.forEach((field) => touch(getValue(obj, field.path)));
174
+ const obj = this._sorting.sortedRows.value[row];
175
+ this?._table?.view?.target?.fields.forEach((field) => touch(getValue(obj, field.path)));
219
176
  this._onCellUpdate?.({ row, col: start.col, plane: 'grid' });
220
177
  }),
221
178
  );
@@ -226,181 +183,6 @@ export class TableModel<T extends BaseTableRow = { id: string }> extends Resourc
226
183
  this._ctx.onDispose(rowEffectManager);
227
184
  }
228
185
 
229
- //
230
- // Get Cells
231
- //
232
-
233
- public getCells = (range: DxGridPlaneRange, plane: DxGridPlane): DxGridPlaneCells => {
234
- switch (plane) {
235
- case 'grid': {
236
- this._visibleRange.value = range;
237
- return this.getMainGridCells(range);
238
- }
239
- case 'frozenRowsStart': {
240
- return this.getHeaderCells(range);
241
- }
242
- case 'frozenColsStart': {
243
- return this.getSelectionColumnCells(range);
244
- }
245
- case 'frozenColsEnd': {
246
- return this.getActionColumnCells(range);
247
- }
248
- case 'fixedStartStart': {
249
- return this.getSelectAllCell();
250
- }
251
- case 'fixedStartEnd': {
252
- return this.getNewColumnCell();
253
- }
254
- default: {
255
- return {};
256
- }
257
- }
258
- };
259
-
260
- private getMainGridCells = (range: DxGridPlaneRange): DxGridPlaneCells => {
261
- const cells: DxGridPlaneCells = {};
262
- const fields = this._table.view?.fields ?? [];
263
-
264
- const addCell = (obj: T, field: FieldType, colIndex: number, displayIndex: number): void => {
265
- const { props } = this._projection.getFieldProjection(field.id);
266
-
267
- const cell: DxGridCellValue = {
268
- get value() {
269
- const value = getValue(obj, field.path);
270
- if (value == null) {
271
- return '';
272
- }
273
-
274
- switch (props.format) {
275
- case FormatEnum.Ref: {
276
- if (!field.referencePath) {
277
- return ''; // TODO(burdon): Show error.
278
- }
279
-
280
- return getValue(value, field.referencePath);
281
- }
282
-
283
- default: {
284
- return formatForDisplay({ type: props.type, format: props.format, value });
285
- }
286
- }
287
- },
288
- };
289
-
290
- const classes = [];
291
- const formatClasses = cellClassesForFieldType({ type: props.type, format: props.format });
292
- if (formatClasses) {
293
- classes.push(formatClasses);
294
- }
295
- const rowSelectionClasses = cellClassesForRowSelection(this._selection.isObjectSelected(obj));
296
- if (rowSelectionClasses) {
297
- classes.push(rowSelectionClasses);
298
- }
299
- if (classes.length > 0) {
300
- cell.className = mx(classes.flat());
301
- }
302
-
303
- if (cell.value && props.format === FormatEnum.Ref && props.referenceSchema) {
304
- const targetObj = getValue(obj, field.path);
305
- cell.accessoryHtml = tableButtons.referencedCell.render({
306
- targetId: targetObj.id,
307
- schemaId: props.referenceSchema,
308
- });
309
- }
310
-
311
- const idx = toPlaneCellIndex({ col: colIndex, row: displayIndex });
312
- cells[idx] = cell;
313
- };
314
-
315
- for (let row = range.start.row; row <= range.end.row && row < this._sortedRows.value.length; row++) {
316
- for (let col = range.start.col; col <= range.end.col && col < fields.length; col++) {
317
- const field = fields[col];
318
- if (!field) {
319
- continue;
320
- }
321
-
322
- addCell(this._sortedRows.value[row], field, col, row);
323
- }
324
- }
325
-
326
- return cells;
327
- };
328
-
329
- private getHeaderCells = (range: DxGridPlaneRange): DxGridPlaneCells => {
330
- const cells: DxGridPlaneCells = {};
331
- const fields = this.table.view?.fields ?? [];
332
- for (let col = range.start.col; col <= range.end.col && col < fields.length; col++) {
333
- const { field, props } = this._projection.getFieldProjection(fields[col].id);
334
- cells[toPlaneCellIndex({ col, row: 0 })] = {
335
- // TODO(burdon): Use same logic as form for fallback title.
336
- value: props.title ?? field.path,
337
- readonly: true,
338
- resizeHandle: 'col',
339
- accessoryHtml: tableButtons.columnSettings.render({ fieldId: field.id }),
340
- };
341
- }
342
-
343
- return cells;
344
- };
345
-
346
- private getSelectionColumnCells = (range: DxGridPlaneRange): DxGridPlaneCells => {
347
- const cells: DxGridPlaneCells = {};
348
- for (let row = range.start.row; row <= range.end.row && row < this._rows.value.length; row++) {
349
- const isSelected = this._selection.isRowIndexSelected(row);
350
- const classes = cellClassesForRowSelection(isSelected);
351
- cells[toPlaneCellIndex({ col: 0, row })] = {
352
- value: '',
353
- readonly: true,
354
- className: classes ? mx(classes) : undefined,
355
- accessoryHtml: tableControls.checkbox.render({ rowIndex: row, checked: isSelected }),
356
- };
357
- }
358
-
359
- return cells;
360
- };
361
-
362
- private getActionColumnCells = (range: DxGridPlaneRange): DxGridPlaneCells => {
363
- const cells: DxGridPlaneCells = {};
364
- for (let row = range.start.row; row <= range.end.row && row < this._rows.value.length; row++) {
365
- const isSelected = this._selection.isRowIndexSelected(row);
366
- const classes = cellClassesForRowSelection(isSelected);
367
- cells[toPlaneCellIndex({ col: 0, row })] = {
368
- value: '',
369
- readonly: true,
370
- className: classes ? mx(classes) : undefined,
371
- accessoryHtml: tableButtons.rowMenu.render({ rowIndex: row }),
372
- };
373
- }
374
-
375
- return cells;
376
- };
377
-
378
- private getSelectAllCell = (): DxGridPlaneCells => {
379
- return {
380
- [toPlaneCellIndex({ col: 0, row: 0 })]: {
381
- value: '',
382
- accessoryHtml: tableControls.checkbox.render({
383
- rowIndex: 0,
384
- header: true,
385
- checked: this._selection.allRowsSeleted.value,
386
- }),
387
- readonly: true,
388
- },
389
- };
390
- };
391
-
392
- private getNewColumnCell = (): DxGridPlaneCells => {
393
- return {
394
- [toPlaneCellIndex({ col: 0, row: 0 })]: {
395
- value: '',
396
- accessoryHtml: tableButtons.addColumn.render({
397
- disabled: (this._table.view?.fields?.length ?? 0) >= VIEW_FIELD_LIMIT,
398
- }),
399
- readonly: true,
400
- },
401
- };
402
- };
403
-
404
186
  //
405
187
  // Data
406
188
  //
@@ -411,15 +193,15 @@ export class TableModel<T extends BaseTableRow = { id: string }> extends Resourc
411
193
 
412
194
  public getRowCount = (): number => this._rows.value.length;
413
195
 
414
- public getColumnCount = (): number => this.table.view?.fields.length ?? 0;
196
+ public getColumnCount = (): number => this.table.view?.target?.fields.length ?? 0;
415
197
 
416
198
  public insertRow = (rowIndex?: number): void => {
417
- const row = rowIndex !== undefined ? this._displayToDataIndex.get(rowIndex) ?? rowIndex : this._rows.value.length;
199
+ const row = rowIndex !== undefined ? this._sorting.getDataIndex(rowIndex) : this._rows.value.length;
418
200
  this._onInsertRow?.(row);
419
201
  };
420
202
 
421
203
  public deleteRow = (rowIndex: number): void => {
422
- const row = this._displayToDataIndex.get(rowIndex) ?? rowIndex;
204
+ const row = this._sorting.getDataIndex(rowIndex);
423
205
  const obj = this._rows.value[row];
424
206
  const objectsToDelete = [];
425
207
 
@@ -434,13 +216,13 @@ export class TableModel<T extends BaseTableRow = { id: string }> extends Resourc
434
216
  };
435
217
 
436
218
  public getCellData = ({ col, row }: DxGridPlanePosition): any => {
437
- const fields = this.table.view?.fields ?? [];
219
+ const fields = this.table.view?.target?.fields ?? [];
438
220
  if (col < 0 || col >= fields.length) {
439
221
  return undefined;
440
222
  }
441
223
 
442
224
  const field = fields[col];
443
- const dataIndex = this._displayToDataIndex.get(row) ?? row;
225
+ const dataIndex = this._sorting.getDataIndex(row);
444
226
  const value = getValue(this._rows.value[dataIndex], field.path);
445
227
  if (value == null) {
446
228
  return '';
@@ -453,7 +235,7 @@ export class TableModel<T extends BaseTableRow = { id: string }> extends Resourc
453
235
  return ''; // TODO(burdon): Show error.
454
236
  }
455
237
 
456
- return getValue(value, field.referencePath);
238
+ return getValue(value.target, field.referencePath);
457
239
  }
458
240
 
459
241
  default: {
@@ -463,8 +245,8 @@ export class TableModel<T extends BaseTableRow = { id: string }> extends Resourc
463
245
  };
464
246
 
465
247
  public setCellData = ({ col, row }: DxGridPlanePosition, value: any): void => {
466
- const rowIdx = this._displayToDataIndex.get(row) ?? row;
467
- const fields = this.table.view?.fields ?? [];
248
+ const rowIdx = this._sorting.getDataIndex(row);
249
+ const fields = this.table.view?.target?.fields ?? [];
468
250
  if (col < 0 || col >= fields.length) {
469
251
  return;
470
252
  }
@@ -473,7 +255,11 @@ export class TableModel<T extends BaseTableRow = { id: string }> extends Resourc
473
255
  const { props } = this._projection.getFieldProjection(field.id);
474
256
  switch (props.format) {
475
257
  case FormatEnum.Ref: {
476
- setValue(this._rows.value[rowIdx], field.path, value);
258
+ // TODO(ZaymonFC): This get's called an additional time by the cell editor onBlur, but with the cell editors
259
+ // plain string value. Maybe onBlur should be called with the actual value?
260
+ if (isReactiveObject(value)) {
261
+ setValue(this._rows.value[rowIdx], field.path, makeRef(value));
262
+ }
477
263
  break;
478
264
  }
479
265
 
@@ -492,6 +278,21 @@ export class TableModel<T extends BaseTableRow = { id: string }> extends Resourc
492
278
  }
493
279
  };
494
280
 
281
+ /**
282
+ * Updates a cell's value using a transform function.
283
+ * @param {DxGridPlanePosition} position - The position of the cell to update.
284
+ * @param {(value: any) => any} update - A function that takes the current value and returns the updated value.
285
+ */
286
+ public updateCellData({ col, row }: DxGridPlanePosition, update: (value: any) => any): void {
287
+ const dataRow = this._sorting.getDataIndex(row);
288
+ const fields = this.table.view?.target?.fields ?? [];
289
+ const field = fields[col];
290
+
291
+ const value = getValue(this._rows.value[dataRow], field.path);
292
+ const updatedValue = update(value);
293
+ setValue(this._rows.value[dataRow], field.path, updatedValue);
294
+ }
295
+
495
296
  //
496
297
  // Column Operations
497
298
  //
@@ -501,45 +302,18 @@ export class TableModel<T extends BaseTableRow = { id: string }> extends Resourc
501
302
  return;
502
303
  }
503
304
 
504
- const field = this.table.view.fields.find((field) => field.id === fieldId);
305
+ const field = this.table.view.target?.fields.find((field) => field.id === fieldId);
505
306
  if (field && this._onDeleteColumn) {
506
- if (this._sorting.value?.fieldId === fieldId) {
507
- this.clearSort();
508
- }
509
307
  this._onDeleteColumn(field.id);
510
308
  }
511
309
  }
512
310
 
513
- //
514
- // Interactions
515
- //
516
- public handleGridClick = (event: React.MouseEvent): void => {
517
- if (!this._modalController.handleClick(event)) {
518
- const target = event.target as HTMLElement;
519
-
520
- const selectionCheckbox = target.closest(`input[${tableControls.checkbox.attributes.checkbox}]`);
521
- if (selectionCheckbox) {
522
- const isHeader = selectionCheckbox.hasAttribute(tableControls.checkbox.attributes.header);
523
- if (isHeader) {
524
- if (this._selection.allRowsSeleted.value) {
525
- this._selection.setSelection('none');
526
- } else {
527
- this._selection.setSelection('all');
528
- }
529
- } else {
530
- const rowIndex = Number(selectionCheckbox.getAttribute(tableControls.checkbox.attributes.checkbox));
531
- this._selection.toggleSelectionForRowIndex(rowIndex);
532
- }
533
- }
534
- }
535
- };
536
-
537
311
  //
538
312
  // Resize
539
313
  //
540
314
 
541
315
  public setColumnWidth(columnIndex: number, width: number): void {
542
- const fields = this.table.view?.fields ?? [];
316
+ const fields = this.table.view?.target?.fields ?? [];
543
317
  if (columnIndex < fields.length) {
544
318
  const newWidth = Math.max(0, width);
545
319
  const field = fields[columnIndex];
@@ -549,18 +323,6 @@ export class TableModel<T extends BaseTableRow = { id: string }> extends Resourc
549
323
  }
550
324
  }
551
325
 
552
- //
553
- // Sorting
554
- //
555
-
556
- public setSort(fieldId: string, direction: SortDirection): void {
557
- this._sorting.value = { fieldId, direction };
558
- }
559
-
560
- public clearSort(): void {
561
- this._sorting.value = undefined;
562
- }
563
-
564
326
  //
565
327
  // Pinning
566
328
  //
@@ -574,4 +336,11 @@ export class TableModel<T extends BaseTableRow = { id: string }> extends Resourc
574
336
  this._pinnedRows.top = this._pinnedRows.top.filter((index: number) => index !== rowIndex);
575
337
  this._pinnedRows.bottom = this._pinnedRows.bottom.filter((index: number) => index !== rowIndex);
576
338
  }
339
+
340
+ //
341
+ // View operations
342
+ //
343
+ public saveView(): void {
344
+ this._sorting.save();
345
+ }
577
346
  }
@@ -0,0 +1,102 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { describe, expect, it, beforeEach } from 'vitest';
6
+
7
+ import { S, TypedObject } from '@dxos/echo-schema';
8
+ import { create, makeRef } from '@dxos/live-object';
9
+ import { createEchoSchema } from '@dxos/live-object/testing';
10
+ import { createView, ViewProjection } from '@dxos/schema';
11
+
12
+ import { TableModel, type TableModelProps } from './table-model';
13
+ import { TablePresentation } from './table-presentation';
14
+ import { TableType } from '../types';
15
+
16
+ describe('TablePresentation', () => {
17
+ describe('row reactivity', () => {
18
+ let data: any[];
19
+ let updateCount: number;
20
+ let model: any;
21
+ let presentation: TablePresentation;
22
+
23
+ beforeEach(async () => {
24
+ updateCount = 0;
25
+ ({ data } = create({
26
+ data: [
27
+ { col1: 'A', col2: 1, col3: true },
28
+ { col1: 'B', col2: 2, col3: false },
29
+ ],
30
+ }));
31
+
32
+ model = createTableModel({
33
+ onCellUpdate: () => updateCount++,
34
+ });
35
+ model.setRows(data);
36
+ await model.open();
37
+ presentation = new TablePresentation(model);
38
+ });
39
+
40
+ it('should update with row-level reactivity', () => {
41
+ // Set up visible range to include our test data.
42
+ presentation.getCells({ start: { row: 0, col: 0 }, end: { row: 1, col: 2 } }, 'grid');
43
+
44
+ // Trigger a row update.
45
+ data[0].col1 = 'New Value';
46
+ expect(updateCount).toBe(1);
47
+
48
+ // Verify the new value through getCells.
49
+ const cells = presentation.getCells({ start: { row: 0, col: 0 }, end: { row: 0, col: 0 } }, 'grid');
50
+ expect(cells['0,0']?.value).toBe('New Value');
51
+ });
52
+
53
+ it('should update reactively when adding a new row', () => {
54
+ // Set up visible range to include our test data.
55
+ presentation.getCells({ start: { row: 0, col: 0 }, end: { row: 2, col: 2 } }, 'grid');
56
+
57
+ data.push({ col1: 'C', col2: 3, col3: true });
58
+ expect(updateCount).toBe(1);
59
+
60
+ const cells = presentation.getCells({ start: { row: 0, col: 0 }, end: { row: 2, col: 2 } }, 'grid');
61
+
62
+ // Verify the new row's data.
63
+ expect(cells['0,2']?.value).toBe('C');
64
+ expect(cells['1,2']?.value).toBe('3');
65
+ expect(cells['2,2']?.value).toBe('true');
66
+ });
67
+
68
+ it('should handle combined operations reactively', () => {
69
+ // Set up visible range.
70
+ presentation.getCells({ start: { row: 0, col: 0 }, end: { row: 2, col: 2 } }, 'grid');
71
+
72
+ // Multiple operations.
73
+ data[0].col1 = 'Updated A';
74
+ data.push({ col1: 'C', col2: 3, col3: true });
75
+ data.splice(1, 1);
76
+
77
+ // We expect one update per operation affecting visible rows.
78
+ expect(updateCount).toBe(3);
79
+
80
+ const cells = presentation.getCells({ start: { row: 0, col: 0 }, end: { row: 1, col: 2 } }, 'grid');
81
+
82
+ // Verify final state.
83
+ expect(cells['0,0']?.value).toBe('Updated A');
84
+ expect(cells['0,1']?.value).toBe('C');
85
+ expect(cells['1,1']?.value).toBe('3');
86
+ expect(cells['2,1']?.value).toBe('true');
87
+ });
88
+ });
89
+ });
90
+
91
+ class Test extends TypedObject({ typename: 'example.com/type/Test', version: '0.1.0' })({
92
+ title: S.String,
93
+ completed: S.Boolean,
94
+ }) {}
95
+
96
+ const createTableModel = (props: Partial<TableModelProps> = {}): TableModel => {
97
+ const schema = createEchoSchema(Test);
98
+ const view = createView({ name: 'Test', typename: schema.typename, jsonSchema: schema.jsonSchema });
99
+ const projection = new ViewProjection(schema, view);
100
+ const table = create(TableType, { view: makeRef(view) });
101
+ return new TableModel({ table, projection, ...props });
102
+ };