feat(roles): add Enabled/Disabled Permissions count columns
New SCHEMA-DEVIATION: role-permission-count-columns — the Roles list only shows Description, so seeing how broad or restrictive a role is required opening it and counting permissions by hand. Generalized DynamicList's existing alias-count handling (previously hardcoded to the single `aliasCount` -> `aliases` mapping) into COUNT_COLUMN_SOURCES, a table of synthetic "*Count" column names to the real set/objectList property they count. account-alias-count-column now runs through the same generic path with no behavior change; role-permission-count-columns (enabledPermissionCount/ disabledPermissionCount -> enabledPermissions/disabledPermissions) is the second consumer, added with zero new DynamicList.tsx branching. Verified against the live dev server: correct counts for both seeded custom roles (Support Agent: 3/0, Read-only Auditor: 1/0) and the built-in roles (System Administrator: 452/0, User: 244/0, etc.). Confirmed no regression on Accounts' existing Aliases column/sort.
This commit is contained in:
@@ -81,6 +81,13 @@ itself stays byte-for-byte alignable with upstream's version of the file.
|
||||
- **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.
|
||||
|
||||
### `role-permission-count-columns` 🟡
|
||||
|
||||
- **Where**: [`src/lib/roleColumns.ts`](src/lib/roleColumns.ts); resolved generically by the same `COUNT_COLUMN_SOURCES` table in [`src/components/lists/DynamicList.tsx`](src/components/lists/DynamicList.tsx) used by `account-alias-count-column`
|
||||
- **What**: adds synthetic `enabledPermissionCount`/`disabledPermissionCount` columns to the `x:Role` list — not real schema properties; resolved from the real `enabledPermissions`/`disabledPermissions` set properties and rendered as entry counts.
|
||||
- **Why**: the Roles list schema only exposes Description as a column; seeing how broad or restrictive a role is requires opening it and counting permissions by hand.
|
||||
- **Ideal fix**: the server's `x:Role` list schema includes computed enabled/disabled permission count columns natively; this column definition is deleted.
|
||||
|
||||
## Not a deviation (for reference)
|
||||
|
||||
A few other `viewName === '...'` / `objectName === '...'` checks exist in
|
||||
|
||||
@@ -272,6 +272,28 @@ function renderQuotaUsage(item: Record<string, unknown>, t: TFn): React.ReactNod
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* SCHEMA-DEVIATION: account-alias-count-column, role-permission-count-columns
|
||||
* (see SCHEMA_DEVIATIONS.md)
|
||||
*
|
||||
* Synthetic "count" columns, keyed by column name, each naming the real
|
||||
* set/objectList property whose entry count they render. Generic and
|
||||
* table-level: whichever list's schema patch tags a column with one of
|
||||
* these names (see withAccountListColumns, withMailingListColumns,
|
||||
* withRoleListColumns) gets it resolved and rendered automatically, with
|
||||
* no per-list wiring in this component.
|
||||
*/
|
||||
const COUNT_COLUMN_SOURCES: Record<string, string> = {
|
||||
aliasCount: 'aliases',
|
||||
enabledPermissionCount: 'enabledPermissions',
|
||||
disabledPermissionCount: 'disabledPermissions',
|
||||
};
|
||||
|
||||
function getCountColumnValue(colName: string, item: Record<string, unknown>): number {
|
||||
const source = COUNT_COLUMN_SOURCES[colName];
|
||||
return Object.keys((item[source] as Record<string, unknown>) ?? {}).length;
|
||||
}
|
||||
|
||||
/**
|
||||
* SCHEMA-DEVIATION: account-client-sort (see SCHEMA_DEVIATIONS.md)
|
||||
*
|
||||
@@ -282,15 +304,16 @@ function renderQuotaUsage(item: Record<string, unknown>, t: TFn): React.ReactNod
|
||||
* or Groups, and needs no per-list wiring in this component.
|
||||
*
|
||||
* Real columns compare their own property directly; synthetic deviation
|
||||
* columns (quotaUsage, aliasCount) aren't real properties, so they need an
|
||||
* override to compute a comparable value from what's actually on the item.
|
||||
* columns (quotaUsage, and any COUNT_COLUMN_SOURCES entry) aren't real
|
||||
* properties, so they need an override to compute a comparable value from
|
||||
* what's actually on the item.
|
||||
*/
|
||||
const CLIENT_SORT_VALUE_OVERRIDES: Record<string, (item: Record<string, unknown>) => string | number> = {
|
||||
quotaUsage: (item) => (typeof item.usedDiskQuota === 'number' ? item.usedDiskQuota : 0),
|
||||
aliasCount: (item) => Object.keys((item.aliases as Record<string, unknown>) ?? {}).length,
|
||||
};
|
||||
|
||||
function getClientSortValue(colName: string, item: Record<string, unknown>): string | number {
|
||||
if (colName in COUNT_COLUMN_SOURCES) return getCountColumnValue(colName, item);
|
||||
const override = CLIENT_SORT_VALUE_OVERRIDES[colName];
|
||||
if (override) return override(item);
|
||||
const raw = item[colName];
|
||||
@@ -528,8 +551,13 @@ export function DynamicList({ viewName }: DynamicListProps) {
|
||||
// (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');
|
||||
// SCHEMA-DEVIATION: account-alias-count-column (see SCHEMA_DEVIATIONS.md)
|
||||
const hasAliasCountColumn = (resolved?.list?.columns ?? []).some((c) => c.name === 'aliasCount');
|
||||
// SCHEMA-DEVIATION: account-alias-count-column, role-permission-count-columns
|
||||
// (see SCHEMA_DEVIATIONS.md) — which COUNT_COLUMN_SOURCES entries this
|
||||
// particular list's columns actually declare.
|
||||
const activeCountColumns = useMemo(
|
||||
() => (resolved?.list?.columns ?? []).filter((c) => c.name in COUNT_COLUMN_SOURCES).map((c) => c.name),
|
||||
[resolved?.list?.columns],
|
||||
);
|
||||
|
||||
const displayColumns = useMemo(() => {
|
||||
const columns = resolved?.list?.columns ?? [];
|
||||
@@ -709,10 +737,10 @@ export function DynamicList({ viewName }: DynamicListProps) {
|
||||
properties.splice(quotaIdx, 1, 'usedDiskQuota', 'quotas');
|
||||
}
|
||||
}
|
||||
if (hasAliasCountColumn) {
|
||||
const aliasIdx = properties.indexOf('aliasCount');
|
||||
if (aliasIdx !== -1) {
|
||||
properties.splice(aliasIdx, 1, 'aliases');
|
||||
for (const countCol of activeCountColumns) {
|
||||
const idx = properties.indexOf(countCol);
|
||||
if (idx !== -1) {
|
||||
properties.splice(idx, 1, COUNT_COLUMN_SOURCES[countCol]);
|
||||
}
|
||||
}
|
||||
if (isMailboxList && !properties.includes('parentId')) {
|
||||
@@ -819,7 +847,7 @@ export function DynamicList({ viewName }: DynamicListProps) {
|
||||
isWebApplications,
|
||||
isAccountsList,
|
||||
hasQuotaUsageColumn,
|
||||
hasAliasCountColumn,
|
||||
activeCountColumns,
|
||||
isMailboxList,
|
||||
clientSortField,
|
||||
sort,
|
||||
@@ -1749,8 +1777,8 @@ export function DynamicList({ viewName }: DynamicListProps) {
|
||||
formatUserRole(item, schema!)
|
||||
) : hasQuotaUsageColumn && col.name === 'quotaUsage' ? (
|
||||
renderQuotaUsage(item, t)
|
||||
) : hasAliasCountColumn && col.name === 'aliasCount' ? (
|
||||
Object.keys((item.aliases as Record<string, unknown>) ?? {}).length
|
||||
) : col.name in COUNT_COLUMN_SOURCES && activeCountColumns.includes(col.name) ? (
|
||||
getCountColumnValue(col.name, item)
|
||||
) : isMailboxList && col.name === 'name' ? (
|
||||
(() => {
|
||||
const depth = mailboxDepths.get(item.id as string) ?? 0;
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
import type { Schema } from '@/types/schema';
|
||||
|
||||
/**
|
||||
* SCHEMA-DEVIATION: role-permission-count-columns (see SCHEMA_DEVIATIONS.md)
|
||||
*
|
||||
* The Roles list only shows Description, hiding how many permissions a
|
||||
* role actually grants or explicitly revokes without opening it. Adds two
|
||||
* synthetic columns — DynamicList's generic count-column handling
|
||||
* resolves them from the real `enabledPermissions`/`disabledPermissions`
|
||||
* set properties and renders their entry counts.
|
||||
*/
|
||||
export function withRoleListColumns(schema: Schema): Schema {
|
||||
const list = schema.lists['x:Role'];
|
||||
if (!list) return schema;
|
||||
|
||||
return {
|
||||
...schema,
|
||||
lists: {
|
||||
...schema.lists,
|
||||
'x:Role': {
|
||||
...list,
|
||||
columns: [
|
||||
...list.columns,
|
||||
{ name: 'enabledPermissionCount', label: 'Enabled Permissions' },
|
||||
{ name: 'disabledPermissionCount', label: 'Disabled Permissions' },
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import { fetchSession, fetchSchema, fetchAccountInfo } from '@/services/jmap/cli
|
||||
import { withClientLogFilters } from '@/lib/logFilters';
|
||||
import { withAccountListColumns } from '@/lib/accountColumns';
|
||||
import { withMailingListColumns } from '@/lib/mailingListColumns';
|
||||
import { withRoleListColumns } from '@/lib/roleColumns';
|
||||
import { setLocale } from '@/i18n';
|
||||
import { TopBar } from '@/components/layout/TopBar';
|
||||
import { Sidebar } from '@/components/layout/Sidebar';
|
||||
@@ -151,7 +152,7 @@ export default function AdminPanel() {
|
||||
|
||||
if (cancelled) return;
|
||||
|
||||
setSchema(withMailingListColumns(withAccountListColumns(withClientLogFilters(schemaData))));
|
||||
setSchema(withRoleListColumns(withMailingListColumns(withAccountListColumns(withClientLogFilters(schemaData)))));
|
||||
|
||||
setAccountInfo(accountData.permissions, accountData.edition, accountData.locale);
|
||||
setLocale(accountData.locale);
|
||||
|
||||
Reference in New Issue
Block a user