mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
(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
This commit is contained in:
parent
7f8f3dc0be
commit
0d460ac2d4
@ -242,6 +242,8 @@ export class ChoiceListEntry extends Disposable {
|
|||||||
const rename = async (to: string) => {
|
const rename = async (to: string) => {
|
||||||
const tokenField = this._tokenFieldHolder.get();
|
const tokenField = this._tokenFieldHolder.get();
|
||||||
if (!tokenField) { return; }
|
if (!tokenField) { return; }
|
||||||
|
|
||||||
|
to = to.trim();
|
||||||
// If user removed the label, revert back to original one.
|
// If user removed the label, revert back to original one.
|
||||||
if (!to) {
|
if (!to) {
|
||||||
choiceText.set(token.label);
|
choiceText.set(token.label);
|
||||||
|
@ -19,7 +19,7 @@ export interface FormatOptions {
|
|||||||
* Formats a value of any type generically (with no type-specific options).
|
* Formats a value of any type generically (with no type-specific options).
|
||||||
*/
|
*/
|
||||||
export function formatUnknown(value: CellValue): string {
|
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
|
* 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.
|
* 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 (typeof value === 'object' && value) {
|
||||||
if (Array.isArray(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)) {
|
} else if (isPlainObject(value)) {
|
||||||
const obj: any = 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(', ') + '}';
|
return '{' + items.join(', ') + '}';
|
||||||
} else if (isTopLevel && value instanceof GristDateTime) {
|
} else if (isTopLevel && value instanceof GristDateTime) {
|
||||||
return moment(value).tz(value.timezone).format("YYYY-MM-DD HH:mm:ssZ");
|
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 gristTypes from 'app/common/gristTypes';
|
||||||
import * as gutil from 'app/common/gutil';
|
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 NumberParse from 'app/common/NumberParse';
|
||||||
import { parseDateStrict } from 'app/common/parseDate';
|
import {parseDateStrict} from 'app/common/parseDate';
|
||||||
import { DateFormatOptions, DateTimeFormatOptions, FormatOptions } from 'app/common/ValueFormatter';
|
import {DateFormatOptions, DateTimeFormatOptions, formatDecoded, FormatOptions} from 'app/common/ValueFormatter';
|
||||||
|
import flatMap = require('lodash/flatMap');
|
||||||
|
|
||||||
|
|
||||||
export class ValueParser {
|
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 } = {
|
const parsers: { [type: string]: typeof ValueParser } = {
|
||||||
Numeric: NumericParser,
|
Numeric: NumericParser,
|
||||||
Int: NumericParser,
|
Int: NumericParser,
|
||||||
Date: DateParser,
|
Date: DateParser,
|
||||||
DateTime: DateTimeParser,
|
DateTime: DateTimeParser,
|
||||||
|
ChoiceList: ChoiceListParser,
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO these are not ready yet
|
// TODO these are not ready yet
|
||||||
|
Loading…
Reference in New Issue
Block a user