mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
(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:
parent
5b16dd2e86
commit
99878c08ed
@ -356,29 +356,40 @@ BaseView.prototype.insertRow = function(index) {
|
|||||||
* @returns {Object} - Object mapping colId to array of column values, suitable for use in Bulk
|
* @returns {Object} - Object mapping colId to array of column values, suitable for use in Bulk
|
||||||
* actions.
|
* actions.
|
||||||
*/
|
*/
|
||||||
BaseView.prototype._parsePasteForView = function(data, cols) {
|
BaseView.prototype._parsePasteForView = function(data, fields) {
|
||||||
let updateCols = cols.map(col => {
|
const updateCols = fields.map(field => {
|
||||||
|
const col = field && field.column();
|
||||||
if (col && !col.isRealFormula() && !col.disableEditData()) {
|
if (col && !col.isRealFormula() && !col.disableEditData()) {
|
||||||
return col;
|
return col;
|
||||||
} else {
|
} else {
|
||||||
return null; // Don't include formulas and missing columns
|
return null; // Don't include formulas and missing columns
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
let updateColIds = updateCols.map(c => c && c.colId());
|
const updateColIds = updateCols.map(c => c && c.colId());
|
||||||
let updateColTypes = updateCols.map(c => c && c.type());
|
const updateColTypes = updateCols.map(c => c && c.type());
|
||||||
|
const parsers = fields.map(field => field && field.valueParser() || (x => x));
|
||||||
|
|
||||||
let richData = data;
|
const richData = data.map((col, idx) => {
|
||||||
|
if (!col.length) {
|
||||||
if (data.length > 0 && data[0].length > 0 &&
|
return col;
|
||||||
_.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 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);
|
return _.omit(_.object(updateColIds, richData), null);
|
||||||
};
|
};
|
||||||
|
@ -167,10 +167,10 @@ DetailView.prototype.deleteRow = function(index) {
|
|||||||
*/
|
*/
|
||||||
DetailView.prototype.paste = function(data, cutCallback) {
|
DetailView.prototype.paste = function(data, cutCallback) {
|
||||||
let pasteData = data[0][0];
|
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 isCompletePaste = (data.length === 1 && data[0].length === 1);
|
||||||
|
|
||||||
let richData = this._parsePasteForView([[pasteData]], [col]);
|
let richData = this._parsePasteForView([[pasteData]], [field]);
|
||||||
if (_.isEmpty(richData)) {
|
if (_.isEmpty(richData)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -355,9 +355,9 @@ GridView.prototype.paste = function(data, cutCallback) {
|
|||||||
pasteData = gutil.growMatrix(pasteData, updateColIndices.length, updateRowIds.length);
|
pasteData = gutil.growMatrix(pasteData, updateColIndices.length, updateRowIds.length);
|
||||||
|
|
||||||
let fields = this.viewSection.viewFields().peek();
|
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);
|
let actions = this._createBulkActionsFromPaste(updateRowIds, richData);
|
||||||
|
|
||||||
if (actions.length > 0) {
|
if (actions.length > 0) {
|
||||||
|
@ -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 modelUtil from 'app/client/models/modelUtil';
|
||||||
import * as UserType from 'app/client/widgets/UserType';
|
import * as UserType from 'app/client/widgets/UserType';
|
||||||
import {DocumentSettings} from 'app/common/DocumentSettings';
|
import { DocumentSettings } from 'app/common/DocumentSettings';
|
||||||
import {BaseFormatter, createFormatter} from 'app/common/ValueFormatter';
|
import { BaseFormatter, createFormatter } from 'app/common/ValueFormatter';
|
||||||
import {Computed, fromKo} from 'grainjs';
|
import { createParser } from 'app/common/ValueParser';
|
||||||
|
import { Computed, fromKo } from 'grainjs';
|
||||||
import * as ko from 'knockout';
|
import * as ko from 'knockout';
|
||||||
|
|
||||||
// Represents a page entry in the tree of pages.
|
// 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>;
|
documentSettings: ko.PureComputed<DocumentSettings>;
|
||||||
|
|
||||||
|
valueParser: ko.Computed<((value: string) => any) | undefined>;
|
||||||
|
|
||||||
// Helper which adds/removes/updates field's displayCol to match the formula.
|
// Helper which adds/removes/updates field's displayCol to match the formula.
|
||||||
saveDisplayFormula(formula: string): Promise<void>|undefined;
|
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());
|
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.
|
// The widgetOptions to read and write: either the column's or the field's own.
|
||||||
this._widgetOptionsStr = modelUtil.savingComputed({
|
this._widgetOptionsStr = modelUtil.savingComputed({
|
||||||
read: () => this._fieldOrColumn().widgetOptions(),
|
read: () => this._fieldOrColumn().widgetOptions(),
|
||||||
|
@ -111,7 +111,7 @@ class IntFormatter extends NumericFormatter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DateFormatOptions {
|
export interface DateFormatOptions {
|
||||||
dateFormat?: string;
|
dateFormat?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -132,7 +132,7 @@ class DateFormatter extends BaseFormatter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DateTimeFormatOptions extends DateFormatOptions {
|
export interface DateTimeFormatOptions extends DateFormatOptions {
|
||||||
timeFormat?: string;
|
timeFormat?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
74
app/common/ValueParser.ts
Normal file
74
app/common/ValueParser.ts
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
import { DocumentSettings } from 'app/common/DocumentSettings';
|
||||||
|
import * as gristTypes from 'app/common/gristTypes';
|
||||||
|
import * as gutil from 'app/common/gutil';
|
||||||
|
import { getCurrency, NumberFormatOptions } from 'app/common/NumberFormat';
|
||||||
|
import NumberParse from 'app/common/NumberParse';
|
||||||
|
import { parseDate } from 'app/common/parseDate';
|
||||||
|
import { DateTimeFormatOptions, FormatOptions } from 'app/common/ValueFormatter';
|
||||||
|
|
||||||
|
|
||||||
|
export class ValueParser {
|
||||||
|
constructor(public type: string, public widgetOpts: object, public docSettings: DocumentSettings) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public cleanParse(value: string): any {
|
||||||
|
if (!value) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
return this.parse(value) ?? value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public parse(value: string): any {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export class NumericParser extends ValueParser {
|
||||||
|
private _parse: NumberParse;
|
||||||
|
|
||||||
|
constructor(type: string, options: NumberFormatOptions, docSettings: DocumentSettings) {
|
||||||
|
super(type, options, docSettings);
|
||||||
|
this._parse = new NumberParse(docSettings.locale, getCurrency(options, docSettings));
|
||||||
|
}
|
||||||
|
|
||||||
|
public parse(value: string): number | null {
|
||||||
|
return this._parse.parse(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DateParser extends ValueParser {
|
||||||
|
public parse(value: string): any {
|
||||||
|
return parseDate(value, this.widgetOpts);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DateTimeParser extends DateParser {
|
||||||
|
constructor(type: string, widgetOpts: DateTimeFormatOptions, docSettings: DocumentSettings) {
|
||||||
|
super(type, widgetOpts, docSettings);
|
||||||
|
const timezone = gutil.removePrefix(type, "DateTime:") || '';
|
||||||
|
this.widgetOpts = {...widgetOpts, timezone};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsers: { [type: string]: typeof ValueParser } = {
|
||||||
|
Numeric: NumericParser,
|
||||||
|
Int: NumericParser,
|
||||||
|
Date: DateParser,
|
||||||
|
DateTime: DateTimeParser,
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO these are not ready yet
|
||||||
|
delete parsers.Date;
|
||||||
|
delete parsers.DateTime;
|
||||||
|
|
||||||
|
export function createParser(
|
||||||
|
type: string, widgetOpts: FormatOptions, docSettings: DocumentSettings
|
||||||
|
): ((value: string) => any) | undefined {
|
||||||
|
const cls = parsers[gristTypes.extractTypeFromColType(type)];
|
||||||
|
if (cls) {
|
||||||
|
const parser = new cls(type, widgetOpts, docSettings);
|
||||||
|
return parser.cleanParse.bind(parser);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user