diff --git a/client/src/WebsocketClient.ts b/client/src/WebsocketClient.ts index 63e1f54..cc880c6 100644 --- a/client/src/WebsocketClient.ts +++ b/client/src/WebsocketClient.ts @@ -13,7 +13,7 @@ import { import { CanvasElement } from '@/stores/CanvasElementsStore'; import { EVENT } from './types'; import { ValueOf } from './lib/misc'; -import { UpdatedTimeMessage } from './stores/WebSocketStore'; +import { BoardMetaData, UpdatedTimeMessage } from './stores/WebSocketStore'; import { Comment } from './stores/CommentsStore'; interface CallBacksType { @@ -215,6 +215,7 @@ export default class WebsocketClient { | string | string[] | UpdatedTimeMessage + | BoardMetaData | null | { elemID: string; comment: Partial } | Record, diff --git a/client/src/components/lib/BoardHeader.tsx b/client/src/components/lib/BoardHeader.tsx index 2b0bb55..0c6458d 100644 --- a/client/src/components/lib/BoardHeader.tsx +++ b/client/src/components/lib/BoardHeader.tsx @@ -226,7 +226,9 @@ const BoardHeader = ({ fields: { serialized: state }, }); setBoardMeta({ lastModified: updated.data.updatedAt }); - updateCanvas(boardMeta.id, updated.data.updatedAt); + updateCanvas(boardMeta.id, { + updatedAt: updated.data.updatedAt, + }); setWebsocketAction( { boardID: boardMeta.id, diff --git a/client/src/components/lib/CommentsSheetContent.tsx b/client/src/components/lib/CommentsSheetContent.tsx index f38964f..9f2e0da 100644 --- a/client/src/components/lib/CommentsSheetContent.tsx +++ b/client/src/components/lib/CommentsSheetContent.tsx @@ -308,7 +308,7 @@ const CommentsSheetContent = () => { fields: { serialized: state }, }); setBoardMeta({ lastModified: updated.data.updatedAt }); - updateCanvas(boardMeta.id, updated.data.updatedAt); + updateCanvas(boardMeta.id, { updatedAt: updated.data.updatedAt }); setWebsocketAction( { boardID: boardMeta.id, diff --git a/client/src/components/lib/EditBoardDataDialog.tsx b/client/src/components/lib/EditBoardDataDialog.tsx index 1bad230..7028f61 100644 --- a/client/src/components/lib/EditBoardDataDialog.tsx +++ b/client/src/components/lib/EditBoardDataDialog.tsx @@ -143,7 +143,9 @@ export default function EditBoardDataDialog({ folder: newFolder, lastModified: updatedAt.data.updatedAt, }); - updateCanvas(boardMeta.id, updatedAt.data.updatedAt); + updateCanvas(boardMeta.id, { + updatedAt: updatedAt.data.updatedAt, + }); updateCanvasInfo( boardMeta.id, newTitle ?? '', @@ -153,9 +155,12 @@ export default function EditBoardDataDialog({ setWebsocketAction( { boardID: boardMeta.id, + title: newTitle, + tags: boardMeta.tags, + folder: newFolder, lastModified: updatedAt.data.updatedAt, }, - 'updateUpdatedTime', + 'changeBoardMeta', ); }} > diff --git a/client/src/components/lib/ShareBoardDialog.tsx b/client/src/components/lib/ShareBoardDialog.tsx index 57f4db3..ba01837 100644 --- a/client/src/components/lib/ShareBoardDialog.tsx +++ b/client/src/components/lib/ShareBoardDialog.tsx @@ -30,6 +30,7 @@ import { useWebSocketStore } from '@/stores/WebSocketStore'; import { REST } from '@/constants'; import { useToast } from '../ui/use-toast'; import { fetchImageFromFirebaseStorage } from '@/views/SignInPage'; +import { TrashIcon } from '@radix-ui/react-icons'; /** * An alert dialog that is controlled by the `open` prop. It displays a list of users @@ -50,18 +51,26 @@ const ShareBoardDialog = ({ }) => { /* Controls visibility of the addition input. */ const [isAddUserOpen, setIsAddUserOpen] = useState(false); - const { boardMeta, updatePermission, addUser, updateAvatars } = - useCanvasBoardStore([ - 'boardMeta', - 'updatePermission', - 'addUser', - 'updateAvatars', - ]); + const { + boardMeta, + updatePermission, + addUser, + updateAvatars, + setBoardMeta, + removeCanvas, + } = useCanvasBoardStore([ + 'boardMeta', + 'updatePermission', + 'addUser', + 'updateAvatars', + 'setBoardMeta', + 'removeCanvas', + ]); const { setSelectedElements, selectedElementIds } = useCanvasElementStore([ 'setSelectedElements', 'selectedElementIds', ]); - const { setTool } = useAppStore(['setTool']); + const { setTool, setMode } = useAppStore(['setTool', 'setMode']); const { socket, setWebsocketAction } = useWebSocketStore([ 'socket', 'setWebsocketAction', @@ -79,6 +88,38 @@ const ShareBoardDialog = ({ if (isOwnPerm) setSelectedElements([]); updatePermission(collabID, permission, isOwnPerm); }); + + socket?.on('removeCollab', async (msg) => { + const collabID = (msg as { payload: string }).payload; + + if (collabID === boardMeta.collabID) { + setMode('dashboard'); + toast({ + variant: 'destructive', + title: `You have been removed from: ${boardMeta.title}`, + description: 'Ask another collaborator to add you again', + }); + removeCanvas(boardMeta.id); + setBoardMeta({ + title: '', + id: '', + lastModified: '', + roomID: '', + shareUrl: '', + folder: '', + tags: [], + collabID: '', + users: [], + permission: '', + }); + return; + } + setBoardMeta({ + users: boardMeta.users.filter( + (metaUser) => collabID !== metaUser.collabID, + ), + }); + }); }, [socket, boardMeta.collabID, updatePermission]); return ( @@ -206,46 +247,73 @@ const ShareBoardDialog = ({

{user.email}

- - - - + onValueChange={(value) => { + if (boardMeta.collabID === user.collabID) { + updatePermission(user.collabID, value, true); + setTool('pan'); + setSelectedElements([]); + setCursor(''); + } else { + updatePermission(user.collabID, value, false); + } + console.log(selectedElementIds); + setWebsocketAction( + { collabID: user.collabID, permission: value }, + 'changePermission', + ); + axios.put(REST.collaborator.update, { + id: user.collabID, + fields: { + permissionLevel: value, + }, + }); + }} + > + + + + + + View + Edit + {user.permission === 'owner' && ( + Owner + )} + + + + ))} diff --git a/client/src/constants.ts b/client/src/constants.ts index 511d945..bfc9389 100644 --- a/client/src/constants.ts +++ b/client/src/constants.ts @@ -35,6 +35,7 @@ export const REST = { deleteBoard: `${REST_ROOT}/board/deleteBoard`, updateBoard: `${REST_ROOT}/board/updateBoard`, addUser: `${REST_ROOT}/board/addUser`, + removeCollab: `${REST_ROOT}/board/updateCollabs`, }, collaborators: { getAvatar: `${REST_ROOT}/collaborator/getCollaboratorAvatars`, diff --git a/client/src/hooks/useSocket.tsx b/client/src/hooks/useSocket.tsx index 960d342..32e78b4 100644 --- a/client/src/hooks/useSocket.tsx +++ b/client/src/hooks/useSocket.tsx @@ -1,5 +1,9 @@ import WebsocketClient from '@/WebsocketClient'; -import { UpdatedTimeMessage, useWebSocketStore } from '@/stores/WebSocketStore'; +import { + BoardMetaData, + UpdatedTimeMessage, + useWebSocketStore, +} from '@/stores/WebSocketStore'; import { CanvasElement, useCanvasElementStore, @@ -233,7 +237,7 @@ export const useSocket = () => { }, updateUpdatedTime: async (fields: UpdatedTimeMessage) => { setBoardMeta({ lastModified: fields.lastModified }); - updateCanvas(fields.boardID, fields.lastModified); + updateCanvas(fields.boardID, { updatedAt: fields.lastModified }); const boardState = await axios.get(REST.board.getBoard, { params: { id: fields.boardID }, @@ -267,6 +271,10 @@ export const useSocket = () => { removeAttachedFileUrl: (ids: string[]) => { removeAttachedFileUrl(ids); }, + changeBoardMeta: (boardMetaData: BoardMetaData) => { + setBoardMeta(boardMetaData); + updateCanvas(boardMetaData.boardID, boardMetaData); + }, }; // Intialize socket @@ -348,7 +356,11 @@ export const useSocket = () => { } //Check if the actionElementID is string[] (collection of ids) - if (typeof actionElementID === 'object' || action === 'addCollab') { + if ( + typeof actionElementID === 'object' || + action === 'addCollab' || + action === 'removeCollab' + ) { socket.current?.sendMsgRoom(action, actionElementID); setWebsocketAction('', ''); return; diff --git a/client/src/stores/CanavasBoardStore.ts b/client/src/stores/CanavasBoardStore.ts index 8848f1e..3436955 100644 --- a/client/src/stores/CanavasBoardStore.ts +++ b/client/src/stores/CanavasBoardStore.ts @@ -62,7 +62,7 @@ interface CanvasBoardActions { setCanvases: (canvases: Canvas[]) => void; addCanvas: (canvas: Canvas) => void; removeCanvas: (id: string) => void; - updateCanvas: (id: string, meta: string) => void; + updateCanvas: (id: string, meta: Partial) => void; updateCanvasInfo: ( id: string, title: string, @@ -126,10 +126,10 @@ const removeCanvas = (set: SetState) => (id: string) => }); const updateCanvas = - (set: SetState) => (id: string, updatedAt: string) => + (set: SetState) => (id: string, meta: Partial) => set((state) => { const canvases = state.canvases.map((canvas) => - canvas.id === id ? { ...canvas, updatedAt } : canvas, + canvas.id === id ? { ...canvas, ...meta } : canvas, ); return { ...state, canvases }; diff --git a/client/src/stores/WebSocketStore.ts b/client/src/stores/WebSocketStore.ts index 9b74fde..d105d70 100644 --- a/client/src/stores/WebSocketStore.ts +++ b/client/src/stores/WebSocketStore.ts @@ -26,6 +26,8 @@ export const Actions = [ 'addNewCollab', 'addAttachedFileUrl', 'removeAttachedFileUrl', + 'removeCollab', + 'changeBoardMeta', ] as const; export type ActionsType = typeof Actions; @@ -46,6 +48,14 @@ export interface UpdatedTimeMessage { lastModified: string; } +export interface BoardMetaData { + boardID: string; + title: string; + tags: string[]; + folder: string; + lastModified: string; +} + /** Definitions */ interface WebSocketState { // Reference for sending non-stateful messages (WebRTC signalling) @@ -59,6 +69,7 @@ interface WebSocketState { | string | string[] | UpdatedTimeMessage + | BoardMetaData | { elemID: string; comment: Partial } | { collabID: string; permission: string } | { selectedElementIds: string[]; downloadURL: string }; @@ -79,6 +90,7 @@ interface WebSocketActions { | string | string[] | UpdatedTimeMessage + | BoardMetaData | { elemID: string; comment: Partial } | { collabID: string; permission: string } | { selectedElementIds: string[]; downloadURL: string }, @@ -122,6 +134,7 @@ const setWebsocketAction = | string | string[] | UpdatedTimeMessage + | BoardMetaData | { elemID: string; comment: Partial } | { collabID: string; permission: string } | { selectedElementIds: string[]; downloadURL: string }, diff --git a/node/src/api/board/board.controller.ts b/node/src/api/board/board.controller.ts index 3e2c91a..84ea793 100644 --- a/node/src/api/board/board.controller.ts +++ b/node/src/api/board/board.controller.ts @@ -265,6 +265,25 @@ export const handleUpdateBoard = async (req: Request, res: Response) => { } }; +// remove collaborator from board +export const handleRemoveCollaborator = async (req: Request, res: Response) => { + try { + // The board ID and new parameters are in the body. + const { boardId, newCollabs } = req.body; + if (!validateId(boardId, res)) return; + const board = (await findBoardById(boardId)) as Board; + + await updateBoard(board, { collaborators: newCollabs }, true); + + return res.status(HTTP_STATUS.SUCCESS).json({}); + } catch (error) { + console.error('Error updating board: ', error); + res + .status(HTTP_STATUS.INTERNAL_SERVER_ERROR) + .json({ error: 'Failed to remove Collaborator' }); + } +}; + // Update board export const handleAddUserbyEmail = async (req: Request, res: Response) => { try { diff --git a/node/src/api/board/board.route.ts b/node/src/api/board/board.route.ts index 4fa6f9e..ccd7b98 100644 --- a/node/src/api/board/board.route.ts +++ b/node/src/api/board/board.route.ts @@ -5,6 +5,7 @@ import { handleDeleteBoard, handleFindBoardById, handleGetCollaboratorBoards, + handleRemoveCollaborator, handleUpdateBoard, } from './board.controller'; @@ -34,4 +35,7 @@ router.put('/addUser', handleAddUserbyEmail); // DELETE board by ID router.delete('/deleteBoard', handleDeleteBoard); +// PUT new collaborators a board +router.put('/updateCollabs', handleRemoveCollaborator); + export default router; diff --git a/node/src/models/board.ts b/node/src/models/board.ts index bb8c71b..36dfdda 100644 --- a/node/src/models/board.ts +++ b/node/src/models/board.ts @@ -74,10 +74,11 @@ export const findBoardById = async (boardId: string) => export const updateBoard = async ( board: Board, updatedFields: Partial>, + isRemoveCollab = false, ) => { updatedFields.updatedAt = new Date().toUTCString(); const { fastFireOptions: _fastFireOptions, id: _id, ...boardFields } = board; - if (updatedFields.collaborators !== undefined) { + if (updatedFields.collaborators !== undefined && !isRemoveCollab) { boardFields.collaborators.push(updatedFields.collaborators as string); delete updatedFields.collaborators; }