From ab68321853e1f51b1b01c0f32b9c1cb0b05483cb Mon Sep 17 00:00:00 2001 From: ad1tyayadav Date: Sat, 4 Oct 2025 11:12:39 +0530 Subject: [PATCH 1/4] Fix #84: Add 'Leave Brain' feature for invited users (frontend + backend) --- nextjs/src/actions/brains.ts | 31 +- nextjs/src/components/Brains/BrainList.tsx | 48 ++- .../src/components/Brains/EditBrainModal.tsx | 342 +++++++++--------- .../src/components/Brains/ShareBrainList.tsx | 49 ++- nextjs/src/types/user.ts | 2 + nextjs/src/utils/constant.ts | 1 + nodejs/src/controller/web/brainController.js | 12 +- nodejs/src/routes/web/brains.js | 1 + nodejs/src/services/brain.js | 37 ++ 9 files changed, 331 insertions(+), 192 deletions(-) diff --git a/nextjs/src/actions/brains.ts b/nextjs/src/actions/brains.ts index 0192349c..73b27822 100644 --- a/nextjs/src/actions/brains.ts +++ b/nextjs/src/actions/brains.ts @@ -263,4 +263,33 @@ export const addDefaultBrainAction = async (workspaceId: string, companyId: stri await revalidateTagging(response, `${REVALIDATE_TAG_NAME.BRAIN}-${companyId}`); return response; -} \ No newline at end of file +} + +export const leaveBrainAction = async (brainId: string) => { + try { + console.log('LEAVE BRAIN ACTION STARTED'); + console.log('Brain ID:', brainId); + + const response = await fetch('/api/brain/leave', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ brainId }), + }); + + if (!response.ok) { + const errorData = await response.json(); + console.error('API Error:', errorData); + throw new Error(errorData.message || `Server returned ${response.status}`); + } + + const result = await response.json(); + console.log('Leave brain success:', result); + return result; + + } catch (error: any) { + console.error('Leave brain action failed:', error); + throw new Error(error.message || 'Failed to leave brain'); + } +}; \ No newline at end of file diff --git a/nextjs/src/components/Brains/BrainList.tsx b/nextjs/src/components/Brains/BrainList.tsx index 3d2bb842..39947290 100644 --- a/nextjs/src/components/Brains/BrainList.tsx +++ b/nextjs/src/components/Brains/BrainList.tsx @@ -36,6 +36,7 @@ import { SetUserData } from '@/types/user'; import { chatMemberListAction } from '@/lib/slices/chat/chatSlice'; import { generateObjectId } from '@/utils/helper'; import Link from 'next/link'; +import LeaveBrainButton from './LeaveBrainButton'; type DefaultEditOptionProps = { onEdit: () => void; @@ -49,6 +50,7 @@ type CommonListProps = { key?: string; currentUser: SetUserData; closeSidebar: () => void; + onBrainLeave?: (brainId: string) => void; } type LinkItemsProps = { @@ -182,7 +184,7 @@ const DefaultEditOption = React.memo( } ); -export const CommonList = ({ b, key, currentUser, closeSidebar }: CommonListProps) => { +export const CommonList = ({ b, key, currentUser, closeSidebar, onBrainLeave }: CommonListProps) => { const dispatch = useDispatch(); const router = useRouter(); @@ -204,6 +206,8 @@ export const CommonList = ({ b, key, currentUser, closeSidebar }: CommonListProp [b?._id, brainId] ); + const isOwner = b.user.id === currentUser._id; + const handleEditClick = () => { setIsEditing(true); setEditedTitle(b.title); // Reset editedTitle to the current title @@ -294,9 +298,8 @@ export const CommonList = ({ b, key, currentUser, closeSidebar }: CommonListProp return ( <> ) : null} + + {/* Show options for non-default, non-general brains */} {b?.slug != `default-brain-${currentUser?._id}` && - b?.slug !== GENERAL_BRAIN_SLUG && - ((currentUser?.roleCode === ROLE_TYPE.USER && - b.user.id === currentUser?._id) || - currentUser?.roleCode !== ROLE_TYPE.USER) && ( - handleEditBrain(b)} - handleDeleteBrain={() => handleDeleteBrain(b)} - isDeletePending={isDeletePending} - /> + b?.slug !== GENERAL_BRAIN_SLUG && ( + <> + {currentUser?.roleCode === ROLE_TYPE.USER && !isOwner && ( +
e.stopPropagation()}> + +
+ )} + + {((currentUser?.roleCode === ROLE_TYPE.USER && isOwner) || + currentUser?.roleCode !== ROLE_TYPE.USER) && ( + handleEditBrain(b)} + handleDeleteBrain={() => handleDeleteBrain(b)} + isDeletePending={isDeletePending} + /> + )} + )} diff --git a/nextjs/src/components/Brains/EditBrainModal.tsx b/nextjs/src/components/Brains/EditBrainModal.tsx index 7181dddd..8ed96cbc 100644 --- a/nextjs/src/components/Brains/EditBrainModal.tsx +++ b/nextjs/src/components/Brains/EditBrainModal.tsx @@ -27,11 +27,10 @@ import { useTeams } from '@/hooks/team/useTeams'; import GroupIcon from '@/icons/GroupIcon'; import RemoveIcon from '@/icons/RemoveIcon'; import useServerAction from '@/hooks/common/useServerActions'; -import { addBrainMemberAction, deleteBrainAction, deleteShareTeamToBrainAction, removeBrainMemberAction, shareTeamToBrainAction, updateBrainAction } from '@/actions/brains'; +import { addBrainMemberAction, deleteBrainAction, deleteShareTeamToBrainAction, leaveBrainAction, removeBrainMemberAction, shareTeamToBrainAction, updateBrainAction } from '@/actions/brains'; import Toast from '@/utils/toast'; import ExitIcon from '@/icons/ExitIcon'; -import TooltipIcon from '@/icons/TooltipIcon'; -import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; +import LeaveBrainButton from './LeaveBrainButton'; const AddNewMemberModal = ({ brain, @@ -72,7 +71,7 @@ const AddNewMemberModal = ({ const response = await addBrainMember(brain?._id, members, brain?.workspaceId); Toast(response?.message); onClose(); - if(response) refetchMemebrs(); + if (response) refetchMemebrs(); setMemberOptions([]); }; @@ -187,12 +186,12 @@ const AddTeamMemberModal = ({ Toast(response?.message); onClose(); setTeamOptions([]); - if(response) refetchTeams(); + if (response) refetchTeams(); }; useEffect(() => { reset(); - if(open){ + if (open) { getTeams({ search: '', pagination: false }); } }, [open]); @@ -298,19 +297,17 @@ const AddTeamMemberModal = ({ ); }; -const AboutBrainDetails = ({ brain, isOwner, onLeaveBrain, onDeleteBrain }: any) => { +const AboutBrainDetails = ({ brain, isOwner, onDeleteBrain }: any) => { return (
- {/* Leave Chat Start*/} {!isOwner && ( -
- - Leave Brain -
+ )} + {isOwner && (
)} - {/* Leave Chat End*/}
); }; @@ -362,13 +358,13 @@ const MemberItem = ({
- + {member.role == ROLE_TYPE.OWNER && ( {member.role} )} - + {(isRemoval && member.role != ROLE_TYPE.OWNER) && ( handleRemoveMember(member.user.id) @@ -387,8 +383,8 @@ const TeamItem = ({ team, handleRemoveTeam, brain }: any) => { return (
- - + +

{team.teamName} @@ -426,7 +422,7 @@ const TeamItem = ({ team, handleRemoveTeam, brain }: any) => {

{ - + handleRemoveTeam(team?.id?._id) }> @@ -437,7 +433,7 @@ const TeamItem = ({ team, handleRemoveTeam, brain }: any) => { ); }; -const EditBrainModal = ({ open, closeModal, brain }: any) => { +const EditBrainModal = ({ open, closeModal, brain }): any => { const currentUser = getCurrentUser(); const isOwner = currentUser?._id == brain?.user?.id; @@ -489,11 +485,11 @@ const EditBrainModal = ({ open, closeModal, brain }: any) => { role: ROLE_TYPE.ADMIN, }, ...brainMembers, - ].filter((m) => {return regex.test(m.user.email) && m.role!=ROLE_TYPE.ADMIN}) + ].filter((m) => { return regex.test(m.user.email) && m.role != ROLE_TYPE.ADMIN }) ); - - setTeamList(brainAddedTeam?.filter((currTeam)=>regex.test(currTeam.id.teamName))) + + setTeamList(brainAddedTeam?.filter((currTeam) => regex.test(currTeam.id.teamName))) }, [filter]); const refetchMemebrs = () => { @@ -517,7 +513,7 @@ const EditBrainModal = ({ open, closeModal, brain }: any) => { refetchMemebrs(); }; - const handleRemoveTeam = async(value) => { + const handleRemoveTeam = async (value) => { const response = await deleteShareTeamToBrain( brain?.workspaceId, brain?.companyId, @@ -528,7 +524,7 @@ const EditBrainModal = ({ open, closeModal, brain }: any) => { refetchTeams(); }; - const onLeaveBrain = () => {}; + const onLeaveBrain = () => { }; const onDeleteBrain = async () => { const data = { @@ -558,20 +554,20 @@ const EditBrainModal = ({ open, closeModal, brain }: any) => { setIsEditingInstruction(false); }; - const totalMembers = (brainAddedTeam, memberList ) => { - - if(brainAddedTeam?.length){ - - return brainAddedTeam?.reduce((acc, currTeam) => { - acc += currTeam?.id?.teamUsers?.length || 0; - return acc; - }, 0) + memberList?.length - } - else{ - return memberList.length - } - - + const totalMembers = (brainAddedTeam, memberList) => { + + if (brainAddedTeam?.length) { + + return brainAddedTeam?.reduce((acc, currTeam) => { + acc += currTeam?.id?.teamUsers?.length || 0; + return acc; + }, 0) + memberList?.length + } + else { + return memberList.length + } + + }; useEffect(() => { @@ -600,20 +596,20 @@ const EditBrainModal = ({ open, closeModal, brain }: any) => { {brain.title}
- - Created By: - {`${displayName(brain?.user)} on ${dateDisplay( - brain?.createdAt - )}`} - -
- -
+ + Created By: + {`${displayName(brain?.user)} on ${dateDisplay( + brain?.createdAt + )}`} + +
+ +
@@ -664,131 +660,137 @@ const EditBrainModal = ({ open, closeModal, brain }: any) => {
)}
- + {brain.isShare && ( - <> -
-
- { - setTimeout(() => { - setFilter({ - ...filter, - search: e.target - .value, - }); - }, 1000); - }} - /> - - - -
- -
- {/* Add Member start */} - {((currentUser.roleCode === - ROLE_TYPE.USER && - brain?.user?.id === + <> +
+
+ { + setTimeout(() => { + setFilter({ + ...filter, + search: e.target + .value, + }); + }, 1000); + }} + /> + + + +
+ {/* Add Member start */} + {((currentUser.roleCode === + ROLE_TYPE.USER && + brain?.user?.id === currentUser._id) || - currentUser.roleCode !== + currentUser.roleCode !== ROLE_TYPE.USER) && ( -
- - setAddMemberModal( - true - ) - } - > - + +
+ + setAddMemberModal( + true + ) + } + > + + + Add + Member + + + + + setAddTeamModal( + true + ) + } + > + + + Add a Team + + +
+
+ + + )} + {/* Add Member End */} +
+ +
+ Members{' '} + + {totalMembers( + teamList, + memberList + )} + +
+ +
+ + {/* Member List Start */} +
+ {memberList?.map((nm) => ( + - - Add Member - - - - - setAddTeamModal( - true - ) - } - > - ( + - - Add a Team - - + ))}
- )} - {/* Add Member End */} - -
- Members{' '} - - {totalMembers( - teamList, - memberList - )} - -
- -
- - {/* Member List Start */} -
- {memberList?.map((nm) => ( - - ))} - {teamList?.map((team) => ( - - ))} + {/* Member List End */}
- {/* Member List End */} -
)}
diff --git a/nextjs/src/components/Brains/ShareBrainList.tsx b/nextjs/src/components/Brains/ShareBrainList.tsx index df0fc05f..95a1b16c 100644 --- a/nextjs/src/components/Brains/ShareBrainList.tsx +++ b/nextjs/src/components/Brains/ShareBrainList.tsx @@ -7,7 +7,7 @@ import { decryptedPersist } from '@/utils/helper'; import { WORKSPACE } from '@/utils/localstorage'; import { useDispatch, useSelector } from 'react-redux'; import { CommonList } from './BrainList'; -import { useMemo } from 'react'; +import { useMemo, useState, useEffect } from 'react'; import { AllBrainListType } from '@/types/brain'; import { WorkspaceListType } from '@/types/workspace'; import { useSidebar } from '@/context/SidebarContext'; @@ -25,24 +25,60 @@ const ShareBrainList = ({ brainList, workspaceFirst }: ShareBrainListProps) => { (store: RootState) => store.workspacelist.selected ); const currentUser = useMemo(() => getCurrentUser(), []); + + const [leftBrains, setLeftBrains] = useState([]); + + useEffect(() => { + if (typeof window !== 'undefined') { + const stored = sessionStorage.getItem('leftBrains'); + if (stored) { + setLeftBrains(JSON.parse(stored)); + } + } + }, []); if (!selectedWorkSpace || !selectedWorkSpace._id) { const persistWorkspace = decryptedPersist(WORKSPACE); const setData = persistWorkspace ? persistWorkspace : workspaceFirst; - dispatch(setSelectedWorkSpaceAction(setData)); + if (setData) { + dispatch(setSelectedWorkSpaceAction(setData)); + } + return null; } const selectedWorkSpaceBrainList = brainList.find( (brain) => brain._id.toString() === selectedWorkSpace._id.toString() ); - const shareBrainList = selectedWorkSpaceBrainList?.brains.filter( - (brain) => brain.isShare - ); + if (!selectedWorkSpaceBrainList) { + return null; + } - const dispatchPayload = shareBrainList ? shareBrainList : []; + const shareBrainList = selectedWorkSpaceBrainList.brains.filter( + (brain) => { + const isBrainOwner = brain.user.id === currentUser._id; + const userLeftThisBrain = leftBrains.includes(brain._id); + + return brain.isShare && (isBrainOwner || !userLeftThisBrain); + } + ); + const dispatchPayload = shareBrainList; dispatch(cacheShareList(dispatchPayload)); + + const handleBrainLeave = (brainId: string) => { + const brainToLeave = selectedWorkSpaceBrainList.brains.find(b => b._id === brainId); + + if (brainToLeave && brainToLeave.user.id !== currentUser._id) { + const updatedLeftBrains = [...leftBrains, brainId]; + setLeftBrains(updatedLeftBrains); + + if (typeof window !== 'undefined') { + sessionStorage.setItem('leftBrains', JSON.stringify(updatedLeftBrains)); + } + } + }; + return ( <> {shareBrainList?.length > 0 && ( @@ -53,6 +89,7 @@ const ShareBrainList = ({ brainList, workspaceFirst }: ShareBrainListProps) => { key={b._id} currentUser={currentUser} closeSidebar={closeSidebar} + onBrainLeave={handleBrainLeave} /> ))}
diff --git a/nextjs/src/types/user.ts b/nextjs/src/types/user.ts index 3093452f..c21266e8 100644 --- a/nextjs/src/types/user.ts +++ b/nextjs/src/types/user.ts @@ -74,6 +74,8 @@ export type SetIronSessionData = SetUserData & { } export type SessionUserType = { + id: string; + token: { Authorization: string; }; email: string; access_token: string; refresh_token: string; diff --git a/nextjs/src/utils/constant.ts b/nextjs/src/utils/constant.ts index 11f55efa..d6e76624 100644 --- a/nextjs/src/utils/constant.ts +++ b/nextjs/src/utils/constant.ts @@ -82,6 +82,7 @@ export const MODULE_ACTIONS = { EXPORT: 'export', SIGNUP: 'register', SEND_MAIL: 'sendMail', + LEAVE: 'leave', FORGOT_PASSWORD: 'forgotPassword', RESET_PASSWORD: 'resetPassword', SEND_MAIL_NOTIFICATION: 'sendMailNotification', diff --git a/nodejs/src/controller/web/brainController.js b/nodejs/src/controller/web/brainController.js index 1547865d..c83d2f43 100644 --- a/nodejs/src/controller/web/brainController.js +++ b/nodejs/src/controller/web/brainController.js @@ -121,6 +121,15 @@ const workspaceWiseList = catchAsync(async (req, res) => { return util.recordNotFound(null, res); }) +const leaveBrain = catchAsync(async (req, res) => { + const result = await brainService.leaveBrain(req); + if (result) { + res.message = _localize('module.leave', req, BRAIN); + return util.successResponse(result, res); + } + return util.failureResponse(_localize('module.leaveError', req, BRAIN), res); +}) + module.exports = { createBrain, updateBrain, @@ -134,6 +143,7 @@ module.exports = { getAllBrainUser, restoreBrain, deleteAllBrain, - workspaceWiseList + workspaceWiseList, + leaveBrain } diff --git a/nodejs/src/routes/web/brains.js b/nodejs/src/routes/web/brains.js index 0ec1f09b..a41d53c5 100644 --- a/nodejs/src/routes/web/brains.js +++ b/nodejs/src/routes/web/brains.js @@ -18,5 +18,6 @@ router.post('/share-doc', validate(shareDocKeys), authentication, brainControlle router.post('/share/list', authentication, checkPromptLimit, brainController.shareList); router.post('/restore/:id', authentication, brainController.restoreBrain); router.post('/list-all', authentication, checkPromptLimit, brainController.workspaceWiseList); +router.post('/:id/leave', authentication, brainController.leaveBrain); module.exports = router; \ No newline at end of file diff --git a/nodejs/src/services/brain.js b/nodejs/src/services/brain.js index b943ad05..2e03df72 100644 --- a/nodejs/src/services/brain.js +++ b/nodejs/src/services/brain.js @@ -554,6 +554,42 @@ async function getBrainStatus(brains) { } } +const leaveBrain = async (req) => { + try { + const { id: brainId } = req.params; + const userId = req.user.id; + + const brain = await Brain.findById(brainId); + if (!brain) { + throw new Error(_localize('module.notFound', req, 'Brain')); + } + + if (brain.user.id.toString() === userId) { + throw new Error(_localize('brain.creatorCannotLeave', req)); + } + + const shareBrain = await ShareBrain.findOne({ + 'brain.id': brainId, + 'user.id': userId + }); + + if (!shareBrain) { + throw new Error(_localize('brain.notMember', req)); + } + + await ShareBrain.deleteOne({ + 'brain.id': brainId, + 'user.id': userId + }); + + await removeBrainChatMember(brainId, userId); + + return { message: 'Successfully left the brain' }; + } catch (error) { + handleError(error, 'Error - leaveBrain'); + } +} + module.exports = { createBrain, updateBrain, @@ -576,5 +612,6 @@ module.exports = { getBrainStatus, getGeneralBrain, defaultGeneralBrainMember, + leaveBrain } \ No newline at end of file From 93b09925b47671a5fdbe89a3762152306adfd79d Mon Sep 17 00:00:00 2001 From: ad1tyayadav Date: Sat, 4 Oct 2025 11:01:26 +0530 Subject: [PATCH 2/4] Fix #84: Add 'Leave Brain' feature for invited users with confirmation modal --- nextjs/src/app/api/brain/leave/route.ts | 83 ++++++++ .../src/components/Brains/EditBrainModal.tsx | 192 +++++++++--------- .../components/Brains/LeaveBrainButton.tsx | 85 ++++++++ nextjs/src/hooks/brains/useLeaveBrain.ts | 62 ++++++ 4 files changed, 326 insertions(+), 96 deletions(-) create mode 100644 nextjs/src/app/api/brain/leave/route.ts create mode 100644 nextjs/src/components/Brains/LeaveBrainButton.tsx create mode 100644 nextjs/src/hooks/brains/useLeaveBrain.ts diff --git a/nextjs/src/app/api/brain/leave/route.ts b/nextjs/src/app/api/brain/leave/route.ts new file mode 100644 index 00000000..8c5f7244 --- /dev/null +++ b/nextjs/src/app/api/brain/leave/route.ts @@ -0,0 +1,83 @@ +import { NextResponse } from 'next/server'; +import { getSession } from '@/config/withSession'; + +export async function POST(request) { + try { + const { brainId } = await request.json(); + console.log('Received brainId:', brainId); + + if (!brainId) { + return NextResponse.json( + { + status: 400, + code: 'BRAIN_ID_REQUIRED', + message: 'Brain ID is required', + data: {} + }, + { status: 400 } + ); + } + + const session = await getSession(); + console.log('Session user exists:', !!session.user); + + if (!session.user) { + return NextResponse.json( + { + status: 401, + code: 'TOKEN_NOT_FOUND', + message: 'You are not allowed to access this platform', + data: {} + }, + { status: 401 } + ); + } + + const jwtToken = session.user.access_token; + console.log('JWT Token found:', !!jwtToken); + + if (!jwtToken) { + return NextResponse.json( + { + status: 401, + code: 'TOKEN_NOT_FOUND', + message: 'You are not allowed to access this platform', + data: {} + }, + { status: 401 } + ); + } + + const response = await fetch(`http://nodejs:4050/napi/v1/web/brain/${brainId}/leave`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `jwt ${jwtToken}`, + }, + }); + + console.log('📡 Backend response status:', response.status); + + if (!response.ok) { + const errorData = await response.json(); + console.log('Backend error:', errorData); + return NextResponse.json(errorData, { status: response.status }); + } + + const data = await response.json(); + console.log('Backend success:', data); + return NextResponse.json(data); + + } catch (error) { + console.error('Proxy error:', error); + return NextResponse.json( + { + status: 500, + code: 'INTERNAL_ERROR', + message: 'Internal server error: ' + error.message, + data: {} + }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/nextjs/src/components/Brains/EditBrainModal.tsx b/nextjs/src/components/Brains/EditBrainModal.tsx index 8ed96cbc..072805a0 100644 --- a/nextjs/src/components/Brains/EditBrainModal.tsx +++ b/nextjs/src/components/Brains/EditBrainModal.tsx @@ -595,21 +595,21 @@ const EditBrainModal = ({ open, closeModal, brain }): any => { /> {brain.title} -
- - Created By: - {`${displayName(brain?.user)} on ${dateDisplay( - brain?.createdAt - )}`} - -
- -
+
+ + Created By: + {`${displayName(brain?.user)} on ${dateDisplay( + brain?.createdAt + )}`} + +
+ +
@@ -662,90 +662,90 @@ const EditBrainModal = ({ open, closeModal, brain }): any => {
{brain.isShare && ( - <> -
-
- { - setTimeout(() => { - setFilter({ - ...filter, - search: e.target - .value, - }); - }, 1000); - }} - /> - - - -
- {/* Add Member start */} - {((currentUser.roleCode === - ROLE_TYPE.USER && - brain?.user?.id === + <> +
+
+ { + setTimeout(() => { + setFilter({ + ...filter, + search: e.target + .value, + }); + }, 1000); + }} + /> + + + +
+ {/* Add Member start */} + {((currentUser.roleCode === + ROLE_TYPE.USER && + brain?.user?.id === currentUser._id) || currentUser.roleCode !== ROLE_TYPE.USER) && ( - - - -
- - setAddMemberModal( - true - ) - } - > - - - Add - Member - - - - - setAddTeamModal( - true - ) - } - > - - - Add a Team - - -
-
-
- - )} - {/* Add Member End */} -
+ + + +
+ + setAddMemberModal( + true + ) + } + > + + + Add + Member + + + + + setAddTeamModal( + true + ) + } + > + + + Add a Team + + +
+
+
+ + )} + {/* Add Member End */} +
void; + buttonClassName?: string; + iconClassName?: string; + hideLabel?: boolean; +} + +const LeaveBrainButton: React.FC = ({ + brainId, + brainTitle, + onLeaveSuccess, + buttonClassName = "", + iconClassName = "", + hideLabel = false, +}) => { + const [showConfirmation, setShowConfirmation] = useState(false); + const { leaveBrain, isLeaving } = useLeaveBrain({ onLeaveSuccess }); + + const handleLeave = () => { + leaveBrain(brainId); + setShowConfirmation(false); + }; + + return ( + <> +
setShowConfirmation(true)} + className={`cursor-pointer flex items-center gap-x-1 text-red-600 hover:opacity-80 transition-opacity ${buttonClassName}`} + > + + {!hideLabel && Leave Brain} +
+ + {showConfirmation && ( +
+
+

Leave Brain

+

+ Are you sure you want to leave{" "} + "{brainTitle}"?
+ You will lose access to all its content. +

+ +
+ + + +
+
+
+ )} + + ); +}; + +export default LeaveBrainButton; \ No newline at end of file diff --git a/nextjs/src/hooks/brains/useLeaveBrain.ts b/nextjs/src/hooks/brains/useLeaveBrain.ts new file mode 100644 index 00000000..e075dff8 --- /dev/null +++ b/nextjs/src/hooks/brains/useLeaveBrain.ts @@ -0,0 +1,62 @@ +import { useState } from 'react'; +import Toast from '@/utils/toast'; + +export const useLeaveBrain = ({ onLeaveSuccess }: { onLeaveSuccess?: (brainId: string) => void } = {}) => { + const [isLeaving, setIsLeaving] = useState(false); + const [leftBrains, setLeftBrains] = useState(() => { + if (typeof window !== 'undefined') { + return JSON.parse(sessionStorage.getItem('leftBrains') || '[]'); + } + return []; + }); + + const leaveBrain = async (brainId: string) => { + setIsLeaving(true); + try { + const response = await fetch('/api/brain/leave', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + credentials: 'include', + body: JSON.stringify({ brainId }), + }); + + const result = await response.json(); + + if (response.ok) { + Toast(result?.data?.message || 'Successfully left the brain'); + + const updatedLeftBrains = [...leftBrains, brainId]; + setLeftBrains(updatedLeftBrains); + + if (typeof window !== 'undefined') { + sessionStorage.setItem('leftBrains', JSON.stringify(updatedLeftBrains)); + } + + onLeaveSuccess?.(brainId); + } else { + throw new Error(result?.message || `Failed to leave brain`); + } + } catch (error: any) { + console.error('Error leaving brain:', error); + Toast(error?.message || 'Error leaving brain'); + } finally { + setIsLeaving(false); + } + }; + + const isBrainLeft = (brainId: string) => leftBrains.includes(brainId); + + const resetLeftBrain = (brainId: string) => { + const updatedLeftBrains = leftBrains.filter(id => id !== brainId); + setLeftBrains(updatedLeftBrains); + if (typeof window !== 'undefined') { + sessionStorage.setItem('leftBrains', JSON.stringify(updatedLeftBrains)); + } + }; + + return { leaveBrain, isLeaving, isBrainLeft, resetLeftBrain, leftBrains }; +}; + +export default useLeaveBrain; \ No newline at end of file From b16cbf7e0ad69c71c3d3d2aadd770216b6ce5b59 Mon Sep 17 00:00:00 2001 From: ad1tyayadav Date: Tue, 7 Oct 2025 23:59:44 +0530 Subject: [PATCH 3/4] fix: applied requested changes from review --- nextjs/src/actions/brains.ts | 53 ++++---- nextjs/src/app/api/brain/leave/route.ts | 83 ------------- .../components/Brains/LeaveBrainButton.tsx | 99 +++++++++------ nextjs/src/hooks/brains/useLeaveBrain.ts | 114 +++++++++--------- nextjs/src/types/user.ts | 2 - nodejs/resources/lang/en/common.json | 5 +- nodejs/src/services/brain.js | 6 +- 7 files changed, 153 insertions(+), 209 deletions(-) delete mode 100644 nextjs/src/app/api/brain/leave/route.ts diff --git a/nextjs/src/actions/brains.ts b/nextjs/src/actions/brains.ts index 73b27822..8967eaeb 100644 --- a/nextjs/src/actions/brains.ts +++ b/nextjs/src/actions/brains.ts @@ -266,30 +266,33 @@ export const addDefaultBrainAction = async (workspaceId: string, companyId: stri } export const leaveBrainAction = async (brainId: string) => { - try { - console.log('LEAVE BRAIN ACTION STARTED'); - console.log('Brain ID:', brainId); - - const response = await fetch('/api/brain/leave', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ brainId }), - }); - - if (!response.ok) { - const errorData = await response.json(); - console.error('API Error:', errorData); - throw new Error(errorData.message || `Server returned ${response.status}`); - } - - const result = await response.json(); - console.log('Leave brain success:', result); - return result; - - } catch (error: any) { - console.error('Leave brain action failed:', error); - throw new Error(error.message || 'Failed to leave brain'); + const sessionUser = await getSessionUser(); + + if (!brainId) { + return { + status: 400, + code: 'BRAIN_ID_REQUIRED', + message: '', + data: {} + }; + } + + if (!sessionUser?._id) { + return { + status: 401, + code: 'TOKEN_NOT_FOUND', + message: '', + data: {} + }; } + + const response = await serverApi({ + action: MODULE_ACTIONS.UNSHARE, + parameters: [brainId], + data: { user_id: sessionUser._id } + }); + + await revalidateTagging(response, `${REVALIDATE_TAG_NAME.BRAIN}-${sessionUser.companyId}`); + + return response; }; \ No newline at end of file diff --git a/nextjs/src/app/api/brain/leave/route.ts b/nextjs/src/app/api/brain/leave/route.ts deleted file mode 100644 index 8c5f7244..00000000 --- a/nextjs/src/app/api/brain/leave/route.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { NextResponse } from 'next/server'; -import { getSession } from '@/config/withSession'; - -export async function POST(request) { - try { - const { brainId } = await request.json(); - console.log('Received brainId:', brainId); - - if (!brainId) { - return NextResponse.json( - { - status: 400, - code: 'BRAIN_ID_REQUIRED', - message: 'Brain ID is required', - data: {} - }, - { status: 400 } - ); - } - - const session = await getSession(); - console.log('Session user exists:', !!session.user); - - if (!session.user) { - return NextResponse.json( - { - status: 401, - code: 'TOKEN_NOT_FOUND', - message: 'You are not allowed to access this platform', - data: {} - }, - { status: 401 } - ); - } - - const jwtToken = session.user.access_token; - console.log('JWT Token found:', !!jwtToken); - - if (!jwtToken) { - return NextResponse.json( - { - status: 401, - code: 'TOKEN_NOT_FOUND', - message: 'You are not allowed to access this platform', - data: {} - }, - { status: 401 } - ); - } - - const response = await fetch(`http://nodejs:4050/napi/v1/web/brain/${brainId}/leave`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `jwt ${jwtToken}`, - }, - }); - - console.log('📡 Backend response status:', response.status); - - if (!response.ok) { - const errorData = await response.json(); - console.log('Backend error:', errorData); - return NextResponse.json(errorData, { status: response.status }); - } - - const data = await response.json(); - console.log('Backend success:', data); - return NextResponse.json(data); - - } catch (error) { - console.error('Proxy error:', error); - return NextResponse.json( - { - status: 500, - code: 'INTERNAL_ERROR', - message: 'Internal server error: ' + error.message, - data: {} - }, - { status: 500 } - ); - } -} \ No newline at end of file diff --git a/nextjs/src/components/Brains/LeaveBrainButton.tsx b/nextjs/src/components/Brains/LeaveBrainButton.tsx index fa8d285b..46ea1be6 100644 --- a/nextjs/src/components/Brains/LeaveBrainButton.tsx +++ b/nextjs/src/components/Brains/LeaveBrainButton.tsx @@ -1,6 +1,15 @@ import React, { useState } from "react"; import useLeaveBrain from "@/hooks/brains/useLeaveBrain"; import ExitIcon from "@/icons/ExitIcon"; +import { + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, + DialogClose, +} from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; interface LeaveBrainButtonProps { brainId: string; @@ -19,65 +28,75 @@ const LeaveBrainButton: React.FC = ({ iconClassName = "", hideLabel = false, }) => { - const [showConfirmation, setShowConfirmation] = useState(false); + const [open, setOpen] = useState(false); const { leaveBrain, isLeaving } = useLeaveBrain({ onLeaveSuccess }); const handleLeave = () => { leaveBrain(brainId); - setShowConfirmation(false); }; return ( <>
setShowConfirmation(true)} - className={`cursor-pointer flex items-center gap-x-1 text-red-600 hover:opacity-80 transition-opacity ${buttonClassName}`} + onClick={() => setOpen(true)} + className={`cursor-pointer flex items-center gap-x-1 text-red hover:opacity-80 transition-opacity ${buttonClassName}`} > {!hideLabel && Leave Brain}
- - {showConfirmation && ( -
-
-

Leave Brain

-

- Are you sure you want to leave{" "} - "{brainTitle}"?
- You will lose access to all its content. -

- -
- - - -
-
-
- )} + + + + + + ); }; diff --git a/nextjs/src/hooks/brains/useLeaveBrain.ts b/nextjs/src/hooks/brains/useLeaveBrain.ts index e075dff8..4851730a 100644 --- a/nextjs/src/hooks/brains/useLeaveBrain.ts +++ b/nextjs/src/hooks/brains/useLeaveBrain.ts @@ -1,62 +1,62 @@ -import { useState } from 'react'; -import Toast from '@/utils/toast'; - -export const useLeaveBrain = ({ onLeaveSuccess }: { onLeaveSuccess?: (brainId: string) => void } = {}) => { - const [isLeaving, setIsLeaving] = useState(false); - const [leftBrains, setLeftBrains] = useState(() => { - if (typeof window !== 'undefined') { - return JSON.parse(sessionStorage.getItem('leftBrains') || '[]'); - } - return []; - }); - - const leaveBrain = async (brainId: string) => { - setIsLeaving(true); - try { - const response = await fetch('/api/brain/leave', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - credentials: 'include', - body: JSON.stringify({ brainId }), - }); - - const result = await response.json(); - - if (response.ok) { - Toast(result?.data?.message || 'Successfully left the brain'); - - const updatedLeftBrains = [...leftBrains, brainId]; - setLeftBrains(updatedLeftBrains); - - if (typeof window !== 'undefined') { - sessionStorage.setItem('leftBrains', JSON.stringify(updatedLeftBrains)); - } - - onLeaveSuccess?.(brainId); - } else { - throw new Error(result?.message || `Failed to leave brain`); - } - } catch (error: any) { - console.error('Error leaving brain:', error); - Toast(error?.message || 'Error leaving brain'); - } finally { - setIsLeaving(false); - } - }; - - const isBrainLeft = (brainId: string) => leftBrains.includes(brainId); - - const resetLeftBrain = (brainId: string) => { - const updatedLeftBrains = leftBrains.filter(id => id !== brainId); +import { useState } from "react"; +import Toast from "@/utils/toast"; +import { leaveBrainAction } from "@/actions/brains"; + +export const useLeaveBrain = ({ + onLeaveSuccess, +}: { onLeaveSuccess?: (brainId: string) => void } = {}) => { + const [isLeaving, setIsLeaving] = useState(false); + const [leftBrains, setLeftBrains] = useState(() => { + if (typeof window !== "undefined") { + return JSON.parse(sessionStorage.getItem("leftBrains") || "[]"); + } + return []; + }); + + const leaveBrain = async (brainId: string) => { + setIsLeaving(true); + try { + const result = await leaveBrainAction(brainId); + + if (result?.status === 200) { + Toast(result.message); + + const updatedLeftBrains = [...leftBrains, brainId]; setLeftBrains(updatedLeftBrains); - if (typeof window !== 'undefined') { - sessionStorage.setItem('leftBrains', JSON.stringify(updatedLeftBrains)); + + if (typeof window !== "undefined") { + sessionStorage.setItem( + "leftBrains", + JSON.stringify(updatedLeftBrains) + ); } - }; - return { leaveBrain, isLeaving, isBrainLeft, resetLeftBrain, leftBrains }; + onLeaveSuccess?.(brainId); + + setTimeout(() => { + window.location.href = "/"; + }, 1000); + } else { + throw new Error(result?.message); + } + } catch (error: any) { + Toast(error?.message); + } finally { + setIsLeaving(false); + } + }; + + const isBrainLeft = (brainId: string) => leftBrains.includes(brainId); + + const resetLeftBrain = (brainId: string) => { + const updatedLeftBrains = leftBrains.filter((id) => id !== brainId); + setLeftBrains(updatedLeftBrains); + if (typeof window !== "undefined") { + sessionStorage.setItem("leftBrains", JSON.stringify(updatedLeftBrains)); + } + }; + + return { leaveBrain, isLeaving, isBrainLeft, resetLeftBrain, leftBrains }; }; -export default useLeaveBrain; \ No newline at end of file +export default useLeaveBrain; diff --git a/nextjs/src/types/user.ts b/nextjs/src/types/user.ts index c21266e8..3093452f 100644 --- a/nextjs/src/types/user.ts +++ b/nextjs/src/types/user.ts @@ -74,8 +74,6 @@ export type SetIronSessionData = SetUserData & { } export type SessionUserType = { - id: string; - token: { Authorization: string; }; email: string; access_token: string; refresh_token: string; diff --git a/nodejs/resources/lang/en/common.json b/nodejs/resources/lang/en/common.json index f34a80bb..48b77e13 100644 --- a/nodejs/resources/lang/en/common.json +++ b/nodejs/resources/lang/en/common.json @@ -43,7 +43,10 @@ "unfavorite": "{module} removed from Favourites", "unfavoriteError": "Error occured while unfavoriting {module}", "credit_expired": "Your credit limit has been reached. Please contact your administrator to increase your credit limit.", - "trial_expired": "Your trial period has expired. Please upgrade to a paid plan to continue using our services." + "trial_expired": "Your trial period has expired. Please upgrade to a paid plan to continue using our services.", + "leave": "You have left the {module} successfully", + "leaveError": "Error occurred while leaving the {module}" + }, "ai": { "open_ai_billing_error": "You exceeded your current quota, please check your plan and billing details.", diff --git a/nodejs/src/services/brain.js b/nodejs/src/services/brain.js index 2e03df72..76d44af2 100644 --- a/nodejs/src/services/brain.js +++ b/nodejs/src/services/brain.js @@ -584,7 +584,11 @@ const leaveBrain = async (req) => { await removeBrainChatMember(brainId, userId); - return { message: 'Successfully left the brain' }; + return { + status: 200, + message: _localize('module.leave', req, 'Brain'), + data: {} + }; } catch (error) { handleError(error, 'Error - leaveBrain'); } From 4ff65c61a3e30712967de17d78c194b5049aad05 Mon Sep 17 00:00:00 2001 From: ad1tyayadav Date: Mon, 27 Oct 2025 14:37:37 +0530 Subject: [PATCH 4/4] Restore convertToSharedAction and ConvertToSharedModal --- nextjs/src/actions/brains.ts | 38 ++- nextjs/src/components/Brains/BrainList.tsx | 207 ++++++++------ .../Brains/ConvertToSharedModal.tsx | 252 ++++++++++++++++++ nextjs/src/utils/constant.ts | 15 +- 4 files changed, 427 insertions(+), 85 deletions(-) create mode 100644 nextjs/src/components/Brains/ConvertToSharedModal.tsx diff --git a/nextjs/src/actions/brains.ts b/nextjs/src/actions/brains.ts index 8967eaeb..82f9eda1 100644 --- a/nextjs/src/actions/brains.ts +++ b/nextjs/src/actions/brains.ts @@ -1,5 +1,5 @@ 'use server'; -import { DEFAULT_SORT, MODULE_ACTIONS, MODULES, REVALIDATE_TAG_NAME, ROLE_TYPE } from '@/utils/constant'; +import { DEFAULT_SORT, MODULE_ACTIONS, MODULES, REVALIDATE_TAG_NAME, ROLE_TYPE, BRAIN_ID_REQUIRED } from '@/utils/constant'; import { revalidateTagging, serverApi } from './serverApi'; import { getSessionUser } from '@/utils/handleAuth'; import { FormatUserType, ObjectType } from '@/types/common'; @@ -18,6 +18,7 @@ type CreateBrainActionData = { workspaceId: string; shareWith?: MemberType[]; teams?: TeamsInput[]; + charimg?: string; } export const fetchBrainList = async () => { @@ -36,6 +37,7 @@ export async function createBrainAction(obj: BrainCreateType) { isShare: obj.isShare, workspaceId: obj.workspaceId, customInstruction: obj.customInstruction, + charimg: obj.charimg, }; if (obj.isShare) { data.shareWith = obj.members.map((user) => { @@ -265,33 +267,53 @@ export const addDefaultBrainAction = async (workspaceId: string, companyId: stri return response; } +export const convertToSharedAction = async (brainId: string, data: { members?: ObjectType[], teams?: ObjectType[], customInstruction?: string }) => { + const sessionUser = await getSessionUser(); + // Map members -> shareWith to match backend service signature + const payload = { + shareWith: data?.members || [], + teams: data?.teams || [], + customInstruction: data?.customInstruction || '', + }; + const response = await serverApi({ + action: MODULE_ACTIONS.CONVERT_TO_SHARED, + data: payload, + parameters: [brainId], + }); + await Promise.all([ + revalidateTagging(response, `${REVALIDATE_TAG_NAME.WORKSPACE}-${sessionUser.companyId}`), + revalidateTagging(response, `${REVALIDATE_TAG_NAME.BRAIN}-${sessionUser.companyId}`), + ]); + return response; +}; + export const leaveBrainAction = async (brainId: string) => { const sessionUser = await getSessionUser(); if (!brainId) { return { status: 400, - code: 'BRAIN_ID_REQUIRED', - message: '', + code: 'ERROR', + message: BRAIN_ID_REQUIRED, data: {} }; } - + if (!sessionUser?._id) { return { status: 401, - code: 'TOKEN_NOT_FOUND', - message: '', + code: 'ERROR', + message: 'Authentication required', data: {} }; } - + const response = await serverApi({ action: MODULE_ACTIONS.UNSHARE, parameters: [brainId], data: { user_id: sessionUser._id } }); - + await revalidateTagging(response, `${REVALIDATE_TAG_NAME.BRAIN}-${sessionUser.companyId}`); return response; diff --git a/nextjs/src/components/Brains/BrainList.tsx b/nextjs/src/components/Brains/BrainList.tsx index 39947290..42f1a6ba 100644 --- a/nextjs/src/components/Brains/BrainList.tsx +++ b/nextjs/src/components/Brains/BrainList.tsx @@ -25,7 +25,7 @@ import { setEditBrainModalAction } from '@/lib/slices/modalSlice'; import { AI_MODEL_CODE, GENERAL_BRAIN_SLUG, ROLE_TYPE } from '@/utils/constant'; import { SettingsIcon } from '@/icons/SettingsIcon'; import { usePathname, useRouter, useSearchParams } from 'next/navigation'; -import { createHandleOutsideClick, truncateText } from '@/utils/common'; +import { createHandleOutsideClick, getRandomCharacter, truncateText } from '@/utils/common'; import useServerAction from '@/hooks/common/useServerActions'; import { deleteBrainAction, updateBrainAction } from '@/actions/brains'; import { setSelectedBrain } from '@/lib/slices/brain/brainlist'; @@ -36,18 +36,22 @@ import { SetUserData } from '@/types/user'; import { chatMemberListAction } from '@/lib/slices/chat/chatSlice'; import { generateObjectId } from '@/utils/helper'; import Link from 'next/link'; +import Image from 'next/image'; +import ConvertToSharedModal from './ConvertToSharedModal'; +import { ShareBrainIcon } from '@/icons/Share'; import LeaveBrainButton from './LeaveBrainButton'; type DefaultEditOptionProps = { onEdit: () => void; handleEditBrain: () => void; handleDeleteBrain: () => void; + handleConvertToShared?: () => void; isDeletePending: boolean; + isPrivate?: boolean; } type CommonListProps = { b: BrainListType; - key?: string; currentUser: SetUserData; closeSidebar: () => void; onBrainLeave?: (brainId: string) => void; @@ -141,11 +145,11 @@ export const LinkItems = React.memo(({ icon, text, href, data }: LinkItemsProps) }); const DefaultEditOption = React.memo( - ({ onEdit, handleEditBrain, handleDeleteBrain, isDeletePending }: DefaultEditOptionProps) => { + ({ onEdit, handleEditBrain, handleDeleteBrain, handleConvertToShared, isDeletePending, isPrivate }: DefaultEditOptionProps) => { return ( -
+
@@ -161,6 +165,19 @@ const DefaultEditOption = React.memo( /> Rename + {isPrivate && handleConvertToShared && ( + + + Convert to Shared + + )} - Manage @@ -184,7 +200,7 @@ const DefaultEditOption = React.memo( } ); -export const CommonList = ({ b, key, currentUser, closeSidebar, onBrainLeave }: CommonListProps) => { +export const CommonList = ({ b, currentUser, closeSidebar, onBrainLeave }: CommonListProps) => { const dispatch = useDispatch(); const router = useRouter(); @@ -194,9 +210,10 @@ export const CommonList = ({ b, key, currentUser, closeSidebar, onBrainLeave }: const [isEditing, setIsEditing] = useState(false); const [editedTitle, setEditedTitle] = useState(b.title); const inputRef = useRef(null); - const buttonRef=useRef(null) + const buttonRef = useRef(null); const [deleteBrain, isDeletePending] = useServerAction(deleteBrainAction); const [updateBrain, isUpdatePending] = useServerAction(updateBrainAction); + const [showConvertModal, setShowConvertModal] = useState(false); const searchParams = useSearchParams(); const brainId = searchParams.get('b') ? decodedObjectId(searchParams.get('b')) : null; @@ -206,6 +223,11 @@ export const CommonList = ({ b, key, currentUser, closeSidebar, onBrainLeave }: [b?._id, brainId] ); + // Memoize the default character based on brain ID to prevent re-fetching on every render + const defaultCharacter = useMemo(() => { + return getRandomCharacter(); + }, [b?._id]); // Only changes if brain ID changes + const isOwner = b.user.id === currentUser._id; const handleEditClick = () => { @@ -239,16 +261,16 @@ export const CommonList = ({ b, key, currentUser, closeSidebar, onBrainLeave }: const handleSaveClick = async () => { - if(b.title !==inputRef.current.value){ + if(b.title !== inputRef.current.value){ const payload = { title: editedTitle, isShare: b?.isShare, workspaceId: b?.workspaceId }; - const response:any=await updateBrain(payload, b?._id); + const response: any = await updateBrain(payload, b?._id); - if(response?.code=='ERROR'){ + if(response?.code == 'ERROR'){ setEditedTitle(b?.title) } setIsEditing(false) @@ -278,6 +300,10 @@ export const CommonList = ({ b, key, currentUser, closeSidebar, onBrainLeave }: Toast(response?.message); }; + const handleConvertToShared = () => { + setShowConvertModal(true); + }; + const handleNewChatClick = () => { const brainId = encodedObjectId(b?._id); const objectId = generateObjectId(); @@ -297,73 +323,104 @@ export const CommonList = ({ b, key, currentUser, closeSidebar, onBrainLeave }: return ( <> - - ) : null} - - {/* Show options for non-default, non-general brains */} - {b?.slug != `default-brain-${currentUser?._id}` && - b?.slug !== GENERAL_BRAIN_SLUG && ( - <> - {currentUser?.roleCode === ROLE_TYPE.USER && !isOwner && ( -
e.stopPropagation()}> - setShowConvertModal(false)} + brain={b} + /> + + + +
- )} - - {((currentUser?.roleCode === ROLE_TYPE.USER && isOwner) || - currentUser?.roleCode !== ROLE_TYPE.USER) && ( - handleEditBrain(b)} - handleDeleteBrain={() => handleDeleteBrain(b)} - isDeletePending={isDeletePending} - /> + ) : ( + {b.title} + )} + {isEditing ? ( + + ) : ( + + {b.title !== editedTitle + ? truncateText(editedTitle, 29) + : truncateText(b.title, 29)} + + )} + {isEditing ? ( + + ) : null} + {b?.slug != `default-brain-${currentUser?._id}` && + b?.slug !== GENERAL_BRAIN_SLUG && ( + <> + {currentUser?.roleCode === ROLE_TYPE.USER && !isOwner && ( +
e.stopPropagation()}> + +
+ )} + {((currentUser?.roleCode === ROLE_TYPE.USER && isOwner) || + currentUser?.roleCode !== ROLE_TYPE.USER) && ( + handleEditBrain(b)} + handleDeleteBrain={() => handleDeleteBrain(b)} + handleConvertToShared={!b.isShare ? handleConvertToShared : undefined} + isDeletePending={isDeletePending} + isPrivate={!b.isShare} + /> + )} + )} - - )} - + + + +

{b.title !== editedTitle ? editedTitle : b.title}

+
+ + ); }; \ No newline at end of file diff --git a/nextjs/src/components/Brains/ConvertToSharedModal.tsx b/nextjs/src/components/Brains/ConvertToSharedModal.tsx new file mode 100644 index 00000000..07bafb64 --- /dev/null +++ b/nextjs/src/components/Brains/ConvertToSharedModal.tsx @@ -0,0 +1,252 @@ +import { useState, useEffect } from 'react'; +import { useDispatch } from 'react-redux'; +import { useSelector } from 'react-redux'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import BrainIcon from '@/icons/BrainIcon'; +import AutoSelectChip from '../ui/AutoSelectChip'; +import { Controller } from 'react-hook-form'; +import useMembers from '@/hooks/members/useMembers'; +import { showNameOrEmail } from '@/utils/common'; +import { useTeams } from '@/hooks/team/useTeams'; +import Label from '@/widgets/Label'; +import { convertToSharedAction } from '@/actions/brains'; +import useServerAction from '@/hooks/common/useServerActions'; +import Toast from '@/utils/toast'; +import { convertBrainToShared } from '@/lib/slices/brain/brainlist'; +import { useForm } from 'react-hook-form'; +import ValidationError from '@/widgets/ValidationError'; +import { CONVERT_TO_SHARED_SUCCESS, CONVERT_TO_SHARED_ERROR } from '@/utils/constant'; + +interface ConvertToSharedModalProps { + open: boolean; + close: () => void; + brain: any; +} + +const ConvertToSharedModal = ({ open, close, brain }: ConvertToSharedModalProps) => { + const dispatch = useDispatch(); + const [searchMemberValue, setSearchMemberValue] = useState(''); + const [memberOptions, setMemberOptions] = useState([]); + const [teamOptions, setTeamOptions] = useState([]); + const [searchTeamValue, setSearchTeamValue] = useState(''); + + const { members, getMembersList } = useMembers(); + const selectedWorkSpace = useSelector((store: any) => store.workspacelist.selected); + + const { + register, + handleSubmit, + formState: { errors }, + control, + setValue: setFormValue, + reset, + } = useForm(); + + const { + getTeams, + teams, + control: teamControl, + clearErrors: clearTeamErrors, + errors: teamErrors, + } = useTeams(); + + const [runAction, isPending] = useServerAction(convertToSharedAction); + + useEffect(() => { + const fetchUsers = () => { + setMemberOptions([]); + getMembersList({ + search: searchMemberValue, + include: true, + workspaceId: selectedWorkSpace._id, + }); + }; + + if (searchMemberValue == '') { + setMemberOptions([]); + } + + if (searchMemberValue) { + const timer = setTimeout(fetchUsers, 1000); + return () => clearTimeout(timer); + } + }, [searchMemberValue]); + + useEffect(() => { + getTeams({ search: '', pagination: false }); + }, [open]); + + useEffect(() => { + setMemberOptions( + members.map((user) => ({ + email: user.email, + id: user.id, + fullname: showNameOrEmail(user), + fname: user?.fname, + lname: user?.lname, + })) + ); + + setTeamOptions( + teams.map((team) => ({ + teamName: team.teamName, + id: team._id, + teamUsers: team.teamUsers, + })) + ); + }, [members, teams]); + + useEffect(() => { + getMembersList({}); + }, []); + + const onSubmit = async ({ members, teamsInput, customInstruction }) => { + try { + const payload = { + members, + teams: teamsInput?.map(team => ({ + id: team.id, + teamName: team.teamName, + teamUsers: team.teamUsers + })) || [], + customInstruction, + }; + + const response = await runAction(brain._id, payload); + + if (response?.code === 'SUCCESS') { + dispatch(convertBrainToShared({ + brainId: brain._id, + convertedBrain: response.data + })); + + Toast(response?.message || CONVERT_TO_SHARED_SUCCESS); + close(); + } else { + Toast(response?.message || CONVERT_TO_SHARED_ERROR); + } + } catch (error) { + Toast('We couldn’t convert this brain. Please try again.'); + } + }; + + return ( + <> + + + + + + Convert to Shared Brain + + + Convert "{brain?.title}" to a shared brain and invite team members to collaborate. + This will make the brain accessible to selected users and teams. + + +
+
+
+
+
+
+