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:
@@ -49,9 +49,9 @@ itself stays byte-for-byte alignable with upstream's version of the file.
|
|||||||
### `account-quota-usage-column` 🟡
|
### `account-quota-usage-column` 🟡
|
||||||
|
|
||||||
- **Where**: [`src/lib/accountColumns.ts`](src/lib/accountColumns.ts)
|
- **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).
|
- **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**: the Accounts list schema doesn't expose usage/role as list columns, only as detail-view fields.
|
- **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` list schema includes `roles` and a computed usage/quota column natively; this file is deleted.
|
- **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` 🟡
|
### `mailbox-client-hierarchy-sort` 🟡
|
||||||
|
|
||||||
|
|||||||
@@ -257,7 +257,7 @@ function renderQuotaUsage(item: Record<string, unknown>, t: TFn): React.ReactNod
|
|||||||
const quotas = item.quotas as Record<string, unknown> | undefined;
|
const quotas = item.quotas as Record<string, unknown> | undefined;
|
||||||
const rawLimit = quotas && typeof quotas.maxDiskQuota === 'number' ? quotas.maxDiskQuota : 0;
|
const rawLimit = quotas && typeof quotas.maxDiskQuota === 'number' ? quotas.maxDiskQuota : 0;
|
||||||
const limit = Number.isFinite(rawLimit) && rawLimit > 0 ? rawLimit : 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) {
|
if (used >= 0) {
|
||||||
return `${formatSize(used)} / ${limitLabel}`;
|
return `${formatSize(used)} / ${limitLabel}`;
|
||||||
@@ -499,6 +499,10 @@ export function DynamicList({ viewName }: DynamicListProps) {
|
|||||||
const isLogEntries = viewName === 'x:Log' || objectName === 'x:Log';
|
const isLogEntries = viewName === 'x:Log' || objectName === 'x:Log';
|
||||||
const isAccountsList = viewName === 'x:Account/User';
|
const isAccountsList = viewName === 'x:Account/User';
|
||||||
const isMailboxList = viewName === 'Mailbox';
|
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 displayColumns = useMemo(() => {
|
||||||
const columns = resolved?.list?.columns ?? [];
|
const columns = resolved?.list?.columns ?? [];
|
||||||
@@ -661,7 +665,7 @@ export function DynamicList({ viewName }: DynamicListProps) {
|
|||||||
if (isWebApplications && !properties.includes('enabled')) {
|
if (isWebApplications && !properties.includes('enabled')) {
|
||||||
properties.push('enabled');
|
properties.push('enabled');
|
||||||
}
|
}
|
||||||
if (isAccountsList) {
|
if (hasQuotaUsageColumn) {
|
||||||
const quotaIdx = properties.indexOf('quotaUsage');
|
const quotaIdx = properties.indexOf('quotaUsage');
|
||||||
if (quotaIdx !== -1) {
|
if (quotaIdx !== -1) {
|
||||||
properties.splice(quotaIdx, 1, 'usedDiskQuota', 'quotas');
|
properties.splice(quotaIdx, 1, 'usedDiskQuota', 'quotas');
|
||||||
@@ -751,7 +755,7 @@ export function DynamicList({ viewName }: DynamicListProps) {
|
|||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[resolved, schema, buildFilter, buildSort, isWebApplications, isAccountsList, isMailboxList, activeClientFilters],
|
[resolved, schema, buildFilter, buildSort, isWebApplications, isAccountsList, hasQuotaUsageColumn, isMailboxList, activeClientFilters],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -1674,7 +1678,7 @@ export function DynamicList({ viewName }: DynamicListProps) {
|
|||||||
)
|
)
|
||||||
) : isAccountsList && col.name === 'roles' ? (
|
) : isAccountsList && col.name === 'roles' ? (
|
||||||
formatUserRole(item, schema!)
|
formatUserRole(item, schema!)
|
||||||
) : isAccountsList && col.name === 'quotaUsage' ? (
|
) : hasQuotaUsageColumn && col.name === 'quotaUsage' ? (
|
||||||
renderQuotaUsage(item, t)
|
renderQuotaUsage(item, t)
|
||||||
) : isMailboxList && col.name === 'name' ? (
|
) : isMailboxList && col.name === 'name' ? (
|
||||||
(() => {
|
(() => {
|
||||||
|
|||||||
+1
-1
@@ -319,7 +319,7 @@
|
|||||||
"showingItems": "Showing {{count}} items",
|
"showingItems": "Showing {{count}} items",
|
||||||
"sort": "Sort",
|
"sort": "Sort",
|
||||||
"unknownError": "Unknown error",
|
"unknownError": "Unknown error",
|
||||||
"unlimitedQuota": "Unlimited",
|
"unlimitedQuota": "∞",
|
||||||
"negativeQuotaInfoAria": "Why is disk usage negative?",
|
"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."
|
"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
@@ -15,22 +15,29 @@ import type { Schema } from '@/types/schema';
|
|||||||
* renders through the normal field pipeline, but `quotaUsage` is synthetic
|
* renders through the normal field pipeline, but `quotaUsage` is synthetic
|
||||||
* (not a real server property) — DynamicList resolves it to the
|
* (not a real server property) — DynamicList resolves it to the
|
||||||
* `usedDiskQuota` + `quotas.maxDiskQuota` pair and formats it specially.
|
* `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 {
|
export function withAccountListColumns(schema: Schema): Schema {
|
||||||
const list = schema.lists['x:Account/User'];
|
let lists = schema.lists;
|
||||||
if (!list) return schema;
|
|
||||||
|
|
||||||
const columns = [
|
const userList = lists['x:Account/User'];
|
||||||
...list.columns.filter((c) => c.name !== 'createdAt'),
|
if (userList) {
|
||||||
{ name: 'roles', label: 'Role' },
|
const columns = [
|
||||||
{ name: 'quotaUsage', label: 'Usage / Quota' },
|
...userList.columns.filter((c) => c.name !== 'createdAt'),
|
||||||
];
|
{ name: 'roles', label: 'Role' },
|
||||||
|
{ name: 'quotaUsage', label: 'Usage / Quota' },
|
||||||
|
];
|
||||||
|
lists = { ...lists, 'x:Account/User': { ...userList, columns } };
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
const groupList = lists['x:Account/Group'];
|
||||||
...schema,
|
if (groupList) {
|
||||||
lists: {
|
const columns = [...groupList.columns, { name: 'quotaUsage', label: 'Usage / Quota' }];
|
||||||
...schema.lists,
|
lists = { ...lists, 'x:Account/Group': { ...groupList, columns } };
|
||||||
'x:Account/User': { ...list, columns },
|
}
|
||||||
},
|
|
||||||
};
|
return { ...schema, lists };
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user