Skip to content

Commit

Permalink
fix(VDataTable): sort on transformed column values
Browse files Browse the repository at this point in the history
fixes #18840
  • Loading branch information
KaelWD committed Apr 15, 2024
1 parent 45c8f61 commit b6b9be5
Show file tree
Hide file tree
Showing 5 changed files with 105 additions and 49 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ export const VDataIterator = genericComponent<new <T> (
const { toggleSort } = provideSort({ sortBy, multiSort, mustSort, page })
const { sortByWithGroups, opened, extractRows, isGroupOpen, toggleGroup } = provideGroupBy({ groupBy, sortBy })

const { sortedItems } = useSortedItems(props, filteredItems, sortByWithGroups)
const { sortedItems } = useSortedItems(props, filteredItems, sortByWithGroups, { transform: item => item.raw })
const { flatItems } = useGroupedItems(sortedItems, groupBy, opened)

const itemsLength = computed(() => flatItems.value.length)
Expand Down
6 changes: 5 additions & 1 deletion packages/vuetify/src/components/VDataTable/VDataTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,11 @@ export const VDataTable = genericComponent<new <T extends readonly any[], V>(
const { toggleSort } = provideSort({ sortBy, multiSort, mustSort, page })
const { sortByWithGroups, opened, extractRows, isGroupOpen, toggleGroup } = provideGroupBy({ groupBy, sortBy })

const { sortedItems } = useSortedItems(props, filteredItems, sortByWithGroups, sortFunctions, sortRawFunctions)
const { sortedItems } = useSortedItems(props, filteredItems, sortByWithGroups, {
transform: item => item.columns,
sortFunctions,
sortRawFunctions,
})
const { flatItems } = useGroupedItems(sortedItems, groupBy, opened)
const itemsLength = computed(() => flatItems.value.length)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,11 @@ export const VDataTableVirtual = genericComponent<new <T extends readonly any[],
const { toggleSort } = provideSort({ sortBy, multiSort, mustSort })
const { sortByWithGroups, opened, extractRows, isGroupOpen, toggleGroup } = provideGroupBy({ groupBy, sortBy })

const { sortedItems } = useSortedItems(props, filteredItems, sortByWithGroups, sortFunctions, sortRawFunctions)
const { sortedItems } = useSortedItems(props, filteredItems, sortByWithGroups, {
transform: item => item.columns,
sortFunctions,
sortRawFunctions,
})
const { flatItems } = useGroupedItems(sortedItems, groupBy, opened)

const allItems = computed(() => extractRows(flatItems.value))
Expand Down
85 changes: 59 additions & 26 deletions packages/vuetify/src/components/VDataTable/__tests__/sort.spec.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,46 @@
// Composables
import { sortItems } from '../composables/sort'
import { transformItems } from '@/composables/list-items'
import { createHeaders } from '../composables/headers'
import { transformItems as _transformItems } from '../composables/items'
import { sortItems as _sortItems } from '../composables/sort'

// Utilities
import { describe, expect, it } from '@jest/globals'
import { mount } from '@vue/test-utils'

// Types
import type { SortItem } from '../composables/sort'
import type { DataTableCompareFunction, DataTableHeader, DataTableItem } from '@/components/VDataTable/types'

function transformItems (items: any[], headers?: DataTableHeader[]) {
let _items: DataTableItem[]
mount({
setup () {
const { columns } = createHeaders({ items, headers })
_items = _transformItems({} as any, items, columns.value)
return () => {}
},
})
return _items!
}

function sortItems (items: any[], sortBy: SortItem[], sortFunctions?: Record<string, DataTableCompareFunction>) {
return _sortItems(items, sortBy, 'en', {
sortFunctions,
transform: item => item.columns,
})
}

describe('VDataTable - sorting', () => {
it('should sort items by single column', () => {
const items = transformItems({} as any, [
const items = transformItems([
{ string: 'foo', number: 1 },
{ string: 'bar', number: 2 },
{ string: 'baz', number: 4 },
{ string: 'fizzbuzz', number: 3 },
])

expect(
sortItems(items, [{ key: 'string', order: 'asc' }], 'en')
sortItems(items, [{ key: 'string', order: 'asc' }])
.map(i => i.raw)
).toStrictEqual([
{ string: 'bar', number: 2 },
Expand All @@ -25,7 +50,7 @@ describe('VDataTable - sorting', () => {
])

expect(
sortItems(items, [{ key: 'string', order: 'desc' }], 'en')
sortItems(items, [{ key: 'string', order: 'desc' }])
.map(i => i.raw)
).toStrictEqual([
{ string: 'foo', number: 1 },
Expand All @@ -35,7 +60,7 @@ describe('VDataTable - sorting', () => {
])

expect(
sortItems(items, [{ key: 'number', order: 'asc' }], 'en')
sortItems(items, [{ key: 'number', order: 'asc' }])
.map(i => i.raw)
).toStrictEqual([
{ string: 'foo', number: 1 },
Expand All @@ -45,7 +70,7 @@ describe('VDataTable - sorting', () => {
])

expect(
sortItems(items, [{ key: 'number', order: 'desc' }], 'en')
sortItems(items, [{ key: 'number', order: 'desc' }])
.map(i => i.raw)
).toStrictEqual([
{ string: 'baz', number: 4 },
Expand All @@ -55,7 +80,7 @@ describe('VDataTable - sorting', () => {
])

expect(
sortItems(items, [{ key: 'number', order: 'asc' }], 'en', { number: (a, b) => b - a })
sortItems(items, [{ key: 'number', order: 'asc' }], { number: (a, b) => b - a })
.map(i => i.raw)
).toStrictEqual([
{ string: 'baz', number: 4 },
Expand All @@ -65,7 +90,7 @@ describe('VDataTable - sorting', () => {
])

expect(
sortItems(items, [{ key: 'number', order: 'desc' }], 'en', { number: (a, b) => b - a })
sortItems(items, [{ key: 'number', order: 'desc' }], { number: (a, b) => b - a })
.map(i => i.raw)
).toStrictEqual([
{ string: 'foo', number: 1 },
Expand All @@ -76,24 +101,32 @@ describe('VDataTable - sorting', () => {
})

it('should sort items with deep structure', () => {
const items = transformItems({} as any, [{ foo: { bar: { baz: 3 } } }, { foo: { bar: { baz: 1 } } }, { foo: { bar: { baz: 2 } } }])
const items = transformItems([
{ foo: { bar: { baz: 3 } } },
{ foo: { bar: { baz: 1 } } },
{ foo: { bar: { baz: 2 } } },
], [{ key: 'foo.bar.baz' }])

expect(
sortItems(items, [{ key: 'foo.bar.baz', order: 'asc' }], 'en')
sortItems(items, [{ key: 'foo.bar.baz', order: 'asc' }])
.map(i => i.raw)
).toStrictEqual([{ foo: { bar: { baz: 1 } } }, { foo: { bar: { baz: 2 } } }, { foo: { bar: { baz: 3 } } }])
).toStrictEqual([
{ foo: { bar: { baz: 1 } } },
{ foo: { bar: { baz: 2 } } },
{ foo: { bar: { baz: 3 } } },
])
})

it('should sort items by multiple columns', () => {
const items = transformItems({} as any, [
const items = transformItems([
{ string: 'foo', number: 1 },
{ string: 'bar', number: 3 },
{ string: 'baz', number: 2 },
{ string: 'baz', number: 1 },
])

expect(
sortItems(items, [{ key: 'string', order: 'asc' }, { key: 'number', order: 'asc' }], 'en')
sortItems(items, [{ key: 'string', order: 'asc' }, { key: 'number', order: 'asc' }])
.map(i => i.raw)
).toStrictEqual([
{ string: 'bar', number: 3 },
Expand All @@ -113,7 +146,7 @@ describe('VDataTable - sorting', () => {
// { string: 'foo', number: 1 },

expect(
sortItems(items, [{ key: 'string', order: 'desc' }, { key: 'number', order: 'asc' }], 'en')
sortItems(items, [{ key: 'string', order: 'desc' }, { key: 'number', order: 'asc' }])
.map(i => i.raw)
).toStrictEqual([
{ string: 'foo', number: 1 },
Expand All @@ -123,7 +156,7 @@ describe('VDataTable - sorting', () => {
])

expect(
sortItems(items, [{ key: 'string', order: 'asc' }, { key: 'number', order: 'desc' }], 'en')
sortItems(items, [{ key: 'string', order: 'asc' }, { key: 'number', order: 'desc' }])
.map(i => i.raw)
).toStrictEqual([
{ string: 'bar', number: 3 },
Expand All @@ -133,7 +166,7 @@ describe('VDataTable - sorting', () => {
])

expect(
sortItems(items, [{ key: 'string', order: 'desc' }, { key: 'number', order: 'desc' }], 'en')
sortItems(items, [{ key: 'string', order: 'desc' }, { key: 'number', order: 'desc' }])
.map(i => i.raw)
).toStrictEqual([
{ string: 'foo', number: 1 },
Expand All @@ -143,7 +176,7 @@ describe('VDataTable - sorting', () => {
])

expect(
sortItems(items, [{ key: 'number', order: 'asc' }, { key: 'string', order: 'asc' }], 'en')
sortItems(items, [{ key: 'number', order: 'asc' }, { key: 'string', order: 'asc' }])
.map(i => i.raw)
).toStrictEqual([
{ string: 'baz', number: 1 },
Expand All @@ -153,7 +186,7 @@ describe('VDataTable - sorting', () => {
])

expect(
sortItems(items, [{ key: 'number', order: 'desc' }, { key: 'string', order: 'asc' }], 'en')
sortItems(items, [{ key: 'number', order: 'desc' }, { key: 'string', order: 'asc' }])
.map(i => i.raw)
).toStrictEqual([
{ string: 'bar', number: 3 },
Expand All @@ -163,7 +196,7 @@ describe('VDataTable - sorting', () => {
])

expect(
sortItems(items, [{ key: 'number', order: 'asc' }, { key: 'string', order: 'desc' }], 'en')
sortItems(items, [{ key: 'number', order: 'asc' }, { key: 'string', order: 'desc' }])
.map(i => i.raw)
).toStrictEqual([
{ string: 'foo', number: 1 },
Expand All @@ -173,7 +206,7 @@ describe('VDataTable - sorting', () => {
])

expect(
sortItems(items, [{ key: 'number', order: 'desc' }, { key: 'string', order: 'desc' }], 'en')
sortItems(items, [{ key: 'number', order: 'desc' }, { key: 'string', order: 'desc' }])
.map(i => i.raw)
).toStrictEqual([
{ string: 'bar', number: 3 },
Expand All @@ -183,7 +216,7 @@ describe('VDataTable - sorting', () => {
])

expect(
sortItems(items, [{ key: 'string', order: 'asc' }, { key: 'number', order: 'asc' }], 'en', { number: (a, b) => b - a })
sortItems(items, [{ key: 'string', order: 'asc' }, { key: 'number', order: 'asc' }], { number: (a, b) => b - a })
.map(i => i.raw)
).toStrictEqual([
{ string: 'bar', number: 3 },
Expand All @@ -193,7 +226,7 @@ describe('VDataTable - sorting', () => {
])

expect(
sortItems(items, [{ key: 'number', order: 'asc' }, { key: 'string', order: 'asc' }], 'en', { number: (a, b) => b - a })
sortItems(items, [{ key: 'number', order: 'asc' }, { key: 'string', order: 'asc' }], { number: (a, b) => b - a })
.map(i => i.raw)
).toStrictEqual([
{ string: 'bar', number: 3 },
Expand All @@ -204,7 +237,7 @@ describe('VDataTable - sorting', () => {
})

it('should sort items with nullable column', () => {
const items = transformItems({} as any, [
const items = transformItems([
{ string: 'foo', number: 1 },
{ string: 'bar', number: null },
{ string: 'baz', number: 4 },
Expand All @@ -215,7 +248,7 @@ describe('VDataTable - sorting', () => {
])

expect(
sortItems(items, [{ key: 'number', order: 'asc' }], 'en')
sortItems(items, [{ key: 'number', order: 'asc' }])
.map(i => i.raw)
).toStrictEqual([
{ string: 'bar', number: null },
Expand All @@ -228,7 +261,7 @@ describe('VDataTable - sorting', () => {
])

expect(
sortItems(items, [{ key: 'number', order: 'desc' }], 'en')
sortItems(items, [{ key: 'number', order: 'desc' }])
.map(i => i.raw)
).toStrictEqual([
{ string: 'foobar', number: 5 },
Expand Down
55 changes: 35 additions & 20 deletions packages/vuetify/src/components/VDataTable/composables/sort.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ import { useProxiedModel } from '@/composables/proxiedModel'

// Utilities
import { computed, inject, provide, toRef } from 'vue'
import { getObjectValueByPath, isEmpty, propsFactory } from '@/util'
import { isEmpty, propsFactory } from '@/util'

// Types
import type { InjectionKey, PropType, Ref } from 'vue'
import type { DataTableCompareFunction, InternalDataTableHeader } from '../types'
import type { InternalItem } from '@/composables/filter'

export const makeDataTableSortProps = propsFactory({
sortBy: {
Expand Down Expand Up @@ -94,62 +95,76 @@ export function useSort () {
}

// TODO: abstract into project composable
export function useSortedItems <T extends Record<string, any>> (
export function useSortedItems <T extends InternalItem> (
props: { customKeySort: Record<string, DataTableCompareFunction> | undefined },
items: Ref<T[]>,
sortBy: Ref<readonly SortItem[]>,
sortFunctions?: Ref<Record<string, DataTableCompareFunction> | undefined>,
sortRawFunctions?: Ref<Record<string, DataTableCompareFunction> | undefined>,
options?: {
transform?: (item: T) => {}
sortFunctions?: Ref<Record<string, DataTableCompareFunction> | undefined>
sortRawFunctions?: Ref<Record<string, DataTableCompareFunction> | undefined>
},
) {
const locale = useLocale()
const sortedItems = computed(() => {
if (!sortBy.value.length) return items.value

return sortItems(items.value, sortBy.value, locale.current.value, {
...props.customKeySort,
...sortFunctions?.value,
}, sortRawFunctions?.value)
transform: options?.transform,
sortFunctions: {
...props.customKeySort,
...options?.sortFunctions?.value,
},
sortRawFunctions: options?.sortRawFunctions?.value,
})
})

return { sortedItems }
}

export function sortItems<T extends Record<string, any>> (
export function sortItems<T extends InternalItem> (
items: T[],
sortByItems: readonly SortItem[],
locale: string,
customSorters?: Record<string, DataTableCompareFunction>,
customRawSorters?: Record<string, DataTableCompareFunction>,
options?: {
transform?: (item: T) => Record<string, any>
sortFunctions?: Record<string, DataTableCompareFunction>
sortRawFunctions?: Record<string, DataTableCompareFunction>
},
): T[] {
const stringCollator = new Intl.Collator(locale, { sensitivity: 'accent', usage: 'sort' })

return [...items].sort((a, b) => {
const transformedItems = items.map(item => (
[item, options?.transform ? options.transform(item) : item as never] as const)
)

return transformedItems.sort((a, b) => {
for (let i = 0; i < sortByItems.length; i++) {
const sortKey = sortByItems[i].key
const sortOrder = sortByItems[i].order ?? 'asc'

if (sortOrder === false) continue

let sortA = getObjectValueByPath(a.raw, sortKey)
let sortB = getObjectValueByPath(b.raw, sortKey)
let sortARaw = a.raw
let sortBRaw = b.raw
let sortA = a[1][sortKey]
let sortB = b[1][sortKey]
let sortARaw = a[0].raw
let sortBRaw = b[0].raw

if (sortOrder === 'desc') {
[sortA, sortB] = [sortB, sortA]
;[sortARaw, sortBRaw] = [sortBRaw, sortARaw]
}

if (customRawSorters?.[sortKey]) {
const customResult = customRawSorters[sortKey](sortARaw, sortBRaw)
if (options?.sortRawFunctions?.[sortKey]) {
const customResult = options.sortRawFunctions[sortKey](sortARaw, sortBRaw)

if (!customResult) continue

return customResult
}

if (customSorters?.[sortKey]) {
const customResult = customSorters[sortKey](sortA, sortB)
if (options?.sortFunctions?.[sortKey]) {
const customResult = options.sortFunctions[sortKey](sortA, sortB)

if (!customResult) continue

Expand All @@ -173,5 +188,5 @@ export function sortItems<T extends Record<string, any>> (
}

return 0
})
}).map(([item]) => item)
}

0 comments on commit b6b9be5

Please sign in to comment.