Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions public/assets/images/add-white.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions public/assets/images/remove-white.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 5 additions & 1 deletion src/app/(payload)/admin/importMap.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,12 @@ import { ReindexButton as ReindexButton_aead06e4cbf6b2620c5c51c9ab283634 } from
import { default as default_0c3317dc490d2187d9715694656ec3d3 } from '@/components/SiteCell'
import { default as default_2e6cf1b1b850543a1bd5771a5b86ee4d } from '@/components/SiteRowLabel'
import { default as default_32d7f490396abda7b466903585904485 } from '@/components/RemoveUser'
import { default as default_7ce813284ffa92867fd9fdc508e13505 } from 'src/components/UserCollectionDescription'
import { default as default_fedc587b86d65a6c9503093fbd9e9e2f } from '@/components/CustomPublishButton'
import { default as default_8a7ab0eb7ab5c511aba12e68480bfe5e } from '@/components/BeforeLogin'
import { S3ClientUploadHandler as S3ClientUploadHandler_f97aa6c64367fa259c5bc0567239ef24 } from '@payloadcms/storage-s3/client'
import { default as default_6d0fe59af291eda0374bdc5d5f9a5829 } from '@/components/CustomDashboard'
import { default as default_7bac276e3812125fd6b97470a9f2783d } from '@/components/UserRolesAndPermissions'

export const importMap = {
"@payloadcms/richtext-lexical/rsc#RscEntryLexicalCell": RscEntryLexicalCell_44fe37237e0ebf4470c9990d8cb7b07e,
Expand Down Expand Up @@ -82,8 +84,10 @@ export const importMap = {
"@/components/SiteCell#default": default_0c3317dc490d2187d9715694656ec3d3,
"@/components/SiteRowLabel#default": default_2e6cf1b1b850543a1bd5771a5b86ee4d,
"@/components/RemoveUser#default": default_32d7f490396abda7b466903585904485,
"src/components/UserCollectionDescription#default": default_7ce813284ffa92867fd9fdc508e13505,
"@/components/CustomPublishButton#default": default_fedc587b86d65a6c9503093fbd9e9e2f,
"@/components/BeforeLogin#default": default_8a7ab0eb7ab5c511aba12e68480bfe5e,
"@payloadcms/storage-s3/client#S3ClientUploadHandler": S3ClientUploadHandler_f97aa6c64367fa259c5bc0567239ef24,
"@/components/CustomDashboard#default": default_6d0fe59af291eda0374bdc5d5f9a5829
"@/components/CustomDashboard#default": default_6d0fe59af291eda0374bdc5d5f9a5829,
"@/components/UserRolesAndPermissions#default": default_7bac276e3812125fd6b97470a9f2783d
}
48 changes: 48 additions & 0 deletions src/app/(payload)/custom.scss
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@
@forward "usa-skipnav";
@forward "usa-summary-box";
@forward "uswds-utilities";
@forward "usa-table";
@forward "usa-accordion";
@forward "usa-in-page-navigation";

// Import USWDS variables early so they're available for custom styles
@import "../styles/uswds-variables.scss";
Expand Down Expand Up @@ -452,3 +455,48 @@ button {
.list-header__actions {
margin-left: var(--spacing-4);
}

// In page nav container
// Roles and Permissions page

.usa-in-page-nav-container {
position: relative;
max-width: 75rem;
margin-left: auto;
margin-right: auto;
main {
max-width: 55rem;
h2, h4 {
font-family: inherit;
}
.usa-prose > ul.margin-top-1 {
margin-top: .5rem;
li {
line-height: 1;
}
}
.usa-prose > .usa-table-container--scrollable td {
vertical-align: top;
}
}
}

.usa-table--borderless thead.bg-base-lighter th {
background-color: #dfe1e2;
}

.usa-accordion__button.bg-primary {
background-color: #005ea2;
color: #ffffff;
background-image: url(/assets/images/remove-white.svg), linear-gradient(transparent, transparent);
&:hover {
background-color: #1a4480;
}
}

.usa-accordion__button.bg-primary[aria-expanded=false] {
background-image: url(/assets/images/add-white.svg), linear-gradient(transparent, transparent);
&:hover {
background-color: #1a4480;
}
}
3 changes: 3 additions & 0 deletions src/collections/Users/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ export const Users: CollectionConfig = {
slug: 'users',
admin: {
group: 'User Management',
components: {
Description: 'src/components/UserCollectionDescription',
},
description: 'Manage who can access and edit the site, including roles and permissions.',
defaultColumns: ['email', 'updatedAt', 'sites'],
useAsTitle: 'email',
Expand Down
21 changes: 21 additions & 0 deletions src/components/CustomDashboard/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,27 @@ const CustomDashboard: React.FC = async (props: { payload: BasePayload }) => {
groupedItems[groupKey].push(item)
})

const usersGroup = collections.find(c => c.slug === 'users')?.group || 'User Management'

const rolesAndPermissionsPage = {
slug: 'sites-roles-and-permissions',
label: 'Site Roles and Permissions',
description: 'Documentation on roles and permissions.',
group: typeof usersGroup === 'string' ? usersGroup : 'User Management',
href: '/admin/sites-roles-and-permissions',
}

if (!groupedItems[rolesAndPermissionsPage.group]) {
groupedItems[rolesAndPermissionsPage.group] = []
}

const usersIdx = groupedItems[rolesAndPermissionsPage.group].findIndex(i => i.slug === 'users')
if(usersIdx >= 0) {
groupedItems[rolesAndPermissionsPage.group].splice(usersIdx + 1, 0, rolesAndPermissionsPage as any)
} else {
groupedItems[rolesAndPermissionsPage.group].push(rolesAndPermissionsPage as any)
}

// Define the order of groups
const groupOrder = [
'Content Collection',
Expand Down
12 changes: 12 additions & 0 deletions src/components/UserCollectionDescription/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import Link from "next/link";

const UserCollectionDescription = () => {
return (
<>
<p>Manage who can access and edit the site, including roles and permissions.</p>
<p>Learn about roles and permissions <Link style={{textDecoration: "underline"}} href="/admin/sites-roles-and-permissions">here</Link>.</p>
</>
)
}

export default UserCollectionDescription;
34 changes: 34 additions & 0 deletions src/components/UserRolesAndPermissions/SetStepNav.client.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"use client";
// ref https://github.com/payloadcms/payload/issues/12344#issuecomment-2867110662
// workaround for Breadcrumbs not working when using DefaultTemplate
import { useEffect } from "react";
import { useStepNav } from "@payloadcms/ui";

export function SetStepNav() {
const { setStepNav } = useStepNav();

useEffect(() => {
// Define your breadcrumb path. The first entry should be the "home"/dashboard.
// Subsequent entries represent the current custom view trail.
setStepNav([
{
label: "Sites Roles and Permissions",
url: "/admin/sites-roles-and-permissions", // your custom view route
},
]);

// Optional: on unmount, clear or reset to dashboard-only
return () => {
try {
setStepNav([
{ label: "Dashboard", url: "/admin" },
]);
} catch {
/* no-op */
}
};
}, [setStepNav]);

// This component only sets step nav; it renders nothing.
return null;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@

"use client";

import { useEffect } from "react";

/**
* USWDSInit — Client-only initializer for:
* - In-page navigation
* - Accordion
*
* It dynamically imports USWDS JS and initializes components on the document.
* Safe for Next.js App Router: nothing runs during SSR.
*/
export function USWDSInit() {
useEffect(() => {
let inPageNav: any | undefined;
let accordion: any | undefined;

let mounted = true;

(async () => {
try {
// In-page navigation
const inPageNavMod = await import("@uswds/uswds/js/usa-in-page-navigation");
inPageNav = inPageNavMod?.default ?? inPageNavMod;

// Accordion
const accordionMod = await import("@uswds/uswds/js/usa-accordion");
accordion = accordionMod?.default ?? accordionMod;

if (mounted) {
// Initialize both against the document
inPageNav?.on(document.body);
accordion?.on(document.body);

// Optional: if headings are dynamically injected/updated,
// re-init the in-page nav when <main> headings change.
const main = document.querySelector("main");
if (main) {
const observer = new MutationObserver(() => {
try {
// Re-run to rebuild TOC (guarding against double init)
inPageNav?.off(document.body);
inPageNav?.on(document.body);
} catch {
/* no-op */
}
});

observer.observe(main, {
subtree: true,
childList: true,
characterData: true,
});

// Clean up the observer on unmount
(USWDSInit as any)._observer = observer;
}
}
} catch {
// Silence errors to avoid noisy logs during dev hot reload
}
})();

return () => {
mounted = false;
try {
// Disconnect the mutation observer if we created one
const observer = (USWDSInit as any)._observer as MutationObserver | undefined;
observer?.disconnect();
} catch {
/* no-op */
}

try {
inPageNav?.off(document.body);
} catch {
/* no-op */
}

try {
accordion?.off(document.body);
} catch {
/* no-op */
}
};
}, []);

return null;
}
Loading