Add Groups management and split Admin navigation
- New backend: Group + GroupMembership models, schemas, CRUD router at /api/admin/groups (list, create, get detail, update, delete, add/remove members) - New Alembic migration: groups and group_memberships tables - Frontend: Admin sidebar item is now an expandable accordion with Users and Groups sub-items; AdminPage redirects to /admin/users; new AdminUsersPage and AdminGroupsPage with inline member management panel - API client: 7 new group functions + TypeScript types Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -13,6 +13,8 @@ import {
|
||||
UserCircle,
|
||||
FileText,
|
||||
Folder,
|
||||
Users,
|
||||
UsersRound,
|
||||
} from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import ThemeToggle from "@/components/ThemeToggle";
|
||||
@@ -28,9 +30,11 @@ export default function Sidebar() {
|
||||
|
||||
const isAppsRoute = location.pathname.startsWith("/apps");
|
||||
const isDocsRoute = location.pathname.startsWith("/apps/documents");
|
||||
const isAdminRoute = location.pathname.startsWith("/admin");
|
||||
|
||||
const [appsOpen, setAppsOpen] = useState(isAppsRoute);
|
||||
const [docsOpen, setDocsOpen] = useState(isDocsRoute);
|
||||
const [adminOpen, setAdminOpen] = useState(isAdminRoute);
|
||||
|
||||
// Auto-open sections when navigating to their routes
|
||||
useEffect(() => {
|
||||
@@ -41,6 +45,10 @@ export default function Sidebar() {
|
||||
if (isDocsRoute) setDocsOpen(true);
|
||||
}, [isDocsRoute]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isAdminRoute) setAdminOpen(true);
|
||||
}, [isAdminRoute]);
|
||||
|
||||
const { data: categories = [] } = useQuery({
|
||||
queryKey: ["categories"],
|
||||
queryFn: listCategories,
|
||||
@@ -200,17 +208,66 @@ export default function Sidebar() {
|
||||
)}
|
||||
</NavLink>
|
||||
|
||||
{/* Admin */}
|
||||
{/* Admin — expandable */}
|
||||
{user?.is_admin && (
|
||||
<NavLink
|
||||
to="/admin"
|
||||
className={({ isActive }) => navItemClass(isActive)}
|
||||
>
|
||||
<ShieldCheck className="h-5 w-5 shrink-0" />
|
||||
{sidebarExpanded && (
|
||||
<span className="text-sm font-medium whitespace-nowrap">Admin</span>
|
||||
<div>
|
||||
{sidebarExpanded ? (
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center rounded-lg transition-colors",
|
||||
isAdminRoute
|
||||
? "bg-primary/10 text-primary"
|
||||
: "text-muted hover:bg-muted/20 hover:text-foreground"
|
||||
)}
|
||||
>
|
||||
<NavLink
|
||||
to="/admin/users"
|
||||
className="flex items-center gap-3 px-3 py-2 flex-1 min-w-0"
|
||||
>
|
||||
<ShieldCheck className="h-5 w-5 shrink-0" />
|
||||
<span className="text-sm font-medium whitespace-nowrap">Admin</span>
|
||||
</NavLink>
|
||||
<button
|
||||
onClick={() => setAdminOpen((o) => !o)}
|
||||
className="px-2 py-2 rounded-r-lg"
|
||||
aria-label={adminOpen ? "Collapse admin" : "Expand admin"}
|
||||
>
|
||||
{adminOpen ? (
|
||||
<ChevronDown className="h-4 w-4 shrink-0" />
|
||||
) : (
|
||||
<ChevronRight className="h-4 w-4 shrink-0" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<NavLink
|
||||
to="/admin/users"
|
||||
className={({ isActive }) => navItemClass(isActive)}
|
||||
>
|
||||
<ShieldCheck className="h-5 w-5 shrink-0" />
|
||||
</NavLink>
|
||||
)}
|
||||
</NavLink>
|
||||
|
||||
{/* Admin sub-items */}
|
||||
{sidebarExpanded && adminOpen && (
|
||||
<div className="mt-0.5 space-y-0.5">
|
||||
<NavLink
|
||||
to="/admin/users"
|
||||
className={({ isActive }) => subItemClass(isActive)}
|
||||
>
|
||||
<Users className="h-4 w-4 shrink-0" />
|
||||
<span className="whitespace-nowrap">Users</span>
|
||||
</NavLink>
|
||||
<NavLink
|
||||
to="/admin/groups"
|
||||
className={({ isActive }) => subItemClass(isActive)}
|
||||
>
|
||||
<UsersRound className="h-4 w-4 shrink-0" />
|
||||
<span className="whitespace-nowrap">Groups</span>
|
||||
</NavLink>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</nav>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user