import { KoArray } from 'app/client/lib/koArray'; import * as koUtil from 'app/client/lib/koUtil'; import * as BaseRowModel from 'app/client/models/BaseRowModel'; import * as DataTableModel from 'app/client/models/DataTableModel'; import { IRowModel } from 'app/client/models/DocModel'; import { ValidationRec } from 'app/client/models/entities/ValidationRec'; import * as modelUtil from 'app/client/models/modelUtil'; import { CellValue, ColValues } from 'app/common/DocActions'; import * as ko from 'knockout'; /** * DataRowModel is a RowModel for a Data Table. It creates observables for each field in colNames. * A DataRowModel is initialized "unassigned", and can be assigned to any rowId using `.assign()`. */ export class DataRowModel extends BaseRowModel { // Instances of this class are indexable, but that is a little awkward to type. // The cells field gives typed access to that aspect of the instance. This is a // bit hacky, and should be cleaned up when BaseRowModel is ported to typescript. public readonly cells: {[key: string]: modelUtil.KoSaveableObservable} = this as any; public _validationFailures: ko.PureComputed>>; public _isAddRow: ko.Observable; private _allValidationsList: ko.Computed>; private _isRealChange: ko.Observable; public constructor(dataTableModel: DataTableModel, colNames: string[]) { super(dataTableModel, colNames); this._allValidationsList = dataTableModel.tableMetaRow.validations; this._isAddRow = ko.observable(false); // Observable that's set whenever a change to a row model is likely to be real, and unset when a // row model is being reassigned to a different row. If a widget uses CSS transitions for // changes, those should only be enabled when _isRealChange is true. this._isRealChange = ko.observable(true); this._validationFailures = this.autoDispose(ko.pureComputed(function() { return this._allValidationsList().all().filter( validation => !this.cells[this.getValidationNameFromId(validation.id())]()); }, this)); } /** * Helper method to get the column id of a validation associated with a given id * No code other than this should need to know what * naming scheme is used */ public getValidationNameFromId(id: number) { return "validation___" + id; } /** * Overrides BaseRowModel.updateColValues(), which is used to save fields, to support the special * "add-row" records, and to ensure values are up-to-date when the action completes. */ public async updateColValues(colValues: ColValues) { const action = this._isAddRow.peek() ? ["AddRecord", null, colValues] : ["UpdateRecord", this._rowId, colValues]; try { return await this._table.sendTableAction(action); } finally { // If the action doesn't actually result in an update to a row, it's important to reset the // observable to the data (if the data did get updated, this will be a no-op). This is also // important for AddRecord: if after the update, this row is again the 'new' row, it needs to // be cleared out. // TODO: in the case when data reverts because an update didn't happen (e.g. typing in // "12.000" into a numeric column that has "12" in it), there should be a visual indication. Object.keys(colValues).forEach(colId => this._assignColumn(colId)); } } /** * Assign the DataRowModel to a different row of the table. This is primarily used with koDomScrolly, * when scrolling is accomplished by reusing a few rows of DOM and their underying RowModels. */ public assign(rowId: number|'new'|null) { this._rowId = rowId; this._isAddRow(rowId === 'new'); // When we reassign a row, unset _isRealChange momentarily (to disable CSS transitions). // NOTE: it would be better to only set this flag when there is a data change (rather than unset // it whenever we scroll), but Chrome will only run a transition if it's enabled before the // actual DOM change, so setting this flag in the same tick as a change is not sufficient. this._isRealChange(false); // Include a check to avoid using the observable after the row model has been disposed. setTimeout(() => this.isDisposed() || this._isRealChange(true), 0); if (this._rowId !== null) { this._fields.forEach(colName => this._assignColumn(colName)); } } /** * Helper method to assign a particular column of this row to the associated tabledata. */ private _assignColumn(colName: string) { if (!this.isDisposed() && this.hasOwnProperty(colName)) { const value = (this._rowId === 'new' || !this._rowId) ? '' : this._table.tableData.getValue(this._rowId, colName); koUtil.withKoUtils(this.cells[colName]).assign(value); } } }