Skip to content

Commit bd91a25

Browse files
authored
Add new Cube.modifyRecordsAsync (#4058)
* Add new `Cube.modifyRecordsAsync` * Rename interfaces + fix bug where `Store.modifyRecords` was not properly handling changes to `SummaryRecords`. * Limit use of rebuildFiltered in Store.modifyRecords
1 parent 86f27b9 commit bd91a25

File tree

3 files changed

+76
-14
lines changed

3 files changed

+76
-14
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@
2323
which `dimensions` are provided to the model.
2424
* Added new `ClipboardButton.errorMessage` prop to customize or suppress a toast alert if the copy
2525
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 bug where `Store.modifyRecords` was not properly handling changes to `SummaryRecords`.
2631

2732
### 🐞 Bug Fixes
2833

data/Store.ts

Lines changed: 50 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,17 @@ export interface StoreTransaction {
153153
rawSummaryData?: Some<PlainObject>;
154154
}
155155

156+
/**
157+
* Collection of changes made to a Store's RecordSet. Unlike `StoreTransaction` which is used to
158+
* specify changes, this object is used to report the actual changes made in a single transaction.
159+
*/
160+
export interface StoreChangeLog {
161+
update?: StoreRecord[];
162+
add?: StoreRecord[];
163+
remove?: StoreRecordId[];
164+
summaryRecords?: StoreRecord[];
165+
}
166+
156167
export interface ChildRawData {
157168
/** ID of the pre-existing parent record. */
158169
parentId: string;
@@ -348,13 +359,13 @@ export class Store extends HoistBase {
348359
*/
349360
@action
350361
@logWithDebug
351-
updateData(rawData: PlainObject[] | StoreTransaction): PlainObject {
362+
updateData(rawData: PlainObject[] | StoreTransaction): StoreChangeLog {
352363
if (isEmpty(rawData)) return null;
353364

354-
const changeLog: PlainObject = {};
365+
const changeLog: StoreChangeLog = {};
355366

356367
// Build a transaction object out of a flat list of adds and updates
357-
let rawTransaction;
368+
let rawTransaction: StoreTransaction;
358369
if (isArray(rawData)) {
359370
const update = [],
360371
add = [];
@@ -381,7 +392,7 @@ export class Store extends HoistBase {
381392
throwIf(!isEmpty(other), 'Unknown argument(s) passed to updateData().');
382393

383394
// 1) Pre-process updates and adds into Records
384-
let updateRecs, addRecs;
395+
let updateRecs: StoreRecord[], addRecs: Map<StoreRecordId, StoreRecord>;
385396
if (update) {
386397
updateRecs = update.map(it => {
387398
const recId = this.idSpec(it),
@@ -426,7 +437,11 @@ export class Store extends HoistBase {
426437
}
427438

428439
// 3) Apply changes
429-
let rsTransaction: any = {};
440+
let rsTransaction: {
441+
update?: StoreRecord[];
442+
add?: StoreRecord[];
443+
remove?: StoreRecordId[];
444+
} = {};
430445
if (!isEmpty(updateRecs)) rsTransaction.update = updateRecs;
431446
if (!isEmpty(addRecs)) rsTransaction.add = Array.from(addRecs.values());
432447
if (!isEmpty(remove)) rsTransaction.remove = remove;
@@ -545,21 +560,23 @@ export class Store extends HoistBase {
545560
* Records in this Store. Each object in the list must have an `id` property identifying
546561
* the StoreRecord to modify, plus any other properties with updated field values to apply,
547562
* e.g. `{id: 4, quantity: 100}, {id: 5, quantity: 99, customer: 'bob'}`.
563+
* @returns changes applied, or null if no record changes were made.
548564
*/
549565
@action
550-
modifyRecords(modifications: Some<PlainObject>) {
566+
modifyRecords(modifications: Some<PlainObject>): StoreChangeLog {
551567
modifications = castArray(modifications);
552568
if (isEmpty(modifications)) return;
553569

554-
const updateRecs = new Map();
570+
// 1) Pre-process modifications into Records
571+
const updateMap = new Map<StoreRecordId, StoreRecord>();
555572
let hadDupes = false;
556573
modifications.forEach(mod => {
557574
let {id} = mod;
558575

559576
// Ignore multiple updates for the same record - we are updating this Store in a
560577
// transaction after processing all modifications, so this method is not currently setup
561578
// to process more than one update for a given rec at a time.
562-
if (updateRecs.has(id)) {
579+
if (updateMap.has(id)) {
563580
hadDupes = true;
564581
return;
565582
}
@@ -577,20 +594,40 @@ export class Store extends HoistBase {
577594
});
578595

579596
if (!equal(currentRec.data, updatedRec.data)) {
580-
updateRecs.set(id, updatedRec);
597+
updateMap.set(id, updatedRec);
581598
}
582599
});
583600

584-
if (isEmpty(updateRecs)) return;
601+
if (isEmpty(updateMap)) return null;
585602

586603
warnIf(
587604
hadDupes,
588605
'Store.modifyRecords() called with multiple updates for the same Records. Only the first modification for each StoreRecord was processed.'
589606
);
590607

591-
this._current = this._current.withTransaction({update: Array.from(updateRecs.values())});
608+
const updateRecs = Array.from(updateMap.values()),
609+
changeLog: StoreChangeLog = {};
592610

593-
this.rebuildFiltered();
611+
// 2) Pre-process summary records, peeling them out of updates if needed
612+
const {summaryRecords} = this;
613+
let summaryUpdateRecs: StoreRecord[];
614+
if (!isEmpty(summaryRecords)) {
615+
summaryUpdateRecs = lodashRemove(updateRecs, ({id}) => some(summaryRecords, {id}));
616+
}
617+
618+
if (!isEmpty(summaryUpdateRecs)) {
619+
this.summaryRecords = summaryUpdateRecs;
620+
changeLog.summaryRecords = this.summaryRecords;
621+
}
622+
623+
// 3) Apply changes
624+
if (!isEmpty(updateRecs)) {
625+
this._current = this._current.withTransaction({update: updateRecs});
626+
changeLog.update = updateRecs;
627+
this.rebuildFiltered();
628+
}
629+
630+
return changeLog;
594631
}
595632

596633
/**
@@ -705,7 +742,7 @@ export class Store extends HoistBase {
705742
/** True if the store has changes which need to be committed. */
706743
@computed
707744
get isDirty(): boolean {
708-
return this._current !== this._committed;
745+
return this._current !== this._committed || this.summaryRecords?.some(it => it.isModified);
709746
}
710747

711748
/** Alias for {@link Store.isDirty} */

data/cube/Cube.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* Copyright © 2025 Extremely Heavy Industries Inc.
66
*/
77

8-
import {HoistBase, managed, PlainObject} from '@xh/hoist/core';
8+
import {HoistBase, managed, PlainObject, Some} from '@xh/hoist/core';
99
import {action, makeObservable, observable} from '@xh/hoist/mobx';
1010
import {forEachAsync} from '@xh/hoist/utils/async';
1111
import {CubeField, CubeFieldSpec} from './CubeField';
@@ -244,6 +244,26 @@ export class Cube extends HoistBase {
244244
}
245245
}
246246

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+
247267
/** Clear any/all data and info from this Cube. */
248268
async clearAsync() {
249269
await this.loadDataAsync([]);

0 commit comments

Comments
 (0)