diff --git a/SCHEMA_DEVIATIONS.md b/SCHEMA_DEVIATIONS.md index cee2485..3235780 100644 --- a/SCHEMA_DEVIATIONS.md +++ b/SCHEMA_DEVIATIONS.md @@ -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` 🟡 diff --git a/src/components/lists/DynamicList.tsx b/src/components/lists/DynamicList.tsx index d55227f..dcfe558 100644 --- a/src/components/lists/DynamicList.tsx +++ b/src/components/lists/DynamicList.tsx @@ -257,7 +257,7 @@ function renderQuotaUsage(item: Record, t: TFn): React.ReactNod const quotas = item.quotas as Record | 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' ? ( (() => { diff --git a/src/i18n/en.json b/src/i18n/en.json index b5552e8..a6d6b24 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -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." }, diff --git a/src/lib/accountColumns.ts b/src/lib/accountColumns.ts index 293855d..e3d801c 100644 --- a/src/lib/accountColumns.ts +++ b/src/lib/accountColumns.ts @@ -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 }; }