@dxos/react-ui-table 0.7.5-main.499c70c → 0.7.5-main.6a330ac

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 (117) hide show
  1. package/dist/lib/browser/{chunk-AT7O4RAD.mjs → chunk-3FR6NHPD.mjs} +6 -4
  2. package/dist/lib/browser/chunk-3FR6NHPD.mjs.map +7 -0
  3. package/dist/lib/browser/index.mjs +482 -284
  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-NB3SFHGA.cjs} +8 -6
  8. package/dist/lib/node/chunk-NB3SFHGA.cjs.map +7 -0
  9. package/dist/lib/node/index.cjs +474 -270
  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-RLVVOZW5.mjs} +6 -4
  15. package/dist/lib/node-esm/chunk-RLVVOZW5.mjs.map +7 -0
  16. package/dist/lib/node-esm/index.mjs +482 -284
  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/components/Cell/Cell.d.ts +2 -2
  21. package/dist/types/src/_deprecated/components/Cell/Cell.d.ts.map +1 -1
  22. package/dist/types/src/_deprecated/components/Cell/CellCombobox.d.ts +1 -2
  23. package/dist/types/src/_deprecated/components/Cell/CellCombobox.d.ts.map +1 -1
  24. package/dist/types/src/_deprecated/components/ColumnMenu/ColumnMenu.d.ts +2 -3
  25. package/dist/types/src/_deprecated/components/ColumnMenu/ColumnMenu.d.ts.map +1 -1
  26. package/dist/types/src/_deprecated/components/ColumnMenu/ColumnMenu.stories.d.ts +6 -7
  27. package/dist/types/src/_deprecated/components/ColumnMenu/ColumnMenu.stories.d.ts.map +1 -1
  28. package/dist/types/src/_deprecated/components/ColumnMenu/ColumnSettings.d.ts +1 -2
  29. package/dist/types/src/_deprecated/components/ColumnMenu/ColumnSettings.d.ts.map +1 -1
  30. package/dist/types/src/_deprecated/components/ColumnMenu/ColumnSettings.stories.d.ts +6 -7
  31. package/dist/types/src/_deprecated/components/ColumnMenu/ColumnSettings.stories.d.ts.map +1 -1
  32. package/dist/types/src/_deprecated/components/Table/Table.d.ts +5 -5
  33. package/dist/types/src/_deprecated/components/Table/Table.d.ts.map +1 -1
  34. package/dist/types/src/_deprecated/components/Table/Table.stories.d.ts +8 -9
  35. package/dist/types/src/_deprecated/components/Table/Table.stories.d.ts.map +1 -1
  36. package/dist/types/src/_deprecated/components/Table/TableBody.d.ts +1 -2
  37. package/dist/types/src/_deprecated/components/Table/TableBody.d.ts.map +1 -1
  38. package/dist/types/src/_deprecated/components/Table/TableContext.d.ts +1 -1
  39. package/dist/types/src/_deprecated/components/Table/TableContext.d.ts.map +1 -1
  40. package/dist/types/src/_deprecated/components/Table/TableHead.d.ts +1 -2
  41. package/dist/types/src/_deprecated/components/Table/TableHead.d.ts.map +1 -1
  42. package/dist/types/src/_deprecated/schema/Schema.stories.d.ts +2 -3
  43. package/dist/types/src/_deprecated/schema/Schema.stories.d.ts.map +1 -1
  44. package/dist/types/src/components/Table/ColumnActionsMenu.d.ts +1 -2
  45. package/dist/types/src/components/Table/ColumnActionsMenu.d.ts.map +1 -1
  46. package/dist/types/src/components/Table/ColumnSettings.d.ts +1 -2
  47. package/dist/types/src/components/Table/ColumnSettings.d.ts.map +1 -1
  48. package/dist/types/src/components/Table/CreateRefPanel.d.ts +1 -2
  49. package/dist/types/src/components/Table/CreateRefPanel.d.ts.map +1 -1
  50. package/dist/types/src/components/Table/RefPanel.d.ts +1 -2
  51. package/dist/types/src/components/Table/RefPanel.d.ts.map +1 -1
  52. package/dist/types/src/components/Table/RowActionsMenu.d.ts +1 -2
  53. package/dist/types/src/components/Table/RowActionsMenu.d.ts.map +1 -1
  54. package/dist/types/src/components/Table/Table.d.ts +1 -1
  55. package/dist/types/src/components/Table/Table.d.ts.map +1 -1
  56. package/dist/types/src/components/Table/Table.stories.d.ts +1 -4
  57. package/dist/types/src/components/Table/Table.stories.d.ts.map +1 -1
  58. package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts +1 -2
  59. package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts.map +1 -1
  60. package/dist/types/src/components/TableToolbar/TableToolbar.d.ts +17 -0
  61. package/dist/types/src/components/TableToolbar/TableToolbar.d.ts.map +1 -0
  62. package/dist/types/src/components/TableToolbar/index.d.ts +2 -0
  63. package/dist/types/src/components/TableToolbar/index.d.ts.map +1 -0
  64. package/dist/types/src/components/index.d.ts +1 -1
  65. package/dist/types/src/components/index.d.ts.map +1 -1
  66. package/dist/types/src/components/testing.d.ts +12 -35
  67. package/dist/types/src/components/testing.d.ts.map +1 -1
  68. package/dist/types/src/hooks/useTableModel.d.ts.map +1 -1
  69. package/dist/types/src/model/table-model.d.ts +11 -19
  70. package/dist/types/src/model/table-model.d.ts.map +1 -1
  71. package/dist/types/src/model/table-presentation.d.ts.map +1 -1
  72. package/dist/types/src/model/table-sorting.d.ts +53 -0
  73. package/dist/types/src/model/table-sorting.d.ts.map +1 -0
  74. package/dist/types/src/translations.d.ts +2 -1
  75. package/dist/types/src/translations.d.ts.map +1 -1
  76. package/dist/types/src/types/table.d.ts +21 -90
  77. package/dist/types/src/types/table.d.ts.map +1 -1
  78. package/dist/types/src/util/table-buttons.d.ts +15 -1
  79. package/dist/types/src/util/table-buttons.d.ts.map +1 -1
  80. package/dist/types/src/util/table.d.ts +2 -1
  81. package/dist/types/src/util/table.d.ts.map +1 -1
  82. package/dist/types/tsconfig.tsbuildinfo +1 -1
  83. package/package.json +36 -34
  84. package/src/_deprecated/schema/Schema.stories.tsx +1 -1
  85. package/src/components/Table/ColumnActionsMenu.tsx +4 -4
  86. package/src/components/Table/CreateRefPanel.tsx +3 -4
  87. package/src/components/Table/Table.stories.tsx +55 -44
  88. package/src/components/Table/Table.tsx +4 -0
  89. package/src/components/TableCellEditor/TableCellEditor.stories.tsx +1 -1
  90. package/src/components/TableToolbar/TableToolbar.tsx +72 -0
  91. package/src/components/{Toolbar → TableToolbar}/index.ts +1 -1
  92. package/src/components/index.ts +1 -1
  93. package/src/hooks/useTableModel.ts +21 -0
  94. package/src/model/table-model.test.ts +0 -5
  95. package/src/model/table-model.ts +41 -99
  96. package/src/model/table-presentation.ts +25 -4
  97. package/src/model/table-sorting.ts +215 -0
  98. package/src/playwright/smoke.spec.ts +35 -13
  99. package/src/translations.ts +2 -1
  100. package/src/types/table.ts +3 -3
  101. package/src/util/table-buttons.ts +33 -3
  102. package/src/util/table-controls.ts +2 -2
  103. package/src/util/table.ts +85 -52
  104. package/dist/lib/browser/chunk-AT7O4RAD.mjs.map +0 -7
  105. package/dist/lib/node/chunk-YSBXNBDJ.cjs.map +0 -7
  106. package/dist/lib/node-esm/chunk-6OH2NHUX.mjs.map +0 -7
  107. package/dist/types/src/components/Toolbar/Toolbar.d.ts +0 -21
  108. package/dist/types/src/components/Toolbar/Toolbar.d.ts.map +0 -1
  109. package/dist/types/src/components/Toolbar/common.d.ts +0 -14
  110. package/dist/types/src/components/Toolbar/common.d.ts.map +0 -1
  111. package/dist/types/src/components/Toolbar/index.d.ts +0 -2
  112. package/dist/types/src/components/Toolbar/index.d.ts.map +0 -1
  113. package/dist/types/src/playwright/playwright.config.d.ts +0 -3
  114. package/dist/types/src/playwright/playwright.config.d.ts.map +0 -1
  115. package/src/components/Toolbar/Toolbar.tsx +0 -87
  116. package/src/components/Toolbar/common.tsx +0 -71
  117. /package/src/playwright/{playwright.config.ts → playwright.config.cts} +0 -0
@@ -0,0 +1,72 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import React, { useCallback, useMemo } from 'react';
6
+
7
+ import { create } from '@dxos/live-object';
8
+ import { type ThemedClassName } from '@dxos/react-ui';
9
+ import {
10
+ createGapSeparator,
11
+ createMenuAction,
12
+ type MenuAction,
13
+ type MenuActionHandler,
14
+ MenuProvider,
15
+ ToolbarMenu,
16
+ useMenuActions,
17
+ } from '@dxos/react-ui-menu';
18
+
19
+ import { translationKey } from '../../translations';
20
+
21
+ export type TableToolbarActionProperties = { type: TableToolbarActionType };
22
+
23
+ export type TableToolbarAction = MenuAction<TableToolbarActionProperties>;
24
+
25
+ export type TableToolbarActionType = 'add-row' | 'comment' | 'save-view';
26
+
27
+ type TableToolbarState = Partial<{ viewDirty: boolean }>;
28
+
29
+ export type TableToolbarProps = ThemedClassName<
30
+ TableToolbarState & { onAction?: MenuActionHandler<TableToolbarAction>; attendableId?: string }
31
+ >;
32
+
33
+ const createTableToolbarActions = (state: TableToolbarState) => {
34
+ const add = createMenuAction<TableToolbarActionProperties>('add-row', {
35
+ type: 'add-row',
36
+ icon: 'ph--plus--regular',
37
+ label: ['add row', { ns: translationKey }],
38
+ testId: 'table.toolbar.add-row',
39
+ });
40
+ const save = createMenuAction<TableToolbarActionProperties>('save-view', {
41
+ type: 'save-view',
42
+ icon: 'ph--floppy-disk--regular',
43
+ label: ['save view label', { ns: translationKey }],
44
+ testId: 'table.toolbar.save-view',
45
+ iconOnly: false,
46
+ hidden: !state.viewDirty,
47
+ });
48
+ const gap = createGapSeparator();
49
+ const comment = createMenuAction('comment', {
50
+ type: 'comment',
51
+ icon: 'ph--chat-text--regular',
52
+ label: ['create comment', { ns: translationKey }],
53
+ testId: 'table.toolbar.comment',
54
+ });
55
+ const nodes = [add, save, ...gap.nodes, comment];
56
+ return {
57
+ nodes,
58
+ edges: nodes.map(({ id: target }) => ({ source: 'root', target })),
59
+ };
60
+ };
61
+
62
+ export const TableToolbar = ({ classNames, viewDirty, attendableId, onAction }: TableToolbarProps) => {
63
+ const state = useMemo(() => create<TableToolbarState>({ viewDirty }), []);
64
+ const actionsCreator = useCallback(() => createTableToolbarActions(state), [state]);
65
+ const menu = useMenuActions(actionsCreator);
66
+
67
+ return (
68
+ <MenuProvider {...menu} attendableId={attendableId} onAction={onAction as MenuActionHandler}>
69
+ <ToolbarMenu classNames={classNames} />
70
+ </MenuProvider>
71
+ );
72
+ };
@@ -2,4 +2,4 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- export * from './Toolbar';
5
+ export * from './TableToolbar';
@@ -3,4 +3,4 @@
3
3
  //
4
4
 
5
5
  export * from './Table';
6
- export * from './Toolbar';
6
+ export * from './TableToolbar';
@@ -2,10 +2,13 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
+ import { effect } from '@preact/signals-core';
5
6
  import { useEffect, useState } from 'react';
6
7
 
7
8
  import { type ReactiveObject } from '@dxos/react-client/echo';
9
+ import { useSelectionActions } from '@dxos/react-ui-attention';
8
10
  import { type ViewProjection } from '@dxos/schema';
11
+ import { isNonNullable } from '@dxos/util';
9
12
 
10
13
  import { type BaseTableRow, TableModel, type TableModelProps } from '../model';
11
14
  import { type TableType } from '../types';
@@ -48,5 +51,23 @@ export const useTableModel = <T extends BaseTableRow = { id: string }>({
48
51
  }
49
52
  }, [model, objects]);
50
53
 
54
+ const { select, clear } = useSelectionActions([table?.id, table?.view?.target?.query.type].filter(isNonNullable));
55
+
56
+ useEffect(() => {
57
+ if (!model) {
58
+ return;
59
+ }
60
+
61
+ const unsubscribe = effect(() => {
62
+ select([...model.selection.selection.value]);
63
+ });
64
+
65
+ // Maybe clear the selection here?
66
+ return () => {
67
+ clear();
68
+ unsubscribe();
69
+ };
70
+ }, [model]);
71
+
51
72
  return model;
52
73
  };
@@ -37,11 +37,6 @@ describe('TableModel', () => {
37
37
  });
38
38
 
39
39
  describe('methods', () => {
40
- it('should set sorting', () => {
41
- model.setSort('col2', 'asc');
42
- expect(model.sorting).toEqual([{ columnId: 'col2', direction: 'asc' }]);
43
- });
44
-
45
40
  it('should pin a row', () => {
46
41
  model.pinRow(1, 'top');
47
42
  expect(model.pinnedRows.top).toContain(1);
@@ -1,16 +1,15 @@
1
1
  //
2
- // Copyright 2024 DXOS.org
2
+ // Copyright 2025 DXOS.org
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
  }
@@ -68,7 +68,8 @@ export class TablePresentation<T extends BaseTableRow = { id: string }> {
68
68
  }
69
69
 
70
70
  switch (props.format) {
71
- case FormatEnum.Boolean: {
71
+ case FormatEnum.Boolean:
72
+ case FormatEnum.SingleSelect: {
72
73
  return '';
73
74
  }
74
75
  case FormatEnum.Ref: {
@@ -119,6 +120,17 @@ export class TablePresentation<T extends BaseTableRow = { id: string }> {
119
120
  cell.readonly = true;
120
121
  }
121
122
 
123
+ if (props.format === FormatEnum.SingleSelect) {
124
+ const value = getValue(obj, field.path);
125
+ const options = this.model.projection.getFieldProjection(field.id).props.options;
126
+ if (options) {
127
+ const option = options.find((o) => o.id === value);
128
+ if (option) {
129
+ cell.accessoryHtml = `<span class="dx-tag" data-hue="${option.color}">${option.title}</span>`;
130
+ }
131
+ }
132
+ }
133
+
122
134
  const idx = toPlaneCellIndex({ col: colIndex, row: displayIndex });
123
135
  cells[idx] = cell;
124
136
  };
@@ -142,12 +154,19 @@ export class TablePresentation<T extends BaseTableRow = { id: string }> {
142
154
  const fields = this.model.table.view?.target?.fields ?? [];
143
155
  for (let col = range.start.col; col <= range.end.col && col < fields.length; col++) {
144
156
  const { field, props } = this.model.projection.getFieldProjection(fields[col].id);
157
+ const sorting = this.model.sorting?.sorting;
158
+ const direction = sorting?.fieldId === field.id ? sorting.direction : undefined;
159
+
145
160
  cells[toPlaneCellIndex({ col, row: 0 })] = {
146
161
  // TODO(burdon): Use same logic as form for fallback title.
147
162
  value: props.title ?? field.path,
148
163
  readonly: true,
149
164
  resizeHandle: 'col',
150
- accessoryHtml: tableButtons.columnSettings.render({ fieldId: field.id }),
165
+ accessoryHtml: `
166
+ ${direction !== undefined ? tableButtons.sort.render({ fieldId: field.id, direction }) : ''}
167
+ ${tableButtons.columnSettings.render({ fieldId: field.id })}
168
+ `,
169
+ className: '!bg-gridHeader',
151
170
  };
152
171
  }
153
172
 
@@ -189,13 +208,14 @@ export class TablePresentation<T extends BaseTableRow = { id: string }> {
189
208
  private getSelectAllCell(): DxGridPlaneCells {
190
209
  return {
191
210
  [toPlaneCellIndex({ col: 0, row: 0 })]: {
192
- value: '',
193
211
  accessoryHtml: tableControls.checkbox.render({
194
212
  rowIndex: 0,
195
213
  header: true,
196
214
  checked: this.model.selection.allRowsSeleted.value,
197
215
  }),
216
+ className: '!bg-gridHeader',
198
217
  readonly: true,
218
+ value: '',
199
219
  },
200
220
  };
201
221
  }
@@ -203,11 +223,12 @@ export class TablePresentation<T extends BaseTableRow = { id: string }> {
203
223
  private getNewColumnCell(): DxGridPlaneCells {
204
224
  return {
205
225
  [toPlaneCellIndex({ col: 0, row: 0 })]: {
206
- value: '',
207
226
  accessoryHtml: tableButtons.addColumn.render({
208
227
  disabled: (this.model.table.view?.target?.fields?.length ?? 0) >= VIEW_FIELD_LIMIT,
209
228
  }),
229
+ className: '!bg-gridHeader',
210
230
  readonly: true,
231
+ value: '',
211
232
  },
212
233
  };
213
234
  }