1
0
Fork 0
mirror of https://github.com/plankanban/planka.git synced 2025-07-18 20:59:44 +02:00

Preserve members and labels when transfer card to another board

This commit is contained in:
Maksim Eltyshev 2020-05-09 05:30:52 +05:00
parent 5e0b2b9f0a
commit 044fe17dbf
17 changed files with 321 additions and 64 deletions

View file

@ -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,
},
});

View file

@ -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,

View file

@ -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);
});

View file

@ -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();

View file

@ -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);
});

View file

@ -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 {

View file

@ -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);
}

View file

@ -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) {

View file

@ -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]);

View file

@ -68,6 +68,12 @@ module.exports = {
return exits.success({
item: card,
included: {
cardMemberships: [],
cardLabels: [],
tasks: [],
attachments: [],
},
});
},
};

View file

@ -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;

View file

@ -65,6 +65,12 @@ module.exports = {
'cardCreate',
{
item: card,
included: {
cardMemberships: [],
cardLabels: [],
tasks: [],
attachments: [],
},
},
inputs.request,
);

View file

@ -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);
},
};

View file

@ -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);
},

View file

@ -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);
},
};

View file

@ -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);
},
};

View file

@ -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