import type { FunctionalComponent, SVGAttributes } from 'vue' import type { ButtonType, ColumnType, FormulaType, IntegrationType, LinkToAnotherRecordType } from 'nocodb-sdk' import { ButtonActionsType, FormulaDataTypes, RelationTypes, UITypes, LongTextAiMetaProp as _LongTextAiMetaProp, checkboxIconList, isAIPromptCol, isLinksOrLTAR, isSystemColumn, isValidURL, isVirtualCol, ratingIconList, substituteColumnIdWithAliasInPrompt, validateEmail, } from 'nocodb-sdk' import isMobilePhone from 'validator/lib/isMobilePhone' export interface UiTypesType { name: UITypes | string icon: FunctionalComponent | VNode virtual?: number | boolean deprecated?: number | boolean isNew?: number | boolean } export const AIButton = 'AIButton' export const AIPrompt = 'AIPrompt' export const LongTextAiMetaProp = _LongTextAiMetaProp const uiTypes: UiTypesType[] = [ { name: AIButton, icon: iconMap.cellAiButton, virtual: 1, isNew: 1, deprecated: 0, }, { name: AIPrompt, icon: iconMap.cellAi, isNew: 1, deprecated: 0, }, { name: UITypes.Links, icon: iconMap.cellLinks, virtual: 1, deprecated: 1, }, { name: UITypes.LinkToAnotherRecord, icon: iconMap.cellLinks, virtual: 1, }, { name: UITypes.Lookup, icon: iconMap.cellLookup, virtual: 1, }, { name: UITypes.SingleLineText, icon: iconMap.cellText, }, { name: UITypes.LongText, icon: iconMap.cellLongText, }, { name: UITypes.Number, icon: iconMap.cellNumber, }, { name: UITypes.AutoNumber, icon: iconMap.cellAutoNumber, }, { name: UITypes.Decimal, icon: iconMap.cellDecimal, }, { name: UITypes.Attachment, icon: iconMap.cellAttachment, }, { name: UITypes.Checkbox, icon: iconMap.cellCheckbox, }, { name: UITypes.MultiSelect, icon: iconMap.cellMultiSelect, }, { name: UITypes.SingleSelect, icon: iconMap.cellSingleSelect, }, { name: UITypes.Date, icon: iconMap.cellDate, }, { name: UITypes.Year, icon: iconMap.cellYear, }, { name: UITypes.Time, icon: iconMap.cellTime, }, { name: UITypes.PhoneNumber, icon: iconMap.cellPhone, }, { name: UITypes.Email, icon: iconMap.cellEmail, }, { name: UITypes.URL, icon: iconMap.cellUrl, }, { name: UITypes.Currency, icon: iconMap.cellCurrency, }, { name: UITypes.Percent, icon: iconMap.cellPercent, }, { name: UITypes.Duration, icon: iconMap.cellDuration, }, { name: UITypes.Rating, icon: iconMap.cellRating, }, { name: UITypes.Colour, icon: iconMap.palette, }, { name: UITypes.Formula, icon: iconMap.cellFormula, virtual: 1, }, { name: UITypes.Rollup, icon: iconMap.cellRollup, virtual: 1, }, { name: UITypes.DateTime, icon: iconMap.cellDatetime, }, { name: UITypes.QrCode, icon: iconMap.cellQrCode, virtual: 1, }, { name: UITypes.Barcode, icon: iconMap.cellBarcode, virtual: 1, }, { name: UITypes.Geometry, icon: iconMap.cellGeometry, }, { name: UITypes.GeoData, icon: iconMap.geoData, }, { name: UITypes.JSON, icon: iconMap.cellJson, }, { name: UITypes.SpecificDBType, icon: iconMap.cellDb, }, { name: UITypes.UUID, icon: iconMap.cellUuid, }, { name: UITypes.User, icon: iconMap.cellUser, }, { name: UITypes.Button, icon: iconMap.cellButton, virtual: 1, }, { name: UITypes.CreatedTime, icon: iconMap.cellSystemDate, }, { name: UITypes.LastModifiedTime, icon: iconMap.cellSystemDate, }, { name: UITypes.CreatedBy, icon: iconMap.cellSystemUser, }, { name: UITypes.LastModifiedBy, icon: iconMap.cellSystemUser, }, ] const getUIDTIcon = (uidt: UITypes | string) => { return ( [ ...uiTypes, { name: UITypes.CreatedTime, icon: iconMap.cellSystemDate, }, { name: UITypes.ID, icon: iconMap.cellSystemKey, }, { name: UITypes.ForeignKey, icon: iconMap.cellLinks, }, ].find((t) => t.name === uidt) || {} ).icon } // treat column as required if `non_null` is true and one of the following is true // 1. column not having default value // 2. column is not auto increment // 3. column is not auto generated const isColumnRequired = (col?: ColumnType) => col && col.rqd && !isValidValue(col?.cdf) && !col.ai && !col.meta?.ag const isVirtualColRequired = (col: ColumnType, columns: ColumnType[]) => col.uidt === UITypes.LinkToAnotherRecord && col.colOptions && (col.colOptions).type === RelationTypes.BELONGS_TO && isColumnRequired(columns.find((c) => c.id === (col.colOptions).fk_child_column_id)) const isColumnRequiredAndNull = (col: ColumnType, row: Record) => { return isColumnRequired(col) && (row[col.title!] === undefined || row[col.title!] === null) } const getUniqueColumnName = (initName: string, columns: ColumnType[]) => { let name = initName let i = 1 while (columns.find((c) => c.title === name)) { name = `${initName}_${i}` i++ } return name } const isTypableInputColumn = (colOrUidt: ColumnType | UITypes) => { let uidt: UITypes if (typeof colOrUidt === 'object') { uidt = colOrUidt.uidt as UITypes } else { uidt = colOrUidt } return [ UITypes.LongText, UITypes.SingleLineText, UITypes.Number, UITypes.PhoneNumber, UITypes.Email, UITypes.Decimal, UITypes.Currency, UITypes.Percent, UITypes.Duration, UITypes.JSON, UITypes.URL, UITypes.SpecificDBType, UITypes.Geometry, ].includes(uidt) } const isColumnSupportsGroupBySettings = (colOrUidt: ColumnType) => { let uidt: UITypes if (typeof colOrUidt === 'object') { uidt = colOrUidt.uidt as UITypes } else { uidt = colOrUidt } return [UITypes.SingleSelect, UITypes.User, UITypes.CreatedBy, UITypes.Checkbox, UITypes.Rating].includes(uidt) } const isColumnInvalid = ({ col, aiIntegrations = [], isReadOnly = false, isNocoAiAvailable = false, columns = [], }: { col: ColumnType aiIntegrations?: Partial[] isReadOnly?: boolean isNocoAiAvailable?: boolean columns?: ColumnType[] }): { isInvalid: boolean; tooltip: string; ignoreTooltip?: boolean } => { const result = { isInvalid: false, tooltip: 'msg.invalidColumnConfiguration', ignoreTooltip: false, } switch (col.uidt) { case UITypes.Formula: result.isInvalid = !!(col.colOptions as FormulaType).error break case UITypes.Button: { const colOptions = col.colOptions as ButtonType if (isAiButton(col) && isReadOnly) { result.isInvalid = true result.ignoreTooltip = true } else if (colOptions.type === ButtonActionsType.Script && isReadOnly) { result.isInvalid = true result.ignoreTooltip = true } else if (colOptions.type === ButtonActionsType.Webhook) { if (isReadOnly) { result.isInvalid = true result.ignoreTooltip = true } else { result.isInvalid = !colOptions.fk_webhook_id } } else if (colOptions.type === ButtonActionsType.Url) { result.isInvalid = !!colOptions.error } else if (colOptions.type === ButtonActionsType.Ai) { const colOptions = col.colOptions as ButtonType const missingIds = substituteColumnIdWithAliasInPrompt( (colOptions as Record)?.formula ?? '', columns, (colOptions as Record)?.formula_raw, ).missingIds const isIntegrationMissing = isNocoAiAvailable ? false : !colOptions.fk_integration_id || (isReadOnly ? false : !!colOptions.fk_integration_id && !ncIsArrayIncludes(aiIntegrations, colOptions.fk_integration_id, 'id')) if (isIntegrationMissing) { result.isInvalid = true result.tooltip = 'title.aiIntegrationMissing' } else if (missingIds.length) { result.isInvalid = true result.tooltip = `Input prompt has deleted column(s): ${missingIds.map((id) => id.title).join(', ')}` } } else if (!colOptions.type) { result.isInvalid = true result.tooltip = 'msg.buttonTypeIsMissing' } break } case UITypes.LongText: { if (isAIPromptCol(col)) { const colOptions = col.colOptions as ButtonType const missingIds = substituteColumnIdWithAliasInPrompt( (colOptions as Record)?.prompt ?? '', columns, (colOptions as Record)?.prompt_raw, ).missingIds const isIntegrationMissing = isNocoAiAvailable ? false : !colOptions.fk_integration_id || (isReadOnly ? false : !!colOptions.fk_integration_id && !ncIsArrayIncludes(aiIntegrations, colOptions.fk_integration_id, 'id')) if (isIntegrationMissing) { result.isInvalid = true result.tooltip = 'title.aiIntegrationMissing' } else if (missingIds.length) { result.isInvalid = true result.tooltip = `Prompt has deleted column(s): ${missingIds.map((id) => id.title).join(', ')}` } } break } } return result } // cater existing v1 cases function extractCheckboxIcon(meta: string | Record = null) { const parsedMeta = parseProp(meta) const icon = { checked: 'mdi-check-circle-outline', unchecked: 'mdi-checkbox-blank-circle-outline', } if (parsedMeta.icon) { icon.checked = parsedMeta.icon.checked || icon.checked icon.unchecked = parsedMeta.icon.unchecked || icon.unchecked } else if (typeof parsedMeta.iconIdx === 'number' && checkboxIconList[parsedMeta.iconIdx]) { icon.checked = checkboxIconList[parsedMeta.iconIdx].checked icon.unchecked = checkboxIconList[parsedMeta.iconIdx].unchecked } return icon } function extractRatingIcon(meta: string | Record = null) { const parsedMeta = parseProp(meta) const icon = { full: 'mdi-star', empty: 'mdi-star-outline', } if (parsedMeta.icon) { icon.full = parsedMeta.icon.full || icon.full icon.empty = parsedMeta.icon.empty || icon.empty } else if (typeof parsedMeta.iconIdx === 'number' && ratingIconList[parsedMeta.iconIdx]) { icon.full = ratingIconList[parsedMeta.iconIdx].full icon.empty = ratingIconList[parsedMeta.iconIdx].empty } return icon } const formViewHiddenColTypes = [ UITypes.Rollup, UITypes.Lookup, UITypes.Formula, UITypes.QrCode, UITypes.Barcode, UITypes.Button, UITypes.SpecificDBType, UITypes.CreatedTime, UITypes.LastModifiedTime, UITypes.CreatedBy, UITypes.LastModifiedBy, UITypes.Meta, UITypes.UUID, AIButton, AIPrompt, ] const isFormViewHiddenCol = (col: ColumnType | UITypes): boolean => { if (typeof col === 'object') { return formViewHiddenColTypes.includes(col.uidt as UITypes) || isAIPromptCol(col) } return formViewHiddenColTypes.includes(col as UITypes) } const columnToValidate = [UITypes.Email, UITypes.URL, UITypes.PhoneNumber] const getColumnValidationError = (column: ColumnType, value?: any) => { if (!columnToValidate.includes(column.uidt as UITypes) || !parseProp(column.meta)?.validate) return '' let cdfValue: any = column.cdf if (!ncIsUndefined(value)) { cdfValue = value } switch (column.uidt) { case UITypes.URL: { if (!cdfValue?.trim() || isValidURL(cdfValue?.trim())) return '' return 'msg.error.invalidURL' } case UITypes.Email: { if (!cdfValue || validateEmail(cdfValue)) return '' return 'msg.error.invalidEmail' } case UITypes.PhoneNumber: { if (!cdfValue || isMobilePhone(cdfValue)) return '' return 'msg.invalidPhoneNumber' } default: { return '' } } } const getFormulaColDataType = (col: ColumnType) => { return (col?.colOptions as any)?.parsed_tree?.dataType ?? FormulaDataTypes.STRING } const isSearchableColumn = (column: ColumnType) => { return ( !isSystemColumn(column) && ![ UITypes.Links, UITypes.Rollup, UITypes.DateTime, UITypes.Date, UITypes.Button, UITypes.LastModifiedTime, UITypes.CreatedTime, UITypes.Barcode, UITypes.QrCode, UITypes.Order, ].includes(column?.uidt as UITypes) ) } const showReadonlyColumnTooltip = (col: ColumnType) => { const shouldApplyDataCell = !(isBarcode(col) || isQrCode(col) || isBoolean(col) || isRating(col)) return isReadOnlyVirtualCell(col) && shouldApplyDataCell && !isLinksOrLTAR(col) } const showEditRestrictedColumnTooltip = (col: ColumnType) => { return ( !isReadOnlyVirtualCell(col) && ![UITypes.Button, UITypes.Count, UITypes.Order, UITypes.ForeignKey].includes(col.uidt as UITypes) && !isAutoNumber(col) ) } const disableMakeCellEditable = (col: ColumnType) => { return showEditRestrictedColumnTooltip(col) && !isLinksOrLTAR(col) } const canUseForRollupLinkField = (c: ColumnType) => { return ( c && isLinksOrLTAR(c) && (c.colOptions as LinkToAnotherRecordType)?.type && ![RelationTypes.BELONGS_TO, RelationTypes.ONE_TO_ONE].includes( (c.colOptions as LinkToAnotherRecordType)?.type as RelationTypes, ) && // exclude system columns (!c.system || // include system columns if it's self-referencing, mm, oo and bt are self-referencing // hm is only used for LTAR with junction table [RelationTypes.MANY_TO_MANY, RelationTypes.ONE_TO_ONE, RelationTypes.BELONGS_TO].includes( (c.colOptions as LinkToAnotherRecordType)?.type as RelationTypes, )) ) } const canUseForLookupLinkField = (c: ColumnType, metaSourceId?: string) => { return ( c && isLinksOrLTAR(c) && // exclude system columns (!c.system || // include system columns if it's self-referencing, mm, oo and bt are self-referencing // hm is only used for LTAR with junction table [RelationTypes.MANY_TO_MANY, RelationTypes.ONE_TO_ONE, RelationTypes.BELONGS_TO].includes( (c.colOptions as LinkToAnotherRecordType)?.type as RelationTypes, )) && c.source_id === metaSourceId ) } const getValidRollupColumn = (c: ColumnType) => { return ( (!isVirtualCol(c.uidt as UITypes) || [ UITypes.CreatedTime, UITypes.CreatedBy, UITypes.LastModifiedTime, UITypes.LastModifiedBy, UITypes.Formula, UITypes.Rollup, ].includes(c.uidt as UITypes)) && (!isSystemColumn(c) || c.pk) ) } export { uiTypes, isTypableInputColumn, isColumnSupportsGroupBySettings, getUIDTIcon, isColumnInvalid, getUniqueColumnName, isColumnRequiredAndNull, isColumnRequired, isVirtualColRequired, checkboxIconList, ratingIconList, extractCheckboxIcon, extractRatingIcon, formViewHiddenColTypes, isFormViewHiddenCol, columnToValidate, getColumnValidationError, getFormulaColDataType, isSearchableColumn, showReadonlyColumnTooltip, showEditRestrictedColumnTooltip, disableMakeCellEditable, canUseForRollupLinkField, canUseForLookupLinkField, getValidRollupColumn, }