(core) Add ValueParser, use when pasting

Summary:
Add ValueParser file, base class, and subclasses for column types. Only NumericParser is used for now.

Add valueParser field to ViewFieldRec.

Use valueParser when parsing pasted text data in Grid and Detail views.

Test Plan: Add test to nbrowser CopyPaste suite, copying into a numeric column with different currency and locale settings.

Reviewers: dsagal

Reviewed By: dsagal

Differential Revision: https://phab.getgrist.com/D3082
This commit is contained in:
Alex Hall
2021-10-21 20:50:49 +02:00
parent 5b16dd2e86
commit 99878c08ed
6 changed files with 116 additions and 24 deletions

View File

@@ -356,29 +356,40 @@ BaseView.prototype.insertRow = function(index) {
* @returns {Object} - Object mapping colId to array of column values, suitable for use in Bulk
* actions.
*/
BaseView.prototype._parsePasteForView = function(data, cols) {
let updateCols = cols.map(col => {
BaseView.prototype._parsePasteForView = function(data, fields) {
const updateCols = fields.map(field => {
const col = field && field.column();
if (col && !col.isRealFormula() && !col.disableEditData()) {
return col;
} else {
return null; // Don't include formulas and missing columns
}
});
let updateColIds = updateCols.map(c => c && c.colId());
let updateColTypes = updateCols.map(c => c && c.type());
const updateColIds = updateCols.map(c => c && c.colId());
const updateColTypes = updateCols.map(c => c && c.type());
const parsers = fields.map(field => field && field.valueParser() || (x => x));
let richData = data;
if (data.length > 0 && data[0].length > 0 &&
_.isObject(data[0][0]) && data[0][0].hasOwnProperty('displayValue')) {
richData = data.map((col, idx) => {
if (col[0].colType === updateColTypes[idx]) {
return col.map(v => v && v.hasOwnProperty('rawValue') ? v.rawValue : v.displayValue);
} else {
return col.map(v => v && v.displayValue);
const richData = data.map((col, idx) => {
if (!col.length) {
return col;
}
const typeMatches = col[0].colType === updateColTypes[idx];
const parser = parsers[idx];
return col.map(v => {
if (v) {
if (typeMatches && v.hasOwnProperty('rawValue')) {
return v.rawValue;
}
if (v.hasOwnProperty('displayValue')) {
return parser(v.displayValue);
}
if (typeof v === "string") {
return parser(v);
}
}
return v;
});
}
});
return _.omit(_.object(updateColIds, richData), null);
};

View File

@@ -167,10 +167,10 @@ DetailView.prototype.deleteRow = function(index) {
*/
DetailView.prototype.paste = function(data, cutCallback) {
let pasteData = data[0][0];
let col = this.currentColumn();
let field = this.viewSection.viewFields().at(this.cursor.fieldIndex());
let isCompletePaste = (data.length === 1 && data[0].length === 1);
let richData = this._parsePasteForView([[pasteData]], [col]);
let richData = this._parsePasteForView([[pasteData]], [field]);
if (_.isEmpty(richData)) {
return;
}

View File

@@ -355,9 +355,9 @@ GridView.prototype.paste = function(data, cutCallback) {
pasteData = gutil.growMatrix(pasteData, updateColIndices.length, updateRowIds.length);
let fields = this.viewSection.viewFields().peek();
let pasteCols = updateColIndices.map(i => fields[i] && fields[i].column() || null);
let pasteFields = updateColIndices.map(i => fields[i] || null);
let richData = this._parsePasteForView(pasteData, pasteCols);
let richData = this._parsePasteForView(pasteData, pasteFields);
let actions = this._createBulkActionsFromPaste(updateRowIds, richData);
if (actions.length > 0) {

View File

@@ -1,9 +1,10 @@
import {ColumnRec, DocModel, IRowModel, refRecord, ViewSectionRec} from 'app/client/models/DocModel';
import { ColumnRec, DocModel, IRowModel, refRecord, ViewSectionRec } from 'app/client/models/DocModel';
import * as modelUtil from 'app/client/models/modelUtil';
import * as UserType from 'app/client/widgets/UserType';
import {DocumentSettings} from 'app/common/DocumentSettings';
import {BaseFormatter, createFormatter} from 'app/common/ValueFormatter';
import {Computed, fromKo} from 'grainjs';
import { DocumentSettings } from 'app/common/DocumentSettings';
import { BaseFormatter, createFormatter } from 'app/common/ValueFormatter';
import { createParser } from 'app/common/ValueParser';
import { Computed, fromKo } from 'grainjs';
import * as ko from 'knockout';
// Represents a page entry in the tree of pages.
@@ -75,6 +76,8 @@ export interface ViewFieldRec extends IRowModel<"_grist_Views_section_field"> {
documentSettings: ko.PureComputed<DocumentSettings>;
valueParser: ko.Computed<((value: string) => any) | undefined>;
// Helper which adds/removes/updates field's displayCol to match the formula.
saveDisplayFormula(formula: string): Promise<void>|undefined;
@@ -177,6 +180,10 @@ export function createViewFieldRec(this: ViewFieldRec, docModel: DocModel): void
createFormatter(this.column().type(), this.widgetOptionsJson(), this.documentSettings());
};
this.valueParser = ko.pureComputed(() =>
createParser(this.column().type(), this.widgetOptionsJson(), this.documentSettings())
);
// The widgetOptions to read and write: either the column's or the field's own.
this._widgetOptionsStr = modelUtil.savingComputed({
read: () => this._fieldOrColumn().widgetOptions(),