Files
Stalwart-webui/SCHEMA_DEVIATIONS.md
T
Steven RYDELL 518ec4059a feat(accounts): add client-side column sorting to Accounts and Groups
Verified against a live server: x:Account/query rejects sort on every
property tried, including real ones like emailAddress, with
unsupportedSort — the schema's empty list.sort is accurate, this is a
systemic server gap, not something specific to this fork's synthetic
columns.

Added client-side sorting (SCHEMA-DEVIATION: account-client-sort) for
Email Address, Full Name, Usage/Quota, and Aliases on both lists,
reusing the fetch-all-then-sort-locally mechanism already established
for mailbox-client-hierarchy-sort: clicking a sortable header switches
that one query to an unpaginated fetch, sorts the results in memory by
the appropriate accessor (numeric for Usage/Aliases, string compare
otherwise), then paginates client-side. Role isn't included (not a
sortable scalar). Normal server-paginated lists are unaffected — this
only activates when a client-sortable column is actually clicked.

Verified end-to-end: sort indicators appear only on the intended
columns, clicking Email Address/Aliases correctly reorders rows and
toggles ascending/descending, Groups behaves the same as Accounts.
2026-08-01 20:23:42 +02:00

8.3 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 in src/lib/schemaDeviationTypes.ts
  • What: injects level and event as filterable columns on the x:Log list, applied entirely client-side (clientOnly flag consumed by DynamicList). The clientOnly flag is declared as ClientOnlyFilterEnum (an intersection type), not on the official FilterEnum in schema.ts.
  • Why: Stalwart's JMAP x:Log/query returns unsupportedFilter for both properties today, even though they're returned per row.
  • Ideal fix: stalwartlabs/stalwart accepts level/event as real query filters; the schema then advertises them normally and logFilters.ts + the ClientOnlyFilterEnum augmentation are deleted.

account-quota-usage-column 🟡

  • Where: src/lib/accountColumns.ts
  • What: adds a synthetic quotaUsage column to the x:Account/User and x:Account/Group lists — not a real schema property, DynamicList resolves it from the usedDiskQuota + quotas.maxDiskQuota pair and formats it specially. Also re-adds roles on 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/quotas fields.
  • Ideal fix: the server's x:Account/User and x:Account/Group list schemas include roles (Users) and a computed usage/quota column natively; this file is deleted.

mailbox-client-hierarchy-sort 🟡

  • Where: src/components/lists/DynamicList.tsxsortMailboxesByHierarchy and the isMailboxList branch
  • What: for the Mailbox list, 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 sort that groups by ancestry) for Mailbox/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.tsxdisplayColumns in the isWebApplications branch
  • What: reordering the x:Application list's real schema columns (Description first, Enabled second) is not a deviation, but if the schema's list.columns doesn't include an enabled column at all, a fallback column definition with a hardcoded label is fabricated client-side so the toggle still renders.
  • Why: the x:Application list schema is not guaranteed to expose enabled as a list column, even though it's a real object property (fetched separately via properties.push('enabled')).
  • Ideal fix: the server's x:Application list schema always includes enabled as 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, rendering in src/components/lists/DynamicList.tsx
  • What: adds a synthetic aliasCount column to the x:Account/User and x:Account/Group lists — not a real schema property; DynamicList resolves it from the real aliases objectList property and renders its entry count.
  • Why: neither list's schema exposes alias count as a column, only the full aliases list on the detail view.
  • Ideal fix: the server's x:Account/User and x:Account/Group list schemas include a computed alias-count column natively; this column definition is deleted.

account-client-sort 🟡

  • Where: src/components/lists/DynamicList.tsxCLIENT_SORT_ACCESSORS, clientSortField, and the fetchData branch that also triggers on clientSortField
  • What: on the x:Account/User and x:Account/Group lists, clicking the Email/Full Name/Usage/Aliases column headers sorts by fetching every matching row (bypassing server pagination, same mechanism as mailbox-client-hierarchy-sort) and sorting it client-side, instead of sending a JMAP sort to the server.
  • Why: neither list's schema declares any sortable property at all (list.sort is absent) — confirmed against a live server: x:Account/query with sort: [{"property":"emailAddress",...}] returns unsupportedSort for every property tried, including the real ones. This is a systemic gap in the current Stalwart server, not specific to this fork's synthetic columns.
  • Ideal fix: the server's x:Account/User/x:Account/Group query methods accept sort on at least emailAddress, description, usedDiskQuota, and the schema declares them in list.sort; the client-sort branch and CLIENT_SORT_ACCESSORS are deleted in favor of the normal server-paginated sortableFields path already used elsewhere.

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).