Merge remote-tracking branch 'upstream/main' into fix/chat-ui

This commit is contained in:
Anish Sarkar 2025-12-19 21:42:26 +05:30
commit 5fcaadf60b
30 changed files with 3065 additions and 583 deletions

View file

@ -9,6 +9,7 @@ import type React from "react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { activeChathatUIAtom, activeChatIdAtom } from "@/atoms/chats/ui.atoms";
import { llmPreferencesAtom } from "@/atoms/llm-config/llm-config-query.atoms";
import { myAccessAtom } from "@/atoms/members/members-query.atoms";
import { activeSearchSpaceIdAtom } from "@/atoms/search-spaces/search-space-query.atoms";
import { ChatPanelContainer } from "@/components/chat/ChatPanel/ChatPanelContainer";
import { DashboardBreadcrumb } from "@/components/dashboard-breadcrumb";
@ -17,7 +18,6 @@ import { AppSidebarProvider } from "@/components/sidebar/AppSidebarProvider";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Separator } from "@/components/ui/separator";
import { SidebarInset, SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar";
import { useUserAccess } from "@/hooks/use-rbac";
import { cn } from "@/lib/utils";
export function DashboardClientLayout({
@ -69,7 +69,7 @@ export function DashboardClientLayout({
);
}, [preferences]);
const { access, loading: accessLoading } = useUserAccess(searchSpaceIdNum);
const { data: access = null, isLoading: accessLoading } = useAtomValue(myAccessAtom);
const [hasCheckedOnboarding, setHasCheckedOnboarding] = useState(false);
// Skip onboarding check if we're already on the onboarding page

View file

@ -46,6 +46,15 @@ import { motion } from "motion/react";
import { useParams, useRouter } from "next/navigation";
import { useCallback, useMemo, useState } from "react";
import { toast } from "sonner";
import {
createInviteMutationAtom,
deleteInviteMutationAtom,
} from "@/atoms/invites/invites-mutation.atoms";
import {
deleteMemberMutationAtom,
updateMemberMutationAtom,
} from "@/atoms/members/members-mutation.atoms";
import { membersAtom, myAccessAtom } from "@/atoms/members/members-query.atoms";
import { permissionsAtom } from "@/atoms/permissions/permissions-query.atoms";
import {
createRoleMutationAtom,
@ -107,20 +116,23 @@ import {
} from "@/components/ui/table";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Textarea } from "@/components/ui/textarea";
import type {
CreateInviteRequest,
DeleteInviteRequest,
Invite,
} from "@/contracts/types/invites.types";
import type {
DeleteMembershipRequest,
Membership,
UpdateMembershipRequest,
} from "@/contracts/types/members.types";
import type {
CreateRoleRequest,
DeleteRoleRequest,
Role,
UpdateRoleRequest,
} from "@/contracts/types/roles.types";
import {
type Invite,
type InviteCreate,
type Member,
useInvites,
useMembers,
useUserAccess,
} from "@/hooks/use-rbac";
import { invitesApiService } from "@/lib/apis/invites-api.service";
import { rolesApiService } from "@/lib/apis/roles-api.service";
import { cacheKeys } from "@/lib/query-client/cache-keys";
import { cn } from "@/lib/utils";
@ -154,18 +166,54 @@ export default function TeamManagementPage() {
const searchSpaceId = Number(params.search_space_id);
const [activeTab, setActiveTab] = useState("members");
const { access, loading: accessLoading, hasPermission } = useUserAccess(searchSpaceId);
const { data: access = null, isLoading: accessLoading } = useAtomValue(myAccessAtom);
const hasPermission = useCallback(
(permission: string) => {
if (!access) return false;
if (access.is_owner) return true;
return access.permissions?.includes(permission) ?? false;
},
[access]
);
const {
members,
loading: membersLoading,
fetchMembers,
updateMemberRole,
removeMember,
} = useMembers(searchSpaceId);
data: members = [],
isLoading: membersLoading,
refetch: fetchMembers,
} = useAtomValue(membersAtom);
const { mutateAsync: createRole } = useAtomValue(createRoleMutationAtom);
const { mutateAsync: updateRole } = useAtomValue(updateRoleMutationAtom);
const { mutateAsync: deleteRole } = useAtomValue(deleteRoleMutationAtom);
const { mutateAsync: updateMember } = useAtomValue(updateMemberMutationAtom);
const { mutateAsync: deleteMember } = useAtomValue(deleteMemberMutationAtom);
const { mutateAsync: createInvite } = useAtomValue(createInviteMutationAtom);
const { mutateAsync: revokeInvite } = useAtomValue(deleteInviteMutationAtom);
const handleRevokeInvite = useCallback(
async (inviteId: number): Promise<boolean> => {
const request: DeleteInviteRequest = {
search_space_id: searchSpaceId,
invite_id: inviteId,
};
await revokeInvite(request);
return true;
},
[revokeInvite, searchSpaceId]
);
const handleCreateInvite = useCallback(
async (inviteData: CreateInviteRequest["data"]) => {
const request: CreateInviteRequest = {
search_space_id: searchSpaceId,
data: inviteData,
};
return await createInvite(request);
},
[createInvite, searchSpaceId]
);
const handleUpdateRole = useCallback(
async (roleId: number, data: { permissions?: string[] }): Promise<Role> => {
@ -202,6 +250,32 @@ export default function TeamManagementPage() {
[createRole, searchSpaceId]
);
const handleUpdateMember = useCallback(
async (membershipId: number, roleId: number | null): Promise<Membership> => {
const request: UpdateMembershipRequest = {
search_space_id: searchSpaceId,
membership_id: membershipId,
data: {
role_id: roleId,
},
};
return (await updateMember(request)) as Membership;
},
[updateMember, searchSpaceId]
);
const handleRemoveMember = useCallback(
async (membershipId: number) => {
const request: DeleteMembershipRequest = {
search_space_id: searchSpaceId,
membership_id: membershipId,
};
await deleteMember(request);
return true;
},
[deleteMember, searchSpaceId]
);
const {
data: roles = [],
isLoading: rolesLoading,
@ -212,12 +286,14 @@ export default function TeamManagementPage() {
enabled: !!searchSpaceId,
});
const {
invites,
loading: invitesLoading,
fetchInvites,
createInvite,
revokeInvite,
} = useInvites(searchSpaceId);
data: invites = [],
isLoading: invitesLoading,
refetch: fetchInvites,
} = useQuery({
queryKey: cacheKeys.invites.all(searchSpaceId.toString()),
queryFn: () => invitesApiService.getInvites({ search_space_id: searchSpaceId }),
staleTime: 5 * 60 * 1000,
});
const { data: permissionsData, isLoading: permissionsLoading } = useAtomValue(permissionsAtom);
const permissions = permissionsData?.permissions || [];
@ -387,7 +463,7 @@ export default function TeamManagementPage() {
{activeTab === "invites" && canInvite && (
<CreateInviteDialog
roles={roles}
onCreateInvite={createInvite}
onCreateInvite={handleCreateInvite}
searchSpaceId={searchSpaceId}
/>
)}
@ -404,8 +480,8 @@ export default function TeamManagementPage() {
members={members}
roles={roles}
loading={membersLoading}
onUpdateRole={updateMemberRole}
onRemoveMember={removeMember}
onUpdateRole={handleUpdateMember}
onRemoveMember={handleRemoveMember}
canManageRoles={hasPermission("members:manage_roles")}
canRemove={hasPermission("members:remove")}
/>
@ -427,7 +503,7 @@ export default function TeamManagementPage() {
<InvitesTab
invites={invites}
loading={invitesLoading}
onRevokeInvite={revokeInvite}
onRevokeInvite={handleRevokeInvite}
canRevoke={canInvite}
/>
</TabsContent>
@ -449,10 +525,10 @@ function MembersTab({
canManageRoles,
canRemove,
}: {
members: Member[];
members: Membership[];
roles: Role[];
loading: boolean;
onUpdateRole: (membershipId: number, roleId: number | null) => Promise<Member>;
onUpdateRole: (membershipId: number, roleId: number | null) => Promise<Membership>;
onRemoveMember: (membershipId: number) => Promise<boolean>;
canManageRoles: boolean;
canRemove: boolean;
@ -1016,7 +1092,7 @@ function CreateInviteDialog({
searchSpaceId,
}: {
roles: Role[];
onCreateInvite: (data: InviteCreate) => Promise<Invite>;
onCreateInvite: (data: CreateInviteRequest["data"]) => Promise<Invite>;
searchSpaceId: number;
}) {
const [open, setOpen] = useState(false);
@ -1031,7 +1107,7 @@ function CreateInviteDialog({
const handleCreate = async () => {
setCreating(true);
try {
const data: InviteCreate = {};
const data: CreateInviteRequest["data"] = {};
if (name) data.name = name;
if (roleId && roleId !== "default") data.role_id = Number(roleId);
if (maxUses) data.max_uses = Number(maxUses);

View file

@ -1,5 +1,7 @@
"use client";
import { useQuery } from "@tanstack/react-query";
import { useAtomValue } from "jotai";
import {
AlertCircle,
ArrowRight,
@ -16,7 +18,9 @@ import { motion } from "motion/react";
import Image from "next/image";
import Link from "next/link";
import { useParams, useRouter } from "next/navigation";
import { use, useEffect, useState } from "react";
import { use, useCallback, useEffect, useState } from "react";
import { toast } from "sonner";
import { acceptInviteMutationAtom } from "@/atoms/invites/invites-mutation.atoms";
import { Button } from "@/components/ui/button";
import {
Card,
@ -26,22 +30,48 @@ import {
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { useInviteInfo } from "@/hooks/use-rbac";
import type { AcceptInviteResponse } from "@/contracts/types/invites.types";
import { invitesApiService } from "@/lib/apis/invites-api.service";
import { getBearerToken } from "@/lib/auth-utils";
import { cacheKeys } from "@/lib/query-client/cache-keys";
export default function InviteAcceptPage() {
const params = useParams();
const router = useRouter();
const inviteCode = params.invite_code as string;
const { inviteInfo, loading, acceptInvite } = useInviteInfo(inviteCode);
const { data: inviteInfo = null, isLoading: loading } = useQuery({
queryKey: cacheKeys.invites.info(inviteCode),
enabled: !!inviteCode,
staleTime: 5 * 60 * 1000,
queryFn: async () => {
if (!inviteCode) return null;
return invitesApiService.getInviteInfo({
invite_code: inviteCode,
});
},
});
const { mutateAsync: acceptInviteMutation } = useAtomValue(acceptInviteMutationAtom);
const acceptInvite = useCallback(async () => {
if (!inviteCode) {
toast.error("No invite code provided");
return null;
}
try {
const result = await acceptInviteMutation({ invite_code: inviteCode });
return result;
} catch (err: any) {
toast.error(err.message || "Failed to accept invite");
throw err;
}
}, [inviteCode, acceptInviteMutation]);
const [accepting, setAccepting] = useState(false);
const [accepted, setAccepted] = useState(false);
const [acceptedData, setAcceptedData] = useState<{
search_space_id: number;
search_space_name: string;
role_name: string;
} | null>(null);
const [acceptedData, setAcceptedData] = useState<AcceptInviteResponse | null>(null);
const [error, setError] = useState<string | null>(null);
const [isLoggedIn, setIsLoggedIn] = useState<boolean | null>(null);