@dxos/react-ui-table 0.8.3-staging.0fa589b → 0.8.4-main.c1de068

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 (125) hide show
  1. package/dist/lib/browser/chunk-5XLSHLOG.mjs +1751 -0
  2. package/dist/lib/browser/chunk-5XLSHLOG.mjs.map +7 -0
  3. package/dist/lib/browser/chunk-L2LSB6KV.mjs +18 -0
  4. package/dist/lib/browser/chunk-L2LSB6KV.mjs.map +7 -0
  5. package/dist/lib/browser/index.mjs +240 -1639
  6. package/dist/lib/browser/index.mjs.map +4 -4
  7. package/dist/lib/browser/meta.json +1 -1
  8. package/dist/lib/browser/testing/index.mjs +121 -0
  9. package/dist/lib/browser/testing/index.mjs.map +7 -0
  10. package/dist/lib/browser/types/index.mjs +3 -5
  11. package/dist/lib/node-esm/chunk-NXZ4CJYX.mjs +20 -0
  12. package/dist/lib/node-esm/chunk-NXZ4CJYX.mjs.map +7 -0
  13. package/dist/lib/node-esm/chunk-Y66YLSTC.mjs +1752 -0
  14. package/dist/lib/node-esm/chunk-Y66YLSTC.mjs.map +7 -0
  15. package/dist/lib/node-esm/index.mjs +240 -1639
  16. package/dist/lib/node-esm/index.mjs.map +4 -4
  17. package/dist/lib/node-esm/meta.json +1 -1
  18. package/dist/lib/node-esm/testing/index.mjs +122 -0
  19. package/dist/lib/node-esm/testing/index.mjs.map +7 -0
  20. package/dist/lib/node-esm/types/index.mjs +3 -5
  21. package/dist/types/src/components/Table/ColumnSettings.d.ts +3 -1
  22. package/dist/types/src/components/Table/ColumnSettings.d.ts.map +1 -1
  23. package/dist/types/src/components/Table/CreateRefPanel.d.ts.map +1 -1
  24. package/dist/types/src/components/Table/DynamicTable.d.ts +1 -1
  25. package/dist/types/src/components/Table/DynamicTable.d.ts.map +1 -1
  26. package/dist/types/src/components/Table/DynamicTable.stories.d.ts +1 -1
  27. package/dist/types/src/components/Table/DynamicTable.stories.d.ts.map +1 -1
  28. package/dist/types/src/components/Table/Table.d.ts.map +1 -1
  29. package/dist/types/src/components/Table/Table.stories.d.ts +1 -1
  30. package/dist/types/src/components/Table/Table.stories.d.ts.map +1 -1
  31. package/dist/types/src/components/Table/relations.stories.d.ts +1 -1
  32. package/dist/types/src/components/Table/relations.stories.d.ts.map +1 -1
  33. package/dist/types/src/components/TableCellEditor/CellValidationMessage.d.ts +2 -1
  34. package/dist/types/src/components/TableCellEditor/CellValidationMessage.d.ts.map +1 -1
  35. package/dist/types/src/components/TableCellEditor/FormCellEditor.d.ts +2 -1
  36. package/dist/types/src/components/TableCellEditor/FormCellEditor.d.ts.map +1 -1
  37. package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts +3 -2
  38. package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts.map +1 -1
  39. package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts +1 -1
  40. package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts.map +1 -1
  41. package/dist/types/src/components/testing.d.ts +3 -40
  42. package/dist/types/src/components/testing.d.ts.map +1 -1
  43. package/dist/types/src/hooks/index.d.ts +1 -0
  44. package/dist/types/src/hooks/index.d.ts.map +1 -1
  45. package/dist/types/src/hooks/useAddRow.d.ts +13 -0
  46. package/dist/types/src/hooks/useAddRow.d.ts.map +1 -0
  47. package/dist/types/src/hooks/useTableModel.d.ts +7 -6
  48. package/dist/types/src/hooks/useTableModel.d.ts.map +1 -1
  49. package/dist/types/src/index.d.ts +1 -2
  50. package/dist/types/src/index.d.ts.map +1 -1
  51. package/dist/types/src/model/selection-model.d.ts +2 -1
  52. package/dist/types/src/model/selection-model.d.ts.map +1 -1
  53. package/dist/types/src/model/table-model.d.ts +56 -20
  54. package/dist/types/src/model/table-model.d.ts.map +1 -1
  55. package/dist/types/src/model/table-presentation.d.ts +8 -0
  56. package/dist/types/src/model/table-presentation.d.ts.map +1 -1
  57. package/dist/types/src/model/table-sorting.d.ts +2 -2
  58. package/dist/types/src/model/table-sorting.d.ts.map +1 -1
  59. package/dist/types/src/playwright/playwright.config.d.ts +3 -0
  60. package/dist/types/src/playwright/playwright.config.d.ts.map +1 -0
  61. package/dist/types/src/testing/index.d.ts +2 -0
  62. package/dist/types/src/testing/index.d.ts.map +1 -0
  63. package/dist/types/src/testing/useTestTableModel.d.ts +41 -0
  64. package/dist/types/src/testing/useTestTableModel.d.ts.map +1 -0
  65. package/dist/types/src/translations.d.ts +3 -4
  66. package/dist/types/src/translations.d.ts.map +1 -1
  67. package/dist/types/src/types/index.d.ts +1 -1
  68. package/dist/types/src/types/index.d.ts.map +1 -1
  69. package/dist/types/src/types/schema.d.ts +8 -0
  70. package/dist/types/src/types/schema.d.ts.map +1 -0
  71. package/dist/types/src/util/dynamic-table.d.ts +3 -4
  72. package/dist/types/src/util/dynamic-table.d.ts.map +1 -1
  73. package/dist/types/src/util/table-buttons.d.ts +15 -1
  74. package/dist/types/src/util/table-buttons.d.ts.map +1 -1
  75. package/dist/types/src/util/table.d.ts +8 -11
  76. package/dist/types/src/util/table.d.ts.map +1 -1
  77. package/dist/types/tsconfig.tsbuildinfo +1 -1
  78. package/package.json +48 -40
  79. package/src/components/Table/ColumnSettings.tsx +14 -10
  80. package/src/components/Table/CreateRefPanel.tsx +5 -3
  81. package/src/components/Table/DynamicTable.stories.tsx +2 -2
  82. package/src/components/Table/DynamicTable.tsx +13 -7
  83. package/src/components/Table/Table.stories.tsx +104 -148
  84. package/src/components/Table/Table.tsx +50 -32
  85. package/src/components/Table/relations.stories.tsx +27 -35
  86. package/src/components/TableCellEditor/CellValidationMessage.tsx +6 -1
  87. package/src/components/TableCellEditor/FormCellEditor.tsx +30 -21
  88. package/src/components/TableCellEditor/TableCellEditor.stories.tsx +24 -28
  89. package/src/components/TableCellEditor/TableCellEditor.tsx +79 -21
  90. package/src/components/testing.ts +7 -24
  91. package/src/hooks/index.ts +1 -0
  92. package/src/hooks/useAddRow.ts +36 -0
  93. package/src/hooks/useTableModel.ts +20 -16
  94. package/src/index.ts +1 -2
  95. package/src/model/selection-model.ts +4 -2
  96. package/src/model/table-model.test.ts +7 -8
  97. package/src/model/table-model.ts +267 -78
  98. package/src/model/table-presentation.test.ts +6 -7
  99. package/src/model/table-presentation.ts +215 -132
  100. package/src/model/table-sorting.ts +9 -9
  101. package/src/playwright/playwright.config.ts +17 -0
  102. package/src/playwright/smoke.spec.ts +21 -20
  103. package/src/testing/index.ts +5 -0
  104. package/src/testing/useTestTableModel.ts +122 -0
  105. package/src/translations.ts +5 -3
  106. package/src/types/index.ts +2 -2
  107. package/src/types/schema.ts +16 -0
  108. package/src/util/dynamic-table.ts +19 -17
  109. package/src/util/table-buttons.ts +24 -1
  110. package/src/util/table.ts +33 -111
  111. package/dist/lib/browser/chunk-TL7CKQQV.mjs +0 -27
  112. package/dist/lib/browser/chunk-TL7CKQQV.mjs.map +0 -7
  113. package/dist/lib/node/chunk-O2VPBNS5.cjs +0 -50
  114. package/dist/lib/node/chunk-O2VPBNS5.cjs.map +0 -7
  115. package/dist/lib/node/index.cjs +0 -2739
  116. package/dist/lib/node/index.cjs.map +0 -7
  117. package/dist/lib/node/meta.json +0 -1
  118. package/dist/lib/node/types/index.cjs +0 -31
  119. package/dist/lib/node/types/index.cjs.map +0 -7
  120. package/dist/lib/node-esm/chunk-GBT6GD4G.mjs +0 -29
  121. package/dist/lib/node-esm/chunk-GBT6GD4G.mjs.map +0 -7
  122. package/dist/types/src/types/table.d.ts +0 -56
  123. package/dist/types/src/types/table.d.ts.map +0 -1
  124. package/src/playwright/playwright.config.cts +0 -18
  125. package/src/types/table.ts +0 -27
@@ -16,9 +16,11 @@ import React, {
16
16
  } from 'react';
17
17
 
18
18
  import { type Client } from '@dxos/client';
19
+ import { Filter } from '@dxos/echo';
19
20
  import { getValue } from '@dxos/echo-schema';
20
21
  import { invariant } from '@dxos/invariant';
21
- import { Filter } from '@dxos/react-client/echo';
22
+ // TODO(wittjosiah): Remove dependency on react-client.
23
+ import { getSpace } from '@dxos/react-client/echo';
22
24
  import { useTranslation } from '@dxos/react-ui';
23
25
  import { useAttention } from '@dxos/react-ui-attention';
24
26
  import {
@@ -50,15 +52,17 @@ import { createOption, TableValueEditor, type TableCellEditorProps } from '../Ta
50
52
 
51
53
  export type TableRootProps = PropsWithChildren<{ role?: string }>;
52
54
 
53
- const TableRoot = ({ children, role }: TableRootProps) => {
55
+ const TableRoot = ({ children, role = 'article' }: TableRootProps) => {
54
56
  return (
55
57
  <div
56
58
  role='none'
57
59
  className={mx(
58
- 'relative !border-separator',
59
- role === 'section' // TODO(burdon): This leaks composer plugin concepts? Standardize for react-ui?
60
- ? 'attention-surface overflow-hidden [&_.dx-grid]:max-is-[--dx-grid-content-inline-size]'
61
- : 'flex flex-col [&_.dx-grid]:grow [&_.dx-grid]:max-is-[--dx-grid-content-inline-size] [&_.dx-grid]:bs-0 [&_.dx-grid]:max-bs-[--dx-grid-content-block-size]',
60
+ 'relative !border-separator [&_.dx-grid]:max-is-[--dx-grid-content-inline-size] [&_.dx-grid]:max-bs-[--dx-grid-content-block-size]',
61
+ role === 'card--popover' && 'popover-card-height',
62
+ role === 'section' && 'attention-surface',
63
+ role === 'card--intrinsic' && '[&_.dx-grid]:bs-[--dx-grid-content-block-size]',
64
+ ['card--popover', 'section', 'card--extrinsic'].includes(role) && 'overflow-hidden',
65
+ ['article', 'slide'].includes(role) && 'flex flex-col [&_.dx-grid]:grow [&_.dx-grid]:bs-0',
62
66
  )}
63
67
  >
64
68
  {children}
@@ -91,6 +95,8 @@ const TableMain = forwardRef<TableController, TableMainProps>(
91
95
  const { t } = useTranslation(translationKey);
92
96
  const modals = useMemo(() => new ModalController(), []);
93
97
 
98
+ const draftRowCount = model?.getDraftRowCount() ?? 0;
99
+
94
100
  const frozen = useMemo(() => {
95
101
  const noActionColumn =
96
102
  model?.features.dataEditable === false &&
@@ -99,10 +105,11 @@ const TableMain = forwardRef<TableController, TableMainProps>(
99
105
 
100
106
  return {
101
107
  frozenRowsStart: 1,
108
+ frozenRowsEnd: draftRowCount,
102
109
  frozenColsStart: model?.features.selection.enabled ? 1 : 0,
103
110
  frozenColsEnd: noActionColumn ? 0 : 1,
104
111
  };
105
- }, [model]);
112
+ }, [model, draftRowCount]);
106
113
 
107
114
  const getCells = useCallback<NonNullable<GridContentProps['getCells']>>(
108
115
  (range: DxGridPlaneRange, plane: DxGridPlane) => presentation?.getCells(range, plane) ?? {},
@@ -119,30 +126,26 @@ const TableMain = forwardRef<TableController, TableMainProps>(
119
126
  /**
120
127
  * Provides an external controller that can be called to repaint the table.
121
128
  */
122
- useImperativeHandle<TableController, TableController>(
123
- forwardedRef,
124
- () => {
125
- if (!presentation || !dxGrid) {
126
- return {};
127
- }
129
+ useImperativeHandle<TableController, TableController>(forwardedRef, () => {
130
+ if (!presentation || !dxGrid) {
131
+ return {};
132
+ }
128
133
 
129
- return {
130
- update: (cell) => {
131
- if (cell) {
132
- dxGrid.updateIfWithinBounds(cell);
133
- } else {
134
- dxGrid.updateCells(true);
135
- dxGrid.requestUpdate();
136
- }
137
- },
138
- };
139
- },
140
- [presentation, dxGrid],
141
- );
134
+ return {
135
+ update: (cell) => {
136
+ if (cell) {
137
+ dxGrid.updateIfWithinBounds(cell, true);
138
+ } else {
139
+ dxGrid.updateCells(true);
140
+ dxGrid.requestUpdate();
141
+ }
142
+ },
143
+ };
144
+ }, [presentation, dxGrid]);
142
145
 
143
146
  const handleGridClick = useCallback(
144
147
  (event: MouseEvent) => {
145
- const rowIndex = safeParseInt((event.target as HTMLElement).ariaRowIndex ?? '');
148
+ const rowIndex = safeParseInt((event.target as HTMLElement).closest('[aria-rowindex]')?.ariaRowIndex ?? '');
146
149
  if (rowIndex != null) {
147
150
  if (onRowClick) {
148
151
  const row = model?.getRowAt(rowIndex);
@@ -188,6 +191,17 @@ const TableMain = forwardRef<TableController, TableMainProps>(
188
191
  model?.sorting?.toggleSort(data.fieldId);
189
192
  break;
190
193
  }
194
+ case 'saveDraftRow': {
195
+ if (model) {
196
+ const didCommitSuccessfully = model.commitDraftRow(data.rowIndex);
197
+ if (dxGrid && didCommitSuccessfully) {
198
+ requestAnimationFrame(() => {
199
+ dxGrid.scrollToEndRow();
200
+ });
201
+ }
202
+ }
203
+ break;
204
+ }
191
205
  }
192
206
  return;
193
207
  }
@@ -222,7 +236,7 @@ const TableMain = forwardRef<TableController, TableMainProps>(
222
236
  }
223
237
  }
224
238
  },
225
- [model, modals],
239
+ [model, modals, dxGrid],
226
240
  );
227
241
 
228
242
  const handleFocus = useCallback<NonNullable<TableCellEditorProps['onFocus']>>(
@@ -242,7 +256,7 @@ const TableMain = forwardRef<TableController, TableMainProps>(
242
256
 
243
257
  // TODO(burdon): Insert row only if bottom row isn't completely blank already.
244
258
  if (model && cell.row === model.getRowCount() - 1) {
245
- model.insertRow(cell.row);
259
+ model.insertRow();
246
260
  if (dxGrid) {
247
261
  requestAnimationFrame(() => {
248
262
  dxGrid?.scrollToRow(cell.row + 1);
@@ -307,7 +321,7 @@ const TableMain = forwardRef<TableController, TableMainProps>(
307
321
  const handleQuery = useCallback<NonNullable<TableCellEditorProps['onQuery']>>(
308
322
  async ({ field, props }, text) => {
309
323
  if (model && props.referenceSchema && field.referencePath) {
310
- const space = model.space;
324
+ const space = getSpace(model.view);
311
325
  invariant(space);
312
326
 
313
327
  let schema;
@@ -349,6 +363,10 @@ const TableMain = forwardRef<TableController, TableMainProps>(
349
363
  [model, client, t],
350
364
  );
351
365
 
366
+ const handleSave = useCallback(() => {
367
+ dxGrid?.updateCells(true);
368
+ }, [dxGrid]);
369
+
352
370
  if (!model || !modals) {
353
371
  return <span role='none' className='attention-surface' />;
354
372
  }
@@ -362,14 +380,14 @@ const TableMain = forwardRef<TableController, TableMainProps>(
362
380
  onEnter={handleEnter}
363
381
  onFocus={handleFocus}
364
382
  onQuery={handleQuery}
383
+ onSave={handleSave}
365
384
  />
366
385
  <Grid.Content
367
386
  className={mx('[--dx-grid-base:var(--baseSurface)]', gridSeparatorInlineEnd, gridSeparatorBlockEnd)}
368
387
  frozen={frozen}
369
- // getCells={getCells}
370
388
  columns={model.columnMeta.value}
371
389
  limitRows={model.getRowCount() ?? 0}
372
- limitColumns={model.view?.fields?.length ?? 0}
390
+ limitColumns={model.projection.fields.length}
373
391
  overscroll='trap'
374
392
  onAxisResize={handleAxisResize}
375
393
  onClick={handleGridClick}
@@ -4,27 +4,24 @@
4
4
 
5
5
  import '@dxos-theme';
6
6
 
7
- import { type StoryObj, type Meta } from '@storybook/react';
8
- import { SchemaAST } from 'effect';
9
- import React, { useEffect, useMemo } from 'react';
10
-
11
- import { Obj, type Type } from '@dxos/echo';
12
- import { getSchemaTypename, toJsonSchema } from '@dxos/echo-schema';
13
- import { getAnnotation } from '@dxos/effect';
14
- import { invariant } from '@dxos/invariant';
7
+ import { type StoryObj, type Meta } from '@storybook/react-vite';
8
+ import React, { useEffect, useMemo, useState } from 'react';
9
+
10
+ import { Type } from '@dxos/echo';
11
+ import { type JsonSchemaType } from '@dxos/echo-schema';
15
12
  import { faker } from '@dxos/random';
16
13
  import { useClient } from '@dxos/react-client';
17
- import { makeRef } from '@dxos/react-client/echo';
18
14
  import { useClientProvider, withClientProvider } from '@dxos/react-client/testing';
19
- import { DataType, createView, ViewProjection, ViewType } from '@dxos/schema';
15
+ import { DataType } from '@dxos/schema';
20
16
  import { createAsyncGenerator, type ValueGenerator } from '@dxos/schema/testing';
21
17
  import { withLayout, withTheme } from '@dxos/storybook-utils';
22
18
 
23
19
  import { Table } from './Table';
24
20
  import { useTableModel } from '../../hooks';
25
21
  import { type TableFeatures, TablePresentation, type TableRow } from '../../model';
26
- import translations from '../../translations';
27
- import { TableType } from '../../types';
22
+ import { translations } from '../../translations';
23
+ import { TableView } from '../../types';
24
+ import { createTable } from '../../util';
28
25
 
29
26
  faker.seed(1);
30
27
  const generator: ValueGenerator = faker as any;
@@ -34,35 +31,30 @@ const generator: ValueGenerator = faker as any;
34
31
  // TODO(burdon): Reconcile schemas types and utils (see API PR).
35
32
  // TODO(burdon): Base type for T (with id); see ECHO API PR?
36
33
  const useTestModel = <S extends Type.Obj.Any>(schema: S, count: number) => {
34
+ const client = useClient();
37
35
  const { space } = useClientProvider();
38
-
39
- const jsonSchema = useMemo(() => toJsonSchema(schema), [schema]);
40
- const table = useMemo(() => {
41
- if (!space) {
42
- return undefined;
43
- }
44
- const typename = getSchemaTypename(schema);
45
- invariant(typename);
46
- const name = getAnnotation<string>(SchemaAST.TitleAnnotationId)(schema.ast) ?? typename;
47
- const view = createView({ name, typename, jsonSchema });
48
- return space.db.add(Obj.make(TableType, { view: makeRef(view) }));
49
- }, [schema, space, jsonSchema]);
50
-
51
- const projection = useMemo(() => {
52
- if (!table?.view?.target) {
53
- return undefined;
54
- }
55
-
56
- // TODO(burdon): Just pass in view? Reuse same jsonSchema instance? View determines if mutable, etc.
57
- return new ViewProjection(jsonSchema, table.view.target);
58
- }, [jsonSchema, table]);
36
+ const [view, setView] = useState<DataType.View>();
37
+ const [jsonSchema, setJsonSchema] = useState<JsonSchemaType>();
59
38
 
60
39
  const features = useMemo<TableFeatures>(
61
40
  () => ({ schemaEditable: false, dataEditable: true, selection: { enabled: false } }),
62
41
  [],
63
42
  );
64
43
 
65
- const model = useTableModel<TableRow>({ table, projection, rows: [], features });
44
+ useEffect(() => {
45
+ if (!space) {
46
+ return;
47
+ }
48
+
49
+ const timeout = setTimeout(async () => {
50
+ const { jsonSchema, view } = await createTable({ client, space, typename: Type.getTypename(schema) });
51
+ setJsonSchema(jsonSchema);
52
+ setView(view);
53
+ });
54
+ return () => clearTimeout(timeout);
55
+ }, [client, space, schema]);
56
+
57
+ const model = useTableModel<TableRow>({ view, schema: jsonSchema, rows: [], features });
66
58
  useEffect(() => {
67
59
  if (!model || !space) {
68
60
  return;
@@ -108,7 +100,7 @@ const meta: Meta<typeof DefaultStory> = {
108
100
  parameters: { translations, controls: { disable: true } },
109
101
  decorators: [
110
102
  withClientProvider({
111
- types: [TableType, ViewType, DataType.Organization, DataType.Person],
103
+ types: [DataType.View, DataType.Organization, DataType.Person, TableView],
112
104
  createIdentity: true,
113
105
  createSpace: true,
114
106
  }),
@@ -8,10 +8,12 @@ import { useGridContext, type GridScopedProps } from '@dxos/react-ui-grid';
8
8
 
9
9
  export type CellValidationMessageProps = {
10
10
  validationError: string | null;
11
+ variant?: 'error' | 'warning';
11
12
  };
12
13
 
13
14
  export const CellValidationMessage = ({
14
15
  validationError,
16
+ variant = 'error',
15
17
  __gridScope,
16
18
  }: GridScopedProps<CellValidationMessageProps>) => {
17
19
  const { editing, editBox: box } = useGridContext('GridSheetCellEditor', __gridScope);
@@ -20,10 +22,13 @@ export const CellValidationMessage = ({
20
22
  return null;
21
23
  }
22
24
 
25
+ const bgClass = variant === 'error' ? 'bg-errorSurface' : 'bg-warningSurface';
26
+ const textClass = variant === 'error' ? 'text-errorSurfaceText' : 'text-warningSurfaceText';
27
+
23
28
  return (
24
29
  <div
25
30
  role='none'
26
- className='absolute bg-errorSurface text-errorSurfaceText rounded-bs-sm text-xs p-1'
31
+ className={`absolute ${bgClass} ${textClass} rounded-bs-sm text-xs p-1`}
27
32
  style={{
28
33
  ...{ '--dx-gridCellWidth': `${box?.inlineSize ?? 200}px` },
29
34
  zIndex: 10,
@@ -3,7 +3,7 @@
3
3
  //
4
4
 
5
5
  import { type Schema } from 'effect';
6
- import React, { useCallback, useEffect, useMemo, useRef } from 'react';
6
+ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
7
7
 
8
8
  import { getSnapshot } from '@dxos/echo-schema';
9
9
  import { invariant } from '@dxos/invariant';
@@ -20,18 +20,26 @@ type FormCellEditorProps = {
20
20
  fieldProjection: FieldProjection;
21
21
  model?: TableModel;
22
22
  schema?: Schema.Schema.AnyNoContext;
23
+ onSave?: () => void;
23
24
  __gridScope: any;
24
25
  };
25
26
 
26
- export const FormCellEditor = ({ fieldProjection, model, schema, __gridScope }: FormCellEditorProps) => {
27
- const { editing, setEditing } = useGridContext('ArrayEditor', __gridScope);
28
- const cellRef = useRef<HTMLButtonElement | null>(null);
27
+ export const FormCellEditor = ({ fieldProjection, model, schema, onSave, __gridScope }: FormCellEditorProps) => {
28
+ const { editing: contextEditing, setEditing } = useGridContext('ArrayEditor', __gridScope);
29
+ const [editing, setLocalEditing] = useState(false);
30
+ const anchorRef = useRef<HTMLButtonElement | null>(null);
29
31
 
30
32
  useEffect(() => {
31
- if (editing && editing.cellElement) {
32
- cellRef.current = editing.cellElement as HTMLButtonElement;
33
+ if (contextEditing && contextEditing.cellElement) {
34
+ anchorRef.current = (contextEditing.cellElement as HTMLElement).querySelector(
35
+ '.dx-grid__cell__content',
36
+ ) as HTMLButtonElement;
37
+ setLocalEditing(true);
38
+ } else {
39
+ anchorRef.current = null;
40
+ setLocalEditing(false);
33
41
  }
34
- }, [editing?.cellElement]);
42
+ }, [contextEditing]);
35
43
 
36
44
  const narrowedSchema = useMemo(() => {
37
45
  if (!schema) {
@@ -41,8 +49,8 @@ export const FormCellEditor = ({ fieldProjection, model, schema, __gridScope }:
41
49
  }, [JSON.stringify(schema), fieldProjection.field.path]); // TODO(burdon): Avoid stringify.
42
50
 
43
51
  const originalRow = useMemo(() => {
44
- if (model && editing) {
45
- const cell = parseCellIndex(editing.index);
52
+ if (model && contextEditing) {
53
+ const cell = parseCellIndex(contextEditing.index);
46
54
  const row = model.getRowAt(cell.row);
47
55
  invariant(row);
48
56
 
@@ -50,7 +58,7 @@ export const FormCellEditor = ({ fieldProjection, model, schema, __gridScope }:
50
58
  }
51
59
 
52
60
  return undefined;
53
- }, [model, editing]);
61
+ }, [model, contextEditing]);
54
62
 
55
63
  const formValues = useMemo(() => {
56
64
  if (originalRow) {
@@ -65,27 +73,28 @@ export const FormCellEditor = ({ fieldProjection, model, schema, __gridScope }:
65
73
  const value = getDeep(values, [path]);
66
74
  setDeep(originalRow, [path], value);
67
75
  setEditing(null);
76
+ setLocalEditing(false);
77
+ onSave?.();
68
78
  },
69
79
  [fieldProjection.field.path, originalRow],
70
80
  );
71
81
 
72
- const handleOpenChange = useCallback(
73
- (nextOpen: boolean) => {
74
- if (nextOpen === false) {
75
- setEditing(null);
76
- }
77
- },
78
- [setEditing],
79
- );
82
+ const handleOpenChange = useCallback((nextOpen: boolean) => {
83
+ if (nextOpen === false) {
84
+ setEditing(null);
85
+ onSave?.();
86
+ }
87
+ setLocalEditing(nextOpen);
88
+ }, []);
80
89
 
81
90
  if (!editing) {
82
91
  return null;
83
92
  }
84
93
 
85
94
  return (
86
- <Popover.Root open={editing !== null} onOpenChange={handleOpenChange}>
87
- <Popover.VirtualTrigger virtualRef={cellRef} />
88
- <Popover.Content tabIndex={-1} classNames='popover-card-width'>
95
+ <Popover.Root open={editing} onOpenChange={handleOpenChange}>
96
+ <Popover.VirtualTrigger virtualRef={anchorRef} />
97
+ <Popover.Content tabIndex={-1} classNames='popover-card-width density-fine'>
89
98
  <Popover.Arrow />
90
99
  <Popover.Viewport>
91
100
  <Form values={formValues} schema={narrowedSchema as any} onSave={handleSave} />
@@ -4,25 +4,25 @@
4
4
 
5
5
  import '@dxos-theme';
6
6
 
7
- import { type Meta, type StoryObj } from '@storybook/react';
7
+ import { type Meta, type StoryObj } from '@storybook/react-vite';
8
8
  import React, { useEffect, useMemo, useState } from 'react';
9
9
 
10
10
  import { Obj } from '@dxos/echo';
11
11
  import { type EchoSchema, isMutable } from '@dxos/echo-schema';
12
12
  import { invariant } from '@dxos/invariant';
13
13
  import { faker } from '@dxos/random';
14
- import { Filter, useQuery, live } from '@dxos/react-client/echo';
14
+ import { Filter, useQuery } from '@dxos/react-client/echo';
15
15
  import { useClientProvider, withClientProvider } from '@dxos/react-client/testing';
16
16
  import { defaultRowSize, Grid, type GridEditing } from '@dxos/react-ui-grid';
17
- import { ViewProjection, ViewType } from '@dxos/schema';
17
+ import { createDefaultSchema, DataType } from '@dxos/schema';
18
18
  import { withLayout, withTheme } from '@dxos/storybook-utils';
19
19
 
20
20
  import { TableCellEditor, type TableCellEditorProps } from './TableCellEditor';
21
21
  import { useTableModel } from '../../hooks';
22
22
  import { type TableFeatures } from '../../model';
23
- import translations from '../../translations';
24
- import { TableType } from '../../types';
25
- import { initializeTable } from '../../util';
23
+ import { translations } from '../../translations';
24
+ import { TableView } from '../../types';
25
+ import { createTable } from '../../util';
26
26
 
27
27
  type StoryProps = {
28
28
  editing: GridEditing;
@@ -32,23 +32,16 @@ const DefaultStory = ({ editing }: StoryProps) => {
32
32
  const { space } = useClientProvider();
33
33
  invariant(space);
34
34
 
35
- const tables = useQuery(space, Filter.type(TableType));
36
- const [table, setTable] = useState<TableType>();
35
+ const views = useQuery(space, Filter.type(DataType.View));
36
+ const [view, setView] = useState<DataType.View>();
37
37
  const [schema, setSchema] = useState<EchoSchema>();
38
38
  useEffect(() => {
39
- if (space && tables.length && !table) {
40
- const table = tables[0];
41
- invariant(table.view);
42
- setTable(table);
43
- setSchema(space.db.schemaRegistry.getSchema(table.view.target!.query.typename!));
39
+ if (space && views.length && !view) {
40
+ const view = views[0];
41
+ setView(view);
42
+ setSchema(space.db.schemaRegistry.getSchema(view.query.typename!));
44
43
  }
45
- }, [space, tables]);
46
-
47
- const projection = useMemo(() => {
48
- if (schema && table?.view) {
49
- return new ViewProjection(schema.jsonSchema, table.view.target!);
50
- }
51
- }, [schema, table?.view]);
44
+ }, [space, views, view]);
52
45
 
53
46
  const features: Partial<TableFeatures> = useMemo(
54
47
  () => ({
@@ -59,7 +52,7 @@ const DefaultStory = ({ editing }: StoryProps) => {
59
52
  [schema],
60
53
  );
61
54
 
62
- const model = useTableModel({ table, projection, features });
55
+ const model = useTableModel({ view, schema: schema?.jsonSchema, features });
63
56
 
64
57
  const handleQuery: TableCellEditorProps['onQuery'] = async ({ field }) => {
65
58
  // TODO(dmaretskyi): If no schema query nothing
@@ -73,7 +66,7 @@ const DefaultStory = ({ editing }: StoryProps) => {
73
66
  });
74
67
  };
75
68
 
76
- if (!model || !schema || !table) {
69
+ if (!model || !schema || !view) {
77
70
  return <div />;
78
71
  }
79
72
 
@@ -87,22 +80,25 @@ const DefaultStory = ({ editing }: StoryProps) => {
87
80
  };
88
81
 
89
82
  const meta: Meta<StoryProps> = {
90
- title: 'plugins/plugin-table/TableCellEditor',
83
+ title: 'ui/react-ui-table/TableCellEditor',
91
84
  component: DefaultStory,
92
85
  render: DefaultStory,
93
86
  parameters: { translations, layout: 'centered' },
94
87
  decorators: [
95
88
  withClientProvider({
96
- types: [TableType, ViewType],
89
+ types: [DataType.View, TableView],
97
90
  createIdentity: true,
98
91
  createSpace: true,
99
92
  onSpaceCreated: async ({ client, space }) => {
100
- const table = space.db.add(Obj.make(TableType, {}));
101
- const schema = await initializeTable({ client, space, table });
93
+ const schema = createDefaultSchema();
94
+ const { view } = await createTable({ client, space, typename: schema.typename });
95
+ space.db.add(view);
102
96
  Array.from({ length: 10 }).forEach(() => {
103
97
  space.db.add(
104
- live(schema, {
105
- name: faker.person.fullName(),
98
+ Obj.make(schema, {
99
+ title: faker.person.fullName(),
100
+ status: faker.helpers.arrayElement(['todo', 'in-progress', 'done'] as const),
101
+ description: faker.lorem.sentence(),
106
102
  }),
107
103
  );
108
104
  });