@dxos/react-ui-table 0.8.4-main.c4373fc → 0.8.4-main.d05539e30a

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-4URYOFDL.mjs +69 -0
  3. package/dist/lib/browser/chunk-4URYOFDL.mjs.map +7 -0
  4. package/dist/lib/browser/chunk-J5LGTIGS.mjs +10 -0
  5. package/dist/lib/browser/chunk-J5LGTIGS.mjs.map +7 -0
  6. package/dist/lib/browser/chunk-YDXPC7HA.mjs +1695 -0
  7. package/dist/lib/browser/chunk-YDXPC7HA.mjs.map +7 -0
  8. package/dist/lib/browser/index.mjs +1136 -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 -45
  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-2AN3QQ3D.mjs +1697 -0
  17. package/dist/lib/node-esm/chunk-2AN3QQ3D.mjs.map +7 -0
  18. package/dist/lib/node-esm/chunk-B2SZYAEC.mjs +70 -0
  19. package/dist/lib/node-esm/chunk-B2SZYAEC.mjs.map +7 -0
  20. package/dist/lib/node-esm/chunk-HSLMI22Q.mjs +11 -0
  21. package/dist/lib/node-esm/chunk-HSLMI22Q.mjs.map +7 -0
  22. package/dist/lib/node-esm/index.mjs +1136 -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 -45
  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 +21 -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 +84 -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 +92 -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 +25 -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 +67 -76
  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 +30 -30
  112. package/src/components/Table/DynamicTable.tsx +31 -32
  113. package/src/components/Table/Relations.stories.tsx +113 -81
  114. package/src/components/Table/RowActionsMenu.tsx +27 -17
  115. package/src/components/Table/Table.stories.tsx +125 -110
  116. package/src/components/Table/Table.tsx +43 -407
  117. package/src/components/Table/TableContent.tsx +425 -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 +26 -22
  122. package/src/components/TableCellEditor/TableCellEditor.tsx +35 -33
  123. package/src/components/index.ts +0 -1
  124. package/src/components/testing.ts +43 -17
  125. package/src/hooks/index.ts +2 -1
  126. package/src/hooks/useAddRow.ts +8 -10
  127. package/src/hooks/useProjectionModel.ts +39 -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 +82 -55
  133. package/src/model/table-model.ts +435 -170
  134. package/src/model/table-presentation.test.ts +59 -50
  135. package/src/model/table-presentation.ts +43 -39
  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 -42
  140. package/src/translations.ts +20 -22
  141. package/src/types/Table.ts +78 -0
  142. package/src/types/index.ts +1 -1
  143. package/src/util/dynamic-table.test.ts +70 -0
  144. package/src/util/dynamic-table.ts +40 -28
  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) {
@@ -79,45 +75,44 @@ const useTestModel = <S extends Type.Obj.Any>(schema: S, count: number) => {
79
75
  return;
80
76
  }
81
77
 
82
- return new TablePresentation(model);
83
- }, [model]);
78
+ return new TablePresentation(registry, model);
79
+ }, [registry, model]);
84
80
 
85
81
  return { model, presentation };
86
82
  };
87
83
 
88
84
  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();
85
+ const { model: orgModel, presentation: orgPresentation } = useTestModel(Organization.Organization, 50);
86
+ const { model: contactModel, presentation: contactPresentation } = useTestModel(Person.Person, 50);
87
+ const { space } = useClientStory();
93
88
 
94
89
  const handleCreate = useCallback(
95
- (schema: Schema.Schema.AnyNoContext, values: any) => {
96
- return client.spaces.default.db.add(Obj.make(schema, values));
90
+ (schema: Type.AnyEntity, values: any) => {
91
+ invariant(Type.isObjectSchema(schema));
92
+ invariant(space);
93
+ return space.db.add(Obj.make(schema, values));
97
94
  },
98
95
  [space],
99
96
  );
100
97
 
101
98
  return (
102
- <div className='is-full bs-full grid grid-cols-2 divide-x divide-separator'>
99
+ <div className='dx-expander grid grid-cols-2 divide-x divide-separator'>
103
100
  <TableComponent.Root>
104
- <TableComponent.Main
101
+ <TableComponent.Content
105
102
  model={orgModel}
106
- schema={DataType.Organization}
103
+ schema={Organization.Organization}
107
104
  presentation={orgPresentation}
108
105
  onCreate={handleCreate}
109
- client={client}
110
106
  ignoreAttention
111
107
  testId='relations-0'
112
108
  />
113
109
  </TableComponent.Root>
114
110
  <TableComponent.Root>
115
- <TableComponent.Main
111
+ <TableComponent.Content
116
112
  model={contactModel}
117
- schema={DataType.Person}
113
+ schema={Person.Person}
118
114
  presentation={contactPresentation}
119
115
  onCreate={handleCreate}
120
- client={client}
121
116
  ignoreAttention
122
117
  testId='relations-1'
123
118
  />
@@ -130,9 +125,12 @@ const meta = {
130
125
  title: 'ui/react-ui-table/Relations',
131
126
  render: DefaultStory,
132
127
  decorators: [
133
- withTheme,
128
+ withTheme(),
129
+ withRegistry,
130
+ // TODO(thure): Shouldn't `layout: 'fullscreen'` below make this unnecessary?
131
+ withLayout({ classNames: 'fixed inset-0' }),
134
132
  withClientProvider({
135
- types: [DataType.View, DataType.Organization, DataType.Person, Table.Table],
133
+ types: [View.View, Organization.Organization, Person.Person, Table.Table],
136
134
  createIdentity: true,
137
135
  createSpace: true,
138
136
  }),
@@ -149,9 +147,19 @@ export default meta;
149
147
  type Story = StoryObj<typeof meta>;
150
148
 
151
149
  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) => {
150
+ // The play function exercises a deep timing race in dx-grid + React render
151
+ // pipeline (cell focus → keydown → `dispatchEditRequest` → `setEditing` →
152
+ // `FormCellEditor` `useEffect` → `Popover` mount). On chromium browser
153
+ // tests the combobox sometimes fails to render in time; local pass rate
154
+ // ~25%, which makes pure CI re-run lottery a poor fit. Excluding from
155
+ // automated test runs (the story still renders in Storybook for visual
156
+ // review and the production code path is covered by
157
+ // `playwright/smoke.spec.ts`). Re-enable once the underlying race is
158
+ // fixed — likely needs `dispatchEditRequest` to be synchronous from
159
+ // keydown and `FormCellEditor` to render the popover off `contextEditing`
160
+ // directly instead of mirroring it through local state.
161
+ tags: ['!test'],
162
+ play: async ({ canvasElement }) => {
155
163
  const canvas = within(canvasElement);
156
164
  const body = within(document.body);
157
165
 
@@ -162,22 +170,42 @@ export const Default: Story = {
162
170
  // Focus on the second table (Person/contacts table)
163
171
  await expect(secondGrid).toBeVisible();
164
172
 
173
+ // Wait for both grids' data generators to finish populating before
174
+ // interacting. `findByTestId` returns as soon as the cell DOM exists,
175
+ // but dx-grid's internal cell registry (used by `cellReadonly` and the
176
+ // edit-request dispatcher) is filled in lazily. If `keydown` lands
177
+ // before the registry has the row, `dispatchEditRequest` short-circuits
178
+ // and the combobox never opens — that's the "Unable to find combobox"
179
+ // flake. Wait for the first row of each grid to render non-empty
180
+ // content (a reliable proxy for "registry populated") BEFORE scrolling
181
+ // — once we scroll the second grid to column 4, dx-grid virtualizes
182
+ // columns 0-3 out of the DOM, so `grid.0.0` becomes unreachable.
183
+ await waitFor(
184
+ async () => {
185
+ const firstOrgCell = within(firstGrid).getByTestId('grid.0.0');
186
+ const orgText = (firstOrgCell.querySelector('.dx-grid__cell__content') as HTMLElement | null)?.textContent;
187
+ await expect(orgText).toBeTruthy();
188
+ const firstContactCell = within(secondGrid).getByTestId('grid.0.0');
189
+ const contactText = (firstContactCell.querySelector('.dx-grid__cell__content') as HTMLElement | null)
190
+ ?.textContent;
191
+ await expect(contactText).toBeTruthy();
192
+ },
193
+ { timeout: 10_000 },
194
+ );
195
+
165
196
  // Scroll to the relations column (column 4) - this mimics the scrollToColumn call
166
197
  (secondGrid.closest('dx-grid') as DxGrid).scrollToColumn(4);
167
198
 
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"]');
199
+ // Find the target cell (first row, relations column).
200
+ const targetCell = await within(secondGrid).findByTestId('grid.4.0', undefined, { timeout: 10_000 });
173
201
  await expect(targetCell).toBeVisible();
174
202
 
175
- // Click to focus the cell
203
+ // Click to focus the cell.
176
204
  await userEvent.click(targetCell as HTMLElement);
177
205
  await expect(targetCell).toHaveFocus();
178
206
  await userEvent.keyboard('{Enter}');
179
207
 
180
- // Look for the combobox that should appear in edit mode
208
+ // Look for the combobox that should appear in edit mode.
181
209
  const combobox = await body.findByRole('combobox', undefined, { timeout: 5000 });
182
210
  await userEvent.click(combobox);
183
211
 
@@ -185,11 +213,9 @@ export const Default: Story = {
185
213
  await userEvent.click(searchField);
186
214
 
187
215
  // 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;
216
+ const orgCell = await within(firstGrid).findByTestId('grid.0.0', undefined, { timeout: 10_000 });
191
217
 
192
- const orgName = orgCell.textContent;
218
+ const orgName = (orgCell.querySelector('.dx-grid__cell__content') as HTMLElement).textContent;
193
219
  // Type the first 4 characters to search
194
220
  await userEvent.type(searchField, orgName.substring(0, 4));
195
221
 
@@ -204,14 +230,19 @@ export const Default: Story = {
204
230
  const saveButton = await body.findByTestId('save-button');
205
231
  await userEvent.click(saveButton);
206
232
 
207
- // Verify the relation was set (cell should now contain the org name)
208
- await expect(targetCell).toHaveTextContent(orgName.substring(0, 4));
233
+ // Verify the relation was set (cell should now contain the org name).
234
+ // Wait for the grid (Lit web component) to repaint after the reactive data update.
235
+ await waitFor(
236
+ async () => {
237
+ const updatedCell = within(secondGrid).getByTestId('grid.4.0');
238
+ await expect(updatedCell).toHaveTextContent(orgName.substring(0, 4));
239
+ },
240
+ { timeout: 5000 },
241
+ );
209
242
 
210
243
  // Test object creation (new relations) - equivalent to "new relations work as expected" test
211
244
  // 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
- );
245
+ const newTargetCell = await within(secondGrid).findByTestId('grid.4.1', undefined, { timeout: 10_000 });
215
246
  await expect(newTargetCell).toBeTruthy();
216
247
 
217
248
  // Click to focus the cell
@@ -226,23 +257,24 @@ export const Default: Story = {
226
257
  const newSearchField = await body.findByPlaceholderText('Search…');
227
258
  await userEvent.click(newSearchField);
228
259
 
229
- // Type a new object name (this will create a new object)
260
+ // Type a new object name (this will create a new object).
230
261
  const newOrgName = 'Salieri LLC';
231
262
  await userEvent.type(newSearchField, newOrgName);
232
263
 
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}');
264
+ // Click the create option directly to open the create form.
265
+ const createOptionLabel = await body.findByText(/Create new object/, undefined, { timeout: 2000 });
266
+ await userEvent.click(createOptionLabel.closest('[role="option"]') as HTMLElement);
239
267
 
240
- // Look for and click save button
241
- const createReferencedObjectForm = await body.findByTestId('create-referenced-object-form');
268
+ // Look for and click save button.
269
+ const createReferencedObjectForm = await body.findByTestId('create-referenced-object-form', undefined, {
270
+ timeout: 2000,
271
+ });
242
272
  const saveObjectButton = await within(createReferencedObjectForm).findByTestId('save-button');
273
+ await expect(saveObjectButton).not.toBeDisabled();
243
274
  await userEvent.click(saveObjectButton);
244
275
 
245
- // Verify the new object was created and relation was set
246
- await expect(newTargetCell).toHaveTextContent(newOrgName);
276
+ // Verify the relation was set by checking for the accessory link anchor in the cell.
277
+ const updatedNewCell = await within(secondGrid).findByTestId('grid.4.1', undefined, { timeout: 5000 });
278
+ await expect(updatedNewCell.querySelector('dx-anchor')).toBeTruthy();
247
279
  },
248
- } as any;
280
+ };
@@ -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>