diff --git a/client/src/actions/card.js b/client/src/actions/card.js index a511e49b..08fcc03b 100644 --- a/client/src/actions/card.js +++ b/client/src/actions/card.js @@ -34,11 +34,22 @@ export const createCardRequested = (localId, data) => ({ }, }); -export const createCardSucceeded = (localId, card) => ({ +export const createCardSucceeded = ( + localId, + card, + cardMemberships, + cardLabels, + tasks, + attachments, +) => ({ type: ActionTypes.CARD_CREATE_SUCCEEDED, payload: { localId, card, + cardMemberships, + cardLabels, + tasks, + attachments, }, }); @@ -50,10 +61,14 @@ export const createCardFailed = (localId, error) => ({ }, }); -export const createCardReceived = (card) => ({ +export const createCardReceived = (card, cardMemberships, cardLabels, tasks, attachments) => ({ type: ActionTypes.CARD_CREATE_RECEIVED, payload: { card, + cardMemberships, + cardLabels, + tasks, + attachments, }, }); diff --git a/client/src/api/cards.js b/client/src/api/cards.js index 9a230382..3a7ef149 100755 --- a/client/src/api/cards.js +++ b/client/src/api/cards.js @@ -1,4 +1,5 @@ import socket from './socket'; +import { transformAttachment } from './attachments'; /* Transformers */ @@ -38,6 +39,10 @@ const createCard = (listId, data, headers) => socket.post(`/lists/${listId}/cards`, transformCardData(data), headers).then((body) => ({ ...body, item: transformCard(body.item), + included: { + ...body.included, + attachments: body.included.attachments.map(transformAttachment), + }, })); const getCard = (id, headers) => @@ -64,12 +69,21 @@ const makeHandleCardCreate = (next) => (body) => { next({ ...body, item: transformCard(body.item), + included: { + ...body.included, + attachments: body.included.attachments.map(transformAttachment), + }, }); }; -const makeHandleCardUpdate = makeHandleCardCreate; +const makeHandleCardUpdate = (next) => (body) => { + next({ + ...body, + item: transformCard(body.item), + }); +}; -const makeHandleCardDelete = makeHandleCardCreate; +const makeHandleCardDelete = makeHandleCardUpdate; export default { createCard, diff --git a/client/src/models/Attachment.js b/client/src/models/Attachment.js index f1eb82fa..1f7fb0a4 100644 --- a/client/src/models/Attachment.js +++ b/client/src/models/Attachment.js @@ -20,6 +20,8 @@ export default class extends Model { static reducer({ type, payload }, Attachment) { switch (type) { case ActionTypes.BOARD_FETCH_SUCCEEDED: + case ActionTypes.CARD_CREATE_SUCCEEDED: + case ActionTypes.CARD_CREATE_RECEIVED: payload.attachments.forEach((attachment) => { Attachment.upsert(attachment); }); diff --git a/client/src/models/Card.js b/client/src/models/Card.js index da21f0d4..9ad77436 100755 --- a/client/src/models/Card.js +++ b/client/src/models/Card.js @@ -80,27 +80,15 @@ export default class extends Model { break; case ActionTypes.CARD_CREATE: - case ActionTypes.CARD_CREATE_RECEIVED: case ActionTypes.CARD_FETCH_SUCCEEDED: case ActionTypes.NOTIFICATION_CREATE_RECEIVED: Card.upsert(payload.card); break; - case ActionTypes.CARD_UPDATE: { - const card = Card.withId(payload.id); - - // FIXME: hack - if (payload.data.boardId && payload.data.boardId !== card.boardId) { - card.isSubscribed = false; - - card.users.clear(); - card.labels.clear(); - } - - card.update(payload.data); + case ActionTypes.CARD_UPDATE: + Card.withId(payload.id).update(payload.data); break; - } case ActionTypes.CARD_DELETE: Card.withId(payload.id).deleteWithRelated(); @@ -109,11 +97,36 @@ export default class extends Model { Card.withId(payload.localId).delete(); Card.upsert(payload.card); - break; - case ActionTypes.CARD_UPDATE_RECEIVED: - Card.withId(payload.card.id).update(payload.card); + payload.cardMemberships.forEach(({ cardId, userId }) => { + Card.withId(cardId).users.add(userId); + }); + + payload.cardLabels.forEach(({ cardId, labelId }) => { + Card.withId(cardId).labels.add(labelId); + }); break; + case ActionTypes.CARD_CREATE_RECEIVED: + Card.upsert(payload.card); + + payload.cardMemberships.forEach(({ cardId, userId }) => { + Card.withId(cardId).users.add(userId); + }); + + payload.cardLabels.forEach(({ cardId, labelId }) => { + Card.withId(cardId).labels.add(labelId); + }); + + break; + case ActionTypes.CARD_UPDATE_RECEIVED: { + const card = Card.withId(payload.card.id); + + if (card) { + card.update(payload.card); + } + + break; + } case ActionTypes.CARD_DELETE_RECEIVED: Card.withId(payload.card.id).deleteWithRelated(); @@ -177,6 +190,7 @@ export default class extends Model { deleteWithRelated() { this.tasks.delete(); + this.attachments.delete(); this.actions.delete(); this.delete(); diff --git a/client/src/models/Task.js b/client/src/models/Task.js index 9322f3e8..9d72b6fd 100755 --- a/client/src/models/Task.js +++ b/client/src/models/Task.js @@ -21,6 +21,8 @@ export default class extends Model { static reducer({ type, payload }, Task) { switch (type) { case ActionTypes.BOARD_FETCH_SUCCEEDED: + case ActionTypes.CARD_CREATE_SUCCEEDED: + case ActionTypes.CARD_CREATE_RECEIVED: payload.tasks.forEach((task) => { Task.upsert(task); }); diff --git a/client/src/sagas/app/requests/card.js b/client/src/sagas/app/requests/card.js index 5001a066..772f400f 100644 --- a/client/src/sagas/app/requests/card.js +++ b/client/src/sagas/app/requests/card.js @@ -26,9 +26,19 @@ export function* createCardRequest(listId, localId, data) { ); try { - const { item } = yield call(request, api.createCard, listId, data); + const { + item, + included: { cardMemberships, cardLabels, tasks, attachments }, + } = yield call(request, api.createCard, listId, data); - const action = createCardSucceeded(localId, item); + const action = createCardSucceeded( + localId, + item, + cardMemberships, + cardLabels, + tasks, + attachments, + ); yield put(action); return { diff --git a/client/src/sagas/app/services/card.js b/client/src/sagas/app/services/card.js index 97bdd1e4..ca1b41d6 100644 --- a/client/src/sagas/app/services/card.js +++ b/client/src/sagas/app/services/card.js @@ -2,11 +2,19 @@ import { call, put, select } from 'redux-saga/effects'; import { goToBoardService } from './router'; import { createCardRequest, deleteCardRequest, updateCardRequest } from '../requests'; -import { nextCardPositionSelector, pathSelector } from '../../../selectors'; +import { + boardByIdSelector, + cardByIdSelector, + listByIdSelector, + nextCardPositionSelector, + pathSelector, +} from '../../../selectors'; import { createCard, deleteCard, updateCard } from '../../../actions'; import { createLocalId } from '../../../utils/local-id'; export function* createCardService(listId, data) { + const { boardId } = yield select(listByIdSelector, listId); + const nextData = { ...data, position: yield select(nextCardPositionSelector, listId), @@ -18,6 +26,7 @@ export function* createCardService(listId, data) { createCard({ ...nextData, listId, + boardId, id: localId, }), ); @@ -52,19 +61,40 @@ export function* moveCurrentCardService(listId, index) { } export function* transferCardService(id, boardId, listId, index) { - const position = yield select(nextCardPositionSelector, listId, index, id); + const { cardId: currentCardId, boardId: currentBoardId } = yield select(pathSelector); - yield call(updateCardService, id, { - boardId, - listId, - position, - }); + if (id === currentCardId) { + yield call(goToBoardService, currentBoardId); + } + + const card = yield select(cardByIdSelector, id); + const board = yield select(boardByIdSelector, boardId); + + yield put(deleteCard(id)); + + if (board.isFetching === false) { + const position = yield select(nextCardPositionSelector, listId, index, id); + + yield put( + createCard({ + ...card, + listId, + boardId, + position, + }), + ); + + yield call(updateCardRequest, id, { + listId, + boardId, + position, + }); + } } export function* transferCurrentCardService(boardId, listId, index) { - const { cardId, boardId: currentBoardId } = yield select(pathSelector); + const { cardId } = yield select(pathSelector); - yield call(goToBoardService, currentBoardId); yield call(transferCardService, cardId, boardId, listId, index); } diff --git a/client/src/sagas/app/services/socket.js b/client/src/sagas/app/services/socket.js index a60cffed..a6fd3b8e 100644 --- a/client/src/sagas/app/services/socket.js +++ b/client/src/sagas/app/services/socket.js @@ -163,8 +163,8 @@ export function* deleteLabelReceivedService(label) { yield put(deleteLabelReceived(label)); } -export function* createCardReceivedService(card) { - yield put(createCardReceived(card)); +export function* createCardReceivedService(card, cardMemberships, cardLabels, tasks, attachments) { + yield put(createCardReceived(card, cardMemberships, cardLabels, tasks, attachments)); } export function* updateCardReceivedService(card) { diff --git a/client/src/sagas/app/watchers/socket.js b/client/src/sagas/app/watchers/socket.js index 70e43cb6..5cb2c637 100644 --- a/client/src/sagas/app/watchers/socket.js +++ b/client/src/sagas/app/watchers/socket.js @@ -116,9 +116,11 @@ const createSocketEventsChannel = () => emit([deleteLabelReceivedService, item]); }; - const handleCardCreate = api.makeHandleCardCreate(({ item }) => { - emit([createCardReceivedService, item]); - }); + const handleCardCreate = api.makeHandleCardCreate( + ({ item, included: { cardMemberships, cardLabels, tasks, attachments } }) => { + emit([createCardReceivedService, item, cardMemberships, cardLabels, tasks, attachments]); + }, + ); const handleCardUpdate = api.makeHandleCardUpdate(({ item }) => { emit([updateCardReceivedService, item]); diff --git a/server/api/controllers/cards/create.js b/server/api/controllers/cards/create.js index f8eefcab..b486f063 100755 --- a/server/api/controllers/cards/create.js +++ b/server/api/controllers/cards/create.js @@ -68,6 +68,12 @@ module.exports = { return exits.success({ item: card, + included: { + cardMemberships: [], + cardLabels: [], + tasks: [], + attachments: [], + }, }); }, }; diff --git a/server/api/controllers/cards/update.js b/server/api/controllers/cards/update.js index 97813858..740e2a74 100755 --- a/server/api/controllers/cards/update.js +++ b/server/api/controllers/cards/update.js @@ -76,7 +76,7 @@ module.exports = { .intercept('pathNotFound', () => Errors.CARD_NOT_FOUND); let { card, project } = cardToProjectPath; - const { list } = cardToProjectPath; + const { list, board } = cardToProjectPath; let isUserMemberForProject = await sails.helpers.isUserMemberForProject( project.id, @@ -88,6 +88,8 @@ module.exports = { } let toList; + let toBoard; + if (!_.isUndefined(inputs.listId) && inputs.listId !== list.id) { toList = await List.findOne({ id: inputs.listId, @@ -98,7 +100,7 @@ module.exports = { throw Errors.LIST_NOT_FOUND; } - ({ project } = await sails.helpers + ({ board: toBoard, project } = await sails.helpers .getListToProjectPath(toList.id) .intercept('pathNotFound', () => Errors.LIST_NOT_FOUND)); @@ -122,7 +124,16 @@ module.exports = { 'isSubscribed', ]); - card = await sails.helpers.updateCard(card, values, toList, list, currentUser, this.req); + card = await sails.helpers.updateCard( + card, + values, + toList, + toBoard, + list, + board, + currentUser, + this.req, + ); if (!card) { throw Errors.CARD_NOT_FOUND; diff --git a/server/api/helpers/create-card.js b/server/api/helpers/create-card.js index 800c8c40..a33360f5 100644 --- a/server/api/helpers/create-card.js +++ b/server/api/helpers/create-card.js @@ -65,6 +65,12 @@ module.exports = { 'cardCreate', { item: card, + included: { + cardMemberships: [], + cardLabels: [], + tasks: [], + attachments: [], + }, }, inputs.request, ); diff --git a/server/api/helpers/get-label-ids-for-card.js b/server/api/helpers/get-label-ids-for-card.js new file mode 100644 index 00000000..3cb5e76b --- /dev/null +++ b/server/api/helpers/get-label-ids-for-card.js @@ -0,0 +1,17 @@ +module.exports = { + inputs: { + id: { + type: 'json', + custom: (value) => _.isString(value) || _.isArray(value), + required: true, + }, + }, + + async fn(inputs, exits) { + const cardLabels = await sails.helpers.getCardLabelsForCard(inputs.id); + + const labelIds = sails.helpers.mapRecords(cardLabels, 'labelId', _.isArray(inputs.id)); + + return exits.success(labelIds); + }, +}; diff --git a/server/api/helpers/get-labels-for-board.js b/server/api/helpers/get-labels-for-board.js index b5aaf3dd..7b54df9b 100644 --- a/server/api/helpers/get-labels-for-board.js +++ b/server/api/helpers/get-labels-for-board.js @@ -8,9 +8,9 @@ module.exports = { }, async fn(inputs, exits) { - const labels = await Label.find({ + const labels = await sails.helpers.getLabels({ boardId: inputs.id, - }).sort('id'); + }); return exits.success(labels); }, diff --git a/server/api/helpers/get-labels-for-card.js b/server/api/helpers/get-labels-for-card.js new file mode 100644 index 00000000..bfbcf46e --- /dev/null +++ b/server/api/helpers/get-labels-for-card.js @@ -0,0 +1,17 @@ +module.exports = { + inputs: { + id: { + type: 'json', + custom: (value) => _.isString(value) || _.isArray(value), + required: true, + }, + }, + + async fn(inputs, exits) { + const labelIds = await sails.helpers.getLabelIdsForCard(inputs.id); + + const labels = await sails.helpers.getLabels(labelIds); + + return exits.success(labels); + }, +}; diff --git a/server/api/helpers/get-labels.js b/server/api/helpers/get-labels.js new file mode 100644 index 00000000..8b2ecb37 --- /dev/null +++ b/server/api/helpers/get-labels.js @@ -0,0 +1,14 @@ +module.exports = { + inputs: { + criteria: { + type: 'json', + custom: (value) => _.isArray(value) || _.isPlainObject(value), + }, + }, + + async fn(inputs, exits) { + const labels = await Label.find(inputs.criteria).sort('id'); + + return exits.success(labels); + }, +}; diff --git a/server/api/helpers/update-card.js b/server/api/helpers/update-card.js index b863c853..2f2bd4fd 100644 --- a/server/api/helpers/update-card.js +++ b/server/api/helpers/update-card.js @@ -13,9 +13,15 @@ module.exports = { toList: { type: 'ref', }, + toBoard: { + type: 'ref', + }, list: { type: 'ref', }, + board: { + type: 'ref', + }, user: { type: 'ref', }, @@ -41,8 +47,16 @@ module.exports = { } else { values.listId = inputs.toList.id; - if (inputs.toList.boardId !== inputs.list.boardId) { - values.boardId = inputs.toList.boardId; + if (inputs.toBoard) { + if (!inputs.board) { + throw 'invalidParams'; + } + + if (inputs.toBoard.id === inputs.board.id) { + delete inputs.toList; // eslint-disable-line no-param-reassign + } else { + values.boardId = inputs.toBoard.id; + } } } } @@ -80,15 +94,29 @@ module.exports = { let card; if (!_.isEmpty(values)) { - // FIXME: hack - if (inputs.toList && inputs.toList.boardId !== inputs.list.boardId) { - await CardSubscription.destroy({ - cardId: inputs.record.id, - }); + let prevLabels; + if (inputs.toList && inputs.toBoard) { + if (inputs.toBoard.projectId !== inputs.board.projectId) { + const userIds = await sails.helpers.getMembershipUserIdsForProject( + inputs.toBoard.projectId, + ); - await CardMembership.destroy({ - cardId: inputs.record.id, - }); + await CardSubscription.destroy({ + cardId: inputs.record.id, + userId: { + '!=': userIds, + }, + }); + + await CardMembership.destroy({ + cardId: inputs.record.id, + userId: { + '!=': userIds, + }, + }); + } + + prevLabels = await sails.helpers.getLabelsForCard(inputs.record.id); await CardLabel.destroy({ cardId: inputs.record.id, @@ -101,19 +129,88 @@ module.exports = { return exits.success(card); } - // FIXME: hack - if (inputs.toList && inputs.toList.boardId !== inputs.list.boardId) { - card.isSubscribed = false; - } + if (inputs.toList && inputs.toBoard) { + sails.sockets.broadcast( + `board:${inputs.board.id}`, + 'cardDelete', + { + item: inputs.record, + }, + inputs.request, + ); - sails.sockets.broadcast( - `board:${card.boardId}`, - 'cardUpdate', - { - item: card, - }, - inputs.request, - ); + const labels = await sails.helpers.getLabelsForBoard(card.boardId); + const labelByNameMap = _.keyBy(labels, 'name'); + + const labelIds = await Promise.all( + await prevLabels.map(async (prevLabel) => { + if (labelByNameMap[prevLabel.name]) { + return labelByNameMap[prevLabel.name].id; + } + + const { id } = await sails.helpers.createLabel( + inputs.toBoard, + _.omit(prevLabel, ['id', 'boardId']), + ); + + return id; + }), + ); + + labelIds.forEach(async (labelId) => { + await CardLabel.create({ + labelId, + cardId: card.id, + }) + .tolerate('E_UNIQUE') + .fetch(); + }); + + const cardMemberships = await sails.helpers.getMembershipsForCard(card.id); + const cardLabels = await sails.helpers.getCardLabelsForCard(card.id); + const tasks = await sails.helpers.getTasksForCard(card.id); + const attachments = await sails.helpers.getAttachmentsForCard(card.id); + + sails.sockets.broadcast( + `board:${card.boardId}`, + 'cardCreate', + { + item: card, + included: { + cardMemberships, + cardLabels, + tasks, + attachments, + }, + }, + inputs.request, + ); + + const userIds = await sails.helpers.getSubscriptionUserIdsForCard(card.id); + + userIds.forEach((userId) => { + sails.sockets.broadcast( + `user:${userId}`, + 'cardUpdate', + { + item: { + id: card.id, + isSubscribed: true, + }, + }, + inputs.request, + ); + }); + } else { + sails.sockets.broadcast( + `board:${card.boardId}`, + 'cardUpdate', + { + item: card, + }, + inputs.request, + ); + } if (inputs.toList) { // TODO: add transfer action