refactor(sort): make client-side column sorting generic, not view-hardcoded

Previous commit hardcoded which lists (viewName === 'x:Account/User' /
'x:Account/Group') and columns (a fixed accessor map) got client-side
sort. Moved the "which columns" decision into the schema itself instead:

- New ClientSortableColumn type in schemaDeviationTypes.ts (intersection
  with the official Column type, same pattern as ClientOnlyFilterEnum —
  src/types/schema.ts stays untouched).
- withAccountListColumns tags Email Address, Full Name, quotaUsage, and
  aliasCount with clientSortable: true when it builds the Accounts/
  Groups column lists — the deviation-specific knowledge lives where the
  columns themselves are defined.
- DynamicList reads that flag generically (clientSortableColumns, a
  useMemo over resolved.list.columns) with no viewName check at all.
  getClientSortValue() replaces the old per-list accessor map: real
  columns compare their own property directly, only the two synthetic
  columns need a value override.

Net effect for Accounts/Groups is unchanged (verified: sort indicators
still only on Email/Full Name/Usage/Aliases, ascending/descending still
works). Any other list can now opt into the same client-sort mechanism
by tagging a column clientSortable, without touching DynamicList.tsx.
This commit is contained in:
Steven RYDELL
2026-08-01 20:32:04 +02:00
parent 518ec4059a
commit 93b10c3ed1
4 changed files with 63 additions and 28 deletions
+21 -6
View File
@@ -5,6 +5,7 @@
*/
import type { Schema } from '@/types/schema';
import type { ClientSortableColumn } from './schemaDeviationTypes';
/**
* SCHEMA-DEVIATION: account-quota-usage-column (see SCHEMA_DEVIATIONS.md)
@@ -25,17 +26,31 @@ import type { Schema } from '@/types/schema';
* Both lists also get a synthetic `aliasCount` column — DynamicList
* resolves it from the real `aliases` objectList property (an id-keyed
* map, per JMAP's objectList wire format) and renders its entry count.
*
* SCHEMA-DEVIATION: account-client-sort (see SCHEMA_DEVIATIONS.md)
*
* Email Address, Full Name (`description`), `quotaUsage`, and
* `aliasCount` are tagged `clientSortable` — DynamicList's generic
* client-sort mechanism picks this flag up from the column definition
* itself, the same way it already resolves `quotaUsage`/`aliasCount`,
* instead of hardcoding which lists/columns support it.
*/
function sortable(column: { name: string; label: string }): ClientSortableColumn {
return { ...column, clientSortable: true };
}
export function withAccountListColumns(schema: Schema): Schema {
let lists = schema.lists;
const userList = lists['x:Account/User'];
if (userList) {
const columns = [
...userList.columns.filter((c) => c.name !== 'createdAt'),
...userList.columns
.filter((c) => c.name !== 'createdAt')
.map((c) => (c.name === 'emailAddress' || c.name === 'description' ? sortable(c) : c)),
{ name: 'roles', label: 'Role' },
{ name: 'quotaUsage', label: 'Usage / Quota' },
{ name: 'aliasCount', label: 'Aliases' },
sortable({ name: 'quotaUsage', label: 'Usage / Quota' }),
sortable({ name: 'aliasCount', label: 'Aliases' }),
];
lists = { ...lists, 'x:Account/User': { ...userList, columns } };
}
@@ -43,9 +58,9 @@ export function withAccountListColumns(schema: Schema): Schema {
const groupList = lists['x:Account/Group'];
if (groupList) {
const columns = [
...groupList.columns,
{ name: 'quotaUsage', label: 'Usage / Quota' },
{ name: 'aliasCount', label: 'Aliases' },
...groupList.columns.map((c) => (c.name === 'emailAddress' || c.name === 'description' ? sortable(c) : c)),
sortable({ name: 'quotaUsage', label: 'Usage / Quota' }),
sortable({ name: 'aliasCount', label: 'Aliases' }),
];
lists = { ...lists, 'x:Account/Group': { ...groupList, columns } };
}
+8 -1
View File
@@ -4,7 +4,7 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
import type { FilterEnum } from '@/types/schema';
import type { Column, FilterEnum } from '@/types/schema';
/**
* Type augmentations for tracked schema deviations (see SCHEMA_DEVIATIONS.md).
@@ -23,3 +23,10 @@ export type ClientOnlyFilterEnum = FilterEnum & { clientOnly?: boolean };
export function isClientOnlyFilterEnum(f: FilterEnum): f is ClientOnlyFilterEnum {
return (f as ClientOnlyFilterEnum).clientOnly === true;
}
// SCHEMA-DEVIATION: account-client-sort (see SCHEMA_DEVIATIONS.md)
export type ClientSortableColumn = Column & { clientSortable?: boolean };
export function isClientSortableColumn(c: Column): boolean {
return (c as ClientSortableColumn).clientSortable === true;
}