Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Additional improvements to Store and Record API #1082

Merged
merged 14 commits into from
Apr 22, 2019
Merged
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@
`agOptions.defaultGroupSortComparator`).
* The `Column.cellClass` and `Column.headerClass` configs now accept functions to dynamically
generate custom classes based on the Record and/or Column being rendered.

### 💥 Breaking Changes
* The argument `LocalStore.processRawData` now expects a function that *returns* a modified object with the
necessary edits. This allows implementations to safely *clone* the raw data rather than mutating it.

### ⚙️ Technical

Expand Down
8 changes: 0 additions & 8 deletions data/Field.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@
* Copyright © 2019 Extremely Heavy Industries Inc.
*/

import {Record} from '@xh/hoist/data/Record';
import {throwIf} from '@xh/hoist/utils/js';
import {startCase, isEqual as lodashIsEqual} from 'lodash';
import {XH} from '@xh/hoist/core';

Expand Down Expand Up @@ -38,12 +36,6 @@ export class Field {
label = startCase(name),
defaultValue = null
}) {

throwIf(
Record.RESERVED_FIELD_NAMES.includes(name),
`Field name "${name}" cannot be used. It is reserved as a top-level property of the Record class.`
);

this.name = name;
this.type = type;
this.label = label;
Expand Down
3 changes: 2 additions & 1 deletion data/LocalStore.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ export class LocalStore extends BaseStore {
/**
* @param {Object} c - LocalStore configuration.
* @param {function} [c.processRawData] - function to run on each individual data object
* presented to loadData() prior to creating a record from that raw object.
* presented to loadData() prior to creating a record from that object. This function should
* return a data object, taking care to clone the original object if edits are necessary.
* @param {(function|string)} [c.idSpec] - specification for selecting or producing an immutable
* unique id for each record. May be either a property (default is 'id') or a function to
* create an id from a record. If there is no natural id to select/generate, you can use
Expand Down
22 changes: 16 additions & 6 deletions data/Record.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
*
* Copyright © 2019 Extremely Heavy Industries Inc.
*/
import {isEqual} from 'lodash';
import {isEqual, isNil, isString} from 'lodash';
import {throwIf} from '@xh/hoist/utils/js';

/**
* Wrapper object for each data element within a {@see BaseStore}.
Expand All @@ -14,8 +15,6 @@ import {isEqual} from 'lodash';
*/
export class Record {

static RESERVED_FIELD_NAMES = ['parentId', 'store', 'xhTreePath']

/** @member {(string|number)} */
id;
/** @member {BaseStore} */
Expand Down Expand Up @@ -43,20 +42,31 @@ export class Record {
* requiring children to also be recreated.)
*
* @param {Object} c - Record configuration
* @param {Object} c.data - data for constructing the record.
* @param {Object} c.raw - raw data for record.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know that apps aren't expected to construct records, but this is still a bit confusing here. How about:

c.data - data used to construct this record, pre-processed if applicable by store.processRawData().
c.raw - the same data, prior to any store pre-processing.

Would that be incrementally more clear?

* @param {BaseStore} c.store - store containing this record.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Realizing that LocalStore defines idSpec, which means that's what you need to be passing in here. Yet BaseStore also declares interfaces that deal in Records/RecordSets.

We've discussed how we might want to unwind BaseStore, taking LocalStore -> Store and having it be the primary/parent store class. I dunno if we want to deal with that now.

Alternatively we could move idSpec to BaseStore, or adjust the docs here to punt a bit and deal with LocalStore only.

* @param {Record} [c.parent] - parent record, if any.
*/
constructor({raw, store, parent}) {
const id = raw.id;
constructor({data, raw, store, parent}) {
const {idSpec} = store,
id = isString(idSpec) ? data[idSpec] : idSpec(data);

throwIf(isNil(id), "Record has an undefined ID. Use 'LocalStore.idSpec' to resolve a unique ID for each record.");

this.id = id;
this.store = store;
this.raw = raw;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should add an @member declaration

this.parentId = parent ? parent.id : null;
this.xhTreePath = parent ? [...parent.xhTreePath, id] : [id];

store.fields.forEach(f => {
this[f.name] = f.parseVal(raw[f.name]);
const {name} = f;
if (name == 'id') return;
throwIf(
name in this,
`Field name "${name}" cannot be used for data. It is reserved as a top-level property of the Record class.`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great that this check moved into this class.

);
this[name] = f.parseVal(data[name]);
});
}

Expand Down
30 changes: 12 additions & 18 deletions data/impl/RecordSet.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* Copyright © 2019 Extremely Heavy Industries Inc.
*/

import {isString, isNil, partition} from 'lodash';
import {partition} from 'lodash';
import {throwIf} from '@xh/hoist/utils/js/';
import {Record} from '../Record';

Expand Down Expand Up @@ -164,28 +164,22 @@ export class RecordSet {
}

createRecord(raw, records, parent) {
const {store} = this,
{idSpec} = store;
const {store} = this;

if (store.processRawData) store.processRawData(raw);

raw.id = isString(idSpec) ? raw[idSpec] : idSpec(raw);

throwIf(
isNil(raw.id),
"Record has a null/undefined ID. Use the 'LocalStore.idSpec' config to resolve a unique ID for each record."
);
let data = raw;
if (store.processRawData) {
data = store.processRawData(raw);
throwIf(!data, 'processRawData should return an object. If writing/editing, be sure to return a clone!');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we enforce processRawData always returning a new object? Is there a likely/important use case where we have processRawData making decisions to not edit some objects (and it would be terribly inefficient to force it to clone anyway)?

}

const rec = new Record({data, raw, parent, store});
throwIf(
records.has(raw.id),
`ID ${raw.id} is not unique. Use the 'LocalStore.idSpec' config to resolve a unique ID for each record.`
records.has(rec.id),
`ID ${rec.id} is not unique. Use the 'LocalStore.idSpec' config to resolve a unique ID for each record.`
);

const rec = new Record({raw, parent, store});
records.set(rec.id, rec);

if (raw.children) {
raw.children.forEach(rawChild => this.createRecord(rawChild, records, rec));
if (data.children) {
data.children.forEach(rawChild => this.createRecord(rawChild, records, rec));
}
}

Expand Down
19 changes: 9 additions & 10 deletions desktop/cmp/leftrightchooser/LeftRightChooserModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import {HoistModel, XH, managed} from '@xh/hoist/core';
import {GridModel} from '@xh/hoist/cmp/grid';
import {computed} from '@xh/hoist/mobx';
import {convertIconToSvg, Icon} from '@xh/hoist/icon';
import {isNil} from 'lodash';

/**
* A Model for managing the state of a LeftRightChooser.
Expand Down Expand Up @@ -173,21 +172,21 @@ export class LeftRightChooserModel {

preprocessData(data) {
return data
.filter(rec => !rec.exclude)
.map(raw => {
raw.group = raw.group || this._ungroupedName;
raw.side = raw.side || 'left';
raw.id = isNil(raw.id) ? XH.genId() : raw.id;
return raw;
.filter(r => !r.exclude)
.map(r => {
return {
id: XH.genId(),
group: this._ungroupedName,
side: 'left',
...r
};
});
}

moveRows(rows) {
rows.forEach(rec => {
if (rec.locked) return;

const rawRec = this._data.find(raw => raw === rec.raw);
rawRec.side = (rec.side === 'left' ? 'right' : 'left');
rec.raw.side = (rec.side === 'left' ? 'right' : 'left');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll be honest - I am not gonna dive into LRChooser code right now, but this seems very weird to be modifying the raw object.

});

this.refreshStores();
Expand Down