@dxos/react-ui-table 0.7.5-main.9d26e3a → 0.7.5-main.9d2a38b

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 (67) hide show
  1. package/dist/lib/browser/{chunk-AT7O4RAD.mjs → chunk-T7CTRLHA.mjs} +2 -2
  2. package/dist/lib/browser/{chunk-AT7O4RAD.mjs.map → chunk-T7CTRLHA.mjs.map} +1 -1
  3. package/dist/lib/browser/index.mjs +425 -280
  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-YSBXNBDJ.cjs → chunk-XQRKYO7Q.cjs} +6 -6
  8. package/dist/lib/node/chunk-XQRKYO7Q.cjs.map +7 -0
  9. package/dist/lib/node/index.cjs +417 -266
  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-6OH2NHUX.mjs → chunk-6HE6MLBK.mjs} +2 -2
  15. package/dist/lib/node-esm/{chunk-6OH2NHUX.mjs.map → chunk-6HE6MLBK.mjs.map} +1 -1
  16. package/dist/lib/node-esm/index.mjs +425 -280
  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/components/Table/Table.d.ts.map +1 -1
  21. package/dist/types/src/components/Table/Table.stories.d.ts.map +1 -1
  22. package/dist/types/src/components/TableToolbar/TableToolbar.d.ts +18 -0
  23. package/dist/types/src/components/TableToolbar/TableToolbar.d.ts.map +1 -0
  24. package/dist/types/src/components/TableToolbar/index.d.ts +2 -0
  25. package/dist/types/src/components/TableToolbar/index.d.ts.map +1 -0
  26. package/dist/types/src/components/index.d.ts +1 -1
  27. package/dist/types/src/components/index.d.ts.map +1 -1
  28. package/dist/types/src/components/testing.d.ts +1 -1
  29. package/dist/types/src/model/table-model.d.ts +11 -19
  30. package/dist/types/src/model/table-model.d.ts.map +1 -1
  31. package/dist/types/src/model/table-presentation.d.ts.map +1 -1
  32. package/dist/types/src/model/table-sorting.d.ts +53 -0
  33. package/dist/types/src/model/table-sorting.d.ts.map +1 -0
  34. package/dist/types/src/translations.d.ts +2 -1
  35. package/dist/types/src/translations.d.ts.map +1 -1
  36. package/dist/types/src/types/table.d.ts +3 -3
  37. package/dist/types/src/types/table.d.ts.map +1 -1
  38. package/dist/types/src/util/table-buttons.d.ts +15 -1
  39. package/dist/types/src/util/table-buttons.d.ts.map +1 -1
  40. package/dist/types/src/util/table.d.ts +2 -1
  41. package/dist/types/src/util/table.d.ts.map +1 -1
  42. package/dist/types/tsconfig.tsbuildinfo +1 -1
  43. package/package.json +30 -29
  44. package/src/components/Table/ColumnActionsMenu.tsx +4 -4
  45. package/src/components/Table/Table.stories.tsx +33 -21
  46. package/src/components/Table/Table.tsx +4 -0
  47. package/src/components/TableToolbar/TableToolbar.tsx +72 -0
  48. package/src/components/{Toolbar → TableToolbar}/index.ts +1 -1
  49. package/src/components/index.ts +1 -1
  50. package/src/model/table-model.test.ts +0 -5
  51. package/src/model/table-model.ts +40 -98
  52. package/src/model/table-presentation.ts +12 -3
  53. package/src/model/table-sorting.ts +201 -0
  54. package/src/playwright/smoke.spec.ts +18 -11
  55. package/src/translations.ts +2 -1
  56. package/src/types/table.ts +1 -1
  57. package/src/util/table-buttons.ts +33 -3
  58. package/src/util/table.ts +78 -52
  59. package/dist/lib/node/chunk-YSBXNBDJ.cjs.map +0 -7
  60. package/dist/types/src/components/Toolbar/Toolbar.d.ts +0 -21
  61. package/dist/types/src/components/Toolbar/Toolbar.d.ts.map +0 -1
  62. package/dist/types/src/components/Toolbar/common.d.ts +0 -14
  63. package/dist/types/src/components/Toolbar/common.d.ts.map +0 -1
  64. package/dist/types/src/components/Toolbar/index.d.ts +0 -2
  65. package/dist/types/src/components/Toolbar/index.d.ts.map +0 -1
  66. package/src/components/Toolbar/Toolbar.tsx +0 -87
  67. package/src/components/Toolbar/common.tsx +0 -71
@@ -3,14 +3,13 @@
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';
11
10
  import { isReactiveObject, makeRef } from '@dxos/live-object';
12
11
  import { PublicKey } from '@dxos/react-client';
13
- import { formatForDisplay, formatForEditing, parseValue } from '@dxos/react-ui-form';
12
+ import { formatForEditing, parseValue } from '@dxos/react-ui-form';
14
13
  import {
15
14
  type DxGridAxisMeta,
16
15
  type DxGridPlaneRange,
@@ -20,18 +19,16 @@ import {
20
19
  import { type ViewProjection } from '@dxos/schema';
21
20
 
22
21
  import { SelectionModel } from './selection-model';
22
+ import { TableSorting } from './table-sorting';
23
23
  import { type TableType } from '../types';
24
24
  import { touch } from '../util';
25
25
 
26
- export type SortDirection = 'asc' | 'desc';
27
- export type SortConfig = { fieldId: string; direction: SortDirection };
28
-
29
26
  export type BaseTableRow = Record<JsonProp, any> & { id: string };
30
27
 
31
28
  export type TableModelProps<T extends BaseTableRow = { id: string }> = {
32
29
  table: TableType;
33
30
  projection: ViewProjection;
34
- sorting?: SortConfig[];
31
+ sorting?: FieldSortType[];
35
32
  pinnedRows?: { top: number[]; bottom: number[] };
36
33
  onInsertRow?: (index?: number) => void;
37
34
  onDeleteRows?: (index: number, obj: T[]) => void;
@@ -51,25 +48,16 @@ export class TableModel<T extends BaseTableRow = { id: string }> extends Resourc
51
48
  end: { row: 0, col: 0 },
52
49
  });
53
50
 
54
- /**
55
- * Maps display indices to data indices.
56
- * Used for translating between sorted/displayed order and original data order.
57
- * Keys are display indices, values are corresponding data indices.
58
- */
59
- private readonly _displayToDataIndex: Map<number, number> = new Map();
60
-
61
51
  private readonly _onInsertRow?: TableModelProps<T>['onInsertRow'];
62
52
  private readonly _onDeleteRows?: TableModelProps<T>['onDeleteRows'];
63
53
  private readonly _onDeleteColumn?: TableModelProps<T>['onDeleteColumn'];
64
54
  private readonly _onCellUpdate?: TableModelProps<T>['onCellUpdate'];
65
55
  private readonly _onRowOrderChanged?: TableModelProps<T>['onRowOrderChanged'];
66
56
 
67
- private readonly _sorting = signal<SortConfig | undefined>(undefined);
68
- private _sortedRows!: ReadonlySignal<T[]>;
69
- private _rows = signal<T[]>([]);
57
+ private readonly _rows = signal<T[]>([]);
58
+ private readonly _sorting: TableSorting<T>;
70
59
 
71
60
  private _pinnedRows: NonNullable<TableModelProps<T>['pinnedRows']>;
72
-
73
61
  private _selection!: SelectionModel<T>;
74
62
  private _columnMeta?: ReadonlySignal<DxGridAxisMeta>;
75
63
 
@@ -87,8 +75,13 @@ export class TableModel<T extends BaseTableRow = { id: string }> extends Resourc
87
75
  super();
88
76
  this._table = table;
89
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
+ }
90
84
 
91
- this._sorting.value = sorting.at(0);
92
85
  this._pinnedRows = pinnedRows;
93
86
  this._onInsertRow = onInsertRow;
94
87
  this._onDeleteRows = onDeleteRows;
@@ -106,7 +99,14 @@ export class TableModel<T extends BaseTableRow = { id: string }> extends Resourc
106
99
  }
107
100
 
108
101
  public get rows(): ReadonlySignal<T[]> {
109
- return this._sortedRows;
102
+ return this._sorting.sortedRows;
103
+ }
104
+
105
+ /**
106
+ * @reactive
107
+ */
108
+ public get sorting(): TableSorting<T> | undefined {
109
+ return this._sorting;
110
110
  }
111
111
 
112
112
  public get pinnedRows(): NonNullable<TableModelProps<T>['pinnedRows']> {
@@ -118,23 +118,23 @@ export class TableModel<T extends BaseTableRow = { id: string }> extends Resourc
118
118
  return this._columnMeta;
119
119
  }
120
120
 
121
- public get sorting(): SortConfig | undefined {
122
- return this._sorting.value;
123
- }
124
-
125
121
  public get selection() {
126
122
  return this._selection;
127
123
  }
128
124
 
125
+ /** @reactive */
126
+ public get isViewDirty(): boolean {
127
+ return this._sorting.isDirty;
128
+ }
129
+
129
130
  //
130
131
  // Initialisation
131
132
  //
132
133
 
133
134
  protected override async _open() {
134
135
  this.initializeColumnMeta();
135
- this.initializeSorting();
136
136
  this.initializeEffects();
137
- this._selection = new SelectionModel(this._sortedRows, () => this._onRowOrderChanged?.());
137
+ this._selection = new SelectionModel(this._sorting.sortedRows, () => this._onRowOrderChanged?.());
138
138
  await this._selection.open(this._ctx);
139
139
  }
140
140
 
@@ -153,59 +153,9 @@ export class TableModel<T extends BaseTableRow = { id: string }> extends Resourc
153
153
  });
154
154
  }
155
155
 
156
- private initializeSorting(): void {
157
- this._sortedRows = computed(() => {
158
- this._displayToDataIndex.clear();
159
- const sort = this._sorting.value;
160
- if (!sort) {
161
- return this._rows.value;
162
- }
163
-
164
- const field = this._table.view?.target?.fields.find((field) => field.id === sort.fieldId);
165
- if (!field) {
166
- return this._rows.value;
167
- }
168
-
169
- const { props } = this._projection.getFieldProjection(field.id);
170
-
171
- const dataWithIndices = this._rows.value.map((item, index) => {
172
- const rawValue = getValue(item, field.path);
173
- let sortValue = rawValue;
174
-
175
- if (props.type.startsWith('boolean')) {
176
- sortValue = !!rawValue; // Coerce null/undefined/false to false for booleans
177
- } else if (rawValue != null) {
178
- if (props.format === FormatEnum.Ref && field.referencePath) {
179
- sortValue = getValue(rawValue, field.referencePath);
180
- } else {
181
- sortValue = formatForDisplay({ type: props.type, format: props.format, value: rawValue });
182
- }
183
- }
184
-
185
- return {
186
- item,
187
- index,
188
- value: sortValue,
189
- isEmpty: !props.type.startsWith('boolean') && (sortValue == null || sortValue === ''),
190
- };
191
- });
192
-
193
- const sorted = orderBy(dataWithIndices, ['isEmpty', 'value'], ['asc', sort.direction]);
194
-
195
- for (let displayIndex = 0; displayIndex < sorted.length; displayIndex++) {
196
- const { index: dataIndex } = sorted[displayIndex];
197
- if (displayIndex !== dataIndex) {
198
- this._displayToDataIndex.set(displayIndex, dataIndex);
199
- }
200
- }
201
-
202
- return sorted.map(({ item }) => item);
203
- });
204
- }
205
-
206
156
  private initializeEffects(): void {
207
157
  const rowOrderWatcher = effect(() => {
208
- touch(this._sortedRows.value);
158
+ touch(this._sorting.sortedRows.value);
209
159
  this._onRowOrderChanged?.();
210
160
  });
211
161
  this._ctx.onDispose(rowOrderWatcher);
@@ -221,7 +171,7 @@ export class TableModel<T extends BaseTableRow = { id: string }> extends Resourc
221
171
  for (let row = start.row; row <= end.row; row++) {
222
172
  rowEffects.push(
223
173
  effect(() => {
224
- const obj = this._sortedRows.value[row];
174
+ const obj = this._sorting.sortedRows.value[row];
225
175
  this?._table?.view?.target?.fields.forEach((field) => touch(getValue(obj, field.path)));
226
176
  this._onCellUpdate?.({ row, col: start.col, plane: 'grid' });
227
177
  }),
@@ -246,12 +196,12 @@ export class TableModel<T extends BaseTableRow = { id: string }> extends Resourc
246
196
  public getColumnCount = (): number => this.table.view?.target?.fields.length ?? 0;
247
197
 
248
198
  public insertRow = (rowIndex?: number): void => {
249
- 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;
250
200
  this._onInsertRow?.(row);
251
201
  };
252
202
 
253
203
  public deleteRow = (rowIndex: number): void => {
254
- const row = this._displayToDataIndex.get(rowIndex) ?? rowIndex;
204
+ const row = this._sorting.getDataIndex(rowIndex);
255
205
  const obj = this._rows.value[row];
256
206
  const objectsToDelete = [];
257
207
 
@@ -272,7 +222,7 @@ export class TableModel<T extends BaseTableRow = { id: string }> extends Resourc
272
222
  }
273
223
 
274
224
  const field = fields[col];
275
- const dataIndex = this._displayToDataIndex.get(row) ?? row;
225
+ const dataIndex = this._sorting.getDataIndex(row);
276
226
  const value = getValue(this._rows.value[dataIndex], field.path);
277
227
  if (value == null) {
278
228
  return '';
@@ -295,7 +245,7 @@ export class TableModel<T extends BaseTableRow = { id: string }> extends Resourc
295
245
  };
296
246
 
297
247
  public setCellData = ({ col, row }: DxGridPlanePosition, value: any): void => {
298
- const rowIdx = this._displayToDataIndex.get(row) ?? row;
248
+ const rowIdx = this._sorting.getDataIndex(row);
299
249
  const fields = this.table.view?.target?.fields ?? [];
300
250
  if (col < 0 || col >= fields.length) {
301
251
  return;
@@ -334,7 +284,7 @@ export class TableModel<T extends BaseTableRow = { id: string }> extends Resourc
334
284
  * @param {(value: any) => any} update - A function that takes the current value and returns the updated value.
335
285
  */
336
286
  public updateCellData({ col, row }: DxGridPlanePosition, update: (value: any) => any): void {
337
- const dataRow = this._displayToDataIndex.get(row) ?? row;
287
+ const dataRow = this._sorting.getDataIndex(row);
338
288
  const fields = this.table.view?.target?.fields ?? [];
339
289
  const field = fields[col];
340
290
 
@@ -354,9 +304,6 @@ export class TableModel<T extends BaseTableRow = { id: string }> extends Resourc
354
304
 
355
305
  const field = this.table.view.target?.fields.find((field) => field.id === fieldId);
356
306
  if (field && this._onDeleteColumn) {
357
- if (this._sorting.value?.fieldId === fieldId) {
358
- this.clearSort();
359
- }
360
307
  this._onDeleteColumn(field.id);
361
308
  }
362
309
  }
@@ -376,18 +323,6 @@ export class TableModel<T extends BaseTableRow = { id: string }> extends Resourc
376
323
  }
377
324
  }
378
325
 
379
- //
380
- // Sorting
381
- //
382
-
383
- public setSort(fieldId: string, direction: SortDirection): void {
384
- this._sorting.value = { fieldId, direction };
385
- }
386
-
387
- public clearSort(): void {
388
- this._sorting.value = undefined;
389
- }
390
-
391
326
  //
392
327
  // Pinning
393
328
  //
@@ -401,4 +336,11 @@ export class TableModel<T extends BaseTableRow = { id: string }> extends Resourc
401
336
  this._pinnedRows.top = this._pinnedRows.top.filter((index: number) => index !== rowIndex);
402
337
  this._pinnedRows.bottom = this._pinnedRows.bottom.filter((index: number) => index !== rowIndex);
403
338
  }
339
+
340
+ //
341
+ // View operations
342
+ //
343
+ public saveView(): void {
344
+ this._sorting.save();
345
+ }
404
346
  }
@@ -142,12 +142,19 @@ export class TablePresentation<T extends BaseTableRow = { id: string }> {
142
142
  const fields = this.model.table.view?.target?.fields ?? [];
143
143
  for (let col = range.start.col; col <= range.end.col && col < fields.length; col++) {
144
144
  const { field, props } = this.model.projection.getFieldProjection(fields[col].id);
145
+ const sorting = this.model.sorting?.sorting;
146
+ const direction = sorting?.fieldId === field.id ? sorting.direction : undefined;
147
+
145
148
  cells[toPlaneCellIndex({ col, row: 0 })] = {
146
149
  // TODO(burdon): Use same logic as form for fallback title.
147
150
  value: props.title ?? field.path,
148
151
  readonly: true,
149
152
  resizeHandle: 'col',
150
- accessoryHtml: tableButtons.columnSettings.render({ fieldId: field.id }),
153
+ accessoryHtml: `
154
+ ${direction !== undefined ? tableButtons.sort.render({ fieldId: field.id, direction }) : ''}
155
+ ${tableButtons.columnSettings.render({ fieldId: field.id })}
156
+ `,
157
+ className: '!bg-gridHeader',
151
158
  };
152
159
  }
153
160
 
@@ -189,13 +196,14 @@ export class TablePresentation<T extends BaseTableRow = { id: string }> {
189
196
  private getSelectAllCell(): DxGridPlaneCells {
190
197
  return {
191
198
  [toPlaneCellIndex({ col: 0, row: 0 })]: {
192
- value: '',
193
199
  accessoryHtml: tableControls.checkbox.render({
194
200
  rowIndex: 0,
195
201
  header: true,
196
202
  checked: this.model.selection.allRowsSeleted.value,
197
203
  }),
204
+ className: '!bg-gridHeader',
198
205
  readonly: true,
206
+ value: '',
199
207
  },
200
208
  };
201
209
  }
@@ -203,11 +211,12 @@ export class TablePresentation<T extends BaseTableRow = { id: string }> {
203
211
  private getNewColumnCell(): DxGridPlaneCells {
204
212
  return {
205
213
  [toPlaneCellIndex({ col: 0, row: 0 })]: {
206
- value: '',
207
214
  accessoryHtml: tableButtons.addColumn.render({
208
215
  disabled: (this.model.table.view?.target?.fields?.length ?? 0) >= VIEW_FIELD_LIMIT,
209
216
  }),
217
+ className: '!bg-gridHeader',
210
218
  readonly: true,
219
+ value: '',
211
220
  },
212
221
  };
213
222
  }
@@ -0,0 +1,201 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { computed, signal, type Signal, type ReadonlySignal } from '@preact/signals-core';
6
+ import orderBy from 'lodash.orderby';
7
+
8
+ import { getValue, FormatEnum, TypeEnum, type SortDirectionType, type FieldSortType } from '@dxos/echo-schema';
9
+ import { formatForDisplay } from '@dxos/react-ui-form';
10
+ import type { PropertyType, FieldType, ViewProjection, ViewType } from '@dxos/schema';
11
+
12
+ import { type BaseTableRow } from './table-model';
13
+
14
+ /**
15
+ * Represents the local sort state.
16
+ * - undefined: No local sort set, fallback to view sort.
17
+ * - cleared: Sort explicitly cleared by user.
18
+ * - active: Active local sort with sort configuration.
19
+ */
20
+ type LocalSort = { type: 'cleared' } | { type: 'active'; sort: FieldSortType } | undefined;
21
+
22
+ /**
23
+ * Manages table sorting functionality through display and data index mapping.
24
+ *
25
+ * When a table is sorted, rows visually reorder but we need to maintain references
26
+ * to the original data positions. This is handled through two types of indices:
27
+ *
28
+ * - Display indices: The visual positions after sorting
29
+ * - Data indices: The original positions in the source data
30
+ *
31
+ * The _displayToDataIndex map maintains these relationships, enabling lookups
32
+ * between visual and source positions.
33
+ *
34
+ * Example mapping between display and data indices:
35
+ *
36
+ * Original Data: Sorted Display: Mapping: (Display → Data)
37
+ * ┌──────────────┐ ┌──────────────┐ ┌────────┐
38
+ * │ 0: "Banana" │ => │ 0: "Apple" │ => │ 0 => 1 │
39
+ * │ 1: "Apple" │ => │ 1: "Banana" │ => │ 1 => 0 │
40
+ * │ 2: "Cherry" │ => │ 2: "Cherry" │ => │ 2 => 2 │
41
+ * └──────────────┘ └──────────────┘ └────────┘
42
+ */
43
+ export class TableSorting<T extends BaseTableRow> {
44
+ private readonly _displayToDataIndex = new Map<number, number>();
45
+ private readonly _rows: Signal<T[]>;
46
+ private readonly _localSort = signal<LocalSort>(undefined);
47
+ private readonly _isDirty: ReadonlySignal<boolean>;
48
+ public readonly sortedRows: ReadonlySignal<T[]>;
49
+
50
+ constructor(
51
+ rows: Signal<T[]>,
52
+ private readonly _view: ViewType | undefined,
53
+ private readonly _projection: ViewProjection,
54
+ ) {
55
+ this._rows = rows;
56
+ this._isDirty = computed(() => {
57
+ const local = this._localSort.value;
58
+ const viewSort = this._view?.query.sort?.[0];
59
+ if (local?.type === 'cleared') {
60
+ return viewSort !== undefined;
61
+ }
62
+ if (local?.type === 'active') {
63
+ if (!viewSort) {
64
+ return true;
65
+ }
66
+ return local.sort.fieldId !== viewSort.fieldId || local.sort.direction !== viewSort.direction;
67
+ }
68
+ return false;
69
+ });
70
+ this.sortedRows = this.initialiseSortedRows();
71
+ }
72
+
73
+ /**
74
+ * @reactive
75
+ * @returns Local sort if present, falls back to view sort.
76
+ */
77
+ public get sorting(): FieldSortType | undefined {
78
+ const local = this._localSort.value;
79
+ if (local?.type === 'cleared') {
80
+ return undefined;
81
+ }
82
+ if (local?.type === 'active') {
83
+ return local.sort;
84
+ }
85
+ return this._view?.query.sort?.[0];
86
+ }
87
+
88
+ /**
89
+ * @reactive
90
+ * @returns Whether local sort differs from view sort.
91
+ */
92
+ public get isDirty(): boolean {
93
+ return this._isDirty.value;
94
+ }
95
+
96
+ public getDataIndex(displayIndex: number): number {
97
+ return this._displayToDataIndex.get(displayIndex) ?? displayIndex;
98
+ }
99
+
100
+ public setSort(fieldId: string, direction: SortDirectionType): void {
101
+ this._localSort.value = {
102
+ type: 'active',
103
+ sort: { fieldId, direction },
104
+ };
105
+ }
106
+
107
+ public toggleSort(fieldId: string): void {
108
+ if (!this._view || !this.sorting || this.sorting.fieldId !== fieldId) {
109
+ return;
110
+ }
111
+
112
+ switch (this.sorting.direction) {
113
+ case 'asc': {
114
+ this.setSort(fieldId, 'desc');
115
+ break;
116
+ }
117
+ case 'desc': {
118
+ this.setSort(fieldId, 'asc');
119
+ break;
120
+ }
121
+ }
122
+ }
123
+
124
+ public clearSort(): void {
125
+ this._localSort.value = { type: 'cleared' };
126
+ }
127
+
128
+ public save(): void {
129
+ if (this._view && this._localSort.value !== undefined) {
130
+ if (this._localSort.value.type === 'active') {
131
+ this._view.query.sort = [this._localSort.value.sort];
132
+ } else {
133
+ this._view.query.sort = [];
134
+ }
135
+ this._localSort.value = undefined;
136
+ }
137
+ }
138
+
139
+ //
140
+ // Initialisation.
141
+ //
142
+
143
+ private initialiseSortedRows(): ReadonlySignal<T[]> {
144
+ return computed(() => {
145
+ this._displayToDataIndex.clear();
146
+ const sort = this.sorting;
147
+ if (!sort || !this._view) {
148
+ return this._rows.value;
149
+ }
150
+
151
+ const field = this._view.fields.find((f) => f.id === sort.fieldId);
152
+ if (!field) {
153
+ return this._rows.value;
154
+ }
155
+
156
+ const { props } = this._projection.getFieldProjection(field.id);
157
+
158
+ const dataWithIndices = this._rows.value.map((item, index) => {
159
+ const sortValue = this.getSortValue(props, field, item);
160
+ const isEmpty = !(props.type === TypeEnum.Boolean) && (sortValue == null || sortValue === '');
161
+
162
+ return { item, index, value: sortValue, isEmpty };
163
+ });
164
+
165
+ const ordered = orderBy(dataWithIndices, ['isEmpty', 'value'], ['asc', sort.direction]);
166
+
167
+ ordered.forEach(({ index: dataIndex }, displayIndex) => {
168
+ if (displayIndex !== dataIndex) {
169
+ this._displayToDataIndex.set(displayIndex, dataIndex);
170
+ }
171
+ });
172
+
173
+ return ordered.map(({ item }) => item);
174
+ });
175
+ }
176
+
177
+ private getSortValue(props: PropertyType, field: FieldType, item: T) {
178
+ const { type, format } = props;
179
+ const value = getValue(item, field.path);
180
+
181
+ if (type === TypeEnum.Boolean) {
182
+ return !!value; // Coerce null/undefined to false for booleans.
183
+ }
184
+ if (type === TypeEnum.Number) {
185
+ return value;
186
+ }
187
+ if (value == null) {
188
+ return value;
189
+ }
190
+
191
+ // TODO(ZaymonFC): Should we format this returned value for display based on the referenced prop format?
192
+ // Right now we're going to be sorting based on the raw values.
193
+ // Maybe we need to recurse this function with the referenced prop format and field?
194
+ if (format === FormatEnum.Ref && field.referencePath) {
195
+ const refValue = getValue(value.target, field.referencePath);
196
+ return refValue;
197
+ }
198
+
199
+ return formatForDisplay({ type, format, value });
200
+ }
201
+ }
@@ -13,17 +13,17 @@ const storyUrl = `http://localhost:9009/iframe.html?id=${storyId}&viewMode=story
13
13
 
14
14
  // NOTE(ZaymonFC): This test suite relies on the faker seed being set to 0 in the story.
15
15
  test.describe('Table', () => {
16
- test('Loads', async ({ browser }) => {
16
+ test('Loads', async ({ browser, browserName }) => {
17
+ test.skip(browserName === 'webkit');
17
18
  const { page } = await setupPage(browser, { url: storyUrl });
18
19
  const table = new TableManager(page);
19
20
 
20
- // (name, email, salary, manager) * 10 rows.
21
21
  await table.grid.ready();
22
- await expect(table.grid.cellsWithinPlane('grid')).toHaveCount(40);
23
22
  await page.close();
24
23
  });
25
24
 
26
- test('sort', async ({ browser }) => {
25
+ test('sort', async ({ browser, browserName }) => {
26
+ test.skip(browserName === 'webkit');
27
27
  const { page } = await setupPage(browser, { url: storyUrl });
28
28
  const table = new TableManager(page);
29
29
 
@@ -38,7 +38,8 @@ test.describe('Table', () => {
38
38
  await page.close();
39
39
  });
40
40
 
41
- test('selection', async ({ browser }) => {
41
+ test('selection', async ({ browser, browserName }) => {
42
+ test.skip(browserName === 'webkit');
42
43
  const { page } = await setupPage(browser, { url: storyUrl });
43
44
  const table = new TableManager(page);
44
45
 
@@ -58,7 +59,8 @@ test.describe('Table', () => {
58
59
  await page.close();
59
60
  });
60
61
 
61
- test('delete row', async ({ browser }) => {
62
+ test('delete row', async ({ browser, browserName }) => {
63
+ test.skip(browserName === 'webkit');
62
64
  const { page } = await setupPage(browser, { url: storyUrl });
63
65
  const table = new TableManager(page);
64
66
 
@@ -68,7 +70,8 @@ test.describe('Table', () => {
68
70
  await page.close();
69
71
  });
70
72
 
71
- test('delete row--select all', async ({ browser }) => {
73
+ test('delete row--select all', async ({ browser, browserName }) => {
74
+ test.skip(browserName === 'webkit');
72
75
  const { page } = await setupPage(browser, { url: storyUrl });
73
76
  const table = new TableManager(page);
74
77
 
@@ -84,7 +87,8 @@ test.describe('Table', () => {
84
87
  await page.close();
85
88
  });
86
89
 
87
- test('delete column', async ({ browser }) => {
90
+ test('delete column', async ({ browser, browserName }) => {
91
+ test.skip(browserName === 'webkit');
88
92
  const { page } = await setupPage(browser, { url: storyUrl });
89
93
  const table = new TableManager(page);
90
94
 
@@ -110,7 +114,8 @@ test.describe('Table', () => {
110
114
 
111
115
  // Rest of add column test remains the same as it's a more complex flow.
112
116
  // TODO(ZaymonFC): Restore this after fixing format selection.
113
- test('add column', async ({ browser }) => {
117
+ test('add column', async ({ browser, browserName }) => {
118
+ test.skip(browserName === 'webkit');
114
119
  const { page } = await setupPage(browser, { url: storyUrl });
115
120
  const table = new TableManager(page);
116
121
 
@@ -123,7 +128,8 @@ test.describe('Table', () => {
123
128
  await page.close();
124
129
  });
125
130
 
126
- test('reference > reference / create new object', async ({ browser }) => {
131
+ test('reference > reference / create new object', async ({ browser, browserName }) => {
132
+ test.skip(browserName === 'webkit');
127
133
  const { page } = await setupPage(browser, { url: storyUrl });
128
134
  const table = new TableManager(page);
129
135
 
@@ -156,7 +162,8 @@ test.describe('Table', () => {
156
162
  await page.close();
157
163
  });
158
164
 
159
- test('test toggles', async ({ browser }) => {
165
+ test('test toggles', async ({ browser, browserName }) => {
166
+ test.skip(browserName === 'webkit');
160
167
  const { page } = await setupPage(browser, { url: storyUrl });
161
168
  const table = new TableManager(page);
162
169
 
@@ -16,7 +16,7 @@ export default [
16
16
  'new schema': 'New schema',
17
17
  'continue label': 'Continue',
18
18
  'create stack section label': 'Create table',
19
- 'add row': 'Add row',
19
+ 'add row label': 'Add row',
20
20
  'create comment': 'Create thread',
21
21
  'column action sort descending': 'Sort Descending',
22
22
  'column action sort ascending': 'Sort Ascending',
@@ -28,6 +28,7 @@ export default [
28
28
  'column deleted label': 'Column deleted',
29
29
  'new column button label': 'Create column',
30
30
  'create new object label': 'Create new object “{{text}}”',
31
+ 'save view label': 'Save changes to view',
31
32
  },
32
33
  },
33
34
  },
@@ -3,7 +3,7 @@
3
3
  //
4
4
 
5
5
  import { Ref, ObjectId, S, TypedObject } from '@dxos/echo-schema';
6
- import { ThreadType } from '@dxos/plugin-space';
6
+ import { ThreadType } from '@dxos/plugin-space/types';
7
7
  import { ViewType } from '@dxos/schema';
8
8
 
9
9
  export const TableSchema = S.Struct({