Files
Stalwart-webui/SCHEMA_DEVIATIONS.md
T
Steven RYDELL 93b10c3ed1 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.
2026-08-01 20:32:04 +02:00

8.7 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: table-level mechanism in src/components/lists/DynamicList.tsx (clientSortableColumns, getClientSortValue, the fetchData branch triggered by clientSortField) reading a clientSortable flag set per-column in src/lib/accountColumns.ts (type: ClientSortableColumn)
  • What: any column tagged clientSortable in the schema gets fetch-all-then-sort-in-memory on click (bypassing server pagination, same mechanism as mailbox-client-hierarchy-sort), instead of sending a JMAP sort to the server. The mechanism itself is generic and not tied to any specific list — currently only withAccountListColumns tags columns with it, for Email Address, Full Name, Usage, and Aliases on x:Account/User and x:Account/Group.
  • 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; withAccountListColumns stops tagging those columns clientSortable and they fall through to the normal server-paginated sortableFields path already used elsewhere. The generic mechanism itself only goes away once nothing tags any column clientSortable anymore.

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