diff --git a/app/client/components/BaseView.js b/app/client/components/BaseView.js index 5c8f6a37..2874540c 100644 --- a/app/client/components/BaseView.js +++ b/app/client/components/BaseView.js @@ -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); }; diff --git a/app/client/components/DetailView.js b/app/client/components/DetailView.js index 402b65ad..bf51d141 100644 --- a/app/client/components/DetailView.js +++ b/app/client/components/DetailView.js @@ -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; } diff --git a/app/client/components/GridView.js b/app/client/components/GridView.js index d7cdca3e..7444bfc2 100644 --- a/app/client/components/GridView.js +++ b/app/client/components/GridView.js @@ -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) { diff --git a/app/client/models/entities/ViewFieldRec.ts b/app/client/models/entities/ViewFieldRec.ts index 7ecf7593..6f177fc3 100644 --- a/app/client/models/entities/ViewFieldRec.ts +++ b/app/client/models/entities/ViewFieldRec.ts @@ -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; + valueParser: ko.Computed<((value: string) => any) | undefined>; + // Helper which adds/removes/updates field's displayCol to match the formula. saveDisplayFormula(formula: string): Promise|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(), diff --git a/app/common/ValueFormatter.ts b/app/common/ValueFormatter.ts index 3fd51f90..3a42c9fe 100644 --- a/app/common/ValueFormatter.ts +++ b/app/common/ValueFormatter.ts @@ -111,7 +111,7 @@ class IntFormatter extends NumericFormatter { } } -interface DateFormatOptions { +export interface DateFormatOptions { dateFormat?: string; } @@ -132,7 +132,7 @@ class DateFormatter extends BaseFormatter { } } -interface DateTimeFormatOptions extends DateFormatOptions { +export interface DateTimeFormatOptions extends DateFormatOptions { timeFormat?: string; } diff --git a/app/common/ValueParser.ts b/app/common/ValueParser.ts new file mode 100644 index 00000000..2b2eeb3a --- /dev/null +++ b/app/common/ValueParser.ts @@ -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); + } +}