import { UITypes } from 'nocodb-sdk' import isURL from 'validator/lib/isURL' // This regex pattern matches email addresses by looking for sequences that start with characters before the "@" symbol, followed by the domain. // It's designed to capture most email formats, including those with periods and "+" symbols in the local part. const validateEmail = (v: string) => /^(([^<>()[\].,;:\s@"]+(\.[^<>()[\].,;:\s@"]+)*)|(".+"))@(([^<>()[\].,;:\s@"]+\.)+[^<>()[\].,;:\s@"]{2,})$/i.test(v) export const extractEmail = (v: string) => { const matches = v.match( /(([^<>()[\].,;:\s@"]+(\.[^<>()[\].,;:\s@"]+)*)|(".+"))@(([^<>()[\].,;:\s@"]+\.)+[^<>()[\].,;:\s@"]{2,})/i, ) return matches ? matches[0] : null } const booleanOptions = [ { checked: true, unchecked: false }, { 'x': true, '': false }, { yes: true, no: false }, { y: true, n: false }, { 1: true, 0: false }, { '[x]': true, '[]': false, '[ ]': false }, { '☑': true, '': false }, { '✅': true, '': false }, { '✓': true, '': false }, { '✔': true, '': false }, { enabled: true, disabled: false }, { on: true, off: false }, { 'done': true, '': false }, { true: true, false: false }, ] const aggBooleanOptions: any = booleanOptions.reduce((obj, o) => ({ ...obj, ...o }), {}) const getColVal = (row: any, col?: number) => { return row && col !== undefined ? row[col] : row } export const isCheckboxType: any = (values: [], col?: number) => { let options = booleanOptions for (let i = 0; i < values.length; i++) { const val = getColVal(values[i], col) if (val === null || val === undefined || val.toString().trim() === '') { continue } options = options.filter((v) => val in v) if (!options.length) { return false } } return true } export const getCheckboxValue = (value: any) => { return value && aggBooleanOptions[value] } export const isMultiLineTextType = (values: [], col?: number) => { return values.some( (r) => (getColVal(r, col) || '').toString().match(/[\r\n]/) || (getColVal(r, col) || '').toString().length > 255, ) } export const extractMultiOrSingleSelectProps = (colData: []) => { const maxSelectOptionsAllowed = 64 const colProps: any = {} if (colData.some((v: any) => v && (v || '').toString().includes(','))) { const flattenedVals = colData.flatMap((v: any) => v ? v .toString() .trim() .split(/\s*,\s*/) : [], ) const uniqueVals = [ ...new Set(flattenedVals.filter((v) => v !== null && v !== undefined).map((v: any) => v.toString().trim())), ] if (uniqueVals.length > maxSelectOptionsAllowed) { // too many options are detected, convert the column to SingleLineText instead colProps.uidt = UITypes.SingleLineText // _disableSelect is used to disable the in TemplateEditor colProps._disableSelect = true } else { // assume the column type is multiple select if there are repeated values if (flattenedVals.length > uniqueVals.length && uniqueVals.length <= Math.ceil(flattenedVals.length / 2)) { colProps.uidt = UITypes.MultiSelect } // set dtxp here so that users can have the options even they switch the type from other types to MultiSelect // once it's set, dtxp needs to be reset if the final column type is not MultiSelect colProps.dtxp = `${uniqueVals.map((v) => `'${v.replace(/'/gi, "''")}'`).join(',')}` } } else { const uniqueVals = [...new Set(colData.filter((v) => v !== null && v !== undefined).map((v: any) => v.toString().trim()))] if (uniqueVals.length > maxSelectOptionsAllowed) { // too many options are detected, convert the column to SingleLineText instead colProps.uidt = UITypes.SingleLineText // _disableSelect is used to disable the in TemplateEditor colProps._disableSelect = true } else { // assume the column type is single select if there are repeated values // once it's set, dtxp needs to be reset if the final column type is not Single Select if (colData.length > uniqueVals.length && uniqueVals.length <= Math.ceil(colData.length / 2)) { colProps.uidt = UITypes.SingleSelect } // set dtxp here so that users can have the options even they switch the type from other types to SingleSelect // once it's set, dtxp needs to be reset if the final column type is not SingleSelect colProps.dtxp = `${uniqueVals.map((v) => `'${v.replace(/'/gi, "''")}'`).join(',')}` } return colProps } } export const extractSelectOptions = (colData: [], type: UITypes.SingleSelect | UITypes.MultiSelect): { dtxp: string } => { const colProps: any = {} if (type === UITypes.MultiSelect) { const flattenedVals = colData.flatMap((v: any) => v ? v .toString() .trim() .split(/\s*,\s*/) : [], ) const uniqueVals = [...new Set(flattenedVals.map((v: any) => v.toString().trim()))] colProps.uidt = UITypes.MultiSelect colProps.dtxp = `${uniqueVals.map((v) => `'${v.replace(/'/gi, "''")}'`).join(',')}` } else { const uniqueVals = [...new Set(colData.map((v: any) => v.toString().trim()))] colProps.uidt = UITypes.SingleSelect colProps.dtxp = `${uniqueVals.map((v) => `'${v.replace(/'/gi, "''")}'`).join(',')}` } return colProps } export const isDecimalType = (colData: []) => colData.some((v: any) => { return v && parseInt(v) !== +v }) export const isEmailType = (colData: [], col?: number) => colData.some((r: any) => { const v = getColVal(r, col) return v && validateEmail(v) }) export const isUrlType = (colData: [], col?: number) => colData.some((r: any) => { const v = getColVal(r, col) // convert to string since isURL only accepts string // and cell data value can be number or any other types return v && isURL(v.toString()) }) export const getColumnUIDTAndMetas = (colData: [], defaultType: string) => { const colProps = { uidt: defaultType } if (colProps.uidt === UITypes.SingleLineText) { // check for long text if (isMultiLineTextType(colData)) { colProps.uidt = UITypes.LongText } if (isEmailType(colData)) { colProps.uidt = UITypes.Email } if (isUrlType(colData)) { colProps.uidt = UITypes.URL } else { if (isCheckboxType(colData)) { colProps.uidt = UITypes.Checkbox } else { Object.assign(colProps, extractMultiOrSingleSelectProps(colData)) } } } else if (colProps.uidt === UITypes.Number) { if (isDecimalType(colData)) { colProps.uidt = UITypes.Decimal } } // TODO(import): currency // TODO(import): date / datetime return colProps } export const filterNullOrUndefinedObjectProperties = >(obj: T): T => { return Object.keys(obj).reduce((result, propName) => { const value = obj[propName] if (value !== null && value !== undefined) { if (!Array.isArray(value) && typeof value === 'object') { // Recursively filter nested objects result[propName] = filterNullOrUndefinedObjectProperties(value) } else { result[propName] = value } } return result }, {} as Record) as T } /** * Extracts the next default name based on the provided namesData, defaultName, and splitOperator. * * @param namesData - An array of strings containing existing names data. * @param defaultName - The default name to extract and generate the next name from. * @param splitOperator - The separator used to split the defaultName and numbers in existing namesData. * Defaults to '-'. Example: If defaultName is 'Token' and splitOperator is '-', * existing names like 'Token-1', 'Token-2', etc., will be considered. * @returns The next default name with an incremented number based on existing namesData. */ export const extractNextDefaultName = (namesData: string[], defaultName: string, splitOperator: string = '-'): string => { // Extract and sort numbers associated with the provided defaultName const extractedSortedNumbers = (namesData .map((name) => { const [_defaultName, number] = name.split(splitOperator) if (_defaultName === defaultName && !isNaN(Number(number?.trim()))) { return Number(number?.trim()) } return undefined }) .filter((e) => e) .sort((a, b) => { if (a !== undefined && b !== undefined) { return a - b } return 0 }) as number[]) || [] return extractedSortedNumbers.length ? `${defaultName}${splitOperator}${extractedSortedNumbers[extractedSortedNumbers.length - 1] + 1}` : `${defaultName}${splitOperator}1` } export const getFormattedViewTabTitle = ({ viewName, tableName, baseName, isDefaultView = false, charLimit = 20, isSharedView = false, }: { viewName: string tableName: string baseName: string isDefaultView?: boolean charLimit?: number isSharedView?: boolean }) => { if (isSharedView) { return viewName || 'NocoDB' } let title = `${viewName} | ${tableName} | ${baseName}` if (isDefaultView) { charLimit = 30 title = `${tableName} | ${baseName}` } if (title.length <= 60) { return title } // Function to truncate text and add ellipsis if needed const truncateText = (text: string) => { return text.length > charLimit ? `${text.substring(0, charLimit - 3)}...` : text } if (isDefaultView) { title = `${truncateText(tableName)} | ${truncateText(baseName)}` } else { title = `${truncateText(viewName)} | ${truncateText(tableName)} | ${truncateText(baseName)}` } return title }