@dxos/react-ui-table 0.8.4-main.67995b8 → 0.8.4-main.70d3990

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 (143) hide show
  1. package/dist/lib/browser/chunk-5XWSB3JP.mjs +71 -0
  2. package/dist/lib/browser/chunk-5XWSB3JP.mjs.map +7 -0
  3. package/dist/lib/browser/{chunk-5XLSHLOG.mjs → chunk-KNC7XYT6.mjs} +573 -541
  4. package/dist/lib/browser/chunk-KNC7XYT6.mjs.map +7 -0
  5. package/dist/lib/browser/index.mjs +625 -698
  6. package/dist/lib/browser/index.mjs.map +4 -4
  7. package/dist/lib/browser/meta.json +1 -1
  8. package/dist/lib/browser/testing/index.mjs +16 -24
  9. package/dist/lib/browser/testing/index.mjs.map +3 -3
  10. package/dist/lib/browser/types/index.mjs +3 -3
  11. package/dist/lib/node-esm/{chunk-Y66YLSTC.mjs → chunk-CWSGS3Y2.mjs} +573 -541
  12. package/dist/lib/node-esm/chunk-CWSGS3Y2.mjs.map +7 -0
  13. package/dist/lib/node-esm/chunk-MKTMOY46.mjs +72 -0
  14. package/dist/lib/node-esm/chunk-MKTMOY46.mjs.map +7 -0
  15. package/dist/lib/node-esm/index.mjs +625 -698
  16. package/dist/lib/node-esm/index.mjs.map +4 -4
  17. package/dist/lib/node-esm/meta.json +1 -1
  18. package/dist/lib/node-esm/testing/index.mjs +16 -24
  19. package/dist/lib/node-esm/testing/index.mjs.map +3 -3
  20. package/dist/lib/node-esm/types/index.mjs +3 -3
  21. package/dist/types/src/components/Table/ColumnActionsMenu.d.ts +1 -1
  22. package/dist/types/src/components/Table/ColumnActionsMenu.d.ts.map +1 -1
  23. package/dist/types/src/components/Table/ColumnSettings.d.ts +1 -1
  24. package/dist/types/src/components/Table/ColumnSettings.d.ts.map +1 -1
  25. package/dist/types/src/components/Table/DynamicTable.d.ts +2 -2
  26. package/dist/types/src/components/Table/DynamicTable.d.ts.map +1 -1
  27. package/dist/types/src/components/Table/DynamicTable.stories.d.ts +51 -3
  28. package/dist/types/src/components/Table/DynamicTable.stories.d.ts.map +1 -1
  29. package/dist/types/src/components/Table/Relations.stories.d.ts +108 -0
  30. package/dist/types/src/components/Table/Relations.stories.d.ts.map +1 -0
  31. package/dist/types/src/components/Table/RowActionsMenu.d.ts +1 -1
  32. package/dist/types/src/components/Table/RowActionsMenu.d.ts.map +1 -1
  33. package/dist/types/src/components/Table/Table.d.ts +8 -4
  34. package/dist/types/src/components/Table/Table.d.ts.map +1 -1
  35. package/dist/types/src/components/Table/Table.stories.d.ts +105 -4
  36. package/dist/types/src/components/Table/Table.stories.d.ts.map +1 -1
  37. package/dist/types/src/components/TableCellEditor/CellValidationMessage.d.ts.map +1 -1
  38. package/dist/types/src/components/TableCellEditor/FormCellEditor.d.ts +12 -6
  39. package/dist/types/src/components/TableCellEditor/FormCellEditor.d.ts.map +1 -1
  40. package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts +12 -17
  41. package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts.map +1 -1
  42. package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts +39 -4
  43. package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts.map +1 -1
  44. package/dist/types/src/components/TableCellEditor/index.d.ts +1 -0
  45. package/dist/types/src/components/TableCellEditor/index.d.ts.map +1 -1
  46. package/dist/types/src/components/TableToolbar/TableToolbar.d.ts +2 -2
  47. package/dist/types/src/components/TableToolbar/TableToolbar.d.ts.map +1 -1
  48. package/dist/types/src/components/testing.d.ts +3 -5
  49. package/dist/types/src/components/testing.d.ts.map +1 -1
  50. package/dist/types/src/hooks/index.d.ts +2 -1
  51. package/dist/types/src/hooks/index.d.ts.map +1 -1
  52. package/dist/types/src/hooks/useAddRow.d.ts +3 -2
  53. package/dist/types/src/hooks/useAddRow.d.ts.map +1 -1
  54. package/dist/types/src/hooks/useProjectionModel.d.ts +5 -0
  55. package/dist/types/src/hooks/useProjectionModel.d.ts.map +1 -0
  56. package/dist/types/src/hooks/useTableModel.d.ts +5 -6
  57. package/dist/types/src/hooks/useTableModel.d.ts.map +1 -1
  58. package/dist/types/src/model/modal-controller.d.ts +0 -7
  59. package/dist/types/src/model/modal-controller.d.ts.map +1 -1
  60. package/dist/types/src/model/selection-model.d.ts.map +1 -1
  61. package/dist/types/src/model/table-model.d.ts +14 -14
  62. package/dist/types/src/model/table-model.d.ts.map +1 -1
  63. package/dist/types/src/model/table-presentation.d.ts +2 -1
  64. package/dist/types/src/model/table-presentation.d.ts.map +1 -1
  65. package/dist/types/src/model/table-sorting.d.ts +9 -6
  66. package/dist/types/src/model/table-sorting.d.ts.map +1 -1
  67. package/dist/types/src/playwright/TableManager.d.ts +1 -1
  68. package/dist/types/src/playwright/TableManager.d.ts.map +1 -1
  69. package/dist/types/src/testing/useTestTableModel.d.ts +21 -34
  70. package/dist/types/src/testing/useTestTableModel.d.ts.map +1 -1
  71. package/dist/types/src/translations.d.ts +2 -2
  72. package/dist/types/src/types/Table.d.ts +28 -0
  73. package/dist/types/src/types/Table.d.ts.map +1 -0
  74. package/dist/types/src/types/index.d.ts +1 -1
  75. package/dist/types/src/types/index.d.ts.map +1 -1
  76. package/dist/types/src/util/dynamic-table.d.ts +9 -9
  77. package/dist/types/src/util/dynamic-table.d.ts.map +1 -1
  78. package/dist/types/src/util/index.d.ts +0 -1
  79. package/dist/types/src/util/index.d.ts.map +1 -1
  80. package/dist/types/src/util/schema.d.ts +1 -1
  81. package/dist/types/src/util/schema.d.ts.map +1 -1
  82. package/dist/types/src/util/table-buttons.d.ts.map +1 -1
  83. package/dist/types/src/util/table-controls.d.ts.map +1 -1
  84. package/dist/types/tsconfig.tsbuildinfo +1 -1
  85. package/package.json +61 -59
  86. package/src/components/Table/ColumnActionsMenu.tsx +1 -1
  87. package/src/components/Table/ColumnSettings.tsx +1 -1
  88. package/src/components/Table/DynamicTable.stories.tsx +21 -17
  89. package/src/components/Table/DynamicTable.tsx +19 -13
  90. package/src/components/Table/Relations.stories.tsx +237 -0
  91. package/src/components/Table/RowActionsMenu.tsx +23 -15
  92. package/src/components/Table/Table.stories.tsx +103 -86
  93. package/src/components/Table/Table.tsx +140 -115
  94. package/src/components/TableCellEditor/CellValidationMessage.tsx +1 -1
  95. package/src/components/TableCellEditor/FormCellEditor.tsx +153 -42
  96. package/src/components/TableCellEditor/TableCellEditor.stories.tsx +28 -66
  97. package/src/components/TableCellEditor/TableCellEditor.tsx +75 -225
  98. package/src/components/TableCellEditor/index.ts +1 -0
  99. package/src/components/TableToolbar/TableToolbar.tsx +34 -24
  100. package/src/components/testing.ts +4 -4
  101. package/src/hooks/index.ts +2 -1
  102. package/src/hooks/useAddRow.ts +7 -5
  103. package/src/hooks/useProjectionModel.ts +19 -0
  104. package/src/hooks/useTableModel.ts +12 -14
  105. package/src/model/modal-controller.ts +1 -29
  106. package/src/model/selection-model.ts +3 -2
  107. package/src/model/table-model.test.ts +21 -12
  108. package/src/model/table-model.ts +62 -56
  109. package/src/model/table-presentation.test.ts +25 -23
  110. package/src/model/table-presentation.ts +82 -41
  111. package/src/model/table-sorting.ts +32 -14
  112. package/src/playwright/TableManager.ts +3 -3
  113. package/src/playwright/smoke.spec.ts +132 -38
  114. package/src/testing/useTestTableModel.ts +34 -22
  115. package/src/translations.ts +2 -2
  116. package/src/types/Table.ts +89 -0
  117. package/src/types/index.ts +1 -1
  118. package/src/util/dynamic-table.ts +20 -24
  119. package/src/util/index.ts +0 -1
  120. package/src/util/schema.ts +4 -4
  121. package/src/util/table-buttons.ts +4 -3
  122. package/src/util/table-controls.ts +4 -1
  123. package/dist/lib/browser/chunk-5XLSHLOG.mjs.map +0 -7
  124. package/dist/lib/browser/chunk-L2LSB6KV.mjs +0 -18
  125. package/dist/lib/browser/chunk-L2LSB6KV.mjs.map +0 -7
  126. package/dist/lib/node-esm/chunk-NXZ4CJYX.mjs +0 -20
  127. package/dist/lib/node-esm/chunk-NXZ4CJYX.mjs.map +0 -7
  128. package/dist/lib/node-esm/chunk-Y66YLSTC.mjs.map +0 -7
  129. package/dist/types/src/components/Table/CreateRefPanel.d.ts +0 -9
  130. package/dist/types/src/components/Table/CreateRefPanel.d.ts.map +0 -1
  131. package/dist/types/src/components/Table/relations.stories.d.ts +0 -9
  132. package/dist/types/src/components/Table/relations.stories.d.ts.map +0 -1
  133. package/dist/types/src/components/TableCellEditor/extension.d.ts +0 -13
  134. package/dist/types/src/components/TableCellEditor/extension.d.ts.map +0 -1
  135. package/dist/types/src/types/schema.d.ts +0 -8
  136. package/dist/types/src/types/schema.d.ts.map +0 -1
  137. package/dist/types/src/util/table.d.ts +0 -11
  138. package/dist/types/src/util/table.d.ts.map +0 -1
  139. package/src/components/Table/CreateRefPanel.tsx +0 -86
  140. package/src/components/Table/relations.stories.tsx +0 -116
  141. package/src/components/TableCellEditor/extension.ts +0 -61
  142. package/src/types/schema.ts +0 -16
  143. package/src/util/table.ts +0 -49
@@ -2,39 +2,37 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import { computed, effect, signal, type ReadonlySignal } from '@preact/signals-core';
5
+ import { type ReadonlySignal, computed, effect, signal } from '@preact/signals-core';
6
6
 
7
7
  import { Resource } from '@dxos/context';
8
- import { Obj, Ref } from '@dxos/echo';
9
- import {
10
- type FieldSortType,
11
- FormatEnum,
12
- getValue,
13
- setValue,
14
- type JsonProp,
15
- getSnapshot,
16
- getSchema,
17
- toEffectSchema,
18
- type JsonSchemaType,
19
- } from '@dxos/echo-schema';
8
+ import { Format, Obj, Ref } from '@dxos/echo';
9
+ import { type JsonProp, type JsonSchemaType, getValue, setValue, toEffectSchema } from '@dxos/echo/internal';
20
10
  import { invariant } from '@dxos/invariant';
21
11
  import { ObjectId } from '@dxos/keys';
22
- import { isLiveObject } from '@dxos/live-object';
23
- import { fullyQualifiedId } from '@dxos/react-client/echo';
12
+ import { getSnapshot, isLiveObject } from '@dxos/live-object';
13
+ import { type Label } from '@dxos/react-ui';
24
14
  import { formatForEditing, parseValue } from '@dxos/react-ui-form';
25
15
  import {
26
16
  type DxGridAxisMeta,
27
- type DxGridPlaneRange,
28
17
  type DxGridPlanePosition,
18
+ type DxGridPlaneRange,
29
19
  type DxGridPosition,
30
20
  } from '@dxos/react-ui-grid';
31
- import { type DataType, ProjectionModel, type PropertyType, validateSchema, type ValidationError } from '@dxos/schema';
21
+ import {
22
+ type FieldSortType,
23
+ type ProjectionModel,
24
+ type PropertyType,
25
+ type ValidationError,
26
+ type View,
27
+ validateSchema,
28
+ } from '@dxos/schema';
29
+
30
+ import { type Table } from '../types';
31
+ import { touch } from '../util';
32
+ import { extractTagIds } from '../util/tag';
32
33
 
33
34
  import { type SelectionMode, SelectionModel } from './selection-model';
34
35
  import { TableSorting } from './table-sorting';
35
- import { TableView } from '../types';
36
- import { touch } from '../util';
37
- import { extractTagIds } from '../util/tag';
38
36
 
39
37
  // Domain types for cell classification
40
38
  export type TableCellType = 'standard' | 'draft' | 'header';
@@ -53,7 +51,7 @@ export type TableRow = Record<JsonProp, any> & { id: string };
53
51
 
54
52
  export type TableRowAction = {
55
53
  id: string;
56
- translationKey: string;
54
+ label: Label;
57
55
  };
58
56
 
59
57
  export type TableFeatures = {
@@ -68,17 +66,18 @@ const defaultFeatures: TableFeatures = {
68
66
  schemaEditable: false,
69
67
  };
70
68
 
69
+ export type InsertRowResult = 'draft' | 'final';
70
+
71
71
  export type TableModelProps<T extends TableRow = TableRow> = {
72
- view: DataType.View;
73
- schema: JsonSchemaType;
74
- projection?: ProjectionModel;
72
+ object: Table.Table;
73
+ projection: ProjectionModel;
75
74
  features?: Partial<TableFeatures>;
76
75
  sorting?: FieldSortType[];
77
76
  initialSelection?: string[];
78
77
  pinnedRows?: { top: number[]; bottom: number[] };
79
78
  rowActions?: TableRowAction[];
80
79
  onResolveSchema?: (typename: string) => Promise<JsonSchemaType>;
81
- onInsertRow?: (data?: any) => boolean;
80
+ onInsertRow?: (data?: any) => InsertRowResult;
82
81
  onDeleteRows?: (index: number, obj: T[]) => void;
83
82
  onDeleteColumn?: (fieldId: string) => void;
84
83
  onCellUpdate?: (cell: DxGridPosition) => void;
@@ -87,16 +86,15 @@ export type TableModelProps<T extends TableRow = TableRow> = {
87
86
  };
88
87
 
89
88
  export class TableModel<T extends TableRow = TableRow> extends Resource {
90
- private readonly _view: DataType.View;
89
+ private readonly _object: Table.Table;
91
90
  private readonly _projection: ProjectionModel;
92
- private _table?: TableView;
93
91
 
94
92
  private readonly _visibleRange = signal<DxGridPlaneRange>({
95
93
  start: { row: 0, col: 0 },
96
94
  end: { row: 0, col: 0 },
97
95
  });
98
96
 
99
- private readonly _onInsertRow?: (data?: any) => boolean;
97
+ private readonly _onInsertRow?: (data?: any) => InsertRowResult;
100
98
  private readonly _onDeleteRows?: TableModelProps<T>['onDeleteRows'];
101
99
  private readonly _onDeleteColumn?: TableModelProps<T>['onDeleteColumn'];
102
100
  private readonly _onCellUpdate?: TableModelProps<T>['onCellUpdate'];
@@ -114,8 +112,7 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
114
112
  private _columnMeta?: ReadonlySignal<DxGridAxisMeta>;
115
113
 
116
114
  constructor({
117
- view,
118
- schema,
115
+ object,
119
116
  projection,
120
117
  features = {},
121
118
  sorting = [],
@@ -130,8 +127,9 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
130
127
  onRowAction,
131
128
  }: TableModelProps<T>) {
132
129
  super();
133
- this._view = view;
134
- this._projection = projection ?? new ProjectionModel(schema, view.projection);
130
+ this._object = object;
131
+ this._projection = projection;
132
+ this._projection.normalizeView();
135
133
 
136
134
  // TODO(ZaymonFC): Use our more robust config merging module?
137
135
  this._features = { ...defaultFeatures, ...features };
@@ -141,7 +139,11 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
141
139
  'Single selection is not compatible with editable tables.',
142
140
  );
143
141
 
144
- this._sorting = new TableSorting(this._rows, this._view, this._projection);
142
+ this._sorting = new TableSorting({
143
+ rows: this._rows,
144
+ getView: () => this.view,
145
+ projection: this._projection,
146
+ });
145
147
 
146
148
  if (sorting.length > 0) {
147
149
  const [sort] = sorting;
@@ -166,16 +168,17 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
166
168
  }
167
169
 
168
170
  public get id(): string {
169
- return fullyQualifiedId(this._view);
171
+ return Obj.getDXN(this._object).toString();
170
172
  }
171
173
 
172
- public get view(): DataType.View {
173
- return this._view;
174
+ public get view(): View.View {
175
+ const view = this._object.view.target;
176
+ invariant(view, 'Table model not initialized');
177
+ return view;
174
178
  }
175
179
 
176
- public get table(): TableView {
177
- invariant(this._table, 'Model not initialized');
178
- return this._table;
180
+ public get table(): Table.Table {
181
+ return this._object;
179
182
  }
180
183
 
181
184
  public get projection(): ProjectionModel {
@@ -235,10 +238,7 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
235
238
  //
236
239
 
237
240
  protected override async _open(): Promise<void> {
238
- const presentation = this._view.presentation.target ?? (await this._view.presentation.load());
239
- invariant(Obj.instanceOf(TableView, presentation));
240
- this._table = presentation;
241
-
241
+ await this._object.view.load();
242
242
  this.initializeColumnMeta();
243
243
  this.initializeEffects();
244
244
  await this._selection.open(this._ctx);
@@ -254,7 +254,7 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
254
254
  return {
255
255
  grid: meta,
256
256
  frozenColsStart: { 0: { size: 30, resizeable: false } },
257
- frozenColsEnd: { 0: { size: 40, resizeable: false } },
257
+ frozenColsEnd: { 0: { size: 32, resizeable: false } },
258
258
  };
259
259
  });
260
260
  }
@@ -334,11 +334,12 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
334
334
 
335
335
  public getColumnCount = (): number => this._projection?.fields.length ?? 0;
336
336
 
337
- public insertRow = (): void => {
337
+ public insertRow = (): InsertRowResult => {
338
338
  const result = this._onInsertRow?.();
339
- if (result === false && this._draftRows.value.length === 0) {
339
+ if (result === 'draft' && this._draftRows.value.length === 0) {
340
340
  this.createDraftRow();
341
341
  }
342
+ return result ?? 'final';
342
343
  };
343
344
 
344
345
  private createDraftRow(): void {
@@ -393,14 +394,14 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
393
394
  return false;
394
395
  }
395
396
 
396
- const success = this._onInsertRow?.(draftRow.data);
397
+ const insertRowResult = this._onInsertRow?.(draftRow.data);
397
398
 
398
- if (success) {
399
+ if (insertRowResult === 'final') {
399
400
  const newDraftRows = this._draftRows.value.filter((_, index) => index !== draftRowIndex);
400
401
  this._draftRows.value = newDraftRows;
401
402
  }
402
403
 
403
- return success ?? false;
404
+ return insertRowResult === 'final';
404
405
  };
405
406
 
406
407
  public deleteRow = (rowIndex: number): void => {
@@ -460,7 +461,7 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
460
461
 
461
462
  const { props } = this._projection.getFieldProjection(field.id);
462
463
  switch (props.format) {
463
- case FormatEnum.Ref: {
464
+ case Format.TypeFormat.Ref: {
464
465
  if (!field.referencePath) {
465
466
  return ''; // TODO(burdon): Show error.
466
467
  }
@@ -469,7 +470,11 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
469
470
  }
470
471
 
471
472
  default: {
472
- return formatForEditing({ type: props.type, format: props.format, value });
473
+ return formatForEditing({
474
+ type: props.type,
475
+ format: props.format,
476
+ value,
477
+ });
473
478
  }
474
479
  }
475
480
  };
@@ -497,6 +502,7 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
497
502
  setValue(snapshot, field.path, transformedValue);
498
503
 
499
504
  const validationErrors = this.validateDraftRowData(snapshot);
505
+ // TODO(thure): These errors sometimes result in a useless message like “is missing” (what is missing?)
500
506
  if (validationErrors.length > 0) {
501
507
  const error = validationErrors.find((err) => err.path === field.path);
502
508
  if (error) {
@@ -512,7 +518,7 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
512
518
  const snapshot = getSnapshot(currentRow);
513
519
  setValue(snapshot, field.path, transformedValue);
514
520
 
515
- const schema = getSchema(currentRow);
521
+ const schema = Obj.getSchema(currentRow);
516
522
  invariant(schema);
517
523
 
518
524
  const validationResult = validateSchema(schema, snapshot);
@@ -538,7 +544,7 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
538
544
  const transformedValue = editorTextToCellValue(props, value);
539
545
 
540
546
  // Special handling for Ref format to preserve existing behavior
541
- if (props.format === FormatEnum.Ref && !isLiveObject(value)) {
547
+ if (props.format === Format.TypeFormat.Ref && !isLiveObject(value)) {
542
548
  return;
543
549
  }
544
550
 
@@ -653,7 +659,7 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
653
659
 
654
660
  const editorTextToCellValue = (props: PropertyType, value: any): any => {
655
661
  switch (props.format) {
656
- case FormatEnum.Ref: {
662
+ case Format.TypeFormat.Ref: {
657
663
  if (isLiveObject(value)) {
658
664
  return Ref.make(value);
659
665
  } else {
@@ -661,7 +667,7 @@ const editorTextToCellValue = (props: PropertyType, value: any): any => {
661
667
  }
662
668
  }
663
669
 
664
- case FormatEnum.SingleSelect: {
670
+ case Format.TypeFormat.SingleSelect: {
665
671
  const ids = extractTagIds(value);
666
672
  if (ids && ids.length > 0) {
667
673
  return ids[0];
@@ -670,7 +676,7 @@ const editorTextToCellValue = (props: PropertyType, value: any): any => {
670
676
  }
671
677
  }
672
678
 
673
- case FormatEnum.MultiSelect: {
679
+ case Format.TypeFormat.MultiSelect: {
674
680
  const ids = extractTagIds(value);
675
681
  return ids || value;
676
682
  }
@@ -2,18 +2,19 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { Schema } from 'effect';
6
- import { describe, expect, it, beforeEach } from 'vitest';
5
+ import * as Schema from 'effect/Schema';
6
+ import { beforeEach, describe, expect, it } from 'vitest';
7
7
 
8
- import { Obj } from '@dxos/echo';
9
- import { TypedObject } from '@dxos/echo-schema';
8
+ import { Filter, Query } from '@dxos/echo';
9
+ import { TypedObject } from '@dxos/echo/internal';
10
+ import { createEchoSchema } from '@dxos/echo/testing';
10
11
  import { live } from '@dxos/live-object';
11
- import { createEchoSchema } from '@dxos/live-object/testing';
12
- import { createView } from '@dxos/schema';
12
+ import { ProjectionModel, View } from '@dxos/schema';
13
+
14
+ import { Table } from '../types';
13
15
 
14
16
  import { TableModel, type TableModelProps } from './table-model';
15
17
  import { TablePresentation } from './table-presentation';
16
- import { TableView } from '../types';
17
18
 
18
19
  describe('TablePresentation', () => {
19
20
  describe('row reactivity', () => {
@@ -26,8 +27,8 @@ describe('TablePresentation', () => {
26
27
  updateCount = 0;
27
28
  ({ data } = live({
28
29
  data: [
29
- { col1: 'A', col2: 1, col3: true },
30
- { col1: 'B', col2: 2, col3: false },
30
+ { title: 'A', count: 1 },
31
+ { title: 'B', count: 2 },
31
32
  ],
32
33
  }));
33
34
 
@@ -44,8 +45,8 @@ describe('TablePresentation', () => {
44
45
  presentation.getCells({ start: { row: 0, col: 0 }, end: { row: 1, col: 2 } }, 'grid');
45
46
 
46
47
  // Trigger a row update.
47
- data[0].col1 = 'New Value';
48
- expect(updateCount).toBe(1);
48
+ data[0].title = 'New Value';
49
+ expect(updateCount).toBe(2);
49
50
 
50
51
  // Verify the new value through getCells.
51
52
  const cells = presentation.getCells({ start: { row: 0, col: 0 }, end: { row: 0, col: 0 } }, 'grid');
@@ -56,15 +57,14 @@ describe('TablePresentation', () => {
56
57
  // Set up visible range to include our test data.
57
58
  presentation.getCells({ start: { row: 0, col: 0 }, end: { row: 2, col: 2 } }, 'grid');
58
59
 
59
- data.push({ col1: 'C', col2: 3, col3: true });
60
- expect(updateCount).toBe(1);
60
+ data.push({ title: 'C', count: 3 });
61
+ expect(updateCount).toBe(2);
61
62
 
62
63
  const cells = presentation.getCells({ start: { row: 0, col: 0 }, end: { row: 2, col: 2 } }, 'grid');
63
64
 
64
65
  // Verify the new row's data.
65
66
  expect(cells['0,2']?.value).toBe('C');
66
67
  expect(cells['1,2']?.value).toBe('3');
67
- expect(cells['2,2']?.value).toBe('true');
68
68
  });
69
69
 
70
70
  it('should handle combined operations reactively', () => {
@@ -72,32 +72,34 @@ describe('TablePresentation', () => {
72
72
  presentation.getCells({ start: { row: 0, col: 0 }, end: { row: 2, col: 2 } }, 'grid');
73
73
 
74
74
  // Multiple operations.
75
- data[0].col1 = 'Updated A';
76
- data.push({ col1: 'C', col2: 3, col3: true });
75
+ data[0].title = 'Updated A';
76
+ data.push({ title: 'C', count: 3, completed: true });
77
77
  data.splice(1, 1);
78
78
 
79
79
  // We expect one update per operation affecting visible rows.
80
- expect(updateCount).toBe(3);
80
+ expect(updateCount).toBe(4);
81
81
 
82
82
  const cells = presentation.getCells({ start: { row: 0, col: 0 }, end: { row: 1, col: 2 } }, 'grid');
83
83
 
84
84
  // Verify final state.
85
85
  expect(cells['0,0']?.value).toBe('Updated A');
86
86
  expect(cells['0,1']?.value).toBe('C');
87
- expect(cells['1,1']?.value).toBe('3');
88
- expect(cells['2,1']?.value).toBe('true');
89
87
  });
90
88
  });
91
89
  });
92
90
 
93
91
  class Test extends TypedObject({ typename: 'example.com/type/Test', version: '0.1.0' })({
94
92
  title: Schema.String,
95
- completed: Schema.Boolean,
93
+ count: Schema.Number,
96
94
  }) {}
97
95
 
98
96
  const createTableModel = (props: Partial<TableModelProps> = {}): TableModel => {
99
97
  const schema = createEchoSchema(Test);
100
- const table = Obj.make(TableView, { sizes: {} });
101
- const view = createView({ typename: schema.typename, jsonSchema: schema.jsonSchema, presentation: table });
102
- return new TableModel({ view, schema: schema.jsonSchema, ...props });
98
+ const view = View.make({
99
+ query: Query.select(Filter.type(schema)),
100
+ jsonSchema: schema.jsonSchema,
101
+ });
102
+ const object = Table.make({ view });
103
+ const projection = new ProjectionModel(schema.jsonSchema, view.projection);
104
+ return new TableModel({ object, projection, ...props });
103
105
  };
@@ -3,10 +3,10 @@
3
3
  //
4
4
 
5
5
  import { signal } from '@preact/signals-core';
6
- import { isNotNullable } from 'effect/Predicate';
6
+ import * as Predicate from 'effect/Predicate';
7
7
 
8
8
  import { Obj } from '@dxos/echo';
9
- import { FormatEnum, getValue, TypeEnum } from '@dxos/echo-schema';
9
+ import { Format, TypeEnum, getValue } from '@dxos/echo/internal';
10
10
  import { cellClassesForFieldType, formatForDisplay } from '@dxos/react-ui-form';
11
11
  import {
12
12
  type DxGridCellValue,
@@ -16,12 +16,13 @@ import {
16
16
  toPlaneCellIndex,
17
17
  } from '@dxos/react-ui-grid';
18
18
  import { mx } from '@dxos/react-ui-theme';
19
- import { VIEW_FIELD_LIMIT, type FieldType } from '@dxos/schema';
19
+ import { type FieldType, VIEW_FIELD_LIMIT } from '@dxos/schema';
20
20
 
21
- import { type SelectionMode } from './selection-model';
22
- import { type TableRow, type TableModel } from './table-model';
23
21
  import { tableButtons, tableControls } from '../util';
24
22
 
23
+ import { type SelectionMode } from './selection-model';
24
+ import { type TableModel, type TableRow } from './table-model';
25
+
25
26
  /**
26
27
  * Presentation layer for a table component, handling cell rendering and grid display logic.
27
28
  * Manages rendering of table cells, headers, selection columns, and action columns across
@@ -67,6 +68,9 @@ export class TablePresentation<T extends TableRow = TableRow> {
67
68
  case 'frozenRowsEnd':
68
69
  cells = this.getDraftRowCells(range);
69
70
  break;
71
+ case 'fixedEndStart':
72
+ cells = this.getDraftIconCells(range);
73
+ break;
70
74
  case 'fixedEndEnd':
71
75
  cells = this.getDraftActionCells(range);
72
76
  break;
@@ -116,12 +120,12 @@ export class TablePresentation<T extends TableRow = TableRow> {
116
120
  }
117
121
 
118
122
  switch (props.format) {
119
- case FormatEnum.Boolean:
120
- case FormatEnum.SingleSelect:
121
- case FormatEnum.MultiSelect: {
123
+ case Format.TypeFormat.Boolean:
124
+ case Format.TypeFormat.SingleSelect:
125
+ case Format.TypeFormat.MultiSelect: {
122
126
  return '';
123
127
  }
124
- case FormatEnum.Ref: {
128
+ case Format.TypeFormat.Ref: {
125
129
  if (!field.referencePath) {
126
130
  return ''; // TODO(burdon): Show error.
127
131
  }
@@ -137,20 +141,22 @@ export class TablePresentation<T extends TableRow = TableRow> {
137
141
  };
138
142
 
139
143
  const classes = [];
140
- const formatClasses = cellClassesForFieldType({ type: props.type, format: props.format });
144
+ const formatClasses = cellClassesForFieldType(props);
145
+
141
146
  if (formatClasses) {
142
147
  classes.push(formatClasses);
143
148
  }
149
+
144
150
  const rowSelectionClasses = cellClassesForRowSelection(
145
151
  this.model.selection.isObjectSelected(obj),
146
152
  this.model.selection.selectionMode,
147
153
  );
154
+
148
155
  if (rowSelectionClasses) {
149
156
  classes.push(rowSelectionClasses);
150
157
  }
151
- if (classes.length > 0) {
152
- cell.className = mx(classes.flat());
153
- }
158
+
159
+ cell.className = mx(classes);
154
160
 
155
161
  // Arrays.
156
162
  if (props.type === TypeEnum.Array) {
@@ -184,7 +190,7 @@ export class TablePresentation<T extends TableRow = TableRow> {
184
190
 
185
191
  const tags = targetArray
186
192
  .map(getLabel)
187
- .filter(isNotNullable)
193
+ .filter(Predicate.isNotNullable)
188
194
  .map((title) => {
189
195
  return `<span class="dx-tag" data-hue="neutral">${title}</span>`;
190
196
  })
@@ -195,16 +201,16 @@ export class TablePresentation<T extends TableRow = TableRow> {
195
201
  }
196
202
 
197
203
  // References.
198
- if (props.format === FormatEnum.Ref && props.referenceSchema) {
204
+ if (props.format === Format.TypeFormat.Ref && props.referenceSchema) {
199
205
  const targetObj = getValue(obj, field.path)?.target;
200
206
  if (targetObj) {
201
207
  const dxn = Obj.getDXN(targetObj)?.toString();
202
- cell.accessoryHtml = `<dx-ref-tag refId=${dxn} class="dx-button is-6 pli-[3px] pbe-[2px] min-bs-0 absolute inline-end-1"><dx-icon icon="ph--link-simple--regular"/></dx-ref-tag>`;
208
+ cell.accessoryHtml = `<div role="none" class="absolute inline-end-0 inset-block-0 p-[--dx-grid-cell-content-padding-block]"><dx-anchor refId=${dxn} class="dx-button is-6 aspect-square min-bs-0" data-dx-grid-action="accessory"><dx-icon icon="ph--link-simple--regular"/></dx-anchor></div>`;
203
209
  }
204
210
  }
205
211
 
206
212
  // Booleans.
207
- if (props.format === FormatEnum.Boolean) {
213
+ if (props.format === Format.TypeFormat.Boolean) {
208
214
  const value = getValue(obj, field.path);
209
215
  cell.accessoryHtml = tableControls.switch.render({
210
216
  colIndex,
@@ -215,7 +221,7 @@ export class TablePresentation<T extends TableRow = TableRow> {
215
221
  }
216
222
 
217
223
  // Single-Selects.
218
- if (props.format === FormatEnum.SingleSelect) {
224
+ if (props.format === Format.TypeFormat.SingleSelect) {
219
225
  const value = getValue(obj, field.path);
220
226
  const options = this.model.projection.getFieldProjection(field.id).props.options;
221
227
  if (options) {
@@ -227,7 +233,7 @@ export class TablePresentation<T extends TableRow = TableRow> {
227
233
  }
228
234
 
229
235
  // Multi-Selects.
230
- if (props.format === FormatEnum.MultiSelect) {
236
+ if (props.format === Format.TypeFormat.MultiSelect) {
231
237
  const values = getValue(obj, field.path) as string[] | undefined;
232
238
  const options = this.model.projection.getFieldProjection(field.id).props.options;
233
239
  if (options && values && values.length > 0) {
@@ -275,23 +281,34 @@ export class TablePresentation<T extends TableRow = TableRow> {
275
281
  const fields = this.model.projection?.fields ?? [];
276
282
  const draftRows = this.model.draftRows.value;
277
283
 
278
- for (let row = range.start.row; row <= range.end.row && row < draftRows.length; row++) {
279
- const draftRow = draftRows[row];
284
+ // Return cells of the CTA row if no draft row is active
285
+ if (draftRows.length === 0) {
280
286
  for (let col = range.start.col; col <= range.end.col && col < fields.length; col++) {
281
- const field = fields[col];
282
- if (!field) {
283
- continue;
284
- }
287
+ cells[toPlaneCellIndex({ col, row: 0 })] = {
288
+ value: '',
289
+ readonly: true,
290
+ className: 'dx-grid__row--cta__cell',
291
+ };
292
+ }
293
+ } else {
294
+ for (let row = range.start.row; row <= range.end.row && row < draftRows.length; row++) {
295
+ const draftRow = draftRows[row];
296
+ for (let col = range.start.col; col <= range.end.col && col < fields.length; col++) {
297
+ const cellIndex = toPlaneCellIndex({ col, row });
298
+ const field = fields[col];
299
+ if (!field) {
300
+ continue;
301
+ }
285
302
 
286
- this.createDataCell(cells, draftRow.data, field, col, row);
303
+ this.createDataCell(cells, draftRow.data, field, col, row);
287
304
 
288
- if (this.model.hasDraftRowValidationError(row, field.path)) {
289
- const cellIndex = toPlaneCellIndex({ col, row });
290
- const cellValue = cells[cellIndex];
291
- if (cellValue) {
292
- const existingClasses = cellValue.className || '';
293
- const draftClasses = 'dx-grid__cell--flagged';
294
- cellValue.className = existingClasses ? `${existingClasses} ${draftClasses}` : draftClasses;
305
+ if (this.model.hasDraftRowValidationError(row, field.path)) {
306
+ const cellValue = cells[cellIndex];
307
+ if (cellValue) {
308
+ const existingClasses = cellValue.className || '';
309
+ const draftClasses = 'dx-grid__cell--flagged';
310
+ cellValue.className = existingClasses ? `${existingClasses} ${draftClasses}` : draftClasses;
311
+ }
295
312
  }
296
313
  }
297
314
  }
@@ -310,14 +327,14 @@ export class TablePresentation<T extends TableRow = TableRow> {
310
327
 
311
328
  cells[toPlaneCellIndex({ col, row: 0 })] = {
312
329
  // TODO(burdon): Use same logic as form for fallback title.
313
- value: props.title ?? field.path,
314
- readonly: true,
330
+ value: '',
315
331
  resizeHandle: 'col',
316
332
  accessoryHtml: `
333
+ <span class="grow min-is-0 truncate">${props.title ?? field.path}</span>
317
334
  ${direction !== undefined ? tableButtons.sort.render({ fieldId: field.id, direction }) : ''}
318
335
  ${tableButtons.columnSettings.render({ fieldId: field.id })}
319
336
  `,
320
- className: '!bg-toolbarSurface',
337
+ className: '!bg-toolbarSurface !text-description [&>div]:flex [&>div]:items-stretch',
321
338
  };
322
339
  }
323
340
 
@@ -400,14 +417,39 @@ export class TablePresentation<T extends TableRow = TableRow> {
400
417
  const cells: DxGridPlaneCells = {};
401
418
  const draftRows = this.model.draftRows.value;
402
419
 
403
- for (let row = range.start.row; row <= range.end.row && row < draftRows.length; row++) {
404
- const draftRow = draftRows[row];
405
- const disabled = !draftRow.valid;
420
+ // Return cells of the CTA row if no draft row is active
421
+ if (draftRows.length === 0) {
422
+ cells[toPlaneCellIndex({ col: 0, row: 0 })] = {
423
+ value: '',
424
+ className: 'dx-grid__row--cta__cell',
425
+ readonly: true,
426
+ };
427
+ } else {
428
+ for (let row = range.start.row; row <= range.end.row && row < draftRows.length; row++) {
429
+ const draftRow = draftRows[row];
430
+ const disabled = !draftRow.valid;
431
+
432
+ cells[toPlaneCellIndex({ col: 0, row })] = {
433
+ value: '',
434
+ readonly: true,
435
+ accessoryHtml: tableButtons.saveDraftRow.render({ rowIndex: row, disabled }),
436
+ };
437
+ }
438
+ }
439
+
440
+ return cells;
441
+ }
442
+
443
+ private getDraftIconCells(range: DxGridPlaneRange): DxGridPlaneCells {
444
+ const cells: DxGridPlaneCells = {};
445
+ const draftRows = this.model.draftRows.value;
406
446
 
447
+ for (let row = range.start.row; row <= range.end.row; row++) {
407
448
  cells[toPlaneCellIndex({ col: 0, row })] = {
408
449
  value: '',
409
450
  readonly: true,
410
- accessoryHtml: tableButtons.saveDraftRow.render({ rowIndex: row, disabled }),
451
+ accessoryHtml: '<dx-icon icon="ph--plus--regular" class="contents"></dx-icon>',
452
+ className: mx('[&>div]:grid [&>div]:place-content-center', draftRows.length < 1 && 'dx-grid__row--cta__cell'),
411
453
  };
412
454
  }
413
455
 
@@ -426,7 +468,6 @@ export const cellClassesForRowSelection = (selected: boolean, selectionMode: Sel
426
468
 
427
469
  switch (selectionMode) {
428
470
  case 'single':
429
- // TODO(ZaymonFC): @thure, do we need a grid version of 'currentRelated'?
430
471
  return ['!bg-currentRelated dx-grid__cell--no-focus-unfurl hover:bg-hoverSurface !cursor-pointer'];
431
472
  case 'multiple':
432
473
  return ['!bg-gridCellSelected'];