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

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 (68) hide show
  1. package/dist/lib/browser/{chunk-TL7CKQQV.mjs → chunk-Q5QQXJUO.mjs} +2 -2
  2. package/dist/lib/browser/index.mjs +552 -227
  3. package/dist/lib/browser/index.mjs.map +4 -4
  4. package/dist/lib/browser/meta.json +1 -1
  5. package/dist/lib/browser/types/index.mjs +1 -1
  6. package/dist/lib/node-esm/{chunk-GBT6GD4G.mjs → chunk-ZNOU6LHG.mjs} +2 -2
  7. package/dist/lib/node-esm/index.mjs +552 -227
  8. package/dist/lib/node-esm/index.mjs.map +4 -4
  9. package/dist/lib/node-esm/meta.json +1 -1
  10. package/dist/lib/node-esm/types/index.mjs +1 -1
  11. package/dist/types/src/components/Table/DynamicTable.stories.d.ts +1 -1
  12. package/dist/types/src/components/Table/DynamicTable.stories.d.ts.map +1 -1
  13. package/dist/types/src/components/Table/Table.d.ts.map +1 -1
  14. package/dist/types/src/components/Table/Table.stories.d.ts +1 -1
  15. package/dist/types/src/components/Table/Table.stories.d.ts.map +1 -1
  16. package/dist/types/src/components/Table/relations.stories.d.ts +1 -1
  17. package/dist/types/src/components/Table/relations.stories.d.ts.map +1 -1
  18. package/dist/types/src/components/TableCellEditor/CellValidationMessage.d.ts +2 -1
  19. package/dist/types/src/components/TableCellEditor/CellValidationMessage.d.ts.map +1 -1
  20. package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts.map +1 -1
  21. package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts +1 -1
  22. package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts.map +1 -1
  23. package/dist/types/src/components/testing.d.ts +2 -2
  24. package/dist/types/src/hooks/index.d.ts +1 -0
  25. package/dist/types/src/hooks/index.d.ts.map +1 -1
  26. package/dist/types/src/hooks/useAddRow.d.ts +13 -0
  27. package/dist/types/src/hooks/useAddRow.d.ts.map +1 -0
  28. package/dist/types/src/index.d.ts +1 -1
  29. package/dist/types/src/index.d.ts.map +1 -1
  30. package/dist/types/src/model/table-model.d.ts +42 -6
  31. package/dist/types/src/model/table-model.d.ts.map +1 -1
  32. package/dist/types/src/model/table-presentation.d.ts +8 -0
  33. package/dist/types/src/model/table-presentation.d.ts.map +1 -1
  34. package/dist/types/src/playwright/playwright.config.d.ts +3 -0
  35. package/dist/types/src/playwright/playwright.config.d.ts.map +1 -0
  36. package/dist/types/src/translations.d.ts +3 -4
  37. package/dist/types/src/translations.d.ts.map +1 -1
  38. package/dist/types/src/types/table.d.ts +4 -4
  39. package/dist/types/src/util/table-buttons.d.ts +15 -1
  40. package/dist/types/src/util/table-buttons.d.ts.map +1 -1
  41. package/dist/types/tsconfig.tsbuildinfo +1 -1
  42. package/package.json +38 -40
  43. package/src/components/Table/DynamicTable.stories.tsx +2 -2
  44. package/src/components/Table/Table.stories.tsx +18 -12
  45. package/src/components/Table/Table.tsx +17 -4
  46. package/src/components/Table/relations.stories.tsx +4 -5
  47. package/src/components/TableCellEditor/CellValidationMessage.tsx +6 -1
  48. package/src/components/TableCellEditor/TableCellEditor.stories.tsx +3 -3
  49. package/src/components/TableCellEditor/TableCellEditor.tsx +54 -15
  50. package/src/hooks/index.ts +1 -0
  51. package/src/hooks/useAddRow.ts +36 -0
  52. package/src/index.ts +1 -1
  53. package/src/model/table-model.ts +220 -34
  54. package/src/model/table-presentation.ts +213 -130
  55. package/src/playwright/playwright.config.ts +17 -0
  56. package/src/playwright/smoke.spec.ts +9 -7
  57. package/src/translations.ts +5 -3
  58. package/src/util/table-buttons.ts +24 -1
  59. package/dist/lib/node/chunk-O2VPBNS5.cjs +0 -50
  60. package/dist/lib/node/chunk-O2VPBNS5.cjs.map +0 -7
  61. package/dist/lib/node/index.cjs +0 -2739
  62. package/dist/lib/node/index.cjs.map +0 -7
  63. package/dist/lib/node/meta.json +0 -1
  64. package/dist/lib/node/types/index.cjs +0 -31
  65. package/dist/lib/node/types/index.cjs.map +0 -7
  66. package/src/playwright/playwright.config.cts +0 -18
  67. /package/dist/lib/browser/{chunk-TL7CKQQV.mjs.map → chunk-Q5QQXJUO.mjs.map} +0 -0
  68. /package/dist/lib/node-esm/{chunk-GBT6GD4G.mjs.map → chunk-ZNOU6LHG.mjs.map} +0 -0
@@ -6,6 +6,7 @@ import { computed, effect, signal, type ReadonlySignal } from '@preact/signals-c
6
6
 
7
7
  import { type Space } from '@dxos/client/echo';
8
8
  import { Resource } from '@dxos/context';
9
+ import { Ref } from '@dxos/echo';
9
10
  import {
10
11
  type FieldSortType,
11
12
  FormatEnum,
@@ -14,9 +15,11 @@ import {
14
15
  type JsonProp,
15
16
  getSnapshot,
16
17
  getSchema,
18
+ toEffectSchema,
17
19
  } from '@dxos/echo-schema';
18
20
  import { invariant } from '@dxos/invariant';
19
- import { isLiveObject, makeRef } from '@dxos/live-object';
21
+ import { ObjectId } from '@dxos/keys';
22
+ import { isLiveObject } from '@dxos/live-object';
20
23
  import { formatForEditing, parseValue } from '@dxos/react-ui-form';
21
24
  import {
22
25
  type DxGridAxisMeta,
@@ -24,16 +27,31 @@ import {
24
27
  type DxGridPlanePosition,
25
28
  type DxGridPosition,
26
29
  } from '@dxos/react-ui-grid';
27
- import { type ViewType, type ViewProjection, type PropertyType, validateSchema } from '@dxos/schema';
30
+ import {
31
+ type ViewType,
32
+ type ViewProjection,
33
+ type PropertyType,
34
+ validateSchema,
35
+ type ValidationError,
36
+ } from '@dxos/schema';
28
37
 
29
38
  import { type SelectionMode, SelectionModel } from './selection-model';
30
39
  import { TableSorting } from './table-sorting';
31
40
  import { touch } from '../util';
32
41
  import { extractTagIds } from '../util/tag';
33
42
 
43
+ // Domain types for cell classification
44
+ export type TableCellType = 'standard' | 'draft' | 'header';
45
+
34
46
  // TODO(ZaymonFC): Use a common type?
35
47
  export type ValidationResult = { valid: true } | { valid: false; error: string };
36
48
 
49
+ export type DraftRow<T extends TableRow = TableRow> = {
50
+ data: T;
51
+ valid: boolean;
52
+ validationErrors: ValidationError[];
53
+ };
54
+
37
55
  // TODO(burdon): Use schema types.
38
56
  export type TableRow = Record<JsonProp, any> & { id: string };
39
57
 
@@ -63,7 +81,7 @@ export type TableModelProps<T extends TableRow = TableRow> = {
63
81
  sorting?: FieldSortType[];
64
82
  pinnedRows?: { top: number[]; bottom: number[] };
65
83
  rowActions?: TableRowAction[];
66
- onInsertRow?: (index?: number) => void;
84
+ onInsertRow?: (data?: any) => boolean;
67
85
  onDeleteRows?: (index: number, obj: T[]) => void;
68
86
  onDeleteColumn?: (fieldId: string) => void;
69
87
  onCellUpdate?: (cell: DxGridPosition) => void;
@@ -82,7 +100,7 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
82
100
  end: { row: 0, col: 0 },
83
101
  });
84
102
 
85
- private readonly _onInsertRow?: TableModelProps<T>['onInsertRow'];
103
+ private readonly _onInsertRow?: (data?: any) => boolean;
86
104
  private readonly _onDeleteRows?: TableModelProps<T>['onDeleteRows'];
87
105
  private readonly _onDeleteColumn?: TableModelProps<T>['onDeleteColumn'];
88
106
  private readonly _onCellUpdate?: TableModelProps<T>['onCellUpdate'];
@@ -92,6 +110,7 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
92
110
  private readonly _features: TableFeatures;
93
111
 
94
112
  private readonly _rows = signal<T[]>([]);
113
+ private readonly _draftRows = signal<DraftRow<T>[]>([]);
95
114
  private readonly _sorting: TableSorting<T>;
96
115
 
97
116
  private _pinnedRows: NonNullable<TableModelProps<T>['pinnedRows']>;
@@ -265,6 +284,13 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
265
284
  return () => rowEffects.forEach((cleanup) => cleanup());
266
285
  });
267
286
  this._ctx.onDispose(rowEffectManager);
287
+
288
+ const draftRowsWatcher = effect(() => {
289
+ touch(this._draftRows.value);
290
+ // Notify grid that draft rows have changed
291
+ this._onRowOrderChange?.();
292
+ });
293
+ this._ctx.onDispose(draftRowsWatcher);
268
294
  }
269
295
 
270
296
  //
@@ -277,11 +303,101 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
277
303
 
278
304
  public getRowCount = (): number => this._rows.value.length;
279
305
 
306
+ /**
307
+ * @reactive
308
+ */
309
+ public getDraftRowCount = (): number => this._draftRows.value.length;
310
+
311
+ // TODO(ZaymonFC): Return .value instead of exposing the signal.
312
+ public get draftRows(): ReadonlySignal<DraftRow<T>[]> {
313
+ return this._draftRows;
314
+ }
315
+
316
+ /**
317
+ * Checks if a specific field path has validation errors for a given draft row.
318
+ * @param draftRowIndex - The index of the draft row to check
319
+ * @param fieldPath - The path of the field to check for validation errors
320
+ * @returns true if the field has validation errors, false otherwise
321
+ */
322
+ public hasDraftRowValidationError(draftRowIndex: number, fieldPath: string): boolean {
323
+ if (draftRowIndex < 0 || draftRowIndex >= this._draftRows.value.length) {
324
+ return false;
325
+ }
326
+
327
+ const draftRow = this._draftRows.value[draftRowIndex];
328
+ const validationErrors = draftRow.validationErrors || [];
329
+ return validationErrors.some((error) => error.path === fieldPath);
330
+ }
331
+
280
332
  public getColumnCount = (): number => this._view?.fields.length ?? 0;
281
333
 
282
- public insertRow = (rowIndex?: number): void => {
283
- const row = rowIndex !== undefined ? this._sorting.getDataIndex(rowIndex) : this._rows.value.length;
284
- this._onInsertRow?.(row);
334
+ public insertRow = (): void => {
335
+ const result = this._onInsertRow?.();
336
+ if (result === false && this._draftRows.value.length === 0) {
337
+ this.createDraftRow();
338
+ }
339
+ };
340
+
341
+ private createDraftRow(): void {
342
+ if (!this._view) {
343
+ return;
344
+ }
345
+
346
+ // NOTE(ZaymonFC): This is initialized with id because it's required in all schemata?
347
+ const draftData = { id: ObjectId.random() } as any as T;
348
+ const validationErrors = this.validateDraftRowData(draftData);
349
+
350
+ const draftRow: DraftRow<T> = {
351
+ data: draftData,
352
+ valid: validationErrors.length === 0,
353
+ validationErrors,
354
+ };
355
+
356
+ this._draftRows.value = [...this._draftRows.value, draftRow];
357
+ }
358
+
359
+ private validateDraftRow(draftRowIndex: number): void {
360
+ if (draftRowIndex < 0 || draftRowIndex >= this._draftRows.value.length) {
361
+ return;
362
+ }
363
+
364
+ const draftRow = this._draftRows.value[draftRowIndex];
365
+ const validationErrors = this.validateDraftRowData(draftRow.data);
366
+
367
+ const updatedDraftRow = {
368
+ ...draftRow,
369
+ valid: validationErrors.length === 0,
370
+ validationErrors,
371
+ };
372
+
373
+ const newDraftRows = [...this._draftRows.value];
374
+ newDraftRows[draftRowIndex] = updatedDraftRow;
375
+ this._draftRows.value = newDraftRows;
376
+ }
377
+
378
+ private validateDraftRowData(data: T): ValidationError[] {
379
+ const schema = toEffectSchema(this._projection.schema);
380
+ return validateSchema(schema, data) || [];
381
+ }
382
+
383
+ public commitDraftRow = (draftRowIndex: number): boolean => {
384
+ if (draftRowIndex < 0 || draftRowIndex >= this._draftRows.value.length) {
385
+ return false;
386
+ }
387
+
388
+ const draftRow = this._draftRows.value[draftRowIndex];
389
+ if (!draftRow.valid) {
390
+ return false;
391
+ }
392
+
393
+ const success = this._onInsertRow?.(draftRow.data);
394
+
395
+ if (success) {
396
+ const newDraftRows = this._draftRows.value.filter((_, index) => index !== draftRowIndex);
397
+ this._draftRows.value = newDraftRows;
398
+ }
399
+
400
+ return success ?? false;
285
401
  };
286
402
 
287
403
  public deleteRow = (rowIndex: number): void => {
@@ -312,15 +428,29 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
312
428
  }
313
429
  };
314
430
 
315
- public getCellData = ({ col, row }: DxGridPlanePosition): any => {
431
+ public getCellData = (cell: DxGridPosition): any => {
432
+ const { col, row, plane } = cell;
316
433
  const fields = this._view?.fields ?? [];
317
434
  if (col < 0 || col >= fields.length) {
318
435
  return undefined;
319
436
  }
320
437
 
321
438
  const field = fields[col];
322
- const dataIndex = this._sorting.getDataIndex(row);
323
- const value = getValue(this._rows.value[dataIndex], field.path);
439
+ let value: any;
440
+
441
+ if (plane === 'frozenRowsEnd') {
442
+ // Get data from draft row
443
+ if (row >= 0 && row < this._draftRows.value.length) {
444
+ value = getValue(this._draftRows.value[row].data, field.path);
445
+ } else {
446
+ return undefined;
447
+ }
448
+ } else {
449
+ // Get data from regular row
450
+ const dataIndex = this._sorting.getDataIndex(row);
451
+ value = getValue(this._rows.value[dataIndex], field.path);
452
+ }
453
+
324
454
  if (value == null) {
325
455
  return '';
326
456
  }
@@ -341,8 +471,7 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
341
471
  }
342
472
  };
343
473
 
344
- public validateCellData = async ({ col, row }: DxGridPlanePosition, value: any): Promise<ValidationResult> => {
345
- const rowIdx = this._sorting.getDataIndex(row);
474
+ public validateCellData = async ({ col, row, plane }: DxGridPosition, value: any): Promise<ValidationResult> => {
346
475
  const fields = this._view?.fields ?? [];
347
476
  if (col < 0 || col >= fields.length) {
348
477
  return { valid: false, error: 'Invalid column index' };
@@ -350,32 +479,52 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
350
479
 
351
480
  const field = fields[col];
352
481
  const { props } = this._projection.getFieldProjection(field.id);
482
+ const transformedValue = editorTextToCellValue(props, value);
353
483
 
354
- const currentRow = this._rows.value[rowIdx];
355
- invariant(currentRow, 'Invalid row index');
484
+ const isDraftRow = plane === 'frozenRowsEnd';
356
485
 
357
- // Get a snapshot of the current object.
358
- const snapshot = getSnapshot(currentRow);
359
- const transformedValue = editorTextToCellValue(props, value);
486
+ if (isDraftRow) {
487
+ const isInvalidDraftRowIndex = row < 0 || row >= this._draftRows.value.length;
488
+ if (isInvalidDraftRowIndex) {
489
+ return { valid: false, error: 'Invalid draft row index' };
490
+ }
491
+
492
+ const draftRow = this._draftRows.value[row];
493
+ const snapshot = { ...draftRow.data };
494
+ setValue(snapshot, field.path, transformedValue);
495
+
496
+ const validationErrors = this.validateDraftRowData(snapshot);
497
+ if (validationErrors.length > 0) {
498
+ const error = validationErrors.find((err) => err.path === field.path);
499
+ if (error) {
500
+ return { valid: false, error: error.message };
501
+ }
502
+ }
503
+ return { valid: true };
504
+ } else {
505
+ const rowIdx = this._sorting.getDataIndex(row);
506
+ const currentRow = this._rows.value[rowIdx];
507
+ invariant(currentRow, 'Invalid row index');
360
508
 
361
- // Set the proposed value.
362
- setValue(snapshot, field.path, transformedValue);
509
+ const snapshot = getSnapshot(currentRow);
510
+ setValue(snapshot, field.path, transformedValue);
363
511
 
364
- const schema = getSchema(currentRow);
365
- invariant(schema);
512
+ const schema = getSchema(currentRow);
513
+ invariant(schema);
366
514
 
367
- const validationResult = validateSchema(schema, snapshot);
368
- if (validationResult && validationResult.length > 0) {
369
- const error = validationResult[0];
370
- invariant(error.path === field.path);
371
- return { valid: false, error: error.message };
372
- }
515
+ const validationResult = validateSchema(schema, snapshot);
516
+ if (validationResult && validationResult.length > 0) {
517
+ const error = validationResult[0];
518
+ invariant(error.path === field.path);
519
+ return { valid: false, error: error.message };
520
+ }
373
521
 
374
- return { valid: true };
522
+ return { valid: true };
523
+ }
375
524
  };
376
525
 
377
- public setCellData = ({ col, row }: DxGridPlanePosition, value: any): void => {
378
- const rowIdx = this._sorting.getDataIndex(row);
526
+ public setCellData = (cell: DxGridPosition, value: any): void => {
527
+ const { col, row, plane } = cell;
379
528
  const fields = this._view?.fields ?? [];
380
529
  if (col < 0 || col >= fields.length) {
381
530
  return;
@@ -387,12 +536,21 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
387
536
 
388
537
  // Special handling for Ref format to preserve existing behavior
389
538
  if (props.format === FormatEnum.Ref && !isLiveObject(value)) {
390
- // TODO(ZaymonFC): This get's called an additional time by the cell editor onBlur, but with the cell editors
391
- // plain string value. Maybe onBlur should be called with the actual value?
392
539
  return;
393
540
  }
394
541
 
395
- setValue(this._rows.value[rowIdx], field.path, transformedValue);
542
+ if (plane === 'frozenRowsEnd') {
543
+ // Update draft row data
544
+ if (row >= 0 && row < this._draftRows.value.length) {
545
+ setValue(this._draftRows.value[row].data, field.path, transformedValue);
546
+ // Re-validate the draft row after the update
547
+ this.validateDraftRow(row);
548
+ }
549
+ } else {
550
+ // Update regular row data
551
+ const rowIdx = this._sorting.getDataIndex(row);
552
+ setValue(this._rows.value[rowIdx], field.path, transformedValue);
553
+ }
396
554
  };
397
555
 
398
556
  /**
@@ -410,6 +568,33 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
410
568
  setValue(this._rows.value[dataRow], field.path, updatedValue);
411
569
  }
412
570
 
571
+ /**
572
+ * Gets the domain-appropriate cell type for a given cell position.
573
+ * This abstracts away the low-level grid plane concepts and provides domain-specific types.
574
+ * @param cell - The cell position to classify
575
+ * @returns The domain-appropriate cell type
576
+ */
577
+ public getCellType(cell: DxGridPosition): TableCellType {
578
+ switch (cell.plane) {
579
+ case 'frozenRowsEnd':
580
+ return 'draft';
581
+ case 'frozenRowsStart':
582
+ return 'header';
583
+ case 'grid':
584
+ default:
585
+ return 'standard';
586
+ }
587
+ }
588
+
589
+ /**
590
+ * Convenience method to check if a cell is a draft cell.
591
+ * @param cell - The cell position to check
592
+ * @returns true if the cell is a draft cell, false otherwise
593
+ */
594
+ public isDraftCell(cell: DxGridPosition): boolean {
595
+ return this.getCellType(cell) === 'draft';
596
+ }
597
+
413
598
  //
414
599
  // Column Operations
415
600
  //
@@ -457,6 +642,7 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
457
642
  //
458
643
  // View operations
459
644
  //
645
+
460
646
  public saveView(): void {
461
647
  this._sorting.save();
462
648
  }
@@ -466,7 +652,7 @@ const editorTextToCellValue = (props: PropertyType, value: any): any => {
466
652
  switch (props.format) {
467
653
  case FormatEnum.Ref: {
468
654
  if (isLiveObject(value)) {
469
- return makeRef(value);
655
+ return Ref.make(value);
470
656
  } else {
471
657
  return value;
472
658
  }