diff --git a/src/__tests__/boardMutations.test.ts b/src/__tests__/boardMutations.test.ts index 14d765622..fa37a0ff4 100644 --- a/src/__tests__/boardMutations.test.ts +++ b/src/__tests__/boardMutations.test.ts @@ -206,6 +206,35 @@ describe('Test boards mutations', () => { expect(updatedPipelineToOrder.order).toBe(9); }); + test('Watch pipeline', async () => { + const mutation = ` + mutation pipelinesWatch($_id: String!, $isAdd: Boolean, $type: String!) { + pipelinesWatch(_id: $_id, isAdd: $isAdd, type: $type) { + _id + isWatched + } + } + `; + + const watchAddPipeline = await graphqlRequest( + mutation, + 'pipelinesWatch', + { _id: pipeline._id, isAdd: true, type: 'deal' }, + context, + ); + + expect(watchAddPipeline.isWatched).toBe(true); + + const watchRemovePipeline = await graphqlRequest( + mutation, + 'pipelinesWatch', + { _id: pipeline._id, isAdd: false, type: 'deal' }, + context, + ); + + expect(watchRemovePipeline.isWatched).toBe(false); + }); + test('Remove pipeline', async () => { // disconnect stages connected to pipeline await Stages.updateMany({}, { $set: { pipelineId: 'fakePipelineId' } }); diff --git a/src/__tests__/dealMutations.test.ts b/src/__tests__/dealMutations.test.ts index 217ee8bff..64e71d588 100644 --- a/src/__tests__/dealMutations.test.ts +++ b/src/__tests__/dealMutations.test.ts @@ -142,4 +142,23 @@ describe('Test deals mutations', () => { expect(await Deals.findOne({ _id: deal._id })).toBe(null); }); + + test('Watch deal', async () => { + const mutation = ` + mutation dealsWatch($_id: String!, $isAdd: Boolean!) { + dealsWatch(_id: $_id, isAdd: $isAdd) { + _id + isWatched + } + } + `; + + const watchAddDeal = await graphqlRequest(mutation, 'dealsWatch', { _id: deal._id, isAdd: true }, context); + + expect(watchAddDeal.isWatched).toBe(true); + + const watchRemoveDeal = await graphqlRequest(mutation, 'dealsWatch', { _id: deal._id, isAdd: false }, context); + + expect(watchRemoveDeal.isWatched).toBe(false); + }); }); diff --git a/src/__tests__/taskMutations.test.ts b/src/__tests__/taskMutations.test.ts index aae1e4d5c..eee1d13c3 100644 --- a/src/__tests__/taskMutations.test.ts +++ b/src/__tests__/taskMutations.test.ts @@ -142,4 +142,23 @@ describe('Test tasks mutations', () => { expect(await Tasks.findOne({ _id: task._id })).toBe(null); }); + + test('Watch task', async () => { + const mutation = ` + mutation tasksWatch($_id: String!, $isAdd: Boolean!) { + tasksWatch(_id: $_id, isAdd: $isAdd) { + _id + isWatched + } + } + `; + + const watchAddTask = await graphqlRequest(mutation, 'tasksWatch', { _id: task._id, isAdd: true }, context); + + expect(watchAddTask.isWatched).toBe(true); + + const watchRemoveTask = await graphqlRequest(mutation, 'tasksWatch', { _id: task._id, isAdd: false }, context); + + expect(watchRemoveTask.isWatched).toBe(false); + }); }); diff --git a/src/__tests__/ticketMutations.test.ts b/src/__tests__/ticketMutations.test.ts index 6c3377daa..de05f5b1d 100644 --- a/src/__tests__/ticketMutations.test.ts +++ b/src/__tests__/ticketMutations.test.ts @@ -142,4 +142,28 @@ describe('Test tickets mutations', () => { expect(await Tickets.findOne({ _id: ticket._id })).toBe(null); }); + + test('Watch ticket', async () => { + const mutation = ` + mutation ticketsWatch($_id: String!, $isAdd: Boolean!) { + ticketsWatch(_id: $_id, isAdd: $isAdd) { + _id + isWatched + } + } + `; + + const watchAddTicket = await graphqlRequest(mutation, 'ticketsWatch', { _id: ticket._id, isAdd: true }, context); + + expect(watchAddTicket.isWatched).toBe(true); + + const watchRemoveTicket = await graphqlRequest( + mutation, + 'ticketsWatch', + { _id: ticket._id, isAdd: false }, + context, + ); + + expect(watchRemoveTicket.isWatched).toBe(false); + }); }); diff --git a/src/data/permissions/actions/permission.ts b/src/data/permissions/actions/permission.ts index ab9ab6017..6bb18765b 100644 --- a/src/data/permissions/actions/permission.ts +++ b/src/data/permissions/actions/permission.ts @@ -151,6 +151,7 @@ export const moduleObjects = { 'dealPipelinesAdd', 'dealPipelinesEdit', 'dealPipelinesUpdateOrder', + 'dealPipelinesWatch', 'dealPipelinesRemove', 'dealStagesAdd', 'dealStagesEdit', @@ -160,6 +161,7 @@ export const moduleObjects = { 'dealsEdit', 'dealsRemove', 'dealsUpdateOrder', + 'dealsWatch', ], }, { @@ -190,6 +192,10 @@ export const moduleObjects = { name: 'dealPipelinesUpdateOrder', description: 'Update pipeline order', }, + { + name: 'dealPipelinesWatch', + description: 'Deal pipeline watch', + }, { name: 'dealStagesAdd', description: 'Add deal stage', @@ -222,6 +228,10 @@ export const moduleObjects = { name: 'dealsRemove', description: 'Remove deal', }, + { + name: 'dealsWatch', + description: 'Watch deal', + }, ], }, tickets: { @@ -239,6 +249,7 @@ export const moduleObjects = { 'ticketPipelinesAdd', 'ticketPipelinesEdit', 'ticketPipelinesUpdateOrder', + 'ticketPipelinesWatch', 'ticketPipelinesRemove', 'ticketStagesAdd', 'ticketStagesEdit', @@ -248,6 +259,7 @@ export const moduleObjects = { 'ticketsEdit', 'ticketsRemove', 'ticketsUpdateOrder', + 'ticketsWatch', ], }, { @@ -274,6 +286,10 @@ export const moduleObjects = { name: 'ticketPipelinesRemove', description: 'Remove ticket pipeline', }, + { + name: 'ticketPipelinesWatch', + description: 'Ticket pipeline watch', + }, { name: 'ticketPipelinesUpdateOrder', description: 'Update pipeline order', @@ -310,6 +326,10 @@ export const moduleObjects = { name: 'ticketsRemove', description: 'Remove ticket', }, + { + name: 'ticketsWatch', + description: 'Watch ticket', + }, ], }, tasks: { @@ -327,6 +347,7 @@ export const moduleObjects = { 'taskPipelinesAdd', 'taskPipelinesEdit', 'taskPipelinesUpdateOrder', + 'taskPipelinesWatch', 'taskPipelinesRemove', 'taskStagesAdd', 'taskStagesEdit', @@ -336,6 +357,7 @@ export const moduleObjects = { 'tasksEdit', 'tasksRemove', 'tasksUpdateOrder', + 'tasksWatch', ], }, { @@ -362,6 +384,10 @@ export const moduleObjects = { name: 'taskPipelinesRemove', description: 'Remove task pipeline', }, + { + name: 'taskPipelinesWatch', + description: 'Task pipeline watch', + }, { name: 'taskPipelinesUpdateOrder', description: 'Update pipeline order', @@ -398,6 +424,10 @@ export const moduleObjects = { name: 'tasksRemove', description: 'Remove task', }, + { + name: 'tasksWatch', + description: 'Watch task', + }, ], }, engages: { diff --git a/src/data/resolvers/boardUtils.ts b/src/data/resolvers/boardUtils.ts index 7a2160040..f8057237d 100644 --- a/src/data/resolvers/boardUtils.ts +++ b/src/data/resolvers/boardUtils.ts @@ -1,12 +1,31 @@ import { Boards, Pipelines, Stages } from '../../db/models'; import { NOTIFICATION_TYPES } from '../../db/models/definitions/constants'; -import { IDealDocument } from '../../db/models/definitions/deals'; -import { ITicketDocument } from '../../db/models/definitions/tickets'; import { IUserDocument } from '../../db/models/definitions/users'; import { can } from '../permissions/utils'; import { checkLogin } from '../permissions/wrappers'; import utils from '../utils'; +export const notifiedUserIds = async (item: any) => { + const userIds: string[] = []; + + if (item.assignedUserIds) { + userIds.concat(item.assignedUserIds); + } + + if (item.watchedUserIds) { + userIds.concat(item.watchedUserIds); + } + + const stage = await Stages.getStage(item.stageId || ''); + const pipeline = await Pipelines.getPipeline(stage.pipelineId || ''); + + if (pipeline.watchedUserIds) { + userIds.concat(pipeline.watchedUserIds); + } + + return userIds; +}; + /** * Send notification to all members of this content except the sender */ @@ -14,7 +33,7 @@ export const sendNotifications = async ( stageId: string, user: IUserDocument, type: string, - assignedUsers: string[], + userIds: string[], content: string, contentType: string, ) => { @@ -38,23 +57,19 @@ export const sendNotifications = async ( link: `/${contentType}/board?id=${pipeline.boardId}&pipelineId=${pipeline._id}`, // exclude current user - receivers: (assignedUsers || []).filter(id => id !== user._id), + receivers: userIds.filter(id => id !== user._id), }); }; -export const manageNotifications = async ( - collection: any, - item: IDealDocument | ITicketDocument, - user: IUserDocument, - type: string, -) => { +export const manageNotifications = async (collection: any, item: any, user: IUserDocument, type: string) => { const { _id } = item; const oldItem = await collection.findOne({ _id }); - const oldAssignedUserIds = oldItem ? oldItem.assignedUserIds || [] : []; - const assignedUserIds = item.assignedUserIds || []; + + const oldUserIds = await notifiedUserIds(oldItem); + const userIds = await notifiedUserIds(item); // new assignee users - const newUserIds = assignedUserIds.filter(userId => oldAssignedUserIds.indexOf(userId) < 0); + const newUserIds = userIds.filter(userId => oldUserIds.indexOf(userId) < 0); if (newUserIds.length > 0) { await sendNotifications( @@ -68,7 +83,7 @@ export const manageNotifications = async ( } // remove from assignee users - const removedUserIds = oldAssignedUserIds.filter(userId => assignedUserIds.indexOf(userId) < 0); + const removedUserIds = oldUserIds.filter(userId => userIds.indexOf(userId) < 0); if (removedUserIds.length > 0) { await sendNotifications( @@ -87,19 +102,14 @@ export const manageNotifications = async ( item.stageId || '', user, NOTIFICATION_TYPES[`${type.toUpperCase()}_EDIT`], - assignedUserIds, + userIds, `'{userName}' edited your ${type} '${item.name}'`, type, ); } }; -export const itemsChange = async ( - collection: any, - item: IDealDocument | ITicketDocument, - type: string, - destinationStageId: string, -) => { +export const itemsChange = async (collection: any, item: any, type: string, destinationStageId: string) => { const oldItem = await collection.findOne({ _id: item._id }); const oldStageId = oldItem ? oldItem.stageId || '' : ''; @@ -118,7 +128,7 @@ export const itemsChange = async ( return content; }; -export const boardId = async (item: IDealDocument | ITicketDocument) => { +export const boardId = async (item: any) => { const stage = await Stages.findOne({ _id: item.stageId }); if (!stage) { diff --git a/src/data/resolvers/deals.ts b/src/data/resolvers/deals.ts index 36e5f6973..c38449ffc 100644 --- a/src/data/resolvers/deals.ts +++ b/src/data/resolvers/deals.ts @@ -1,5 +1,6 @@ import { Companies, Customers, Pipelines, Products, Stages, Users } from '../../db/models'; import { IDealDocument } from '../../db/models/definitions/deals'; +import { IUserDocument } from '../../db/models/definitions/users'; import { boardId } from './boardUtils'; export default { @@ -66,7 +67,17 @@ export default { return boardId(deal); }, - async stage(deal: IDealDocument) { + stage(deal: IDealDocument) { return Stages.findOne({ _id: deal.stageId }); }, + + isWatched(deal: IDealDocument, _args, { user }: { user: IUserDocument }) { + const watchedUserIds = deal.watchedUserIds || []; + + if (watchedUserIds.includes(user._id)) { + return true; + } + + return false; + }, }; diff --git a/src/data/resolvers/mutations/boards.ts b/src/data/resolvers/mutations/boards.ts index 9ed7e9f5a..4ebffb3cc 100644 --- a/src/data/resolvers/mutations/boards.ts +++ b/src/data/resolvers/mutations/boards.ts @@ -72,6 +72,25 @@ const boardMutations = { return Pipelines.updateOrder(orders); }, + /** + * Watch pipeline + */ + async pipelinesWatch( + _root, + { _id, isAdd, type }: { _id: string; isAdd: boolean; type: string }, + { user }: { user: IUserDocument }, + ) { + await checkPermission(type, user, 'pipelinesWatch'); + + const pipeline = await Pipelines.findOne({ _id }); + + if (!pipeline) { + throw new Error('Pipeline not found'); + } + + return Pipelines.watchPipeline(_id, isAdd, user._id); + }, + /** * Remove pipeline */ diff --git a/src/data/resolvers/mutations/deals.ts b/src/data/resolvers/mutations/deals.ts index 82ab8beb9..2c32937d6 100644 --- a/src/data/resolvers/mutations/deals.ts +++ b/src/data/resolvers/mutations/deals.ts @@ -4,7 +4,7 @@ import { NOTIFICATION_TYPES } from '../../../db/models/definitions/constants'; import { IDeal } from '../../../db/models/definitions/deals'; import { IUserDocument } from '../../../db/models/definitions/users'; import { checkPermission } from '../../permissions/wrappers'; -import { itemsChange, manageNotifications, sendNotifications } from '../boardUtils'; +import { itemsChange, manageNotifications, notifiedUserIds, sendNotifications } from '../boardUtils'; interface IDealsEdit extends IDeal { _id: string; @@ -68,7 +68,7 @@ const dealMutations = { deal.stageId || '', user, NOTIFICATION_TYPES.DEAL_CHANGE, - deal.assignedUserIds || [], + await notifiedUserIds(deal), content, 'deal', ); @@ -97,18 +97,32 @@ const dealMutations = { deal.stageId || '', user, NOTIFICATION_TYPES.DEAL_DELETE, - deal.assignedUserIds || [], + await notifiedUserIds(deal), `'{userName}' deleted deal: '${deal.name}'`, 'deal', ); return Deals.removeDeal(_id); }, + + /** + * Watch deal + */ + async dealsWatch(_root, { _id, isAdd }: { _id: string; isAdd: boolean }, { user }: { user: IUserDocument }) { + const deal = await Deals.findOne({ _id }); + + if (!deal) { + throw new Error('Deal not found'); + } + + return Deals.watchDeal(_id, isAdd, user._id); + }, }; checkPermission(dealMutations, 'dealsAdd', 'dealsAdd'); checkPermission(dealMutations, 'dealsEdit', 'dealsEdit'); checkPermission(dealMutations, 'dealsUpdateOrder', 'dealsUpdateOrder'); checkPermission(dealMutations, 'dealsRemove', 'dealsRemove'); +checkPermission(dealMutations, 'dealsWatch', 'dealsWatch'); export default dealMutations; diff --git a/src/data/resolvers/mutations/tasks.ts b/src/data/resolvers/mutations/tasks.ts index b5f29d0f4..a6e37385a 100644 --- a/src/data/resolvers/mutations/tasks.ts +++ b/src/data/resolvers/mutations/tasks.ts @@ -4,7 +4,7 @@ import { NOTIFICATION_TYPES } from '../../../db/models/definitions/constants'; import { ITask } from '../../../db/models/definitions/tasks'; import { IUserDocument } from '../../../db/models/definitions/users'; import { checkPermission } from '../../permissions/wrappers'; -import { itemsChange, manageNotifications, sendNotifications } from '../boardUtils'; +import { itemsChange, manageNotifications, notifiedUserIds, sendNotifications } from '../boardUtils'; interface ITasksEdit extends ITask { _id: string; @@ -15,21 +15,10 @@ const taskMutations = { * Create new task */ async tasksAdd(_root, doc: ITask, { user }: { user: IUserDocument }) { - const task = await Tasks.createTask({ + return Tasks.createTask({ ...doc, modifiedBy: user._id, }); - - await sendNotifications( - task.stageId || '', - user, - NOTIFICATION_TYPES.TASK_ADD, - task.assignedUserIds || [], - `'{userName}' invited you to the '${task.name}'.`, - 'task', - ); - - return task; }, /** @@ -67,7 +56,7 @@ const taskMutations = { task.stageId || '', user, NOTIFICATION_TYPES.TASK_CHANGE, - task.assignedUserIds || [], + await notifiedUserIds(task), content, 'task', ); @@ -96,18 +85,32 @@ const taskMutations = { task.stageId || '', user, NOTIFICATION_TYPES.TASK_DELETE, - task.assignedUserIds || [], + await notifiedUserIds(task), `'{userName}' deleted task: '${task.name}'`, 'task', ); return Tasks.removeTask(_id); }, + + /** + * Watch task + */ + async tasksWatch(_root, { _id, isAdd }: { _id: string; isAdd: boolean }, { user }: { user: IUserDocument }) { + const task = await Tasks.findOne({ _id }); + + if (!task) { + throw new Error('Task not found'); + } + + return Tasks.watchTask(_id, isAdd, user._id); + }, }; checkPermission(taskMutations, 'tasksAdd', 'tasksAdd'); checkPermission(taskMutations, 'tasksEdit', 'tasksEdit'); checkPermission(taskMutations, 'tasksUpdateOrder', 'tasksUpdateOrder'); checkPermission(taskMutations, 'tasksRemove', 'tasksRemove'); +checkPermission(taskMutations, 'tasksWatch', 'tasksWatch'); export default taskMutations; diff --git a/src/data/resolvers/mutations/tickets.ts b/src/data/resolvers/mutations/tickets.ts index aa8ecb52d..8e513f70a 100644 --- a/src/data/resolvers/mutations/tickets.ts +++ b/src/data/resolvers/mutations/tickets.ts @@ -4,7 +4,7 @@ import { NOTIFICATION_TYPES } from '../../../db/models/definitions/constants'; import { ITicket } from '../../../db/models/definitions/tickets'; import { IUserDocument } from '../../../db/models/definitions/users'; import { checkPermission } from '../../permissions/wrappers'; -import { itemsChange, manageNotifications, sendNotifications } from '../boardUtils'; +import { itemsChange, manageNotifications, notifiedUserIds, sendNotifications } from '../boardUtils'; interface ITicketsEdit extends ITicket { _id: string; @@ -15,21 +15,10 @@ const ticketMutations = { * Create new ticket */ async ticketsAdd(_root, doc: ITicket, { user }: { user: IUserDocument }) { - const ticket = await Tickets.createTicket({ + return Tickets.createTicket({ ...doc, modifiedBy: user._id, }); - - await sendNotifications( - ticket.stageId || '', - user, - NOTIFICATION_TYPES.TICKET_ADD, - ticket.assignedUserIds || [], - `'{userName}' invited you to the '${ticket.name}'.`, - 'ticket', - ); - - return ticket; }, /** @@ -67,7 +56,7 @@ const ticketMutations = { ticket.stageId || '', user, NOTIFICATION_TYPES.TICKET_CHANGE, - ticket.assignedUserIds || [], + await notifiedUserIds(ticket), content, 'ticket', ); @@ -96,18 +85,32 @@ const ticketMutations = { ticket.stageId || '', user, NOTIFICATION_TYPES.TICKET_DELETE, - ticket.assignedUserIds || [], + await notifiedUserIds(ticket), `'{userName}' deleted ticket: '${ticket.name}'`, 'ticket', ); return Tickets.removeTicket(_id); }, + + /** + * Watch ticket + */ + async ticketsWatch(_root, { _id, isAdd }: { _id: string; isAdd: boolean }, { user }: { user: IUserDocument }) { + const ticket = await Tickets.findOne({ _id }); + + if (!ticket) { + throw new Error('Ticket not found'); + } + + return Tickets.watchTicket(_id, isAdd, user._id); + }, }; checkPermission(ticketMutations, 'ticketsAdd', 'ticketsAdd'); checkPermission(ticketMutations, 'ticketsEdit', 'ticketsEdit'); checkPermission(ticketMutations, 'ticketsUpdateOrder', 'ticketsUpdateOrder'); checkPermission(ticketMutations, 'ticketsRemove', 'ticketsRemove'); +checkPermission(ticketMutations, 'ticketsWatch', 'ticketsWatch'); export default ticketMutations; diff --git a/src/data/resolvers/pipeline.ts b/src/data/resolvers/pipeline.ts index 738fdefb8..d75521586 100644 --- a/src/data/resolvers/pipeline.ts +++ b/src/data/resolvers/pipeline.ts @@ -1,6 +1,7 @@ import { Users } from '../../db/models'; import { IPipelineDocument } from '../../db/models/definitions/boards'; import { PIPELINE_VISIBLITIES } from '../../db/models/definitions/constants'; +import { IUserDocument } from '../../db/models/definitions/users'; export default { members(pipeline: IPipelineDocument, {}) { @@ -10,4 +11,14 @@ export default { return []; }, + + isWatched(pipeline: IPipelineDocument, _args, { user }: { user: IUserDocument }) { + const watchedUserIds = pipeline.watchedUserIds || []; + + if (watchedUserIds.includes(user._id)) { + return true; + } + + return false; + }, }; diff --git a/src/data/resolvers/tasks.ts b/src/data/resolvers/tasks.ts index 6cf379698..dbe778202 100644 --- a/src/data/resolvers/tasks.ts +++ b/src/data/resolvers/tasks.ts @@ -1,5 +1,6 @@ import { Companies, Customers, Pipelines, Stages, Users } from '../../db/models'; import { ITaskDocument } from '../../db/models/definitions/tasks'; +import { IUserDocument } from '../../db/models/definitions/users'; import { boardId } from './boardUtils'; export default { @@ -29,7 +30,17 @@ export default { return boardId(task); }, - async stage(task: ITaskDocument) { + stage(task: ITaskDocument) { return Stages.findOne({ _id: task.stageId }); }, + + isWatched(task: ITaskDocument, _args, { user }: { user: IUserDocument }) { + const watchedUserIds = task.watchedUserIds || []; + + if (watchedUserIds.includes(user._id)) { + return true; + } + + return false; + }, }; diff --git a/src/data/resolvers/tickets.ts b/src/data/resolvers/tickets.ts index a398d692a..4114eb662 100644 --- a/src/data/resolvers/tickets.ts +++ b/src/data/resolvers/tickets.ts @@ -1,5 +1,6 @@ import { Companies, Customers, Pipelines, Stages, Users } from '../../db/models'; import { ITicketDocument } from '../../db/models/definitions/tickets'; +import { IUserDocument } from '../../db/models/definitions/users'; import { boardId } from './boardUtils'; export default { @@ -29,7 +30,17 @@ export default { return boardId(ticket); }, - async stage(ticket: ITicketDocument) { + stage(ticket: ITicketDocument) { return Stages.findOne({ _id: ticket.stageId }); }, + + isWatched(ticket: ITicketDocument, _args, { user }: { user: IUserDocument }) { + const watchedUserIds = ticket.watchedUserIds || []; + + if (watchedUserIds.includes(user._id)) { + return true; + } + + return false; + }, }; diff --git a/src/data/schema/board.ts b/src/data/schema/board.ts index 53b1a27c5..6dbc60ba9 100644 --- a/src/data/schema/board.ts +++ b/src/data/schema/board.ts @@ -20,6 +20,7 @@ export const types = ` memberIds: [String] members: [User] bgColor: String + isWatched: Boolean ${commonTypes} } @@ -89,6 +90,7 @@ export const mutations = ` pipelinesAdd(${commonParams}, ${pipelineParams}): Pipeline pipelinesEdit(_id: String!, ${commonParams}, ${pipelineParams}): Pipeline pipelinesUpdateOrder(orders: [OrderItem]): [Pipeline] + pipelinesWatch(_id: String!, isAdd: Boolean, type: String!): Pipeline pipelinesRemove(_id: String!): JSON stagesUpdateOrder(orders: [OrderItem]): [Stage] diff --git a/src/data/schema/deal.ts b/src/data/schema/deal.ts index b7c6bcd20..027c9347d 100644 --- a/src/data/schema/deal.ts +++ b/src/data/schema/deal.ts @@ -24,6 +24,7 @@ export const types = ` modifiedAt: Date modifiedBy: String stage: Stage + isWatched: Boolean ${commonTypes} } @@ -92,4 +93,5 @@ export const mutations = ` dealsChange( _id: String!, destinationStageId: String): Deal dealsUpdateOrder(stageId: String!, orders: [OrderItem]): [Deal] dealsRemove(_id: String!): Deal + dealsWatch(_id: String, isAdd: Boolean): Deal `; diff --git a/src/data/schema/task.ts b/src/data/schema/task.ts index 25d1a519a..05bd4443f 100644 --- a/src/data/schema/task.ts +++ b/src/data/schema/task.ts @@ -18,6 +18,7 @@ export const types = ` companies: [Company] customers: [Customer] assignedUsers: [User] + isWatched: Boolean stage: Stage pipeline: Pipeline modifiedAt: Date @@ -64,4 +65,5 @@ export const mutations = ` tasksChange( _id: String!, destinationStageId: String): Task tasksUpdateOrder(stageId: String!, orders: [OrderItem]): [Task] tasksRemove(_id: String!): Task + tasksWatch(_id: String, isAdd: Boolean): Task `; diff --git a/src/data/schema/ticket.ts b/src/data/schema/ticket.ts index 75bca3c42..ea50f1453 100644 --- a/src/data/schema/ticket.ts +++ b/src/data/schema/ticket.ts @@ -19,6 +19,7 @@ export const types = ` companies: [Company] customers: [Customer] assignedUsers: [User] + isWatched: Boolean stage: Stage pipeline: Pipeline modifiedAt: Date @@ -67,4 +68,5 @@ export const mutations = ` ticketsChange( _id: String!, destinationStageId: String): Ticket ticketsUpdateOrder(stageId: String!, orders: [OrderItem]): [Ticket] ticketsRemove(_id: String!): Ticket + ticketsWatch(_id: String, isAdd: Boolean): Ticket `; diff --git a/src/db/models/Boards.ts b/src/db/models/Boards.ts index bcff9f7ab..630ae5235 100644 --- a/src/db/models/Boards.ts +++ b/src/db/models/Boards.ts @@ -1,5 +1,6 @@ import { Model, model } from 'mongoose'; import { Deals, Tasks, Tickets } from './'; +import { updateOrder, watchItem } from './boardUtils'; import { boardSchema, IBoard, @@ -12,7 +13,6 @@ import { stageSchema, } from './definitions/boards'; import { BOARD_TYPES } from './definitions/constants'; -import { updateOrder } from './utils'; export interface IOrderInput { _id: string; @@ -128,6 +128,7 @@ export interface IPipelineModel extends Model { createPipeline(doc: IPipeline, stages: IPipelineStage[]): Promise; updatePipeline(_id: string, doc: IPipeline, stages: IPipelineStage[]): Promise; updateOrder(orders: IOrderInput[]): Promise; + watchPipeline(_id: string, isAdd: boolean, userId: string): void; removePipeline(_id: string): void; } @@ -197,6 +198,10 @@ export const loadPipelineClass = () => { return Pipelines.deleteOne({ _id }); } + + public static async watchPipeline(_id: string, isAdd: boolean, userId: string) { + return watchItem(Pipelines, _id, isAdd, userId); + } } pipelineSchema.loadClass(Pipeline); diff --git a/src/db/models/Deals.ts b/src/db/models/Deals.ts index 67b54884a..9a206aea3 100644 --- a/src/db/models/Deals.ts +++ b/src/db/models/Deals.ts @@ -1,8 +1,8 @@ import { Model, model } from 'mongoose'; import { ActivityLogs } from '.'; +import { changeCompany, changeCustomer, updateOrder, watchItem } from './boardUtils'; import { IOrderInput } from './definitions/boards'; import { dealSchema, IDeal, IDealDocument } from './definitions/deals'; -import { changeCompany, changeCustomer, updateOrder } from './utils'; export interface IDealModel extends Model { getDeal(_id: string): Promise; @@ -10,6 +10,7 @@ export interface IDealModel extends Model { updateDeal(_id: string, doc: IDeal): Promise; updateOrder(stageId: string, orders: IOrderInput[]): Promise; removeDeal(_id: string): void; + watchDeal(_id: string, isAdd: boolean, userId: string): void; changeCustomer(newCustomerId: string, oldCustomerIds: string[]): Promise; changeCompany(newCompanyId: string, oldCompanyIds: string[]): Promise; } @@ -75,6 +76,13 @@ export const loadDealClass = () => { return deal.remove(); } + /** + * Watch deal + */ + public static async watchDeal(_id: string, isAdd: boolean, userId: string) { + return watchItem(Deals, _id, isAdd, userId); + } + /** * Change customer */ diff --git a/src/db/models/Tasks.ts b/src/db/models/Tasks.ts index dde8fde82..b619b3e55 100644 --- a/src/db/models/Tasks.ts +++ b/src/db/models/Tasks.ts @@ -1,14 +1,15 @@ import { Model, model } from 'mongoose'; import { ActivityLogs } from '.'; +import { changeCompany, changeCustomer, updateOrder, watchItem } from './boardUtils'; import { IOrderInput } from './definitions/boards'; import { ITask, ITaskDocument, taskSchema } from './definitions/tasks'; -import { changeCompany, changeCustomer, updateOrder } from './utils'; export interface ITaskModel extends Model { createTask(doc: ITask): Promise; updateTask(_id: string, doc: ITask): Promise; updateOrder(stageId: string, orders: IOrderInput[]): Promise; removeTask(_id: string): void; + watchTask(_id: string, isAdd: boolean, userId: string): void; changeCustomer(newCustomerId: string, oldCustomerIds: string[]): Promise; changeCompany(newCompanyId: string, oldCompanyIds: string[]): Promise; } @@ -64,6 +65,13 @@ export const loadTaskClass = () => { return task.remove(); } + /** + * Watch task + */ + public static async watchTask(_id: string, isAdd: boolean, userId: string) { + return watchItem(Tasks, _id, isAdd, userId); + } + /** * Change customer */ diff --git a/src/db/models/Tickets.ts b/src/db/models/Tickets.ts index a90a46151..b1b9a0fdc 100644 --- a/src/db/models/Tickets.ts +++ b/src/db/models/Tickets.ts @@ -1,14 +1,15 @@ import { Model, model } from 'mongoose'; import { ActivityLogs } from '.'; +import { changeCompany, changeCustomer, updateOrder, watchItem } from './boardUtils'; import { IOrderInput } from './definitions/boards'; import { ITicket, ITicketDocument, ticketSchema } from './definitions/tickets'; -import { changeCompany, changeCustomer, updateOrder } from './utils'; export interface ITicketModel extends Model { createTicket(doc: ITicket): Promise; updateTicket(_id: string, doc: ITicket): Promise; updateOrder(stageId: string, orders: IOrderInput[]): Promise; removeTicket(_id: string): void; + watchTicket(_id: string, isAdd: boolean, userId: string): void; changeCustomer(newCustomerId: string, oldCustomerIds: string[]): Promise; changeCompany(newCompanyId: string, oldCompanyIds: string[]): Promise; } @@ -64,6 +65,13 @@ export const loadTicketClass = () => { return ticket.remove(); } + /** + * Watch ticket + */ + public static async watchTicket(_id: string, isAdd: boolean, userId: string) { + return watchItem(Tickets, _id, isAdd, userId); + } + /** * Change customer */ diff --git a/src/db/models/boardUtils.ts b/src/db/models/boardUtils.ts new file mode 100644 index 000000000..8bd0c0ad4 --- /dev/null +++ b/src/db/models/boardUtils.ts @@ -0,0 +1,81 @@ +import { IOrderInput } from './definitions/boards'; + +export const updateOrder = async (collection: any, orders: IOrderInput[], stageId?: string) => { + if (orders.length === 0) { + return []; + } + + const ids: string[] = []; + const bulkOps: Array<{ + updateOne: { + filter: { _id: string }; + update: { stageId?: string; order: number }; + }; + }> = []; + + for (const { _id, order } of orders) { + ids.push(_id); + + const selector: { order: number; stageId?: string } = { order }; + + if (stageId) { + selector.stageId = stageId; + } + + bulkOps.push({ + updateOne: { + filter: { _id }, + update: selector, + }, + }); + } + + if (bulkOps) { + await collection.bulkWrite(bulkOps); + } + + return collection.find({ _id: { $in: ids } }).sort({ order: 1 }); +}; + +export const changeCustomer = async (collection: any, newCustomerId: string, oldCustomerIds: string[]) => { + if (oldCustomerIds) { + await collection.updateMany( + { customerIds: { $in: oldCustomerIds } }, + { $addToSet: { customerIds: newCustomerId } }, + ); + await collection.updateMany( + { customerIds: { $in: oldCustomerIds } }, + { $pullAll: { customerIds: oldCustomerIds } }, + ); + } + + return collection.find({ customerIds: { $in: oldCustomerIds } }); +}; + +export const changeCompany = async (collection: any, newCompanyId: string, oldCompanyIds: string[]) => { + if (oldCompanyIds) { + await collection.updateMany({ companyIds: { $in: oldCompanyIds } }, { $addToSet: { companyIds: newCompanyId } }); + + await collection.updateMany({ companyIds: { $in: oldCompanyIds } }, { $pullAll: { companyIds: oldCompanyIds } }); + } + + return collection.find({ customerIds: { $in: oldCompanyIds } }); +}; + +export const watchItem = async (collection: any, _id: string, isAdd: boolean, userId: string) => { + const item = await collection.findOne({ _id }); + + const watchedUserIds = item.watchedUserIds || []; + + if (isAdd) { + watchedUserIds.push(userId); + } else { + const index = watchedUserIds.indexOf(userId); + + watchedUserIds.splice(index, 1); + } + + await collection.updateOne({ _id }, { $set: { watchedUserIds } }); + + return collection.findOne({ _id }); +}; diff --git a/src/db/models/definitions/boards.ts b/src/db/models/definitions/boards.ts index 70808c995..be56d8428 100644 --- a/src/db/models/definitions/boards.ts +++ b/src/db/models/definitions/boards.ts @@ -9,6 +9,24 @@ interface ICommonFields { type: string; } +export interface IItemCommonFields { + name?: string; + companyIds?: string[]; + customerIds?: string[]; + closeDate?: Date; + description?: string; + assignedUserIds?: string[]; + watchedUserIds?: string[]; + notifiedUserIds?: string[]; + stageId?: string; + initialStageId?: string; + modifiedAt?: Date; + modifiedBy?: string; + userId?: string; + createdAt?: Date; + order?: number; +} + export interface IBoard extends ICommonFields { name?: string; isDefault?: boolean; @@ -24,6 +42,7 @@ export interface IPipeline extends ICommonFields { visibility?: string; memberIds?: string[]; bgColor?: string; + watchedUserIds?: string[]; } export interface IPipelineDocument extends IPipeline, Document { @@ -60,6 +79,30 @@ const commonFieldsSchema = { }), }; +export const commonItemFieldsSchema = { + _id: field({ pkey: true }), + userId: field({ type: String }), + createdAt: field({ + type: Date, + default: new Date(), + }), + order: field({ type: Number }), + name: field({ type: String }), + companyIds: field({ type: [String] }), + customerIds: field({ type: [String] }), + closeDate: field({ type: Date }), + description: field({ type: String, optional: true }), + assignedUserIds: field({ type: [String] }), + watchedUserIds: field({ type: [String] }), + stageId: field({ type: String, optional: true }), + initialStageId: field({ type: String, optional: true }), + modifiedAt: field({ + type: Date, + default: new Date(), + }), + modifiedBy: field({ type: String }), +}; + export const boardSchema = new Schema({ _id: field({ pkey: true }), name: field({ type: String }), @@ -79,6 +122,7 @@ export const pipelineSchema = new Schema({ enum: PIPELINE_VISIBLITIES.ALL, default: PIPELINE_VISIBLITIES.PUBLIC, }), + watchedUserIds: field({ type: [String] }), memberIds: field({ type: [String] }), bgColor: field({ type: String }), ...commonFieldsSchema, diff --git a/src/db/models/definitions/deals.ts b/src/db/models/definitions/deals.ts index ea024fc24..ee7eb09d5 100644 --- a/src/db/models/definitions/deals.ts +++ b/src/db/models/definitions/deals.ts @@ -1,13 +1,8 @@ import { Document, Schema } from 'mongoose'; import { field } from '../utils'; +import { commonItemFieldsSchema, IItemCommonFields } from './boards'; import { PRODUCT_TYPES } from './constants'; -interface ICommonFields { - userId?: string; - createdAt?: Date; - order?: number; -} - export interface IProduct { name: string; type?: string; @@ -34,33 +29,15 @@ interface IProductData extends Document { amount?: number; } -export interface IDeal extends ICommonFields { - name?: string; +export interface IDeal extends IItemCommonFields { productsData?: IProductData[]; - companyIds?: string[]; - customerIds?: string[]; - closeDate?: Date; - description?: string; - assignedUserIds?: string[]; - stageId?: string; - initialStageId?: string; - modifiedAt?: Date; - modifiedBy?: string; } -export interface IDealDocument extends IDeal, ICommonFields, Document { +export interface IDealDocument extends IDeal, Document { _id: string; } // Mongoose schemas ======================= -const commonFieldsSchema = { - userId: field({ type: String }), - createdAt: field({ - type: Date, - default: new Date(), - }), - order: field({ type: Number }), -}; export const productSchema = new Schema({ _id: field({ pkey: true }), @@ -96,20 +73,7 @@ const productDataSchema = new Schema( ); export const dealSchema = new Schema({ - _id: field({ pkey: true }), - name: field({ type: String }), + ...commonItemFieldsSchema, + productsData: field({ type: [productDataSchema] }), - companyIds: field({ type: [String] }), - customerIds: field({ type: [String] }), - closeDate: field({ type: Date }), - description: field({ type: String, optional: true }), - assignedUserIds: field({ type: [String] }), - stageId: field({ type: String, optional: true }), - initialStageId: field({ type: String, optional: true }), - modifiedAt: field({ - type: Date, - default: new Date(), - }), - modifiedBy: field({ type: String }), - ...commonFieldsSchema, }); diff --git a/src/db/models/definitions/tasks.ts b/src/db/models/definitions/tasks.ts index fb4b323d4..84aca92d1 100644 --- a/src/db/models/definitions/tasks.ts +++ b/src/db/models/definitions/tasks.ts @@ -1,53 +1,18 @@ import { Document, Schema } from 'mongoose'; import { field } from '../utils'; +import { commonItemFieldsSchema, IItemCommonFields } from './boards'; -interface ICommonFields { - userId?: string; - createdAt?: Date; - order?: number; -} - -export interface ITask extends ICommonFields { - name?: string; - companyIds?: string[]; - customerIds?: string[]; - closeDate?: Date; - description?: string; - assignedUserIds?: string[]; - stageId?: string; +export interface ITask extends IItemCommonFields { priority?: string; - modifiedAt?: Date; - modifiedBy?: string; } -export interface ITaskDocument extends ITask, ICommonFields, Document { +export interface ITaskDocument extends ITask, Document { _id: string; } // Mongoose schemas ======================= -const commonFieldsSchema = { - userId: field({ type: String }), - createdAt: field({ - type: Date, - default: new Date(), - }), - order: field({ type: Number }), -}; - export const taskSchema = new Schema({ - _id: field({ pkey: true }), - name: field({ type: String }), - companyIds: field({ type: [String] }), - customerIds: field({ type: [String] }), - closeDate: field({ type: Date }), - description: field({ type: String, optional: true }), - assignedUserIds: field({ type: [String] }), - stageId: field({ type: String, optional: true }), + ...commonItemFieldsSchema, + priority: field({ type: String, optional: true }), - modifiedAt: field({ - type: Date, - default: new Date(), - }), - modifiedBy: field({ type: String }), - ...commonFieldsSchema, }); diff --git a/src/db/models/definitions/tickets.ts b/src/db/models/definitions/tickets.ts index 74bcdf8a7..459993f9c 100644 --- a/src/db/models/definitions/tickets.ts +++ b/src/db/models/definitions/tickets.ts @@ -1,55 +1,20 @@ import { Document, Schema } from 'mongoose'; import { field } from '../utils'; +import { commonItemFieldsSchema, IItemCommonFields } from './boards'; -interface ICommonFields { - userId?: string; - createdAt?: Date; - order?: number; -} - -export interface ITicket extends ICommonFields { - name?: string; - companyIds?: string[]; - customerIds?: string[]; - closeDate?: Date; - description?: string; +export interface ITicket extends IItemCommonFields { priority?: string; source?: string; - assignedUserIds?: string[]; - stageId?: string; - modifiedAt?: Date; - modifiedBy?: string; } -export interface ITicketDocument extends ITicket, ICommonFields, Document { +export interface ITicketDocument extends ITicket, Document { _id: string; } // Mongoose schemas ======================= -const commonFieldsSchema = { - userId: field({ type: String }), - createdAt: field({ - type: Date, - default: new Date(), - }), - order: field({ type: Number }), -}; - export const ticketSchema = new Schema({ - _id: field({ pkey: true }), - name: field({ type: String }), - companyIds: field({ type: [String] }), - customerIds: field({ type: [String] }), - closeDate: field({ type: Date }), - description: field({ type: String, optional: true }), - assignedUserIds: field({ type: [String] }), - stageId: field({ type: String, optional: true }), - modifiedAt: field({ - type: Date, - default: new Date(), - }), - modifiedBy: field({ type: String }), + ...commonItemFieldsSchema, + priority: field({ type: String }), source: field({ type: String }), - ...commonFieldsSchema, }); diff --git a/src/db/models/utils.ts b/src/db/models/utils.ts index 0048ad7a8..67a3b9c2a 100644 --- a/src/db/models/utils.ts +++ b/src/db/models/utils.ts @@ -1,7 +1,6 @@ import * as Random from 'meteor-random'; import { COMPANY_BASIC_INFOS, CUSTOMER_BASIC_INFOS } from '../../data/constants'; import { Fields } from './'; -import { IOrderInput } from './definitions/boards'; /* * Mongoose field options wrapper @@ -64,65 +63,3 @@ export const checkFieldNames = async (type: string, fields: string[]) => { return properties; }; - -export const updateOrder = async (collection: any, orders: IOrderInput[], stageId?: string) => { - if (orders.length === 0) { - return []; - } - - const ids: string[] = []; - const bulkOps: Array<{ - updateOne: { - filter: { _id: string }; - update: { stageId?: string; order: number }; - }; - }> = []; - - for (const { _id, order } of orders) { - ids.push(_id); - - const selector: { order: number; stageId?: string } = { order }; - - if (stageId) { - selector.stageId = stageId; - } - - bulkOps.push({ - updateOne: { - filter: { _id }, - update: selector, - }, - }); - } - - if (bulkOps) { - await collection.bulkWrite(bulkOps); - } - - return collection.find({ _id: { $in: ids } }).sort({ order: 1 }); -}; - -export const changeCustomer = async (collection: any, newCustomerId: string, oldCustomerIds: string[]) => { - if (oldCustomerIds) { - await collection.updateMany( - { customerIds: { $in: oldCustomerIds } }, - { $addToSet: { customerIds: newCustomerId } }, - ); - await collection.updateMany( - { customerIds: { $in: oldCustomerIds } }, - { $pullAll: { customerIds: oldCustomerIds } }, - ); - } - - return collection.find({ customerIds: { $in: oldCustomerIds } }); -}; - -export const changeCompany = async (collection: any, newCompanyId: string, oldCompanyIds: string[]) => { - if (oldCompanyIds) { - await collection.updateMany({ companyIds: { $in: oldCompanyIds } }, { $addToSet: { companyIds: newCompanyId } }); - - await collection.updateMany({ companyIds: { $in: oldCompanyIds } }, { $pullAll: { companyIds: oldCompanyIds } }); - } - - return collection.find({ customerIds: { $in: oldCompanyIds } }); -};