diff --git a/src/__tests__/boardDb.test.ts b/src/__tests__/boardDb.test.ts index 2b695d836..0a436da11 100644 --- a/src/__tests__/boardDb.test.ts +++ b/src/__tests__/boardDb.test.ts @@ -7,7 +7,7 @@ import { stageFactory, userFactory, } from '../db/factories'; -import { Boards, Forms, Pipelines, Stages } from '../db/models'; +import { Boards, Deals, Forms, Pipelines, Stages } from '../db/models'; import { IBoardDocument, IPipelineDocument, IStageDocument } from '../db/models/definitions/boards'; import { IUserDocument } from '../db/models/definitions/users'; @@ -34,6 +34,7 @@ describe('Test board model', () => { await Pipelines.deleteMany({}); await Stages.deleteMany({}); await Pipelines.deleteMany({}); + await Deals.deleteMany({}); }); test('Get board', async () => { @@ -375,4 +376,58 @@ describe('Test board model', () => { expect(updatedStage.order).toBe(5); expect(updatedStageToOrder.order).toBe(9); }); + + test('Test copyStage()', async () => { + const secondPipeline = await pipelineFactory(); + + const params = { + stageId: stage._id, + pipelineId: secondPipeline._id, + includeCards: false, + userId: user._id, + }; + + const copiedStage = await Stages.copyStage(params); + + const { name, pipelineId } = copiedStage; + + expect(name).toBe(`${stage.name}-copied`); + expect(pipelineId).toBe(secondPipeline._id); + }); + + test('Test copyStage() with cards', async () => { + const secondPipeline = await pipelineFactory(); + + await dealFactory({ stageId: stage._id }); + + const params = { + stageId: stage._id, + pipelineId: secondPipeline._id, + includeCards: true, + userId: user._id, + }; + + const copiedStage = await Stages.copyStage(params); + const items = await Stages.getCards(copiedStage._id); + + // above 1 & copied 1 + expect(items.length).toBe(2); + }); + + test('Test moveStage()', async () => { + const secondPipeline = await pipelineFactory(); + + const params = { + stageId: stage._id, + pipelineId: secondPipeline._id, + includeCards: false, + userId: user._id, + }; + + const movedStage = await Stages.moveStage(params); + + const { pipelineId } = movedStage; + + expect(pipelineId).toBe(secondPipeline._id); + }); }); diff --git a/src/__tests__/boardMutations.test.ts b/src/__tests__/boardMutations.test.ts index 653e09b4e..8ec943b10 100644 --- a/src/__tests__/boardMutations.test.ts +++ b/src/__tests__/boardMutations.test.ts @@ -34,6 +34,9 @@ describe('Test boards mutations', () => { memberIds: $memberIds `; + const stageCopyMoveParamDefs = `$_id: String!, $pipelineId: String!, $includeCards: Boolean`; + const stageCopyMoveParams = `_id: $_id, pipelineId: $pipelineId, includeCards: $includeCards`; + beforeEach(async () => { // Creating test data board = await boardFactory(); @@ -319,6 +322,68 @@ describe('Test boards mutations', () => { expect(updatedStageToOrder.order).toBe(9); }); + test('Test stagesMove()', async () => { + const secondPipeline = await pipelineFactory(); + + const params = { + _id: stage._id, + pipelineId: secondPipeline._id, + includeCards: false, + }; + + const mutation = ` + mutation stagesMove(${stageCopyMoveParamDefs}) { + stagesMove(${stageCopyMoveParams}) { pipelineId } + } + `; + + const result = await graphqlRequest(mutation, 'stagesMove', params, context); + + expect(result.pipelineId).toBe(params.pipelineId); + }); + + test('Test stagesCopy()', async () => { + const secondPipeline = await pipelineFactory(); + + const params = { + _id: stage._id, + pipelineId: secondPipeline._id, + includeCards: false, + }; + + const mutation = ` + mutation stagesCopy(${stageCopyMoveParamDefs}) { + stagesCopy(${stageCopyMoveParams}) { name } + } + `; + + const result = await graphqlRequest(mutation, 'stagesCopy', params, context); + + expect(result.name).toBe(`${stage.name}-copied`); + }); + + test('Test stagesCopy() with wrong type', async () => { + const secondPipeline = await pipelineFactory({ type: BOARD_TYPES.TASK }); + + const params = { + _id: stage._id, + pipelineId: secondPipeline._id, + includeCards: false, + }; + + const mutation = ` + mutation stagesCopy(${stageCopyMoveParamDefs}) { + stagesCopy(${stageCopyMoveParams}) { name } + } + `; + + try { + await graphqlRequest(mutation, 'stagesCopy', params, context); + } catch (e) { + expect(e[0].message).toBe('Pipeline and stage type does not match'); + } + }); + test('Edit stage', async () => { const mutation = ` mutation stagesEdit($_id: String!, $type: String, $name: String) { diff --git a/src/data/resolvers/boardUtils.ts b/src/data/resolvers/boardUtils.ts index ac1f3a720..9650f8efa 100644 --- a/src/data/resolvers/boardUtils.ts +++ b/src/data/resolvers/boardUtils.ts @@ -372,3 +372,15 @@ export const prepareBoardItemDoc = async (_id: string, type: string, userId: str return doc; }; + +/** + * Used in stage move, copy mutations. + * Target pipeline type must be the same as stage type. + */ +export const verifyPipelineType = async (pipelineId: string, stageType: string) => { + const pipeline = await Pipelines.getPipeline(pipelineId); + + if (pipeline.type !== stageType) { + throw new Error('Pipeline and stage type does not match'); + } +}; diff --git a/src/data/resolvers/mutations/boards.ts b/src/data/resolvers/mutations/boards.ts index 6de726015..0c04f7916 100644 --- a/src/data/resolvers/mutations/boards.ts +++ b/src/data/resolvers/mutations/boards.ts @@ -2,7 +2,7 @@ import { Boards, Pipelines, Stages } from '../../../db/models'; import { IBoard, IOrderInput, IPipeline, IStage, IStageDocument } from '../../../db/models/definitions/boards'; import { putCreateLog, putDeleteLog, putUpdateLog } from '../../logUtils'; import { IContext } from '../../types'; -import { checkPermission } from '../boardUtils'; +import { checkPermission, verifyPipelineType } from '../boardUtils'; interface IBoardsEdit extends IBoard { _id: string; @@ -16,6 +16,12 @@ interface IPipelinesEdit extends IPipelinesAdd { _id: string; } +interface IStagesCopyMove { + _id: string; + pipelineId: string; + includeCards: boolean; +} + interface IStageEdit extends IStage { _id: string; } @@ -161,6 +167,35 @@ const boardMutations = { return Stages.updateOrder(orders); }, + async stagesMove(_root, { _id, includeCards, pipelineId }: IStagesCopyMove, { user }: IContext) { + const stage = await Stages.getStage(_id); + + await checkPermission(stage.type, user, 'stagesEdit'); + + await verifyPipelineType(pipelineId, stage.type); + + return Stages.moveStage({ + includeCards, + stageId: _id, + pipelineId, + userId: user._id, + }); + }, + + async stagesCopy(_root, { _id, includeCards, pipelineId }: IStagesCopyMove, { user }: IContext) { + const stage = await Stages.getStage(_id); + + await checkPermission(stage.type, user, 'stagesEdit'); + + await verifyPipelineType(pipelineId, stage.type); + + return Stages.copyStage({ + stageId: _id, + pipelineId, + includeCards, + userId: user._id, + }); + }, /** * Edit stage */ diff --git a/src/data/resolvers/queries/boards.ts b/src/data/resolvers/queries/boards.ts index f6b3191ee..c6d9dfe27 100644 --- a/src/data/resolvers/queries/boards.ts +++ b/src/data/resolvers/queries/boards.ts @@ -80,9 +80,16 @@ const boardQueries = { pipelines( _root, { boardId, type, ...queryParams }: { boardId: string; type: string; page: number; perPage: number }, + { user }: IContext, ) { - const query: any = {}; const { page, perPage } = queryParams; + const query: any = { + $or: [{ visibility: 'public' }, { visibility: 'private', $or: [{ memberIds: user._id }, { userId: user._id }] }], + }; + + if (user.isOwner) { + delete query.$or; + } if (boardId) { query.boardId = boardId; diff --git a/src/data/schema/board.ts b/src/data/schema/board.ts index 0e0706aac..db39df6b6 100644 --- a/src/data/schema/board.ts +++ b/src/data/schema/board.ts @@ -120,6 +120,8 @@ const pipelineParams = ` excludeCheckUserIds: [String], `; +const copyMoveParams = `_id: String!, pipelineId: String!, includeCards: Boolean`; + export const mutations = ` boardsAdd(${commonParams}): Board boardsEdit(_id: String!, ${commonParams}): Board @@ -133,5 +135,7 @@ export const mutations = ` stagesUpdateOrder(orders: [OrderItem]): [Stage] stagesRemove(_id: String!): JSON + stagesCopy(${copyMoveParams}): Stage + stagesMove(${copyMoveParams}): Stage stagesEdit(_id: String!, type: String, name: String, status: String): Stage `; diff --git a/src/db/models/Boards.ts b/src/db/models/Boards.ts index 82f8cc74a..6d58c0f99 100644 --- a/src/db/models/Boards.ts +++ b/src/db/models/Boards.ts @@ -5,6 +5,7 @@ import { boardSchema, IBoard, IBoardDocument, + ICopyMoveParams, IPipeline, IPipelineDocument, IStage, @@ -12,6 +13,11 @@ import { pipelineSchema, stageSchema, } from './definitions/boards'; +import { PROBABILITY } from './definitions/constants'; +import { IDealDocument } from './definitions/deals'; +import { IGrowthHackDocument } from './definitions/growthHacks'; +import { ITaskDocument } from './definitions/tasks'; +import { ITicketDocument } from './definitions/tickets'; import { getDuplicatedStages } from './PipelineTemplates'; export interface IOrderInput { @@ -264,12 +270,18 @@ export const loadPipelineClass = () => { return pipelineSchema; }; +type Cards = IDealDocument[] | ITaskDocument[] | ITicketDocument[] | IGrowthHackDocument[]; + export interface IStageModel extends Model { getStage(_id: string): Promise; createStage(doc: IStage): Promise; removeStage(_id: string): object; updateStage(_id: string, doc: IStage): Promise; updateOrder(orders: IOrderInput[]): Promise; + getCards(_id: string): Promise; + cloneCards(_id: string, destStageId: string, userId: string): Promise; + copyStage(params: ICopyMoveParams): Promise; + moveStage(params: ICopyMoveParams): Promise; } export const loadStageClass = () => { @@ -310,6 +322,77 @@ export const loadStageClass = () => { return updateOrder(Stages, orders); } + public static async getCards(_id: string) { + const stage: IStageDocument = await Stages.getStage(_id); + + const collection = getCollection(stage.type); + + return collection.find({ stageId: stage._id }).lean(); + } + + public static async cloneCards(_id: string, destStageId: string, userId: string) { + const stage = await Stages.getStage(_id); + const cards = await Stages.getCards(stage._id); + const collection = getCollection(stage.type); + + for (const card of cards) { + const itemDoc = { + name: `${card.name}-copied`, + stageId: destStageId, + initialStageId: destStageId, + createdAt: new Date(), + assignedUserIds: card.assignedUserIds, + watchedUserIds: card.watchedUserIds, + labelIds: card.labelIds, + priority: card.priority, + userId, + description: card.description, + status: card.status, + }; + + await collection.create(itemDoc); + } + + return collection.find({ stageId: destStageId }); + } + + public static async copyStage(params: ICopyMoveParams) { + const { stageId, pipelineId, includeCards, userId } = params; + + const destinationPipeline = await Pipelines.getPipeline(pipelineId); + const stage = await Stages.getStage(stageId); + + const copiedStage = await Stages.createStage({ + pipelineId: destinationPipeline._id, + createdAt: new Date(), + name: `${stage.name}-copied`, + userId, + type: stage.type, + formId: stage.formId, + probability: stage.probability || PROBABILITY.TEN, + status: stage.status, + }); + + if (includeCards === true) { + await Stages.cloneCards(stage._id, copiedStage._id, userId); + } + + return copiedStage; + } + + /** + * Moves a stage to given pipeline + */ + public static async moveStage(params: ICopyMoveParams) { + const { stageId, pipelineId } = params; + + const pipeline = await Pipelines.getPipeline(pipelineId); + + await Stages.updateOne({ _id: stageId }, { $set: { pipelineId: pipeline._id } }); + + return Stages.findOne({ _id: stageId }); + } + public static async removeStage(_id: string) { const stage = await Stages.getStage(_id); const pipeline = await Pipelines.getPipeline(stage.pipelineId); diff --git a/src/db/models/definitions/boards.ts b/src/db/models/definitions/boards.ts index acb5b6d8a..89c9075ea 100644 --- a/src/db/models/definitions/boards.ts +++ b/src/db/models/definitions/boards.ts @@ -87,6 +87,13 @@ export interface IOrderInput { order: number; } +export interface ICopyMoveParams { + stageId: string; + pipelineId: string; + includeCards: boolean; + userId: string; +} + export const attachmentSchema = new Schema( { name: field({ type: String, label: 'Name' }),