mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
242 lines
9.2 KiB
JavaScript
242 lines
9.2 KiB
JavaScript
|
/**
|
||
|
* MetaTableModel maintains the model for a built-in table, with MetaRowModels. It provides
|
||
|
* access to individual row models, as well as to collections of rows in that table.
|
||
|
*/
|
||
|
|
||
|
|
||
|
var _ = require('underscore');
|
||
|
var ko = require('knockout');
|
||
|
var dispose = require('../lib/dispose');
|
||
|
var MetaRowModel = require('./MetaRowModel');
|
||
|
var TableModel = require('./TableModel');
|
||
|
var rowset = require('./rowset');
|
||
|
var assert = require('assert');
|
||
|
var gutil = require('app/common/gutil');
|
||
|
|
||
|
/**
|
||
|
* MetaTableModel maintains observables for one table's rows. It accepts a list of fields to
|
||
|
* include into each RowModel, and an additional constructor to call when constructing RowModels.
|
||
|
* It exposes all rows, as well as groups of rows, as observable collections.
|
||
|
*/
|
||
|
function MetaTableModel(docModel, tableData, fields, rowConstructor) {
|
||
|
TableModel.call(this, docModel, tableData);
|
||
|
|
||
|
this._fields = fields;
|
||
|
this._rowConstructor = rowConstructor;
|
||
|
|
||
|
// Start out with empty list of row models. It's populated in loadData().
|
||
|
this.rowModels = [];
|
||
|
|
||
|
// It is possible for a new rowModel to be deleted and replaced with a new one for the same
|
||
|
// rowId. To allow a computed() to depend on the row version, we keep a permanent observable
|
||
|
// "version" associated with each rowId, which is incremented any time a rowId is replaced.
|
||
|
this._rowModelVersions = [];
|
||
|
|
||
|
// Whenever rowNotify is triggered, also send the action to all row RowModels that we maintain.
|
||
|
this.listenTo(this, 'rowNotify', function(rows, action) {
|
||
|
assert(rows !== rowset.ALL, "Unexpected schema action on a metadata table");
|
||
|
for (let r of rows) {
|
||
|
if (this.rowModels[r]) {
|
||
|
this.rowModels[r].dispatchAction(action);
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
dispose.makeDisposable(MetaTableModel);
|
||
|
_.extend(MetaTableModel.prototype, TableModel.prototype);
|
||
|
|
||
|
/**
|
||
|
* This is called from DocModel as soon as all the MetaTableModel objects have been created.
|
||
|
*/
|
||
|
MetaTableModel.prototype.loadData = function() {
|
||
|
// Whereas user-defined tables may not be initially loaded, MetaTableModels should only exist
|
||
|
// for built-in tables, which *should* already be loaded (and should never be reloaded).
|
||
|
assert(this.tableData.isLoaded, "MetaTableModel: tableData not yet loaded");
|
||
|
|
||
|
// Create and populate the array mapping rowIds to RowModels.
|
||
|
this.getAllRows().forEach(function(rowId) {
|
||
|
this._createRowModel(rowId);
|
||
|
}, this);
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Returns an existing or a blank row. Used for `recordRef` descriptor in DocModel.
|
||
|
*
|
||
|
* A computed() that uses getRowModel() may not realize if a rowId gets deleted and later re-used
|
||
|
* for another row. If optDependOnVersion is set, then a dependency on the row version gets
|
||
|
* created automatically. It is only relevant when the computed is pure and may not get updated
|
||
|
* when the row is deleted; in that case lacking such dependency may cause subtle rare bugs.
|
||
|
*/
|
||
|
MetaTableModel.prototype.getRowModel = function(rowId, optDependOnVersion) {
|
||
|
let r = this.rowModels[rowId] || this.getEmptyRowModel();
|
||
|
if (optDependOnVersion) {
|
||
|
this._rowModelVersions[rowId]();
|
||
|
}
|
||
|
return r;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Returns the RowModel to use for invalid rows.
|
||
|
*/
|
||
|
MetaTableModel.prototype.getEmptyRowModel = function() {
|
||
|
return this._createRowModel(0);
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Private helper to create a MetaRowModel for the given rowId. For public use, there are
|
||
|
* getRowModel(rowId) and createFloatingRowModel(rowIdObs).
|
||
|
*/
|
||
|
MetaTableModel.prototype._createRowModel = function(rowId) {
|
||
|
if (!this.rowModels[rowId]) {
|
||
|
// When creating a new row, we create new MetaRowModels which use observables. If
|
||
|
// _createRowModel is called from within the evaluation of a computed(), we do NOT want that
|
||
|
// computed to subscribe to observables used by individual MetaRowModels.
|
||
|
ko.ignoreDependencies(() => {
|
||
|
this.rowModels[rowId] = MetaRowModel.create(this, this._fields, this._rowConstructor, rowId);
|
||
|
|
||
|
// Whenever a rowModel is created, increment its version number.
|
||
|
let inc = this._rowModelVersions[rowId] || (this._rowModelVersions[rowId] = ko.observable(0));
|
||
|
inc(inc.peek() + 1);
|
||
|
});
|
||
|
}
|
||
|
return this.rowModels[rowId];
|
||
|
};
|
||
|
|
||
|
|
||
|
/**
|
||
|
* Returns a MetaRowModel-like object tied to an observable rowId. When the observable changes,
|
||
|
* the fields of the returned model start reflecting the values for the new rowId. See also
|
||
|
* MetaRowModel.Floater docs.
|
||
|
*
|
||
|
* There should be very few such floating rows. If you ever want a set, you should be using
|
||
|
* createAllRowsModel() or createRowGroupModel().
|
||
|
*
|
||
|
* @param {ko.observable} rowIdObs: observable that evaluates to a rowId.
|
||
|
*/
|
||
|
MetaTableModel.prototype.createFloatingRowModel = function(rowIdObs) {
|
||
|
return MetaRowModel.Floater.create(this, rowIdObs);
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Override TableModel's _process_RemoveRecord to also remove our reference to this row model.
|
||
|
*/
|
||
|
MetaTableModel.prototype._process_RemoveRecord = function(action, tableId, rowId) {
|
||
|
TableModel.prototype._process_RemoveRecord.apply(this, arguments);
|
||
|
this._deleteRowModel(rowId);
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Clean up the RowModel for a row when it's deleted by an action from the server.
|
||
|
*/
|
||
|
MetaTableModel.prototype._deleteRowModel = function(rowId) {
|
||
|
this.rowModels[rowId]._isDeleted(true);
|
||
|
this.rowModels[rowId].dispose();
|
||
|
delete this.rowModels[rowId];
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* We have to remember to override Bulk versions too.
|
||
|
*/
|
||
|
MetaTableModel.prototype._process_BulkRemoveRecord = function(action, tableId, rowIds) {
|
||
|
TableModel.prototype._process_BulkRemoveRecord.apply(this, arguments);
|
||
|
rowIds.forEach(rowId => this._deleteRowModel(rowId));
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Override TableModel's _process_AddRecord to also add a row model for the given rowId.
|
||
|
*/
|
||
|
MetaTableModel.prototype._process_AddRecord = function(action, tableId, rowId, columnValues) {
|
||
|
this._createRowModel(rowId);
|
||
|
TableModel.prototype._process_AddRecord.apply(this, arguments);
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* We have to remember to override Bulk versions too.
|
||
|
*/
|
||
|
MetaTableModel.prototype._process_BulkAddRecord = function(action, tableId, rowIds, columns) {
|
||
|
rowIds.forEach(rowId => this._createRowModel(rowId));
|
||
|
TableModel.prototype._process_BulkAddRecord.apply(this, arguments);
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Override TableModel's applySchemaAction to assert that there are NO metadata schema changes.
|
||
|
*/
|
||
|
MetaTableModel.prototype.applySchemaAction = function(action) {
|
||
|
throw new Error("No schema actions should apply to metadata");
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Returns a new observable array (koArray) of MetaRowModels for all the rows in this table,
|
||
|
* sorted by the given column. It is the caller's responsibility to dispose this array.
|
||
|
* @param {string} sortColId: Column ID by which to sort.
|
||
|
*/
|
||
|
MetaTableModel.prototype.createAllRowsModel = function(sortColId) {
|
||
|
return this._createRowSetModel(this, sortColId);
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Returns a new observable array (koArray) of MetaRowModels matching the given `groupValue`.
|
||
|
* It is the caller's responsibility to dispose this array.
|
||
|
* @param {String|Number} groupValue - The group value to match.
|
||
|
* @param {String} options.groupBy - RowModel field by which to group.
|
||
|
* @param {String} options.sortBy - RowModel field by which to sort.
|
||
|
*/
|
||
|
MetaTableModel.prototype.createRowGroupModel = function(groupValue, options) {
|
||
|
var grouping = this.getRowGrouping(options.groupBy);
|
||
|
return this._createRowSetModel(grouping.getGroup(groupValue), options.sortBy);
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Helper that returns a new observable koArray of MetaRowModels subscribed to the given
|
||
|
* rowSource, and sorted by the given column. It is the caller's responsibility to dispose it.
|
||
|
*/
|
||
|
MetaTableModel.prototype._createRowSetModel = function(rowSource, sortColId) {
|
||
|
var getter = this.tableData.getRowPropFunc(sortColId);
|
||
|
var sortedRowSet = rowset.SortedRowSet.create(null, function(r1, r2) {
|
||
|
return gutil.nativeCompare(getter(r1), getter(r2));
|
||
|
});
|
||
|
sortedRowSet.subscribeTo(rowSource);
|
||
|
|
||
|
// When the returned value is disposed, dispose the underlying SortedRowSet too.
|
||
|
var ret = this._createRowModelArray(sortedRowSet.getKoArray());
|
||
|
ret.autoDispose(sortedRowSet);
|
||
|
return ret;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Helper which takes an observable array (koArray) of rowIds, and returns a new koArray of
|
||
|
* objects having those RowModels as prototypes, and with an additional `_index` observable to
|
||
|
* contain their index in the array. The index is kept correct as the array changes.
|
||
|
*
|
||
|
* TODO: this needs a unittest.
|
||
|
*/
|
||
|
MetaTableModel.prototype._createRowModelArray = function(rowIdArray) {
|
||
|
var ret = rowIdArray.map(this._createRowModelItem, this);
|
||
|
ret.subscribe(function(splice) {
|
||
|
var arr = splice.array, i;
|
||
|
for (i = 0; i < splice.deleted.length; i++) {
|
||
|
splice.deleted[i]._index(null);
|
||
|
}
|
||
|
var delta = splice.added - splice.deleted.length;
|
||
|
if (delta !== 0) {
|
||
|
for (i = splice.start + splice.added; i < arr.length; i++) {
|
||
|
arr[i]._index(i);
|
||
|
}
|
||
|
}
|
||
|
}, null, 'spliceChange');
|
||
|
return ret;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Creates and returns a RowModel with its own `_index` observable.
|
||
|
*/
|
||
|
MetaTableModel.prototype._createRowModelItem = function(rowId, index) {
|
||
|
var rowModel = this._createRowModel(rowId);
|
||
|
assert.ok(rowModel, "MetaTableModel._createRowModelItem called for invalid rowId " + rowId);
|
||
|
var ret = Object.create(rowModel); // New object, with rowModel as its prototype.
|
||
|
ret._index = ko.observable(index); // New _index observable overrides the existing one.
|
||
|
return ret;
|
||
|
};
|
||
|
|
||
|
module.exports = MetaTableModel;
|