Skip to content

Commit

Permalink
Fix: Table: customSort does not work when sortMultiple is enabled (bu…
Browse files Browse the repository at this point in the history
…efy#2681) (buefy#3945)

* test(lib): add tests around BTable sort

- Adds test cases that test sortable columns of BTable. Tests the
  following functionalities:
    - Sort single column
    - Sort multicolumns
    - Sort single column with custom sort

* feat(lib): support multi-column sort with custom sort

- `BTable` now uses `customSort` of each column if exists when it sorts
  multiple columns. Actual sorting is done by `multiColumnSort` function
  defined in `src/utils/helpers.js`. To support custom sort,
  `multiColumnSort` changes the format of the second parameter
  `sortingPriority` which used to accept an array of dot-separated field
  paths but now accepts an array of objects with the following fields:
    - `field`: dot-separated field path
    - `order`: sort direction:
        - 'asc' | undefined → ascending order
        - 'desc' → descending order
    - `customSort`: function to compare two field values for sorting.
      The same function given to the column definition. Natural ordering
      is used if omitted.

  As far as I checked, `multiColumnSort` is only used by `BTable`.

  Adds test cases that test multi-column sorting with custom sort.

- Includes migration to Vue 3, and Vue Test Utils v2:
    - Applies `toRaw` before testing if two columns, `column` and
      `currentSortColumn`, are indentical because they are reactive
      states. See Vue's documentation for more details:
        - https://vuejs.org/guide/essentials/reactivity-fundamentals.html#reactive-proxy-vs-original
        - https://vuejs.org/api/reactivity-advanced.html#toraw
    - Renames `propsData` → `props`
  • Loading branch information
kikuomax committed Jan 9, 2024
1 parent d78bcf6 commit a0c5ad3
Show file tree
Hide file tree
Showing 3 changed files with 320 additions and 19 deletions.
300 changes: 300 additions & 0 deletions packages/buefy-next/src/components/table/Table.spec.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import '@testing-library/jest-dom'
import { toRaw } from 'vue'
import { shallowMount } from '@vue/test-utils'
import BInput from '@components/input/Input'
import BTable from '@components/table/Table'
Expand Down Expand Up @@ -241,4 +242,303 @@ describe('BTable', () => {
jest.useRealTimers()
})
})

describe('Sortable', () => {
let wrapper
const data = [
{ id: 1, name: 'Jesse' },
{ id: 2, name: 'João' },
{ id: 3, name: 'Tina' },
{ id: 4, name: 'Anne' },
{ id: 5, name: 'Clarence' }
]
const columnsData = [
{
field: 'id',
label: 'ID',
numeric: true,
sortable: true
},
{
field: 'name',
label: 'Name',
sortable: true
}
]
let columns

beforeEach(() => {
wrapper = shallowMount(BTable, {
props: {
columns: columnsData,
data
}
})
// columnsData is transformed into newColumns with new objects
columns = wrapper.vm.newColumns
})

it('should be able to sort by ID', () => {
const sorted = [...data]
wrapper.vm.sort(columns[0])
expect(toRaw(wrapper.vm.currentSortColumn)).toBe(toRaw(columns[0]))
expect(wrapper.vm.isAsc).toBe(true)
expect(wrapper.vm.visibleData).toEqual(sorted)
// toggles
wrapper.vm.sort(columns[0])
expect(wrapper.vm.isAsc).toBe(false)
expect(wrapper.vm.visibleData).toEqual(sorted.reverse())
})

it('should be able to sort by Name', () => {
const sorted = [
data[3], data[4], data[0], data[1], data[2]
]
wrapper.vm.sort(columns[1])
expect(toRaw(wrapper.vm.currentSortColumn)).toBe(toRaw(columns[1]))
expect(wrapper.vm.isAsc).toBe(true)
expect(wrapper.vm.visibleData).toEqual(sorted)
// toggles
wrapper.vm.sort(columns[1])
expect(wrapper.vm.isAsc).toBe(false)
expect(wrapper.vm.visibleData).toEqual(sorted.reverse())
})
})

describe('Multi-sortable', () => {
let wrapper
const data = [
{ id: 1, name: 'Jesse', age: 23 },
{ id: 2, name: 'João', age: 22 },
{ id: 3, name: 'Tina', age: 22 },
{ id: 4, name: 'Anne', age: 23 },
{ id: 5, name: 'Clarence', age: 22 }
]
const columnsData = [
{
field: 'id',
label: 'ID'
},
{
field: 'name',
label: 'Name',
sortable: true
},
{
field: 'age',
label: 'Age',
numeric: true,
sortable: true
}
]
let columns

beforeEach(() => {
wrapper = shallowMount(BTable, {
props: {
columns: columnsData,
data,
sortMultiple: true
}
})
// columnsData is transformed into newColumns with new objects
columns = wrapper.vm.newColumns
})

it('should be able to sort by Age then Name', () => {
wrapper.vm.sort(columns[2])
wrapper.vm.sort(columns[1])
expect(wrapper.vm.sortMultipleDataLocal).toEqual([
{ field: 'age', order: undefined },
{ field: 'name', order: undefined }
])
expect(wrapper.vm.visibleData).toEqual([
data[4], data[1], data[2], data[3], data[0]
])
// toggles age
wrapper.vm.sort(columns[2])
expect(wrapper.vm.sortMultipleDataLocal).toEqual([
{ field: 'age', order: 'desc' },
{ field: 'name', order: undefined }
])
expect(wrapper.vm.visibleData).toEqual([
data[3], data[0], data[4], data[1], data[2]
])
// toggles name
wrapper.vm.sort(columns[1])
expect(wrapper.vm.sortMultipleDataLocal).toEqual([
{ field: 'age', order: 'desc' },
{ field: 'name', order: 'desc' }
])
expect(wrapper.vm.visibleData).toEqual([
data[0], data[3], data[2], data[1], data[4]
])
})
})

describe('Sortable with custom sort', () => {
let wrapper
const weekdays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
const data = weekdays.map((day, i) => ({
id: i + 1,
day
}))
const customSort = jest.fn((a, b, isAsc) => {
const ord = weekdays.indexOf(a.day) - weekdays.indexOf(b.day)
return isAsc ? ord : -ord
})
const columnsData = [
{
field: 'id',
label: 'ID',
numeric: true
},
{
field: 'day',
label: 'Day',
sortable: true,
customSort
}
]
let columns

beforeEach(() => {
wrapper = shallowMount(BTable, {
props: {
columns: columnsData,
data
}
})
// columnsData is transformed into newColumns with new objects
columns = wrapper.vm.newColumns
})

afterEach(() => {
customSort.mockClear()
})

it('should be able to sort by Day with custom sort', async () => {
const sorted = [...data]
wrapper.vm.sort(columns[1])
expect(toRaw(wrapper.vm.currentSortColumn)).toBe(toRaw(columns[1]))
expect(wrapper.vm.isAsc).toBe(true)
expect(wrapper.vm.visibleData).toEqual(sorted)
expect(customSort).toHaveBeenCalled()
// toggles
wrapper.vm.sort(columns[1])
expect(wrapper.vm.isAsc).toBe(false)
expect(wrapper.vm.visibleData).toEqual(sorted.reverse())
})
})

describe('Multi-sortable with custom sort', () => {
let wrapper
const weekdays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
const data = [
{ id: 1, day: 'Sun', fee: 15 },
{ id: 2, day: 'Mon', fee: 12 },
{ id: 3, day: 'Tue', fee: 12 },
{ id: 4, day: 'Wed', fee: 12 },
{ id: 5, day: 'Thu', fee: 12 },
{ id: 6, day: 'Fri', fee: 12 },
{ id: 7, day: 'Sat', fee: 15 }
]
const dayCustomSort = jest.fn((a, b, isAsc) => {
const ord = weekdays.indexOf(a.day) - weekdays.indexOf(b.day)
return isAsc ? ord : -ord
})
const feeCustomSort = jest.fn((a, b, isAsc) => {
const ord = a.fee - b.fee
return isAsc ? -ord : ord
})
const columnsData = [
{
field: 'id',
label: 'ID',
numeric: true
},
{
field: 'day',
label: 'Day',
sortable: true,
customSort: dayCustomSort
},
{
field: 'fee',
label: 'Fee',
sortable: true,
customSort: feeCustomSort
}
]
let columns

beforeEach(() => {
wrapper = shallowMount(BTable, {
props: {
columns: columnsData,
data,
sortMultiple: true
}
})
columns = wrapper.vm.newColumns
})

afterEach(() => {
dayCustomSort.mockClear()
feeCustomSort.mockClear()
})

it('should be able to sort by Fee then Day with custom sort', () => {
wrapper.vm.sort(columns[2])
wrapper.vm.sort(columns[1])
expect(wrapper.vm.sortMultipleDataLocal).toEqual([
{ field: 'fee', order: undefined, customSort: feeCustomSort },
{ field: 'day', order: undefined, customSort: dayCustomSort }
])
expect(wrapper.vm.visibleData).toEqual([
data[0], data[6], data[1], data[2], data[3], data[4], data[5]
])
expect(feeCustomSort).toHaveBeenCalled()
expect(dayCustomSort).toHaveBeenCalled()
// toggles fee
wrapper.vm.sort(columns[2])
expect(wrapper.vm.sortMultipleDataLocal).toEqual([
{ field: 'fee', order: 'desc', customSort: feeCustomSort },
{ field: 'day', order: undefined, customSort: dayCustomSort }
])
expect(wrapper.vm.visibleData).toEqual([
data[1], data[2], data[3], data[4], data[5], data[0], data[6]
])
// toggles day
wrapper.vm.sort(columns[1])
expect(wrapper.vm.sortMultipleDataLocal).toEqual([
{ field: 'fee', order: 'desc', customSort: feeCustomSort },
{ field: 'day', order: 'desc', customSort: dayCustomSort }
])
expect(wrapper.vm.visibleData).toEqual([
data[5], data[4], data[3], data[2], data[1], data[6], data[0]
])
})

it('should be able to remove column from sort (Fee+Day → Day)', () => {
wrapper.vm.sort(columns[2])
wrapper.vm.sort(columns[1])
wrapper.vm.sort(columns[1]) // day → descending order
expect(wrapper.vm.sortMultipleDataLocal).toEqual([
{ field: 'fee', order: undefined, customSort: feeCustomSort },
{ field: 'day', order: 'desc', customSort: dayCustomSort }
])
expect(wrapper.vm.visibleData).toEqual([
data[6], data[0], data[5], data[4], data[3], data[2], data[1]
])
// removes fee
wrapper.vm.removeSortingPriority(columns[2])
expect(wrapper.vm.sortMultipleDataLocal).toEqual([
{ field: 'day', order: 'desc', customSort: dayCustomSort }
])
expect(wrapper.vm.visibleData).toEqual([
data[6], data[5], data[4], data[3], data[2], data[1], data[0]
])
})
})
})
24 changes: 10 additions & 14 deletions packages/buefy-next/src/components/table/Table.vue
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,7 @@
</template>

<script>
import { toRaw } from 'vue'
import { getValueByPath, indexOf, multiColumnSort, escapeRegExpChars, toCssWidth, removeDiacriticsFromString, isFragment, isNil, translateTouchAsDragEvent, createAbsoluteElement, removeElement } from '../../utils/helpers'
import debounce from '../../utils/debounce'
import Checkbox from '../checkbox/Checkbox.vue'
Expand Down Expand Up @@ -975,14 +976,10 @@ export default {
this.sortMultipleDataLocal = this.sortMultipleDataLocal.filter(
(priority) => priority.field !== column.field)
const formattedSortingPriority = this.sortMultipleDataLocal.map((i) => {
return (i.order && i.order === 'desc' ? '-' : '') + i.field
})
if (formattedSortingPriority.length === 0) {
if (this.sortMultipleDataLocal.length === 0) {
this.resetMultiSorting()
} else {
this.newData = multiColumnSort(this.newData, formattedSortingPriority)
this.newData = multiColumnSort(this.newData, this.sortMultipleDataLocal)
}
}
},
Expand Down Expand Up @@ -1041,19 +1038,18 @@ export default {
if (existingPriority) {
existingPriority.order = existingPriority.order === 'desc' ? 'asc' : 'desc'
} else {
this.sortMultipleDataLocal.push(
{ field: column.field, order: column.isAsc }
)
this.sortMultipleDataLocal.push({
field: column.field,
order: column.isAsc,
customSort: column.customSort
})
}
this.doSortMultiColumn()
}
},
doSortMultiColumn() {
const formattedSortingPriority = this.sortMultipleDataLocal.map((i) => {
return (i.order && i.order === 'desc' ? '-' : '') + i.field
})
this.newData = multiColumnSort(this.newData, formattedSortingPriority)
this.newData = multiColumnSort(this.newData, this.sortMultipleDataLocal)
},
/**
Expand Down Expand Up @@ -1082,7 +1078,7 @@ export default {
}
if (!updatingData) {
this.isAsc = column === this.currentSortColumn
this.isAsc = toRaw(column) === toRaw(this.currentSortColumn)
? !this.isAsc
: (this.defaultSortDirection.toLowerCase() !== 'desc')
}
Expand Down
15 changes: 10 additions & 5 deletions packages/buefy-next/src/utils/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -189,14 +189,19 @@ export function removeDiacriticsFromString(value) {
}

export function multiColumnSort(inputArray, sortingPriority) {
// NOTE: this function is intended to be used by BTable
// clone it to prevent the any watchers from triggering every sorting iteration
const array = JSON.parse(JSON.stringify(inputArray))
const fieldSorter = (fields) => (a, b) => fields.map((o) => {
let dir = 1
if (o[0] === '-') { dir = -1; o = o.substring(1) }
const aValue = getValueByPath(a, o)
const bValue = getValueByPath(b, o)
return aValue > bValue ? dir : aValue < bValue ? -(dir) : 0
const { field, order, customSort } = o
if (typeof customSort === 'function') {
return customSort(a, b, order !== 'desc')
} else {
const aValue = getValueByPath(a, field)
const bValue = getValueByPath(b, field)
const ord = aValue > bValue ? 1 : aValue < bValue ? -1 : 0
return order === 'desc' ? -ord : ord
}
}).reduce((p, n) => p || n, 0)

return array.sort(fieldSorter(sortingPriority))
Expand Down

0 comments on commit a0c5ad3

Please sign in to comment.