@xh/hoist 75.0.0-SNAPSHOT.1753731626581 → 75.0.0-SNAPSHOT.1754335298677

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.
package/CHANGELOG.md CHANGED
@@ -23,6 +23,12 @@
23
23
  which `dimensions` are provided to the model.
24
24
  * Added new `ClipboardButton.errorMessage` prop to customize or suppress a toast alert if the copy
25
25
  operation fails. Set to `false` to fail silently (the behavior prior to this change).
26
+ * Added new `Cube.modifyRecordsAsync` for modifying individual field values in a local uncommitted
27
+ state. Additionally enhanced `Store.modifyRecords` to return a `StoreChangeLog` of updates.
28
+
29
+ ### 🐞 Bug Fixes
30
+ * Fixed bugs where `Store.modifyRecords`, `Store.revertRecords` and `Store.revert` were not properly
31
+ handling changes to `SummaryRecords`.
26
32
 
27
33
  ### 🐞 Bug Fixes
28
34
 
@@ -110,7 +110,7 @@ export declare class DataViewModel extends HoistModel {
110
110
  ensureSelectionVisibleAsync(): Promise<void>;
111
111
  doLoadAsync(loadSpec: LoadSpec): Promise<any>;
112
112
  loadData(rawData: any[], rawSummaryData?: PlainObject): void;
113
- updateData(rawData: PlainObject[] | StoreTransaction): PlainObject;
113
+ updateData(rawData: PlainObject[] | StoreTransaction): import("@xh/hoist/data").StoreChangeLog;
114
114
  clear(): void;
115
115
  setGroupBy(colIds: Some<string>): void;
116
116
  setSortBy(sorters: Some<GridSorterLike>): void;
@@ -450,7 +450,7 @@ export declare class GridModel extends HoistModel {
450
450
  /** Load the underlying store. */
451
451
  loadData(rawData: any[], rawSummaryData?: Some<PlainObject>): void;
452
452
  /** Update the underlying store. */
453
- updateData(rawData: PlainObject[] | StoreTransaction): PlainObject;
453
+ updateData(rawData: PlainObject[] | StoreTransaction): import("@xh/hoist/data").StoreChangeLog;
454
454
  /** Clear the underlying store, removing all rows. */
455
455
  clear(): void;
456
456
  /** @param colConfigs - {@link Column} or {@link ColumnGroup} configs. */
@@ -243,7 +243,7 @@ export declare class ZoneGridModel extends HoistModel {
243
243
  ensureSelectionVisibleAsync(): Promise<void>;
244
244
  doLoadAsync(loadSpec: LoadSpec): Promise<any>;
245
245
  loadData(rawData: any[], rawSummaryData?: Some<PlainObject>): void;
246
- updateData(rawData: PlainObject[] | StoreTransaction): PlainObject;
246
+ updateData(rawData: PlainObject[] | StoreTransaction): import("@xh/hoist/data").StoreChangeLog;
247
247
  clear(): void;
248
248
  setGroupBy(colIds: Some<string>): void;
249
249
  private createGridModel;
@@ -104,6 +104,16 @@ export interface StoreTransaction {
104
104
  */
105
105
  rawSummaryData?: Some<PlainObject>;
106
106
  }
107
+ /**
108
+ * Collection of changes made to a Store's RecordSet. Unlike `StoreTransaction` which is used to
109
+ * specify changes, this object is used to report the actual changes made in a single transaction.
110
+ */
111
+ export interface StoreChangeLog {
112
+ update?: StoreRecord[];
113
+ add?: StoreRecord[];
114
+ remove?: StoreRecordId[];
115
+ summaryRecords?: StoreRecord[];
116
+ }
107
117
  export interface ChildRawData {
108
118
  /** ID of the pre-existing parent record. */
109
119
  parentId: string;
@@ -197,7 +207,7 @@ export declare class Store extends HoistBase {
197
207
  * into adds and updates, with updates determined by matching existing records by ID.
198
208
  * @returns changes applied, or null if no record changes were made.
199
209
  */
200
- updateData(rawData: PlainObject[] | StoreTransaction): PlainObject;
210
+ updateData(rawData: PlainObject[] | StoreTransaction): StoreChangeLog;
201
211
  /**
202
212
  * Re-runs the Filter on the current data. Applications only need to call this method if
203
213
  * the state underlying the filter, other than the record data itself, has changed. Store will
@@ -242,8 +252,9 @@ export declare class Store extends HoistBase {
242
252
  * Records in this Store. Each object in the list must have an `id` property identifying
243
253
  * the StoreRecord to modify, plus any other properties with updated field values to apply,
244
254
  * e.g. `{id: 4, quantity: 100}, {id: 5, quantity: 99, customer: 'bob'}`.
255
+ * @returns changes applied, or null if no record changes were made.
245
256
  */
246
- modifyRecords(modifications: Some<PlainObject>): void;
257
+ modifyRecords(modifications: Some<PlainObject>): StoreChangeLog;
247
258
  /**
248
259
  * Revert all changes made to the specified Records since they were last committed.
249
260
  *
@@ -396,4 +407,5 @@ export declare class Store extends HoistBase {
396
407
  private createFieldMap;
397
408
  private parseExperimental;
398
409
  private parseIdSpec;
410
+ private revertSummaryRecords;
399
411
  }
@@ -1,4 +1,4 @@
1
- import { HoistBase, PlainObject } from '@xh/hoist/core';
1
+ import { HoistBase, PlainObject, Some } from '@xh/hoist/core';
2
2
  import { CubeField, CubeFieldSpec } from './CubeField';
3
3
  import { QueryConfig } from './Query';
4
4
  import { View } from './View';
@@ -126,6 +126,19 @@ export declare class Cube extends HoistBase {
126
126
  * @param infoUpdates - new key-value pairs to be applied to existing info on this cube.
127
127
  */
128
128
  updateDataAsync(rawData: PlainObject[] | StoreTransaction, infoUpdates?: PlainObject): Promise<void>;
129
+ /**
130
+ * Similar to `updateDataAsync`, but intended for modifying individual field values in a local
131
+ * uncommitted state - i.e. when updating via an inline grid editor or similar control. Like
132
+ * `updateDataAsync`, this method will update its views asynchronously.
133
+ *
134
+ * This method largely delegates to {@link Store.modifyRecords} - see that method for more info.
135
+ *
136
+ * @param modifications - field-level modifications to apply to existing
137
+ * Records in this Cube. Each object in the list must have an `id` property identifying
138
+ * the StoreRecord to modify, plus any other properties with updated field values to apply,
139
+ * e.g. `{id: 4, quantity: 100}, {id: 5, quantity: 99, customer: 'bob'}`.
140
+ */
141
+ modifyRecordsAsync(modifications: Some<PlainObject>): Promise<void>;
129
142
  /** Clear any/all data and info from this Cube. */
130
143
  clearAsync(): Promise<void>;
131
144
  /**
package/data/Store.ts CHANGED
@@ -24,7 +24,8 @@ import {
24
24
  remove as lodashRemove,
25
25
  uniq,
26
26
  first,
27
- some
27
+ some,
28
+ partition
28
29
  } from 'lodash';
29
30
  import {Field, FieldSpec} from './Field';
30
31
  import {parseFilter} from './filter/Utils';
@@ -153,6 +154,17 @@ export interface StoreTransaction {
153
154
  rawSummaryData?: Some<PlainObject>;
154
155
  }
155
156
 
157
+ /**
158
+ * Collection of changes made to a Store's RecordSet. Unlike `StoreTransaction` which is used to
159
+ * specify changes, this object is used to report the actual changes made in a single transaction.
160
+ */
161
+ export interface StoreChangeLog {
162
+ update?: StoreRecord[];
163
+ add?: StoreRecord[];
164
+ remove?: StoreRecordId[];
165
+ summaryRecords?: StoreRecord[];
166
+ }
167
+
156
168
  export interface ChildRawData {
157
169
  /** ID of the pre-existing parent record. */
158
170
  parentId: string;
@@ -348,13 +360,13 @@ export class Store extends HoistBase {
348
360
  */
349
361
  @action
350
362
  @logWithDebug
351
- updateData(rawData: PlainObject[] | StoreTransaction): PlainObject {
363
+ updateData(rawData: PlainObject[] | StoreTransaction): StoreChangeLog {
352
364
  if (isEmpty(rawData)) return null;
353
365
 
354
- const changeLog: PlainObject = {};
366
+ const changeLog: StoreChangeLog = {};
355
367
 
356
368
  // Build a transaction object out of a flat list of adds and updates
357
- let rawTransaction;
369
+ let rawTransaction: StoreTransaction;
358
370
  if (isArray(rawData)) {
359
371
  const update = [],
360
372
  add = [];
@@ -381,7 +393,7 @@ export class Store extends HoistBase {
381
393
  throwIf(!isEmpty(other), 'Unknown argument(s) passed to updateData().');
382
394
 
383
395
  // 1) Pre-process updates and adds into Records
384
- let updateRecs, addRecs;
396
+ let updateRecs: StoreRecord[], addRecs: Map<StoreRecordId, StoreRecord>;
385
397
  if (update) {
386
398
  updateRecs = update.map(it => {
387
399
  const recId = this.idSpec(it),
@@ -426,7 +438,11 @@ export class Store extends HoistBase {
426
438
  }
427
439
 
428
440
  // 3) Apply changes
429
- let rsTransaction: any = {};
441
+ let rsTransaction: {
442
+ update?: StoreRecord[];
443
+ add?: StoreRecord[];
444
+ remove?: StoreRecordId[];
445
+ } = {};
430
446
  if (!isEmpty(updateRecs)) rsTransaction.update = updateRecs;
431
447
  if (!isEmpty(addRecs)) rsTransaction.add = Array.from(addRecs.values());
432
448
  if (!isEmpty(remove)) rsTransaction.remove = remove;
@@ -545,13 +561,15 @@ export class Store extends HoistBase {
545
561
  * Records in this Store. Each object in the list must have an `id` property identifying
546
562
  * the StoreRecord to modify, plus any other properties with updated field values to apply,
547
563
  * e.g. `{id: 4, quantity: 100}, {id: 5, quantity: 99, customer: 'bob'}`.
564
+ * @returns changes applied, or null if no record changes were made.
548
565
  */
549
566
  @action
550
- modifyRecords(modifications: Some<PlainObject>) {
567
+ modifyRecords(modifications: Some<PlainObject>): StoreChangeLog {
551
568
  modifications = castArray(modifications);
552
569
  if (isEmpty(modifications)) return;
553
570
 
554
- const updateRecs = new Map();
571
+ // 1) Pre-process modifications into Records
572
+ const updateMap = new Map<StoreRecordId, StoreRecord>();
555
573
  let hadDupes = false;
556
574
  modifications.forEach(mod => {
557
575
  let {id} = mod;
@@ -559,7 +577,7 @@ export class Store extends HoistBase {
559
577
  // Ignore multiple updates for the same record - we are updating this Store in a
560
578
  // transaction after processing all modifications, so this method is not currently setup
561
579
  // to process more than one update for a given rec at a time.
562
- if (updateRecs.has(id)) {
580
+ if (updateMap.has(id)) {
563
581
  hadDupes = true;
564
582
  return;
565
583
  }
@@ -573,24 +591,45 @@ export class Store extends HoistBase {
573
591
  data: updatedData,
574
592
  parent: currentRec.parent,
575
593
  store: currentRec.store,
576
- committedData: currentRec.committedData
594
+ committedData: currentRec.committedData,
595
+ isSummary: currentRec.isSummary
577
596
  });
578
597
 
579
598
  if (!equal(currentRec.data, updatedRec.data)) {
580
- updateRecs.set(id, updatedRec);
599
+ updateMap.set(id, updatedRec);
581
600
  }
582
601
  });
583
602
 
584
- if (isEmpty(updateRecs)) return;
603
+ if (isEmpty(updateMap)) return null;
585
604
 
586
605
  warnIf(
587
606
  hadDupes,
588
607
  'Store.modifyRecords() called with multiple updates for the same Records. Only the first modification for each StoreRecord was processed.'
589
608
  );
590
609
 
591
- this._current = this._current.withTransaction({update: Array.from(updateRecs.values())});
610
+ const updateRecs = Array.from(updateMap.values()),
611
+ changeLog: StoreChangeLog = {};
592
612
 
593
- this.rebuildFiltered();
613
+ // 2) Pre-process summary records, peeling them out of updates if needed
614
+ const {summaryRecords} = this;
615
+ let summaryUpdateRecs: StoreRecord[];
616
+ if (!isEmpty(summaryRecords)) {
617
+ summaryUpdateRecs = lodashRemove(updateRecs, ({id}) => some(summaryRecords, {id}));
618
+ }
619
+
620
+ if (!isEmpty(summaryUpdateRecs)) {
621
+ this.summaryRecords = summaryUpdateRecs;
622
+ changeLog.summaryRecords = this.summaryRecords;
623
+ }
624
+
625
+ // 3) Apply changes
626
+ if (!isEmpty(updateRecs)) {
627
+ this._current = this._current.withTransaction({update: updateRecs});
628
+ changeLog.update = updateRecs;
629
+ this.rebuildFiltered();
630
+ }
631
+
632
+ return changeLog;
594
633
  }
595
634
 
596
635
  /**
@@ -606,13 +645,20 @@ export class Store extends HoistBase {
606
645
  records = castArray(records);
607
646
  if (isEmpty(records)) return;
608
647
 
609
- const recs = records.map(it => (it instanceof StoreRecord ? it : this.getOrThrow(it)));
648
+ const recs = records.map(it => (it instanceof StoreRecord ? it : this.getOrThrow(it))),
649
+ [summaryRecsToRevert, recsToRevert] = partition(recs, 'isSummary');
610
650
 
611
- this._current = this._current
612
- .withTransaction({update: recs.map(r => this.getCommittedOrThrow(r.id))})
613
- .normalize(this._committed);
651
+ if (!isEmpty(summaryRecsToRevert)) {
652
+ this.revertSummaryRecords(summaryRecsToRevert);
653
+ }
614
654
 
615
- this.rebuildFiltered();
655
+ if (!isEmpty(recsToRevert)) {
656
+ this._current = this._current
657
+ .withTransaction({update: recsToRevert.map(r => this.getCommittedOrThrow(r.id))})
658
+ .normalize(this._committed);
659
+
660
+ this.rebuildFiltered();
661
+ }
616
662
  }
617
663
 
618
664
  /**
@@ -625,6 +671,7 @@ export class Store extends HoistBase {
625
671
  @action
626
672
  revert() {
627
673
  this._current = this._committed;
674
+ if (this.summaryRecords) this.revertSummaryRecords(this.summaryRecords);
628
675
  this.rebuildFiltered();
629
676
  }
630
677
 
@@ -705,7 +752,7 @@ export class Store extends HoistBase {
705
752
  /** True if the store has changes which need to be committed. */
706
753
  @computed
707
754
  get isDirty(): boolean {
708
- return this._current !== this._committed;
755
+ return this._current !== this._committed || this.summaryRecords?.some(it => it.isModified);
709
756
  }
710
757
 
711
758
  /** Alias for {@link Store.isDirty} */
@@ -1082,6 +1129,24 @@ export class Store extends HoistBase {
1082
1129
  'idSpec should be either a name of a field, or a function to generate an id.'
1083
1130
  );
1084
1131
  }
1132
+
1133
+ @action
1134
+ private revertSummaryRecords(records: StoreRecord[]) {
1135
+ this.summaryRecords = this.summaryRecords.map(summaryRec => {
1136
+ const recToRevert = records.find(it => it.id === summaryRec.id);
1137
+ if (!recToRevert) return summaryRec;
1138
+
1139
+ const ret = new StoreRecord({
1140
+ id: recToRevert.id,
1141
+ raw: recToRevert.raw,
1142
+ data: {...recToRevert.committedData},
1143
+ store: this,
1144
+ isSummary: true
1145
+ });
1146
+ ret.finalize();
1147
+ return ret;
1148
+ });
1149
+ }
1085
1150
  }
1086
1151
 
1087
1152
  //---------------------------------------------------------------------
package/data/cube/Cube.ts CHANGED
@@ -5,7 +5,7 @@
5
5
  * Copyright © 2025 Extremely Heavy Industries Inc.
6
6
  */
7
7
 
8
- import {HoistBase, managed, PlainObject} from '@xh/hoist/core';
8
+ import {HoistBase, managed, PlainObject, Some} from '@xh/hoist/core';
9
9
  import {action, makeObservable, observable} from '@xh/hoist/mobx';
10
10
  import {forEachAsync} from '@xh/hoist/utils/async';
11
11
  import {CubeField, CubeFieldSpec} from './CubeField';
@@ -244,6 +244,26 @@ export class Cube extends HoistBase {
244
244
  }
245
245
  }
246
246
 
247
+ /**
248
+ * Similar to `updateDataAsync`, but intended for modifying individual field values in a local
249
+ * uncommitted state - i.e. when updating via an inline grid editor or similar control. Like
250
+ * `updateDataAsync`, this method will update its views asynchronously.
251
+ *
252
+ * This method largely delegates to {@link Store.modifyRecords} - see that method for more info.
253
+ *
254
+ * @param modifications - field-level modifications to apply to existing
255
+ * Records in this Cube. Each object in the list must have an `id` property identifying
256
+ * the StoreRecord to modify, plus any other properties with updated field values to apply,
257
+ * e.g. `{id: 4, quantity: 100}, {id: 5, quantity: 99, customer: 'bob'}`.
258
+ */
259
+ async modifyRecordsAsync(modifications: Some<PlainObject>): Promise<void> {
260
+ const changeLog = this.store.modifyRecords(modifications);
261
+
262
+ if (changeLog) {
263
+ await forEachAsync(this._connectedViews, v => v.noteCubeUpdated(changeLog));
264
+ }
265
+ }
266
+
247
267
  /** Clear any/all data and info from this Cube. */
248
268
  async clearAsync() {
249
269
  await this.loadDataAsync([]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xh/hoist",
3
- "version": "75.0.0-SNAPSHOT.1753731626581",
3
+ "version": "75.0.0-SNAPSHOT.1754335298677",
4
4
  "description": "Hoist add-on for building and deploying React Applications.",
5
5
  "repository": "github:xh/hoist-react",
6
6
  "homepage": "https://xh.io",