|
|
|
import { type ColumnType, type SortType, UITypes } from 'nocodb-sdk'
|
|
|
|
import dayjs from 'dayjs'
|
|
|
|
|
|
|
|
export const getSortDirectionOptions = (uidt: UITypes | string, isGroupBy?: boolean) => {
|
|
|
|
const groupByOptions = isGroupBy
|
|
|
|
? [
|
|
|
|
{ text: 'Count (9 → 1)', value: 'count-desc' },
|
|
|
|
{ text: 'Count (1 → 9)', value: 'count-asc' },
|
|
|
|
]
|
|
|
|
: []
|
|
|
|
|
|
|
|
switch (uidt) {
|
|
|
|
case UITypes.Year:
|
|
|
|
case UITypes.Number:
|
|
|
|
case UITypes.Decimal:
|
|
|
|
case UITypes.Rating:
|
|
|
|
case UITypes.Count:
|
|
|
|
case UITypes.AutoNumber:
|
|
|
|
case UITypes.Time:
|
|
|
|
case UITypes.Currency:
|
|
|
|
case UITypes.Percent:
|
|
|
|
case UITypes.Duration:
|
|
|
|
case UITypes.PhoneNumber:
|
|
|
|
case UITypes.Date:
|
|
|
|
case UITypes.DateTime:
|
|
|
|
case UITypes.CreatedTime:
|
|
|
|
case UITypes.LastModifiedTime:
|
|
|
|
return [
|
|
|
|
{ text: '1 → 9', value: 'asc' },
|
|
|
|
{ text: '9 → 1', value: 'desc' },
|
|
|
|
].concat(groupByOptions)
|
|
|
|
case UITypes.Checkbox:
|
|
|
|
return [
|
|
|
|
{ text: '▢ → ✓', value: 'asc' },
|
|
|
|
{ text: '✓ → ▢', value: 'desc' },
|
|
|
|
].concat(groupByOptions)
|
|
|
|
default:
|
|
|
|
return [
|
|
|
|
{ text: 'A → Z', value: 'asc' },
|
|
|
|
{ text: 'Z → A', value: 'desc' },
|
|
|
|
].concat(groupByOptions)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export const sortByUIType = ({
|
|
|
|
uidt,
|
|
|
|
a,
|
|
|
|
b,
|
|
|
|
options: { caseSensitive = true, direction },
|
|
|
|
}: {
|
|
|
|
uidt: UITypes
|
|
|
|
a: any
|
|
|
|
b: any
|
|
|
|
options: {
|
|
|
|
caseSensitive?: boolean
|
|
|
|
direction?: 'asc' | 'desc' | 'count-asc' | 'count-desc'
|
|
|
|
}
|
|
|
|
}) => {
|
|
|
|
let nullsLast = direction !== 'asc'
|
|
|
|
|
|
|
|
if ([UITypes.Formula, UITypes.User].includes(uidt)) {
|
|
|
|
nullsLast = !nullsLast
|
|
|
|
}
|
|
|
|
|
|
|
|
if (a === null || a === undefined) {
|
|
|
|
return nullsLast ? 1 : -1
|
|
|
|
}
|
|
|
|
if (b === null || b === undefined) {
|
|
|
|
return nullsLast ? -1 : 1
|
|
|
|
}
|
|
|
|
|
|
|
|
if (a === '' && b !== '') return nullsLast ? 1 : -1
|
|
|
|
if (b === '' && a !== '') return nullsLast ? -1 : 1
|
|
|
|
|
|
|
|
let result = 0
|
|
|
|
|
|
|
|
switch (uidt) {
|
|
|
|
case UITypes.Number:
|
|
|
|
case UITypes.Decimal:
|
|
|
|
case UITypes.Currency:
|
|
|
|
case UITypes.Percent:
|
|
|
|
case UITypes.Rating:
|
|
|
|
case UITypes.Duration:
|
|
|
|
case UITypes.ID:
|
|
|
|
case UITypes.Rollup:
|
|
|
|
result = Number(a) - Number(b)
|
|
|
|
break
|
|
|
|
|
|
|
|
case UITypes.Links: {
|
|
|
|
const getLinksValue = (links: any) => {
|
|
|
|
if (links === null) return null
|
|
|
|
|
|
|
|
if (typeof links === 'number') return links
|
|
|
|
|
|
|
|
if (links && typeof links === 'object') {
|
|
|
|
return Object.values(links)[0]
|
|
|
|
}
|
|
|
|
return links
|
|
|
|
}
|
|
|
|
|
|
|
|
const valA = getLinksValue(a)
|
|
|
|
const valB = getLinksValue(b)
|
|
|
|
|
|
|
|
if (typeof valA === 'number' && typeof valB === 'number') {
|
|
|
|
result = valA - valB
|
|
|
|
} else {
|
|
|
|
result = String(valA).localeCompare(String(valB))
|
|
|
|
}
|
|
|
|
break
|
|
|
|
}
|
|
|
|
|
|
|
|
case UITypes.DateTime:
|
|
|
|
case UITypes.CreatedTime:
|
|
|
|
case UITypes.LastModifiedTime:
|
|
|
|
result = dayjs(a).valueOf() - dayjs(b).valueOf()
|
|
|
|
break
|
|
|
|
case UITypes.Time: {
|
|
|
|
const normalizeTimeValue = (value: any): dayjs.Dayjs => {
|
|
|
|
// If it's already a dayjs object
|
|
|
|
if (dayjs.isDayjs(value)) {
|
|
|
|
return dayjs(`1999-01-01 ${value.format('HH:mm:ss')}`)
|
|
|
|
}
|
|
|
|
|
|
|
|
// If it's a string in HH:mm:ss format (from server)
|
|
|
|
if (typeof value === 'string' && /^\d{2}:\d{2}:\d{2}$/.test(value)) {
|
|
|
|
return dayjs(`1999-01-01 ${value}`)
|
|
|
|
}
|
|
|
|
|
|
|
|
// If it's a string in HH:mm format (from local state)
|
|
|
|
if (typeof value === 'string' && /^\d{2}:\d{2}$/.test(value)) {
|
|
|
|
return dayjs(`1999-01-01 ${value}:00`)
|
|
|
|
}
|
|
|
|
|
|
|
|
// For any other format, try parsing with dayjs
|
|
|
|
let parsed = dayjs(value)
|
|
|
|
|
|
|
|
// If not valid, try parsing as time only
|
|
|
|
if (!parsed.isValid()) {
|
|
|
|
parsed = dayjs(value, 'HH:mm:ss')
|
|
|
|
}
|
|
|
|
|
|
|
|
// If still not valid, try with dummy date
|
|
|
|
if (!parsed.isValid()) {
|
|
|
|
parsed = dayjs(`1999-01-01 ${value}`)
|
|
|
|
}
|
|
|
|
|
|
|
|
return parsed
|
|
|
|
}
|
|
|
|
|
|
|
|
const timeA = normalizeTimeValue(a)
|
|
|
|
const timeB = normalizeTimeValue(b)
|
|
|
|
result = timeA.valueOf() - timeB.valueOf()
|
|
|
|
break
|
|
|
|
}
|
|
|
|
|
|
|
|
case UITypes.Year:
|
|
|
|
result = Number(a) - Number(b)
|
|
|
|
break
|
|
|
|
|
|
|
|
case UITypes.Checkbox:
|
|
|
|
result = a === b ? 0 : a ? -1 : 1
|
|
|
|
break
|
|
|
|
case UITypes.SingleSelect:
|
|
|
|
case UITypes.MultiSelect:
|
|
|
|
result = String(a).localeCompare(String(b))
|
|
|
|
break
|
|
|
|
case UITypes.Attachment: {
|
|
|
|
const getAttachmentValue = (att) => {
|
|
|
|
if (Array.isArray(att) && att.length > 0) {
|
|
|
|
return att[0].title || att[0].path || ''
|
|
|
|
}
|
|
|
|
return ''
|
|
|
|
}
|
|
|
|
result = getAttachmentValue(a).localeCompare(getAttachmentValue(b))
|
|
|
|
break
|
|
|
|
}
|
|
|
|
case UITypes.User:
|
|
|
|
case UITypes.CreatedBy:
|
|
|
|
case UITypes.LastModifiedBy: {
|
|
|
|
const getUserValue = (user) => {
|
|
|
|
if (Array.isArray(user) && user.length > 0) {
|
|
|
|
return user[0].display_name || user[0].email || ''
|
|
|
|
}
|
|
|
|
if (user && typeof user === 'object') {
|
|
|
|
return user.display_name || user.email || ''
|
|
|
|
}
|
|
|
|
return String(user)
|
|
|
|
}
|
|
|
|
result = getUserValue(a).localeCompare(getUserValue(b))
|
|
|
|
break
|
|
|
|
}
|
|
|
|
|
|
|
|
case UITypes.SingleLineText:
|
|
|
|
case UITypes.LongText:
|
|
|
|
case UITypes.Email:
|
|
|
|
case UITypes.URL:
|
|
|
|
case UITypes.PhoneNumber:
|
|
|
|
case UITypes.Formula:
|
|
|
|
if (caseSensitive) {
|
|
|
|
result = String(a).localeCompare(String(b))
|
|
|
|
} else {
|
|
|
|
result = String(a).toLowerCase().localeCompare(String(b).toLowerCase())
|
|
|
|
}
|
|
|
|
break
|
|
|
|
|
|
|
|
case UITypes.JSON:
|
|
|
|
result = JSON.stringify(a).localeCompare(JSON.stringify(b))
|
|
|
|
break
|
|
|
|
|
|
|
|
default:
|
|
|
|
result = String(a).localeCompare(String(b))
|
|
|
|
}
|
|
|
|
|
|
|
|
return direction === 'desc' ? -result : result
|
|
|
|
}
|
|
|
|
|
|
|
|
export const isSortRelevantChange = (
|
|
|
|
changedFields: string[],
|
|
|
|
sorts: SortType[],
|
|
|
|
columnsById: Record<string, ColumnType>,
|
|
|
|
): boolean => {
|
|
|
|
const sortColumnTitles = new Set(sorts.map((sort) => columnsById[sort.fk_column_id!]?.title).filter(Boolean))
|
|
|
|
|
|
|
|
return changedFields.some((field) => sortColumnTitles.has(field))
|
|
|
|
}
|