diff --git a/backend/src/agents/workflow.agent.ts b/backend/src/agents/workflow.agent.ts index 61d86608..8db9dbcd 100644 --- a/backend/src/agents/workflow.agent.ts +++ b/backend/src/agents/workflow.agent.ts @@ -21,6 +21,7 @@ import dalPost from '../repository/dalPost'; import dalWorkflow from '../repository/dalWorkflow'; import dalGroupTask, { GroupTaskExpanded } from '../repository/dalGroupTask'; import dalVote from '../repository/dalVote'; +import dalGroup from '../repository/dalGroup'; import { convertPostsFromID } from '../utils/converter'; import { isDistribution, @@ -167,12 +168,22 @@ class WorkflowManager { } else if (assignmentType === AssignmentType.INDIVIDUAL) { const assignedIndividual = taskWorkflow.assignedIndividual; if (!assignedIndividual) return; + + // Fetch the group to get current members + const group = await dalGroup.getById(assignedIndividual.groupID); + if (!group) { + console.error(`Group not found for ID: ${assignedIndividual.groupID}`); + return; // Or throw an error, depending on your error handling + } + const members = group.members; // Use the *current* members from the group + const split: string[][] = await distribute( shuffle(sourcePosts), - sourcePosts.length / assignedIndividual.members.length + sourcePosts.length / members.length ); - for (let i = 0; i < assignedIndividual?.members.length; i++) { - const assignedMember = assignedIndividual.members[i]; + + for (let i = 0; i < members.length; i++) { + const assignedMember = members[i]; const posts = taskWorkflow?.type === TaskWorkflowType.GENERATION ? [] @@ -323,4 +334,4 @@ class WorkflowManager { } } -export default WorkflowManager; +export default WorkflowManager; \ No newline at end of file diff --git a/backend/src/api/auth.ts b/backend/src/api/auth.ts index 754077bd..d36ed7ae 100644 --- a/backend/src/api/auth.ts +++ b/backend/src/api/auth.ts @@ -60,7 +60,7 @@ router.post('/register', async (req, res) => { const exists = await dalUser.findByEmail(body.email); if (exists) { - return res.status(400).send({message: 'Email already in use.'}); + return res.status(400).send({ message: 'Email already in use.' }); } const savedUser = await dalUser.create(body); @@ -107,27 +107,21 @@ router.post('/forgot-password', async (req, res) => { // 5. Send an email to the user with a link containing the token const resetLink = `${ - process.env.CKBOARD_SERVER_ADDRESS!.startsWith('http') - ? '' - : 'https://' + process.env.CKBOARD_SERVER_ADDRESS!.startsWith('http') ? '' : 'https://' }${process.env.CKBOARD_SERVER_ADDRESS!}/reset-password?token=${resetToken}`; try { await generateEmail(email, 'Password Reset Request', resetLink); - return res - .status(200) - .send({ - success: true, - message: - 'If an account with that email exists, a password reset link has been sent.', - }); + return res.status(200).send({ + success: true, + message: + 'If an account with that email exists, a password reset link has been sent.', + }); } catch (err) { - return res - .status(500) - .send({ - success: false, - message: 'There was an error sending the password reset email.', - }); + return res.status(500).send({ + success: false, + message: 'There was an error sending the password reset email.', + }); } } catch (error) { console.error('Error in /forgot-password route:', error); @@ -247,4 +241,29 @@ router.get('/project/:id', isAuthenticated, async (req, res) => { res.status(200).json(users); }); +router.patch('/:boardID/currentView', async (req, res) => { + const { boardID } = req.params; + const { viewType } = req.body; // Expect viewType in the request body + + if (!viewType) { + return res.status(400).json({ error: 'viewType is required' }); + } + + try { + // Update the board with the new currentView + const updatedBoard = await dalUser.update(boardID, { + currentView: viewType, + }); + + if (!updatedBoard) { + return res.status(404).json({ error: 'Board not found' }); + } + + res.status(200).json(updatedBoard); + } catch (error) { + console.error('Error updating currentView:', error); + res.status(500).json({ error: 'Internal server error' }); + } +}); + export default router; diff --git a/backend/src/api/workflows.ts b/backend/src/api/workflows.ts index e71aaad7..1b8410e2 100644 --- a/backend/src/api/workflows.ts +++ b/backend/src/api/workflows.ts @@ -197,9 +197,9 @@ router.get('/task/boards/:id', async (req, res) => { router.delete('/task/:id', async (req, res) => { const id = req.params.id; - await dalWorkflow.remove(WorkflowType.TASK, id); + const deletedWorkflow = await dalWorkflow.remove(WorkflowType.TASK, id); - res.status(200).end(); + res.status(200).json(deletedWorkflow); }); router.get('/task/group/:id', async (req, res) => { @@ -375,23 +375,40 @@ router.post('/task/groupTask/:groupTaskID/submit', async (req, res) => { // if post from bucket => delete from bucket if (workflow.source.type === ContainerType.WORKFLOW) { if (destination.type === ContainerType.BOARD) { - await dalPost.update(post, { type: PostType.BOARD }); + const updatedPost = await dalPost.update(post, { + type: PostType.BOARD, + }); + Socket.Instance.emit( + SocketEvent.POST_CREATE, + updatedPost, + workflow.boardID + ); } else { await dalPost.update(post, { type: PostType.BUCKET }); + Socket.Instance.emit( + SocketEvent.WORKFLOW_POST_SUBMIT, + post, + workflow.boardID + ); } } else { if (workflow.source.type === ContainerType.BOARD) { await dalPost.update(post, { type: PostType.LIST }); + Socket.Instance.emit( + SocketEvent.WORKFLOW_POST_SUBMIT, + post, + workflow.boardID + ); } else if (workflow.source.type === ContainerType.BUCKET) { await dalBucket.removePost(workflow.source.id, [post]); + Socket.Instance.emit( + SocketEvent.WORKFLOW_POST_SUBMIT, + post, + workflow.boardID + ); } } - Socket.Instance.emit( - SocketEvent.WORKFLOW_POST_SUBMIT, - post, - workflow.boardID - ); return res.status(200).json(updatedGroupTask); } catch (e) { return res.status(500).end('Unable to submit post!'); diff --git a/backend/src/constants.ts b/backend/src/constants.ts index 38dd6e61..b0b67926 100644 --- a/backend/src/constants.ts +++ b/backend/src/constants.ts @@ -37,6 +37,12 @@ export enum SocketEvent { WORKFLOW_RUN_TASK = 'WORKFLOW_RUN_TASK', WORKFLOW_PROGRESS_UPDATE = 'WORKFLOW_PROGRESS_UPDATE', WORKFLOW_POST_SUBMIT = 'WORKFLOW_POST_SUBMIT', + WORKFLOW_DELETE_TASK = 'WORKFLOW_DELETE_TASK', + WORKFLOW_POST_ADD = 'WORKFLOW_POST_ADD', + WORKFLOW_TASK_COMPLETE = 'WORKFLOW_TASK_COMPLETE', + + GROUP_CHANGE = 'GROUP_CHANGE', + GROUP_DELETE = 'GROUP_DELETE', NOTIFICATION_CREATE = 'NOTIFICATION_CREATE', BOARD_NOTIFICATION_CREATE = 'BOARD_NOTIFICATION_CREATE', diff --git a/backend/src/models/Trace.ts b/backend/src/models/Trace.ts index c04c17b7..2f4e5af6 100644 --- a/backend/src/models/Trace.ts +++ b/backend/src/models/Trace.ts @@ -5,7 +5,7 @@ import { Severity, } from '@typegoose/typegoose'; -import { BoardType } from './Board'; +import { BoardType, ViewType } from './Board'; @modelOptions({ schemaOptions: { collection: 'trace', timestamps: true }, @@ -30,6 +30,9 @@ export class TraceModel { @prop({ required: true }) boardContext!: string; + @prop({ required: false }) + viewType?: ViewType | undefined; + @prop({ required: true }) agentUserID!: string; diff --git a/backend/src/models/User.ts b/backend/src/models/User.ts index 4ee4f602..881cb2c7 100644 --- a/backend/src/models/User.ts +++ b/backend/src/models/User.ts @@ -1,4 +1,5 @@ import { prop, getModelForClass, modelOptions } from '@typegoose/typegoose'; +import { ViewType } from './Board'; export enum Role { TEACHER = 'TEACHER', @@ -27,6 +28,9 @@ export class UserModel { @prop() public resetPasswordExpires?: Date; + + @prop({ required: false }) + public currentView?: ViewType; } export default getModelForClass(UserModel); diff --git a/backend/src/models/Workflow.ts b/backend/src/models/Workflow.ts index 93a9722d..01a70e11 100644 --- a/backend/src/models/Workflow.ts +++ b/backend/src/models/Workflow.ts @@ -15,6 +15,7 @@ export enum WorkflowType { export enum TaskWorkflowType { PEER_REVIEW = 'PEER_REVIEW', GENERATION = 'GENERATION', + DISTRIBUTION = 'DISTRIBUTION' } export enum DistributionWorkflowType { diff --git a/backend/src/socket/events.ts b/backend/src/socket/events.ts index 0472a0a7..766310c1 100644 --- a/backend/src/socket/events.ts +++ b/backend/src/socket/events.ts @@ -3,6 +3,7 @@ import postEvents from './events/post.events'; import workflowEvents from './events/workflow.events'; import notificationEvents from './events/notification.events'; import bucketEvents from './events/bucket.events'; +import groupEvents from './events/group.events'; import aiEvents from './events/ai.events'; import roomcastEvents from './events/roomcast.events'; @@ -13,6 +14,7 @@ const events = [ ...notificationEvents, ...bucketEvents, ...aiEvents, + ...groupEvents, ...roomcastEvents, ]; diff --git a/backend/src/socket/events/group.events.ts b/backend/src/socket/events/group.events.ts new file mode 100644 index 00000000..d7b01eb1 --- /dev/null +++ b/backend/src/socket/events/group.events.ts @@ -0,0 +1,44 @@ +import { SocketEvent } from '../../constants'; +import { GroupModel } from '../../models/Group'; +import { SocketPayload } from '../types/event.types'; +import { Server, Socket } from 'socket.io'; + +class GroupChange { + static type: SocketEvent = SocketEvent.GROUP_CHANGE; + + static async handleEvent( + input: SocketPayload + ): Promise { + return input.eventData; + } + + static async handleResult( + io: Server, + socket: Socket, + result: GroupModel | null + ) { + socket.to(socket.data.room).emit(this.type, result); + } +} + +class GroupDelete { + static type: SocketEvent = SocketEvent.GROUP_DELETE; + + static async handleEvent( + input: SocketPayload + ): Promise { + return input.eventData; + } + + static async handleResult( + io: Server, + socket: Socket, + result: GroupModel | null + ) { + socket.to(socket.data.room).emit(this.type, result); + } +} + +const groupEvents = [GroupChange, GroupDelete]; + +export default groupEvents; diff --git a/backend/src/socket/events/workflow.events.ts b/backend/src/socket/events/workflow.events.ts index 69fce51c..248e3991 100644 --- a/backend/src/socket/events/workflow.events.ts +++ b/backend/src/socket/events/workflow.events.ts @@ -7,6 +7,8 @@ import { import { SocketPayload } from '../types/event.types'; import { GroupTaskModel } from '../../models/GroupTask'; import { GroupModel } from '../../models/Group'; +import { PostModel } from '../../models/Post'; +import workflowTrace from '../trace/workflow.trace'; class WorkflowRunDistribution { static type: SocketEvent = SocketEvent.WORKFLOW_RUN_DISTRIBUTION; @@ -62,10 +64,82 @@ class WorkflowUpdate { } } +class WorkflowPostSubmit { + static type: SocketEvent = SocketEvent.WORKFLOW_POST_SUBMIT; + + static async handleEvent( + input: SocketPayload + ): Promise { + if (input.trace.allowTracing) await workflowTrace.addPost(input, this.type); + return input.eventData; + } + + static async handleResult(io: Server, socket: Socket, result: PostModel) { + socket.to(socket.data.room).emit(this.type, result); + } +} +class WorkflowDeleteTask { + static type: SocketEvent = SocketEvent.WORKFLOW_DELETE_TASK; + + static async handleEvent( + input: SocketPayload + ): Promise { + return input.eventData; + } + + static async handleResult( + io: Server, + socket: Socket, + result: GroupTaskModel | null + ) { + socket.to(socket.data.room).emit(this.type, result); + } +} + +class WorkflowPostAdd { + static type: SocketEvent = SocketEvent.WORKFLOW_POST_ADD; + + static async handleEvent( + input: SocketPayload + ): Promise { + return input.eventData; + } + + static async handleResult( + io: Server, + socket: Socket, + result: GroupModel | null + ) { + socket.to(socket.data.room).emit(this.type, result); + } +} + +class WorkflowTaskComplete { + static type: SocketEvent = SocketEvent.WORKFLOW_TASK_COMPLETE; + + static async handleEvent( + input: SocketPayload + ): Promise { + return input.eventData; + } + + static async handleResult( + io: Server, + socket: Socket, + result: GroupTaskModel | null + ) { + socket.to(socket.data.room).emit(this.type, result); + } +} + const workflowEvents = [ WorkflowRunDistribution, WorkflowRunTask, WorkflowUpdate, + WorkflowPostSubmit, + WorkflowDeleteTask, + WorkflowPostAdd, + WorkflowTaskComplete, ]; export default workflowEvents; diff --git a/backend/src/socket/trace/base.trace.ts b/backend/src/socket/trace/base.trace.ts index 08982a93..fb531d46 100644 --- a/backend/src/socket/trace/base.trace.ts +++ b/backend/src/socket/trace/base.trace.ts @@ -30,6 +30,7 @@ export const createTrace = async ( boardName: board.name, boardType: board.type, boardContext: boardScopeAsString(board.scope), + viewType: user.currentView, agentUserID: user.userID, agentUserName: user.username, clientTimestamp: new Date(traceContext.clientTimestamp), diff --git a/backend/src/socket/trace/board.trace.ts b/backend/src/socket/trace/board.trace.ts index 72b8628d..5db63d9d 100644 --- a/backend/src/socket/trace/board.trace.ts +++ b/backend/src/socket/trace/board.trace.ts @@ -70,8 +70,11 @@ const clearBoard = async ( eventType: string ) => { const trace = await createTrace(input.trace); + const post = input.eventData.at(0); trace.eventType = eventType; - trace.event = {}; + trace.event = { + postID: post?.postID, + }; await dalTrace.create(trace); }; diff --git a/backend/src/socket/trace/post.trace.ts b/backend/src/socket/trace/post.trace.ts index 3b3df3ea..9f81c049 100644 --- a/backend/src/socket/trace/post.trace.ts +++ b/backend/src/socket/trace/post.trace.ts @@ -18,12 +18,29 @@ import { createTrace } from './base.trace'; const create = async (input: SocketPayload, eventType: string) => { const trace = await createTrace(input.trace); const post = input.eventData; - trace.event = { - postID: post.postID, - postModifiedTitle: post.title, - postModifiedMessage: post.desc, - }; + const tagNames: string[] = []; + const tagIDs: string[] = []; + if (post.tags.length > 0) { + post.tags.forEach((tag) => { + tagNames.push(tag.name); + tagIDs.push(tag.tagID); + }); + trace.event = { + postID: post.postID, + postModifiedTitle: post.title, + postModifiedMessage: post.desc, + postTagNameAdded: tagNames.toString(), + postTagIDAdded: tagIDs.toString(), + }; + } else { + trace.event = { + postID: post.postID, + postModifiedTitle: post.title, + postModifiedMessage: post.desc, + }; + } trace.eventType = eventType; + return dalTrace.create(trace); }; diff --git a/backend/src/socket/trace/workflow.trace.ts b/backend/src/socket/trace/workflow.trace.ts new file mode 100644 index 00000000..3eca578f --- /dev/null +++ b/backend/src/socket/trace/workflow.trace.ts @@ -0,0 +1,38 @@ +import { PostModel } from '../../models/Post'; +import dalTrace from '../../repository/dalTrace'; +import { SocketPayload } from '../types/event.types'; +import { createTrace } from './base.trace'; + +const addPost = async (input: SocketPayload, eventType: string) => { + const trace = await createTrace(input.trace); + const post = input.eventData; + const tagNames: string[] = []; + const tagIDs: string[] = []; + if (post.tags.length > 0) { + post.tags.forEach((tag) => { + tagNames.push(tag.name); + tagIDs.push(tag.tagID); + }); + trace.event = { + postID: post.postID, + postModifiedTitle: post.title, + postModifiedMessage: post.desc, + postTagNameAdded: tagNames.toString(), + postTagIDAdded: tagIDs.toString(), + }; + } else { + trace.event = { + postID: post.postID, + postModifiedTitle: post.title, + postModifiedMessage: post.desc, + }; + } + trace.eventType = eventType; + return dalTrace.create(trace); +}; + +const workflowTrace = { + addPost, +}; + +export default workflowTrace; diff --git a/backend/src/utils/project.helpers.ts b/backend/src/utils/project.helpers.ts index d10f7fd3..d851e240 100644 --- a/backend/src/utils/project.helpers.ts +++ b/backend/src/utils/project.helpers.ts @@ -109,23 +109,28 @@ export async function addUserToProject( return updatedProject; } -export async function addUserToWorkflows(groupID: string, userID: string) { +export const addUserToWorkflows = async (groupID: string, userID: string) => { console.log('in the helper function'); const workflows = await dalWorkflow.getAllByGroupId(groupID); - console.log(workflows); + for (const workflow of workflows) { - if (!workflow.active) return; - let taskExists = true; - const groupTask = await dalGroupTask.getByWorkflowGroup( - groupID, - workflow.workflowID, - userID - ); - console.log(groupTask); - if (groupTask === null) { - taskExists = false; + if (!workflow.active) { + continue; } - console.log(taskExists); + + let taskExists = false; // Initialize as false + try { + const groupTask = await dalGroupTask.getByWorkflowGroup( + groupID, + workflow.workflowID, + userID + ); + if (groupTask) taskExists = true; //if it does not throw error, then set exists to true. + } catch (error) { + taskExists = false; //if there is error then the task does not yet exist. + } + console.log('taskExists: ', taskExists); + if (!taskExists) { console.log('task doesnt exist'); const source = workflow.source; @@ -135,6 +140,7 @@ export async function addUserToWorkflows(groupID: string, userID: string) { string, TaskAction[] >(); + if (workflow.type != TaskWorkflowType.GENERATION) { let sourcePosts; if (source.type == ContainerType.BOARD) { @@ -145,7 +151,6 @@ export async function addUserToWorkflows(groupID: string, userID: string) { sourcePosts = bucket ? bucket.posts : []; } - // if (taskWorkflow?.type === TaskWorkflowType.GENERATION) sourcePosts = []; const commentAction = workflow.requiredActions.find( (a) => a.type == TaskActionType.COMMENT ); @@ -157,25 +162,34 @@ export async function addUserToWorkflows(groupID: string, userID: string) { ); const actions: TaskAction[] = []; - if (commentAction) + if (commentAction) { actions.push({ type: TaskActionType.COMMENT, amountRequired: commentAction.amountRequired, }); - if (tagAction) + } + if (tagAction) { actions.push({ type: TaskActionType.TAG, amountRequired: tagAction.amountRequired, }); - if (!assignedIndividual) return; + } + if (!assignedIndividual) { + continue; + } + const split = await distribute( - shuffle(sourcePosts), + + await shuffle(sourcePosts), sourcePosts.length / assignedIndividual.members.length ); - posts = split[0]; - posts.forEach((post) => { - progress.set(post, actions); - }); + if (split && split.length > 0) { + // Check if split has data. + posts = split[0]; + posts.forEach((post) => { + progress.set(post, actions); + }); + } } console.log( 'groupID:', @@ -204,4 +218,4 @@ export async function addUserToWorkflows(groupID: string, userID: string) { await dalGroupTask.create(newGroupTask); } } -} +}; diff --git a/backend/src/utils/workflow.helpers.ts b/backend/src/utils/workflow.helpers.ts index d0e36556..30228586 100644 --- a/backend/src/utils/workflow.helpers.ts +++ b/backend/src/utils/workflow.helpers.ts @@ -72,11 +72,7 @@ export const movePostsToDestination = async ( destination: Container, posts: string[] ) => { - if (destination.type == ContainerType.BOARD) { - const originals: PostModel[] = await convertPostsFromID(posts); - const copied: PostModel[] = cloneManyToBoard(destination.id, originals); - await dalPost.createMany(copied); - } else { + if (destination.type != ContainerType.BOARD) { await dalBucket.addPost(destination.id, posts); } }; diff --git a/frontend/src/app/components/add-post-modal/add-post.component.ts b/frontend/src/app/components/add-post-modal/add-post.component.ts index b216f74a..f1035db1 100644 --- a/frontend/src/app/components/add-post-modal/add-post.component.ts +++ b/frontend/src/app/components/add-post-modal/add-post.component.ts @@ -269,7 +269,7 @@ export class AddPostComponent { async addPost() { const post = this.getBoardPost(); - await this.canvasService.createPost(post); + await this.canvasService.createPost(post, this.board.boardID); return post; } diff --git a/frontend/src/app/components/buckets-modal/buckets-modal.component.ts b/frontend/src/app/components/buckets-modal/buckets-modal.component.ts index e00feb68..5d0e56b5 100644 --- a/frontend/src/app/components/buckets-modal/buckets-modal.component.ts +++ b/frontend/src/app/components/buckets-modal/buckets-modal.component.ts @@ -265,7 +265,10 @@ export class BucketsModalComponent implements OnInit, OnDestroy { displayAttributes: renderAttr, }; - await this.canvasService.createBoardPostFromBucket(post); + await this.canvasService.createBoardPostFromBucket( + post, + this.board.boardID + ); htmlPost.bucketOnly = false; this.Yoffset += 50; diff --git a/frontend/src/app/components/canvas/canvas.component.ts b/frontend/src/app/components/canvas/canvas.component.ts index 3ce08c9c..5f9bbd39 100644 --- a/frontend/src/app/components/canvas/canvas.component.ts +++ b/frontend/src/app/components/canvas/canvas.component.ts @@ -176,47 +176,58 @@ export class CanvasComponent implements OnInit, OnDestroy { if (this.projectID && this.boardID) { this.user = this.userService.user!; this.isTeacher = this.user.role === Role.TEACHER; - this.canvas = new fabric.Canvas( - 'canvas', - this.embedded - ? this.fabricUtils.embeddedCanvasConfig - : this.fabricUtils.canvasConfig - ); - this.fabricUtils._canvas = this.canvas; - await this.configureBoard(); // Load board data + this.canvas = new fabric.Canvas( + 'canvas', + this.embedded + ? this.fabricUtils.embeddedCanvasConfig + : this.fabricUtils.canvasConfig + ); + this.fabricUtils._canvas = this.canvas; + await this.configureBoard(); // Load board data this.socketService.connect(this.user.userID, this.boardID); this.initCanvasEventsListener(); this.initGroupEventsListener(); window.onbeforeunload = () => this.ngOnDestroy(); - - } else { - // Fallback to ActivatedRoute (for direct routing) - this.activatedRoute.queryParams.subscribe((params) => { - if (params.embedded == 'true') { - this.embedded = true; - } - }); + // Fallback to ActivatedRoute (for direct routing) + this.activatedRoute.queryParams.subscribe((params) => { + if (params.embedded == 'true') { + this.embedded = true; + } + }); - this.user = this.userService.user!; - this.isTeacher = this.user.role === Role.TEACHER; - this.canvas = new fabric.Canvas( - 'canvas', - this.embedded - ? this.fabricUtils.embeddedCanvasConfig - : this.fabricUtils.canvasConfig - ); - this.fabricUtils._canvas = this.canvas; + this.user = this.userService.user!; + this.isTeacher = this.user.role === Role.TEACHER; + this.canvas = new fabric.Canvas( + 'canvas', + this.embedded + ? this.fabricUtils.embeddedCanvasConfig + : this.fabricUtils.canvasConfig + ); + this.fabricUtils._canvas = this.canvas; - this.configureBoard().then(() => { //Use then as configure board is now async + this.configureBoard().then(() => { + //Use then as configure board is now async - this.socketService.connect(this.user.userID, this.boardID); - this.initCanvasEventsListener(); - this.initGroupEventsListener(); + this.socketService.connect(this.user.userID, this.boardID); + this.initCanvasEventsListener(); + this.initGroupEventsListener(); + this.setTraceViewType(); + }); + window.onbeforeunload = () => this.ngOnDestroy(); + } + } - }); - window.onbeforeunload = () => this.ngOnDestroy(); + async setTraceViewType() { + if (this.user) { + this.user.currentView = this.viewType; + this.userService.updateCurrentView( + this.user.userID, + this.user.currentView + ); + return true; } + return false; } initCanvasEventsListener() { @@ -255,8 +266,12 @@ export class CanvasComponent implements OnInit, OnDestroy { } handlePostCreateEvent = (post: Post) => { - if (post.type === PostType.BOARD) { - const fabricPost = new FabricPostComponent(post); + if (post.type === PostType.BOARD && post.boardID === this.boardID) { + const fabricPost = new FabricPostComponent( + this.user.role, + this.board.permissions, + post + ); this.canvas.add(fabricPost); } }; @@ -445,7 +460,11 @@ export class CanvasComponent implements OnInit, OnDestroy { postData.post.type === PostType.BOARD && this.board.scope === BoardScope.PROJECT_PERSONAL ) { - const fabricPost = new FabricPostComponent(postData.post); + const fabricPost = new FabricPostComponent( + this.user.role, + this.board.permissions, + postData.post + ); this.canvas.add(fabricPost); } }; @@ -497,172 +516,196 @@ export class CanvasComponent implements OnInit, OnDestroy { const map = this.activatedRoute.snapshot.paramMap; if (this.projectID && this.boardID) { - //use inputs if available - try { - const tempBoard = await this.boardService.get(this.boardID); // Await here - if (!tempBoard) { - // Handle the case where the board is not found. - console.error("Board not found:", this.boardID); - this.snackbarService.queueSnackbar("Board not found."); - this.router.navigate(['/error']); // Redirect, or show error - return; // IMPORTANT: Stop execution - } - this.board = tempBoard; - this.project = await this.projectService.get(this.projectID); //load project - if(!this.project){ - console.error("Project not found:", this.projectID); - this.snackbarService.queueSnackbar("Project not found."); - this.router.navigate(['/error']); // Redirect, or show error - return; // IMPORTANT: Stop execution - } - - this.intermediateBoardConfig(this.board); // Configure - this.traceService.setTraceContext(this.projectID, this.boardID); //moved here - this.postService.getAllByBoard(this.boardID).then((data) => { //get all posts - data.forEach(async (post) => { - if (post.type == PostType.BOARD) { - const upvotes = await this.upvotesService.getUpvotesByPost( - post.postID - ); - const comments = await this.commentService.getCommentsByPost( - post.postID - ); - this.canvas.add( - new FabricPostComponent(post, { - upvotes: upvotes.length, - comments: comments.length, - }) - ); - } - }); - }); + //use inputs if available + try { + const tempBoard = await this.boardService.get(this.boardID); // Await here + if (!tempBoard) { + // Handle the case where the board is not found. + console.error('Board not found:', this.boardID); + this.snackbarService.queueSnackbar('Board not found.'); + this.router.navigate(['/error']); // Redirect, or show error + return; // IMPORTANT: Stop execution } - catch(error: any) - { - console.error("Error in configure board", error); - this.snackbarService.queueSnackbar("Error in configure board"); - this.router.navigate(['/error']); - return; + this.board = tempBoard; + this.project = await this.projectService.get(this.projectID); //load project + if (!this.project) { + console.error('Project not found:', this.projectID); + this.snackbarService.queueSnackbar('Project not found.'); + this.router.navigate(['/error']); // Redirect, or show error + return; // IMPORTANT: Stop execution } - } - else if (map.has('boardID') && map.has('projectID')) { //get from routed params - this.boardID = this.activatedRoute.snapshot.paramMap.get('boardID') ?? ''; - this.projectID = - this.activatedRoute.snapshot.paramMap.get('projectID') ?? ''; + this.intermediateBoardConfig(this.board); this.traceService.setTraceContext(this.projectID, this.boardID); - try { - const tempBoard = await this.boardService.get(this.boardID); // Await here - if (!tempBoard) { - console.error("Board not found for ID:", this.boardID); - this.snackbarService.queueSnackbar("Board not found."); - this.router.navigate(['/error']); - return; - } - this.board = tempBoard; - - this.project = await this.projectService.get(this.projectID); // Await here - if (!this.project) { - console.error("Project not found for ID:", this.projectID); - this.snackbarService.queueSnackbar("Project not found."); - this.router.navigate(['/error']); - return; + this.postService.getAllByBoard(this.boardID).then((data) => { + //get all posts + data.forEach(async (post) => { + if (post.type == PostType.BOARD) { + const upvotes = await this.upvotesService.getUpvotesByPost( + post.postID + ); + const comments = await this.commentService.getCommentsByPost( + post.postID + ); + + this.canvas.add( + new FabricPostComponent( + this.user.role, + this.board.permissions, + post, + { + upvotes: upvotes.length, + comments: comments.length, + } + ) + ); } - - this.intermediateBoardConfig(this.board); - this.postService.getAllByBoard(this.boardID).then((data) => { - data.forEach(async (post) => { - if (post.type == PostType.BOARD) { - const upvotes = await this.upvotesService.getUpvotesByPost( - post.postID - ); - const comments = await this.commentService.getCommentsByPost( - post.postID - ); - this.canvas.add( - new FabricPostComponent(post, { - upvotes: upvotes.length, - comments: comments.length, - }) - ); - } - }); - if ( - !this.isTeacher && - this.board && - !this.board.viewSettings?.allowCanvas - ) { - this.router.navigateByUrl( - `project/${this.projectID}/board/${ - this.boardID - }/${this.board.defaultView?.toLowerCase()}` - ); - } - }); - } catch (error: any) { - console.error("Error configuring board (routed):", error); - this.snackbarService.queueSnackbar("Error configuring board."); - this.router.navigate(['/error']); // Or handle differently - return; - } - } else if (map.has('projectID')) { //personal board - this.projectID = + }); + }); + } catch (error: any) { + console.error('Error in configure board', error); + this.snackbarService.queueSnackbar('Error in configure board'); + this.router.navigate(['/error']); + return; + } + } else if (map.has('boardID') && map.has('projectID')) { + //get from routed params + this.boardID = this.activatedRoute.snapshot.paramMap.get('boardID') ?? ''; + this.projectID = this.activatedRoute.snapshot.paramMap.get('projectID') ?? ''; - try { - const personalBoard = await this.boardService.getPersonal(this.projectID); - if (personalBoard) { - this.boardID = personalBoard.boardID; - this.traceService.setTraceContext(this.projectID, this.boardID); - } else { - console.error("Personal board not found for projectID:", this.projectID); - this.snackbarService.queueSnackbar("Personal board not found"); - this.router.navigate(['/error']); - return; - } - this.project = await this.projectService.get(this.projectID); // Await here - if (!this.project) { - console.error("Project not found for ID:", this.projectID); - this.snackbarService.queueSnackbar("Project not found."); - this.router.navigate(['/error']); - return; - } + this.traceService.setTraceContext(this.projectID, this.boardID); + + try { + const tempBoard = await this.boardService.get(this.boardID); // Await here + if (!tempBoard) { + console.error('Board not found for ID:', this.boardID); + this.snackbarService.queueSnackbar('Board not found.'); + this.router.navigate(['/error']); + return; + } + this.board = tempBoard; + + this.project = await this.projectService.get(this.projectID); // Await here + if (!this.project) { + console.error('Project not found for ID:', this.projectID); + this.snackbarService.queueSnackbar('Project not found.'); + this.router.navigate(['/error']); + return; + } - this.postService.getAllByBoard(this.boardID).then((data) => { //get all posts - data.forEach(async (post) => { - if (post.type == PostType.BOARD) { - const upvotes = await this.upvotesService.getUpvotesByPost( - post.postID - ); - const comments = await this.commentService.getCommentsByPost( - post.postID - ); - this.canvas.add( - new FabricPostComponent(post, { + this.intermediateBoardConfig(this.board); + this.postService.getAllByBoard(this.boardID).then((data) => { + data.forEach(async (post) => { + if (post.type == PostType.BOARD) { + const upvotes = await this.upvotesService.getUpvotesByPost( + post.postID + ); + const comments = await this.commentService.getCommentsByPost( + post.postID + ); + this.canvas.add( + new FabricPostComponent( + this.user.role, + this.board.permissions, + post, + { + //CRITICAL: Role and permissions! upvotes: upvotes.length, comments: comments.length, - }) - ); - } - }); - if (personalBoard) this.intermediateBoardConfig(personalBoard); - }); - + } + ) + ); + } + }); + if ( + !this.isTeacher && + this.board && + !this.board.viewSettings?.allowCanvas + ) { + this.router.navigateByUrl( + `project/${this.projectID}/board/${ + this.boardID + }/${this.board.defaultView?.toLowerCase()}` + ); + } + }); + } catch (error: any) { + console.error('Error configuring board (routed):', error); + this.snackbarService.queueSnackbar('Error configuring board.'); + this.router.navigate(['/error']); // Or handle differently + return; + } + } else if (map.has('projectID')) { + //personal board + this.projectID = + this.activatedRoute.snapshot.paramMap.get('projectID') ?? ''; + try { + const personalBoard = await this.boardService.getPersonal( + this.projectID + ); + if (personalBoard) { + this.boardID = personalBoard.boardID; + this.traceService.setTraceContext(this.projectID, this.boardID); + } else { + console.error( + 'Personal board not found for projectID:', + this.projectID + ); + this.snackbarService.queueSnackbar('Personal board not found'); + this.router.navigate(['/error']); + return; } - catch(error: any){ - console.error("Error in configure board, personal board", error); - this.snackbarService.queueSnackbar("Error in configure board"); - this.router.navigate(['/error']); - return; + this.project = await this.projectService.get(this.projectID); // Await here + if (!this.project) { + console.error('Project not found for ID:', this.projectID); + this.snackbarService.queueSnackbar('Project not found.'); + this.router.navigate(['/error']); + return; } - } else { //no project id or board id - console.error("Missing required route parameters (projectID and/or boardID)"); - this.snackbarService.queueSnackbar("Error in configure board"); - this.router.navigate(['error']); // Or handle differently - return; // IMPORTANT + this.postService.getAllByBoard(this.boardID).then((data) => { + //get all posts + data.forEach(async (post) => { + if (post.type == PostType.BOARD) { + const upvotes = await this.upvotesService.getUpvotesByPost( + post.postID + ); + const comments = await this.commentService.getCommentsByPost( + post.postID + ); + this.canvas.add( + new FabricPostComponent( + this.user.role, + this.board.permissions, + post, + { + //CRITICAL: Role and permissions! + upvotes: upvotes.length, + comments: comments.length, + } + ) + ); + } + }); + if (personalBoard) this.intermediateBoardConfig(personalBoard); + }); + } catch (error: any) { + console.error('Error in configure board, personal board', error); + this.snackbarService.queueSnackbar('Error in configure board'); + this.router.navigate(['/error']); + return; + } + } else { + //no project id or board id + console.error( + 'Missing required route parameters (projectID and/or boardID)' + ); + this.snackbarService.queueSnackbar('Error in configure board'); + this.router.navigate(['error']); // Or handle differently + return; // IMPORTANT } -} + } // TODO: handle board update from toolbar-menu configureZoom() { diff --git a/frontend/src/app/components/ck-buckets/ck-buckets.component.html b/frontend/src/app/components/ck-buckets/ck-buckets.component.html index ca29e8cd..399d5dc7 100644 --- a/frontend/src/app/components/ck-buckets/ck-buckets.component.html +++ b/frontend/src/app/components/ck-buckets/ck-buckets.component.html @@ -92,6 +92,7 @@

{{ bucket.name }}

([]); } @@ -76,23 +85,23 @@ export class CkBucketsComponent implements OnInit, OnDestroy { // Prioritize Input properties. If they are provided, use them. if (this.boardID && this.projectID) { // We got the IDs from the inputs, proceed as normal. - await this.configureBoard(); // Load board data - this.loadBuckets(); // Load buckets - + await this.configureBoard(); // Load board data + this.loadBuckets(); // Load buckets } else { // Fallback to ActivatedRoute *only* if inputs are not provided. - this.activatedRoute.paramMap.subscribe(async params => { // Use paramMap (Observable) + this.activatedRoute.paramMap.subscribe(async (params) => { + // Use paramMap (Observable) this.boardID = params.get('boardID')!; //use of ! operator this.projectID = params.get('projectID')!; //use of ! operator if (!this.boardID || !this.projectID) { - console.error("Missing boardID or projectID in route parameters"); + console.error('Missing boardID or projectID in route parameters'); this.router.navigate(['/error']); // Redirect to an error page, or handle appropriately return; // IMPORTANT: Stop execution } await this.configureBoard(); // Load board data - this.loadBuckets(); // Load buckets + this.loadBuckets(); // Load buckets }); } this.socketService.connect(this.user.userID, this.boardID); @@ -106,17 +115,17 @@ export class CkBucketsComponent implements OnInit, OnDestroy { } async loadBuckets() { - if(!this.boardID) return; + if (!this.boardID) return; const buckets = await this.bucketService.getAllByBoard(this.boardID); - if(buckets) { - for (const bucket of buckets) { - if (bucket.addedToView) { - this.bucketsOnView.push(bucket); - this.loadBucketPosts(bucket); - } else { - this.buckets.push(bucket); - } + if (buckets) { + for (const bucket of buckets) { + if (bucket.addedToView) { + this.bucketsOnView.push(bucket); + this.loadBucketPosts(bucket); + } else { + this.buckets.push(bucket); } + } } } @@ -143,10 +152,17 @@ export class CkBucketsComponent implements OnInit, OnDestroy { } else { this.board = undefined; } - + if (this.user) { + this.user.currentView = this.viewType; + this.userService.updateCurrentView( + this.user.userID, + this.user.currentView + ); + } this.projectService.get(this.projectID).then((project) => { this.project = project; }); + this.traceService.setTraceContext(this.projectID, this.boardID); } } @@ -189,19 +205,18 @@ export class CkBucketsComponent implements OnInit, OnDestroy { async refreshBuckets() { //Set all the buckets to loading - for(let i = 0; i < this.bucketsOnView.length; i++){ - this.bucketsOnView[i].loading = true; + for (let i = 0; i < this.bucketsOnView.length; i++) { + this.bucketsOnView[i].loading = true; } // Refetch posts for each displayed bucket. for (const bucket of this.bucketsOnView) { - const currentBucket = await this.bucketService.get(bucket.bucketID); // get bucket from the service - if(currentBucket){ - bucket.posts = currentBucket.posts; // update the posts - bucket.htmlPosts = await this.converters.toHTMLPosts(bucket.posts); // reconvert - } - bucket.loading = false; - + const currentBucket = await this.bucketService.get(bucket.bucketID); // get bucket from the service + if (currentBucket) { + bucket.posts = currentBucket.posts; // update the posts + bucket.htmlPosts = await this.converters.toHTMLPosts(bucket.posts); // reconvert + } + bucket.loading = false; } } @@ -271,4 +286,4 @@ export class CkBucketsComponent implements OnInit, OnDestroy { data: data, }); } -} \ No newline at end of file +} diff --git a/frontend/src/app/components/ck-monitor/ck-monitor.component.html b/frontend/src/app/components/ck-monitor/ck-monitor.component.html index f3fc6514..ba6ceb06 100644 --- a/frontend/src/app/components/ck-monitor/ck-monitor.component.html +++ b/frontend/src/app/components/ck-monitor/ck-monitor.component.html @@ -1,4 +1,4 @@ -
+
{ @@ -253,6 +255,14 @@ export class CkMonitorComponent implements OnInit, OnDestroy { this.socketService.connect(this.user.userID, this.boardID); }); } + + if (this.studentView) this.showModels = true; + // Assign the viewType to the user's currentView property on initialization + if (this.user) { + this.user.currentView = this.viewType; + // Send the updated viewType to the backend + this.userService.updateCurrentView(this.user.userID, this.viewType); + } } async loadWorkspaceData(): Promise { @@ -288,7 +298,8 @@ export class CkMonitorComponent implements OnInit, OnDestroy { if (!this.studentView) await this.updateWorkflowData(this.boardID, this.projectID); - + this.socketService.connect(this.user.userID, this.board.boardID); + this.traceService.setTraceContext(this.projectID, this.boardID); return true; } diff --git a/frontend/src/app/components/ck-workspace/ck-workspace.component.html b/frontend/src/app/components/ck-workspace/ck-workspace.component.html index 352fc9dd..5c6f6ef7 100644 --- a/frontend/src/app/components/ck-workspace/ck-workspace.component.html +++ b/frontend/src/app/components/ck-workspace/ck-workspace.component.html @@ -315,12 +315,18 @@

{{ runningGroupTask.workflow.prompt }}

-
+

Group: {{ runningGroupTask.group.name }}

  • {{ member.username }}
+
+

Task Assignment:

+
    +
  • Work Individually
  • +
+
- +

- Canvas - Workspace - Buckets - Monitor + Canvas + Workspace + Buckets + Monitor

- Canvas - Workspace - Buckets - Monitor + Canvas + Workspace + Buckets + Monitor

@@ -222,13 +254,12 @@

Content Settings

-

Post Features

+

Canvas Features

Allow students to move any post

-

Post Features

Allow students to create, edit, and delete postsAnonymization >Show author names to teachers

-

UI Elements

-

- Show buckets to students -

diff --git a/frontend/src/app/components/create-workflow-modal/create-workflow-modal.component.html b/frontend/src/app/components/create-workflow-modal/create-workflow-modal.component.html index 6b3a2997..45a931be 100644 --- a/frontend/src/app/components/create-workflow-modal/create-workflow-modal.component.html +++ b/frontend/src/app/components/create-workflow-modal/create-workflow-modal.component.html @@ -631,7 +631,7 @@

-
+

Required Actions:

+ +
+

+ Assignment Type: + + {{ workflow.assignmentType === AssignmentType.GROUP ? 'Small Group' : 'Individuals' }} + +

+
+ +
+

+ Assigned Groups: + + {{getGroupName(groupID)}} + +

+
+ +
+

+ Assigned Individual: + + {{workflow.assignedIndividual.name}} + +

+
+ +
+

Prompt:

+

{{ workflow.prompt }}

+
+
Remove From Source Enabled diff --git a/frontend/src/app/components/create-workflow-modal/create-workflow-modal.component.ts b/frontend/src/app/components/create-workflow-modal/create-workflow-modal.component.ts index 60dbf249..e5d58167 100644 --- a/frontend/src/app/components/create-workflow-modal/create-workflow-modal.component.ts +++ b/frontend/src/app/components/create-workflow-modal/create-workflow-modal.component.ts @@ -54,6 +54,7 @@ import { UserService } from 'src/app/services/user.service'; import User, { AuthUser, Role } from 'src/app/models/user'; import { SocketEvent } from 'src/app/utils/constants'; import { saveAs } from 'file-saver'; +import { EventBusService } from 'src/app/services/event-bus.service'; interface ChatMessage { role: 'user' | 'assistant'; @@ -151,6 +152,8 @@ export class CreateWorkflowModalComponent implements OnInit, OnDestroy { // Store the socket listener aiResponseListener: Subscription | undefined; + groupMap: Map = new Map(); + @ViewChild('scrollableDiv') private scrollableDiv!: ElementRef; @ViewChild('aiInput') aiInput: ElementRef; @@ -167,6 +170,7 @@ export class CreateWorkflowModalComponent implements OnInit, OnDestroy { public groupService: GroupService, private http: HttpClient, private socketService: SocketService, + private eventBus: EventBusService, private changeDetectorRef: ChangeDetectorRef, @Inject(MAT_DIALOG_DATA) public data: any ) { @@ -176,12 +180,16 @@ export class CreateWorkflowModalComponent implements OnInit, OnDestroy { ngOnInit(): void { // Initialization - this.board = this.data.board; // Load the current board from the passed data - this.tags = this.data.board.tags; // Load tags associated with the board - this.loadGroups(); // Load groups associated with the project - this.upvoteLimit = this.data.board.upvoteLimit; // Load the upvote limit for the board - this.loadBucketsBoards(); // Load buckets and boards for source/destination options - this.loadWorkflows(); // Load existing workflows for the board + this.board = this.data.board; + this.tags = this.data.board.tags; + this.loadGroups().then(() => { + this.groupOptions.forEach((group) => { + this.groupMap.set(group.groupID, group.name); + }); + }); + this.upvoteLimit = this.data.board.upvoteLimit; + this.loadBucketsBoards(); + this.loadWorkflows(); this.user = this.userService.user!; // Set the selected tab index if provided @@ -352,6 +360,7 @@ export class CreateWorkflowModalComponent implements OnInit, OnDestroy { .runTaskWorkflow(workflow) .then(() => { workflow.active = true; + this.eventBus.emit('createWorkflowTask', workflow); this.openSnackBar('Workflow: ' + workflow.name + ' now active!'); }) .catch(() => { @@ -388,6 +397,7 @@ export class CreateWorkflowModalComponent implements OnInit, OnDestroy { handleConfirm: async () => { if (this._isTaskWorkflow(workflow)) { await this.workflowService.removeTask(workflow.workflowID); + this.eventBus.emit('deleteWorkflowTask', workflow.workflowID); } else { await this.workflowService.removeDistribution(workflow.workflowID); } @@ -930,6 +940,10 @@ export class CreateWorkflowModalComponent implements OnInit, OnDestroy { }; } + getGroupName(groupID: string): string { + return this.groupMap.get(groupID) || 'Unknown Group'; + } + ngOnDestroy() { if (this.aiResponseListener) { this.aiResponseListener.unsubscribe(); diff --git a/frontend/src/app/components/csv-download-button/traceDefaults.ts b/frontend/src/app/components/csv-download-button/traceDefaults.ts index 6f718783..eac86663 100644 --- a/frontend/src/app/components/csv-download-button/traceDefaults.ts +++ b/frontend/src/app/components/csv-download-button/traceDefaults.ts @@ -33,6 +33,10 @@ export default [ value: 'boardContext', default: '', }, + { + value: 'viewType', + default: '', + }, { value: 'agentUserID', default: '', diff --git a/frontend/src/app/components/fabric-post/fabric-post.component.ts b/frontend/src/app/components/fabric-post/fabric-post.component.ts index 4f817803..14c8b2db 100644 --- a/frontend/src/app/components/fabric-post/fabric-post.component.ts +++ b/frontend/src/app/components/fabric-post/fabric-post.component.ts @@ -1,6 +1,9 @@ import { Component, Inject } from '@angular/core'; import { fabric } from 'fabric'; import Post from 'src/app/models/post'; +import { Role } from 'src/app/models/user'; +import { BoardService } from 'src/app/services/board.service'; +import { UserService } from 'src/app/services/user.service'; import { POST_DEFAULT_BORDER, POST_DEFAULT_BORDER_THICKNESS, @@ -24,6 +27,8 @@ export interface PostOptions { }) export class FabricPostComponent extends fabric.Group { constructor( + @Inject(Object) userRole: string, + @Inject(Object) boardPermissions: any, @Inject(Object) post: Post, @Inject(Object) options?: PostOptions ) { @@ -39,7 +44,7 @@ export class FabricPostComponent extends fabric.Group { splitByGrapheme: true, }); - const author = new fabric.Textbox(post.author, { + let author = new fabric.Textbox('Anonymous', { name: 'author', width: 300, left: 18, @@ -50,6 +55,27 @@ export class FabricPostComponent extends fabric.Group { splitByGrapheme: true, }); + let hideAuthorName = true; + if (userRole == Role.STUDENT && boardPermissions.showAuthorNameStudent) { + hideAuthorName = false; + } else if ( + userRole == Role.TEACHER && + boardPermissions.showAuthorNameTeacher + ) { + hideAuthorName = false; + } + if (!hideAuthorName) { + author = new fabric.Textbox(post.author, { + name: 'author', + width: 300, + left: 18, + top: title.getScaledHeight() + AUTHOR_OFFSET, + fontSize: 13, + fontFamily: 'Helvetica', + fill: '#555555', + splitByGrapheme: true, + }); + } const desc = new fabric.Textbox( post.desc.length > 200 ? post.desc.substr(0, 200) + '...' : post.desc, { diff --git a/frontend/src/app/components/groups/manage-group-modal/manage-group-modal.component.ts b/frontend/src/app/components/groups/manage-group-modal/manage-group-modal.component.ts index f64235aa..f268a46a 100644 --- a/frontend/src/app/components/groups/manage-group-modal/manage-group-modal.component.ts +++ b/frontend/src/app/components/groups/manage-group-modal/manage-group-modal.component.ts @@ -14,6 +14,10 @@ import { GroupService } from 'src/app/services/group.service'; import { Group } from 'src/app/models/group'; import { MatLegacyTabChangeEvent as MatTabChangeEvent } from '@angular/material/legacy-tabs'; import { SnackbarService } from 'src/app/services/snackbar.service'; +import { SocketEvent } from 'src/app/utils/constants'; +import { SocketService } from 'src/app/services/socket.service'; +import { EventBusService } from 'src/app/services/event-bus.service'; + import { WorkflowService } from 'src/app/services/workflow.service'; import { ContainerType, @@ -49,6 +53,8 @@ export class ManageGroupModalComponent implements OnInit { matcher = new MyErrorStateMatcher(); constructor( + private socketService: SocketService, + private eventBus: EventBusService, private workflowService: WorkflowService, private groupService: GroupService, private postService: PostService, @@ -90,6 +96,8 @@ export class ManageGroupModalComponent implements OnInit { const promises: Promise[] = []; this.updatedGroups.forEach((group) => { promises.push(this.groupService.update(group.groupID, group)); + this.socketService.emit(SocketEvent.GROUP_CHANGE, group.groupID); + this.eventBus.emit('groupChange', group.groupID); if (this.editGroup && this.editGroup.groupID === group.groupID) this.editGroup = group; this.groups.forEach((elem, index) => { @@ -163,16 +171,12 @@ export class ManageGroupModalComponent implements OnInit { (member) => !originalGroup.members.includes(member) ); - console.log(removedMembers); - console.log(addedMembers); const workflows = await this.workflowService.getWorkflowsByGroup( group.groupID ); - console.log(workflows); for (const member of addedMembers) { for (const workflow of workflows) { - if (!workflow.active) return; - console.log('here2', workflow); + if (!workflow.active) continue; let taskExists = true; try { const groupTask = @@ -186,9 +190,7 @@ export class ManageGroupModalComponent implements OnInit { taskExists = false; } if (!taskExists) { - console.log('here5'); const source = workflow.source; - console.log(source); const assignedIndividual = workflow.assignedIndividual; let posts: string[] = []; const progress: Map = new Map< @@ -240,7 +242,6 @@ export class ManageGroupModalComponent implements OnInit { progress.set(post, actions); }); } - console.log(progress); const newGroupTask: GroupTask = { groupTaskID: generateUniqueID(), groupID: group.groupID, @@ -256,12 +257,13 @@ export class ManageGroupModalComponent implements OnInit { userID: member, status: GroupTaskStatus.INACTIVE, }; - console.log(newGroupTask); await this.groupTaskService.createGroupTask(newGroupTask); } } } await this.groupService.update(group.groupID, group); + this.socketService.emit(SocketEvent.GROUP_CHANGE, group.groupID); + this.eventBus.emit('groupChange', group.groupID); this.closeEdit(); this.groups.forEach((elem, index) => { if (elem.groupID === group.groupID) this.groups[index] = group; @@ -288,6 +290,8 @@ export class ManageGroupModalComponent implements OnInit { message: 'Are you sure you want to permanently delete this group?', handleConfirm: async () => { await this.groupService.delete(group.groupID); + this.socketService.emit(SocketEvent.GROUP_DELETE, group); + this.eventBus.emit('deleteWorkflowTask', group); this.groups.forEach((obj, index) => { if (obj.groupID == group.groupID) this.groups.splice(index, 1); }); diff --git a/frontend/src/app/components/html-post/html-post.component.ts b/frontend/src/app/components/html-post/html-post.component.ts index 718a3aa0..f133bd06 100644 --- a/frontend/src/app/components/html-post/html-post.component.ts +++ b/frontend/src/app/components/html-post/html-post.component.ts @@ -2,9 +2,9 @@ import { DELETE } from '@angular/cdk/keycodes'; import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; import { MatLegacyDialog as MatDialog } from '@angular/material/legacy-dialog'; import { PostModalComponent } from 'src/app/components/post-modal/post-modal.component'; -import { Board } from 'src/app/models/board'; +import { Board, ViewType } from 'src/app/models/board'; import Post from 'src/app/models/post'; -import { AuthUser } from 'src/app/models/user'; +import { AuthUser, Role } from 'src/app/models/user'; import { BoardService } from 'src/app/services/board.service'; import { CanvasService } from 'src/app/services/canvas.service'; import { CommentService } from 'src/app/services/comment.service'; @@ -49,6 +49,7 @@ export class HtmlPostComponent implements OnInit { @Input() onCommentEvent: Function; @Input() onTagEvent: Function; @Input() onDeleteEvent: Function; + @Input() currentView: ViewType; @Output() movePostToBoardEvent = new EventEmitter(); exists = true; @@ -75,6 +76,7 @@ export class HtmlPostComponent implements OnInit { ngOnInit(): void { this.user = this.userService.user!; this.postColor = this.post.post.displayAttributes.fillColor; + this.setIsAuthorVisible(); } openPostDialog(commentPress = false) { @@ -91,6 +93,7 @@ export class HtmlPostComponent implements OnInit { onCommentEvent: this.onCommentEvent, onTagEvent: this.onTagEvent, onDeleteEvent: this.onDeleteEvent, + currentView: this.currentView, }, }) .afterClosed() @@ -120,6 +123,23 @@ export class HtmlPostComponent implements OnInit { .catch((e) => this.setError(getErrorMessage(e))); } + async setIsAuthorVisible() { + const board = this.post.board; + if ( + this.user.role == Role.STUDENT && + board?.permissions.showAuthorNameStudent + ) { + this.post.hideAuthorName = false; + } else if ( + this.user.role == Role.TEACHER && + board?.permissions.showAuthorNameTeacher + ) { + this.post.hideAuthorName = false; + } else { + this.post.hideAuthorName = true; + } + } + movePostToBoard(postID: string) { this.movePostToBoardEvent.next(postID); } diff --git a/frontend/src/app/components/list-modal/list-modal.component.ts b/frontend/src/app/components/list-modal/list-modal.component.ts index 03f58dd6..ac272420 100644 --- a/frontend/src/app/components/list-modal/list-modal.component.ts +++ b/frontend/src/app/components/list-modal/list-modal.component.ts @@ -299,7 +299,10 @@ export class ListModalComponent implements OnInit, OnDestroy { displayAttributes: renderAttr, }; - await this.canvasService.createBoardPostFromBucket(post); + await this.canvasService.createBoardPostFromBucket( + post, + this.board.boardID + ); htmlPost.bucketOnly = false; this.Yoffset += 50; diff --git a/frontend/src/app/components/post-modal/post-modal.component.ts b/frontend/src/app/components/post-modal/post-modal.component.ts index 0c4cc12c..68201bfd 100644 --- a/frontend/src/app/components/post-modal/post-modal.component.ts +++ b/frontend/src/app/components/post-modal/post-modal.component.ts @@ -28,7 +28,7 @@ import { generateUniqueID, getErrorMessage } from 'src/app/utils/Utils'; import { Tag } from 'src/app/models/tag'; import { AddPostComponent } from '../add-post-modal/add-post.component'; import Upvote from 'src/app/models/upvote'; -import { Board } from 'src/app/models/board'; +import { Board, ViewType } from 'src/app/models/board'; import { BoardService } from 'src/app/services/board.service'; import { SnackbarService } from 'src/app/services/snackbar.service'; import { Project } from 'src/app/models/project'; @@ -50,6 +50,7 @@ export class PostModalData { post!: Post; user!: User; board!: Board; + currentView?: ViewType; commentPress?: boolean; onCommentEvent?: Function; onTagEvent?: Function; @@ -186,11 +187,16 @@ export class PostModalComponent implements OnInit, OnDestroy { const isTeacher = this.user.role == Role.TEACHER; this.showEditDelete = (isStudent && data.board.permissions.allowStudentEditAddDeletePost) || + (data.currentView && data.currentView != ViewType.CANVAS) || isTeacher; this.canStudentComment = - (isStudent && data.board.permissions.allowStudentCommenting) || isTeacher; + (isStudent && data.board.permissions.allowStudentCommenting) || + (data.currentView && data.currentView != ViewType.CANVAS) || + isTeacher; this.canStudentTag = - (isStudent && data.board.permissions.allowStudentTagging) || isTeacher; + (isStudent && data.board.permissions.allowStudentTagging) || + (data.currentView && data.currentView != ViewType.CANVAS) || + isTeacher; this.showAuthorName = (isStudent && data.board.permissions.showAuthorNameStudent) || (isTeacher && data.board.permissions.showAuthorNameTeacher); @@ -236,7 +242,8 @@ export class PostModalComponent implements OnInit, OnDestroy { if (destType == PostType.BOARD) { if (event.checked) { this.post = await this.canvasService.createBoardPostFromBucket( - this.post + this.post, + this.data.board.boardID ); } else { this.post = ( @@ -531,6 +538,9 @@ export class PostModalComponent implements OnInit, OnDestroy { } private _votingLocked(): boolean { + if (this.data.currentView) { + return false; + } return ( this.user.role == Role.STUDENT && !this.data.board.permissions.allowStudentUpvoting diff --git a/frontend/src/app/models/board.ts b/frontend/src/app/models/board.ts index 4c3d779f..50c10e62 100644 --- a/frontend/src/app/models/board.ts +++ b/frontend/src/app/models/board.ts @@ -71,5 +71,6 @@ export class Board { visible: boolean; defaultTodoDateRange: DateRange | null; defaultView: ViewType | undefined | null; + currentView: ViewType | undefined | null; viewSettings: ViewSettings | undefined | null; } diff --git a/frontend/src/app/models/user.ts b/frontend/src/app/models/user.ts index 749a0da6..43767f9a 100644 --- a/frontend/src/app/models/user.ts +++ b/frontend/src/app/models/user.ts @@ -3,6 +3,7 @@ export interface AuthUser { email: string; username: string; role: string; + currentView: string; } export interface TokenResponse { diff --git a/frontend/src/app/models/workflow.ts b/frontend/src/app/models/workflow.ts index 39e4f146..0f8f04dd 100644 --- a/frontend/src/app/models/workflow.ts +++ b/frontend/src/app/models/workflow.ts @@ -37,6 +37,7 @@ export enum AssignmentType { export enum TaskWorkflowType { PEER_REVIEW = 'PEER_REVIEW', GENERATION = 'GENERATION', + DISTRIBUTION = 'DISTRIBUTION' } export enum TaskActionType { diff --git a/frontend/src/app/services/canvas.service.ts b/frontend/src/app/services/canvas.service.ts index 3908261c..de2b4057 100644 --- a/frontend/src/app/services/canvas.service.ts +++ b/frontend/src/app/services/canvas.service.ts @@ -44,13 +44,21 @@ export class CanvasService { private fabricUtils: FabricUtils ) {} - async createPost(post: Post) { + async createPost(post: Post, boardID: string) { const savedPost = await this.postService.create(post); if (!savedPost) { throw new Error('Failed to create post'); } - - const fabricPost = new FabricPostComponent(post); + const user = await this.userService.user!; + const board = await this.boardService.get(boardID); + if (!board) { + throw new Error('Board not found'); + } + const fabricPost = new FabricPostComponent( + user.role, + board.permissions, + post + ); this.fabricUtils._canvas.add(fabricPost); this.socketService.emit(SocketEvent.POST_CREATE, savedPost); } @@ -88,15 +96,25 @@ export class CanvasService { return savedPost; } - async createBoardPostFromBucket(post: Post): Promise { + async createBoardPostFromBucket(post: Post, boardID): Promise { const upvotes = await this.upvotesService.getUpvotesByPost(post.postID); const comments = await this.commentService.getCommentsByPost(post.postID); post.type = PostType.BOARD; - const fabricPost = new FabricPostComponent(post, { - upvotes: upvotes.length, - comments: comments.length, - }); + const user = await this.userService.user!; + const board = await this.boardService.get(boardID); + if (!board) { + throw new Error('Board not found'); + } + const fabricPost = new FabricPostComponent( + user.role, + board.permissions, + post, + { + upvotes: upvotes.length, + comments: comments.length, + } + ); post = await this.postService.update(post.postID, post); if (!post) { throw new Error('Failed to update post'); diff --git a/frontend/src/app/services/event-bus.service.ts b/frontend/src/app/services/event-bus.service.ts new file mode 100644 index 00000000..c8ed5183 --- /dev/null +++ b/frontend/src/app/services/event-bus.service.ts @@ -0,0 +1,17 @@ +import { Injectable } from '@angular/core'; +import { Subject } from 'rxjs'; + +@Injectable({ + providedIn: 'root', +}) +export class EventBusService { + private eventSubject = new Subject<{ event: string; data: any }>(); + + // Observable to listen for events + event$ = this.eventSubject.asObservable(); + + // Emit an event + emit(event: string, data: any) { + this.eventSubject.next({ event, data }); + } +} diff --git a/frontend/src/app/services/socket.service.ts b/frontend/src/app/services/socket.service.ts index d46f2be5..f6461334 100644 --- a/frontend/src/app/services/socket.service.ts +++ b/frontend/src/app/services/socket.service.ts @@ -45,7 +45,9 @@ export class SocketService { listen(event: SocketEvent, handler: Function): Subscription { try { const observable = this.socket.fromEvent(event); - return observable.subscribe((value) => handler(value)); + return observable.subscribe((value) => { + handler(value); + }); } catch (error) { console.error('Error listening for event:', error); // Handle the error appropriately, e.g., by returning an empty observable diff --git a/frontend/src/app/services/user.service.ts b/frontend/src/app/services/user.service.ts index 7c7eb736..f41c3c32 100644 --- a/frontend/src/app/services/user.service.ts +++ b/frontend/src/app/services/user.service.ts @@ -37,7 +37,7 @@ export class UserService { const result = await this.http .post('auth/register', user) .toPromise(); - + if (result) { localStorage.setItem('user', JSON.stringify(result.user)); localStorage.setItem('access_token', result.token); @@ -158,4 +158,11 @@ export class UserService { } return null; } + + updateCurrentView(userID: string, viewType: string): Promise { + return this.http + .patch(`auth/${userID}/currentView`, { viewType }) + .toPromise() + .catch(() => undefined); // Handle undefined case + } } diff --git a/frontend/src/app/services/workflow.service.ts b/frontend/src/app/services/workflow.service.ts index e7e318ec..0907b4ff 100644 --- a/frontend/src/app/services/workflow.service.ts +++ b/frontend/src/app/services/workflow.service.ts @@ -12,6 +12,9 @@ import { import { BucketService } from './bucket.service'; import { PostService } from './post.service'; import { UserService } from './user.service'; +import { SocketService } from './socket.service'; +import { SocketEvent } from '../utils/constants'; +import { error } from 'console'; @Injectable({ providedIn: 'root', @@ -21,6 +24,7 @@ export class WorkflowService { public userService: UserService, public postService: PostService, public bucketService: BucketService, + private socketService: SocketService, public http: HttpClient ) {} @@ -115,6 +119,8 @@ export class WorkflowService { .delete('workflows/task/' + workflowID) .toPromise(); if (!result) throw new Error('Failed to remove task workflow.'); + + this.socketService.emit(SocketEvent.WORKFLOW_DELETE_TASK, workflowID); return result; } diff --git a/frontend/src/app/utils/constants.ts b/frontend/src/app/utils/constants.ts index 9e5658c2..73bdec15 100644 --- a/frontend/src/app/utils/constants.ts +++ b/frontend/src/app/utils/constants.ts @@ -43,6 +43,12 @@ export enum SocketEvent { WORKFLOW_RUN_TASK = 'WORKFLOW_RUN_TASK', WORKFLOW_PROGRESS_UPDATE = 'WORKFLOW_PROGRESS_UPDATE', WORKFLOW_POST_SUBMIT = 'WORKFLOW_POST_SUBMIT', + WORKFLOW_DELETE_TASK = 'WORKFLOW_DELETE_TASK', + WORKFLOW_POST_ADD = 'WORKFLOW_POST_ADD', + WORKFLOW_TASK_COMPLETE = 'WORKFLOW_TASK_COMPLETE', + + GROUP_CHANGE = 'GROUP_CHANGE', + GROUP_DELETE = 'GROUP_DELETE', NOTIFICATION_CREATE = 'NOTIFICATION_CREATE', BOARD_NOTIFICATION_CREATE = 'BOARD_NOTIFICATION_CREATE',