Skip to content

Commit 89b7cb9

Browse files
committed
feat: create user roles and permissions page
chore: enable breadcrumb workaround and title meta fix: remove unneeded whitepace rule, add missing table text fix: revise slug to sites roles and permissions page feat: add Roles and Permissions to User collection description fix: accomodate grid-col for mobile fix: unescaped apostrophe fix: gate the page to logged in users, correct mainly bot-oriented table info
1 parent 8e3af11 commit 89b7cb9

File tree

11 files changed

+907
-2
lines changed

11 files changed

+907
-2
lines changed

public/assets/images/add-white.svg

Lines changed: 1 addition & 0 deletions
Loading
Lines changed: 1 addition & 0 deletions
Loading

src/app/(payload)/admin/importMap.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,12 @@ import { ReindexButton as ReindexButton_aead06e4cbf6b2620c5c51c9ab283634 } from
3737
import { default as default_0c3317dc490d2187d9715694656ec3d3 } from '@/components/SiteCell'
3838
import { default as default_2e6cf1b1b850543a1bd5771a5b86ee4d } from '@/components/SiteRowLabel'
3939
import { default as default_32d7f490396abda7b466903585904485 } from '@/components/RemoveUser'
40+
import { default as default_7ce813284ffa92867fd9fdc508e13505 } from 'src/components/UserCollectionDescription'
4041
import { default as default_fedc587b86d65a6c9503093fbd9e9e2f } from '@/components/CustomPublishButton'
4142
import { default as default_8a7ab0eb7ab5c511aba12e68480bfe5e } from '@/components/BeforeLogin'
4243
import { S3ClientUploadHandler as S3ClientUploadHandler_f97aa6c64367fa259c5bc0567239ef24 } from '@payloadcms/storage-s3/client'
4344
import { default as default_6d0fe59af291eda0374bdc5d5f9a5829 } from '@/components/CustomDashboard'
45+
import { default as default_7bac276e3812125fd6b97470a9f2783d } from '@/components/UserRolesAndPermissions'
4446

4547
export const importMap = {
4648
"@payloadcms/richtext-lexical/rsc#RscEntryLexicalCell": RscEntryLexicalCell_44fe37237e0ebf4470c9990d8cb7b07e,
@@ -82,8 +84,10 @@ export const importMap = {
8284
"@/components/SiteCell#default": default_0c3317dc490d2187d9715694656ec3d3,
8385
"@/components/SiteRowLabel#default": default_2e6cf1b1b850543a1bd5771a5b86ee4d,
8486
"@/components/RemoveUser#default": default_32d7f490396abda7b466903585904485,
87+
"src/components/UserCollectionDescription#default": default_7ce813284ffa92867fd9fdc508e13505,
8588
"@/components/CustomPublishButton#default": default_fedc587b86d65a6c9503093fbd9e9e2f,
8689
"@/components/BeforeLogin#default": default_8a7ab0eb7ab5c511aba12e68480bfe5e,
8790
"@payloadcms/storage-s3/client#S3ClientUploadHandler": S3ClientUploadHandler_f97aa6c64367fa259c5bc0567239ef24,
88-
"@/components/CustomDashboard#default": default_6d0fe59af291eda0374bdc5d5f9a5829
91+
"@/components/CustomDashboard#default": default_6d0fe59af291eda0374bdc5d5f9a5829,
92+
"@/components/UserRolesAndPermissions#default": default_7bac276e3812125fd6b97470a9f2783d
8993
}

src/app/(payload)/custom.scss

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@
1717
@forward "usa-skipnav";
1818
@forward "usa-summary-box";
1919
@forward "uswds-utilities";
20+
@forward "usa-table";
21+
@forward "usa-accordion";
22+
@forward "usa-in-page-navigation";
2023

2124
// Import USWDS variables early so they're available for custom styles
2225
@import "../styles/uswds-variables.scss";
@@ -452,3 +455,48 @@ button {
452455
.list-header__actions {
453456
margin-left: var(--spacing-4);
454457
}
458+
459+
// In page nav container
460+
// Roles and Permissions page
461+
462+
.usa-in-page-nav-container {
463+
position: relative;
464+
max-width: 75rem;
465+
margin-left: auto;
466+
margin-right: auto;
467+
main {
468+
max-width: 55rem;
469+
h2, h4 {
470+
font-family: inherit;
471+
}
472+
.usa-prose > ul.margin-top-1 {
473+
margin-top: .5rem;
474+
li {
475+
line-height: 1;
476+
}
477+
}
478+
.usa-prose > .usa-table-container--scrollable td {
479+
vertical-align: top;
480+
}
481+
}
482+
}
483+
484+
.usa-table--borderless thead.bg-base-lighter th {
485+
background-color: #dfe1e2;
486+
}
487+
488+
.usa-accordion__button.bg-primary {
489+
background-color: #005ea2;
490+
color: #ffffff;
491+
background-image: url(/assets/images/remove-white.svg), linear-gradient(transparent, transparent);
492+
&:hover {
493+
background-color: #1a4480;
494+
}
495+
}
496+
497+
.usa-accordion__button.bg-primary[aria-expanded=false] {
498+
background-image: url(/assets/images/add-white.svg), linear-gradient(transparent, transparent);
499+
&:hover {
500+
background-color: #1a4480;
501+
}
502+
}

src/collections/Users/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ export const Users: CollectionConfig = {
2121
slug: 'users',
2222
admin: {
2323
group: 'User Management',
24+
components: {
25+
Description: 'src/components/UserCollectionDescription',
26+
},
2427
description: 'Manage who can access and edit the site, including roles and permissions.',
2528
defaultColumns: ['email', 'updatedAt', 'sites'],
2629
useAsTitle: 'email',

src/components/CustomDashboard/index.tsx

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,27 @@ const CustomDashboard: React.FC = async (props: { payload: BasePayload }) => {
100100
groupedItems[groupKey].push(item)
101101
})
102102

103+
const usersGroup = collections.find(c => c.slug === 'users')?.group || 'User Management'
104+
105+
const rolesAndPermissionsPage = {
106+
slug: 'sites-roles-and-permissions',
107+
label: 'Site Roles and Permissions',
108+
description: 'Documentation on roles and permissions.',
109+
group: typeof usersGroup === 'string' ? usersGroup : 'User Management',
110+
href: '/admin/sites-roles-and-permissions',
111+
}
112+
113+
if (!groupedItems[rolesAndPermissionsPage.group]) {
114+
groupedItems[rolesAndPermissionsPage.group] = []
115+
}
116+
117+
const usersIdx = groupedItems[rolesAndPermissionsPage.group].findIndex(i => i.slug === 'users')
118+
if(usersIdx >= 0) {
119+
groupedItems[rolesAndPermissionsPage.group].splice(usersIdx + 1, 0, rolesAndPermissionsPage as any)
120+
} else {
121+
groupedItems[rolesAndPermissionsPage.group].push(rolesAndPermissionsPage as any)
122+
}
123+
103124
// Define the order of groups
104125
const groupOrder = [
105126
'Content Collection',
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import Link from "next/link";
2+
3+
const UserCollectionDescription = () => {
4+
return (
5+
<>
6+
<p>Manage who can access and edit the site, including roles and permissions.</p>
7+
<p>Learn about roles and permissions <Link style={{textDecoration: "underline"}} href="/admin/sites-roles-and-permissions">here</Link>.</p>
8+
</>
9+
)
10+
}
11+
12+
export default UserCollectionDescription;
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
"use client";
2+
// ref https://github.com/payloadcms/payload/issues/12344#issuecomment-2867110662
3+
// workaround for Breadcrumbs not working when using DefaultTemplate
4+
import { useEffect } from "react";
5+
import { useStepNav } from "@payloadcms/ui";
6+
7+
export function SetStepNav() {
8+
const { setStepNav } = useStepNav();
9+
10+
useEffect(() => {
11+
// Define your breadcrumb path. The first entry should be the "home"/dashboard.
12+
// Subsequent entries represent the current custom view trail.
13+
setStepNav([
14+
{
15+
label: "Sites Roles and Permissions",
16+
url: "/admin/sites-roles-and-permissions", // your custom view route
17+
},
18+
]);
19+
20+
// Optional: on unmount, clear or reset to dashboard-only
21+
return () => {
22+
try {
23+
setStepNav([
24+
{ label: "Dashboard", url: "/admin" },
25+
]);
26+
} catch {
27+
/* no-op */
28+
}
29+
};
30+
}, [setStepNav]);
31+
32+
// This component only sets step nav; it renders nothing.
33+
return null;
34+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
2+
"use client";
3+
4+
import { useEffect } from "react";
5+
6+
/**
7+
* USWDSInit — Client-only initializer for:
8+
* - In-page navigation
9+
* - Accordion
10+
*
11+
* It dynamically imports USWDS JS and initializes components on the document.
12+
* Safe for Next.js App Router: nothing runs during SSR.
13+
*/
14+
export function USWDSInit() {
15+
useEffect(() => {
16+
let inPageNav: any | undefined;
17+
let accordion: any | undefined;
18+
19+
let mounted = true;
20+
21+
(async () => {
22+
try {
23+
// In-page navigation
24+
const inPageNavMod = await import("@uswds/uswds/js/usa-in-page-navigation");
25+
inPageNav = inPageNavMod?.default ?? inPageNavMod;
26+
27+
// Accordion
28+
const accordionMod = await import("@uswds/uswds/js/usa-accordion");
29+
accordion = accordionMod?.default ?? accordionMod;
30+
31+
if (mounted) {
32+
// Initialize both against the document
33+
inPageNav?.on(document.body);
34+
accordion?.on(document.body);
35+
36+
// Optional: if headings are dynamically injected/updated,
37+
// re-init the in-page nav when <main> headings change.
38+
const main = document.querySelector("main");
39+
if (main) {
40+
const observer = new MutationObserver(() => {
41+
try {
42+
// Re-run to rebuild TOC (guarding against double init)
43+
inPageNav?.off(document.body);
44+
inPageNav?.on(document.body);
45+
} catch {
46+
/* no-op */
47+
}
48+
});
49+
50+
observer.observe(main, {
51+
subtree: true,
52+
childList: true,
53+
characterData: true,
54+
});
55+
56+
// Clean up the observer on unmount
57+
(USWDSInit as any)._observer = observer;
58+
}
59+
}
60+
} catch {
61+
// Silence errors to avoid noisy logs during dev hot reload
62+
}
63+
})();
64+
65+
return () => {
66+
mounted = false;
67+
try {
68+
// Disconnect the mutation observer if we created one
69+
const observer = (USWDSInit as any)._observer as MutationObserver | undefined;
70+
observer?.disconnect();
71+
} catch {
72+
/* no-op */
73+
}
74+
75+
try {
76+
inPageNav?.off(document.body);
77+
} catch {
78+
/* no-op */
79+
}
80+
81+
try {
82+
accordion?.off(document.body);
83+
} catch {
84+
/* no-op */
85+
}
86+
};
87+
}, []);
88+
89+
return null;
90+
}

0 commit comments

Comments
 (0)