feat(accounts): add Usage/Quota column to Groups, show unlimited as ∞

Groups have real usedDiskQuota/quotas fields, same as Users, but the
Groups list schema doesn't expose usage as a column any more than the
Accounts list did — extend the existing account-quota-usage-column
deviation to x:Account/Group too (SCHEMA_DEVIATIONS.md updated).

The isAccountsList gate that resolved/rendered quotaUsage was hardcoded
to viewName === 'x:Account/User', so it silently no-oped for Groups.
Replaced with hasQuotaUsageColumn, derived from whether the resolved
list's own columns include the synthetic quotaUsage column — works for
any list withAccountListColumns patches, not just a hardcoded pair of
view names.

Also: "Unlimited" (no quota configured) now renders as "∞" instead of
the word, matching the size formatting style used elsewhere in the
column.
This commit is contained in:
Steven RYDELL
2026-08-01 20:02:10 +02:00
parent 09910a7bf9
commit 0aa92c394c
4 changed files with 33 additions and 22 deletions
+3 -3
View File
@@ -49,9 +49,9 @@ itself stays byte-for-byte alignable with upstream's version of the file.
### `account-quota-usage-column` 🟡
- **Where**: [`src/lib/accountColumns.ts`](src/lib/accountColumns.ts)
- **What**: adds a synthetic `quotaUsage` column to the `x:Account/User` list that isn't a real schema property `DynamicList` resolves it from the `usedDiskQuota` + `quotas.maxDiskQuota` pair and formats it specially. Also re-adds `roles` (a real property, just not in the list's default columns).
- **Why**: the Accounts list schema doesn't expose usage/role as list columns, only as detail-view fields.
- **Ideal fix**: the server's `x:Account/User` list schema includes `roles` and a computed usage/quota column natively; this file is deleted.
- **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` 🟡
+8 -4
View File
@@ -257,7 +257,7 @@ function renderQuotaUsage(item: Record<string, unknown>, t: TFn): React.ReactNod
const quotas = item.quotas as Record<string, unknown> | undefined;
const rawLimit = quotas && typeof quotas.maxDiskQuota === 'number' ? quotas.maxDiskQuota : 0;
const limit = Number.isFinite(rawLimit) && rawLimit > 0 ? rawLimit : 0;
const limitLabel = limit ? formatSize(limit) : t('list.unlimitedQuota', 'Unlimited');
const limitLabel = limit ? formatSize(limit) : t('list.unlimitedQuota', '');
if (used >= 0) {
return `${formatSize(used)} / ${limitLabel}`;
@@ -499,6 +499,10 @@ export function DynamicList({ viewName }: DynamicListProps) {
const isLogEntries = viewName === 'x:Log' || objectName === 'x:Log';
const isAccountsList = viewName === 'x:Account/User';
const isMailboxList = viewName === 'Mailbox';
// Not tied to a specific viewName: any list whose schema-driven columns
// (see withAccountListColumns, account-quota-usage-column deviation)
// include the synthetic `quotaUsage` column gets it resolved and rendered.
const hasQuotaUsageColumn = (resolved?.list?.columns ?? []).some((c) => c.name === 'quotaUsage');
const displayColumns = useMemo(() => {
const columns = resolved?.list?.columns ?? [];
@@ -661,7 +665,7 @@ export function DynamicList({ viewName }: DynamicListProps) {
if (isWebApplications && !properties.includes('enabled')) {
properties.push('enabled');
}
if (isAccountsList) {
if (hasQuotaUsageColumn) {
const quotaIdx = properties.indexOf('quotaUsage');
if (quotaIdx !== -1) {
properties.splice(quotaIdx, 1, 'usedDiskQuota', 'quotas');
@@ -751,7 +755,7 @@ export function DynamicList({ viewName }: DynamicListProps) {
setLoading(false);
}
},
[resolved, schema, buildFilter, buildSort, isWebApplications, isAccountsList, isMailboxList, activeClientFilters],
[resolved, schema, buildFilter, buildSort, isWebApplications, isAccountsList, hasQuotaUsageColumn, isMailboxList, activeClientFilters],
);
useEffect(() => {
@@ -1674,7 +1678,7 @@ export function DynamicList({ viewName }: DynamicListProps) {
)
) : isAccountsList && col.name === 'roles' ? (
formatUserRole(item, schema!)
) : isAccountsList && col.name === 'quotaUsage' ? (
) : hasQuotaUsageColumn && col.name === 'quotaUsage' ? (
renderQuotaUsage(item, t)
) : isMailboxList && col.name === 'name' ? (
(() => {
+1 -1
View File
@@ -319,7 +319,7 @@
"showingItems": "Showing {{count}} items",
"sort": "Sort",
"unknownError": "Unknown error",
"unlimitedQuota": "Unlimited",
"unlimitedQuota": "",
"negativeQuotaInfoAria": "Why is disk usage negative?",
"negativeQuotaTooltip": "This disk-usage counter is out of sync (often after a migration or reset). Schedule a task: Perform account maintenance operations → Recalculate storage quota usage for the account. Or for all accounts: Perform store maintenance operations → Reset all user quotas."
},
+21 -14
View File
@@ -15,22 +15,29 @@ import type { Schema } from '@/types/schema';
* renders through the normal field pipeline, but `quotaUsage` is synthetic
* (not a real server property) — DynamicList resolves it to the
* `usedDiskQuota` + `quotas.maxDiskQuota` pair and formats it specially.
*
* The Groups list gets the same `quotaUsage` column (groups have their own
* `usedDiskQuota`/`quotas`, same as users), but not `roles` — that column
* is specific to the Users list.
*/
export function withAccountListColumns(schema: Schema): Schema {
const list = schema.lists['x:Account/User'];
if (!list) return schema;
let lists = schema.lists;
const columns = [
...list.columns.filter((c) => c.name !== 'createdAt'),
{ name: 'roles', label: 'Role' },
{ name: 'quotaUsage', label: 'Usage / Quota' },
];
const userList = lists['x:Account/User'];
if (userList) {
const columns = [
...userList.columns.filter((c) => c.name !== 'createdAt'),
{ name: 'roles', label: 'Role' },
{ name: 'quotaUsage', label: 'Usage / Quota' },
];
lists = { ...lists, 'x:Account/User': { ...userList, columns } };
}
return {
...schema,
lists: {
...schema.lists,
'x:Account/User': { ...list, columns },
},
};
const groupList = lists['x:Account/Group'];
if (groupList) {
const columns = [...groupList.columns, { name: 'quotaUsage', label: 'Usage / Quota' }];
lists = { ...lists, 'x:Account/Group': { ...groupList, columns } };
}
return { ...schema, lists };
}