@dxos/react-ui-table 0.8.4-main.e8ec1fe → 0.8.4-main.effb148878

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 (166) hide show
  1. package/LICENSE +102 -5
  2. package/dist/lib/browser/chunk-J5LGTIGS.mjs +10 -0
  3. package/dist/lib/browser/chunk-J5LGTIGS.mjs.map +7 -0
  4. package/dist/lib/browser/{chunk-GS2UNGJK.mjs → chunk-KC62K4WS.mjs} +643 -722
  5. package/dist/lib/browser/chunk-KC62K4WS.mjs.map +7 -0
  6. package/dist/lib/browser/chunk-VWQWF3CP.mjs +67 -0
  7. package/dist/lib/browser/chunk-VWQWF3CP.mjs.map +7 -0
  8. package/dist/lib/browser/index.mjs +1103 -1023
  9. package/dist/lib/browser/index.mjs.map +4 -4
  10. package/dist/lib/browser/meta.json +1 -1
  11. package/dist/lib/browser/testing/index.mjs +35 -46
  12. package/dist/lib/browser/testing/index.mjs.map +3 -3
  13. package/dist/lib/browser/translations.mjs +36 -0
  14. package/dist/lib/browser/translations.mjs.map +7 -0
  15. package/dist/lib/browser/types/index.mjs +4 -3
  16. package/dist/lib/node-esm/{chunk-TRR3C5NX.mjs → chunk-62FOU4M7.mjs} +643 -721
  17. package/dist/lib/node-esm/chunk-62FOU4M7.mjs.map +7 -0
  18. package/dist/lib/node-esm/chunk-HSLMI22Q.mjs +11 -0
  19. package/dist/lib/node-esm/chunk-HSLMI22Q.mjs.map +7 -0
  20. package/dist/lib/node-esm/chunk-UBXOF64L.mjs +68 -0
  21. package/dist/lib/node-esm/chunk-UBXOF64L.mjs.map +7 -0
  22. package/dist/lib/node-esm/index.mjs +1103 -1023
  23. package/dist/lib/node-esm/index.mjs.map +4 -4
  24. package/dist/lib/node-esm/meta.json +1 -1
  25. package/dist/lib/node-esm/testing/index.mjs +35 -46
  26. package/dist/lib/node-esm/testing/index.mjs.map +3 -3
  27. package/dist/lib/node-esm/translations.mjs +37 -0
  28. package/dist/lib/node-esm/translations.mjs.map +7 -0
  29. package/dist/lib/node-esm/types/index.mjs +4 -3
  30. package/dist/types/src/components/Table/ColumnActionsMenu.d.ts +1 -2
  31. package/dist/types/src/components/Table/ColumnActionsMenu.d.ts.map +1 -1
  32. package/dist/types/src/components/Table/ColumnSettings.d.ts +2 -2
  33. package/dist/types/src/components/Table/ColumnSettings.d.ts.map +1 -1
  34. package/dist/types/src/components/Table/DynamicTable.d.ts +6 -7
  35. package/dist/types/src/components/Table/DynamicTable.d.ts.map +1 -1
  36. package/dist/types/src/components/Table/DynamicTable.stories.d.ts +22 -37
  37. package/dist/types/src/components/Table/DynamicTable.stories.d.ts.map +1 -1
  38. package/dist/types/src/components/Table/Relations.stories.d.ts +85 -80
  39. package/dist/types/src/components/Table/Relations.stories.d.ts.map +1 -1
  40. package/dist/types/src/components/Table/RowActionsMenu.d.ts.map +1 -1
  41. package/dist/types/src/components/Table/Table.d.ts +20 -21
  42. package/dist/types/src/components/Table/Table.d.ts.map +1 -1
  43. package/dist/types/src/components/Table/Table.stories.d.ts +93 -26
  44. package/dist/types/src/components/Table/Table.stories.d.ts.map +1 -1
  45. package/dist/types/src/components/Table/TableContent.d.ts +18 -0
  46. package/dist/types/src/components/Table/TableContent.d.ts.map +1 -0
  47. package/dist/types/src/components/Table/TableToolbar.d.ts +21 -0
  48. package/dist/types/src/components/Table/TableToolbar.d.ts.map +1 -0
  49. package/dist/types/src/components/TableCellEditor/CellValidationMessage.d.ts +1 -1
  50. package/dist/types/src/components/TableCellEditor/CellValidationMessage.d.ts.map +1 -1
  51. package/dist/types/src/components/TableCellEditor/FormCellEditor.d.ts +10 -12
  52. package/dist/types/src/components/TableCellEditor/FormCellEditor.d.ts.map +1 -1
  53. package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts +5 -7
  54. package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts.map +1 -1
  55. package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts +24 -26
  56. package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts.map +1 -1
  57. package/dist/types/src/components/index.d.ts +0 -1
  58. package/dist/types/src/components/index.d.ts.map +1 -1
  59. package/dist/types/src/components/testing.d.ts +22 -15
  60. package/dist/types/src/components/testing.d.ts.map +1 -1
  61. package/dist/types/src/hooks/index.d.ts +2 -1
  62. package/dist/types/src/hooks/index.d.ts.map +1 -1
  63. package/dist/types/src/hooks/useAddRow.d.ts +5 -6
  64. package/dist/types/src/hooks/useAddRow.d.ts.map +1 -1
  65. package/dist/types/src/hooks/useProjectionModel.d.ts +6 -0
  66. package/dist/types/src/hooks/useProjectionModel.d.ts.map +1 -0
  67. package/dist/types/src/hooks/useTableModel.d.ts +9 -9
  68. package/dist/types/src/hooks/useTableModel.d.ts.map +1 -1
  69. package/dist/types/src/index.d.ts +0 -2
  70. package/dist/types/src/index.d.ts.map +1 -1
  71. package/dist/types/src/model/modal-controller.d.ts +5 -1
  72. package/dist/types/src/model/modal-controller.d.ts.map +1 -1
  73. package/dist/types/src/model/selection-model.d.ts +18 -9
  74. package/dist/types/src/model/selection-model.d.ts.map +1 -1
  75. package/dist/types/src/model/table-model.d.ts +103 -24
  76. package/dist/types/src/model/table-model.d.ts.map +1 -1
  77. package/dist/types/src/model/table-presentation.d.ts +3 -1
  78. package/dist/types/src/model/table-presentation.d.ts.map +1 -1
  79. package/dist/types/src/playwright/TableManager.d.ts +2 -1
  80. package/dist/types/src/playwright/TableManager.d.ts.map +1 -1
  81. package/dist/types/src/playwright/playwright.config.d.ts.map +1 -1
  82. package/dist/types/src/testing/useTestTableModel.d.ts +18 -15
  83. package/dist/types/src/testing/useTestTableModel.d.ts.map +1 -1
  84. package/dist/types/src/translations.d.ts +21 -23
  85. package/dist/types/src/translations.d.ts.map +1 -1
  86. package/dist/types/src/types/Table.d.ts +32 -0
  87. package/dist/types/src/types/Table.d.ts.map +1 -0
  88. package/dist/types/src/types/index.d.ts +1 -1
  89. package/dist/types/src/types/index.d.ts.map +1 -1
  90. package/dist/types/src/util/dynamic-table.d.ts +14 -11
  91. package/dist/types/src/util/dynamic-table.d.ts.map +1 -1
  92. package/dist/types/src/util/dynamic-table.test.d.ts +2 -0
  93. package/dist/types/src/util/dynamic-table.test.d.ts.map +1 -0
  94. package/dist/types/src/util/index.d.ts +2 -0
  95. package/dist/types/src/util/index.d.ts.map +1 -1
  96. package/dist/types/src/util/query-ast.d.ts +7 -0
  97. package/dist/types/src/util/query-ast.d.ts.map +1 -0
  98. package/dist/types/src/util/schema.d.ts.map +1 -1
  99. package/dist/types/src/util/signals.d.ts.map +1 -1
  100. package/dist/types/src/util/sort.d.ts +12 -0
  101. package/dist/types/src/util/sort.d.ts.map +1 -0
  102. package/dist/types/src/util/table-buttons.d.ts +20 -20
  103. package/dist/types/src/util/table-buttons.d.ts.map +1 -1
  104. package/dist/types/src/util/table-controls.d.ts +4 -4
  105. package/dist/types/src/util/table-controls.d.ts.map +1 -1
  106. package/dist/types/src/util/tag.d.ts.map +1 -1
  107. package/dist/types/tsconfig.tsbuildinfo +1 -1
  108. package/package.json +69 -79
  109. package/src/components/Table/ColumnActionsMenu.tsx +17 -12
  110. package/src/components/Table/ColumnSettings.tsx +8 -9
  111. package/src/components/Table/DynamicTable.stories.tsx +32 -32
  112. package/src/components/Table/DynamicTable.tsx +32 -33
  113. package/src/components/Table/Relations.stories.tsx +104 -64
  114. package/src/components/Table/RowActionsMenu.tsx +6 -4
  115. package/src/components/Table/Table.stories.tsx +125 -112
  116. package/src/components/Table/Table.tsx +43 -407
  117. package/src/components/Table/TableContent.tsx +426 -0
  118. package/src/components/{TableToolbar → Table}/TableToolbar.tsx +44 -45
  119. package/src/components/TableCellEditor/CellValidationMessage.tsx +5 -6
  120. package/src/components/TableCellEditor/FormCellEditor.tsx +120 -136
  121. package/src/components/TableCellEditor/TableCellEditor.stories.tsx +25 -19
  122. package/src/components/TableCellEditor/TableCellEditor.tsx +35 -33
  123. package/src/components/index.ts +0 -1
  124. package/src/components/testing.ts +38 -17
  125. package/src/hooks/index.ts +2 -1
  126. package/src/hooks/useAddRow.ts +8 -10
  127. package/src/hooks/useProjectionModel.ts +38 -0
  128. package/src/hooks/useTableModel.ts +36 -34
  129. package/src/index.ts +0 -4
  130. package/src/model/modal-controller.ts +23 -12
  131. package/src/model/selection-model.ts +75 -44
  132. package/src/model/table-model.test.ts +77 -55
  133. package/src/model/table-model.ts +431 -168
  134. package/src/model/table-presentation.test.ts +54 -50
  135. package/src/model/table-presentation.ts +44 -40
  136. package/src/playwright/TableManager.ts +13 -6
  137. package/src/playwright/playwright.config.ts +1 -1
  138. package/src/playwright/smoke.spec.ts +52 -8
  139. package/src/testing/useTestTableModel.ts +49 -43
  140. package/src/translations.ts +20 -22
  141. package/src/types/Table.ts +81 -0
  142. package/src/types/index.ts +1 -1
  143. package/src/util/dynamic-table.test.ts +63 -0
  144. package/src/util/dynamic-table.ts +48 -33
  145. package/src/util/index.ts +2 -0
  146. package/src/util/query-ast.ts +22 -0
  147. package/src/util/sort.ts +90 -0
  148. package/src/util/table-buttons.ts +1 -1
  149. package/src/util/table-controls.ts +2 -2
  150. package/dist/lib/browser/chunk-2PFAPQL4.mjs +0 -66
  151. package/dist/lib/browser/chunk-2PFAPQL4.mjs.map +0 -7
  152. package/dist/lib/browser/chunk-GS2UNGJK.mjs.map +0 -7
  153. package/dist/lib/node-esm/chunk-6POOFFYF.mjs +0 -67
  154. package/dist/lib/node-esm/chunk-6POOFFYF.mjs.map +0 -7
  155. package/dist/lib/node-esm/chunk-TRR3C5NX.mjs.map +0 -7
  156. package/dist/types/src/components/TableToolbar/TableToolbar.d.ts +0 -21
  157. package/dist/types/src/components/TableToolbar/TableToolbar.d.ts.map +0 -1
  158. package/dist/types/src/components/TableToolbar/index.d.ts +0 -2
  159. package/dist/types/src/components/TableToolbar/index.d.ts.map +0 -1
  160. package/dist/types/src/model/table-sorting.d.ts +0 -52
  161. package/dist/types/src/model/table-sorting.d.ts.map +0 -1
  162. package/dist/types/src/types/schema.d.ts +0 -28
  163. package/dist/types/src/types/schema.d.ts.map +0 -1
  164. package/src/components/TableToolbar/index.ts +0 -5
  165. package/src/model/table-sorting.ts +0 -225
  166. package/src/types/schema.ts +0 -73
@@ -2,54 +2,62 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import { useCallback, useMemo, useRef } from 'react';
5
+ import { RegistryContext } from '@effect-atom/atom-react';
6
+ import { type RefObject, useCallback, useContext, useMemo, useRef } from 'react';
6
7
 
7
- import { isMutable, toJsonSchema } from '@dxos/echo/internal';
8
- import { useGlobalFilteredObjects } from '@dxos/plugin-search';
9
- import { faker } from '@dxos/random';
10
- import { useClient } from '@dxos/react-client';
11
- import { Filter, useQuery, useSchema } from '@dxos/react-client/echo';
12
- import { useClientProvider } from '@dxos/react-client/testing';
13
- import { ProjectionModel, View, getTypenameFromQuery } from '@dxos/schema';
8
+ import { type Database, Filter, Type } from '@dxos/echo';
9
+ import { random } from '@dxos/random';
10
+ import { useQuery, useType } from '@dxos/react-client/echo';
11
+ import { useClientStory } from '@dxos/react-client/testing';
12
+ import { useGlobalFilteredObjects } from '@dxos/react-ui-search';
13
+ import { type ProjectionModel, getTypenameFromQuery } from '@dxos/schema';
14
14
 
15
15
  import { type TableController } from '../components';
16
- import { useAddRow, useTableModel } from '../hooks';
17
- import { TablePresentation } from '../model';
18
-
19
- faker.seed(0); // NOTE(ZaymonFC): Required for smoke tests.
16
+ import { useAddRow, useProjectionModel, useTableModel } from '../hooks';
17
+ import { type TableModel, TablePresentation } from '../model';
18
+ import { Table } from '../types';
19
+
20
+ random.seed(0); // NOTE(ZaymonFC): Required for smoke tests.
21
+
22
+ export type TestTableModel<T extends Type.AnyObj = Type.AnyObj> = {
23
+ schema: T | undefined;
24
+ table: Table.Table | undefined;
25
+ projection: ProjectionModel | undefined;
26
+ tableRef: RefObject<TableController | null>;
27
+ model: TableModel | undefined;
28
+ presentation: TablePresentation | undefined;
29
+ db: Database.Database | undefined;
30
+ handleInsertRow: () => void;
31
+ handleSaveView: () => void;
32
+ handleDeleteRows: (rowIndex: number, objects: any[]) => void;
33
+ handleDeleteColumn: (fieldId: string) => void;
34
+ };
20
35
 
21
36
  /**
22
37
  * Custom hook to create and manage a test table model for storybook demonstrations.
23
38
  * Provides table data, schema, and handlers for table operations.
24
39
  */
25
- export const useTestTableModel = () => {
26
- const client = useClient();
27
- const { space } = useClientProvider();
28
-
29
- const views = useQuery(space, Filter.type(View.View));
30
- const view = useMemo(() => views.at(0), [views]);
31
- const typename = view?.query ? getTypenameFromQuery(view.query.ast) : undefined;
32
- const schema = useSchema(client, space, typename);
33
- const jsonSchema = useMemo(() => (schema ? toJsonSchema(schema) : undefined), [schema]);
34
-
35
- const projection = useMemo(() => {
36
- if (schema && view?.projection) {
37
- const projection = new ProjectionModel(toJsonSchema(schema), view.projection);
38
- projection.normalizeView();
39
- return projection;
40
- }
41
- }, [schema, view?.projection]);
40
+ export const useTestTableModel = <T extends Type.AnyObj = Type.AnyObj>(): TestTableModel<T> => {
41
+ const registry = useContext(RegistryContext);
42
+ const { space } = useClientStory();
43
+ const db = space?.db;
44
+
45
+ const tables = useQuery(space?.db, Filter.type(Table.Table));
46
+ const table = tables.at(0);
47
+ const typename = table?.view.target?.query ? getTypenameFromQuery(table.view.target.query.ast) : undefined;
48
+ const schema = useType<T>(space?.db, typename);
49
+ const projection = useProjectionModel(schema, table, registry);
42
50
 
43
51
  const features = useMemo(
44
52
  () => ({
45
53
  selection: { enabled: true, mode: 'multiple' as const },
46
54
  dataEditable: true,
47
- schemaEditable: schema && isMutable(schema),
55
+ schemaEditable: schema != null && Type.getDatabase(schema) != null,
48
56
  }),
49
57
  [schema],
50
58
  );
51
59
 
52
- const objects = useQuery(space, schema ? Filter.type(schema) : Filter.nothing());
60
+ const objects = useQuery(db, schema ? Filter.type(schema) : Filter.nothing());
53
61
  const filteredObjects = useGlobalFilteredObjects(objects);
54
62
 
55
63
  const tableRef = useRef<TableController>(null);
@@ -61,15 +69,15 @@ export const useTestTableModel = () => {
61
69
  tableRef.current?.update?.();
62
70
  }, []);
63
71
 
64
- const addRow = useAddRow({ space, schema });
72
+ const addRow = useAddRow({ db, schema });
65
73
 
66
74
  const handleDeleteRows = useCallback(
67
75
  (_: number, objects: any[]) => {
68
76
  for (const object of objects) {
69
- space?.db.remove(object);
77
+ db?.remove(object);
70
78
  }
71
79
  },
72
- [space],
80
+ [db],
73
81
  );
74
82
 
75
83
  const handleDeleteColumn = useCallback(
@@ -82,14 +90,13 @@ export const useTestTableModel = () => {
82
90
  );
83
91
 
84
92
  const model = useTableModel({
85
- view,
86
- schema: jsonSchema,
93
+ object: table,
87
94
  projection,
88
95
  features,
89
96
  rows: filteredObjects,
90
97
  onInsertRow: addRow,
91
98
  onDeleteRows: handleDeleteRows,
92
- onDeleteColumn: handleDeleteColumn,
99
+ onColumnDelete: handleDeleteColumn,
93
100
  onCellUpdate: handleCellUpdate,
94
101
  onRowOrderChange: handleRowOrderChange,
95
102
  });
@@ -97,7 +104,7 @@ export const useTestTableModel = () => {
97
104
  const handleInsertRow = useCallback(() => {
98
105
  const insertResult = model?.insertRow();
99
106
  tableRef.current?.handleInsertRowResult?.(insertResult);
100
- }, [model, tableRef.current]);
107
+ }, [model]);
101
108
 
102
109
  const handleSaveView = useCallback(() => {
103
110
  model?.saveView();
@@ -105,19 +112,18 @@ export const useTestTableModel = () => {
105
112
 
106
113
  const presentation = useMemo(() => {
107
114
  if (model) {
108
- return new TablePresentation(model);
115
+ return new TablePresentation(registry, model);
109
116
  }
110
- }, [model]);
117
+ }, [registry, model]);
111
118
 
112
119
  return {
113
120
  schema,
114
- view,
121
+ table,
115
122
  projection,
116
123
  tableRef,
117
124
  model,
118
125
  presentation,
119
- space,
120
- client,
126
+ db,
121
127
  handleInsertRow,
122
128
  handleSaveView,
123
129
  handleDeleteRows,
@@ -4,33 +4,31 @@
4
4
 
5
5
  import { type Resource } from '@dxos/react-ui';
6
6
 
7
- export const translationKey = 'react-ui-table';
7
+ export const translationKey = '@dxos/react-ui-table';
8
8
 
9
9
  export const translations = [
10
10
  {
11
11
  'en-US': {
12
12
  [translationKey]: {
13
- 'object placeholder': 'New table',
14
- 'create object label': 'Create table',
15
- 'table name placeholder': 'Table name',
16
- 'settings title': 'Table settings',
17
- 'table schema label': 'Record type',
18
- 'new schema': 'New record type',
19
- 'continue label': 'Continue',
20
- 'create stack section label': 'Create table',
21
- 'add row label': 'Add row',
22
- 'create comment': 'Create thread',
23
- 'column action sort descending': 'Sort Descending',
24
- 'column action sort ascending': 'Sort Ascending',
25
- 'column action clear sorting': 'Clear Sorting',
26
- 'column action settings': 'Column Settings',
27
- 'column action delete': 'Delete Column',
28
- 'delete row label': 'Delete row',
29
- 'bulk delete row label': 'Delete selected rows',
30
- 'column deleted label': 'Column deleted',
31
- 'new column button label': 'Create column',
32
- 'create new object label': 'Create new object “{{text}}”',
33
- 'save view label': 'Save changes to view',
13
+ 'object.placeholder': 'New table',
14
+ 'create-object.label': 'Create table',
15
+ 'table-name.placeholder': 'Table name',
16
+ 'settings.title': 'Table settings',
17
+ 'table-schema.label': 'Type',
18
+ 'continue.label': 'Continue',
19
+ 'create-stack-section.label': 'Create table',
20
+ 'add-row.label': 'Add row',
21
+ 'column-action-sort-descending.menu': 'Sort Descending',
22
+ 'column-action-sort-ascending.menu': 'Sort Ascending',
23
+ 'column-action-clear-sorting.menu': 'Clear Sorting',
24
+ 'column-action-settings.menu': 'Column Settings',
25
+ 'column-action-delete.menu': 'Delete Column',
26
+ 'delete-row.label': 'Delete row',
27
+ 'bulk-delete-row.label': 'Delete selected rows',
28
+ 'column-deleted.label': 'Column deleted',
29
+ 'new-column-button.label': 'Create column',
30
+ 'create-new-object.label': 'Create new object “{{text}}”',
31
+ 'save-view.label': 'Save changes to view',
34
32
  },
35
33
  },
36
34
  },
@@ -0,0 +1,81 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import * as Match from 'effect/Match';
6
+ import * as Schema from 'effect/Schema';
7
+ import * as SchemaAST from 'effect/SchemaAST';
8
+
9
+ // QueryAST is referenced indirectly through `Type.InstanceType<typeof TableSchema>`
10
+ // (Ref.Ref(View.View) → View.View → QueryAST.Query) in the emitted .d.ts; the
11
+ // namespace import keeps the inferred types portable.
12
+ // eslint-disable-next-line unused-imports/no-unused-imports
13
+ import { DXN, Annotation, JsonSchema, Obj, QueryAST, Ref, Type, View } from '@dxos/echo';
14
+ import { FormInputAnnotation, type JsonPath, type JsonSchemaType, LabelAnnotation } from '@dxos/echo/internal';
15
+ import { ViewAnnotation } from '@dxos/schema';
16
+
17
+ const TableSchema = Schema.Struct({
18
+ name: Schema.String.pipe(Schema.optional),
19
+
20
+ view: Ref.Ref(View.View).pipe(FormInputAnnotation.set(false)),
21
+
22
+ sizes: Schema.Record({
23
+ // TODO(wittjosiah): Should be JsonPath.
24
+ key: Schema.String,
25
+ value: Schema.Number,
26
+ }).pipe(Schema.mutable, FormInputAnnotation.set(false)),
27
+ }).pipe(
28
+ LabelAnnotation.set(['name']),
29
+ ViewAnnotation.set(['view']),
30
+ Annotation.IconAnnotation.set({
31
+ icon: 'ph--table--regular',
32
+ hue: 'green',
33
+ }),
34
+ Type.makeObject(DXN.make('org.dxos.type.table', '0.1.0')),
35
+ );
36
+
37
+ // TODO(wittjosiah): Try to clean up this type inference.
38
+ export interface Table extends Type.InstanceType<typeof TableSchema> {}
39
+ export const Table = TableSchema;
40
+
41
+ type MakeProps = {
42
+ name?: string;
43
+ sizes?: Record<string, number>;
44
+ view: View.View;
45
+ /** Required to auto-size columns. */
46
+ jsonSchema?: JsonSchemaType;
47
+ };
48
+
49
+ /**
50
+ * Make a table as a view of a data set.
51
+ */
52
+ export const make = ({ name, sizes = {}, view, jsonSchema }: MakeProps): Table => {
53
+ const table = Obj.make(Table, { name, view: Ref.make(view), sizes } as Obj.MakeProps<typeof Table>);
54
+
55
+ // Preset sizes.
56
+ if (jsonSchema) {
57
+ const schema = JsonSchema.toEffectSchema(jsonSchema);
58
+ const properties = SchemaAST.getPropertySignatures(schema.ast);
59
+ for (const property of properties) {
60
+ const name = property.name.toString() as JsonPath;
61
+ if (sizes?.[name]) {
62
+ table.sizes[name] = sizes[name];
63
+ continue;
64
+ }
65
+
66
+ Match.type<SchemaAST.AST>().pipe(
67
+ Match.when({ _tag: 'BooleanKeyword' }, () => {
68
+ table.sizes[name] = 100;
69
+ }),
70
+ Match.when({ _tag: 'NumberKeyword' }, () => {
71
+ table.sizes[name] = 100;
72
+ }),
73
+ Match.orElse(() => {
74
+ // Noop.
75
+ }),
76
+ )(property.type);
77
+ }
78
+ }
79
+
80
+ return table;
81
+ };
@@ -2,4 +2,4 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- export * as Table from './schema';
5
+ export * as Table from './Table';
@@ -0,0 +1,63 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { Registry } from '@effect-atom/atom-react';
6
+ import { describe, expect, test } from 'vitest';
7
+
8
+ import { Format } from '@dxos/echo';
9
+
10
+ import { type TablePropertyDefinition, getBaseSchema, makeDynamicTable } from './dynamic-table';
11
+
12
+ describe('makeDynamicTable', () => {
13
+ /**
14
+ * Builds the Type entity from a JSON schema, then makes the table.
15
+ * Confirms makeDynamicTable + setProperties (title) works for the jsonSchema path.
16
+ */
17
+ test('makeDynamicTable from a jsonSchema with properties with title does not throw', () => {
18
+ const registry = Registry.make();
19
+ const jsonSchema = {
20
+ typename: 'com.example.type.dynamicTablePlain',
21
+ version: '0.1.0',
22
+ type: 'object' as const,
23
+ properties: {
24
+ id: { type: 'string' as const },
25
+ name: { type: 'string' as const },
26
+ },
27
+ required: ['id'],
28
+ };
29
+ const properties: TablePropertyDefinition[] = [
30
+ { name: 'id', format: Format.TypeFormat.String },
31
+ { name: 'name', format: Format.TypeFormat.String, title: 'Name' },
32
+ ];
33
+
34
+ expect(() => {
35
+ const { type } = getBaseSchema({ jsonSchema });
36
+ const { projection, object } = makeDynamicTable({ registry, type, properties });
37
+ expect(projection).toBeDefined();
38
+ expect(object).toBeDefined();
39
+ expect(projection.getFields().length).toBeGreaterThan(0);
40
+ }).not.toThrow();
41
+ });
42
+
43
+ /**
44
+ * Builds the Type entity from property definitions, then makes the table.
45
+ * Confirms makeDynamicTable + setProperties (title) works for the property-definitions path.
46
+ */
47
+ test('makeDynamicTable from getBaseSchema(typename, properties) with properties with title does not throw', () => {
48
+ const registry = Registry.make();
49
+ const typename = 'com.example.type.dynamicTableTest';
50
+ const properties: TablePropertyDefinition[] = [
51
+ { name: 'id', format: Format.TypeFormat.String },
52
+ { name: 'name', format: Format.TypeFormat.String, title: 'Name' },
53
+ ];
54
+
55
+ expect(() => {
56
+ const { type } = getBaseSchema({ typename, properties });
57
+ const { projection, object } = makeDynamicTable({ registry, type, properties });
58
+ expect(projection).toBeDefined();
59
+ expect(object).toBeDefined();
60
+ expect(projection.getFields().length).toBeGreaterThan(0);
61
+ }).not.toThrow();
62
+ });
63
+ });
@@ -2,16 +2,16 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import type * as Schema from 'effect/Schema';
5
+ import { type Registry } from '@effect-atom/atom-react';
6
+ import type * as Types from 'effect/Types';
6
7
 
7
- import { Filter, Obj, Query } from '@dxos/echo';
8
- import { getTypename, toJsonSchema } from '@dxos/echo/internal';
9
- import type { JsonSchemaType } from '@dxos/echo/internal';
8
+ import { Filter, type JsonSchema, Obj, Order, Query, type QueryAST, Ref, Type, type View } from '@dxos/echo';
10
9
  import {
11
10
  ProjectionModel,
12
11
  type SchemaPropertyDefinition,
13
- type SortDirectionType,
14
- View,
12
+ ViewModel,
13
+ createEchoChangeCallback,
14
+ getSchemaFromJsonSchema,
15
15
  getSchemaFromPropertyDefinitions,
16
16
  } from '@dxos/schema';
17
17
 
@@ -21,7 +21,7 @@ import { Table } from '../types';
21
21
  type PropertyDisplayProps = {
22
22
  size: number;
23
23
  title: string;
24
- sort: SortDirectionType;
24
+ sort: QueryAST.OrderDirection;
25
25
  };
26
26
 
27
27
  export type TablePropertyDefinition = SchemaPropertyDefinition & Partial<PropertyDisplayProps>;
@@ -31,50 +31,57 @@ export type TablePropertyDefinition = SchemaPropertyDefinition & Partial<Propert
31
31
  */
32
32
  // TODO(burdon): Remove variance.
33
33
  export const getBaseSchema = ({
34
+ type,
34
35
  typename,
35
36
  properties,
36
37
  jsonSchema,
37
- schema,
38
38
  }: {
39
+ type?: Type.AnyEntity;
39
40
  typename?: string;
40
41
  properties?: TablePropertyDefinition[];
41
- jsonSchema?: JsonSchemaType;
42
- schema?: Schema.Schema.AnyNoContext;
43
- }): { typename: string; jsonSchema: JsonSchemaType } => {
44
- if (typename && properties) {
45
- const schema = getSchemaFromPropertyDefinitions(typename, properties);
46
- return { typename: schema.typename, jsonSchema: schema.jsonSchema };
47
- } else if (schema) {
48
- return { typename: getTypename(schema)!, jsonSchema: toJsonSchema(schema) };
49
- } else if (typename && jsonSchema) {
50
- return { typename, jsonSchema };
51
- } else {
42
+ jsonSchema?: Types.DeepMutable<JsonSchema.JsonSchema>;
43
+ }): { typename: string; type: Type.AnyEntity } => {
44
+ const resolved =
45
+ typename && properties
46
+ ? getSchemaFromPropertyDefinitions(typename, properties)
47
+ : (type ?? (jsonSchema ? getSchemaFromJsonSchema(jsonSchema, typename) : undefined));
48
+ if (!resolved) {
52
49
  throw new Error('invalid properties');
53
50
  }
51
+
52
+ return { typename: Type.getTypename(resolved)!, type: resolved };
54
53
  };
55
54
 
56
55
  export const makeDynamicTable = ({
57
- jsonSchema,
56
+ registry,
57
+ type,
58
58
  properties,
59
59
  }: {
60
- jsonSchema: JsonSchemaType;
60
+ registry: Registry.Registry;
61
+ type: Type.AnyEntity;
61
62
  properties?: TablePropertyDefinition[];
62
- }): { projection: ProjectionModel; view: View.View } => {
63
- const table = Obj.make(Table.Table, { sizes: {} });
64
- const view = View.make({
63
+ }): { projection: ProjectionModel; object: Table.Table } => {
64
+ const jsonSchema = type.jsonSchema;
65
+ const view = ViewModel.make({
65
66
  query: Query.select(Filter.everything()),
66
67
  jsonSchema,
67
- presentation: table,
68
+ type,
68
69
  ...(properties && { fields: properties.map((property) => property.name) }),
69
70
  });
71
+ const object = Obj.make(Table.Table, { view: Ref.make(view), sizes: {} });
70
72
 
71
- const projection = new ProjectionModel(jsonSchema, view.projection);
73
+ const projection = new ProjectionModel({
74
+ registry,
75
+ view,
76
+ baseSchema: jsonSchema,
77
+ change: createEchoChangeCallback(view, type),
78
+ });
72
79
  projection.normalizeView();
73
- if (properties && projection.fields) {
74
- setProperties(view, projection, table, properties);
80
+ if (properties && projection.getFields()) {
81
+ setProperties(view, projection, object, properties);
75
82
  }
76
83
 
77
- return { projection, view };
84
+ return { projection, object };
78
85
  };
79
86
 
80
87
  const setProperties = (
@@ -84,10 +91,12 @@ const setProperties = (
84
91
  properties: TablePropertyDefinition[],
85
92
  ) => {
86
93
  for (const property of properties) {
87
- const field = projection.fields.find((field) => field.path === property.name);
94
+ const field = projection.getFields().find((field) => field.path === property.name);
88
95
  if (field) {
89
96
  if (property.size !== undefined) {
90
- table.sizes[field.path] = property.size;
97
+ Obj.update(table, (table) => {
98
+ table.sizes[field.path] = property.size!;
99
+ });
91
100
  }
92
101
 
93
102
  if (property.title !== undefined) {
@@ -99,8 +108,14 @@ const setProperties = (
99
108
  }
100
109
 
101
110
  if (property.sort) {
102
- const fieldId = field.id;
103
- view.sort = [{ fieldId, direction: property.sort }];
111
+ // Apply sort to query instead of deprecated view.sort
112
+ const currentQuery = Query.fromAst(Obj.getSnapshot(view).query.ast);
113
+ // Use any type parameter since we're working with dynamic field paths
114
+ const newQuery = currentQuery.orderBy(Order.property<any>(field.path as string, property.sort));
115
+ Obj.update(view, (view) => {
116
+ // Type assertion needed because Query AST types have some variance issues.
117
+ view.query.ast = newQuery.ast as typeof view.query.ast;
118
+ });
104
119
  }
105
120
  }
106
121
  }
package/src/util/index.ts CHANGED
@@ -3,7 +3,9 @@
3
3
  //
4
4
 
5
5
  export * from './dynamic-table';
6
+ export * from './query-ast';
6
7
  export * from './signals';
8
+ export * from './sort';
7
9
  export * from './table-buttons';
8
10
  export * from './table-controls';
9
11
  export * from './schema';
@@ -0,0 +1,22 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { type QueryAST } from '@dxos/echo';
6
+
7
+ /**
8
+ * Extracts order clauses from a query AST.
9
+ * Handles nested structures (options, order, etc.).
10
+ */
11
+ export const extractOrder = (queryAst: QueryAST.Query): readonly QueryAST.Order[] | undefined => {
12
+ if (queryAst.type === 'order') {
13
+ return queryAst.order;
14
+ }
15
+ if (queryAst.type === 'options') {
16
+ return extractOrder(queryAst.query);
17
+ }
18
+ if (queryAst.type === 'from') {
19
+ return extractOrder(queryAst.query);
20
+ }
21
+ return undefined;
22
+ };
@@ -0,0 +1,90 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { Format, type QueryAST } from '@dxos/echo';
6
+
7
+ /**
8
+ * Gets a normalized sort value for a given value and format.
9
+ * This ensures consistent sorting behavior across different data types.
10
+ */
11
+ export const getSortValue = (value: any, format: Format.TypeFormat): any => {
12
+ // Handle null/undefined
13
+ if (value === undefined || value === null) {
14
+ return null;
15
+ }
16
+
17
+ // Handle dates chronologically if schema indicates date format
18
+ const isDateFormat = format === Format.TypeFormat.DateTime || format === Format.TypeFormat.Date;
19
+ if (isDateFormat) {
20
+ const date = new Date(value);
21
+ if (!isNaN(date.getTime())) {
22
+ return date.getTime();
23
+ }
24
+ // Fallback to string if date parsing fails
25
+ return String(value);
26
+ }
27
+
28
+ // Handle numbers if schema indicates number format
29
+ if (
30
+ format === Format.TypeFormat.Number ||
31
+ format === Format.TypeFormat.Integer ||
32
+ format === Format.TypeFormat.Currency ||
33
+ format === Format.TypeFormat.Percent ||
34
+ format === Format.TypeFormat.Duration
35
+ ) {
36
+ const num = typeof value === 'number' ? value : Number(value);
37
+ if (!isNaN(num)) {
38
+ return num;
39
+ }
40
+ // Fallback to string if number parsing fails
41
+ return String(value);
42
+ }
43
+
44
+ // Default to string comparison
45
+ if (typeof value === 'string') {
46
+ return value;
47
+ }
48
+ if (typeof value === 'number') {
49
+ return value;
50
+ }
51
+
52
+ // Fallback to string representation
53
+ return String(value);
54
+ };
55
+
56
+ /**
57
+ * Compares two values for sorting based on the field format.
58
+ * Handles dates, numbers, and strings appropriately.
59
+ */
60
+ export const compareValues = (
61
+ aValue: any,
62
+ bValue: any,
63
+ format: Format.TypeFormat,
64
+ direction: QueryAST.OrderDirection,
65
+ ): number => {
66
+ // Handle null/undefined values - nulls sort to the end
67
+ if (aValue === undefined || aValue === null) {
68
+ return bValue === undefined || bValue === null ? 0 : 1;
69
+ }
70
+ if (bValue === undefined || bValue === null) {
71
+ return -1;
72
+ }
73
+
74
+ // Get normalized sort values
75
+ const aSortValue = getSortValue(aValue, format);
76
+ const bSortValue = getSortValue(bValue, format);
77
+
78
+ // Compare based on type
79
+ let comparison = 0;
80
+ if (typeof aSortValue === 'number' && typeof bSortValue === 'number') {
81
+ comparison = aSortValue - bSortValue;
82
+ } else if (typeof aSortValue === 'string' && typeof bSortValue === 'string') {
83
+ comparison = aSortValue.localeCompare(bSortValue);
84
+ } else {
85
+ // Mixed types - convert both to strings
86
+ comparison = String(aSortValue).localeCompare(String(bSortValue));
87
+ }
88
+
89
+ return direction === 'desc' ? -comparison : comparison;
90
+ };
@@ -39,7 +39,7 @@ const createButton = ({
39
39
  .map(([k, v]) => `${k}="${v}"`)
40
40
  .join(' ');
41
41
 
42
- return `<button ${attr} data-testid="${testId}" class="dx-button is-6 aspect-square pli-0.5 min-bs-0" ${dataAttrs} ${disabled ? 'disabled' : ''} data-dx-grid-action="accessory"><svg data-size="4"><use href="/icons.svg#${icon}"/></svg></button>`;
42
+ return `<button ${attr} data-testid="${testId}" class="dx-button w-6 aspect-square px-0.5 min-h-0" ${dataAttrs} ${disabled ? 'disabled' : ''} data-dx-grid-action="accessory"><svg data-size="4"><use href="/icons.svg#${icon}"/></svg></button>`;
43
43
  };
44
44
 
45
45
  const addColumnButton = {
@@ -25,8 +25,8 @@ export const CONTROL_IDENTIFIERS = {
25
25
  } as const;
26
26
 
27
27
  const BASE_CLASSES = {
28
- checkbox: 'absolute inset-block-[.375rem] inline-end-[.375rem] dx-checkbox',
29
- switch: 'absolute inset-block-[.25rem] inline-end-[.25rem] dx-checkbox--switch',
28
+ checkbox: 'absolute inset-y-[.375rem] end-[.375rem] dx-checkbox',
29
+ switch: 'absolute inset-y-[.25rem] end-[.25rem] dx-checkbox--switch',
30
30
  } as const;
31
31
 
32
32
  const renderAttributes = (data: Record<string, string>) => {