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:
parent
5e0b2b9f0a
commit
044fe17dbf
17 changed files with 321 additions and 64 deletions
|
@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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]);
|
||||
|
|
|
@ -68,6 +68,12 @@ module.exports = {
|
|||
|
||||
return exits.success({
|
||||
item: card,
|
||||
included: {
|
||||
cardMemberships: [],
|
||||
cardLabels: [],
|
||||
tasks: [],
|
||||
attachments: [],
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -65,6 +65,12 @@ module.exports = {
|
|||
'cardCreate',
|
||||
{
|
||||
item: card,
|
||||
included: {
|
||||
cardMemberships: [],
|
||||
cardLabels: [],
|
||||
tasks: [],
|
||||
attachments: [],
|
||||
},
|
||||
},
|
||||
inputs.request,
|
||||
);
|
||||
|
|
17
server/api/helpers/get-label-ids-for-card.js
Normal file
17
server/api/helpers/get-label-ids-for-card.js
Normal 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);
|
||||
},
|
||||
};
|
|
@ -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);
|
||||
},
|
||||
|
|
17
server/api/helpers/get-labels-for-card.js
Normal file
17
server/api/helpers/get-labels-for-card.js
Normal 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);
|
||||
},
|
||||
};
|
14
server/api/helpers/get-labels.js
Normal file
14
server/api/helpers/get-labels.js
Normal 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);
|
||||
},
|
||||
};
|
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue