2020-10-02 15:10:00 +00:00
|
|
|
/**
|
|
|
|
* This module contains various logic for converting columns between types. It is used from
|
|
|
|
* TypeTransform.js.
|
|
|
|
*/
|
|
|
|
// tslint:disable:no-console
|
|
|
|
|
2022-03-03 18:48:25 +00:00
|
|
|
import {isString} from 'app/client/lib/sessionObs';
|
2020-10-02 15:10:00 +00:00
|
|
|
import {DocModel} from 'app/client/models/DocModel';
|
|
|
|
import {ColumnRec} from 'app/client/models/entities/ColumnRec';
|
|
|
|
import * as gristTypes from 'app/common/gristTypes';
|
2021-07-23 15:29:35 +00:00
|
|
|
import {isFullReferencingType} from 'app/common/gristTypes';
|
2020-10-02 15:10:00 +00:00
|
|
|
import * as gutil from 'app/common/gutil';
|
2022-03-03 18:48:25 +00:00
|
|
|
import NumberParse from 'app/common/NumberParse';
|
2022-02-21 14:45:17 +00:00
|
|
|
import {dateTimeWidgetOptions, guessDateFormat} from 'app/common/parseDate';
|
2020-10-02 15:10:00 +00:00
|
|
|
import {TableData} from 'app/common/TableData';
|
2021-08-12 18:06:40 +00:00
|
|
|
import {decodeObject} from 'app/plugin/objtypes';
|
2020-10-02 15:10:00 +00:00
|
|
|
|
|
|
|
export interface ColInfo {
|
|
|
|
type: string;
|
|
|
|
isFormula: boolean;
|
|
|
|
formula: string;
|
|
|
|
visibleCol: number;
|
|
|
|
widgetOptions?: string;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns the suggested full type for `column` given a desired pure type to convert it to.
|
|
|
|
* Specifically, a pure type of "DateTime" returns a full type of "DateTime:{timezone}", and "Ref"
|
|
|
|
* returns a full type of "Ref:{TableId}". A `type` that's already complete is returned unchanged.
|
|
|
|
*/
|
|
|
|
export function addColTypeSuffix(type: string, column: ColumnRec, docModel: DocModel) {
|
|
|
|
switch (type) {
|
2021-07-23 15:29:35 +00:00
|
|
|
case "Ref":
|
|
|
|
case "RefList":
|
|
|
|
{
|
2020-10-02 15:10:00 +00:00
|
|
|
const refTableId = getRefTableIdFromData(docModel, column) || column.table().primaryTableId();
|
2021-07-23 15:29:35 +00:00
|
|
|
return `${type}:${refTableId}`;
|
2020-10-02 15:10:00 +00:00
|
|
|
}
|
|
|
|
case "DateTime":
|
2021-08-26 16:35:11 +00:00
|
|
|
return 'DateTime:' + docModel.docInfoRow.timezone();
|
2020-10-02 15:10:00 +00:00
|
|
|
default:
|
|
|
|
return type;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Looks through the data of the given column to find the first value of the form
|
2021-08-20 20:35:41 +00:00
|
|
|
* [R|r, <tableId>, <rowId>] (a Reference(List) value returned from a formula), and returns the tableId
|
2020-10-02 15:10:00 +00:00
|
|
|
* from that.
|
|
|
|
*/
|
|
|
|
function getRefTableIdFromData(docModel: DocModel, column: ColumnRec): string|null {
|
|
|
|
const tableData = docModel.docData.getTable(column.table().tableId());
|
|
|
|
const columnData = tableData && tableData.getColValues(column.colId());
|
|
|
|
if (columnData) {
|
|
|
|
for (const value of columnData) {
|
2021-08-20 20:35:41 +00:00
|
|
|
if (gristTypes.isReferencing(value)) {
|
2020-10-02 15:10:00 +00:00
|
|
|
return value[1];
|
2021-08-20 20:35:41 +00:00
|
|
|
} else if (gristTypes.isList(value)) {
|
|
|
|
const item = value[1];
|
|
|
|
if (gristTypes.isReference(item)) {
|
|
|
|
return item[1];
|
|
|
|
}
|
2020-10-02 15:10:00 +00:00
|
|
|
} else if (typeof value === 'string') {
|
2021-08-20 20:35:41 +00:00
|
|
|
// If it looks like a formatted Ref(List) value, e.g:
|
|
|
|
// - Table1[123]
|
|
|
|
// - [Table1[1], Table1[2], Table1[3]]
|
|
|
|
// - Table1[[1, 2, 3]]
|
|
|
|
// and the tableId is valid,
|
2020-10-02 15:10:00 +00:00
|
|
|
// use it. (This helps if a Ref-returning formula column got converted to Text first.)
|
2021-08-20 20:35:41 +00:00
|
|
|
const match = value.match(/^\[?(\w+)\[/);
|
2020-10-02 15:10:00 +00:00
|
|
|
if (match && docModel.docData.getTable(match[1])) {
|
|
|
|
return match[1];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Given info about the original column, and the type of the new one, returns a promise for the
|
|
|
|
// ColInfo to use for the transform column. Note that isFormula will be set to true, and formula
|
|
|
|
// will be set to the expression to compute the new values from the old ones.
|
|
|
|
// @param toTypeMaybeFull: Type to convert the column to, either full ('Ref:Foo') or pure ('Ref').
|
|
|
|
export async function prepTransformColInfo(docModel: DocModel, origCol: ColumnRec, origDisplayCol: ColumnRec,
|
|
|
|
toTypeMaybeFull: string): Promise<ColInfo> {
|
|
|
|
const toType = gristTypes.extractTypeFromColType(toTypeMaybeFull);
|
|
|
|
const tableData: TableData = docModel.docData.getTable(origCol.table().tableId())!;
|
|
|
|
let widgetOptions: any = null;
|
|
|
|
|
|
|
|
const colInfo: ColInfo = {
|
|
|
|
type: addColTypeSuffix(toTypeMaybeFull, origCol, docModel),
|
|
|
|
isFormula: true,
|
|
|
|
visibleCol: 0,
|
2022-02-04 11:13:03 +00:00
|
|
|
formula: "CURRENT_CONVERSION(rec)",
|
2020-10-02 15:10:00 +00:00
|
|
|
};
|
|
|
|
|
2022-02-21 14:45:17 +00:00
|
|
|
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() || {};
|
2020-10-02 15:10:00 +00:00
|
|
|
switch (toType) {
|
2022-02-21 14:45:17 +00:00
|
|
|
case 'Date':
|
|
|
|
case 'DateTime': {
|
|
|
|
let {dateFormat} = prevOptions;
|
|
|
|
if (!dateFormat) {
|
|
|
|
const colValues = tableData.getColValues(sourceCol.colId()) || [];
|
|
|
|
dateFormat = guessDateFormat(colValues.map(String)) || "YYYY-MM-DD";
|
|
|
|
}
|
2022-03-01 12:50:12 +00:00
|
|
|
widgetOptions = dateTimeWidgetOptions(dateFormat, true);
|
2022-02-21 14:45:17 +00:00
|
|
|
break;
|
|
|
|
}
|
2022-03-03 18:48:25 +00:00
|
|
|
case 'Numeric':
|
|
|
|
case 'Int': {
|
|
|
|
if (["Numeric", "Int"].includes(sourceCol.type())) {
|
|
|
|
widgetOptions = prevOptions;
|
|
|
|
} else {
|
|
|
|
const numberParse = NumberParse.fromSettings(docModel.docData.docSettings());
|
|
|
|
const colValues = tableData.getColValues(sourceCol.colId()) || [];
|
|
|
|
widgetOptions = numberParse.guessOptions(colValues.filter(isString));
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
2020-10-02 15:10:00 +00:00
|
|
|
case 'Choice': {
|
2021-05-18 05:01:53 +00:00
|
|
|
if (Array.isArray(prevOptions.choices)) {
|
|
|
|
// Use previous choices if they are set, e.g. if converting from ChoiceList
|
2021-07-08 21:35:16 +00:00
|
|
|
widgetOptions = {choices: prevOptions.choices, choiceOptions: prevOptions.choiceOptions};
|
2021-05-18 05:01:53 +00:00
|
|
|
} else {
|
|
|
|
// 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.
|
2022-02-21 14:45:17 +00:00
|
|
|
const columnData = tableData.getDistinctValues(sourceCol.colId(), 100);
|
2021-05-18 05:01:53 +00:00
|
|
|
if (columnData) {
|
|
|
|
columnData.delete("");
|
2021-08-12 18:06:40 +00:00
|
|
|
columnData.delete(null);
|
2021-05-18 05:01:53 +00:00
|
|
|
widgetOptions = {choices: Array.from(columnData, String)};
|
|
|
|
}
|
2020-10-02 15:10:00 +00:00
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
2021-05-12 14:34:49 +00:00
|
|
|
case 'ChoiceList': {
|
2021-05-18 05:01:53 +00:00
|
|
|
if (Array.isArray(prevOptions.choices)) {
|
|
|
|
// Use previous choices if they are set, e.g. if converting from ChoiceList
|
2021-07-08 21:35:16 +00:00
|
|
|
widgetOptions = {choices: prevOptions.choices, choiceOptions: prevOptions.choiceOptions};
|
2021-05-18 05:01:53 +00:00
|
|
|
} else {
|
|
|
|
// Set suggested choices. This happens before the conversion to ChoiceList, so we do some
|
|
|
|
// light guessing for likely choices to suggest.
|
|
|
|
const choices = new Set<string>();
|
2022-02-21 14:45:17 +00:00
|
|
|
for (let value of tableData.getColValues(sourceCol.colId()) || []) {
|
2021-08-12 18:06:40 +00:00
|
|
|
if (value === null) { continue; }
|
|
|
|
value = String(decodeObject(value)).trim();
|
2021-11-08 18:25:41 +00:00
|
|
|
const tags: unknown[] = (value.startsWith('[') && gutil.safeJsonParse(value, null)) || value.split(",");
|
2021-05-18 05:01:53 +00:00
|
|
|
for (const tag of tags) {
|
2021-11-08 18:25:41 +00:00
|
|
|
choices.add(String(tag).trim());
|
2021-05-18 05:01:53 +00:00
|
|
|
if (choices.size > 100) { break; } // Don't suggest excessively many choices.
|
|
|
|
}
|
2021-05-12 14:34:49 +00:00
|
|
|
}
|
2021-05-18 05:01:53 +00:00
|
|
|
choices.delete("");
|
|
|
|
widgetOptions = {choices: Array.from(choices)};
|
2021-05-12 14:34:49 +00:00
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
2021-07-23 15:29:35 +00:00
|
|
|
case 'Ref':
|
|
|
|
case 'RefList':
|
|
|
|
{
|
2020-10-02 15:10:00 +00:00
|
|
|
// Set suggested destination table and visible column.
|
2022-02-04 11:13:03 +00:00
|
|
|
// Undefined if toTypeMaybeFull is a pure type (e.g. converting to Ref before a table is chosen).
|
|
|
|
const optTableId = gutil.removePrefix(toTypeMaybeFull, `${toType}:`) || undefined;
|
|
|
|
|
|
|
|
let suggestedColRef: number;
|
|
|
|
let suggestedTableId: string;
|
|
|
|
const origColTypeInfo = gristTypes.extractInfoFromColType(origCol.type.peek());
|
|
|
|
if (!optTableId && origColTypeInfo.type === "Ref" || origColTypeInfo.type === "RefList") {
|
|
|
|
// When converting between Ref and Reflist, initially suggest the same table and visible column.
|
|
|
|
// When converting, if the table is the same, it's a special case.
|
|
|
|
// The visible column will not affect conversion.
|
|
|
|
// It will simply wrap the reference (row ID) in a list or extract the one element of a reference list.
|
|
|
|
suggestedColRef = origCol.visibleCol.peek();
|
|
|
|
suggestedTableId = origColTypeInfo.tableId;
|
|
|
|
} else {
|
|
|
|
// Finds a reference suggestion column and sets it as the current reference value.
|
|
|
|
const columnData = tableData.getDistinctValues(origDisplayCol.colId(), 100);
|
|
|
|
if (!columnData) { break; }
|
|
|
|
columnData.delete(gristTypes.getDefaultForType(origCol.type()));
|
|
|
|
|
|
|
|
// 'findColFromValues' function requires an array since it sends the values to the sandbox.
|
|
|
|
const matches: number[] = await docModel.docData.findColFromValues(Array.from(columnData), 2, optTableId);
|
|
|
|
suggestedColRef = matches.find(match => match !== origCol.getRowId())!;
|
|
|
|
if (!suggestedColRef) { break; }
|
|
|
|
const suggestedCol = docModel.columns.getRowModel(suggestedColRef);
|
|
|
|
suggestedTableId = suggestedCol.table().tableId();
|
|
|
|
if (optTableId && suggestedTableId !== optTableId) {
|
|
|
|
console.warn("Inappropriate column received from findColFromValues");
|
|
|
|
break;
|
|
|
|
}
|
2020-10-02 15:10:00 +00:00
|
|
|
}
|
2021-07-23 15:29:35 +00:00
|
|
|
colInfo.type = `${toType}:${suggestedTableId}`;
|
2020-10-02 15:10:00 +00:00
|
|
|
colInfo.visibleCol = suggestedColRef;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (widgetOptions) {
|
|
|
|
colInfo.widgetOptions = JSON.stringify(widgetOptions);
|
|
|
|
}
|
|
|
|
return colInfo;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Given the transformCol, calls (if needed) a user action to update its displayCol.
|
|
|
|
export async function setDisplayFormula(
|
|
|
|
docModel: DocModel, transformCol: ColumnRec, visibleCol?: number
|
|
|
|
): Promise<void> {
|
|
|
|
const vcolRef = (visibleCol == null) ? transformCol.visibleCol() : visibleCol;
|
|
|
|
if (isReferenceCol(transformCol)) {
|
|
|
|
const vcol = getVisibleColName(docModel, vcolRef);
|
|
|
|
const tcol = transformCol.colId();
|
|
|
|
const displayFormula = (vcolRef === 0 ? '' : `$${tcol}.${vcol}`);
|
|
|
|
return transformCol.saveDisplayFormula(displayFormula);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Returns the name of the visibleCol given its rowId.
|
|
|
|
function getVisibleColName(docModel: DocModel, visibleColRef: number): string|undefined {
|
|
|
|
return visibleColRef ? docModel.columns.getRowModel(visibleColRef).colId() : undefined;
|
|
|
|
}
|
|
|
|
|
2021-07-23 15:29:35 +00:00
|
|
|
// Returns whether the given column model is of type Ref or RefList.
|
2020-10-02 15:10:00 +00:00
|
|
|
function isReferenceCol(colModel: ColumnRec) {
|
2021-07-23 15:29:35 +00:00
|
|
|
return isFullReferencingType(colModel.type());
|
2020-10-02 15:10:00 +00:00
|
|
|
}
|