(core) Don't clear widget options when changing column type

Summary: Previously, changing the type of a column would clear its widget options and conditional style rules by default, with a few exceptions to explicitly keep them. This diff reverses that behaviour, keeping the options by default.

Test Plan: Updated several existing tests, plus lots of manual testing.

Reviewers: cyprien

Reviewed By: cyprien

Subscribers: dsagal

Differential Revision: https://phab.getgrist.com/D3491
This commit is contained in:
Alex Hall 2022-07-07 14:58:37 +02:00
parent 8469b7ded0
commit 77775401fc
3 changed files with 51 additions and 36 deletions

View File

@ -11,7 +11,7 @@ import * as gristTypes from 'app/common/gristTypes';
import {isFullReferencingType} from 'app/common/gristTypes'; import {isFullReferencingType} from 'app/common/gristTypes';
import * as gutil from 'app/common/gutil'; import * as gutil from 'app/common/gutil';
import NumberParse from 'app/common/NumberParse'; import NumberParse from 'app/common/NumberParse';
import {dateTimeWidgetOptions, guessDateFormat} from 'app/common/parseDate'; import {dateTimeWidgetOptions, guessDateFormat, timeFormatOptions} from 'app/common/parseDate';
import {TableData} from 'app/common/TableData'; import {TableData} from 'app/common/TableData';
import {decodeObject} from 'app/plugin/objtypes'; import {decodeObject} from 'app/plugin/objtypes';
@ -87,31 +87,60 @@ export async function prepTransformColInfo(docModel: DocModel, origCol: ColumnRe
toTypeMaybeFull: string): Promise<ColInfo> { toTypeMaybeFull: string): Promise<ColInfo> {
const toType = gristTypes.extractTypeFromColType(toTypeMaybeFull); const toType = gristTypes.extractTypeFromColType(toTypeMaybeFull);
const tableData: TableData = docModel.docData.getTable(origCol.table().tableId())!; const tableData: TableData = docModel.docData.getTable(origCol.table().tableId())!;
let widgetOptions: any = null;
let rules: gristTypes.RefListValue = null; const visibleCol = origCol.visibleColModel();
// Column used to derive previous widget options and sample values for guessing
const sourceCol = visibleCol.getRowId() !== 0 ? visibleCol : origCol;
let widgetOptions = {...(sourceCol.widgetOptionsJson() || {})};
if (isReferenceCol(origCol)) {
// While reference columns copy most options from their visible column, conditional style rules are kept
// from the original reference column for a few reasons:
// 1. The rule formula of the visible column is less likely to make sense after conversion,
// especially if the reference points to a different table.
// 2. Overwriting the conditional styles of the original reference column could be annoying, whereas
// most other widget options in reference columns aren't particularly valuable.
// 3. The `rules` column (i.e. a reflist to other formula columns) can't simply be copied because those rule columns
// can't currently be shared by multiple columns.
// So in general we keep `rules: origCol.rules()` (further below) and the corresponding
// `widgetOptions.rulesOptions`.
// A quirk of this is that the default (non-conditional) cell style can still be copied from the visible column,
// so a subset of the overall cell styling can change.
delete widgetOptions.rulesOptions;
const {rulesOptions} = origCol.widgetOptionsJson() || {};
if (rulesOptions) {
widgetOptions.rulesOptions = rulesOptions;
}
}
const colInfo: ColInfo = { const colInfo: ColInfo = {
type: addColTypeSuffix(toTypeMaybeFull, origCol, docModel), type: addColTypeSuffix(toTypeMaybeFull, origCol, docModel),
isFormula: true, isFormula: true,
visibleCol: 0, visibleCol: 0,
formula: "CURRENT_CONVERSION(rec)", formula: "CURRENT_CONVERSION(rec)",
rules, rules: origCol.rules(),
}; };
const visibleCol = origCol.visibleColModel();
// Column used to derive previous widget options and sample values for guessing
const sourceCol = visibleCol.getRowId() !== 0 ? visibleCol : origCol;
const prevOptions = sourceCol.widgetOptionsJson.peek() || {};
const prevRules = sourceCol.rules.peek();
switch (toType) { switch (toType) {
case 'Bool':
// Most types use a TextBox as the default widget.
// We don't want to reuse that for Toggle columns, which should be a CheckBox by default.
delete widgetOptions.widget;
break;
case 'Date': case 'Date':
case 'DateTime': { case 'DateTime': {
let {dateFormat} = prevOptions; let {dateFormat} = widgetOptions;
if (!dateFormat) { if (!dateFormat) {
// Guess date and time format if there aren't any already
const colValues = tableData.getColValues(sourceCol.colId()) || []; const colValues = tableData.getColValues(sourceCol.colId()) || [];
dateFormat = guessDateFormat(colValues.map(String)); dateFormat = guessDateFormat(colValues.map(String));
widgetOptions = {...widgetOptions, ...(dateTimeWidgetOptions(dateFormat, true))};
}
if (toType === 'DateTime' && !widgetOptions.timeFormat) {
// Ensure DateTime columns have a time format. This is needed when converting from a Date column.
widgetOptions.timeFormat = timeFormatOptions[0];
widgetOptions.isCustomTimeFormat = false;
} }
widgetOptions = dateTimeWidgetOptions(dateFormat, true);
break; break;
} }
case 'Numeric': case 'Numeric':
@ -119,34 +148,27 @@ export async function prepTransformColInfo(docModel: DocModel, origCol: ColumnRe
if (!["Numeric", "Int"].includes(sourceCol.type())) { if (!["Numeric", "Int"].includes(sourceCol.type())) {
const numberParse = NumberParse.fromSettings(docModel.docData.docSettings()); const numberParse = NumberParse.fromSettings(docModel.docData.docSettings());
const colValues = tableData.getColValues(sourceCol.colId()) || []; const colValues = tableData.getColValues(sourceCol.colId()) || [];
widgetOptions = numberParse.guessOptions(colValues.filter(isString)); widgetOptions = {...widgetOptions, ...numberParse.guessOptions(colValues.filter(isString))};
} else {
widgetOptions = prevOptions;
rules = prevRules;
} }
break; break;
} }
case 'Choice': { case 'Choice': {
if (Array.isArray(prevOptions.choices)) { // Use previous choices if they are set, e.g. if converting from ChoiceList
// Use previous choices if they are set, e.g. if converting from ChoiceList if (!Array.isArray(widgetOptions.choices)) {
widgetOptions = {choices: prevOptions.choices, choiceOptions: prevOptions.choiceOptions};
} else {
// Set suggested choices. Limit to 100, since too many choices is more likely to cause // Set suggested choices. Limit to 100, since too many choices is more likely to cause
// trouble than desired behavior. For many choices, recommend using a Ref to helper table. // trouble than desired behavior. For many choices, recommend using a Ref to helper table.
const columnData = tableData.getDistinctValues(sourceCol.colId(), 100); const columnData = tableData.getDistinctValues(sourceCol.colId(), 100);
if (columnData) { if (columnData) {
columnData.delete(""); columnData.delete("");
columnData.delete(null); columnData.delete(null);
widgetOptions = {choices: Array.from(columnData, String)}; widgetOptions = {...widgetOptions, choices: Array.from(columnData, String)};
} }
} }
break; break;
} }
case 'ChoiceList': { case 'ChoiceList': {
if (Array.isArray(prevOptions.choices)) { // Use previous choices if they are set, e.g. if converting from Choice
// Use previous choices if they are set, e.g. if converting from ChoiceList if (!Array.isArray(widgetOptions.choices)) {
widgetOptions = {choices: prevOptions.choices, choiceOptions: prevOptions.choiceOptions};
} else {
// Set suggested choices. This happens before the conversion to ChoiceList, so we do some // Set suggested choices. This happens before the conversion to ChoiceList, so we do some
// light guessing for likely choices to suggest. // light guessing for likely choices to suggest.
const choices = new Set<string>(); const choices = new Set<string>();
@ -160,7 +182,7 @@ export async function prepTransformColInfo(docModel: DocModel, origCol: ColumnRe
} }
} }
choices.delete(""); choices.delete("");
widgetOptions = {choices: Array.from(choices)}; widgetOptions = {...widgetOptions, choices: Array.from(choices)};
} }
break; break;
} }
@ -204,12 +226,9 @@ export async function prepTransformColInfo(docModel: DocModel, origCol: ColumnRe
} }
} }
if (widgetOptions) { if (Object.keys(widgetOptions).length) {
colInfo.widgetOptions = JSON.stringify(widgetOptions); colInfo.widgetOptions = JSON.stringify(widgetOptions);
} }
if (rules) {
colInfo.rules = rules;
}
return colInfo; return colInfo;
} }

View File

@ -104,7 +104,6 @@ class TestUserActions(test_engine.EngineTestCase):
self.assertPartialOutActions(out_actions, { "stored": [ self.assertPartialOutActions(out_actions, { "stored": [
['ModifyColumn', 'Schools', 'city', {'type': 'Ref:Address'}], ['ModifyColumn', 'Schools', 'city', {'type': 'Ref:Address'}],
['UpdateRecord', 'Schools', 4, {'city': 0}], ['UpdateRecord', 'Schools', 4, {'city': 0}],
['UpdateRecord', '_grist_Views_section_field', 1, {'widgetOptions': ''}],
['UpdateRecord', '_grist_Tables_column', 23, { ['UpdateRecord', '_grist_Tables_column', 23, {
'type': 'Ref:Address', 'widgetOptions': 'world' 'type': 'Ref:Address', 'widgetOptions': 'world'
}], }],

View File

@ -705,11 +705,10 @@ class UserActions(object):
if has_diff_value(values, 'colId', c.colId): if has_diff_value(values, 'colId', c.colId):
self._do_doc_action(actions.RenameColumn(c.parentId.tableId, c.colId, values['colId'])) self._do_doc_action(actions.RenameColumn(c.parentId.tableId, c.colId, values['colId']))
# If we change a column's type, we should ALSO unset each affected field's widgetOptions and # If we change a column's type, we should ALSO unset each affected field's displayCol.
# displayCol.
type_changed = [c for c, values in update_pairs if has_diff_value(values, 'type', c.type)] type_changed = [c for c, values in update_pairs if has_diff_value(values, 'type', c.type)]
self._docmodel.update([f for c in type_changed for f in c.viewFields], self._docmodel.update([f for c in type_changed for f in c.viewFields],
widgetOptions='', displayCol=0) displayCol=0)
self.doBulkUpdateFromPairs(table_id, update_pairs) self.doBulkUpdateFromPairs(table_id, update_pairs)
@ -820,10 +819,8 @@ class UserActions(object):
# Look at the actual data for that column (first 1000 values) to decide on the type. # Look at the actual data for that column (first 1000 values) to decide on the type.
col_values['type'] = guess_type(self._get_column_values(col), convert=False) col_values['type'] = guess_type(self._get_column_values(col), convert=False)
# If changing the type of a column, unset its widgetOptions, displayCol and rules by default. # If changing the type of a column, unset its displayCol by default.
if 'type' in col_values: if 'type' in col_values:
col_values.setdefault('widgetOptions', '')
col_values.setdefault('rules', None)
col_values.setdefault('displayCol', 0) col_values.setdefault('displayCol', 0)
# Collect all updates for dependent summary columns. # Collect all updates for dependent summary columns.