(core) Parsing pasted ChoiceLists

Summary:
Added ChoiceListParser capable of parsing JSON, CSVs, and other strings containing user-configured choices (e.g. separated by spaces)

I got a little carried away here. It works, and I can't think of any bugs, but it's complicated enough that there could be hidden edge cases or difficulties maintaining it in the future. The advantage of the current method is that it should work well for ambiguous or poorly formatted inputs, e.g. choices separated only by spaces or choices containing commas which are not escaped/quoted properly. The code can be vastly simplified if we don't try to support that and require that users paste proper JSON or CSVs.

Test Plan: Added a new file test/common/ChoiceListParser.ts with pure unit tests. Waiting for approval of the overall approach before adding to the nbrowser CopyPaste test.

Reviewers: dsagal

Reviewed By: dsagal

Differential Revision: https://phab.getgrist.com/D3141
pull/115/head
Alex Hall 3 years ago
parent 7f8f3dc0be
commit 0d460ac2d4

@ -242,6 +242,8 @@ export class ChoiceListEntry extends Disposable {
const rename = async (to: string) => {
const tokenField = this._tokenFieldHolder.get();
if (!tokenField) { return; }
to = to.trim();
// If user removed the label, revert back to original one.
if (!to) {
choiceText.set(token.label);

@ -19,7 +19,7 @@ export interface FormatOptions {
* Formats a value of any type generically (with no type-specific options).
*/
export function formatUnknown(value: CellValue): string {
return formatHelper(decodeObject(value));
return formatDecoded(decodeObject(value));
}
/**
@ -27,13 +27,13 @@ export function formatUnknown(value: CellValue): string {
* like to see them in a cell or in, say, CSV export. For lists and objects, nested values are
* formatted slighly differently, with quoted strings and ISO format for dates.
*/
function formatHelper(value: unknown, isTopLevel: boolean = true): string {
export function formatDecoded(value: unknown, isTopLevel: boolean = true): string {
if (typeof value === 'object' && value) {
if (Array.isArray(value)) {
return '[' + value.map(v => formatHelper(v, false)).join(', ') + ']';
return '[' + value.map(v => formatDecoded(v, false)).join(', ') + ']';
} else if (isPlainObject(value)) {
const obj: any = value;
const items = Object.keys(obj).map(k => `${JSON.stringify(k)}: ${formatHelper(obj[k], false)}`);
const items = Object.keys(obj).map(k => `${JSON.stringify(k)}: ${formatDecoded(obj[k], false)}`);
return '{' + items.join(', ') + '}';
} else if (isTopLevel && value instanceof GristDateTime) {
return moment(value).tz(value.timezone).format("YYYY-MM-DD HH:mm:ssZ");

@ -1,10 +1,13 @@
import { DocumentSettings } from 'app/common/DocumentSettings';
import {csvDecodeRow} from 'app/common/csvFormat';
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 {safeJsonParse} from 'app/common/gutil';
import {getCurrency, NumberFormatOptions} from 'app/common/NumberFormat';
import NumberParse from 'app/common/NumberParse';
import { parseDateStrict } from 'app/common/parseDate';
import { DateFormatOptions, DateTimeFormatOptions, FormatOptions } from 'app/common/ValueFormatter';
import {parseDateStrict} from 'app/common/parseDate';
import {DateFormatOptions, DateTimeFormatOptions, formatDecoded, FormatOptions} from 'app/common/ValueFormatter';
import flatMap = require('lodash/flatMap');
export class ValueParser {
@ -52,11 +55,48 @@ class DateTimeParser extends DateParser {
}
}
class ChoiceListParser extends ValueParser {
public cleanParse(value: string): string[] | null {
value = value.trim();
const result = (
this._parseJson(value) ||
this._parseCsv(value)
).map(v => v.trim())
.filter(v => v);
if (!result.length) {
return null;
}
return ["L", ...result];
}
private _parseJson(value: string): string[] | undefined {
// Don't parse JSON non-arrays
if (value[0] === "[") {
const arr: unknown[] | null = safeJsonParse(value, null);
return arr
// Remove nulls and empty strings
?.filter(v => v || v === 0)
// Convert values to strings, formatting nested JSON objects/arrays as JSON
.map(v => formatDecoded(v));
}
}
private _parseCsv(value: string): string[] {
// Split everything on newlines which are not allowed by the choice editor.
return flatMap(value.split(/[\n\r]+/), row => {
return csvDecodeRow(row)
.map(v => v.trim());
});
}
}
const parsers: { [type: string]: typeof ValueParser } = {
Numeric: NumericParser,
Int: NumericParser,
Date: DateParser,
DateTime: DateTimeParser,
ChoiceList: ChoiceListParser,
};
// TODO these are not ready yet

Loading…
Cancel
Save