mirror of https://github.com/nocodb/nocodb
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
225 lines
5.9 KiB
225 lines
5.9 KiB
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)) |
|
}
|
|
|