diff --git a/client/package.json b/client/package.json index c7a4275..ec87914 100644 --- a/client/package.json +++ b/client/package.json @@ -22,6 +22,7 @@ "@radix-ui/react-dropdown-menu": "^2.0.6", "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-label": "^2.0.2", + "@radix-ui/react-portal": "^1.0.4", "@radix-ui/react-select": "^2.0.0", "@radix-ui/react-separator": "^1.0.3", "@radix-ui/react-slider": "^1.1.2", diff --git a/client/pnpm-lock.yaml b/client/pnpm-lock.yaml index 17a972a..6f75f11 100644 --- a/client/pnpm-lock.yaml +++ b/client/pnpm-lock.yaml @@ -35,6 +35,9 @@ dependencies: '@radix-ui/react-label': specifier: ^2.0.2 version: 2.0.2(@types/react-dom@18.2.7)(@types/react@18.2.21)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-portal': + specifier: ^1.0.4 + version: 1.0.4(@types/react-dom@18.2.7)(@types/react@18.2.21)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-select': specifier: ^2.0.0 version: 2.0.0(@types/react-dom@18.2.7)(@types/react@18.2.21)(react-dom@18.2.0)(react@18.2.0) diff --git a/client/src/components/lib/BoardHeader.tsx b/client/src/components/lib/BoardHeader.tsx index 004a359..abaee50 100644 --- a/client/src/components/lib/BoardHeader.tsx +++ b/client/src/components/lib/BoardHeader.tsx @@ -132,6 +132,8 @@ const BoardHeader = ({ folder: '', tags: [], collabID: '', + users: [], + permission: '', }); }} > @@ -159,96 +161,101 @@ const BoardHeader = ({ {/* Avatars of active tenants */} {/* Comment section button */} - + {boardMeta.permission !== 'view' && ( + + )} {/* Share Dialog */}
- + {boardMeta.permission !== 'view' && ( + + + + const updated = await axios.put(REST.board.updateBoard, { + id: boardMeta.id, + fields: { serialized: state }, + }); + setBoardMeta({ lastModified: updated.data.updatedAt }); + updateCanvas(boardMeta.id, updated.data.updatedAt); + setWebsocketAction( + { + boardID: boardMeta.id, + lastModified: updated.data.updatedAt, + }, + 'updateUpdatedTime', + ); + } catch (error) { + toast({ + variant: 'destructive', + title: 'Something went wrong.', + description: 'There was a problem with your request.', + action: ( + window.location.reload()} + altText="Refresh" + > + Refresh + + ), + }); + } + }} + > + Save + {' '} + + )}
diff --git a/client/src/components/lib/BoardScroll.tsx b/client/src/components/lib/BoardScroll.tsx index 2612a74..e6da30c 100644 --- a/client/src/components/lib/BoardScroll.tsx +++ b/client/src/components/lib/BoardScroll.tsx @@ -82,6 +82,7 @@ export const BoardScroll = () => { return (
+ {/* eslint-disable-next-line sonarjs/cognitive-complexity */} {canvases.map((board) => (
{ try { let collabID = ' '; + let users = []; + let permission = ''; if (!isSelected) { //todo const boardState = await axios.get(REST.board.getBoard, { @@ -101,6 +104,8 @@ export const BoardScroll = () => { }); collabID = boardState.data.collabID; + users = boardState.data.users; + permission = boardState.data.permissionLevel; const state = createStateWithRoughElement( boardState.data.board.serialized, @@ -164,6 +169,8 @@ export const BoardScroll = () => { tags: isSelected ? [] : board.tags, collabID: isSelected ? '' : collabID, collaboratorAvatars: collaboratorAvatarUrlsMap, + users: isSelected ? '' : users, + permission: isSelected ? '' : permission, }); } catch (error) { toast({ diff --git a/client/src/components/lib/Canvas.tsx b/client/src/components/lib/Canvas.tsx index e62247b..73f21b9 100644 --- a/client/src/components/lib/Canvas.tsx +++ b/client/src/components/lib/Canvas.tsx @@ -23,6 +23,7 @@ import { getScaleOffset } from '@/lib/canvasElements/render'; import { IS_ELECTRON_INSTANCE, PERIPHERAL_CODES, + REST, SECONDS_TO_MS, WS_TOPICS, } from '@/constants'; @@ -42,6 +43,7 @@ import { useAuthStore } from '@/stores/AuthStore'; import CursorPresence from './CursorPresence'; import { throttle } from 'lodash'; import { idToColour } from '@/lib/userColours'; +import axios from 'axios'; /** * Main Canvas View @@ -142,7 +144,7 @@ export default function Canvas() { 'setTenants', 'clearTenants', ]); - const { boardMeta } = useCanvasBoardStore(['boardMeta']); + const { boardMeta, addUser } = useCanvasBoardStore(['boardMeta', 'addUser']); const { userEmail } = useAuthStore(['userEmail']); // Id of the element currently being drawn. @@ -164,7 +166,7 @@ export default function Canvas() { socket?.on(WS_TOPICS.NOTIFY_JOIN_ROOM, initTenants); socket?.on(WS_TOPICS.NOTIFY_LEAVE_ROOM, initTenants); return clearTenants; - }, [socket]); + }, [socket, boardMeta.users]); /** * Called everytime someone joins or leaves the room, and once we initially join, to maintain @@ -174,6 +176,22 @@ export default function Canvas() { */ const initTenants = async () => { const tenantIds = (await tenancy.get(boardMeta.roomID)) as string[]; + + tenantIds + .map((id) => extractCollabID(id) as string) + .filter( + (id) => !new Set(boardMeta.users.map((user) => user.collabID)).has(id), + ) + .forEach(async (collabID) => { + if (!collabID) return; + const userInfo = await axios.get(REST.collaborator.getUserInfo, { + params: { + id: collabID, + }, + }); + addUser(userInfo.data.userInfo); + }); + const activeTenants = tenantIds.reduce( (acc, id) => { const collabId = extractCollabID(id); diff --git a/client/src/components/lib/DeleteCanvasDialog.tsx b/client/src/components/lib/DeleteCanvasDialog.tsx index a135abc..2c86cd9 100644 --- a/client/src/components/lib/DeleteCanvasDialog.tsx +++ b/client/src/components/lib/DeleteCanvasDialog.tsx @@ -60,6 +60,8 @@ export const DeleteCanavasDialog = ({ folder: '', tags: [], collabID: '', + users: [], + permission: '', }); axios.delete(REST.board.deleteBoard, { params: { id: boardMeta.id }, diff --git a/client/src/components/lib/ShareBoardDialog.tsx b/client/src/components/lib/ShareBoardDialog.tsx index fc9f400..9af781c 100644 --- a/client/src/components/lib/ShareBoardDialog.tsx +++ b/client/src/components/lib/ShareBoardDialog.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import { AlertDialog, AlertDialogAction, @@ -21,7 +21,14 @@ import { SelectTrigger, SelectValue, } from '../ui/select'; -import { User } from '@/stores/WebSocketStore'; +import { SharedUser, useCanvasBoardStore } from '@/stores/CanavasBoardStore'; +import axios from 'axios'; +import { useAppStore } from '@/stores/AppStore'; +import { setCursor } from '@/lib/misc'; +import { useCanvasElementStore } from '@/stores/CanvasElementsStore'; +import { useWebSocketStore } from '@/stores/WebSocketStore'; +import { REST } from '@/constants'; +import { useToast } from '../ui/use-toast'; /** * An alert dialog that is controlled by the `open` prop. It displays a list of users @@ -38,10 +45,38 @@ const ShareBoardDialog = ({ open: boolean; setOpen: (value: boolean) => void; boardLink: string; - users: User[]; + users: SharedUser[]; }) => { /* Controls visibility of the addition input. */ const [isAddUserOpen, setIsAddUserOpen] = useState(false); + const { boardMeta, updatePermission, addUser } = useCanvasBoardStore([ + 'boardMeta', + 'updatePermission', + 'addUser', + ]); + const { setSelectedElements, selectedElementIds } = useCanvasElementStore([ + 'setSelectedElements', + 'selectedElementIds', + ]); + const { setTool } = useAppStore(['setTool']); + const { socket, setWebsocketAction } = useWebSocketStore([ + 'socket', + 'setWebsocketAction', + ]); + const newUserEmail = useRef(null); + const newUserPerm = useRef(null); + const { toast } = useToast(); + + useEffect(() => { + socket?.on('changePermission', (msg) => { + const { collabID, permission } = ( + msg as { payload: { collabID: string; permission: string } } + ).payload; + const isOwnPerm = boardMeta.collabID === collabID; + if (isOwnPerm) setSelectedElements([]); + updatePermission(collabID, permission, isOwnPerm); + }); + }, [socket, boardMeta.collabID, updatePermission]); return ( @@ -85,17 +120,62 @@ const ShareBoardDialog = ({ - +
)} {/* The user list */} @@ -103,7 +183,9 @@ const ShareBoardDialog = ({ {users.map((user) => (
- + {user.initials}
@@ -113,13 +195,44 @@ const ShareBoardDialog = ({

{user.email}

- { + 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/components/lib/ToolBar.tsx b/client/src/components/lib/ToolBar.tsx index a417a09..d95c65d 100644 --- a/client/src/components/lib/ToolBar.tsx +++ b/client/src/components/lib/ToolBar.tsx @@ -22,6 +22,7 @@ import { fileOpen } from '@/lib/fs'; import { injectImageElement } from '@/lib/image'; import { useWebSocketStore } from '@/stores/WebSocketStore'; import { IS_ELECTRON_INSTANCE } from '@/constants'; +import { useCanvasBoardStore } from '@/stores/CanavasBoardStore'; /** * This is the toolbar that is displayed on the canvas. @@ -170,6 +171,8 @@ const ToolGroup = ({ */ const ToolBar = () => { const { tool, isTransparent } = useAppStore(['tool', 'isTransparent']); + const { boardMeta } = useCanvasBoardStore(['boardMeta']); + const allowEdit = boardMeta.permission !== 'view'; return ( { width: 'fit-content', }} > - - + {allowEdit && ( + + + + + )} ); }; diff --git a/client/src/components/lib/TopBar.tsx b/client/src/components/lib/TopBar.tsx index 713fd96..b256d86 100644 --- a/client/src/components/lib/TopBar.tsx +++ b/client/src/components/lib/TopBar.tsx @@ -97,6 +97,8 @@ export const TopBar = () => { folder: boardData.folder, tags: boardData.tags, collabID: data.data.collabID, + users: data.data.users, + permission: data.data.permissionLevel, }); setMode('canvas'); diff --git a/client/src/components/ui/toast.tsx b/client/src/components/ui/toast.tsx index 76861dc..21a484f 100644 --- a/client/src/components/ui/toast.tsx +++ b/client/src/components/ui/toast.tsx @@ -2,6 +2,7 @@ import * as React from 'react'; import { Cross2Icon } from '@radix-ui/react-icons'; import * as ToastPrimitives from '@radix-ui/react-toast'; import { cva, type VariantProps } from 'class-variance-authority'; +import * as Portal from '@radix-ui/react-portal'; import { cn } from '@/lib/utils'; @@ -11,14 +12,16 @@ const ToastViewport = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( - + + + )); ToastViewport.displayName = ToastPrimitives.Viewport.displayName; diff --git a/client/src/constants.ts b/client/src/constants.ts index 338dc90..87db459 100644 --- a/client/src/constants.ts +++ b/client/src/constants.ts @@ -34,6 +34,7 @@ export const REST = { getBoard: `${REST_ROOT}/board/getBoard`, deleteBoard: `${REST_ROOT}/board/deleteBoard`, updateBoard: `${REST_ROOT}/board/updateBoard`, + addUser: `${REST_ROOT}/board/addUser`, }, collaborators: { getAvatar: `${REST_ROOT}/collaborator/getCollaboratorAvatars`, @@ -44,6 +45,10 @@ export const REST = { update: `${REST_ROOT}/comment/handleLike`, delete: `${REST_ROOT}/comment/deleteComment`, }, + collaborator: { + update: `${REST_ROOT}/collaborator/updateCollaborator`, + getUserInfo: `${REST_ROOT}/collaborator/getCollaborator`, + }, }; export const HTTP_STATUS = { SUCCESS: 200, diff --git a/client/src/hooks/useSocket.tsx b/client/src/hooks/useSocket.tsx index 6e1c019..ef07b36 100644 --- a/client/src/hooks/useSocket.tsx +++ b/client/src/hooks/useSocket.tsx @@ -10,8 +10,11 @@ import { useAuthStore } from '@/stores/AuthStore'; import { fileCache } from '@/lib/cache'; import { dataURLToFile } from '@/lib/bytes'; import { commitImageToCache, isSupportedImageFile } from '@/lib/image'; -import { useCanvasBoardStore } from '@/stores/CanavasBoardStore'; +import { SharedUser, useCanvasBoardStore } from '@/stores/CanavasBoardStore'; import { Vector2 } from '@/types'; +import axios from 'axios'; +import { REST } from '@/constants'; +import { createStateWithRoughElement } from '@/components/lib/BoardScroll'; /** * Defines a hook that controls all socket related activities @@ -37,6 +40,7 @@ export const useSocket = () => { ]); const { + setCanvasElementState, addCanvasShape, addCanvasFreehand, editCanvasElement, @@ -63,6 +67,7 @@ export const useSocket = () => { textStrings, fileIds, } = useCanvasElementStore([ + 'setCanvasElementState', 'addCanvasShape', 'addCanvasFreehand', 'editCanvasElement', @@ -90,11 +95,13 @@ export const useSocket = () => { 'fileIds', ]); - const { boardMeta, setBoardMeta, updateCanvas } = useCanvasBoardStore([ - 'boardMeta', - 'setBoardMeta', - 'updateCanvas', - ]); + const { boardMeta, setBoardMeta, updateCanvas, addUser } = + useCanvasBoardStore([ + 'boardMeta', + 'setBoardMeta', + 'updateCanvas', + 'addUser', + ]); const socket = useRef(); @@ -218,15 +225,27 @@ export const useSocket = () => { redoCanvasHistory: () => { redoCanvasHistory(); }, - updateUpdatedTime: (fields: UpdatedTimeMessage) => { + updateUpdatedTime: async (fields: UpdatedTimeMessage) => { setBoardMeta({ lastModified: fields.lastModified }); updateCanvas(fields.boardID, fields.lastModified); + + const boardState = await axios.get(REST.board.getBoard, { + params: { id: fields.boardID }, + }); + + const state = createStateWithRoughElement( + boardState.data.board.serialized, + ); + setCanvasElementState(state); }, updateCursorPosition: (cursorPosition: Vector2 & { userId: string }) => setCursorPosition(cursorPosition.userId, { x: cursorPosition.x, y: cursorPosition.y, }), + addNewCollab: (newUser: SharedUser) => { + addUser(newUser); + }, }; // Intialize socket diff --git a/client/src/stores/CanavasBoardStore.ts b/client/src/stores/CanavasBoardStore.ts index 19f1b1c..7842de8 100644 --- a/client/src/stores/CanavasBoardStore.ts +++ b/client/src/stores/CanavasBoardStore.ts @@ -1,6 +1,7 @@ import { create } from 'zustand'; import { SetState } from './types'; import { createStoreWithSelectors } from './utils'; +// import { User } from 'firebase/auth'; /** * Define Global CanvasBoard states and reducers @@ -23,6 +24,15 @@ export interface Canvas { roomID: string; } +export interface SharedUser { + email: string; + avatar: string; + initials: string; + username: string; + permission: string; + collabID: string; +} + /** Definitions */ interface CanvasBoardState { canvases: Canvas[]; @@ -41,6 +51,8 @@ interface CanvasBoardState { tags: string[]; collabID: string; collaboratorAvatars: Record; + users: SharedUser[]; + permission: string; }; } @@ -57,6 +69,12 @@ interface CanvasBoardActions { folder: string, tags: string[], ) => void; + updatePermission: ( + collabID: string, + permission: string, + isOwnPerm: boolean, + ) => void; + addUser: (user: SharedUser) => void; setBoardMeta: (meta: Partial) => void; setTag: (tags: Array) => void; } @@ -78,6 +96,8 @@ export const initialCanvasState: CanvasBoardState = { tags: [], collabID: '', collaboratorAvatars: {}, + users: [], + permission: '', }, }; @@ -125,6 +145,26 @@ const updateCanvasInfo = return { ...state, canvases }; }); +const updatePermission = + (set: SetState) => + (collabID: string, permission: string, isOwnPerm: boolean) => + set((state) => { + const users = state.boardMeta.users.map((user) => + user.collabID === collabID ? { ...user, permission } : user, + ); + + if (isOwnPerm) + return { boardMeta: { ...state.boardMeta, users, permission } }; + return { boardMeta: { ...state.boardMeta, users } }; + }); + +const addUser = (set: SetState) => (user: SharedUser) => + set((state) => { + const users = [...state.boardMeta.users, user]; + + return { boardMeta: { ...state.boardMeta, users } }; + }); + const setBoardMeta = (set: SetState) => (meta: Partial) => { @@ -143,6 +183,8 @@ const CanvasBoardStore = create()((set) => ({ removeCanvas: removeCanvas(set), updateCanvas: updateCanvas(set), updateCanvasInfo: updateCanvasInfo(set), + updatePermission: updatePermission(set), + addUser: addUser(set), setBoardMeta: setBoardMeta(set), setTag: setTag(set), })); diff --git a/client/src/stores/WebSocketStore.ts b/client/src/stores/WebSocketStore.ts index a3686d7..8e097b4 100644 --- a/client/src/stores/WebSocketStore.ts +++ b/client/src/stores/WebSocketStore.ts @@ -22,6 +22,8 @@ export const Actions = [ 'updateComment', 'addComment', 'removeComment', + 'changePermission', + 'addNewCollab', ] as const; export type ActionsType = typeof Actions; @@ -55,7 +57,8 @@ interface WebSocketState { | string | string[] | UpdatedTimeMessage - | { elemID: string; comment: Partial }; + | { elemID: string; comment: Partial } + | { collabID: string; permission: string }; // The current active tenants activeTenants: Record; // The current cursor positions keyed by user ID @@ -73,7 +76,8 @@ interface WebSocketActions { | string | string[] | UpdatedTimeMessage - | { elemID: string; comment: Partial }, + | { elemID: string; comment: Partial } + | { collabID: string; permission: string }, action: string, ) => void; // Set the socket reference @@ -114,7 +118,8 @@ const setWebsocketAction = | string | string[] | UpdatedTimeMessage - | { elemID: string; comment: Partial }, + | { elemID: string; comment: Partial } + | { collabID: string; permission: string }, action: string, ) => set(() => { diff --git a/client/src/views/SignInPage.tsx b/client/src/views/SignInPage.tsx index 89f0e65..eeb8126 100644 --- a/client/src/views/SignInPage.tsx +++ b/client/src/views/SignInPage.tsx @@ -23,7 +23,11 @@ import axios from 'axios'; import { REST } from '@/constants'; import { ACCESS_TOKEN_TAG } from '@/constants'; import { useAuthStore } from '@/stores/AuthStore'; -import { useCanvasBoardStore, Canvas } from '@/stores/CanavasBoardStore'; +import { + useCanvasBoardStore, + Canvas, + SharedUser, +} from '@/stores/CanavasBoardStore'; import { getStorage, ref, getDownloadURL } from 'firebase/storage'; import { createStateWithRoughElement } from '@/components/lib/BoardScroll'; import { @@ -85,6 +89,8 @@ export async function getUserDetails( folder: string; tags: string[]; collabID: string; + users: SharedUser[]; + permission: string; }>, ) => void, setCanvasElementState: (element: CanvasElementState) => void, @@ -133,6 +139,8 @@ export const checkURL = async ( folder: string; tags: string[]; collabID: string; + users: SharedUser[]; + permission: string; }>, ) => void, setCanvasElementState: (element: CanvasElementState) => void, @@ -149,6 +157,9 @@ export const checkURL = async ( fields: { collaborators: userID }, }); + console.log('users ', board.data.users); + console.log('permissions', board.data.permission); + console.log(board); setBoardMeta({ roomID: board.data.roomID, title: board.data.title, @@ -158,6 +169,8 @@ export const checkURL = async ( folder: board.data.folder, tags: board.data.tags, collabID: board.data.collabID, + users: board.data.users, + permission: board.data.permission, }); setCanvasElementState(createStateWithRoughElement(board.data.serialized)); diff --git a/client/src/views/Viewport.tsx b/client/src/views/Viewport.tsx index 9413186..ae8e6b4 100644 --- a/client/src/views/Viewport.tsx +++ b/client/src/views/Viewport.tsx @@ -19,7 +19,6 @@ import { isDrawingTool } from '@/lib/misc'; import { Separator } from '@/components/ui/separator'; import BoardHeader from '@/components/lib/BoardHeader'; import ShareBoardDialog from '@/components/lib/ShareBoardDialog'; -import { users } from '@/stores/WebSocketStore'; import { useCanvasBoardStore } from '@/stores/CanavasBoardStore'; import EditBoardDataDialog from '@/components/lib/EditBoardDataDialog'; @@ -61,7 +60,7 @@ const Viewport = () => { open={isShareDialogOpen} setOpen={setIsShareDialogOpen} boardLink={boardMeta.shareUrl} - users={users} + users={boardMeta.users} /> { }} > - + {boardMeta.permission !== 'view' && } - + {boardMeta.permission !== 'view' && } {IS_ELECTRON_INSTANCE && }
diff --git a/node/src/api/board/board.controller.ts b/node/src/api/board/board.controller.ts index a08873f..3e2c91a 100644 --- a/node/src/api/board/board.controller.ts +++ b/node/src/api/board/board.controller.ts @@ -5,16 +5,20 @@ import { updateBoard, deleteBoard, findBoardsByCollaboratorsId, + Board, } from '../../models/board'; import { HTTP_STATUS } from '../../constants'; import { + Collaborator, createCollaborator, deleteCollaborator, findCollaboratorById, findCollaboratorByIdAndBoard, findCollaboratorsById, } from '../../models/collaborator'; -import { generateRandId } from '../../utils/misc'; +import { generateRandId, getInitials } from '../../utils/misc'; +import { findUserById } from '../../models/user'; +import { findUserByEmail } from '../user/user.controller'; /** * Firebase API controllers, logic for endpoint routes. @@ -29,13 +33,31 @@ export const handleCreateBoard = async (req: Request, res: Response) => { const boardID = generateRandId(); const { user, serialized, title, shareUrl } = req.body; // The board parameters are in the body. - const collaborator = await createCollaborator('edit', user, boardID); + const { + id: collabID, + permissionLevel, + uid, + } = await createCollaborator('owner', user, boardID); const board = await createBoard(boardID, serialized, title, shareUrl, [ - collaborator.uid, + uid, ]); - res.status(HTTP_STATUS.SUCCESS).json({ board, collabID: collaborator.id }); + const userInfo = await findUserById(user); + const users = [ + { + email: userInfo?.email, + avatar: userInfo?.avatar, + initials: getInitials(userInfo?.firstname + ' ' + userInfo?.lastname), + username: userInfo?.firstname + ' ' + userInfo?.lastname, + permission: permissionLevel, + collabID, + }, + ]; + + res + .status(HTTP_STATUS.SUCCESS) + .json({ board, collabID, users, permissionLevel }); } catch (error) { console.error('Error creating board:', error); res @@ -63,11 +85,42 @@ export const handleFindBoardById = async (req: Request, res: Response) => { if (!validateId(boardId, res)) return; const board = await findBoardById(boardId as string); - const collabID = (await findCollaboratorByIdAndBoard(userID, boardId)).pop() - ?.id; + + let collabID; + let permissionLevel; + if (userID) { + const { id, permissionLevel: perm } = ( + await findCollaboratorByIdAndBoard(userID, boardId) + ).pop() as Collaborator; + + collabID = id; + permissionLevel = perm; + } else { + collabID = undefined; + permissionLevel = undefined; + } + + const users = + board && + (await Promise.all( + board.collaborators.map(async (collabID) => { + const collab = await findCollaboratorById(collabID); + const user = await findUserById(collab?.user as string); + return { + email: user?.email, + avatar: user?.avatar, + initials: getInitials(user?.firstname + ' ' + user?.lastname), + username: user?.firstname + ' ' + user?.lastname, + permission: collab?.permissionLevel, + collabID: collab?.id, + }; + }), + )); return board - ? res.status(HTTP_STATUS.SUCCESS).json({ board, collabID }) + ? res + .status(HTTP_STATUS.SUCCESS) + .json({ board, collabID, users, permissionLevel }) : notFoundError(res); } catch (error) { console.error('Error finding board by ID:', error); @@ -140,7 +193,32 @@ export const handleUpdateBoard = async (req: Request, res: Response) => { if (collab.length !== 0) { collabID = collab.pop()?.uid; const board = await findBoardById(boardId); - return res.status(HTTP_STATUS.SUCCESS).json({ ...board, collabID }); + const users = + board && + (await Promise.all( + board.collaborators.map(async (collabID) => { + const collab = await findCollaboratorById(collabID); + const user = await findUserById(collab?.user as string); + return { + email: user?.email, + avatar: user?.avatar, + initials: getInitials(user?.firstname + ' ' + user?.lastname), + username: user?.firstname + ' ' + user?.lastname, + permission: collab?.permissionLevel, + collabID: collab?.id, + }; + }), + )); + + const { permissionLevel: permission } = (await findCollaboratorById( + collabID as string, + )) as Collaborator; + return res.status(HTTP_STATUS.SUCCESS).json({ + ...board, + collabID, + users, + permission: permission, + }); } collabID = ( @@ -155,8 +233,27 @@ export const handleUpdateBoard = async (req: Request, res: Response) => { } const update = await updateBoard(board, updatedFields); + const users = + board && + (await Promise.all( + (update.collaborators as string[]).map(async (collabID) => { + const collab = await findCollaboratorById(collabID); + const user = await findUserById(collab?.user as string); + return { + email: user?.email, + avatar: user?.avatar, + initials: getInitials(user?.firstname + ' ' + user?.lastname), + username: user?.firstname + ' ' + user?.lastname, + permission: collab?.permissionLevel, + collabID: collab?.id, + }; + }), + )); + const { fastFireOptions: _fastFireOptions, ...fields } = update; // TODO(yousef): Should make a helper method to extract the options - return res.status(HTTP_STATUS.SUCCESS).json({ ...fields, collabID }); + return res + .status(HTTP_STATUS.SUCCESS) + .json({ ...fields, collabID, users, permission: 'edit' }); } else { return notFoundError(res); } @@ -168,6 +265,39 @@ export const handleUpdateBoard = async (req: Request, res: Response) => { } }; +// Update board +export const handleAddUserbyEmail = async (req: Request, res: Response) => { + try { + // The board ID and new parameters are in the body. + const { boardId, email, perm } = req.body; + if (!validateId(boardId, res)) return; + const board = await findBoardById(boardId); + const user = await findUserByEmail(email); + if (user === null) return res.status(HTTP_STATUS.ERROR).json(); + + const collab = await createCollaborator(perm, user.uid, boardId); + await updateBoard(board as Board, { + collaborators: collab.id, + }); + + return res.status(HTTP_STATUS.SUCCESS).json({ + user: { + email: user.email, + avatar: user.avatar, + initials: getInitials(user.firstname + ' ' + user.lastname), + username: user.firstname + ' ' + user.lastname, + permission: perm, + collabID: collab.id, + }, + }); + } catch (error) { + console.error('Error adding user: ', error); + res + .status(HTTP_STATUS.INTERNAL_SERVER_ERROR) + .json({ error: 'Failed to update board' }); + } +}; + // Delete board export const handleDeleteBoard = 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 10a819a..4fa6f9e 100644 --- a/node/src/api/board/board.route.ts +++ b/node/src/api/board/board.route.ts @@ -1,5 +1,6 @@ import express from 'express'; import { + handleAddUserbyEmail, handleCreateBoard, handleDeleteBoard, handleFindBoardById, @@ -27,6 +28,9 @@ router.get('/getCollaboratorsBoard', handleGetCollaboratorBoards); // PUT update a board router.put('/updateBoard', handleUpdateBoard); +// PUT add user by email +router.put('/addUser', handleAddUserbyEmail); + // DELETE board by ID router.delete('/deleteBoard', handleDeleteBoard); diff --git a/node/src/api/collaborator/collaborator.controller.ts b/node/src/api/collaborator/collaborator.controller.ts index ec5c763..377d22a 100644 --- a/node/src/api/collaborator/collaborator.controller.ts +++ b/node/src/api/collaborator/collaborator.controller.ts @@ -8,6 +8,7 @@ import { } from '../../models/collaborator'; import { HTTP_STATUS } from '../../constants'; import { findUserById } from '../../models/user'; +import { getInitials } from '../../utils/misc'; /** * Firebase API controllers, logic for endpoint routes. @@ -73,12 +74,22 @@ export const handleFindCollaboratorById = async ( res: Response, ) => { try { - const collabId = req.body.id; // The collaborator ID parameter is in the body. + const collabId = req.query.id as string; // The collaborator ID parameter is in the body. if (!validateId(collabId, res)) return; const collaborator = await findCollaboratorById(collabId as string); + const user = await findUserById(collaborator?.user as string); if (collaborator) { - res.status(HTTP_STATUS.SUCCESS).json({ collaborator }); + res.status(HTTP_STATUS.SUCCESS).json({ + userInfo: { + email: user?.email, + avatar: user?.avatar, + initials: getInitials(user?.firstname + ' ' + user?.lastname), + username: user?.firstname + ' ' + user?.lastname, + permission: collaborator.permissionLevel, + collabID: collaborator.id, + }, + }); } else { return notFoundError(res); } diff --git a/node/src/api/user/user.controller.ts b/node/src/api/user/user.controller.ts index b1e5dda..354199a 100644 --- a/node/src/api/user/user.controller.ts +++ b/node/src/api/user/user.controller.ts @@ -4,6 +4,7 @@ import { findUserById, deleteUser, updateUser, + User, } from '../../models/user'; import { HTTP_STATUS } from '../../constants'; import { firebaseApp } from '../../firebase/firebaseApp'; @@ -15,12 +16,6 @@ import { firebaseApp } from '../../firebase/firebaseApp'; // TODO: JSDOC -interface User { - id: string; - email: string; - name: string; -} - //Create user export const handleCreateUser = async (req: Request, res: Response) => { try { @@ -63,6 +58,7 @@ export const handleFindUserById = async (req: Request, res: Response) => { } const user = await findUserByEmail(email); + //todo if (user) { res.status(HTTP_STATUS.SUCCESS).json({ user }); } else { @@ -78,7 +74,7 @@ export const handleFindUserById = async (req: Request, res: Response) => { } }; -async function findUserByEmail(email: string): Promise { +export async function findUserByEmail(email: string): Promise { const usersCollection = firebaseApp.firestore().collection('User'); const querySnapshot = await usersCollection.where('email', '==', email).get();