@dxos/react-ui-table 0.8.4-main.ead640a → 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 (169) 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-KC62K4WS.mjs +1696 -0
  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 +1128 -1044
  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-62FOU4M7.mjs +1698 -0
  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 +1128 -1044
  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 -81
  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 +104 -25
  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 -78
  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 +116 -82
  114. package/src/components/Table/RowActionsMenu.tsx +27 -17
  115. package/src/components/Table/Table.stories.tsx +129 -116
  116. package/src/components/Table/Table.tsx +43 -407
  117. package/src/components/Table/TableContent.tsx +426 -0
  118. package/src/components/Table/TableToolbar.tsx +111 -0
  119. package/src/components/TableCellEditor/CellValidationMessage.tsx +5 -6
  120. package/src/components/TableCellEditor/FormCellEditor.tsx +119 -135
  121. package/src/components/TableCellEditor/TableCellEditor.stories.tsx +29 -22
  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 +437 -171
  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 +59 -13
  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 +49 -35
  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-4K7JPALT.mjs +0 -66
  151. package/dist/lib/browser/chunk-4K7JPALT.mjs.map +0 -7
  152. package/dist/lib/browser/chunk-OZTXLZGM.mjs +0 -1804
  153. package/dist/lib/browser/chunk-OZTXLZGM.mjs.map +0 -7
  154. package/dist/lib/node-esm/chunk-MK6OVUF6.mjs +0 -67
  155. package/dist/lib/node-esm/chunk-MK6OVUF6.mjs.map +0 -7
  156. package/dist/lib/node-esm/chunk-S56Z4V4X.mjs +0 -1805
  157. package/dist/lib/node-esm/chunk-S56Z4V4X.mjs.map +0 -7
  158. package/dist/types/src/components/TableToolbar/TableToolbar.d.ts +0 -21
  159. package/dist/types/src/components/TableToolbar/TableToolbar.d.ts.map +0 -1
  160. package/dist/types/src/components/TableToolbar/index.d.ts +0 -2
  161. package/dist/types/src/components/TableToolbar/index.d.ts.map +0 -1
  162. package/dist/types/src/model/table-sorting.d.ts +0 -52
  163. package/dist/types/src/model/table-sorting.d.ts.map +0 -1
  164. package/dist/types/src/types/schema.d.ts +0 -28
  165. package/dist/types/src/types/schema.d.ts.map +0 -1
  166. package/src/components/TableToolbar/TableToolbar.tsx +0 -102
  167. package/src/components/TableToolbar/index.ts +0 -5
  168. package/src/model/table-sorting.ts +0 -225
  169. package/src/types/schema.ts +0 -73
@@ -2,43 +2,43 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
+ import { RegistryContext } from '@effect-atom/atom-react';
5
6
  import { type Meta, type StoryObj } from '@storybook/react-vite';
6
- import type * as Schema from 'effect/Schema';
7
- import React, { useCallback, useEffect, useMemo, useState } from 'react';
8
- import { expect, userEvent, within } from 'storybook/test';
7
+ import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react';
8
+ import { expect, userEvent, waitFor, within } from 'storybook/test';
9
9
 
10
- import { Obj, Type } from '@dxos/echo';
11
- import { type JsonSchemaType } from '@dxos/echo/internal';
10
+ import { Obj, Type, View } from '@dxos/echo';
11
+ import { invariant } from '@dxos/invariant';
12
12
  import { type DxGrid } from '@dxos/lit-grid';
13
- import '@dxos/lit-ui/dx-tag-picker.pcss';
14
- import { faker } from '@dxos/random';
15
- import { useClient } from '@dxos/react-client';
16
- import { useClientProvider, withClientProvider } from '@dxos/react-client/testing';
13
+ import { random } from '@dxos/random';
14
+ import { useClientStory, withClientProvider } from '@dxos/react-client/testing';
17
15
  import { useAsyncEffect } from '@dxos/react-ui';
18
- import { withTheme } from '@dxos/react-ui/testing';
19
- import { translations as formTranslations } from '@dxos/react-ui-form';
20
- import { DataType } from '@dxos/schema';
16
+ import { translations as formTranslations } from '@dxos/react-ui-form/translations';
17
+ import { withLayout, withTheme } from '@dxos/react-ui/testing';
18
+ import { ViewModel } from '@dxos/schema';
21
19
  import { type ValueGenerator, createAsyncGenerator } from '@dxos/schema/testing';
20
+ import { withRegistry } from '@dxos/storybook-utils';
21
+ import { Organization, Person } from '@dxos/types';
22
+ import '@dxos/lit-ui/dx-tag-picker.pcss';
22
23
 
23
- import { useTableModel } from '../../hooks';
24
+ import { translations } from '#translations';
25
+
26
+ import { useProjectionModel, useTableModel } from '../../hooks';
24
27
  import { type TableFeatures, TablePresentation, type TableRow } from '../../model';
25
- import { translations } from '../../translations';
26
28
  import { Table } from '../../types';
27
-
28
29
  import { Table as TableComponent } from './Table';
29
30
 
30
- faker.seed(1);
31
- const generator: ValueGenerator = faker as any;
31
+ random.seed(1);
32
+ const generator: ValueGenerator = random as any;
32
33
 
33
34
  // TODO(burdon): Many-to-many relations.
34
35
  // TODO(burdon): Mutable and immutable views.
35
36
  // TODO(burdon): Reconcile schemas types and utils (see API PR).
36
37
  // TODO(burdon): Base type for T (with id); see ECHO API PR?
37
- const useTestModel = <S extends Type.Obj.Any>(schema: S, count: number) => {
38
- const client = useClient();
39
- const { space } = useClientProvider();
40
- const [view, setView] = useState<DataType.View>();
41
- const [jsonSchema, setJsonSchema] = useState<JsonSchemaType>();
38
+ const useTestModel = <S extends Type.AnyObj>(schema: S, count: number) => {
39
+ const registry = useContext(RegistryContext);
40
+ const { space } = useClientStory();
41
+ const [object, setObject] = useState<Table.Table>();
42
42
 
43
43
  const features = useMemo<TableFeatures>(
44
44
  () => ({ schemaEditable: false, dataEditable: true, selection: { enabled: false } }),
@@ -50,18 +50,14 @@ const useTestModel = <S extends Type.Obj.Any>(schema: S, count: number) => {
50
50
  return;
51
51
  }
52
52
 
53
- const {
54
- jsonSchema,
55
- schema: effectSchema,
56
- view,
57
- } = await Table.makeView({ client, space, typename: Type.getTypename(schema) });
58
- setJsonSchema(jsonSchema);
59
- setView(view);
60
- space.db.add(view);
61
- await space.db.schemaRegistry.register([effectSchema]);
62
- }, [client, space, schema]);
53
+ const { view, jsonSchema } = await ViewModel.makeFromDatabase({ db: space.db, typename: Type.getTypename(schema) });
54
+ const object = Table.make({ view, jsonSchema });
55
+ setObject(object);
56
+ space.db.add(object);
57
+ }, [space, schema]);
63
58
 
64
- const model = useTableModel<TableRow>({ view, schema: jsonSchema, rows: [], features });
59
+ const projection = useProjectionModel(schema, object, registry);
60
+ const model = useTableModel<TableRow>({ object, projection, db: space?.db, features });
65
61
 
66
62
  useEffect(() => {
67
63
  if (!model || !space) {
@@ -70,7 +66,9 @@ const useTestModel = <S extends Type.Obj.Any>(schema: S, count: number) => {
70
66
 
71
67
  const objectGenerator = createAsyncGenerator(generator, schema, { db: space?.db, force: true });
72
68
  void objectGenerator.createObjects(count).then((objects) => {
73
- model.setRows(objects);
69
+ // TODO(wittjosiah): Remove cast. Type.InstanceType is now strict (no index signature),
70
+ // so it isn't assignable to TableRow (Record<JsonProp, any>); reconcile the row model type.
71
+ model.setRows(objects as unknown as TableRow[]);
74
72
  });
75
73
  }, [model, space]);
76
74
 
@@ -79,45 +77,44 @@ const useTestModel = <S extends Type.Obj.Any>(schema: S, count: number) => {
79
77
  return;
80
78
  }
81
79
 
82
- return new TablePresentation(model);
83
- }, [model]);
80
+ return new TablePresentation(registry, model);
81
+ }, [registry, model]);
84
82
 
85
83
  return { model, presentation };
86
84
  };
87
85
 
88
86
  const DefaultStory = () => {
89
- const client = useClient();
90
- const { model: orgModel, presentation: orgPresentation } = useTestModel(DataType.Organization, 50);
91
- const { model: contactModel, presentation: contactPresentation } = useTestModel(DataType.Person, 50);
92
- const { space } = useClientProvider();
87
+ const { model: orgModel, presentation: orgPresentation } = useTestModel(Organization.Organization, 50);
88
+ const { model: contactModel, presentation: contactPresentation } = useTestModel(Person.Person, 50);
89
+ const { space } = useClientStory();
93
90
 
94
91
  const handleCreate = useCallback(
95
- (schema: Schema.Schema.AnyNoContext, values: any) => {
96
- return client.spaces.default.db.add(Obj.make(schema, values));
92
+ (schema: Type.AnyEntity, values: any) => {
93
+ invariant(Type.isObject(schema));
94
+ invariant(space);
95
+ return space.db.add(Obj.make(schema, values));
97
96
  },
98
97
  [space],
99
98
  );
100
99
 
101
100
  return (
102
- <div className='is-full bs-full grid grid-cols-2 divide-x divide-separator'>
101
+ <div className='dx-expander grid grid-cols-2 divide-x divide-separator'>
103
102
  <TableComponent.Root>
104
- <TableComponent.Main
103
+ <TableComponent.Content
105
104
  model={orgModel}
106
- schema={DataType.Organization}
105
+ schema={Organization.Organization}
107
106
  presentation={orgPresentation}
108
107
  onCreate={handleCreate}
109
- client={client}
110
108
  ignoreAttention
111
109
  testId='relations-0'
112
110
  />
113
111
  </TableComponent.Root>
114
112
  <TableComponent.Root>
115
- <TableComponent.Main
113
+ <TableComponent.Content
116
114
  model={contactModel}
117
- schema={DataType.Person}
115
+ schema={Person.Person}
118
116
  presentation={contactPresentation}
119
117
  onCreate={handleCreate}
120
- client={client}
121
118
  ignoreAttention
122
119
  testId='relations-1'
123
120
  />
@@ -130,9 +127,12 @@ const meta = {
130
127
  title: 'ui/react-ui-table/Relations',
131
128
  render: DefaultStory,
132
129
  decorators: [
133
- withTheme,
130
+ withTheme(),
131
+ withRegistry,
132
+ // TODO(thure): Shouldn't `layout: 'fullscreen'` below make this unnecessary?
133
+ withLayout({ classNames: 'fixed inset-0' }),
134
134
  withClientProvider({
135
- types: [DataType.View, DataType.Organization, DataType.Person, Table.Table],
135
+ types: [View.View, Organization.Organization, Person.Person, Table.Table],
136
136
  createIdentity: true,
137
137
  createSpace: true,
138
138
  }),
@@ -149,9 +149,19 @@ export default meta;
149
149
  type Story = StoryObj<typeof meta>;
150
150
 
151
151
  export const Default: Story = {
152
- // TODO(wittjosiah): This test is flaky, especially in CI.
153
- // Most typical failure is `targetCell` returns `null`.
154
- _play: async ({ canvasElement }: any) => {
152
+ // The play function exercises a deep timing race in dx-grid + React render
153
+ // pipeline (cell focus → keydown → `dispatchEditRequest` → `setEditing` →
154
+ // `FormCellEditor` `useEffect` → `Popover` mount). On chromium browser
155
+ // tests the combobox sometimes fails to render in time; local pass rate
156
+ // ~25%, which makes pure CI re-run lottery a poor fit. Excluding from
157
+ // automated test runs (the story still renders in Storybook for visual
158
+ // review and the production code path is covered by
159
+ // `playwright/smoke.spec.ts`). Re-enable once the underlying race is
160
+ // fixed — likely needs `dispatchEditRequest` to be synchronous from
161
+ // keydown and `FormCellEditor` to render the popover off `contextEditing`
162
+ // directly instead of mirroring it through local state.
163
+ tags: ['!test'],
164
+ play: async ({ canvasElement }) => {
155
165
  const canvas = within(canvasElement);
156
166
  const body = within(document.body);
157
167
 
@@ -162,22 +172,42 @@ export const Default: Story = {
162
172
  // Focus on the second table (Person/contacts table)
163
173
  await expect(secondGrid).toBeVisible();
164
174
 
175
+ // Wait for both grids' data generators to finish populating before
176
+ // interacting. `findByTestId` returns as soon as the cell DOM exists,
177
+ // but dx-grid's internal cell registry (used by `cellReadonly` and the
178
+ // edit-request dispatcher) is filled in lazily. If `keydown` lands
179
+ // before the registry has the row, `dispatchEditRequest` short-circuits
180
+ // and the combobox never opens — that's the "Unable to find combobox"
181
+ // flake. Wait for the first row of each grid to render non-empty
182
+ // content (a reliable proxy for "registry populated") BEFORE scrolling
183
+ // — once we scroll the second grid to column 4, dx-grid virtualizes
184
+ // columns 0-3 out of the DOM, so `grid.0.0` becomes unreachable.
185
+ await waitFor(
186
+ async () => {
187
+ const firstOrgCell = within(firstGrid).getByTestId('grid.0.0');
188
+ const orgText = (firstOrgCell.querySelector('.dx-grid__cell__content') as HTMLElement | null)?.textContent;
189
+ await expect(orgText).toBeTruthy();
190
+ const firstContactCell = within(secondGrid).getByTestId('grid.0.0');
191
+ const contactText = (firstContactCell.querySelector('.dx-grid__cell__content') as HTMLElement | null)
192
+ ?.textContent;
193
+ await expect(contactText).toBeTruthy();
194
+ },
195
+ { timeout: 10_000 },
196
+ );
197
+
165
198
  // Scroll to the relations column (column 4) - this mimics the scrollToColumn call
166
199
  (secondGrid.closest('dx-grid') as DxGrid).scrollToColumn(4);
167
200
 
168
- // Wait for scroll to complete
169
- await new Promise((resolve) => setTimeout(resolve, 1000));
170
-
171
- // Find and click the target cell (first row, relations column)
172
- const targetCell = secondGrid.querySelector('[data-dx-grid-plane="grid"] [aria-rowindex="0"][aria-colindex="4"]');
201
+ // Find the target cell (first row, relations column).
202
+ const targetCell = await within(secondGrid).findByTestId('grid.4.0', undefined, { timeout: 10_000 });
173
203
  await expect(targetCell).toBeVisible();
174
204
 
175
- // Click to focus the cell
205
+ // Click to focus the cell.
176
206
  await userEvent.click(targetCell as HTMLElement);
177
207
  await expect(targetCell).toHaveFocus();
178
208
  await userEvent.keyboard('{Enter}');
179
209
 
180
- // Look for the combobox that should appear in edit mode
210
+ // Look for the combobox that should appear in edit mode.
181
211
  const combobox = await body.findByRole('combobox', undefined, { timeout: 5000 });
182
212
  await userEvent.click(combobox);
183
213
 
@@ -185,11 +215,9 @@ export const Default: Story = {
185
215
  await userEvent.click(searchField);
186
216
 
187
217
  // Get the first organization name from the first table to search for
188
- const orgCell = firstGrid.querySelector(
189
- '[data-dx-grid-plane="grid"] [aria-rowindex="0"][aria-colindex="0"] .dx-grid__cell__content',
190
- ) as HTMLElement;
218
+ const orgCell = await within(firstGrid).findByTestId('grid.0.0', undefined, { timeout: 10_000 });
191
219
 
192
- const orgName = orgCell.textContent;
220
+ const orgName = (orgCell.querySelector('.dx-grid__cell__content') as HTMLElement).textContent;
193
221
  // Type the first 4 characters to search
194
222
  await userEvent.type(searchField, orgName.substring(0, 4));
195
223
 
@@ -204,14 +232,19 @@ export const Default: Story = {
204
232
  const saveButton = await body.findByTestId('save-button');
205
233
  await userEvent.click(saveButton);
206
234
 
207
- // Verify the relation was set (cell should now contain the org name)
208
- await expect(targetCell).toHaveTextContent(orgName.substring(0, 4));
235
+ // Verify the relation was set (cell should now contain the org name).
236
+ // Wait for the grid (Lit web component) to repaint after the reactive data update.
237
+ await waitFor(
238
+ async () => {
239
+ const updatedCell = within(secondGrid).getByTestId('grid.4.0');
240
+ await expect(updatedCell).toHaveTextContent(orgName.substring(0, 4));
241
+ },
242
+ { timeout: 5000 },
243
+ );
209
244
 
210
245
  // Test object creation (new relations) - equivalent to "new relations work as expected" test
211
246
  // Find a different cell to test object creation (second row, relations column)
212
- const newTargetCell = secondGrid.querySelector(
213
- '[data-dx-grid-plane="grid"] [aria-rowindex="1"][aria-colindex="4"]',
214
- );
247
+ const newTargetCell = await within(secondGrid).findByTestId('grid.4.1', undefined, { timeout: 10_000 });
215
248
  await expect(newTargetCell).toBeTruthy();
216
249
 
217
250
  // Click to focus the cell
@@ -226,23 +259,24 @@ export const Default: Story = {
226
259
  const newSearchField = await body.findByPlaceholderText('Search…');
227
260
  await userEvent.click(newSearchField);
228
261
 
229
- // Type a new object name (this will create a new object)
262
+ // Type a new object name (this will create a new object).
230
263
  const newOrgName = 'Salieri LLC';
231
264
  await userEvent.type(newSearchField, newOrgName);
232
265
 
233
- // Look for an option to select (should be the create new option)
234
- const newOption = await body.findAllByRole('option');
235
- await expect(newOption[0]).toBeVisible();
236
-
237
- // Press Enter to select/create
238
- await userEvent.keyboard('{Enter}');
266
+ // Click the create option directly to open the create form.
267
+ const createOptionLabel = await body.findByText(/Create new object/, undefined, { timeout: 2000 });
268
+ await userEvent.click(createOptionLabel.closest('[role="option"]') as HTMLElement);
239
269
 
240
- // Look for and click save button
241
- const createReferencedObjectForm = await body.findByTestId('create-referenced-object-form');
270
+ // Look for and click save button.
271
+ const createReferencedObjectForm = await body.findByTestId('create-referenced-object-form', undefined, {
272
+ timeout: 2000,
273
+ });
242
274
  const saveObjectButton = await within(createReferencedObjectForm).findByTestId('save-button');
275
+ await expect(saveObjectButton).not.toBeDisabled();
243
276
  await userEvent.click(saveObjectButton);
244
277
 
245
- // Verify the new object was created and relation was set
246
- await expect(newTargetCell).toHaveTextContent(newOrgName);
278
+ // Verify the relation was set by checking for the accessory link anchor in the cell.
279
+ const updatedNewCell = await within(secondGrid).findByTestId('grid.4.1', undefined, { timeout: 5000 });
280
+ await expect(updatedNewCell.querySelector('dx-anchor')).toBeTruthy();
247
281
  },
248
- } as any;
282
+ };
@@ -2,19 +2,21 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
+ import { useAtomValue } from '@effect-atom/atom-react';
5
6
  import React from 'react';
6
7
 
7
8
  import { DropdownMenu, toLocalizedString, useTranslation } from '@dxos/react-ui';
8
9
 
10
+ import { translationKey } from '#translations';
11
+
9
12
  import { type ModalController, type TableModel } from '../../model';
10
- import { translationKey } from '../../translations';
11
13
 
12
14
  type RowActionsMenuProps = { model: TableModel; modals: ModalController };
13
15
 
14
16
  export const RowActionsMenu = ({ model, modals }: RowActionsMenuProps) => {
15
17
  const { t } = useTranslation(translationKey);
16
- const hasSelection = model.selection.hasSelection.value;
17
- const state = modals.state.value;
18
+ const hasSelection = model.selection.hasSelection;
19
+ const state = useAtomValue(modals.state);
18
20
  if (state?.type !== 'row') {
19
21
  return null;
20
22
  }
@@ -23,24 +25,32 @@ export const RowActionsMenu = ({ model, modals }: RowActionsMenuProps) => {
23
25
  <DropdownMenu.VirtualTrigger virtualRef={modals.trigger} />
24
26
  <DropdownMenu.Content>
25
27
  <DropdownMenu.Viewport>
28
+ {/* Custom actions */}
29
+ {model.rowActions?.length > 0 && (
30
+ <>
31
+ <DropdownMenu.Group>
32
+ {model.rowActions?.map((action) => (
33
+ <DropdownMenu.Item
34
+ key={action.id}
35
+ data-testid={`row-action-${action.id}`}
36
+ onClick={() => {
37
+ modals.close();
38
+ model.handleRowAction(action.id, state.rowIndex);
39
+ }}
40
+ >
41
+ {toLocalizedString(action.label, t)}
42
+ </DropdownMenu.Item>
43
+ ))}
44
+ </DropdownMenu.Group>
45
+ <DropdownMenu.Separator />
46
+ </>
47
+ )}
48
+ {/* Default actions */}
26
49
  {model.features.dataEditable !== false && (
27
50
  <DropdownMenu.Item data-testid='row-menu-delete' onClick={() => model.deleteRow(state.rowIndex)}>
28
- {t(hasSelection ? 'bulk delete row label' : 'delete row label')}
51
+ {t(hasSelection ? 'bulk-delete-row.label' : 'delete-row.label')}
29
52
  </DropdownMenu.Item>
30
53
  )}
31
- {/* Custom actions */}
32
- {model.rowActions?.map((action) => (
33
- <DropdownMenu.Item
34
- key={action.id}
35
- data-testid={`row-action-${action.id}`}
36
- onClick={() => {
37
- modals.close();
38
- model.handleRowAction(action.id, state.rowIndex);
39
- }}
40
- >
41
- {toLocalizedString(action.label, t)}
42
- </DropdownMenu.Item>
43
- ))}
44
54
  </DropdownMenu.Viewport>
45
55
  <DropdownMenu.Arrow />
46
56
  </DropdownMenu.Content>