@dxos/react-ui-table 0.8.1-staging.391c573 → 0.8.1-staging.9eaf14f

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 (191) hide show
  1. package/dist/lib/browser/{chunk-3FR6NHPD.mjs → chunk-YWNRUF4J.mjs} +8 -6
  2. package/dist/lib/browser/chunk-YWNRUF4J.mjs.map +7 -0
  3. package/dist/lib/browser/index.mjs +625 -225
  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-NB3SFHGA.cjs → chunk-TGG6OZ54.cjs} +10 -8
  8. package/dist/lib/node/chunk-TGG6OZ54.cjs.map +7 -0
  9. package/dist/lib/node/index.cjs +618 -220
  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-RLVVOZW5.mjs → chunk-KKLP54L3.mjs} +8 -6
  15. package/dist/lib/node-esm/chunk-KKLP54L3.mjs.map +7 -0
  16. package/dist/lib/node-esm/index.mjs +625 -225
  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/ColumnActionsMenu.d.ts.map +1 -1
  21. package/dist/types/src/components/Table/ColumnSettings.d.ts.map +1 -1
  22. package/dist/types/src/components/Table/DynamicTable.d.ts +24 -0
  23. package/dist/types/src/components/Table/DynamicTable.d.ts.map +1 -0
  24. package/dist/types/src/components/Table/DynamicTable.stories.d.ts +10 -0
  25. package/dist/types/src/components/Table/DynamicTable.stories.d.ts.map +1 -0
  26. package/dist/types/src/components/Table/RowActionsMenu.d.ts.map +1 -1
  27. package/dist/types/src/components/Table/Table.d.ts +1 -0
  28. package/dist/types/src/components/Table/Table.d.ts.map +1 -1
  29. package/dist/types/src/components/Table/Table.stories.d.ts +4 -3
  30. package/dist/types/src/components/Table/Table.stories.d.ts.map +1 -1
  31. package/dist/types/src/components/Table/index.d.ts +1 -0
  32. package/dist/types/src/components/Table/index.d.ts.map +1 -1
  33. package/dist/types/src/components/Table/relations.stories.d.ts +9 -0
  34. package/dist/types/src/components/Table/relations.stories.d.ts.map +1 -0
  35. package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts.map +1 -1
  36. package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts.map +1 -1
  37. package/dist/types/src/components/testing.d.ts +7 -7
  38. package/dist/types/src/components/testing.d.ts.map +1 -1
  39. package/dist/types/src/hooks/useTableModel.d.ts +8 -9
  40. package/dist/types/src/hooks/useTableModel.d.ts.map +1 -1
  41. package/dist/types/src/model/index.d.ts +1 -0
  42. package/dist/types/src/model/index.d.ts.map +1 -1
  43. package/dist/types/src/model/selection-model.d.ts +6 -3
  44. package/dist/types/src/model/selection-model.d.ts.map +1 -1
  45. package/dist/types/src/model/table-model.d.ts +42 -16
  46. package/dist/types/src/model/table-model.d.ts.map +1 -1
  47. package/dist/types/src/model/table-presentation.d.ts +4 -4
  48. package/dist/types/src/model/table-presentation.d.ts.map +1 -1
  49. package/dist/types/src/model/table-sorting.d.ts +2 -2
  50. package/dist/types/src/model/table-sorting.d.ts.map +1 -1
  51. package/dist/types/src/types/table.d.ts +7 -32
  52. package/dist/types/src/types/table.d.ts.map +1 -1
  53. package/dist/types/src/util/dynamic-table.d.ts +31 -0
  54. package/dist/types/src/util/dynamic-table.d.ts.map +1 -0
  55. package/dist/types/src/util/index.d.ts +1 -0
  56. package/dist/types/src/util/index.d.ts.map +1 -1
  57. package/dist/types/src/util/table-controls.d.ts +2 -2
  58. package/dist/types/src/util/table-controls.d.ts.map +1 -1
  59. package/dist/types/src/util/table.d.ts +5 -3
  60. package/dist/types/src/util/table.d.ts.map +1 -1
  61. package/package.json +37 -36
  62. package/src/components/Table/ColumnActionsMenu.tsx +6 -4
  63. package/src/components/Table/ColumnSettings.tsx +5 -6
  64. package/src/components/Table/CreateRefPanel.tsx +3 -3
  65. package/src/components/Table/DynamicTable.stories.tsx +155 -0
  66. package/src/components/Table/DynamicTable.tsx +104 -0
  67. package/src/components/Table/RefPanel.tsx +3 -3
  68. package/src/components/Table/RowActionsMenu.tsx +18 -3
  69. package/src/components/Table/Table.stories.tsx +170 -115
  70. package/src/components/Table/Table.tsx +71 -26
  71. package/src/components/Table/index.ts +1 -0
  72. package/src/components/Table/relations.stories.tsx +113 -0
  73. package/src/components/TableCellEditor/TableCellEditor.stories.tsx +15 -5
  74. package/src/components/TableCellEditor/TableCellEditor.tsx +102 -4
  75. package/src/components/testing.ts +0 -1
  76. package/src/hooks/useTableModel.ts +35 -14
  77. package/src/model/index.ts +1 -0
  78. package/src/model/selection-model.ts +12 -2
  79. package/src/model/table-model.test.ts +2 -2
  80. package/src/model/table-model.ts +117 -32
  81. package/src/model/table-presentation.test.ts +2 -2
  82. package/src/model/table-presentation.ts +94 -22
  83. package/src/model/table-sorting.ts +3 -2
  84. package/src/playwright/smoke.spec.ts +2 -3
  85. package/src/types/table.ts +9 -10
  86. package/src/util/dynamic-table.ts +103 -0
  87. package/src/util/index.ts +1 -0
  88. package/src/util/table-controls.ts +4 -4
  89. package/src/util/table.ts +68 -65
  90. package/dist/lib/browser/_deprecated/index.mjs +0 -1483
  91. package/dist/lib/browser/_deprecated/index.mjs.map +0 -7
  92. package/dist/lib/browser/chunk-3FR6NHPD.mjs.map +0 -7
  93. package/dist/lib/node/_deprecated/index.cjs +0 -1511
  94. package/dist/lib/node/_deprecated/index.cjs.map +0 -7
  95. package/dist/lib/node/chunk-NB3SFHGA.cjs.map +0 -7
  96. package/dist/lib/node-esm/_deprecated/index.mjs +0 -1485
  97. package/dist/lib/node-esm/_deprecated/index.mjs.map +0 -7
  98. package/dist/lib/node-esm/chunk-RLVVOZW5.mjs.map +0 -7
  99. package/dist/types/src/_deprecated/components/Cell/Cell.d.ts +0 -12
  100. package/dist/types/src/_deprecated/components/Cell/Cell.d.ts.map +0 -1
  101. package/dist/types/src/_deprecated/components/Cell/CellCombobox.d.ts +0 -13
  102. package/dist/types/src/_deprecated/components/Cell/CellCombobox.d.ts.map +0 -1
  103. package/dist/types/src/_deprecated/components/Cell/index.d.ts +0 -2
  104. package/dist/types/src/_deprecated/components/Cell/index.d.ts.map +0 -1
  105. package/dist/types/src/_deprecated/components/ColumnMenu/ColumnMenu.d.ts +0 -23
  106. package/dist/types/src/_deprecated/components/ColumnMenu/ColumnMenu.d.ts.map +0 -1
  107. package/dist/types/src/_deprecated/components/ColumnMenu/ColumnMenu.stories.d.ts +0 -36
  108. package/dist/types/src/_deprecated/components/ColumnMenu/ColumnMenu.stories.d.ts.map +0 -1
  109. package/dist/types/src/_deprecated/components/ColumnMenu/ColumnSettings.d.ts +0 -15
  110. package/dist/types/src/_deprecated/components/ColumnMenu/ColumnSettings.d.ts.map +0 -1
  111. package/dist/types/src/_deprecated/components/ColumnMenu/ColumnSettings.stories.d.ts +0 -84
  112. package/dist/types/src/_deprecated/components/ColumnMenu/ColumnSettings.stories.d.ts.map +0 -1
  113. package/dist/types/src/_deprecated/components/ColumnMenu/index.d.ts +0 -3
  114. package/dist/types/src/_deprecated/components/ColumnMenu/index.d.ts.map +0 -1
  115. package/dist/types/src/_deprecated/components/Table/Table.d.ts +0 -20
  116. package/dist/types/src/_deprecated/components/Table/Table.d.ts.map +0 -1
  117. package/dist/types/src/_deprecated/components/Table/Table.stories.d.ts +0 -116
  118. package/dist/types/src/_deprecated/components/Table/Table.stories.d.ts.map +0 -1
  119. package/dist/types/src/_deprecated/components/Table/TableBody.d.ts +0 -12
  120. package/dist/types/src/_deprecated/components/Table/TableBody.d.ts.map +0 -1
  121. package/dist/types/src/_deprecated/components/Table/TableContext.d.ts +0 -13
  122. package/dist/types/src/_deprecated/components/Table/TableContext.d.ts.map +0 -1
  123. package/dist/types/src/_deprecated/components/Table/TableHead.d.ts +0 -9
  124. package/dist/types/src/_deprecated/components/Table/TableHead.d.ts.map +0 -1
  125. package/dist/types/src/_deprecated/components/Table/TableRootContext.d.ts +0 -17
  126. package/dist/types/src/_deprecated/components/Table/TableRootContext.d.ts.map +0 -1
  127. package/dist/types/src/_deprecated/components/Table/index.d.ts +0 -5
  128. package/dist/types/src/_deprecated/components/Table/index.d.ts.map +0 -1
  129. package/dist/types/src/_deprecated/components/Table/props.d.ts +0 -42
  130. package/dist/types/src/_deprecated/components/Table/props.d.ts.map +0 -1
  131. package/dist/types/src/_deprecated/components/index.d.ts +0 -4
  132. package/dist/types/src/_deprecated/components/index.d.ts.map +0 -1
  133. package/dist/types/src/_deprecated/helpers.d.ts +0 -91
  134. package/dist/types/src/_deprecated/helpers.d.ts.map +0 -1
  135. package/dist/types/src/_deprecated/hooks/index.d.ts +0 -5
  136. package/dist/types/src/_deprecated/hooks/index.d.ts.map +0 -1
  137. package/dist/types/src/_deprecated/hooks/useColumnSizing.d.ts +0 -12
  138. package/dist/types/src/_deprecated/hooks/useColumnSizing.d.ts.map +0 -1
  139. package/dist/types/src/_deprecated/hooks/useColumnSorting.d.ts +0 -15
  140. package/dist/types/src/_deprecated/hooks/useColumnSorting.d.ts.map +0 -1
  141. package/dist/types/src/_deprecated/hooks/usePinLastRow.d.ts +0 -3
  142. package/dist/types/src/_deprecated/hooks/usePinLastRow.d.ts.map +0 -1
  143. package/dist/types/src/_deprecated/hooks/useRowSelection.d.ts +0 -7
  144. package/dist/types/src/_deprecated/hooks/useRowSelection.d.ts.map +0 -1
  145. package/dist/types/src/_deprecated/index.d.ts +0 -8
  146. package/dist/types/src/_deprecated/index.d.ts.map +0 -1
  147. package/dist/types/src/_deprecated/schema/Schema.stories.d.ts +0 -11
  148. package/dist/types/src/_deprecated/schema/Schema.stories.d.ts.map +0 -1
  149. package/dist/types/src/_deprecated/schema/column-utils.d.ts +0 -7
  150. package/dist/types/src/_deprecated/schema/column-utils.d.ts.map +0 -1
  151. package/dist/types/src/_deprecated/schema/index.d.ts +0 -3
  152. package/dist/types/src/_deprecated/schema/index.d.ts.map +0 -1
  153. package/dist/types/src/_deprecated/schema/types.d.ts +0 -27
  154. package/dist/types/src/_deprecated/schema/types.d.ts.map +0 -1
  155. package/dist/types/src/_deprecated/theme.d.ts +0 -29
  156. package/dist/types/src/_deprecated/theme.d.ts.map +0 -1
  157. package/dist/types/src/_deprecated/translations.d.ts +0 -21
  158. package/dist/types/src/_deprecated/translations.d.ts.map +0 -1
  159. package/dist/types/src/_deprecated/types.d.ts +0 -26
  160. package/dist/types/src/_deprecated/types.d.ts.map +0 -1
  161. package/src/_deprecated/components/Cell/Cell.tsx +0 -88
  162. package/src/_deprecated/components/Cell/CellCombobox.tsx +0 -103
  163. package/src/_deprecated/components/Cell/index.ts +0 -5
  164. package/src/_deprecated/components/ColumnMenu/ColumnMenu.stories.tsx +0 -129
  165. package/src/_deprecated/components/ColumnMenu/ColumnMenu.tsx +0 -182
  166. package/src/_deprecated/components/ColumnMenu/ColumnSettings.stories.tsx +0 -108
  167. package/src/_deprecated/components/ColumnMenu/ColumnSettings.tsx +0 -207
  168. package/src/_deprecated/components/ColumnMenu/index.ts +0 -6
  169. package/src/_deprecated/components/Table/Table.stories.tsx +0 -508
  170. package/src/_deprecated/components/Table/Table.tsx +0 -301
  171. package/src/_deprecated/components/Table/TableBody.tsx +0 -82
  172. package/src/_deprecated/components/Table/TableContext.tsx +0 -34
  173. package/src/_deprecated/components/Table/TableHead.tsx +0 -78
  174. package/src/_deprecated/components/Table/TableRootContext.ts +0 -39
  175. package/src/_deprecated/components/Table/index.ts +0 -8
  176. package/src/_deprecated/components/Table/props.ts +0 -60
  177. package/src/_deprecated/components/index.ts +0 -7
  178. package/src/_deprecated/helpers.tsx +0 -526
  179. package/src/_deprecated/hooks/index.ts +0 -8
  180. package/src/_deprecated/hooks/useColumnSizing.ts +0 -46
  181. package/src/_deprecated/hooks/useColumnSorting.ts +0 -64
  182. package/src/_deprecated/hooks/usePinLastRow.ts +0 -47
  183. package/src/_deprecated/hooks/useRowSelection.ts +0 -40
  184. package/src/_deprecated/index.ts +0 -12
  185. package/src/_deprecated/schema/Schema.stories.tsx +0 -59
  186. package/src/_deprecated/schema/column-utils.ts +0 -54
  187. package/src/_deprecated/schema/index.ts +0 -6
  188. package/src/_deprecated/schema/types.ts +0 -40
  189. package/src/_deprecated/theme.ts +0 -82
  190. package/src/_deprecated/translations.ts +0 -26
  191. package/src/_deprecated/types.ts +0 -38
@@ -0,0 +1,113 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import '@dxos-theme';
6
+
7
+ import { type StoryObj, type Meta } from '@storybook/react';
8
+ import React, { useEffect, useMemo } from 'react';
9
+
10
+ import { AST, type BaseObject, ImmutableSchema, type BaseSchema, type HasId } from '@dxos/echo-schema';
11
+ import { getAnnotation } from '@dxos/effect';
12
+ import { faker } from '@dxos/random';
13
+ import { create, makeRef, type ReactiveObject } from '@dxos/react-client/echo';
14
+ import { withClientProvider } from '@dxos/react-client/testing';
15
+ import { createView, ViewProjection, ViewType } from '@dxos/schema';
16
+ import { createGenerator, Testing, type ValueGenerator } from '@dxos/schema/testing';
17
+ import { withLayout, withTheme } from '@dxos/storybook-utils';
18
+
19
+ import { Table } from './Table';
20
+ import { useTableModel } from '../../hooks';
21
+ import { TablePresentation } from '../../model';
22
+ import translations from '../../translations';
23
+ import { TableType } from '../../types';
24
+
25
+ faker.seed(1);
26
+ const generator: ValueGenerator = faker as any;
27
+
28
+ // TODO(burdon): Many-to-many relations.
29
+ // TODO(burdon): Mutable and immutable views.
30
+ // TODO(burdon): Reconcile schemas types and utils (see API PR).
31
+ // TODO(burdon): Base type for T (with id); see ECHO API PR?
32
+ const useTestModel = <T extends BaseObject & HasId>(schema: BaseSchema<T>, count: number) => {
33
+ const table = useMemo(() => {
34
+ // const { typename } = pipe(schema.ast, AST.getAnnotation<TypeAnnotation>(TypeAnnotationId), Option.getOrThrow);
35
+ const typename = schema.typename;
36
+ const name = getAnnotation<string>(AST.TitleAnnotationId)(schema.ast) ?? typename;
37
+ const view = createView({ name, typename, jsonSchema: schema.jsonSchema });
38
+ return create(TableType, { view: makeRef(view) });
39
+ }, [schema]);
40
+
41
+ const projection = useMemo(() => {
42
+ if (!table.view?.target) {
43
+ return undefined;
44
+ }
45
+
46
+ // TODO(burdon): Just pass in view? Reuse same jsonSchema instance? View determines if mutable, etc.
47
+ return new ViewProjection(schema.jsonSchema, table.view.target);
48
+ }, [schema, table]);
49
+
50
+ const model = useTableModel({ table, projection, rows: [] });
51
+ useEffect(() => {
52
+ if (!model) {
53
+ return;
54
+ }
55
+
56
+ const objectGenerator = createGenerator(generator, schema, { optional: true });
57
+ const objects: ReactiveObject<T>[] = Array.from({ length: count }).map(() => objectGenerator.createObject());
58
+ model.setRows(objects);
59
+ }, [model]);
60
+
61
+ const presentation = useMemo(() => {
62
+ if (!model) {
63
+ return;
64
+ }
65
+
66
+ return new TablePresentation(model);
67
+ }, [model]);
68
+
69
+ return { model, presentation };
70
+ };
71
+
72
+ const DefaultStory = () => {
73
+ // TODO(burdon): Remove need for ImmutableSchema wrapper at API-level.
74
+ const orgSchema = useMemo(() => new ImmutableSchema(Testing.Org), []);
75
+ const { model: orgModel, presentation: orgPresentation } = useTestModel<Testing.Org>(orgSchema, 50);
76
+
77
+ // TODO(burdon): Generate links with references.
78
+ const contactSchema = useMemo(() => new ImmutableSchema(Testing.Contact), []);
79
+ const { model: contactModel, presentation: contactPresentation } = useTestModel<Testing.Contact>(contactSchema, 50);
80
+
81
+ // TODO(burdon): Scrolling isn't working.
82
+ return (
83
+ <div className='grow grid grid-cols-2 divide-x divide-separator'>
84
+ <Table.Root>
85
+ <Table.Main model={orgModel} presentation={orgPresentation} />
86
+ </Table.Root>
87
+ <Table.Root>
88
+ <Table.Main model={contactModel} presentation={contactPresentation} />
89
+ </Table.Root>
90
+ </div>
91
+ );
92
+ };
93
+
94
+ const meta: Meta<typeof DefaultStory> = {
95
+ title: 'ui/react-ui-table/relations',
96
+ render: DefaultStory,
97
+ parameters: { translations },
98
+ decorators: [
99
+ withClientProvider({
100
+ types: [TableType, ViewType, Testing.Org, Testing.Contact],
101
+ createIdentity: true,
102
+ createSpace: true,
103
+ }),
104
+ withTheme,
105
+ withLayout({ fullscreen: true, tooltips: true }),
106
+ ],
107
+ };
108
+
109
+ export default meta;
110
+
111
+ type Story = StoryObj<typeof DefaultStory>;
112
+
113
+ export const Default: Story = {};
@@ -7,7 +7,7 @@ import '@dxos-theme';
7
7
  import { type Meta, type StoryObj } from '@storybook/react';
8
8
  import React, { useEffect, useMemo, useState } from 'react';
9
9
 
10
- import { type EchoSchema } from '@dxos/echo-schema';
10
+ import { ImmutableSchema, type EchoSchema } from '@dxos/echo-schema';
11
11
  import { invariant } from '@dxos/invariant';
12
12
  import { faker } from '@dxos/random';
13
13
  import { Filter, useQuery, create } from '@dxos/react-client/echo';
@@ -18,6 +18,7 @@ import { withLayout, withTheme } from '@dxos/storybook-utils';
18
18
 
19
19
  import { TableCellEditor, type TableCellEditorProps } from './TableCellEditor';
20
20
  import { useTableModel } from '../../hooks';
21
+ import { type TableFeatures } from '../../model';
21
22
  import translations from '../../translations';
22
23
  import { TableType } from '../../types';
23
24
  import { initializeTable } from '../../util';
@@ -44,11 +45,20 @@ const DefaultStory = ({ editing }: StoryProps) => {
44
45
 
45
46
  const projection = useMemo(() => {
46
47
  if (schema && table?.view) {
47
- return new ViewProjection(schema, table.view.target!);
48
+ return new ViewProjection(schema.jsonSchema, table.view.target!);
48
49
  }
49
50
  }, [schema, table?.view]);
50
51
 
51
- const model = useTableModel({ table, projection });
52
+ const features: Partial<TableFeatures> = useMemo(
53
+ () => ({
54
+ selection: { enabled: true, mode: 'multiple' },
55
+ dataEditable: true,
56
+ schemaEditable: !(schema instanceof ImmutableSchema),
57
+ }),
58
+ [],
59
+ );
60
+
61
+ const model = useTableModel({ table, projection, features });
52
62
 
53
63
  const handleQuery: TableCellEditorProps['onQuery'] = async ({ field }) => {
54
64
  const { objects } = await space.db.query(schema).run();
@@ -84,9 +94,9 @@ const meta: Meta<StoryProps> = {
84
94
  types: [TableType, ViewType],
85
95
  createIdentity: true,
86
96
  createSpace: true,
87
- onSpaceCreated: async ({ space }) => {
97
+ onSpaceCreated: async ({ client, space }) => {
88
98
  const table = space.db.add(create(TableType, {}));
89
- const schema = await initializeTable({ space, table });
99
+ const schema = await initializeTable({ client, space, table });
90
100
  Array.from({ length: 10 }).forEach(() => {
91
101
  space.db.add(
92
102
  create(schema, {
@@ -8,6 +8,8 @@ import React, { useCallback, useMemo, useRef } from 'react';
8
8
  import { FormatEnum } from '@dxos/echo-schema';
9
9
  import { invariant } from '@dxos/invariant';
10
10
  import { type DxGrid } from '@dxos/lit-grid';
11
+ import { useThemeContext } from '@dxos/react-ui';
12
+ import { createMarkdownExtensions } from '@dxos/react-ui-editor';
11
13
  import {
12
14
  cellQuery,
13
15
  editorKeys,
@@ -21,6 +23,7 @@ import {
21
23
  type GridCellEditorProps,
22
24
  type GridScopedProps,
23
25
  } from '@dxos/react-ui-grid';
26
+ import { tagPickerExtension, createLinks } from '@dxos/react-ui-tag-picker';
24
27
  import { type FieldProjection } from '@dxos/schema';
25
28
 
26
29
  import { completion } from './extension';
@@ -55,6 +58,7 @@ export const TableCellEditor = ({
55
58
  }: GridScopedProps<TableCellEditorProps>) => {
56
59
  const { id: gridId, editing, setEditing } = useGridContext('TableCellEditor', __gridScope);
57
60
  const suppressNextBlur = useRef(false);
61
+ const { themeMode } = useThemeContext();
58
62
 
59
63
  const fieldProjection = useMemo<FieldProjection | undefined>(() => {
60
64
  if (!model || !editing) {
@@ -80,7 +84,7 @@ export const TableCellEditor = ({
80
84
  onFocus?.();
81
85
  setEditing(null);
82
86
  },
83
- [model, editing],
87
+ [model, editing, onEnter, onFocus, setEditing],
84
88
  );
85
89
 
86
90
  const handleBlur = useCallback<EditorBlurHandler>(
@@ -113,7 +117,7 @@ export const TableCellEditor = ({
113
117
  onFocus(determineNavigationAxis(event), determineNavigationDelta(event));
114
118
  }
115
119
  },
116
- [model, editing, onFocus, onEnter, fieldProjection, determineNavigationAxis, determineNavigationDelta],
120
+ [model, editing, onFocus, onEnter, fieldProjection],
117
121
  );
118
122
 
119
123
  const extension = useMemo(() => {
@@ -128,6 +132,50 @@ export const TableCellEditor = ({
128
132
  }),
129
133
  ];
130
134
 
135
+ const format = fieldProjection.props.format;
136
+
137
+ if (format === FormatEnum.SingleSelect || format === FormatEnum.MultiSelect) {
138
+ // TODO(ZaymonFC): Reconcile this with the TagPicker component?
139
+ // Add markdown extensions needed by tag picker.
140
+ extension.push(createMarkdownExtensions({ themeMode }));
141
+
142
+ const options = fieldProjection.props.options || [];
143
+
144
+ const mode = format === FormatEnum.SingleSelect ? ('single-select' as const) : ('multi-select' as const);
145
+
146
+ extension.push(
147
+ tagPickerExtension({
148
+ mode,
149
+ inGrid: true,
150
+ onSearch: (text, selectedIds) => {
151
+ return options
152
+ .filter(
153
+ (option) =>
154
+ selectedIds.indexOf(option.id) === -1 &&
155
+ (text.length === 0 || option.title.toLowerCase().includes(text.toLowerCase())),
156
+ )
157
+ .map((option) => ({
158
+ id: option.id,
159
+ label: option.title,
160
+ hue: option.color as any,
161
+ }));
162
+ },
163
+ onUpdate: (ids) => {
164
+ if (model && editing) {
165
+ if (ids.length === 0) {
166
+ return;
167
+ }
168
+ if (mode === 'single-select') {
169
+ handleEnter(ids[0]);
170
+ } else {
171
+ handleEnter(ids);
172
+ }
173
+ }
174
+ },
175
+ }),
176
+ );
177
+ }
178
+
131
179
  if (onQuery) {
132
180
  switch (fieldProjection.props.format) {
133
181
  case FormatEnum.Ref: {
@@ -164,11 +212,61 @@ export const TableCellEditor = ({
164
212
  }
165
213
 
166
214
  return extension;
167
- }, [model, modals, editing, fieldProjection, handleClose]);
215
+ }, [model, modals, editing, fieldProjection, handleClose, themeMode]);
168
216
 
169
217
  const getCellContent = useCallback<GridCellEditorProps['getCellContent']>(() => {
170
218
  if (model && editing) {
171
- const value = model.getCellData(parseCellIndex(editing.index));
219
+ const cell = parseCellIndex(editing.index);
220
+ const { col } = cell;
221
+ const field = model.projection.view.fields[col];
222
+ const fieldProjection = model.projection.getFieldProjection(field.id);
223
+
224
+ if (
225
+ fieldProjection?.props.format === FormatEnum.SingleSelect ||
226
+ fieldProjection?.props.format === FormatEnum.MultiSelect
227
+ ) {
228
+ const value = model.getCellData(cell);
229
+
230
+ if (value !== undefined) {
231
+ const options = fieldProjection.props.options || [];
232
+
233
+ if (fieldProjection.props.format === FormatEnum.MultiSelect && Array.isArray(value)) {
234
+ const tagItems = value
235
+ .map((id) => {
236
+ const option = options.find((o) => o.id === id);
237
+ if (option) {
238
+ return {
239
+ id,
240
+ label: option.title,
241
+ hue: option.color as any,
242
+ };
243
+ }
244
+ return undefined;
245
+ })
246
+ .filter((item): item is { id: any; label: string; hue: any } => item !== undefined);
247
+
248
+ return createLinks(tagItems);
249
+ } else {
250
+ const option = options.find((o) => o.id === value);
251
+
252
+ if (option) {
253
+ const tagItem = {
254
+ id: value,
255
+ label: option.title,
256
+ hue: option.color as any,
257
+ };
258
+
259
+ return createLinks([tagItem]);
260
+ }
261
+ }
262
+ }
263
+
264
+ // Return empty string if no value
265
+ return '';
266
+ }
267
+
268
+ // Default behavior for other formats
269
+ const value = model.getCellData(cell);
172
270
  return value !== undefined ? String(value) : '';
173
271
  }
174
272
  }, [model, editing]);
@@ -8,7 +8,6 @@ import { setValue, toJsonSchema, S, TypeEnum, TypedObject, FormatEnum } from '@d
8
8
  import { faker } from '@dxos/random';
9
9
  import { create, makeRef } from '@dxos/react-client/echo';
10
10
  import { createView, type ViewProjection } from '@dxos/schema';
11
- import {} from '@dxos/schema';
12
11
 
13
12
  import { TableType } from '../types';
14
13
 
@@ -5,24 +5,34 @@
5
5
  import { effect } from '@preact/signals-core';
6
6
  import { useEffect, useState } from 'react';
7
7
 
8
- import { type ReactiveObject } from '@dxos/react-client/echo';
8
+ import { fullyQualifiedId, getSpace, type ReactiveObject } from '@dxos/react-client/echo';
9
9
  import { useSelectionActions } from '@dxos/react-ui-attention';
10
10
  import { type ViewProjection } from '@dxos/schema';
11
11
  import { isNonNullable } from '@dxos/util';
12
12
 
13
- import { type BaseTableRow, TableModel, type TableModelProps } from '../model';
13
+ import { type TableRow, TableModel, type TableModelProps, type TableRowAction } from '../model';
14
14
  import { type TableType } from '../types';
15
15
 
16
- export type UseTableModelParams<T extends BaseTableRow = { id: string }> = {
16
+ export type UseTableModelParams<T extends TableRow = TableRow> = {
17
17
  table?: TableType;
18
18
  projection?: ViewProjection;
19
- objects?: ReactiveObject<T>[];
20
- } & Pick<TableModelProps<T>, 'onInsertRow' | 'onDeleteRows' | 'onDeleteColumn' | 'onCellUpdate' | 'onRowOrderChanged'>;
19
+ rows?: ReactiveObject<T>[];
20
+ rowActions?: TableRowAction[];
21
+ onSelectionChanged?: (selection: string[]) => void;
22
+ onRowAction?: (actionId: string, data: T) => void;
23
+ } & Pick<
24
+ TableModelProps<T>,
25
+ 'features' | 'onInsertRow' | 'onDeleteRows' | 'onDeleteColumn' | 'onCellUpdate' | 'onRowOrderChange'
26
+ >;
21
27
 
22
- export const useTableModel = <T extends BaseTableRow = { id: string }>({
23
- objects,
28
+ export const useTableModel = <T extends TableRow = TableRow>({
24
29
  table,
25
30
  projection,
31
+ rows,
32
+ rowActions,
33
+ features,
34
+ onSelectionChanged,
35
+ onRowAction,
26
36
  ...props
27
37
  }: UseTableModelParams<T>): TableModel<T> | undefined => {
28
38
  const [model, setModel] = useState<TableModel<T>>();
@@ -33,7 +43,16 @@ export const useTableModel = <T extends BaseTableRow = { id: string }>({
33
43
 
34
44
  let model: TableModel<T> | undefined;
35
45
  const t = setTimeout(async () => {
36
- model = new TableModel<T>({ table, projection, ...props });
46
+ model = new TableModel<T>({
47
+ id: fullyQualifiedId(table),
48
+ space: getSpace(table),
49
+ view: table.view?.target,
50
+ projection,
51
+ features,
52
+ rowActions,
53
+ onRowAction,
54
+ ...props,
55
+ });
37
56
  await model.open();
38
57
  setModel(model);
39
58
  });
@@ -42,14 +61,14 @@ export const useTableModel = <T extends BaseTableRow = { id: string }>({
42
61
  clearTimeout(t);
43
62
  void model?.close();
44
63
  };
45
- }, [table, projection]); // TODO(burdon): Trigger if callbacks change?
64
+ }, [table, projection, table?.view?.target, features, rowActions]); // TODO(burdon): Trigger if callbacks change?
46
65
 
47
66
  // Update data.
48
67
  useEffect(() => {
49
- if (objects) {
50
- model?.setRows(objects);
68
+ if (rows) {
69
+ model?.setRows(rows);
51
70
  }
52
- }, [model, objects]);
71
+ }, [model, rows]);
53
72
 
54
73
  const { select, clear } = useSelectionActions([table?.id, table?.view?.target?.query.typename].filter(isNonNullable));
55
74
 
@@ -59,7 +78,9 @@ export const useTableModel = <T extends BaseTableRow = { id: string }>({
59
78
  }
60
79
 
61
80
  const unsubscribe = effect(() => {
62
- select([...model.selection.selection.value]);
81
+ const selectedItems = [...model.selection.selection.value];
82
+ select(selectedItems);
83
+ onSelectionChanged?.(selectedItems);
63
84
  });
64
85
 
65
86
  // Maybe clear the selection here?
@@ -67,7 +88,7 @@ export const useTableModel = <T extends BaseTableRow = { id: string }>({
67
88
  clear();
68
89
  unsubscribe();
69
90
  };
70
- }, [model]);
91
+ }, [model, onSelectionChanged]);
71
92
 
72
93
  return model;
73
94
  };
@@ -5,3 +5,4 @@
5
5
  export * from './table-model';
6
6
  export * from './modal-controller';
7
7
  export * from './table-presentation';
8
+ export type { SelectionMode } from './selection-model';
@@ -6,10 +6,12 @@ import { computed, effect, signal, type ReadonlySignal } from '@preact/signals-c
6
6
 
7
7
  import { Resource } from '@dxos/context';
8
8
 
9
- import { type BaseTableRow } from './table-model';
9
+ import { type TableRow } from './table-model';
10
10
  import { touch } from '../util';
11
11
 
12
- export class SelectionModel<T extends BaseTableRow> extends Resource {
12
+ export type SelectionMode = 'single' | 'multiple';
13
+
14
+ export class SelectionModel<T extends TableRow> extends Resource {
13
15
  private readonly _selection = signal<Set<string>>(new Set());
14
16
 
15
17
  private readonly _validSelectedIds = computed<Set<string>>(() => {
@@ -35,6 +37,7 @@ export class SelectionModel<T extends BaseTableRow> extends Resource {
35
37
 
36
38
  constructor(
37
39
  private readonly _rows: ReadonlySignal<T[]>,
40
+ private readonly _selectionMode: SelectionMode,
38
41
  private readonly _onSelectionChanged?: () => void,
39
42
  ) {
40
43
  super();
@@ -64,6 +67,10 @@ export class SelectionModel<T extends BaseTableRow> extends Resource {
64
67
  return this._allSelected;
65
68
  }
66
69
 
70
+ public get selectionMode(): SelectionMode {
71
+ return this._selectionMode;
72
+ }
73
+
67
74
  public getSelectedRows = (): T[] => {
68
75
  const selectedIds = this._validSelectedIds.value;
69
76
  return this._rows.value.filter((row) => selectedIds.has(row.id));
@@ -89,6 +96,9 @@ export class SelectionModel<T extends BaseTableRow> extends Resource {
89
96
  if (newSelection.has(row.id)) {
90
97
  newSelection.delete(row.id);
91
98
  } else {
99
+ if (this._selectionMode === 'single') {
100
+ newSelection.clear();
101
+ }
92
102
  newSelection.add(row.id);
93
103
  }
94
104
 
@@ -106,7 +106,7 @@ class Test extends TypedObject({ typename: 'example.com/type/Test', version: '0.
106
106
  const createTableModel = (props: Partial<TableModelProps> = {}): TableModel => {
107
107
  const schema = createEchoSchema(Test);
108
108
  const view = createView({ name: 'Test', typename: schema.typename, jsonSchema: schema.jsonSchema });
109
- const projection = new ViewProjection(schema, view);
109
+ const projection = new ViewProjection(schema.jsonSchema, view);
110
110
  const table = create(TableType, { view: makeRef(view) });
111
- return new TableModel({ table, projection, ...props });
111
+ return new TableModel({ id: table.id, space: undefined, view, projection, ...props });
112
112
  };