feat(admin+search): user-mgmt polish, role labels, search keyword index
Admin search now matches against per-card keyword lists so typing "client portal", "smtp", "tier ladder" lands on the System Settings card (which hosts those flags). The same keyword list extends the topbar global search (NAV_CATALOG) so any setting key resolves from the cmd-K input — settings results sort to the bottom of the dropdown beneath entity hits. User management: - Third action button (Power/PowerOff) enables/disables sign-in from the desktop list; mobile card dropdown gains the same item. Backed by the existing userProfiles.isActive flag — withAuth already refuses disabled sessions with 403. - UserForm collects first + last name (canonical) alongside displayName, with admin email-change behind a confirmation modal. On confirm we send the OLD address an automated "your admin changed your sign-in email" notice (new template at admin-email-change.ts) and rewrite the Better Auth user row. - Phone field swaps the bare tel input for the shared PhoneInput (country combobox + AsYouType formatting + E.164 storage). - "Manage permissions" link points to /admin/roles?focusUser=… as a stepping stone for the future fine-tuned-permissions UI. Role names normalize through a new ROLE_LABELS + formatRole() helper in constants.ts. Replaces the ad-hoc humanizeRole in sidebar and the prettifyRoleName in role-list; user-list and user-card now render "Sales Agent" instead of "sales_agent". Custom roles pass through unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -71,9 +71,12 @@ const BUCKETS: BucketConfig[] = [
|
||||
{ type: 'reminders', label: 'Reminders', icon: Bell },
|
||||
{ type: 'brochures', label: 'Brochures', icon: Camera },
|
||||
{ type: 'tags', label: 'Tags', icon: TagIcon },
|
||||
{ type: 'navigation', label: 'Settings', icon: SettingsIcon },
|
||||
// Notes always last — broad content search is noisy.
|
||||
// Notes are noisy content search.
|
||||
{ type: 'notes', label: 'Notes', icon: MessageSquare },
|
||||
// Navigation (settings pages + admin sub-cards) lives at the very bottom —
|
||||
// users open the search to find entity records first; pages/settings are
|
||||
// the long-tail jump targets.
|
||||
{ type: 'navigation', label: 'Settings', icon: SettingsIcon },
|
||||
];
|
||||
|
||||
const NAV_ICON: Record<string, typeof User> = {
|
||||
@@ -1099,25 +1102,9 @@ export function buildFlatRows(args: BuildFlatRowsArgs): FlatRow[] {
|
||||
});
|
||||
}
|
||||
}
|
||||
if (include('navigation')) {
|
||||
for (const n of results.navigation) {
|
||||
const Icon = NAV_ICON[n.category] ?? SettingsIcon;
|
||||
rows.push({
|
||||
kind: 'result',
|
||||
key: `navigation:${n.id}`,
|
||||
bucket: 'navigation',
|
||||
icon: Icon,
|
||||
label: n.label,
|
||||
sub: n.category,
|
||||
// Catalog hrefs already have :portSlug substituted server-side.
|
||||
href: n.href,
|
||||
});
|
||||
}
|
||||
}
|
||||
// Notes go LAST — content matches inside notes are noisy by nature
|
||||
// (free-text search across thousands of rows), so the user sees
|
||||
// them only after the entity-specific buckets above have surfaced
|
||||
// their tighter matches.
|
||||
// Notes — content matches inside free-text notes are noisy by nature, so
|
||||
// the user sees them after the entity-specific buckets above have
|
||||
// surfaced their tighter matches.
|
||||
if (include('notes')) {
|
||||
for (const n of results.notes) {
|
||||
const sourceCollection =
|
||||
@@ -1139,6 +1126,25 @@ export function buildFlatRows(args: BuildFlatRowsArgs): FlatRow[] {
|
||||
});
|
||||
}
|
||||
}
|
||||
// Navigation (settings pages + admin section cards) goes LAST — these
|
||||
// are jump targets, not the primary thing a user opens the search to
|
||||
// find. Surfacing them after entity matches keeps the top of the
|
||||
// dropdown focused on records.
|
||||
if (include('navigation')) {
|
||||
for (const n of results.navigation) {
|
||||
const Icon = NAV_ICON[n.category] ?? SettingsIcon;
|
||||
rows.push({
|
||||
kind: 'result',
|
||||
key: `navigation:${n.id}`,
|
||||
bucket: 'navigation',
|
||||
icon: Icon,
|
||||
label: n.label,
|
||||
sub: n.category,
|
||||
// Catalog hrefs already have :portSlug substituted server-side.
|
||||
href: n.href,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (results.otherPorts && activeBucket === 'all') {
|
||||
for (const op of results.otherPorts) {
|
||||
|
||||
Reference in New Issue
Block a user