Mailing Lists, Roles, and Domains got new/existing columns in prior commits but no way to sort them — the account-client-sort mechanism was only wired up in accountColumns.ts. Extracted its one-line clientSortable() column tagger into schemaDeviationTypes.ts (shared, same intersection-type pattern) and used it in all four column-patch files: - Mailing Lists: Email Address, Description, Aliases - Roles: Description, Enabled Permissions, Disabled Permissions - Domains: Domain Name, Enabled, Aliases Re-verified server sort support live, per list this time rather than just Accounts: every property tried returns unsupportedSort except one surprise — x:Domain/query actually accepts sort by "name", despite the schema not declaring it. Documented in SCHEMA_DEVIATIONS.md and deliberately routed through the same client-sort path as everything else instead of adding a one-off "trust an undeclared sort" mechanism for that single case. Verified in the running dev server: sort indicators appear on the intended columns for all three lists, clicking reorders rows correctly (alphabetical on Mailing Lists' Email Address, numeric on Roles' Enabled Permissions: 1 -> 3 -> 50 -> 229 -> 244 -> 452), Domains sorts without error. No regression on Accounts/Groups.
10 KiB
Schema deviations
Stalwart WebUI is schema-driven: the server's JSON schema is the single source of truth for what forms, fields, filters, and columns exist. This fork tries to stay aligned with that philosophy (see stalwartlabs/webui#discussion and the maintainer's note on why exceptions belong server-side, not in the UI).
Everything in this file is a deliberate exception: a place where the UI
does something the schema doesn't (yet) describe, because the equivalent
server-side capability doesn't exist in stalwartlabs/stalwart
or stalwartlabs/webui. Each entry
is tagged in code with // SCHEMA-DEVIATION: <id> so they're greppable
(grep -rn "SCHEMA-DEVIATION" src/).
The goal is not to remove these — they're real functionality this fork wants to keep — but to track them separately from schema-driven code, so it's always clear which is which, and so each one can be dropped the day the server (or upstream webui) grows the equivalent native capability.
src/types/schema.ts is never touched for a deviation
src/types/schema.ts mirrors the server's schema contract exactly and
must stay aligned with the official webui/server types — it is never
edited to accommodate a deviation, even to add an extra optional field.
If a deviation needs to carry extra data on an otherwise-official schema
shape (e.g. a flag consumed only by the deviation's own code), the
augmented type lives in the deviation's own module or in
src/lib/schemaDeviationTypes.ts, as
an intersection with the official type (OfficialType & { extra?: ... }),
and is imported only where the deviation is actually used. schema.ts
itself stays byte-for-byte alignable with upstream's version of the file.
Status legend
- 🟡 Workaround — client-only, would be removed if the server supported it natively.
- 🔵 Upstream tracked — an issue has been filed upstream; link included.
Deviations
log-client-filters 🟡
- Where:
src/lib/logFilters.ts, type augmentation insrc/lib/schemaDeviationTypes.ts - What: injects
levelandeventas filterable columns on thex:Loglist, applied entirely client-side (clientOnlyflag consumed byDynamicList). TheclientOnlyflag is declared asClientOnlyFilterEnum(an intersection type), not on the officialFilterEnuminschema.ts. - Why: Stalwart's JMAP
x:Log/queryreturnsunsupportedFilterfor both properties today, even though they're returned per row. - Ideal fix:
stalwartlabs/stalwartacceptslevel/eventas real query filters; the schema then advertises them normally andlogFilters.ts+ theClientOnlyFilterEnumaugmentation are deleted.
account-quota-usage-column 🟡
- Where:
src/lib/accountColumns.ts - What: adds a synthetic
quotaUsagecolumn to thex:Account/Userandx:Account/Grouplists — not a real schema property,DynamicListresolves it from theusedDiskQuota+quotas.maxDiskQuotapair and formats it specially. Also re-addsroleson the Users list only (a real property, just not in the list's default columns). - Why: neither the Accounts nor the Groups list schema exposes usage/role as list columns, only as detail-view fields, even though both object types have real
usedDiskQuota/quotasfields. - Ideal fix: the server's
x:Account/Userandx:Account/Grouplist schemas includeroles(Users) and a computed usage/quota column natively; this file is deleted.
mailbox-client-hierarchy-sort 🟡
- Where:
src/components/lists/DynamicList.tsx—sortMailboxesByHierarchyand theisMailboxListbranch - What: for the
Mailboxlist, fetches the entire result set (bypassing normal server pagination) and sorts it client-side so each parent mailbox is immediately followed by its children, with indentation depth tracked in React state. - Why: the server returns mailboxes in whatever order the query produces, not grouped by parent/child, and a mailbox's parent can land on a different page than the mailbox itself, so hierarchy can't be reconstructed one page at a time.
- Ideal fix: the server offers a native tree/hierarchical ordering (or a
sortthat groups by ancestry) forMailbox/query; the client-side full-fetch-and-sort is deleted in favor of normal paginated queries.
webapp-enabled-column-fallback 🟡
- Where:
src/components/lists/DynamicList.tsx—displayColumnsin theisWebApplicationsbranch - What: reordering the
x:Applicationlist's real schema columns (Description first, Enabled second) is not a deviation, but if the schema'slist.columnsdoesn't include anenabledcolumn at all, a fallback column definition with a hardcoded label is fabricated client-side so the toggle still renders. - Why: the
x:Applicationlist schema is not guaranteed to exposeenabledas a list column, even though it's a real object property (fetched separately viaproperties.push('enabled')). - Ideal fix: the server's
x:Applicationlist schema always includesenabledas a real column; the fallback branch is deleted (only the reordering logic remains, which is not a deviation).
account-alias-count-column 🟡
- Where:
src/lib/accountColumns.ts,src/lib/mailingListColumns.ts,src/lib/domainColumns.ts, resolved generically viaCOUNT_COLUMN_SOURCESinsrc/components/lists/DynamicList.tsx - What: adds a synthetic
aliasCountcolumn to thex:Account/User,x:Account/Group,x:MailingList, andx:Domainlists — not a real schema property;DynamicListresolves it from the realaliasesproperty (an objectList on Accounts/Groups/Mailing Lists, asetof domain names on Domains — same id-keyed wire format either way) and renders its entry count. - Why: none of these lists' schemas expose alias count as a column, only the full
aliaseslist on the detail view. - Ideal fix: the server's list schemas include a computed alias-count column natively; these column definitions are deleted.
account-client-sort 🟡
- Where: table-level mechanism in
src/components/lists/DynamicList.tsx(clientSortableColumns,getClientSortValue, thefetchDatabranch triggered byclientSortField) reading aclientSortableflag set per-column via the sharedclientSortable()helper insrc/lib/schemaDeviationTypes.ts(type:ClientSortableColumn), used byaccountColumns.ts,mailingListColumns.ts,roleColumns.ts, anddomainColumns.ts - What: any column tagged
clientSortablein the schema gets fetch-all-then-sort-in-memory on click (bypassing server pagination, same mechanism asmailbox-client-hierarchy-sort), instead of sending a JMAPsortto the server. The mechanism itself is generic and not tied to any specific list. Currently tagged: Email Address/Full Name/Usage/Aliases onx:Account/Userandx:Account/Group; Email Address/Description/Aliases onx:MailingList; Description/Enabled Permissions/Disabled Permissions onx:Role; Domain Name/Enabled/Aliases onx:Domain. - Why: none of these lists' schemas declare any sortable property at all (
list.sortis absent on all four) — confirmed against a live server by tryingsorton every displayed real column: all returnunsupportedSort, exceptx:Domain/querywithsort: [{"property":"name",...}], which the server actually accepts despite the schema not declaring it. Rather than add a second "trust an undeclared sort" pathway for that one case, Domain Name is routed through the same client-sort mechanism as everything else, for consistency; it's marginally less efficient (fetch-all instead of a paginated server sort) but domain lists are typically small. - Ideal fix: the server's query methods accept
sorton these properties and the schema declares them in each list'slist.sort; eachwith*Columnshelper stops tagging its columnsclientSortableand they fall through to the normal server-paginatedsortableFieldspath already used elsewhere. The generic mechanism itself only goes away once nothing tags any columnclientSortableanymore.
role-permission-count-columns 🟡
- Where:
src/lib/roleColumns.ts; resolved generically by the sameCOUNT_COLUMN_SOURCEStable insrc/components/lists/DynamicList.tsxused byaccount-alias-count-column - What: adds synthetic
enabledPermissionCount/disabledPermissionCountcolumns to thex:Rolelist — not real schema properties; resolved from the realenabledPermissions/disabledPermissionsset properties and rendered as entry counts. - Why: the Roles list schema only exposes Description as a column; seeing how broad or restrictive a role is requires opening it and counting permissions by hand.
- Ideal fix: the server's
x:Rolelist schema includes computed enabled/disabled permission count columns natively; this column definition is deleted.
Not a deviation (for reference)
A few other viewName === '...' / objectName === '...' checks exist in
DynamicList.tsx, MainContent.tsx, Sidebar.tsx, layout.ts, and
FieldWidget.tsx (e.g. x:OtpAuth, x:Expression, x:Rate, x:Action,
x:Trace, CustomComponent/Dashboard and other CustomComponent/*
pages, the x:Application column reordering itself, the active-WebApp
info card). These are not tracked here: they render real schema data
with a custom widget or extra display, the same pattern already used
upstream for special object types — they don't fabricate data or bypass
the server's filtering/pagination. Verified against upstream/main for
each: the object/view names above already drive special-cased rendering
there too, except x:Application/Mailbox/x:Log/x:Account/User
which are fork-only and covered by the entries above (or explicitly
noted as presentation-only, e.g. the Web Applications column reorder).