diff --git a/app/client/widgets/ChoiceListEntry.ts b/app/client/widgets/ChoiceListEntry.ts index c3a1a248..1ec74195 100644 --- a/app/client/widgets/ChoiceListEntry.ts +++ b/app/client/widgets/ChoiceListEntry.ts @@ -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); diff --git a/app/common/ValueFormatter.ts b/app/common/ValueFormatter.ts index 3a42c9fe..16338861 100644 --- a/app/common/ValueFormatter.ts +++ b/app/common/ValueFormatter.ts @@ -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"); diff --git a/app/common/ValueParser.ts b/app/common/ValueParser.ts index 6ce641c7..fa609279 100644 --- a/app/common/ValueParser.ts +++ b/app/common/ValueParser.ts @@ -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