mirror of
https://github.com/fosrl/pangolin.git
synced 2025-08-02 00:55:48 +02:00
Add support for menu children and moved invitations under users
This commit is contained in:
parent
c7f3c9da92
commit
7a55c9ad03
8 changed files with 105 additions and 58 deletions
|
@ -72,7 +72,6 @@ export async function listInvitations(
|
|||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
// Validate query parameters
|
||||
const parsedQuery = listInvitationsQuerySchema.safeParse(req.query);
|
||||
if (!parsedQuery.success) {
|
||||
return next(
|
||||
|
@ -84,7 +83,6 @@ export async function listInvitations(
|
|||
}
|
||||
const { limit, offset } = parsedQuery.data;
|
||||
|
||||
// Validate path parameters
|
||||
const parsedParams = listInvitationsParamsSchema.safeParse(req.params);
|
||||
if (!parsedParams.success) {
|
||||
return next(
|
||||
|
@ -96,16 +94,13 @@ export async function listInvitations(
|
|||
}
|
||||
const { orgId } = parsedParams.data;
|
||||
|
||||
// Query invitations
|
||||
const invitations = await queryInvitations(orgId, limit, offset);
|
||||
|
||||
// Get total count of invitations
|
||||
const [{ count }] = await db
|
||||
.select({ count: sql<number>`count(*)` })
|
||||
.from(userInvites)
|
||||
.where(sql`${userInvites.orgId} = ${orgId}`);
|
||||
|
||||
// Return response
|
||||
return response<ListInvitationsResponse>(res, {
|
||||
data: {
|
||||
invitations,
|
||||
|
|
|
@ -22,7 +22,6 @@ export async function removeInvitation(
|
|||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
// Validate path parameters
|
||||
const parsedParams = removeInvitationParamsSchema.safeParse(req.params);
|
||||
if (!parsedParams.success) {
|
||||
return next(
|
||||
|
@ -35,7 +34,6 @@ export async function removeInvitation(
|
|||
|
||||
const { orgId, inviteId } = parsedParams.data;
|
||||
|
||||
// Delete the invitation from the database
|
||||
const deletedInvitation = await db
|
||||
.delete(userInvites)
|
||||
.where(
|
||||
|
@ -46,7 +44,6 @@ export async function removeInvitation(
|
|||
)
|
||||
.returning();
|
||||
|
||||
// If no rows were deleted, the invitation was not found
|
||||
if (deletedInvitation.length === 0) {
|
||||
return next(
|
||||
createHttpError(
|
||||
|
@ -56,7 +53,6 @@ export async function removeInvitation(
|
|||
);
|
||||
}
|
||||
|
||||
// Return success response
|
||||
return response(res, {
|
||||
data: null,
|
||||
success: true,
|
||||
|
|
|
@ -5,19 +5,25 @@ import { SidebarSettings } from "@app/components/SidebarSettings";
|
|||
|
||||
type AccessPageHeaderAndNavProps = {
|
||||
children: React.ReactNode;
|
||||
hasInvitations: boolean;
|
||||
};
|
||||
|
||||
export default function AccessPageHeaderAndNav({
|
||||
children
|
||||
children,
|
||||
hasInvitations
|
||||
}: AccessPageHeaderAndNavProps) {
|
||||
const sidebarNavItems = [
|
||||
{
|
||||
title: "Users",
|
||||
href: `/{orgId}/settings/access/users`
|
||||
},
|
||||
{
|
||||
title: "Invitations",
|
||||
href: `/{orgId}/settings/access/invitations`
|
||||
href: `/{orgId}/settings/access/users`,
|
||||
children: hasInvitations
|
||||
? [
|
||||
{
|
||||
title: "• Invitations",
|
||||
href: `/{orgId}/settings/access/invitations`
|
||||
}
|
||||
]
|
||||
: []
|
||||
},
|
||||
{
|
||||
title: "Roles",
|
||||
|
@ -29,8 +35,7 @@ export default function AccessPageHeaderAndNav({
|
|||
<>
|
||||
<SettingsSectionTitle
|
||||
title="Manage Users & Roles"
|
||||
description="Invite users and add them to roles to manage access to your
|
||||
organization"
|
||||
description="Invite users and add them to roles to manage access to your organization"
|
||||
/>
|
||||
|
||||
<SidebarSettings sidebarNavItems={sidebarNavItems}>
|
||||
|
|
|
@ -20,7 +20,7 @@ import { useEnvContext } from "@app/hooks/useEnvContext";
|
|||
export type InvitationRow = {
|
||||
id: string;
|
||||
email: string;
|
||||
expiresAt: string; // ISO string or timestamp
|
||||
expiresAt: string;
|
||||
role: string;
|
||||
};
|
||||
|
||||
|
|
|
@ -28,16 +28,20 @@ export default async function InvitationsPage(props: InvitationsPageProps) {
|
|||
roleId: string;
|
||||
roleName?: string;
|
||||
}[] = [];
|
||||
let hasInvitations = false;
|
||||
|
||||
const res = await internal
|
||||
.get<
|
||||
AxiosResponse<{
|
||||
invitations: typeof invitations;
|
||||
pagination: { total: number };
|
||||
}>
|
||||
>(`/org/${params.orgId}/invitations`, await authCookieHeader())
|
||||
.catch((e) => {});
|
||||
|
||||
if (res && res.status === 200) {
|
||||
invitations = res.data.data.invitations;
|
||||
hasInvitations = res.data.data.pagination.total > 0;
|
||||
}
|
||||
|
||||
let org: GetOrgResponse | null = null;
|
||||
|
@ -61,13 +65,13 @@ export default async function InvitationsPage(props: InvitationsPageProps) {
|
|||
id: invite.inviteId,
|
||||
email: invite.email,
|
||||
expiresAt: new Date(Number(invite.expiresAt)).toISOString(),
|
||||
role: invite.roleName || "Unknown Role" // Use roleName if available
|
||||
role: invite.roleName || "Unknown Role"
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<AccessPageHeaderAndNav>
|
||||
<AccessPageHeaderAndNav hasInvitations={hasInvitations}>
|
||||
<UserProvider user={user!}>
|
||||
<OrgProvider org={org}>
|
||||
<InvitationsTable invitations={invitationRows} />
|
||||
|
|
|
@ -19,6 +19,8 @@ export default async function RolesPage(props: RolesPageProps) {
|
|||
const params = await props.params;
|
||||
|
||||
let roles: ListRolesResponse["roles"] = [];
|
||||
let hasInvitations = false;
|
||||
|
||||
const res = await internal
|
||||
.get<
|
||||
AxiosResponse<ListRolesResponse>
|
||||
|
@ -29,6 +31,21 @@ export default async function RolesPage(props: RolesPageProps) {
|
|||
roles = res.data.data.roles;
|
||||
}
|
||||
|
||||
const invitationsRes = await internal
|
||||
.get<
|
||||
AxiosResponse<{
|
||||
pagination: { total: number };
|
||||
}>
|
||||
>(
|
||||
`/org/${params.orgId}/invitations?limit=1&offset=0`,
|
||||
await authCookieHeader()
|
||||
)
|
||||
.catch((e) => {});
|
||||
|
||||
if (invitationsRes && invitationsRes.status === 200) {
|
||||
hasInvitations = invitationsRes.data.data.pagination.total > 0;
|
||||
}
|
||||
|
||||
let org: GetOrgResponse | null = null;
|
||||
const getOrg = cache(async () =>
|
||||
internal
|
||||
|
@ -47,7 +64,7 @@ export default async function RolesPage(props: RolesPageProps) {
|
|||
|
||||
return (
|
||||
<>
|
||||
<AccessPageHeaderAndNav>
|
||||
<AccessPageHeaderAndNav hasInvitations={hasInvitations}>
|
||||
<OrgProvider org={org}>
|
||||
<RolesTable roles={roleRows} />
|
||||
</OrgProvider>
|
||||
|
|
|
@ -23,6 +23,8 @@ export default async function UsersPage(props: UsersPageProps) {
|
|||
const user = await getUser();
|
||||
|
||||
let users: ListUsersResponse["users"] = [];
|
||||
let hasInvitations = false;
|
||||
|
||||
const res = await internal
|
||||
.get<
|
||||
AxiosResponse<ListUsersResponse>
|
||||
|
@ -33,6 +35,21 @@ export default async function UsersPage(props: UsersPageProps) {
|
|||
users = res.data.data.users;
|
||||
}
|
||||
|
||||
const invitationsRes = await internal
|
||||
.get<
|
||||
AxiosResponse<{
|
||||
pagination: { total: number };
|
||||
}>
|
||||
>(
|
||||
`/org/${params.orgId}/invitations?limit=1&offset=0`,
|
||||
await authCookieHeader()
|
||||
)
|
||||
.catch((e) => {});
|
||||
|
||||
if (invitationsRes && invitationsRes.status === 200) {
|
||||
hasInvitations = invitationsRes.data.data.pagination.total > 0;
|
||||
}
|
||||
|
||||
let org: GetOrgResponse | null = null;
|
||||
const getOrg = cache(async () =>
|
||||
internal
|
||||
|
@ -61,7 +78,7 @@ export default async function UsersPage(props: UsersPageProps) {
|
|||
|
||||
return (
|
||||
<>
|
||||
<AccessPageHeaderAndNav>
|
||||
<AccessPageHeaderAndNav hasInvitations={hasInvitations}>
|
||||
<UserProvider user={user!}>
|
||||
<OrgProvider org={org}>
|
||||
<UsersTable users={userRows} />
|
||||
|
|
|
@ -10,15 +10,18 @@ import {
|
|||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
SelectValue
|
||||
} from "@/components/ui/select";
|
||||
|
||||
interface SidebarNavItem {
|
||||
href: string;
|
||||
title: string;
|
||||
icon?: React.ReactNode;
|
||||
children?: SidebarNavItem[];
|
||||
}
|
||||
|
||||
interface SidebarNavProps extends React.HTMLAttributes<HTMLElement> {
|
||||
items: {
|
||||
href: string;
|
||||
title: string;
|
||||
icon?: React.ReactNode;
|
||||
}[];
|
||||
items: SidebarNavItem[];
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
|
@ -35,7 +38,8 @@ export function SidebarNav({
|
|||
const resourceId = params.resourceId as string;
|
||||
const userId = params.userId as string;
|
||||
|
||||
const [selectedValue, setSelectedValue] = React.useState<string>(getSelectedValue());
|
||||
const [selectedValue, setSelectedValue] =
|
||||
React.useState<string>(getSelectedValue());
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedValue(getSelectedValue());
|
||||
|
@ -62,6 +66,43 @@ export function SidebarNav({
|
|||
.replace("{userId}", userId);
|
||||
}
|
||||
|
||||
function renderItems(items: SidebarNavItem[]) {
|
||||
return items.map((item) => (
|
||||
<div key={hydrateHref(item.href)}>
|
||||
<Link
|
||||
href={hydrateHref(item.href)}
|
||||
className={cn(
|
||||
buttonVariants({ variant: "ghost" }),
|
||||
pathname === hydrateHref(item.href) &&
|
||||
!pathname.includes("create")
|
||||
? "bg-accent hover:bg-accent dark:bg-border dark:hover:bg-border"
|
||||
: "hover:bg-transparent hover:underline",
|
||||
"justify-start",
|
||||
disabled && "cursor-not-allowed"
|
||||
)}
|
||||
onClick={disabled ? (e) => e.preventDefault() : undefined}
|
||||
tabIndex={disabled ? -1 : undefined}
|
||||
aria-disabled={disabled}
|
||||
>
|
||||
{item.icon ? (
|
||||
<div className="flex items-center space-x-2">
|
||||
{item.icon}
|
||||
<span>{item.title}</span>
|
||||
</div>
|
||||
) : (
|
||||
item.title
|
||||
)}
|
||||
</Link>
|
||||
{item.children && (
|
||||
<div className="ml-4 space-y-2">
|
||||
{renderItems(item.children)}{" "}
|
||||
{/* Recursively render children */}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
));
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="block lg:hidden">
|
||||
|
@ -94,35 +135,7 @@ export function SidebarNav({
|
|||
)}
|
||||
{...props}
|
||||
>
|
||||
{items.map((item) => (
|
||||
<Link
|
||||
key={hydrateHref(item.href)}
|
||||
href={hydrateHref(item.href)}
|
||||
className={cn(
|
||||
buttonVariants({ variant: "ghost" }),
|
||||
pathname === hydrateHref(item.href) &&
|
||||
!pathname.includes("create")
|
||||
? "bg-accent hover:bg-accent dark:bg-border dark:hover:bg-border"
|
||||
: "hover:bg-transparent hover:underline",
|
||||
"justify-start",
|
||||
disabled && "cursor-not-allowed"
|
||||
)}
|
||||
onClick={
|
||||
disabled ? (e) => e.preventDefault() : undefined
|
||||
}
|
||||
tabIndex={disabled ? -1 : undefined}
|
||||
aria-disabled={disabled}
|
||||
>
|
||||
{item.icon ? (
|
||||
<div className="flex items-center space-x-2">
|
||||
{item.icon}
|
||||
<span>{item.title}</span>
|
||||
</div>
|
||||
) : (
|
||||
item.title
|
||||
)}
|
||||
</Link>
|
||||
))}
|
||||
{renderItems(items)}
|
||||
</nav>
|
||||
</div>
|
||||
);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue