mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-10-16 01:33:18 +01:00
fix empty-value sorting edge cases and simplify sort components
This commit is contained in:
@@ -13,7 +13,7 @@ import {
|
|||||||
archiveProjectViaApi,
|
archiveProjectViaApi,
|
||||||
updateOrganizationSettingViaApi,
|
updateOrganizationSettingViaApi,
|
||||||
} from './utils/api';
|
} from './utils/api';
|
||||||
import { clearTableState, getTableRowNames } from './utils/table';
|
import { clearTableState, getSeededRowOrder } from './utils/table';
|
||||||
|
|
||||||
async function goToProjectsOverview(page: Page) {
|
async function goToProjectsOverview(page: Page) {
|
||||||
await page.goto(PLAYWRIGHT_BASE_URL + '/projects');
|
await page.goto(PLAYWRIGHT_BASE_URL + '/projects');
|
||||||
@@ -654,14 +654,12 @@ test('test that projects without a client or estimate are ordered by name at the
|
|||||||
page,
|
page,
|
||||||
ctx,
|
ctx,
|
||||||
}) => {
|
}) => {
|
||||||
// The seeding below has to cross two second boundaries, which eats into the default
|
// Seeded a second apart: created_at only has second precision and same-second rows
|
||||||
// per-test budget.
|
// fall back to a random UUID order. The spacing makes the API order of the clientless
|
||||||
test.slow();
|
// rows (created_at desc: ZZZ, AAA) deterministic and different from the alphabetical
|
||||||
|
// order the name tie-break should produce.
|
||||||
await createProjectViaApi(ctx, { name: 'AAA Tiebreak Project' });
|
await createProjectViaApi(ctx, { name: 'AAA Tiebreak Project' });
|
||||||
await page.waitForTimeout(1100);
|
await page.waitForTimeout(1100);
|
||||||
await createProjectViaApi(ctx, { name: 'BBB Tiebreak Project' });
|
|
||||||
await page.waitForTimeout(1100);
|
|
||||||
await createProjectViaApi(ctx, { name: 'ZZZ Tiebreak Project' });
|
await createProjectViaApi(ctx, { name: 'ZZZ Tiebreak Project' });
|
||||||
|
|
||||||
const clientAardvark = await createClientViaApi(ctx, { name: 'Aardvark Co' });
|
const clientAardvark = await createClientViaApi(ctx, { name: 'Aardvark Co' });
|
||||||
@@ -688,17 +686,11 @@ test('test that projects without a client or estimate are ordered by name at the
|
|||||||
|
|
||||||
const seeded = [
|
const seeded = [
|
||||||
'AAA Tiebreak Project',
|
'AAA Tiebreak Project',
|
||||||
'BBB Tiebreak Project',
|
|
||||||
'MMM Tiebreak Project',
|
'MMM Tiebreak Project',
|
||||||
'NNN Tiebreak Project',
|
'NNN Tiebreak Project',
|
||||||
'ZZZ Tiebreak Project',
|
'ZZZ Tiebreak Project',
|
||||||
];
|
];
|
||||||
const getOrder = async () => {
|
const getOrder = () => getSeededRowOrder(table, seeded);
|
||||||
const rowNames = await getTableRowNames(table);
|
|
||||||
return rowNames
|
|
||||||
.map((rowName) => seeded.find((name) => rowName.includes(name)))
|
|
||||||
.filter((name): name is string => Boolean(name));
|
|
||||||
};
|
|
||||||
|
|
||||||
// -- Client: empty rows last in both directions, alphabetical among themselves --
|
// -- Client: empty rows last in both directions, alphabetical among themselves --
|
||||||
const clientHeader = table.locator('.select-none', { hasText: 'Client' }).first();
|
const clientHeader = table.locator('.select-none', { hasText: 'Client' }).first();
|
||||||
@@ -709,7 +701,6 @@ test('test that projects without a client or estimate are ordered by name at the
|
|||||||
'MMM Tiebreak Project',
|
'MMM Tiebreak Project',
|
||||||
'NNN Tiebreak Project',
|
'NNN Tiebreak Project',
|
||||||
'AAA Tiebreak Project',
|
'AAA Tiebreak Project',
|
||||||
'BBB Tiebreak Project',
|
|
||||||
'ZZZ Tiebreak Project',
|
'ZZZ Tiebreak Project',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -720,7 +711,6 @@ test('test that projects without a client or estimate are ordered by name at the
|
|||||||
'NNN Tiebreak Project',
|
'NNN Tiebreak Project',
|
||||||
'MMM Tiebreak Project',
|
'MMM Tiebreak Project',
|
||||||
'AAA Tiebreak Project',
|
'AAA Tiebreak Project',
|
||||||
'BBB Tiebreak Project',
|
|
||||||
'ZZZ Tiebreak Project',
|
'ZZZ Tiebreak Project',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -733,7 +723,6 @@ test('test that projects without a client or estimate are ordered by name at the
|
|||||||
'NNN Tiebreak Project',
|
'NNN Tiebreak Project',
|
||||||
'MMM Tiebreak Project',
|
'MMM Tiebreak Project',
|
||||||
'AAA Tiebreak Project',
|
'AAA Tiebreak Project',
|
||||||
'BBB Tiebreak Project',
|
|
||||||
'ZZZ Tiebreak Project',
|
'ZZZ Tiebreak Project',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -744,7 +733,6 @@ test('test that projects without a client or estimate are ordered by name at the
|
|||||||
'MMM Tiebreak Project',
|
'MMM Tiebreak Project',
|
||||||
'NNN Tiebreak Project',
|
'NNN Tiebreak Project',
|
||||||
'AAA Tiebreak Project',
|
'AAA Tiebreak Project',
|
||||||
'BBB Tiebreak Project',
|
|
||||||
'ZZZ Tiebreak Project',
|
'ZZZ Tiebreak Project',
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -15,6 +15,16 @@ export async function getTableRowNames(table: Locator): Promise<string[]> {
|
|||||||
return names;
|
return names;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The visual order of the given seeded names within the table, ignoring any other rows.
|
||||||
|
*/
|
||||||
|
export async function getSeededRowOrder(table: Locator, seeded: string[]): Promise<string[]> {
|
||||||
|
const rowNames = await getTableRowNames(table);
|
||||||
|
return rowNames
|
||||||
|
.map((rowName) => seeded.find((name) => rowName.includes(name)))
|
||||||
|
.filter((name): name is string => Boolean(name));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Drop a table's persisted sort/filter state so a test starts from the defaults.
|
* Drop a table's persisted sort/filter state so a test starts from the defaults.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue';
|
|
||||||
import TableHeading from '@/Components/Common/TableHeading.vue';
|
import TableHeading from '@/Components/Common/TableHeading.vue';
|
||||||
import SortableTableHeaderCell from '@/Components/Common/SortableTableHeaderCell.vue';
|
import SortableTableHeaderCell from '@/Components/Common/SortableTableHeaderCell.vue';
|
||||||
import type { SortColumn, SortDirection } from '@/Components/Common/Client/ClientTable.vue';
|
import type { SortColumn, SortDirection } from '@/Components/Common/Client/ClientTable.vue';
|
||||||
@@ -10,20 +9,9 @@ const props = defineProps<{
|
|||||||
descFirstColumns: ReadonlySet<SortColumn>;
|
descFirstColumns: ReadonlySet<SortColumn>;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
defineEmits<{
|
||||||
sort: [column: SortColumn];
|
sort: [column: SortColumn];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
// Bound once per cell instead of repeating the three sort props on every column.
|
|
||||||
const sortState = computed(() => ({
|
|
||||||
sortColumn: props.sortColumn,
|
|
||||||
sortDirection: props.sortDirection,
|
|
||||||
descFirstColumns: props.descFirstColumns,
|
|
||||||
}));
|
|
||||||
|
|
||||||
function handleSort(column: SortColumn) {
|
|
||||||
emit('sort', column);
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -31,14 +19,17 @@ function handleSort(column: SortColumn) {
|
|||||||
<SortableTableHeaderCell
|
<SortableTableHeaderCell
|
||||||
class="pr-3 pl-4 sm:pl-6 lg:pl-8 3xl:pl-12"
|
class="pr-3 pl-4 sm:pl-6 lg:pl-8 3xl:pl-12"
|
||||||
column="name"
|
column="name"
|
||||||
v-bind="sortState"
|
v-bind="props"
|
||||||
@sort="handleSort">
|
@sort="$emit('sort', $event)">
|
||||||
Name
|
Name
|
||||||
</SortableTableHeaderCell>
|
</SortableTableHeaderCell>
|
||||||
<SortableTableHeaderCell column="projects_count" v-bind="sortState" @sort="handleSort">
|
<SortableTableHeaderCell
|
||||||
|
column="projects_count"
|
||||||
|
v-bind="props"
|
||||||
|
@sort="$emit('sort', $event)">
|
||||||
Projects
|
Projects
|
||||||
</SortableTableHeaderCell>
|
</SortableTableHeaderCell>
|
||||||
<SortableTableHeaderCell column="status" v-bind="sortState" @sort="handleSort">
|
<SortableTableHeaderCell column="status" v-bind="props" @sort="$emit('sort', $event)">
|
||||||
Status
|
Status
|
||||||
</SortableTableHeaderCell>
|
</SortableTableHeaderCell>
|
||||||
<div class="relative py-1.5 pl-3 pr-4 sm:pr-6 lg:pr-8 3xl:pr-12">
|
<div class="relative py-1.5 pl-3 pr-4 sm:pr-6 lg:pr-8 3xl:pr-12">
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue';
|
|
||||||
import TableHeading from '@/Components/Common/TableHeading.vue';
|
import TableHeading from '@/Components/Common/TableHeading.vue';
|
||||||
import SortableTableHeaderCell from '@/Components/Common/SortableTableHeaderCell.vue';
|
import SortableTableHeaderCell from '@/Components/Common/SortableTableHeaderCell.vue';
|
||||||
import type { SortColumn, SortDirection } from '@/Components/Common/Member/MemberTable.vue';
|
import type { SortColumn, SortDirection } from '@/Components/Common/Member/MemberTable.vue';
|
||||||
@@ -10,20 +9,9 @@ const props = defineProps<{
|
|||||||
descFirstColumns: ReadonlySet<SortColumn>;
|
descFirstColumns: ReadonlySet<SortColumn>;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
defineEmits<{
|
||||||
sort: [column: SortColumn];
|
sort: [column: SortColumn];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
// Bound once per cell instead of repeating the three sort props on every column.
|
|
||||||
const sortState = computed(() => ({
|
|
||||||
sortColumn: props.sortColumn,
|
|
||||||
sortDirection: props.sortDirection,
|
|
||||||
descFirstColumns: props.descFirstColumns,
|
|
||||||
}));
|
|
||||||
|
|
||||||
function handleSort(column: SortColumn) {
|
|
||||||
emit('sort', column);
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -31,20 +19,23 @@ function handleSort(column: SortColumn) {
|
|||||||
<SortableTableHeaderCell
|
<SortableTableHeaderCell
|
||||||
class="pr-3 pl-4 sm:pl-6 lg:pl-8 3xl:pl-12"
|
class="pr-3 pl-4 sm:pl-6 lg:pl-8 3xl:pl-12"
|
||||||
column="name"
|
column="name"
|
||||||
v-bind="sortState"
|
v-bind="props"
|
||||||
@sort="handleSort">
|
@sort="$emit('sort', $event)">
|
||||||
Name
|
Name
|
||||||
</SortableTableHeaderCell>
|
</SortableTableHeaderCell>
|
||||||
<SortableTableHeaderCell column="email" v-bind="sortState" @sort="handleSort">
|
<SortableTableHeaderCell column="email" v-bind="props" @sort="$emit('sort', $event)">
|
||||||
Email
|
Email
|
||||||
</SortableTableHeaderCell>
|
</SortableTableHeaderCell>
|
||||||
<SortableTableHeaderCell column="role" v-bind="sortState" @sort="handleSort">
|
<SortableTableHeaderCell column="role" v-bind="props" @sort="$emit('sort', $event)">
|
||||||
Role
|
Role
|
||||||
</SortableTableHeaderCell>
|
</SortableTableHeaderCell>
|
||||||
<SortableTableHeaderCell column="billable_rate" v-bind="sortState" @sort="handleSort">
|
<SortableTableHeaderCell
|
||||||
|
column="billable_rate"
|
||||||
|
v-bind="props"
|
||||||
|
@sort="$emit('sort', $event)">
|
||||||
Billable Rate
|
Billable Rate
|
||||||
</SortableTableHeaderCell>
|
</SortableTableHeaderCell>
|
||||||
<SortableTableHeaderCell column="status" v-bind="sortState" @sort="handleSort">
|
<SortableTableHeaderCell column="status" v-bind="props" @sort="$emit('sort', $event)">
|
||||||
Status
|
Status
|
||||||
</SortableTableHeaderCell>
|
</SortableTableHeaderCell>
|
||||||
<div class="relative py-1.5 pl-3 pr-4 sm:pr-6 lg:pr-8 3xl:pr-12 bg-row-heading-background">
|
<div class="relative py-1.5 pl-3 pr-4 sm:pr-6 lg:pr-8 3xl:pr-12 bg-row-heading-background">
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ const columns = computed<SortableColumnDef<Project, SortColumn>[]>(() => [
|
|||||||
{
|
{
|
||||||
id: 'billable_rate',
|
id: 'billable_rate',
|
||||||
sortDescFirst: true,
|
sortDescFirst: true,
|
||||||
accessorFn: (row: Project) => row.billable_rate ?? 0,
|
accessorFn: (row: Project) => row.billable_rate,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'status',
|
id: 'status',
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script setup lang="ts" generic="TColumn extends string">
|
<script setup lang="ts" generic="TColumn extends string">
|
||||||
import { computed, useAttrs } from 'vue';
|
import { computed, useAttrs } from 'vue';
|
||||||
import { twMerge } from 'tailwind-merge';
|
import type { ClassValue } from 'clsx';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
import { ChevronUpIcon, ChevronDownIcon } from '@heroicons/vue/16/solid';
|
import { ChevronUpIcon, ChevronDownIcon } from '@heroicons/vue/16/solid';
|
||||||
import type { SortDirection } from '@/utils/useSortableTable';
|
import type { SortDirection } from '@/utils/useSortableTable';
|
||||||
|
|
||||||
@@ -29,24 +30,15 @@ const isChevronDown = computed(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const cellClass = computed(() =>
|
const cellClass = computed(() =>
|
||||||
twMerge(
|
cn(
|
||||||
'px-3 py-1.5 text-left text-text-tertiary cursor-pointer hover:bg-secondary hover:text-text-primary transition-colors select-none flex items-center gap-1',
|
'px-3 py-1.5 text-left text-text-tertiary cursor-pointer hover:bg-secondary hover:text-text-primary transition-colors select-none flex items-center gap-1',
|
||||||
attrs.class as string | undefined
|
attrs.class as ClassValue
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
const passthroughAttrs = computed(() => {
|
|
||||||
const { class: _class, ...rest } = attrs;
|
|
||||||
return rest;
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<button
|
<button type="button" :class="cellClass" @click="emit('sort', column)">
|
||||||
v-bind="passthroughAttrs"
|
|
||||||
type="button"
|
|
||||||
:class="cellClass"
|
|
||||||
@click="emit('sort', column)">
|
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
<span class="sr-only">
|
<span class="sr-only">
|
||||||
{{
|
{{
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue';
|
|
||||||
import TableHeading from '@/Components/Common/TableHeading.vue';
|
import TableHeading from '@/Components/Common/TableHeading.vue';
|
||||||
import SortableTableHeaderCell from '@/Components/Common/SortableTableHeaderCell.vue';
|
import SortableTableHeaderCell from '@/Components/Common/SortableTableHeaderCell.vue';
|
||||||
import type { SortColumn, SortDirection } from '@/Components/Common/Tag/TagTable.vue';
|
import type { SortColumn, SortDirection } from '@/Components/Common/Tag/TagTable.vue';
|
||||||
@@ -10,20 +9,9 @@ const props = defineProps<{
|
|||||||
descFirstColumns: ReadonlySet<SortColumn>;
|
descFirstColumns: ReadonlySet<SortColumn>;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
defineEmits<{
|
||||||
sort: [column: SortColumn];
|
sort: [column: SortColumn];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
// Bound once per cell instead of repeating the three sort props on every column.
|
|
||||||
const sortState = computed(() => ({
|
|
||||||
sortColumn: props.sortColumn,
|
|
||||||
sortDirection: props.sortDirection,
|
|
||||||
descFirstColumns: props.descFirstColumns,
|
|
||||||
}));
|
|
||||||
|
|
||||||
function handleSort(column: SortColumn) {
|
|
||||||
emit('sort', column);
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -31,8 +19,8 @@ function handleSort(column: SortColumn) {
|
|||||||
<SortableTableHeaderCell
|
<SortableTableHeaderCell
|
||||||
class="pr-3 pl-4 sm:pl-6 lg:pl-8 3xl:pl-12"
|
class="pr-3 pl-4 sm:pl-6 lg:pl-8 3xl:pl-12"
|
||||||
column="name"
|
column="name"
|
||||||
v-bind="sortState"
|
v-bind="props"
|
||||||
@sort="handleSort">
|
@sort="$emit('sort', $event)">
|
||||||
Name
|
Name
|
||||||
</SortableTableHeaderCell>
|
</SortableTableHeaderCell>
|
||||||
<div class="relative py-1.5 pl-3 pr-4 sm:pr-6 lg:pr-8 3xl:pr-12">
|
<div class="relative py-1.5 pl-3 pr-4 sm:pr-6 lg:pr-8 3xl:pr-12">
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue';
|
|
||||||
import TableHeading from '@/Components/Common/TableHeading.vue';
|
import TableHeading from '@/Components/Common/TableHeading.vue';
|
||||||
import SortableTableHeaderCell from '@/Components/Common/SortableTableHeaderCell.vue';
|
import SortableTableHeaderCell from '@/Components/Common/SortableTableHeaderCell.vue';
|
||||||
import type { SortColumn, SortDirection } from '@/Components/Common/Task/TaskTable.vue';
|
import type { SortColumn, SortDirection } from '@/Components/Common/Task/TaskTable.vue';
|
||||||
@@ -10,20 +9,9 @@ const props = defineProps<{
|
|||||||
descFirstColumns: ReadonlySet<SortColumn>;
|
descFirstColumns: ReadonlySet<SortColumn>;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
defineEmits<{
|
||||||
sort: [column: SortColumn];
|
sort: [column: SortColumn];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
// Bound once per cell instead of repeating the three sort props on every column.
|
|
||||||
const sortState = computed(() => ({
|
|
||||||
sortColumn: props.sortColumn,
|
|
||||||
sortDirection: props.sortDirection,
|
|
||||||
descFirstColumns: props.descFirstColumns,
|
|
||||||
}));
|
|
||||||
|
|
||||||
function handleSort(column: SortColumn) {
|
|
||||||
emit('sort', column);
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -31,14 +19,14 @@ function handleSort(column: SortColumn) {
|
|||||||
<SortableTableHeaderCell
|
<SortableTableHeaderCell
|
||||||
class="pr-3 pl-4 sm:pl-6 lg:pl-8 3xl:pl-12"
|
class="pr-3 pl-4 sm:pl-6 lg:pl-8 3xl:pl-12"
|
||||||
column="name"
|
column="name"
|
||||||
v-bind="sortState"
|
v-bind="props"
|
||||||
@sort="handleSort">
|
@sort="$emit('sort', $event)">
|
||||||
Task Name
|
Task Name
|
||||||
</SortableTableHeaderCell>
|
</SortableTableHeaderCell>
|
||||||
<SortableTableHeaderCell column="spent_time" v-bind="sortState" @sort="handleSort">
|
<SortableTableHeaderCell column="spent_time" v-bind="props" @sort="$emit('sort', $event)">
|
||||||
Total Time
|
Total Time
|
||||||
</SortableTableHeaderCell>
|
</SortableTableHeaderCell>
|
||||||
<SortableTableHeaderCell column="progress" v-bind="sortState" @sort="handleSort">
|
<SortableTableHeaderCell column="progress" v-bind="props" @sort="$emit('sort', $event)">
|
||||||
Progress
|
Progress
|
||||||
</SortableTableHeaderCell>
|
</SortableTableHeaderCell>
|
||||||
<div class="px-3 py-1.5 text-left text-text-tertiary">Status</div>
|
<div class="px-3 py-1.5 text-left text-text-tertiary">Status</div>
|
||||||
|
|||||||
@@ -13,26 +13,23 @@ import { computed, type ComputedRef } from 'vue';
|
|||||||
export type SortDirection = 'asc' | 'desc';
|
export type SortDirection = 'asc' | 'desc';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The comparator every sortable column gets: a row whose accessor returns `undefined`
|
* Comparator for every sortable column: empty values (`null`/`undefined`) sort last in
|
||||||
* sorts to the bottom in both directions, and two such rows compare as equal so the
|
* both directions and compare as equal, so the tie-break orders them. TanStack's own
|
||||||
* tie-break decides their order.
|
* `sortUndefined` cannot do this: `'last'` bypasses the tie-break and the default flips
|
||||||
*
|
* empty rows to the top when descending.
|
||||||
* TanStack cannot express that combination. Its `sortUndefined: 'last'` keeps empty rows
|
|
||||||
* at the bottom but returns a non-zero result even when both rows are empty, which
|
|
||||||
* swallows the tie-break and leaves those rows in API (created_at) order; its default of
|
|
||||||
* `1` returns zero for that case but flips empty rows to the top when descending.
|
|
||||||
*/
|
*/
|
||||||
function sortEmptyLast<TData>(getDirection: () => SortDirection): SortingFn<TData> {
|
function sortEmptyLast<TData>(getSorting: () => SortingState): SortingFn<TData> {
|
||||||
return (rowA: Row<TData>, rowB: Row<TData>, columnId: string) => {
|
return (rowA: Row<TData>, rowB: Row<TData>, columnId: string) => {
|
||||||
const a = rowA.getValue(columnId);
|
const a = rowA.getValue(columnId);
|
||||||
const b = rowB.getValue(columnId);
|
const b = rowB.getValue(columnId);
|
||||||
|
|
||||||
if (a === undefined && b === undefined) {
|
if (a == null && b == null) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
if (a === undefined || b === undefined) {
|
if (a == null || b == null) {
|
||||||
const emptyLast = a === undefined ? 1 : -1;
|
const emptyLast = a == null ? 1 : -1;
|
||||||
return getDirection() === 'desc' ? -emptyLast : emptyLast;
|
const desc = getSorting().find((entry) => entry.id === columnId)?.desc ?? false;
|
||||||
|
return desc ? -emptyLast : emptyLast;
|
||||||
}
|
}
|
||||||
return typeof a === 'string' || typeof b === 'string'
|
return typeof a === 'string' || typeof b === 'string'
|
||||||
? sortingFns.alphanumeric(rowA, rowB, columnId)
|
? sortingFns.alphanumeric(rowA, rowB, columnId)
|
||||||
@@ -41,13 +38,13 @@ function sortEmptyLast<TData>(getDirection: () => SortDirection): SortingFn<TDat
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A column definition whose `id` has to be one of the table's sortable columns, so a
|
* Requires `id` to be one of the table's sortable columns, so a mistyped id is a compile
|
||||||
* renamed or mistyped id is a compile error rather than a column that silently stops
|
* error instead of a column that silently stops sorting. `sortingFn` is forbidden
|
||||||
* sorting: TanStack drops sort entries for ids it cannot resolve, leaving the rows in
|
* because the composable always installs its own comparator.
|
||||||
* their original order with no chevron and no error.
|
|
||||||
*/
|
*/
|
||||||
export type SortableColumnDef<TData, TColumn extends string> = ColumnDef<TData, unknown> & {
|
export type SortableColumnDef<TData, TColumn extends string> = ColumnDef<TData, unknown> & {
|
||||||
id: TColumn;
|
id: TColumn;
|
||||||
|
sortingFn?: never;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function useSortableTable<TData, TColumn extends string>(options: {
|
export function useSortableTable<TData, TColumn extends string>(options: {
|
||||||
@@ -72,15 +69,11 @@ export function useSortableTable<TData, TColumn extends string>(options: {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
const resolvedColumns = computed<ColumnDef<TData, unknown>[]>(() =>
|
const resolvedColumns = computed<ColumnDef<TData, unknown>[]>(() =>
|
||||||
options.columns().map((column) =>
|
options.columns().map((column) => ({
|
||||||
column.sortingFn
|
...column,
|
||||||
? column
|
sortUndefined: false as const,
|
||||||
: {
|
sortingFn: sortEmptyLast<TData>(() => sorting.value),
|
||||||
...column,
|
}))
|
||||||
sortUndefined: false as const,
|
|
||||||
sortingFn: sortEmptyLast<TData>(options.sortDirection),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const descFirstColumns = computed<ReadonlySet<TColumn>>(
|
const descFirstColumns = computed<ReadonlySet<TColumn>>(
|
||||||
|
|||||||
Reference in New Issue
Block a user