import moment from "moment"; import { cloneDeep, first, flatMap, flatMapDeep, isArray, isString, keysIn, valuesIn, } from "lodash"; import { customAlphabet as customAlphabetSync } from "nanoid"; import { ButtonActionType, ButtonOptions, CascadeOption, CascadeOptions, CheckboxIcon, CheckboxOptions, CollaboratorOptions, ColumnType, CreatedAtOptions, CreatedByOptions, DateOptions, LastModifiedByOptions, LastModifiedTimeOptions, LookupOptions, NumberOptions, SelectOptions, TableColumn, TextOptions, } from "../Column"; import { AttachmentValue, ButtonValue, CascadeValue, CellValue, DataStatusValue, DateValue, ForeignKeyValue, GeolocationValue, LookupValue, MultiSelectValue, MutiCollaboratorValue, NumberValue, TextValue, } from "../Cell"; import type { User } from "../Base"; import { checkCellValue, validFilter, validFilterOperator, validFilterValue, } from "./validCellVlaue"; import { IdPrefix } from "../Constant"; import { Color, TagColor } from "../Color"; import { RowData } from "../Record"; import { Sorts } from "../View"; import { dateToString } from "./DateFormat"; export const getValueByPath = function getValueByPathFn( object: any, prop: string[] ) { const paths = prop || []; let current = object; let result = null; for (let i = 0, j = paths.length; i < j; i++) { const path = paths[i]; if (!current) break; if (i === j - 1) { result = current[path as keyof typeof current]; break; } current = current[path as keyof typeof current]; } return result; }; export function setValueByPath(obj: any, value: any, _path: string) { let i; const path = _path.split("."); for (i = 0; i < path.length - 1; i++) { if (!obj[path[i]]) { obj[path[i]] = {}; } obj = obj[path[i]]; } obj[path[i]] = value; } const nanoidSync = customAlphabetSync( "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz", 14 ); export const isEmptyValue = (v: any) => ["", NaN, null, undefined].includes(v) || v?.length === 0; const compare = (property: PropertyKey, order = 1) => (a: any, b: any): number => { const value1 = Reflect.get(a || {}, property); const value2 = Reflect.get(b || {}, property); return order === -1 ? value1 - value2 : (value1 - value2) * -1; }; export const getColumnType = ( column: Pick, config?: { lookup: boolean } ): ColumnType => { const { type, typeOptions = {} } = column || {}; if (type === ColumnType.Text) { if ((typeOptions as TextOptions).validatorName === "url") { return ColumnType.Url; } return ColumnType.Text; } if (type === ColumnType.Number) { if ((typeOptions as NumberOptions).format === "currency") { return ColumnType.Currency; } if ((typeOptions as NumberOptions).format === "percent") { return ColumnType.Percent; } return ColumnType.Number; } if (type === ColumnType.Cascade) { if ((typeOptions as CascadeOptions).multiple) { return ColumnType.MultiCascade; } return ColumnType.Cascade; } if ( (typeOptions as CreatedAtOptions)?.formulaTextParsed === "{column_modification_created_at_all}" ) { return ColumnType.CreatedAt; } if ((typeOptions as CreatedAtOptions)?.formulaText === "CREATED_TIME()") { return ColumnType.CreatedAt; } if ( (typeOptions as LastModifiedTimeOptions)?.formulaTextParsed === "{column_modification_updated_at_all}" ) { return ColumnType.LastModifiedTime; } if ( (column?.typeOptions as LastModifiedTimeOptions)?.formulaText === "LAST_MODIFIED_TIME()" ) { return ColumnType.LastModifiedTime; } if ( type === ColumnType.Formula || type === ColumnType.Rollup || type === ColumnType.Lookup ) { return config?.lookup ? getColumnType({ typeOptions, type: (typeOptions as LookupOptions).resultType as any, }) : type; } return (column?.typeOptions as CreatedByOptions)?.computationType || type; }; export const getSubfix = (h: number): string => { if (h >= 0 && h <= 12) { return "上午"; } if (h >= 13 && h <= 24) { return "下午"; } return ""; }; export function formatTime(typeOptions: DateOptions, dateVal: string): string { if (!isString(dateVal)) { return ""; } const { isDate } = typeOptions; let date = ""; let time = ""; const parser = typeOptions.timeZone === "GMT" ? moment : moment.utc; const YYYY = parser(dateVal).utcOffset(8).year(); const MM = String(parser(dateVal).utcOffset(8).month() + 1).padStart(2, "0"); const DD = isDate === false ? "" : String(parser(dateVal).utcOffset(8).date()).padStart(2, "0"); const mm = isDate === false ? "" : String(parser(dateVal).utcOffset(8).minute()).padStart(2, "0"); const hh = isDate === false ? "" : String(parser(dateVal).utcOffset(8).hour()).padStart(2, "0"); switch (typeOptions?.dateFormat) { case "Local": date = `${YYYY}-${MM}${DD ? `-${DD}` : ""}`; break; case "Friendly": date = `${YYYY}年${MM}${DD ? `月${DD}` : ""}日`; break; case "US": date = `${MM}${DD ? `-${DD}` : ""}-${YYYY}`; break; case "European": date = `${DD ? `${DD}-` : ""}-${MM}-${YYYY}`; break; case "ISO": date = `${YYYY}-${MM}${DD ? `-${DD}` : ""}`; break; default: date = `${YYYY}-${MM}-${DD}`; break; } switch (typeOptions?.timeFormat) { case "12hour": time = `${Number(hh) % 12}:${mm} ${getSubfix(Number(hh))}`; break; case "24hour": time = `${hh}:${mm}`; break; default: time = `${hh}:${mm}`; break; } return ( date + (typeOptions.isDateTime && typeOptions.isDate ? ` ${time}` : "") ); } /** * @description: 根据type和单元格数据获取最终展示的文本string * @param {Field} column * @param {any} data * @return {*} */ export const getCellDisplayText = ( params: { column: Pick; userInfoById: Record>; attachmentOptions?: { name?: boolean; url?: boolean; }; }, data: CellValue ): string => { const { column, userInfoById, attachmentOptions } = params; if ( (!checkCellValue(getColumnType(column), column.typeOptions, data) && column?.type !== "checkbox") || !column || (column?.type !== "checkbox" && ["", null, undefined].includes(data as any)) ) { return ""; } switch (getColumnType(column)) { case ColumnType.Collaborator: case ColumnType.CreatedBy: case ColumnType.LastModifiedBy: return userInfoById[data as number]?.name || ""; case ColumnType.Date: case ColumnType.LastModifiedTime: case ColumnType.CreatedAt: return fastFormatTime( column.typeOptions as DateOptions, data as DateValue ); case ColumnType.Number: case ColumnType.Percent: case ColumnType.Currency: return formatNumber( column.typeOptions as NumberOptions, data as NumberValue ); case ColumnType.Text: case ColumnType.Url: case ColumnType.AutoNumber: return data!.toString(); case ColumnType.MultilineText: return replaceMentionName(data as TextValue, userInfoById); case ColumnType.MultiCollaborator: return (data as MutiCollaboratorValue) .map((v) => userInfoById[v]?.name || "") .join(","); case ColumnType.MultiSelect: { const typeOptions = column.typeOptions as SelectOptions; const options = typeOptions.optionOrder.map( (v) => typeOptions.options[v] ); return (data as MultiSelectValue) .map((id: string) => options.find((op) => op.id === id)?.name) .join(","); } case ColumnType.MultiCascade: case ColumnType.Cascade: { const typeOptions = column.typeOptions as CascadeOptions; const { options } = typeOptions; return (data as MultiSelectValue) .map((id: string) => { const str = [options.find((v) => v.id === id).name]; const { parentId } = options.find((v) => v.id === id); let parentNode = options.find((v) => v.id === parentId); while (parentNode) { str.splice(0, 0, parentNode.name); parentNode = options.find((v) => v.id === parentNode?.parentId); } return str.join("/"); }) .join(","); } case ColumnType.Select: { const typeOptions = column.typeOptions as SelectOptions; const options = typeOptions.optionOrder.map( (v) => typeOptions.options[v] ); return options.find((op) => op.id === data)?.name || ""; } case ColumnType.Checkbox: return data ? "是" : "否"; case ColumnType.DataStatus: { if (data === DataStatusValue.inProgress) { return "处理中"; } else if (data === DataStatusValue.completed) { return "已完成"; } else { return ""; } } case ColumnType.Attachment: { if (attachmentOptions?.name && attachmentOptions?.url) { return (data as AttachmentValue) .map((v) => `${v.filename}(${resolveAssetsUrl(v.url)})`) .join(","); } if (!attachmentOptions?.name && attachmentOptions?.url) { return (data as AttachmentValue) .map((v) => `${resolveAssetsUrl(v.url)}`) .join(","); } return (data as AttachmentValue).map((v) => v.filename).join(","); } case ColumnType.ForeignKey: return (data as ForeignKeyValue) .map((v) => v.foreignRowDisplayName) .join(","); case ColumnType.GeoLocation: return `${(data as GeolocationValue).name} ${ (data as GeolocationValue).address }`; case ColumnType.Button: return ""; case ColumnType.Lookup: { const { resultType } = params.column.typeOptions as LookupOptions; const typeOptions = params.column.typeOptions as LookupOptions; let val: CellValue[] = flatMapDeep( ((data as LookupValue)?.foreignRowIdOrder || []).map( (id) => (data as LookupValue).valuesByForeignRowId[id] ) as [] ) as any; if ( [ ColumnType.MultiSelect, ColumnType.Collaborator, ColumnType.MultiCollaborator, ColumnType.MultiCascade, ColumnType.ForeignKey, ].includes(resultType) ) { val = [cloneDeep(val) as any]; } return val .map((a) => getCellDisplayText( { ...params, column: { type: resultType as any, typeOptions: { ...typeOptions, optionOrder: typeOptions.choiceOrder, options: typeOptions.options || typeOptions.choices, }, }, }, a ) ) .join(","); } default: return data?.toString?.() || ""; } }; export const replaceMentionName = ( text: string, userInfoById?: Record> ): string => { const msg = text || ""; const TOKEN = /\[\$!\/(men[0-9a-zA-Z]{14})\/(\d+)\|(((?!!\$\]).)*)!\$\]/g; let l = ""; let index = 0; msg.replace(TOKEN, (_, p1, p2, p3, i, j) => { l += msg.slice(index, j); index = j + _.length; l += `@${ (userInfoById && userInfoById[p2] && userInfoById[p2].name) || "用户不存在或已离开" }`; return _; }); l += msg.slice(index, msg.length); return l; }; export const formatNumber = ( typeOptions: NumberOptions, v: NumberValue ): string => { if (typeOptions.format === "percent") { return `${(v * 100)?.toFixed(typeOptions.precision || 1)}%`; } if (typeOptions.format === "currency") { return `${typeOptions.symbol}${v?.toFixed(typeOptions.precision || 0)} `; } return v?.toFixed(typeOptions.precision || 0); }; function resolveAssetsUrl(url: string) { return url ? resolvePath("https://assets.cdn.dtable.cloud", url) : "null"; } export default function resolvePath(pathA = "", pathB = "") { if (pathA.endsWith("/") && pathB.startsWith("/")) { return `${pathA.slice(0, pathA.length - 1)}${pathB}`; } return `${pathA}${pathB}`; } export function fastFormatTime( typeOptions: DateOptions, dateVal: string ): string { if (!isString(dateVal)) { return ""; } return dateToString(typeOptions, dateVal); const text = dateVal; let date = text?.split("T")[0]; let time = ""; const YYYY = dateVal.slice(0, 4); const MM = dateVal.slice(5, 7); const DD = dateVal.slice(8, 10); const hh = dateVal.slice(11, 13); const mm = dateVal.slice(14, 16); if (typeOptions.isDate === false) { return `${YYYY}-${MM}`; } switch (typeOptions?.dateFormat) { case "Local": date = `${YYYY}-${MM}${DD ? `-${DD}` : ""}`; break; case "Friendly": date = `${YYYY}年${MM}${DD ? `月${DD}` : ""}日`; break; case "US": date = `${MM}/${DD}/${YYYY}`; break; case "European": date = `${DD}/${MM}/${YYYY}`; break; case "ISO": date = `${YYYY}-${MM}-${DD}`; break; default: date = `${YYYY}-${MM}-${DD}`; break; } switch (typeOptions?.timeFormat) { case "12hour": time = `${Number(hh) % 12}:${mm} ${getSubfix(Number(hh))}`; break; case "24hour": time = `${hh}:${mm}`; break; default: time = `${hh}:${mm}`; break; } return ( date + (typeOptions.isDateTime && typeOptions.isDate ? ` ${time}` : "") ); } export const formatFieldFromString = ( newField: Pick, arr: string[] ): Pick => { const typeOptions = {}; switch (newField.type) { case ColumnType.Cascade: { const typeOptionsCopy = { options: [], multiple: true, checkStrictly: false, titles: [{ name: "标题", id: createNanoidSync(IdPrefix.CascadeTitle) }], } as CascadeOptions; return { ...newField, typeOptions: typeOptionsCopy, }; } case ColumnType.MultiSelect: { const options = Array.from( new Set(flatMap(arr.map((str) => (str as string).trim().split(",")))) ); const ids: string[] = []; options.forEach(() => { ids.push(createNanoidSync(IdPrefix.Select)); }); const typeOptionsCopy = { optionOrder: ids, options: {}, disableColors: false, } as SelectOptions; ids.forEach((id, j) => { typeOptionsCopy.options[id] = { color: getNewColor( Object.keys(typeOptionsCopy.options).map( (key) => typeOptionsCopy.options[key].color ) ) as any, name: options[j] as string, id, }; }); return { ...newField, typeOptions: typeOptionsCopy, }; } case ColumnType.Cascade: case ColumnType.MultiCascade: { const treeObj = {} as any; const options = Array.from( new Set(flatMap(arr.map((str) => (str as string).trim().split(",")))) ); let titles: CascadeOptions["titles"] = []; options.forEach((v) => { // ids.push(createNanoidSync(IdPrefix.Select)); const paths = v.split("/"); if (paths.length > titles.length) { titles = paths.map((p, k) => ({ name: `标题${k + 1}`, id: createNanoidSync(IdPrefix.CascadeTitle), })); } paths.forEach((t, k) => { if (!getValueByPath(treeObj, paths)) { setValueByPath(treeObj, {}, paths.join(".")); } }); }); const typeOptionsCopy = { options: getOptionsFromObj(treeObj, 0, [], { titles }), multiple: true, checkStrictly: true, titles, } as CascadeOptions; return { ...newField, typeOptions: typeOptionsCopy, }; } case ColumnType.Select: { const options = Array.from( new Set( arr.filter((v) => isString(v)).map((str) => (str as string).trim()) ) ); const ids: string[] = []; options.forEach(() => { ids.push(createNanoidSync(IdPrefix.Select)); }); const typeOptionsCopy = { optionOrder: ids, options: {}, disableColors: false, } as SelectOptions; ids.forEach((id, j) => { typeOptionsCopy.options[id] = { color: getNewColor( Object.keys(typeOptionsCopy.options).map( (key) => typeOptionsCopy.options[key].color ) ) as any, name: options[j] as string, id, }; }); return { ...newField, typeOptions: typeOptionsCopy, }; } case ColumnType.Collaborator: return { ...newField, typeOptions: { shouldNotify: true, } as CollaboratorOptions, }; case ColumnType.MultiCollaborator: return { ...newField, typeOptions: { shouldNotify: true, } as CollaboratorOptions, }; case ColumnType.Number: return { ...newField, typeOptions: { format: "decimal", precision: 1, negative: false }, }; case ColumnType.Url: return { ...newField, type: ColumnType.Text, typeOptions: { validatorName: "url", } as TextOptions, }; case ColumnType.CreatedAt: return { ...newField, type: ColumnType.Formula, typeOptions: { formulaText: "CREATED_TIME()", resultType: "date", dateFormat: "Local", isDateTime: false, timeFormat: "12hour", timeZone: "UTC", } as CreatedAtOptions, }; case ColumnType.CreatedBy: return { ...newField, type: "computation" as any, typeOptions: { computationType: "createdBy", computationParams: {}, } as CreatedByOptions, }; case ColumnType.Checkbox: return { ...newField, type: ColumnType.Checkbox, typeOptions: { icon: CheckboxIcon.Check, color: Color.GreenDark, } as CheckboxOptions, }; case ColumnType.LastModifiedBy: return { ...newField, type: ColumnType.Computation, typeOptions: { computationType: ColumnType.LastModifiedBy, computationParams: { allColumns: true, columnIds: [], }, } as LastModifiedByOptions, }; case ColumnType.LastModifiedTime: return { ...newField, type: ColumnType.Formula, typeOptions: { formulaText: "LAST_MODIFIED_TIME()", resultType: "date", dateFormat: "Local", isDateTime: false, timeFormat: "12hour", timeZone: "UTC", } as LastModifiedTimeOptions, }; case ColumnType.Currency: return { ...newField, type: ColumnType.Number, typeOptions: { format: "currency", symbol: "¥", } as NumberOptions, }; case ColumnType.Percent: return { ...newField, type: ColumnType.Number, typeOptions: { format: "percent", } as NumberOptions, }; case ColumnType.Button: return { ...newField, type: ColumnType.Button, typeOptions: { label: { type: "static", staticText: "打开链接", }, variant: { type: "static", staticVariant: "#25BF84", }, formulaText: "", actionType: ButtonActionType.openUrl, } as ButtonOptions, }; default: return { ...newField, typeOptions, }; } }; export function createNanoidSync(prefix: string) { const id = nanoidSync(); return `${prefix}${id}`; } export const getNewColor = ( colorList: string[], options?: { from?: string[] } ): string => { const { from = valuesIn(TagColor) } = options || {}; const darkColorList = from .filter((v) => !colorList.includes(v)) .filter((v) => isDarkColor(v)); const lightColorList = from .filter((v) => !colorList.includes(v)) .filter((v) => isLightColor(v)); const repeated = from.filter((v) => colorList.includes(v)); return [...darkColorList, ...lightColorList, ...repeated][ colorList.length % from.length ]; }; export function isDarkColor(color?: string): boolean { return Boolean( color && [ Color.BlueDark, Color.IceDark, Color.RedDark, Color.RoseDark, Color.GrayDark, Color.GreenDark, Color.PurpleDark, Color.YellowDark, Color.OrangeDark, // 兼容判断旧数据 "#25BF84", "#67D0F1", "#639BF3", "#B992EB", "#F572A9", "#FC6A6A", "#F88C5E", "#F6B30D", "#1F1F1F", ].includes(color) ); } export function isLightColor(color?: string): boolean { return [ Color.IceLight, Color.RedLight, Color.BlueLight, Color.GrayLight, Color.RoseLight, Color.GreenLight, Color.OrangeLight, Color.YellowLight, Color.PurpleLight, ].includes(color as any); } export const sortRecords = ( rows: RowData[], config: { userInfoById: Record>; columns: TableColumn[]; sorts: Sorts; } ): RowData[] => { const { sorts, columns, userInfoById = {} } = config; const arr = cloneDeep(rows); const ruleValsList: Array = []; const weightList: { weight: number; field: string; value: any }[] = []; let newArr: RowData[] = []; sorts.forEach((srt) => { const vals: CellValue[] = []; const field = columns.find((col) => col.id === srt.columnId); const fieldType = getColumnType(field as TableColumn); arr .filter((r) => fieldType === ColumnType.Checkbox ? true : r.cellValuesByColumnId[srt.columnId] ) .forEach((row) => { if ( !vals.includes( getSortableCellValue( { column: field as TableColumn, userInfoById }, row.cellValuesByColumnId[srt.columnId] ) ) ) { vals.push( getSortableCellValue( { column: field as TableColumn, userInfoById }, row.cellValuesByColumnId[srt.columnId] ) ); } }); let arrSort: CellValue[] = []; arrSort = (vals as TextValue[]).sort((a, b) => { if ( [ ColumnType.Number, ColumnType.Percent, ColumnType.Currency, ColumnType.AutoNumber, ].includes( getColumnType(columns.find((col) => col.id === srt.columnId)!) ) ) { return parseFloat(a) > parseFloat(b) ? 1 : -1; } return a.localeCompare(b); }); if (srt.ascending === false) { arrSort = arrSort.reverse(); } // 不管倒序还是顺序, 把代表数据为空的总是放后面,(checkbox除外) if ( columns.find((v) => v.id === srt.columnId)?.type === ColumnType.Checkbox ) { ruleValsList.push([...arrSort.filter((_v) => _v !== "null")]); } else { ruleValsList.push([...arrSort.filter((_v) => _v !== "null"), "null"]); } }); ruleValsList.forEach((v1, i) => { v1.forEach((v2: any) => { weightList.push({ field: sorts[i].columnId, value: v2, weight: 0, }); }); }); weightList.forEach((v, i) => { weightList[i].weight = 2 ** (weightList.length - i); }); for (let k = 0; k < arr.length; ) { (arr[k] as any).weight = 1; weightList.forEach((v2) => { const field = columns.find((col) => col.id === v2.field); if ( getSortableCellValue( { column: field as TableColumn, userInfoById }, arr[k].cellValuesByColumnId[v2.field] ) === v2.value ) { (arr[k] as any).weight += v2.weight; } }); k++; } newArr = [...arr.sort(compare("weight"))]; return newArr; }; export function getSortableCellValue( params: { column: Pick; userInfoById: Record>; }, _data: CellValue ): string { const data = cloneDeep(_data); const { column, userInfoById = {} } = params; if (column.type === ColumnType.Checkbox) { return data ? "2" : "1"; } if (!column || isEmptyValue(data)) { return ""; } switch (getColumnType(column)) { case ColumnType.Text: case ColumnType.MultilineText: case ColumnType.Url: return _data as TextValue; case ColumnType.LastModifiedTime: case ColumnType.CreatedAt: case ColumnType.Date: { return fastFormatTime( { ...(column.typeOptions as DateOptions), timeFormat: "24hour", } as DateOptions, _data as DateValue ); } case ColumnType.Attachment: case ColumnType.ForeignKey: case ColumnType.MultiCollaborator: case ColumnType.Collaborator: { return getCellDisplayText({ column, userInfoById }, _data); } case ColumnType.Cascade: case ColumnType.MultiCascade: { return ((data || []) as CascadeValue) .map((v) => { const str = Array.from( { length: ( column.typeOptions as CascadeOptions ).options.length.toString().length, }, () => "0" ).join(""); const { titles } = column.typeOptions as CascadeOptions; const options = cloneDeep( (column.typeOptions as CascadeOptions).options ).sort((a, b) => { if (a.titleId === b.titleId) { return a.rank < b.rank ? 1 : -1; } return titles.findIndex((t) => t.id === a.titleId) < titles.findIndex((t) => t.id === b.titleId) ? 1 : -1; }); const i = options.findIndex((a) => a.id === v).toString(); return `${str.slice(0, str.length - i.length)}${i}`; }) .toString(); } case ColumnType.MultiSelect: { return ((data as MultiSelectValue) || []) .map((v) => { const str = Array.from( { length: ( column.typeOptions as SelectOptions ).optionOrder.length.toString().length, }, () => "0" ).join(""); const i = (column.typeOptions as SelectOptions).optionOrder .findIndex((a) => a === v) .toString(); return `${str.slice(0, str.length - i.length)}${i}`; }) .toString(); } case ColumnType.Select: { return (data ? [data] : []) .map((v) => { const str = Array.from( { length: ( column.typeOptions as SelectOptions ).optionOrder.length.toString().length, }, () => "0" ).join(""); const i = (column.typeOptions as SelectOptions).optionOrder .findIndex((a) => a === v) .toString(); return `${str.slice(0, str.length - i.length)}${i}`; }) .toString(); } case ColumnType.AutoNumber: case ColumnType.Currency: case ColumnType.Number: { return (data as NumberValue).toFixed( (column.typeOptions as NumberOptions).precision || 0 ); } case ColumnType.Lookup: { const vals = ((data as LookupValue).foreignRowIdOrder || []).map( (v) => (data as LookupValue).valuesByForeignRowId[v] ); if ( isArray(first(vals)) && [ColumnType.Text, ColumnType.Date, ColumnType.Number].includes( (column.typeOptions as LookupOptions).resultType ) ) { return flatMap(vals) .map((v) => getSortableCellValue( { column: { ...column, type: (column.typeOptions as LookupOptions).resultType, typeOptions: { ...column.typeOptions, optionOrder: (column?.typeOptions as LookupOptions) ?.choiceOrder, options: (column?.typeOptions as LookupOptions)?.choices, }, }, userInfoById, }, v ) ) .join(""); } return vals .map((v) => getSortableCellValue( { column: { ...column, type: (column.typeOptions as LookupOptions).resultType, typeOptions: { ...column.typeOptions, optionOrder: (column?.typeOptions as LookupOptions) ?.choiceOrder, options: (column?.typeOptions as LookupOptions)?.choices, }, }, userInfoById, }, v ) ) .join(""); } case ColumnType.GeoLocation: { return data ? `${(data as GeolocationValue).name} ${ (data as GeolocationValue).address }` : ""; } case ColumnType.Button: { return ""; } default: return data?.toString() || ""; } } export const getOptionsFromObj = ( obj: any, level: number, list: CascadeOption[], { titles = [], parentId, }: { titles: CascadeOptions["titles"]; parentId?: string } ) => { let l = cloneDeep(list); let rank = 1; // eslint-disable-next-line guard-for-in for (const key in obj) { const id = createNanoidSync(IdPrefix.CascadeId); // eslint-disable-next-line @typescript-eslint/naming-convention const _titles = level + 1 > titles.length ? [ ...titles, { id: createNanoidSync(IdPrefix.CascadeTitle), name: getNewUnrepeatedName( "标题", titles.map((t) => t.name) ), }, ] : titles; l.push({ name: key, id, rank, titleId: _titles[level].id, ...(parentId ? { parentId } : {}), }); if (keysIn(obj[key]).length > 0) { l = getOptionsFromObj(obj[key], level + 1, l, { parentId: id, titles: _titles, }); } rank++; } return l; }; export const getNewUnrepeatedName = ( name: string, existNames: string[] ): string => { let newName = `${name}`; let i = 1; // eslint-disable-next-line @typescript-eslint/no-loop-func while (existNames.some((n) => n === newName)) { i++; newName = `${name} ${i}`; } return newName; }; export { validFilter, validFilterOperator, validFilterValue }; export const isValidId = (idPrefix: IdPrefix, vid: string) => { const pattern = new RegExp(`^(${idPrefix})[a-z|A-Z|0-9]{14}$`); return pattern.test(vid); }; export const getCellSearchableText = ( params: { column: Pick; userInfoById: Record>; withAttachmentUrl?: boolean; }, data: CellValue ): string => { const { column } = params; if (column.type === ColumnType.Button) { return `${(data as ButtonValue)?.label || ""}${ (data as ButtonValue)?.url || "" }`; } return getCellDisplayText(params, data); };