Repository: safeinsights/management-app
Pull request #187: Org Admin - Org Admin Entry Points
Author: login: therealmarv
Created: 2025-05-07T16:20:21Z | Merged: 2025-05-08T20:29:46Z

see https://openstax.atlassian.net/browse/OTTER-166


Diff:
diff --git a/src/components/layout/navbar-items.tsx b/src/components/layout/navbar-items.tsx
index 3bf3df9c8..b8f35ab6b 100644
--- a/src/components/layout/navbar-items.tsx
+++ b/src/components/layout/navbar-items.tsx
@@ -8,11 +8,16 @@ import { House } from '@phosphor-icons/react/dist/ssr'
 import { OrganizationSwitcher } from '@clerk/nextjs'
 import { useAuthInfo } from '@/components/auth'
 import styles from './navbar-items.module.css'
+import { OrgAdminDashboardLink } from './org-admin-dashboard-link'
 
 export const NavbarItems: FC = () => {
-    const { isReviewer, isResearcher, isAdmin } = useAuthInfo()
+    const { isLoaded, isReviewer, isResearcher, isAdmin, orgSlug } = useAuthInfo()
+
     const pathname = usePathname()
 
+    // wait for Clerk to finish loading before showing nav links
+    if (!isLoaded) return null
+
     const dashboardURL = () => {
         if (isReviewer) return '/reviewer/openstax/dashboard'
         if (isResearcher) return '/researcher/dashboard'
@@ -34,18 +39,31 @@ export const NavbarItems: FC = () => {
                 className={styles.navLinkHover}
             />
 
-            <Group justify="left" pl="xs" c="white">
+            <OrgAdminDashboardLink orgSlug={orgSlug} pathname={pathname} />
+
+            <Group justify="left" c="white" w="100%">
                 <OrganizationSwitcher
                     afterSelectOrganizationUrl="/"
                     appearance={{
                         elements: {
+                            rootBox: {
+                                width: '100%',
+                            },
                             organizationSwitcherTrigger: {
                                 color: 'white !important',
                                 '& span': { color: 'white !important' },
-                                padding: 0,
+                                padding: `12px 10px`,
+                                width: '100%',
+                                borderRadius: '0',
                                 '&:hover': {
                                     backgroundColor: 'var(--mantine-color-blue-9)',
                                 },
+                                display: 'flex',
+                                justifyContent: 'space-between',
+                                alignItems: 'center',
+                            },
+                            organizationPreview: {
+                                gap: 'var(--mantine-spacing-sm)',
                             },
                         },
                     }}
diff --git a/src/components/layout/org-admin-dashboard-link.tsx b/src/components/layout/org-admin-dashboard-link.tsx
new file mode 100644
index 000000000..a7561a54c
--- /dev/null
+++ b/src/components/layout/org-admin-dashboard-link.tsx
@@ -0,0 +1,82 @@
+'use client'
+
+import { FC, useState, useEffect } from 'react'
+import { NavLink } from '@mantine/core'
+import Link from 'next/link'
+import { Gear, UsersThree } from '@phosphor-icons/react/dist/ssr'
+import { Protect } from '@clerk/nextjs'
+import styles from './navbar-items.module.css'
+
+interface OrgAdminDashboardLinkProps {
+    orgSlug: string | null | undefined
+    pathname: string
+}
+
+export const OrgAdminDashboardLink: FC<OrgAdminDashboardLinkProps> = ({ orgSlug, pathname }) => {
+    const orgAdminBaseUrl = `/organization/${orgSlug}/admin`
+    // avoid a "closed->open" flash on selecting submenus first time by seeding state from the current path
+    const [isAdminMenuOpen, setIsAdminMenuOpen] = useState(() =>
+        Boolean(orgSlug && pathname.startsWith(orgAdminBaseUrl)),
+    )
+
+    useEffect(() => {
+        if (orgSlug) {
+            if (pathname.startsWith(orgAdminBaseUrl)) {
+                setIsAdminMenuOpen(true)
+            } else {
+                setIsAdminMenuOpen(false)
+            }
+        } else {
+            setIsAdminMenuOpen(false)
+        }
+    }, [pathname, orgAdminBaseUrl, orgSlug])
+
+    if (!orgSlug) {
+        return null
+    }
+
+    return (
+        <Protect role="org:admin">
+            <NavLink
+                label="Admin"
+                leftSection={<Gear />}
+                onClick={() => {
+                    if (!pathname.startsWith(orgAdminBaseUrl)) {
+                        setIsAdminMenuOpen((prev) => !prev)
+                    }
+                }}
+                active={false}
+                opened={isAdminMenuOpen}
+                c="white"
+                className={styles.navLinkHover}
+                rightSection={null}
+            >
+                <NavLink
+                    label="Manage Team"
+                    leftSection={<UsersThree size={20} />}
+                    component={Link}
+                    href={`${orgAdminBaseUrl}/users`}
+                    active={pathname === `${orgAdminBaseUrl}/users`}
+                    c="white"
+                    color="blue.7"
+                    variant="filled"
+                    className={styles.navLinkHover}
+                    pl="xl"
+                />
+                {/* TODO: re-add if we have a org admin settings page */}
+                {/* <NavLink
+                    label="Settings"
+                    leftSection={<Sliders size={20} />}
+                    component={Link}
+                    href={`${orgAdminBaseUrl}/settings`}
+                    active={pathname === `${orgAdminBaseUrl}/settings`}
+                    c="white"
+                    color="blue.7"
+                    variant="filled"
+                    className={styles.navLinkHover}
+                    pl="xl"
+                /> */}
+            </NavLink>
+        </Protect>
+    )
+}


Reviews:
user: login: nathanstitt
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-05-07T20:51:27Z
user: login: therealmarv
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T21:04:10Z
user: login: nathanstitt
body: thank you @therealmarv 
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-08T20:20:04Z

Review comments:
diff_hunk: @@ -34,18 +52,76 @@ export const NavbarItems: FC = () => {
                 className={styles.navLinkHover}
             />
 
-            <Group justify="left" pl="xs" c="white">
+            {orgSlug && (
+                <Protect role="org:admin">
+                    <NavLink
+                        label="Admin"
+                        leftSection={<Gear />}
+                        onClick={() => {
+                            if (!pathname.startsWith(orgAdminBaseUrl)) {
+                                setIsAdminMenuOpen((prev) => !prev)
+                            }
+                            // If pathname.startsWith(orgAdminBaseUrl) is true, do nothing here.
+                            // The menu is already open (due to useEffect) and should stay open.
+                        }}
+                        active={false}
+                        opened={isAdminMenuOpen}
+                        c="white"
+                        className={styles.navLinkHover}
+                        rightSection={<></>}
+                    >
+                        <NavLink
+                            label="Manage Team"
+                            leftSection={<UsersThree size={20} />}
+                            component={Link}
+                            href={`${orgAdminBaseUrl}/users`}
+                            active={pathname === `${orgAdminBaseUrl}/users`}
+                            c="white"
+                            color="blue.7"
+                            variant="filled"
+                            className={styles.navLinkHover}
+                            pl="xl"
+                        />
+                        {/* TODO: re-add if we have a org admin settings page */}
+                        {/* <NavLink
+                            label="Settings"
+                            leftSection={<Sliders size={20} />}
+                            component={Link}
+                            href={`${orgAdminBaseUrl}/settings`}
+                            active={pathname === `${orgAdminBaseUrl}/settings`}
+                            c="white"
+                            color="blue.7"
+                            variant="filled"
+                            className={styles.navLinkHover}
+                            pl="xl"
+                        /> */}
+                    </NavLink>
+                </Protect>
+            )}
path: src/components/layout/navbar-items.tsx
user: login: nathanstitt
body: this is too much code to put inside a jsx `{}` conditional block.  please extract it to an AdminNavLink (or something named like that) component as previously suggested, then the 
created_at: 2025-05-07T20:51:22Z
author_association: MEMBER
diff_hunk: @@ -34,18 +52,76 @@ export const NavbarItems: FC = () => {
                 className={styles.navLinkHover}
             />
 
-            <Group justify="left" pl="xs" c="white">
+            {orgSlug && (
+                <Protect role="org:admin">
+                    <NavLink
+                        label="Admin"
+                        leftSection={<Gear />}
+                        onClick={() => {
+                            if (!pathname.startsWith(orgAdminBaseUrl)) {
+                                setIsAdminMenuOpen((prev) => !prev)
+                            }
+                            // If pathname.startsWith(orgAdminBaseUrl) is true, do nothing here.
+                            // The menu is already open (due to useEffect) and should stay open.
+                        }}
+                        active={false}
+                        opened={isAdminMenuOpen}
+                        c="white"
+                        className={styles.navLinkHover}
+                        rightSection={<></>}
+                    >
+                        <NavLink
+                            label="Manage Team"
+                            leftSection={<UsersThree size={20} />}
+                            component={Link}
+                            href={`${orgAdminBaseUrl}/users`}
+                            active={pathname === `${orgAdminBaseUrl}/users`}
+                            c="white"
+                            color="blue.7"
+                            variant="filled"
+                            className={styles.navLinkHover}
+                            pl="xl"
+                        />
+                        {/* TODO: re-add if we have a org admin settings page */}
+                        {/* <NavLink
+                            label="Settings"
+                            leftSection={<Sliders size={20} />}
+                            component={Link}
+                            href={`${orgAdminBaseUrl}/settings`}
+                            active={pathname === `${orgAdminBaseUrl}/settings`}
+                            c="white"
+                            color="blue.7"
+                            variant="filled"
+                            className={styles.navLinkHover}
+                            pl="xl"
+                        /> */}
+                    </NavLink>
+                </Protect>
+            )}
path: src/components/layout/navbar-items.tsx
user: login: therealmarv
body: okay, putting it into a component
created_at: 2025-05-07T21:04:10Z
author_association: CONTRIBUTOR

PR comments:
user: login: therealmarv
created_at: 2025-05-07T19:58:32Z
body: <img width="1021" alt="image" src="https://github.com/user-attachments/assets/4f7e21de-c529-4c93-863e-c04b7e507557" />

author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-05-08T10:43:11Z
body: 
### Total coverage

| Lines           | Branches           | Functions           | Statements           |
| --------------- | ------------------ | ------------------- | -------------------- |
| 87.78% (-0.01%) | 62.05% (+0.24%) | 82.77% (-0.14%) | 86.40% (+0.02%) | 


#### Detailed report

<details><summary>click to open the diff coverage report</summary>

|   | File | Lines | Branches | Functions | Statements |
| - | ---- | ----- | -------- | --------- | ---------- |
| ✅ | src/components/layout/navbar-items.tsx | 89.47% (+1.24%) | 62.50% (+12.50%) | 100.00% | 88.00% (+1.64%) |,
| ✅ | src/components/layout/org-admin-dashboard-link.tsx | 85.71% (+85.71%) | 70.00% (+70.00%) | 66.66% (+66.66%) | 86.95% (+86.95%) |
</details>

<!-- This comment was produced by coverage-diff-action -->
author_association: CONTRIBUTOR