@dxos/react-ui-table 0.7.1 → 0.7.2-main.f1adc9f

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 (61) hide show
  1. package/dist/lib/browser/{chunk-A5W4XWPD.mjs → chunk-EUQZHWXM.mjs} +10 -6
  2. package/dist/lib/browser/chunk-EUQZHWXM.mjs.map +7 -0
  3. package/dist/lib/browser/index.mjs +29 -15
  4. package/dist/lib/browser/index.mjs.map +3 -3
  5. package/dist/lib/browser/meta.json +1 -1
  6. package/dist/lib/browser/types/index.mjs +3 -1
  7. package/dist/lib/node/{chunk-32IGMRME.cjs → chunk-OM65ZSS6.cjs} +14 -9
  8. package/dist/lib/node/chunk-OM65ZSS6.cjs.map +7 -0
  9. package/dist/lib/node/index.cjs +30 -16
  10. package/dist/lib/node/index.cjs.map +3 -3
  11. package/dist/lib/node/meta.json +1 -1
  12. package/dist/lib/node/types/index.cjs +4 -2
  13. package/dist/lib/node/types/index.cjs.map +2 -2
  14. package/dist/lib/node-esm/{chunk-DWQ4KGLD.mjs → chunk-U6VD6VZ4.mjs} +10 -6
  15. package/dist/lib/node-esm/chunk-U6VD6VZ4.mjs.map +7 -0
  16. package/dist/lib/node-esm/index.mjs +29 -15
  17. package/dist/lib/node-esm/index.mjs.map +3 -3
  18. package/dist/lib/node-esm/meta.json +1 -1
  19. package/dist/lib/node-esm/types/index.mjs +3 -1
  20. package/dist/types/src/components/Table/ColumnActionsMenu.d.ts.map +1 -1
  21. package/dist/types/src/components/Table/RowActionsMenu.d.ts.map +1 -1
  22. package/dist/types/src/components/Table/Table.d.ts.map +1 -1
  23. package/dist/types/src/components/Table/Table.stories.d.ts.map +1 -1
  24. package/dist/types/src/components/testing.d.ts +3 -2
  25. package/dist/types/src/components/testing.d.ts.map +1 -1
  26. package/dist/types/src/model/table-model.d.ts.map +1 -1
  27. package/dist/types/src/playwright/TableManager.d.ts +17 -0
  28. package/dist/types/src/playwright/TableManager.d.ts.map +1 -0
  29. package/dist/types/src/playwright/playwright.config.d.ts +3 -0
  30. package/dist/types/src/playwright/playwright.config.d.ts.map +1 -0
  31. package/dist/types/src/playwright/smoke.spec.d.ts +2 -0
  32. package/dist/types/src/playwright/smoke.spec.d.ts.map +1 -0
  33. package/dist/types/src/types/table.d.ts +60 -7
  34. package/dist/types/src/types/table.d.ts.map +1 -1
  35. package/dist/types/src/util/grid.d.ts +3 -3
  36. package/dist/types/src/util/grid.d.ts.map +1 -1
  37. package/dist/types/src/util/table-buttons.d.ts.map +1 -1
  38. package/dist/types/src/util/table.d.ts +5 -2
  39. package/dist/types/src/util/table.d.ts.map +1 -1
  40. package/package.json +38 -39
  41. package/src/_deprecated/components/ColumnMenu/ColumnMenu.stories.tsx +1 -1
  42. package/src/_deprecated/components/ColumnMenu/ColumnSettings.stories.tsx +1 -1
  43. package/src/_deprecated/components/Table/Table.stories.tsx +1 -1
  44. package/src/components/Table/ColumnActionsMenu.tsx +11 -6
  45. package/src/components/Table/ColumnSettings.tsx +2 -2
  46. package/src/components/Table/RowActionsMenu.tsx +2 -3
  47. package/src/components/Table/Table.stories.tsx +6 -4
  48. package/src/components/Table/Table.tsx +2 -3
  49. package/src/components/TableCellEditor/TableCellEditor.stories.tsx +1 -1
  50. package/src/model/table-model.ts +1 -0
  51. package/src/playwright/TableManager.ts +60 -0
  52. package/src/playwright/playwright.config.ts +18 -0
  53. package/src/playwright/smoke.spec.ts +105 -0
  54. package/src/types/table.ts +13 -3
  55. package/src/util/grid.ts +3 -3
  56. package/src/util/table-buttons.ts +9 -4
  57. package/src/util/table-controls.ts +1 -1
  58. package/src/util/table.ts +11 -3
  59. package/dist/lib/browser/chunk-A5W4XWPD.mjs.map +0 -7
  60. package/dist/lib/node/chunk-32IGMRME.cjs.map +0 -7
  61. package/dist/lib/node-esm/chunk-DWQ4KGLD.mjs.map +0 -7
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dxos/react-ui-table",
3
- "version": "0.7.1",
3
+ "version": "0.7.2-main.f1adc9f",
4
4
  "description": "A table component.",
5
5
  "homepage": "https://dxos.org",
6
6
  "bugs": "https://github.com/dxos/dxos/issues",
@@ -8,32 +8,30 @@
8
8
  "author": "DXOS.org",
9
9
  "sideEffects": true,
10
10
  "exports": {
11
+ "./deprecated": {
12
+ "types": "./dist/types/src/_deprecated/index.d.ts",
13
+ "browser": "./dist/lib/browser/_deprecated/index.mjs",
14
+ "node": "./dist/lib/node-esm/_deprecated/index.mjs"
15
+ },
11
16
  ".": {
12
17
  "types": "./dist/types/src/index.d.ts",
13
18
  "browser": "./dist/lib/browser/index.mjs",
14
- "require": "./dist/lib/node/index.cjs",
15
- "import": "./dist/lib/node-esm/index.mjs"
19
+ "node": "./dist/lib/node-esm/index.mjs"
16
20
  },
17
21
  "./types": {
18
22
  "types": "./dist/types/src/types/index.d.ts",
19
23
  "browser": "./dist/lib/browser/types/index.mjs",
20
- "require": "./dist/lib/node/types/index.cjs",
21
- "import": "./dist/lib/node-esm/types/index.mjs"
22
- },
23
- "./deprecated": {
24
- "types": "./dist/types/src/_deprecated/index.d.ts",
25
- "browser": "./dist/lib/browser/_deprecated/index.mjs",
26
- "require": "./dist/lib/node/_deprecated/index.cjs",
27
- "import": "./dist/lib/node-esm/_deprecated/index.mjs"
24
+ "node": "./dist/lib/node-esm/types/index.mjs"
28
25
  }
29
26
  },
27
+ "types": "dist/types/src/index.d.ts",
30
28
  "typesVersions": {
31
29
  "*": {
32
30
  "deprecated": [
33
- "./dist/types/src/_deprecated/index.d.ts"
31
+ "dist/types/src/_deprecated/index.d.ts"
34
32
  ],
35
33
  "types": [
36
- "./dist/types/src/types/index.d.ts"
34
+ "dist/types/src/types/index.d.ts"
37
35
  ]
38
36
  }
39
37
  },
@@ -55,25 +53,25 @@
55
53
  "@tanstack/react-virtual": "^3.9.0",
56
54
  "date-fns": "^3.3.1",
57
55
  "lodash.orderby": "^4.6.0",
58
- "@dxos/debug": "0.7.1",
59
- "@dxos/context": "0.7.1",
60
- "@dxos/keys": "0.7.1",
61
- "@dxos/invariant": "0.7.1",
62
- "@dxos/echo-schema": "0.7.1",
63
- "@dxos/lit-grid": "0.7.1",
64
- "@dxos/log": "0.7.1",
65
- "@dxos/plugin-search": "0.7.1",
66
- "@dxos/react-client": "0.7.1",
67
- "@dxos/plugin-space": "0.7.1",
68
- "@dxos/react-ui-attention": "0.7.1",
69
- "@dxos/react-ui-form": "0.7.1",
70
- "@dxos/react-ui-searchlist": "0.7.1",
71
- "@dxos/react-ui-stack": "0.7.1",
72
- "@dxos/react-ui-syntax-highlighter": "0.7.1",
73
- "@dxos/react-ui-grid": "0.7.1",
74
- "@dxos/react-ui-types": "0.7.1",
75
- "@dxos/schema": "0.7.1",
76
- "@dxos/util": "0.7.1"
56
+ "@dxos/debug": "0.7.2-main.f1adc9f",
57
+ "@dxos/context": "0.7.2-main.f1adc9f",
58
+ "@dxos/invariant": "0.7.2-main.f1adc9f",
59
+ "@dxos/echo-schema": "0.7.2-main.f1adc9f",
60
+ "@dxos/lit-grid": "0.7.2-main.f1adc9f",
61
+ "@dxos/keys": "0.7.2-main.f1adc9f",
62
+ "@dxos/log": "0.7.2-main.f1adc9f",
63
+ "@dxos/plugin-search": "0.7.2-main.f1adc9f",
64
+ "@dxos/plugin-space": "0.7.2-main.f1adc9f",
65
+ "@dxos/react-ui-attention": "0.7.2-main.f1adc9f",
66
+ "@dxos/react-ui-form": "0.7.2-main.f1adc9f",
67
+ "@dxos/react-ui-grid": "0.7.2-main.f1adc9f",
68
+ "@dxos/react-client": "0.7.2-main.f1adc9f",
69
+ "@dxos/react-ui-searchlist": "0.7.2-main.f1adc9f",
70
+ "@dxos/react-ui-stack": "0.7.2-main.f1adc9f",
71
+ "@dxos/react-ui-syntax-highlighter": "0.7.2-main.f1adc9f",
72
+ "@dxos/schema": "0.7.2-main.f1adc9f",
73
+ "@dxos/util": "0.7.2-main.f1adc9f",
74
+ "@dxos/react-ui-types": "0.7.2-main.f1adc9f"
77
75
  },
78
76
  "devDependencies": {
79
77
  "@phosphor-icons/react": "^2.1.5",
@@ -84,18 +82,19 @@
84
82
  "react": "~18.2.0",
85
83
  "react-dom": "~18.2.0",
86
84
  "vite": "5.4.7",
87
- "@dxos/echo-signals": "0.7.1",
88
- "@dxos/random": "0.7.1",
89
- "@dxos/react-ui": "0.7.1",
90
- "@dxos/react-ui-theme": "0.7.1",
91
- "@dxos/storybook-utils": "0.7.1"
85
+ "@dxos/echo-signals": "0.7.2-main.f1adc9f",
86
+ "@dxos/random": "0.7.2-main.f1adc9f",
87
+ "@dxos/react-ui": "0.7.2-main.f1adc9f",
88
+ "@dxos/storybook-utils": "0.7.2-main.f1adc9f",
89
+ "@dxos/test-utils": "0.7.2-main.f1adc9f",
90
+ "@dxos/react-ui-theme": "0.7.2-main.f1adc9f"
92
91
  },
93
92
  "peerDependencies": {
94
93
  "@phosphor-icons/react": "^2.1.5",
95
94
  "react": "~18.2.0",
96
95
  "react-dom": "~18.2.0",
97
- "@dxos/react-ui": "0.7.1",
98
- "@dxos/react-ui-theme": "0.7.1"
96
+ "@dxos/react-ui": "0.7.2-main.f1adc9f",
97
+ "@dxos/react-ui-theme": "0.7.2-main.f1adc9f"
99
98
  },
100
99
  "publishConfig": {
101
100
  "access": "public"
@@ -15,7 +15,7 @@ import { type ColumnSettingsProps } from './ColumnSettings';
15
15
  import { type ColumnDef } from '../../schema';
16
16
 
17
17
  export default {
18
- title: 'ui/react-ui-table/ColumnMenu',
18
+ title: 'ui/react-ui-table/deprecated/ColumnMenu',
19
19
  component: ColumnMenu,
20
20
  decorators: [withTheme],
21
21
  };
@@ -20,7 +20,7 @@ const DefaultStory = (props: ColumnSettingsProps) => (
20
20
  );
21
21
 
22
22
  export default {
23
- title: 'ui/react-ui-table/ColumnSettings',
23
+ title: 'ui/react-ui-table/deprecated/ColumnSettings',
24
24
  component: ColumnSettings,
25
25
  render: DefaultStory,
26
26
  decorators: [withTheme, withLayout()],
@@ -128,7 +128,7 @@ const MinimalTable = (props: any) => {
128
128
  };
129
129
 
130
130
  export default {
131
- title: 'ui/react-ui-table/Table',
131
+ title: 'ui/react-ui-table/deprecated/Table',
132
132
  component: MinimalTable,
133
133
  args: {
134
134
  header: true,
@@ -14,7 +14,6 @@ type ColumnActionsMenuProps = { model?: TableModel };
14
14
  export const ColumnActionsMenu = ({ model }: ColumnActionsMenuProps) => {
15
15
  const { t } = useTranslation(translationKey);
16
16
  const state = model?.modalController.state.value;
17
-
18
17
  if (!model || state?.type !== 'column') {
19
18
  return null;
20
19
  }
@@ -29,26 +28,32 @@ export const ColumnActionsMenu = ({ model }: ColumnActionsMenuProps) => {
29
28
  <DropdownMenu.Content>
30
29
  <DropdownMenu.Viewport>
31
30
  {(!isCurrentColumnSorted || currentSort?.direction === 'asc') && (
32
- <DropdownMenu.Item onClick={() => model.setSort(state.fieldId, 'desc')}>
31
+ <DropdownMenu.Item
32
+ data-testid='column-sort-descending'
33
+ onClick={() => model.setSort(state.fieldId, 'desc')}
34
+ >
33
35
  {t('column action sort descending')}
34
36
  </DropdownMenu.Item>
35
37
  )}
36
38
  {(!isCurrentColumnSorted || currentSort?.direction === 'desc') && (
37
- <DropdownMenu.Item onClick={() => model.setSort(state.fieldId, 'asc')}>
39
+ <DropdownMenu.Item
40
+ data-testid='column-sort-ascending'
41
+ onClick={() => model.setSort(state.fieldId, 'asc')}
42
+ >
38
43
  {t('column action sort ascending')}
39
44
  </DropdownMenu.Item>
40
45
  )}
41
46
  {isCurrentColumnSorted && (
42
- <DropdownMenu.Item onClick={() => model.clearSort()}>
47
+ <DropdownMenu.Item data-testid='column-clear-sort' onClick={() => model.clearSort()}>
43
48
  {t('column action clear sorting')}
44
49
  </DropdownMenu.Item>
45
50
  )}
46
51
  {model.getColumnCount() > 1 && (
47
- <DropdownMenu.Item onClick={() => model.deleteColumn(state.fieldId)}>
52
+ <DropdownMenu.Item data-testid='column-delete' onClick={() => model.deleteColumn(state.fieldId)}>
48
53
  {t('column action delete')}
49
54
  </DropdownMenu.Item>
50
55
  )}
51
- <DropdownMenu.Item onClick={() => model.modalController.openColumnSettings()}>
56
+ <DropdownMenu.Item data-testid='column-settings' onClick={() => model.modalController.openColumnSettings()}>
52
57
  {t('column action settings')}
53
58
  </DropdownMenu.Item>
54
59
  </DropdownMenu.Viewport>
@@ -39,7 +39,7 @@ export const ColumnSettings = ({ model }: ColumnSettingsProps) => {
39
39
 
40
40
  const field = existingField ?? newField;
41
41
 
42
- const handleClose = useCallback(() => {
42
+ const handleSave = useCallback(() => {
43
43
  model?.modalController.close();
44
44
  }, [model?.modalController]);
45
45
 
@@ -64,7 +64,7 @@ export const ColumnSettings = ({ model }: ColumnSettingsProps) => {
64
64
  projection={model.projection}
65
65
  field={field}
66
66
  registry={space?.db.schemaRegistry}
67
- onClose={handleClose}
67
+ onSave={handleSave}
68
68
  onCancel={handleCancel}
69
69
  />
70
70
  </DropdownMenu.Viewport>
@@ -13,9 +13,8 @@ type RowActionsMenuProps = { model: TableModel };
13
13
 
14
14
  export const RowActionsMenu = ({ model }: RowActionsMenuProps) => {
15
15
  const { t } = useTranslation(translationKey);
16
- const state = model.modalController.state.value;
17
16
  const hasSelection = model.selection.hasSelection.value;
18
-
17
+ const state = model.modalController.state.value;
19
18
  if (state?.type !== 'row') {
20
19
  return null;
21
20
  }
@@ -25,7 +24,7 @@ export const RowActionsMenu = ({ model }: RowActionsMenuProps) => {
25
24
  <DropdownMenu.VirtualTrigger virtualRef={model.modalController.trigger} />
26
25
  <DropdownMenu.Content>
27
26
  <DropdownMenu.Viewport>
28
- <DropdownMenu.Item onClick={() => model.deleteRow(state.rowIndex)}>
27
+ <DropdownMenu.Item data-testid='row-menu-delete' onClick={() => model.deleteRow(state.rowIndex)}>
29
28
  {t(hasSelection ? 'bulk delete row label' : 'delete row label')}
30
29
  </DropdownMenu.Item>
31
30
  </DropdownMenu.Viewport>
@@ -27,6 +27,8 @@ import { initializeTable } from '../../util';
27
27
  import { Toolbar } from '../Toolbar';
28
28
  import { createItems, createTable, type SimulatorProps, useSimulator } from '../testing';
29
29
 
30
+ faker.seed(0);
31
+
30
32
  //
31
33
  // Story components.
32
34
  //
@@ -42,7 +44,7 @@ const DefaultStory = () => {
42
44
  const table = tables[0];
43
45
  invariant(table.view);
44
46
  setTable(table);
45
- setSchema(space.db.schemaRegistry.getSchema(table.view.query.type));
47
+ setSchema(space.db.schemaRegistry.getSchema(table.view.query.typename));
46
48
  }
47
49
  }, [tables]);
48
50
 
@@ -189,7 +191,7 @@ const TablePerformanceStory = (props: StoryProps) => {
189
191
  //
190
192
 
191
193
  const meta: Meta<StoryProps> = {
192
- title: 'plugins/plugin-table/Table',
194
+ title: 'ui/react-ui-table/Table',
193
195
  component: Table.Main as any,
194
196
  render: DefaultStory,
195
197
  parameters: { translations },
@@ -200,8 +202,8 @@ const meta: Meta<StoryProps> = {
200
202
  createSpace: true,
201
203
  onSpaceCreated: async ({ space }) => {
202
204
  const table = space.db.add(create(TableType, {}));
203
- const schema = initializeTable({ space, table });
204
- Array.from({ length: 30 }).map(() => {
205
+ const schema = initializeTable({ space, table, initialRow: false });
206
+ Array.from({ length: 10 }).map(() => {
205
207
  return space.db.add(
206
208
  create(schema, {
207
209
  name: faker.person.fullName(),
@@ -48,8 +48,8 @@ const TableRoot = ({ children, role }: TableRootProps) => {
48
48
  className={mx(
49
49
  'relative border-bs !border-separator',
50
50
  role === 'section'
51
- ? 'attention-surface overflow-hidden [&_.dx-grid]:max-is-max'
52
- : 'flex flex-col [&_.dx-grid]:grow [&_.dx-grid]:max-is-max [&_.dx-grid]:bs-0 [&_.dx-grid]:max-bs-max',
51
+ ? 'attention-surface overflow-hidden [&_.dx-grid]:max-is-[--dx-grid-content-inline-size]'
52
+ : '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]',
53
53
  )}
54
54
  >
55
55
  {children}
@@ -72,7 +72,6 @@ export type TableMainProps = {
72
72
 
73
73
  const TableMain = forwardRef<TableController, TableMainProps>(({ model, ignoreAttention }, forwardedRef) => {
74
74
  const [dxGrid, setDxGrid] = useState<DxGridElement | null>(null);
75
-
76
75
  const { hasAttention } = useAttention(model?.table ? fullyQualifiedId(model.table) : 'table');
77
76
 
78
77
  /**
@@ -37,7 +37,7 @@ const DefaultStory = ({ editing }: StoryProps) => {
37
37
  const table = tables[0];
38
38
  invariant(table.view);
39
39
  setTable(table);
40
- setSchema(space.db.schemaRegistry.getSchema(table.view.query.type));
40
+ setSchema(space.db.schemaRegistry.getSchema(table.view.query.typename));
41
41
  }
42
42
  }, [tables]);
43
43
 
@@ -321,6 +321,7 @@ export class TableModel<T extends BaseTableRow = { id: string }> extends Resourc
321
321
  for (let col = range.start.col; col <= range.end.col && col < fields.length; col++) {
322
322
  const { field, props } = this._projection.getFieldProjection(fields[col].id);
323
323
  values[fromGridCell({ col, row: 0 })] = {
324
+ // TODO(burdon): Use same logic as form for fallback title.
324
325
  value: props.title ?? field.path,
325
326
  readonly: true,
326
327
  resizeHandle: 'col',
@@ -0,0 +1,60 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { type Page } from '@playwright/test';
6
+
7
+ import { DxGridManager } from '@dxos/lit-grid/testing';
8
+
9
+ const TABLE_SELECTORS = {
10
+ columnSettingsButton: 'table-column-settings-button',
11
+ rowMenuButton: 'table-row-menu-button',
12
+ newColumnButton: 'table-new-column-button',
13
+ selection: 'table-selection',
14
+ columnSort: {
15
+ ascending: 'column-sort-ascending',
16
+ descending: 'column-sort-descending',
17
+ },
18
+ columnDelete: 'column-delete',
19
+ rowDelete: 'row-menu-delete',
20
+ } as const;
21
+
22
+ export class TableManager {
23
+ private readonly _grid: DxGridManager;
24
+
25
+ constructor(private readonly page: Page) {
26
+ this._grid = new DxGridManager(page);
27
+ }
28
+
29
+ public get grid(): DxGridManager {
30
+ return this._grid;
31
+ }
32
+
33
+ public async deleteColumn(index: number): Promise<void> {
34
+ await this.page.getByTestId(TABLE_SELECTORS.columnSettingsButton).nth(index).click();
35
+ await this.page.getByTestId(TABLE_SELECTORS.columnDelete).click();
36
+ }
37
+
38
+ public async deleteRow(index = 0): Promise<void> {
39
+ await this.page.getByTestId(TABLE_SELECTORS.rowMenuButton).nth(index).click();
40
+ await this.page.getByTestId(TABLE_SELECTORS.rowDelete).click();
41
+ }
42
+
43
+ public async toggleSelectAll(): Promise<void> {
44
+ await this.page.getByTestId(TABLE_SELECTORS.selection).nth(0).click();
45
+ }
46
+
47
+ public async sortColumn(index: number, direction: 'ascending' | 'descending'): Promise<void> {
48
+ await this.page.getByTestId(TABLE_SELECTORS.columnSettingsButton).nth(index).click();
49
+ await this.page.getByTestId(TABLE_SELECTORS.columnSort[direction]).click();
50
+ }
51
+
52
+ public async addColumn({ label, format }: { label: string; format: 'number' }): Promise<void> {
53
+ await this.page.getByTestId(TABLE_SELECTORS.newColumnButton).click();
54
+ await this.page.getByRole('combobox').click();
55
+ await this.page.getByLabel(`format ${format}`).click();
56
+ await this.page.getByPlaceholder('Property label.').click();
57
+ await this.page.getByPlaceholder('Property label.').fill(label);
58
+ await this.page.getByRole('button', { name: 'button save' }).click();
59
+ }
60
+ }
@@ -0,0 +1,18 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import { nxE2EPreset } from '@nx/playwright/preset';
6
+ import { defineConfig } from '@playwright/test';
7
+
8
+ import { e2ePreset } from '@dxos/test-utils/playwright';
9
+
10
+ export default defineConfig({
11
+ ...nxE2EPreset(__filename, { testDir: __dirname }),
12
+ ...e2ePreset(__dirname),
13
+ webServer: {
14
+ command: 'pnpm -w nx storybook-e2e stories',
15
+ port: 9009,
16
+ reuseExistingServer: !process.env.CI,
17
+ },
18
+ });
@@ -0,0 +1,105 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { expect, test } from '@playwright/test';
6
+
7
+ import { setupPage } from '@dxos/test-utils/playwright';
8
+
9
+ import { TableManager } from './TableManager';
10
+
11
+ const storyId = 'ui-react-ui-table-table';
12
+ const storyUrl = `http://localhost:9009/iframe.html?id=${storyId}&viewMode=story`;
13
+
14
+ // NOTE(ZaymonFC): This test suite relies on the faker seed being set to 0 in the story.
15
+ test.describe('Table', () => {
16
+ test('Loads', async ({ browser }) => {
17
+ const { page } = await setupPage(browser, { url: storyUrl });
18
+ const table = new TableManager(page);
19
+
20
+ // (name, email, salary, manager) * 10 rows.
21
+ await table.grid.ready();
22
+ await expect(table.grid.cellsWithinPlane('grid')).toHaveCount(40);
23
+ await page.close();
24
+ });
25
+
26
+ test('sort', async ({ browser }) => {
27
+ const { page } = await setupPage(browser, { url: storyUrl });
28
+ const table = new TableManager(page);
29
+
30
+ await table.grid.ready();
31
+ await table.sortColumn(0, 'descending');
32
+ await expect(table.grid.cell(0, 0, 'grid')).toHaveText('Uwe Øvergård');
33
+ await expect(table.grid.cell(0, 9, 'grid')).toContainText('Anita');
34
+
35
+ await table.sortColumn(0, 'ascending');
36
+ await expect(table.grid.cell(0, 0, 'grid')).toContainText('Anita');
37
+ await expect(table.grid.cell(0, 9, 'grid')).toHaveText('Uwe Øvergård');
38
+ await page.close();
39
+ });
40
+
41
+ test('delete row', async ({ browser }) => {
42
+ const { page } = await setupPage(browser, { url: storyUrl });
43
+ const table = new TableManager(page);
44
+
45
+ await table.grid.ready();
46
+ await table.deleteRow(0);
47
+ await expect(page.getByRole('gridcell', { name: 'Anita Mayer' })).toHaveCount(0);
48
+ await page.close();
49
+ });
50
+
51
+ test('delete row--select all', async ({ browser }) => {
52
+ const { page } = await setupPage(browser, { url: storyUrl });
53
+ const table = new TableManager(page);
54
+
55
+ await table.grid.ready();
56
+ await table.toggleSelectAll();
57
+
58
+ // Delete action affects all selected rows.
59
+ await table.deleteRow(0);
60
+
61
+ await expect(page.getByRole('gridcell', { name: 'Anita Mayer' })).toHaveCount(0);
62
+ await expect(page.getByRole('gridcell', { name: 'Phonthip Sigurjónsson' })).toHaveCount(0);
63
+ await expect(table.grid.cellsWithinPlane('grid')).toHaveCount(0);
64
+ await page.close();
65
+ });
66
+
67
+ test('delete column', async ({ browser }) => {
68
+ const { page } = await setupPage(browser, { url: storyUrl });
69
+ const table = new TableManager(page);
70
+
71
+ await table.grid.ready();
72
+ await table.deleteColumn(0);
73
+ await expect(table.grid.cellsWithinPlane('grid')).toHaveCount(30);
74
+
75
+ await table.deleteColumn(0);
76
+ await expect(table.grid.cellsWithinPlane('grid')).toHaveCount(20);
77
+
78
+ await table.deleteColumn(0);
79
+ await expect(table.grid.cellsWithinPlane('grid')).toHaveCount(10);
80
+
81
+ await page.getByTestId('table-column-settings-button').nth(0).click();
82
+
83
+ // Ensure that the delete menu item is not visible when there is only one column left.
84
+ await expect(page.getByTestId('column-delete')).toHaveCount(0);
85
+ await page.close();
86
+ });
87
+
88
+ // Rest of add column test remains the same as it's a more complex flow
89
+ test('add column', async ({ browser }) => {
90
+ const { page } = await setupPage(browser, { url: storyUrl });
91
+ const table = new TableManager(page);
92
+
93
+ await table.grid.ready();
94
+ const newColumnLabel = 'TEST LABEL';
95
+
96
+ await table.addColumn({ label: newColumnLabel, format: 'number' });
97
+
98
+ // Delete first two columns to get the new column into view
99
+ await table.deleteColumn(0);
100
+ await table.deleteColumn(0);
101
+
102
+ await expect(page.getByRole('gridcell', { name: newColumnLabel })).toBeVisible();
103
+ await page.close();
104
+ });
105
+ });
@@ -6,8 +6,18 @@ import { ref, S, TypedObject } from '@dxos/echo-schema';
6
6
  import { ThreadType } from '@dxos/plugin-space';
7
7
  import { ViewType } from '@dxos/schema';
8
8
 
9
- export class TableType extends TypedObject({ typename: 'dxos.org/type/Table', version: '0.1.0' })({
9
+ export const TableSchema = S.Struct({
10
+ id: S.String,
10
11
  name: S.optional(S.String),
11
12
  view: S.optional(ref(ViewType)),
12
- threads: S.optional(S.mutable(S.Array(ref(ThreadType)))),
13
- }) {}
13
+ // TODO(burdon): Should not import from plugin. Either factor out type or use reverse deps when supported.
14
+ threads: S.optional(S.Array(ref(ThreadType))),
15
+ });
16
+
17
+ // type TableType = S.ClassType<typeof TableSchema>;
18
+
19
+ // TODO(burdon): UX should not depend on ECHO types.
20
+ export class TableType extends TypedObject({
21
+ typename: 'dxos.org/type/Table',
22
+ version: '0.1.0',
23
+ })(TableSchema.fields) {}
package/src/util/grid.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- import { type DxGridCellIndex } from '@dxos/react-ui-grid';
5
+ import { type DxGridPlaneCellIndex } from '@dxos/react-ui-grid';
6
6
 
7
7
  // TODO(burdon): Factor out to grid.
8
8
  export type GridCell = { col: number; row: number };
@@ -10,14 +10,14 @@ export type GridCell = { col: number; row: number };
10
10
  /**
11
11
  * Generates a string key for a cell based on its column and row indices.
12
12
  */
13
- export const fromGridCell = ({ col, row }: GridCell): DxGridCellIndex => {
13
+ export const fromGridCell = ({ col, row }: GridCell): DxGridPlaneCellIndex => {
14
14
  return `${col},${row}` as const; // TODO(burdon): ":" delim.
15
15
  };
16
16
 
17
17
  /**
18
18
  * Converts a cell key string back into column and row coordinates.
19
19
  */
20
- export const toGridCell = (cell: DxGridCellIndex): GridCell => {
20
+ export const toGridCell = (cell: DxGridPlaneCellIndex): GridCell => {
21
21
  const [col, row] = cell.split(',').map(Number);
22
22
  return { row, col };
23
23
  };
@@ -20,29 +20,34 @@ const createButtonHtml = ({
20
20
  button,
21
21
  value = '',
22
22
  disabled = false,
23
+ testId,
23
24
  }: {
24
25
  button: TableButton;
25
26
  value?: string;
26
27
  disabled?: boolean;
28
+ testId: string;
27
29
  }): string => {
28
30
  const buttonClasses = 'ch-button is-6 pli-0.5 min-bs-0 absolute inset-block-1 inline-end-2' as const;
29
- return `<button class="${buttonClasses}" ${TABLE_ATTRS[button]}="${value}" ${disabled ? 'disabled' : ''}><svg><use href="/icons.svg#${ICONS[button]}"/></svg></button>`;
31
+ return `<button class="${buttonClasses}" ${TABLE_ATTRS[button]}="${value}" data-testid="${testId}" ${disabled ? 'disabled' : ''}><svg><use href="/icons.svg#${ICONS[button]}"/></svg></button>`;
30
32
  };
31
33
 
32
34
  export const tableButtons = {
33
35
  columnSettings: {
34
36
  attr: TABLE_ATTRS.columnSettings,
35
37
  icon: ICONS.columnSettings,
36
- render: ({ fieldId }: { fieldId: string }) => createButtonHtml({ button: 'columnSettings', value: fieldId }),
38
+ render: ({ fieldId }: { fieldId: string }) =>
39
+ createButtonHtml({ button: 'columnSettings', value: fieldId, testId: 'table-column-settings-button' }),
37
40
  },
38
41
  rowMenu: {
39
42
  attr: TABLE_ATTRS.rowMenu,
40
43
  icon: ICONS.rowMenu,
41
- render: ({ rowIndex }: { rowIndex: number }) => createButtonHtml({ button: 'rowMenu', value: rowIndex.toString() }),
44
+ render: ({ rowIndex }: { rowIndex: number }) =>
45
+ createButtonHtml({ button: 'rowMenu', value: rowIndex.toString(), testId: 'table-row-menu-button' }),
42
46
  },
43
47
  addColumn: {
44
48
  attr: TABLE_ATTRS.newColumn,
45
49
  icon: ICONS.newColumn,
46
- render: ({ disabled }: { disabled?: boolean } = {}) => createButtonHtml({ button: 'newColumn', disabled }),
50
+ render: ({ disabled }: { disabled?: boolean } = {}) =>
51
+ createButtonHtml({ button: 'newColumn', disabled, testId: 'table-new-column-button' }),
47
52
  },
48
53
  } as const;
@@ -16,7 +16,7 @@ export const renderCheckbox = ({
16
16
  }: RenderCheckboxProps): string => {
17
17
  const baseClasses = 'absolute inset-block-[6px] inline-end-[6px] ch-checkbox' as const;
18
18
  const { attributes } = tableControls.checkbox;
19
- return `<input type="checkbox" class="${baseClasses}" ${attributes.checkbox}="${rowIndex}" ${header ? attributes.header : ''} ${checked ? 'checked' : ''} ${disabled ? 'disabled' : ''}/>`;
19
+ return `<input type="checkbox" class="${baseClasses}" ${attributes.checkbox}="${rowIndex}" ${header ? attributes.header : ''} ${checked ? 'checked' : ''} ${disabled ? 'disabled' : ''} data-testid="table-selection"/>`;
20
20
  };
21
21
 
22
22
  export const tableControls = {
package/src/util/table.ts CHANGED
@@ -20,9 +20,15 @@ import { createFieldId, createView, ViewProjection } from '@dxos/schema';
20
20
 
21
21
  import { type TableType } from '../types';
22
22
 
23
+ type InitialiseTableProps = {
24
+ space: Space;
25
+ table: TableType;
26
+ initialRow?: boolean;
27
+ };
28
+
23
29
  // TODO(burdon): Pass in type.
24
30
  // TODO(burdon): User should determine typename.
25
- export const initializeTable = ({ space, table }: { space: Space; table: TableType }): MutableSchema => {
31
+ export const initializeTable = ({ space, table, initialRow = true }: InitialiseTableProps): MutableSchema => {
26
32
  log.info('initializeTable', { table });
27
33
 
28
34
  const ContactSchema = TypedObject({
@@ -70,8 +76,10 @@ export const initializeTable = ({ space, table }: { space: Space; table: TableTy
70
76
  },
71
77
  });
72
78
 
73
- // TODO(burdon): Last (first) row should not be in db and should be managed by the model.
74
- space.db.add(create(contactSchema, {}));
79
+ if (initialRow) {
80
+ // TODO(burdon): Last (first) row should not be in db and should be managed by the model.
81
+ space.db.add(create(contactSchema, {}));
82
+ }
75
83
 
76
84
  return contactSchema;
77
85
  };
@@ -1,7 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": ["../../../src/types/table.ts"],
4
- "sourcesContent": ["//\n// Copyright 2024 DXOS.org\n//\n\nimport { ref, S, TypedObject } from '@dxos/echo-schema';\nimport { ThreadType } from '@dxos/plugin-space';\nimport { ViewType } from '@dxos/schema';\n\nexport class TableType extends TypedObject({ typename: 'dxos.org/type/Table', version: '0.1.0' })({\n name: S.optional(S.String),\n view: S.optional(ref(ViewType)),\n threads: S.optional(S.mutable(S.Array(ref(ThreadType)))),\n}) {}\n"],
5
- "mappings": ";AAIA,SAASA,KAAKC,GAAGC,mBAAmB;AACpC,SAASC,kBAAkB;AAC3B,SAASC,gBAAgB;AAElB,IAAMC,YAAN,cAAwBC,YAAY;EAAEC,UAAU;EAAuBC,SAAS;AAAQ,CAAA,EAAG;EAChGC,MAAMC,EAAEC,SAASD,EAAEE,MAAM;EACzBC,MAAMH,EAAEC,SAASG,IAAIC,QAAAA,CAAAA;EACrBC,SAASN,EAAEC,SAASD,EAAEO,QAAQP,EAAEQ,MAAMJ,IAAIK,UAAAA,CAAAA,CAAAA,CAAAA;AAC5C,CAAA,EAAA;AAAI;",
6
- "names": ["ref", "S", "TypedObject", "ThreadType", "ViewType", "TableType", "TypedObject", "typename", "version", "name", "S", "optional", "String", "view", "ref", "ViewType", "threads", "mutable", "Array", "ThreadType"]
7
- }
@@ -1,7 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": ["../../../src/types/table.ts"],
4
- "sourcesContent": ["//\n// Copyright 2024 DXOS.org\n//\n\nimport { ref, S, TypedObject } from '@dxos/echo-schema';\nimport { ThreadType } from '@dxos/plugin-space';\nimport { ViewType } from '@dxos/schema';\n\nexport class TableType extends TypedObject({ typename: 'dxos.org/type/Table', version: '0.1.0' })({\n name: S.optional(S.String),\n view: S.optional(ref(ViewType)),\n threads: S.optional(S.mutable(S.Array(ref(ThreadType)))),\n}) {}\n"],
5
- "mappings": ";;;;;;;;;;;;;;;;;;;;;;;AAIA,yBAAoC;AACpC,0BAA2B;AAC3B,oBAAyB;AAElB,IAAMA,YAAN,kBAAwBC,gCAAY;EAAEC,UAAU;EAAuBC,SAAS;AAAQ,CAAA,EAAG;EAChGC,MAAMC,qBAAEC,SAASD,qBAAEE,MAAM;EACzBC,MAAMH,qBAAEC,aAASG,wBAAIC,sBAAAA,CAAAA;EACrBC,SAASN,qBAAEC,SAASD,qBAAEO,QAAQP,qBAAEQ,UAAMJ,wBAAIK,8BAAAA,CAAAA,CAAAA,CAAAA;AAC5C,CAAA,EAAA;AAAI;",
6
- "names": ["TableType", "TypedObject", "typename", "version", "name", "S", "optional", "String", "view", "ref", "ViewType", "threads", "mutable", "Array", "ThreadType"]
7
- }