import { CellValue, ColumnType, ITypeOptions, Language, MaybeAbsentCellValue, } from '../../../types'; import { exhaustiveSwitch, exhaustiveSwitchThrow, getTypeOptions, isAbsentCellValue, isArrayColumnType, isEmptyCellValue, isNotNullable, } from '../../../typeUtils'; import { convertMaybeCollaboratorCellValueToGroupIdArray } from '../../../typeUtils/cells'; import { ICollaboratorsById } from '../types'; import { arrayToStringDelimiter, stringToArrayDelimiter } from './constants'; import { transformCurrencyToString } from './transformCurrencyToString'; import { transformDatetimeToString } from './transformDatetimeToString'; import { transformFormulaToString } from './transformFormulaToString'; import { transformNumberToString } from './transformNumberToString'; const debug = require('debug')('treelab:common'); export const transformCellValueToString = ({ cellValue, typeOptions, opts = {}, }: { cellValue: MaybeAbsentCellValue; typeOptions: ITypeOptions; opts?: { preserveLongText?: boolean; locale?: Language; collaboratorsById?: ICollaboratorsById; }; }): string | null => { if (isAbsentCellValue(cellValue) || isEmptyCellValue(cellValue)) return null; switch (typeOptions.type) { case ColumnType.TEXT: case ColumnType.PHONE: case ColumnType.EMAIL: return cellValue as CellValue< ColumnType.TEXT | ColumnType.PHONE | ColumnType.EMAIL >; case ColumnType.AUTO_NUMBER: case ColumnType.CHECKBOX: case ColumnType.COLLABORATOR: case ColumnType.CURRENCY: case ColumnType.DATETIME: case ColumnType.FORMULA: case ColumnType.PROGRESS: case ColumnType.INTEGRATION_REFERENCE: case ColumnType.LONG_TEXT: case ColumnType.LOOKUP: case ColumnType.MULTI_ATTACHMENT: case ColumnType.MULTI_SELECT: case ColumnType.NOOP: case ColumnType.NUMBER: case ColumnType.RATING: case ColumnType.RECORD_REFERENCE: case ColumnType.SUBTABLE: case ColumnType.ROLLUP: case ColumnType.SELECT: case ColumnType.STATUS: case ColumnType.UNIQUE_ID: case ColumnType.CREATED_AT: case ColumnType.CREATED_BY: return transformCellValueToStringArray({ cellValue, typeOptions, opts, }) .join(arrayToStringDelimiter) .trim(); default: { const { type } = typeOptions; return exhaustiveSwitch({ switchValue: type, returnValue: null, }); } } }; /** * The returned string will not contain any newlines, assuming valid input. */ export const transformCellValueToStringArray = < OT extends ColumnType = ColumnType >({ cellValue, typeOptions, opts = {}, }: { cellValue: MaybeAbsentCellValue; typeOptions: ITypeOptions; opts?: { preserveLongText?: boolean; locale?: Language; collaboratorsById?: ICollaboratorsById; }; }): string[] => { if (isAbsentCellValue(cellValue) || isEmptyCellValue(cellValue)) return []; const stringArray = (() => { switch (typeOptions.type) { case ColumnType.TEXT: case ColumnType.PHONE: case ColumnType.EMAIL: case ColumnType.UNIQUE_ID: case ColumnType.RATING: case ColumnType.AUTO_NUMBER: return `${ cellValue as CellValue< | ColumnType.TEXT | ColumnType.PHONE | ColumnType.EMAIL | ColumnType.UNIQUE_ID | ColumnType.RATING | ColumnType.AUTO_NUMBER > }`.split(stringToArrayDelimiter); case ColumnType.LONG_TEXT: { if (opts.preserveLongText) { return (cellValue as CellValue).split( stringToArrayDelimiter ); } return (cellValue as CellValue) .trim() .replace(/\r/g, '') .replace(/\s*\n+\s*/g, ' ') .split(stringToArrayDelimiter); } case ColumnType.NUMBER: return [ transformNumberToString( cellValue as CellValue, typeOptions as ITypeOptions ), ]; case ColumnType.CURRENCY: { const formattedCurrency = transformCurrencyToString( cellValue as CellValue, typeOptions as ITypeOptions ); if (formattedCurrency === null) return ['']; return [formattedCurrency]; } case ColumnType.DATETIME: case ColumnType.CREATED_AT: { return [ transformDatetimeToString( cellValue as CellValue, typeOptions as ITypeOptions, opts.locale ), ]; } case ColumnType.MULTI_ATTACHMENT: { return (cellValue as CellValue).map( (attachment) => `${attachment.fileName} (${attachment.url})` ); } case ColumnType.RECORD_REFERENCE: { const defaultVisibleName = opts.locale === Language.en ? 'Unnamed Record' : '未命名记录'; return (cellValue as CellValue) .map(({ visibleName }) => visibleName ?? defaultVisibleName) .filter(isNotNullable); } case ColumnType.SUBTABLE: { return ( opts.locale === Language.en ? `${(cellValue as CellValue).length}条记录` : '' ).split(stringToArrayDelimiter); } case ColumnType.STATUS: { return [ getStatusOptionName({ statusId: cellValue as CellValue, originTypeOptions: typeOptions as ITypeOptions, }), ]; } case ColumnType.SELECT: { return [ getSelectOptionName({ optionId: cellValue as CellValue, originTypeOptions: typeOptions as ITypeOptions, }), ]; } case ColumnType.MULTI_SELECT: { return (cellValue as CellValue).map( (optionId) => getSelectOptionName({ optionId, originTypeOptions: typeOptions as ITypeOptions, }) ); } case ColumnType.COLLABORATOR: case ColumnType.CREATED_BY: { const collabIds = convertMaybeCollaboratorCellValueToGroupIdArray( cellValue as CellValue< ColumnType.COLLABORATOR | ColumnType.CREATED_BY > ); return collabIds.map((groupId) => { const nickName = opts.collaboratorsById?.[groupId]?.nickName; debug( 'warning: transforming value to COLLABORATOR with no collaboratorsById' ); return ( nickName ?? (opts.locale === Language.en ? 'Unknown User' : '用户不存在') ); }); } case ColumnType.CHECKBOX: { return [ (cellValue as CellValue) ? opts.locale === Language.en ? 'checked' : '已勾选' : '', ]; } case ColumnType.ROLLUP: case ColumnType.FORMULA: case ColumnType.PROGRESS: { return [ transformFormulaToString( cellValue as CellValue, typeOptions as ITypeOptions, { locale: opts.locale } ), ]; } case ColumnType.LOOKUP: { const originTypeOptions = typeOptions as ITypeOptions; const lookupCellValue = cellValue as CellValue; if ( isArrayColumnType(originTypeOptions.lookupColumnType) && lookupCellValue.length > 0 && !Array.isArray(lookupCellValue[0]) ) { return [ transformCellValueToString({ cellValue: cellValue as CellValue< typeof originTypeOptions.lookupColumnType >, typeOptions: getTypeOptions(originTypeOptions) as ITypeOptions< typeof originTypeOptions.lookupColumnType >, opts: { locale: opts.locale, collaboratorsById: opts.collaboratorsById, }, }), ].filter(isNotNullable); } return lookupCellValue .map((value) => { return transformCellValueToString({ cellValue: value as CellValue< typeof originTypeOptions.lookupColumnType >, typeOptions: getTypeOptions(originTypeOptions) as ITypeOptions< typeof originTypeOptions.lookupColumnType >, opts: { locale: opts.locale, collaboratorsById: opts.collaboratorsById, }, }); }) .filter(isNotNullable); } case ColumnType.INTEGRATION_REFERENCE: case ColumnType.NOOP: throw new Error( `Attempted to convert to string cell value of unknown type: ${typeOptions.type}` ); default: const { type } = typeOptions; return exhaustiveSwitchThrow({ switchValue: type, }); } })(); return stringArray .map((stringValue) => stringValue.trim()) .filter(isNotNullable); }; const getSelectOptionName = ({ originTypeOptions, optionId, }: { originTypeOptions: Pick< ITypeOptions, 'options' >; optionId: string; }) => { // TODO: it would be nice if we could avoid doing a linear search here. // maybe this logic can be moved to a class which preserves state between // individual cell transformations in order to do this more efficiently. const option = originTypeOptions.options.find( (option) => option.optionId === optionId ); if (!option) { throw new Error( 'Could not find source option in type options for select cell value' ); } return option.name.trim(); }; const getStatusOptionName = ({ originTypeOptions, statusId, }: { originTypeOptions: Pick, 'statuses'>; statusId: string; }) => { const option = originTypeOptions.statuses.find( (status) => status.statusId === statusId ); if (!option) { throw new Error( 'Could not find source option in type options for status cell value' ); } return option.name.trim(); };