mirror of
https://github.com/plankanban/planka.git
synced 2025-07-18 12:49:43 +02:00
Merge 869d9c1d11
into fdac299fc7
This commit is contained in:
commit
028a9e5503
42 changed files with 410 additions and 5 deletions
22
client/src/api/client.js
Normal file
22
client/src/api/client.js
Normal file
|
@ -0,0 +1,22 @@
|
|||
const API_BASE = 'http://localhost:1337/api';
|
||||
|
||||
const client = {
|
||||
post: async (url, data, headers = {}) => {
|
||||
const isAbsolute = url.startsWith('http://') || url.startsWith('https://');
|
||||
const response = await fetch(isAbsolute ? url : `${API_BASE}${url}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...headers,
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
credentials: 'include',
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
return response.json();
|
||||
},
|
||||
};
|
||||
|
||||
export default client;
|
|
@ -57,6 +57,8 @@ const deleteList = (id, headers) =>
|
|||
},
|
||||
}));
|
||||
|
||||
const moveToBoard = (id, data, headers) => socket.post(`/lists/${id}/move-to-board`, data, headers);
|
||||
|
||||
/* Event handlers */
|
||||
|
||||
const makeHandleListDelete = (next) => (body) => {
|
||||
|
@ -78,4 +80,5 @@ export default {
|
|||
clearList,
|
||||
deleteList,
|
||||
makeHandleListDelete,
|
||||
moveToBoard,
|
||||
};
|
||||
|
|
|
@ -7,6 +7,7 @@ import socketIOClient from 'socket.io-client';
|
|||
import sailsIOClient from 'sails.io.js';
|
||||
|
||||
import Config from '../constants/Config';
|
||||
import { getAccessToken } from '../utils/access-token-storage';
|
||||
|
||||
const io = sailsIOClient(socketIOClient);
|
||||
|
||||
|
@ -21,13 +22,18 @@ const { socket } = io;
|
|||
socket.connect = socket._connect; // eslint-disable-line no-underscore-dangle
|
||||
|
||||
['GET', 'POST', 'PUT', 'PATCH', 'DELETE'].forEach((method) => {
|
||||
socket[method.toLowerCase()] = (url, data, headers) =>
|
||||
new Promise((resolve, reject) => {
|
||||
socket[method.toLowerCase()] = (url, data, headers = {}) => {
|
||||
const accessToken = getAccessToken();
|
||||
const mergedHeaders = { ...headers };
|
||||
if (accessToken && !mergedHeaders.Authorization) {
|
||||
mergedHeaders.Authorization = `Bearer ${accessToken}`;
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
socket.request(
|
||||
{
|
||||
method,
|
||||
data,
|
||||
headers,
|
||||
headers: mergedHeaders,
|
||||
url: `/api${url}`,
|
||||
},
|
||||
(_, { body, error }) => {
|
||||
|
@ -39,6 +45,7 @@ socket.connect = socket._connect; // eslint-disable-line no-underscore-dangle
|
|||
},
|
||||
);
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
export default socket;
|
||||
|
|
|
@ -19,6 +19,7 @@ import SortStep from './SortStep';
|
|||
import SelectListTypeStep from '../SelectListTypeStep';
|
||||
import ConfirmationStep from '../../common/ConfirmationStep';
|
||||
import ArchiveCardsStep from '../../cards/ArchiveCardsStep';
|
||||
import BoardSelectStep from './BoardSelectStep';
|
||||
|
||||
import styles from './ActionsStep.module.scss';
|
||||
|
||||
|
@ -28,6 +29,7 @@ const StepTypes = {
|
|||
SORT: 'SORT',
|
||||
ARCHIVE_CARDS: 'ARCHIVE_CARDS',
|
||||
DELETE: 'DELETE',
|
||||
MOVE_TO_BOARD: 'MOVE_TO_BOARD',
|
||||
};
|
||||
|
||||
const ActionsStep = React.memo(({ listId, onNameEdit, onCardAdd, onClose }) => {
|
||||
|
@ -84,6 +86,13 @@ const ActionsStep = React.memo(({ listId, onNameEdit, onCardAdd, onClose }) => {
|
|||
openStep(StepTypes.DELETE);
|
||||
}, [openStep]);
|
||||
|
||||
const handleMoveToBoard = useCallback(
|
||||
(targetBoardId) => {
|
||||
dispatch(entryActions.moveListToBoardRequest(listId, targetBoardId));
|
||||
},
|
||||
[listId, dispatch],
|
||||
);
|
||||
|
||||
if (step) {
|
||||
switch (step.type) {
|
||||
case StepTypes.EDIT_TYPE:
|
||||
|
@ -114,6 +123,14 @@ const ActionsStep = React.memo(({ listId, onNameEdit, onCardAdd, onClose }) => {
|
|||
onBack={handleBack}
|
||||
/>
|
||||
);
|
||||
case StepTypes.MOVE_TO_BOARD:
|
||||
return (
|
||||
<BoardSelectStep
|
||||
currentBoardId={list.boardId}
|
||||
onSelect={handleMoveToBoard}
|
||||
onBack={handleBack}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
@ -171,6 +188,9 @@ const ActionsStep = React.memo(({ listId, onNameEdit, onCardAdd, onClose }) => {
|
|||
context: 'title',
|
||||
})}
|
||||
</Menu.Item>
|
||||
<Menu.Item className={styles.menuItem} onClick={() => openStep(StepTypes.MOVE_TO_BOARD)}>
|
||||
{t('action.moveListToBoard', { context: 'title' })}
|
||||
</Menu.Item>
|
||||
</Menu>
|
||||
</Popup.Content>
|
||||
</>
|
||||
|
|
|
@ -19,3 +19,34 @@
|
|||
margin: 0 0.5em 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
.boardSelectStep {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.boardButton {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 9px 0;
|
||||
border: none;
|
||||
border-radius: 0.28571429rem !important;
|
||||
font-size: 15px;
|
||||
color: #234;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
background: none;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.boardButton:hover,
|
||||
.boardButton:focus {
|
||||
background: #f4f6f8;
|
||||
color: #16324a;
|
||||
}
|
||||
|
||||
.noBoards {
|
||||
color: #888;
|
||||
text-align: center;
|
||||
padding: 0;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
|
61
client/src/components/lists/List/BoardSelectStep.jsx
Normal file
61
client/src/components/lists/List/BoardSelectStep.jsx
Normal file
|
@ -0,0 +1,61 @@
|
|||
import React, { useMemo } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { createSelector } from 'reselect';
|
||||
import PopupHeader from '../../../lib/custom-ui/components/Popup/PopupHeader';
|
||||
import styles from './ActionsStep.module.scss';
|
||||
import selectors from '../../../selectors';
|
||||
|
||||
const makeSelectBoardsByIds = () =>
|
||||
createSelector(
|
||||
(state, boardIds) => boardIds,
|
||||
(state) => state,
|
||||
(boardIds, state) => boardIds.map((id) => selectors.selectBoardById(state, id)),
|
||||
);
|
||||
|
||||
function BoardSelectStep({ currentBoardId, onSelect, onBack, onClose }) {
|
||||
const [t] = useTranslation();
|
||||
const projectId = useSelector((state) => selectors.selectPath(state).projectId);
|
||||
const boardIds = useSelector((state) => selectors.selectBoardIdsByProjectId(state, projectId));
|
||||
const selectBoardsByIds = useMemo(makeSelectBoardsByIds, []);
|
||||
const boards = useSelector((state) => selectBoardsByIds(state, boardIds));
|
||||
|
||||
return (
|
||||
<div className={styles.boardSelectStep}>
|
||||
<PopupHeader onBack={onBack} onClose={onClose}>
|
||||
{t('action.moveListToBoard', { context: 'title' })}
|
||||
</PopupHeader>
|
||||
<div className={styles.menu}>
|
||||
{boards
|
||||
.filter((b) => b && b.id !== currentBoardId)
|
||||
.map((board) => (
|
||||
<button
|
||||
key={board.id}
|
||||
type="button"
|
||||
className={styles.boardButton}
|
||||
onClick={() => onSelect(board.id)}
|
||||
>
|
||||
{board.name}
|
||||
</button>
|
||||
))}
|
||||
{boards.filter((b) => b && b.id !== currentBoardId).length === 0 && (
|
||||
<div className={styles.noBoards}>{t('common.noOtherBoards')}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
BoardSelectStep.propTypes = {
|
||||
currentBoardId: PropTypes.string.isRequired,
|
||||
onSelect: PropTypes.func.isRequired,
|
||||
onBack: PropTypes.func.isRequired,
|
||||
onClose: PropTypes.func,
|
||||
};
|
||||
|
||||
BoardSelectStep.defaultProps = {
|
||||
onClose: undefined,
|
||||
};
|
||||
|
||||
export default BoardSelectStep;
|
|
@ -285,4 +285,8 @@ export default {
|
|||
NOTIFICATION_SERVICE_TEST: `${PREFIX}/NOTIFICATION_SERVICE_TEST`,
|
||||
NOTIFICATION_SERVICE_DELETE: `${PREFIX}/NOTIFICATION_SERVICE_DELETE`,
|
||||
NOTIFICATION_SERVICE_DELETE_HANDLE: `${PREFIX}/NOTIFICATION_SERVICE_DELETE_HANDLE`,
|
||||
|
||||
/* Move List to Board Request */
|
||||
|
||||
MOVE_LIST_TO_BOARD_REQUEST: `${PREFIX}/MOVE_LIST_TO_BOARD_REQUEST`,
|
||||
};
|
||||
|
|
|
@ -86,6 +86,14 @@ const handleListDelete = (list, cards) => ({
|
|||
},
|
||||
});
|
||||
|
||||
const moveListToBoardRequest = (listId, targetBoardId) => ({
|
||||
type: EntryActionTypes.MOVE_LIST_TO_BOARD_REQUEST,
|
||||
payload: {
|
||||
listId,
|
||||
targetBoardId,
|
||||
},
|
||||
});
|
||||
|
||||
export default {
|
||||
createListInCurrentBoard,
|
||||
handleListCreate,
|
||||
|
@ -98,4 +106,5 @@ export default {
|
|||
handleListClear,
|
||||
deleteList,
|
||||
handleListDelete,
|
||||
moveListToBoardRequest,
|
||||
};
|
||||
|
|
|
@ -232,6 +232,7 @@ export default {
|
|||
unsubscribe: 'إلغاء الاشتراك',
|
||||
uploadNewAvatar: 'رفع صورة رمزية جديدة',
|
||||
uploadNewImage: 'رفع صورة جديدة',
|
||||
moveListToBoard: 'نقل القائمة إلى لوحة أخرى',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -406,6 +406,7 @@ export default {
|
|||
unsubscribe: 'Neodebírat',
|
||||
uploadNewAvatar: 'Nahrát nový avatar',
|
||||
uploadNewImage: 'Nahrát nový obrázek',
|
||||
moveListToBoard: 'Přesunout seznam na jinou nástěnku',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -434,6 +434,7 @@ export default {
|
|||
unsubscribe: 'Opsig abonnement',
|
||||
uploadNewAvatar: 'Tilføj nyt profilbillede',
|
||||
uploadNewImage: 'Tilføj nyt billede',
|
||||
moveListToBoard: 'Flyt liste til anden tavle',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -425,6 +425,7 @@ export default {
|
|||
unsubscribe: 'De-abonnieren',
|
||||
uploadNewAvatar: 'Neuen Avatar hochladen',
|
||||
uploadNewImage: 'Neues Bild hochladen',
|
||||
moveListToBoard: 'Liste auf andere Arbeitsbereich verschieben',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -450,6 +450,7 @@ export default {
|
|||
unsubscribe: 'Απεγγραφή',
|
||||
uploadNewAvatar: 'Μεταφόρτωση νέου avatar',
|
||||
uploadNewImage: 'Μεταφόρτωση νέας εικόνας',
|
||||
moveListToBoard: 'Μετακίνηση λίστας σε άλλο πίνακα',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -444,6 +444,7 @@ export default {
|
|||
unsubscribe: 'Unsubscribe',
|
||||
uploadNewAvatar: 'Upload new avatar',
|
||||
uploadNewImage: 'Upload new image',
|
||||
moveListToBoard: 'Move list to another board',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -439,6 +439,7 @@ export default {
|
|||
unsubscribe: 'Unsubscribe',
|
||||
uploadNewAvatar: 'Upload new avatar',
|
||||
uploadNewImage: 'Upload new image',
|
||||
moveListToBoard: 'Move list to another board',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -423,6 +423,7 @@ export default {
|
|||
unsubscribe: 'Desuscribirse',
|
||||
uploadNewAvatar: 'Subir un nuevo avatar',
|
||||
uploadNewImage: 'Subir una nueva imagen',
|
||||
moveListToBoard: 'Mover lista a otro tablero',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -234,6 +234,7 @@ export default {
|
|||
unsubscribe: 'لغو اشتراک',
|
||||
uploadNewAvatar: 'آپلود آواتار جدید',
|
||||
uploadNewImage: 'آپلود تصویر جدید',
|
||||
moveListToBoard: 'انتقال لیست به برد دیگر',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -432,6 +432,7 @@ export default {
|
|||
unsubscribe: 'Peru tilaus',
|
||||
uploadNewAvatar: 'Lataa uusi avatar',
|
||||
uploadNewImage: 'Lataa uusi kuva',
|
||||
moveListToBoard: 'Siirrä lista toiselle taululle',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -447,6 +447,7 @@ export default {
|
|||
unsubscribe: 'Se désabonner',
|
||||
uploadNewAvatar: 'Télécharger un nouvel avatar',
|
||||
uploadNewImage: 'Télécharger une nouvelle image',
|
||||
moveListToBoard: 'Déplacer la liste vers un autre tableau',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -235,6 +235,7 @@ export default {
|
|||
unsubscribe: 'Leiratkozás',
|
||||
uploadNewAvatar: 'Új avatar feltöltése',
|
||||
uploadNewImage: 'Új kép feltöltése',
|
||||
moveListToBoard: 'Lista áthelyezése másik táblára',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -227,6 +227,7 @@ export default {
|
|||
unsubscribe: 'Berhenti berlangganan',
|
||||
uploadNewAvatar: 'Unggah avatar baru',
|
||||
uploadNewImage: 'Unggah gambar baru',
|
||||
moveListToBoard: 'Pindahkan daftar ke papan lain',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -429,6 +429,7 @@ export default {
|
|||
unsubscribe: 'Annulla iscrizione',
|
||||
uploadNewAvatar: 'Carica nuovo avatar',
|
||||
uploadNewImage: 'Carica nuova immagine',
|
||||
moveListToBoard: 'Muovi lista a un altra bacheca',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -227,6 +227,7 @@ export default {
|
|||
unsubscribe: '購読解除',
|
||||
uploadNewAvatar: '新しいアバターをアップロード',
|
||||
uploadNewImage: '新しい画像をアップロード',
|
||||
moveListToBoard: 'リストを別のボードに移動',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -237,6 +237,7 @@ export default {
|
|||
unsubscribe: '구독 취소',
|
||||
uploadNewAvatar: '새 아바타 업로드',
|
||||
uploadNewImage: '새 이미지 업로드',
|
||||
moveListToBoard: '목록을 다른 보드로 이동',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -228,6 +228,7 @@ export default {
|
|||
unsubscribe: 'Afmelden',
|
||||
uploadNewAvatar: 'Nieuwe avatar uploaden',
|
||||
uploadNewImage: 'Nieuwe afbeelding uploaden',
|
||||
moveListToBoard: 'Lijst verplaatsen naar ander bord',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -406,6 +406,7 @@ export default {
|
|||
unsubscribe: 'Odsubskrybuj',
|
||||
uploadNewAvatar: 'Wgraj nowy awatar',
|
||||
uploadNewImage: 'Wgraj nowy obraz',
|
||||
moveListToBoard: 'Przenieś listę na inną tablicę',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -228,6 +228,7 @@ export default {
|
|||
unsubscribe: 'Cancelar inscrição',
|
||||
uploadNewAvatar: 'Enviar novo avatar',
|
||||
uploadNewImage: 'Enviar nova imagem',
|
||||
moveListToBoard: 'Mover lista para outro quadro',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -412,6 +412,7 @@ export default {
|
|||
unsubscribe: 'Отписаться',
|
||||
uploadNewAvatar: 'Загрузить новый аватар',
|
||||
uploadNewImage: 'Загрузить новое изображение',
|
||||
moveListToBoard: 'Переместить в другую доску',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -209,6 +209,7 @@ export default {
|
|||
unsubscribe: 'Neodoberať',
|
||||
uploadNewAvatar: 'Nahrať nový avatar',
|
||||
uploadNewImage: 'Nahrať nový obrázok',
|
||||
moveListToBoard: 'Presunúť zoznam na inú tabuľu',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -236,6 +236,7 @@ export default {
|
|||
unsubscribe: 'Укини претплату',
|
||||
uploadNewAvatar: 'Постави нови аватар',
|
||||
uploadNewImage: 'Постави нову слику',
|
||||
moveListToBoard: 'Премести на другу таблу',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -233,6 +233,7 @@ export default {
|
|||
unsubscribe: 'Ukini pretplatu',
|
||||
uploadNewAvatar: 'Postavi novi avatar',
|
||||
uploadNewImage: 'Postavi novu sliku',
|
||||
moveListToBoard: 'Premesti spisak na drugu tablu',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -207,6 +207,7 @@ export default {
|
|||
unsubscribe: 'Avprenumerera',
|
||||
uploadNewAvatar: 'Ladda upp ny avatar',
|
||||
uploadNewImage: 'Ladda upp ny bild',
|
||||
moveListToBoard: 'Flytta lista till annan tavla',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -207,6 +207,7 @@ export default {
|
|||
unsubscribe: 'Abonelikten çık',
|
||||
uploadNewAvatar: 'Yeni avatar yükle',
|
||||
uploadNewImage: 'Yeni resim yükle',
|
||||
moveListToBoard: 'Listeyi başka bir panoya taşı',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -408,6 +408,7 @@ export default {
|
|||
unsubscribe: 'Відписатися',
|
||||
uploadNewAvatar: 'Завантажити новий аватар',
|
||||
uploadNewImage: 'Завантажити нове зображення',
|
||||
moveListToBoard: 'Перемістити список на іншу дошку',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -203,6 +203,7 @@ export default {
|
|||
unsubscribe: 'Obunani bekor qilish',
|
||||
uploadNewAvatar: 'Yangi avatar yuklash',
|
||||
uploadNewImage: 'Yangi rasm yuklash',
|
||||
moveListToBoard: "Ro'yxatni boshqa doskaga ko'chirish",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -422,6 +422,7 @@ export default {
|
|||
unsubscribe: '取消关注',
|
||||
uploadNewAvatar: '上传新头像',
|
||||
uploadNewImage: '上传图片',
|
||||
moveListToBoard: '移动列表到另一个面板',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -220,6 +220,7 @@ export default {
|
|||
unsubscribe: '取消訂閱',
|
||||
uploadNewAvatar: '上傳新頭像',
|
||||
uploadNewImage: '上傳圖片',
|
||||
moveListToBoard: '移動列表到另一個面板',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -5,13 +5,13 @@
|
|||
|
||||
import { call, put, select } from 'redux-saga/effects';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
import request from '../request';
|
||||
import selectors from '../../../selectors';
|
||||
import actions from '../../../actions';
|
||||
import api from '../../../api';
|
||||
import { createLocalId } from '../../../utils/local-id';
|
||||
import ToastTypes from '../../../constants/ToastTypes';
|
||||
import modalActions from '../../../actions/modals';
|
||||
|
||||
export function* createList(boardId, data) {
|
||||
const localId = yield call(createLocalId);
|
||||
|
@ -183,6 +183,23 @@ export function* handleListDelete(list, cards) {
|
|||
yield put(actions.handleListDelete(list, cards));
|
||||
}
|
||||
|
||||
export function* moveListToBoardSaga(action) {
|
||||
const { listId, targetBoardId } = action.payload;
|
||||
try {
|
||||
const { item: updatedList, included } = yield call(request, api.moveToBoard, listId, {
|
||||
targetBoardId,
|
||||
});
|
||||
yield put(actions.handleListUpdate(updatedList));
|
||||
if (included && included.cards) {
|
||||
yield put(actions.handleCardsUpdate(included.cards, []));
|
||||
}
|
||||
yield put(modalActions.closeModal());
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
createList,
|
||||
createListInCurrentBoard,
|
||||
|
@ -196,4 +213,5 @@ export default {
|
|||
handleListClear,
|
||||
deleteList,
|
||||
handleListDelete,
|
||||
moveListToBoardSaga,
|
||||
};
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
import { all, takeEvery } from 'redux-saga/effects';
|
||||
import { all, takeEvery, takeLatest } from 'redux-saga/effects';
|
||||
|
||||
import services from '../services';
|
||||
import EntryActionTypes from '../../../constants/EntryActionTypes';
|
||||
|
@ -41,5 +41,6 @@ export default function* listsWatchers() {
|
|||
takeEvery(EntryActionTypes.LIST_DELETE_HANDLE, ({ payload: { list, cards } }) =>
|
||||
services.handleListDelete(list, cards),
|
||||
),
|
||||
takeLatest(EntryActionTypes.MOVE_LIST_TO_BOARD_REQUEST, services.moveListToBoardSaga),
|
||||
]);
|
||||
}
|
||||
|
|
82
server/api/controllers/lists/move-to-board.js
Normal file
82
server/api/controllers/lists/move-to-board.js
Normal file
|
@ -0,0 +1,82 @@
|
|||
const { idInput } = require('../../../utils/inputs');
|
||||
|
||||
const Errors = {
|
||||
NOT_ENOUGH_RIGHTS: {
|
||||
notEnoughRights: 'Not enough rights',
|
||||
},
|
||||
LIST_NOT_FOUND: {
|
||||
listNotFound: 'List not found',
|
||||
},
|
||||
BOARD_NOT_FOUND: {
|
||||
boardNotFound: 'Board not found',
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
inputs: {
|
||||
id: {
|
||||
...idInput,
|
||||
required: true, // listId
|
||||
},
|
||||
targetBoardId: {
|
||||
...idInput,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
exits: {
|
||||
notEnoughRights: {
|
||||
responseType: 'forbidden',
|
||||
},
|
||||
listNotFound: {
|
||||
responseType: 'notFound',
|
||||
},
|
||||
boardNotFound: {
|
||||
responseType: 'notFound',
|
||||
},
|
||||
},
|
||||
|
||||
async fn(inputs) {
|
||||
const { currentUser } = this.req;
|
||||
|
||||
const { list, board: sourceBoard } = await sails.helpers.lists
|
||||
.getPathToProjectById(inputs.id)
|
||||
.intercept('pathNotFound', () => Errors.LIST_NOT_FOUND);
|
||||
|
||||
const targetBoard = await Board.qm.getOneById(inputs.targetBoardId);
|
||||
if (!targetBoard) {
|
||||
throw Errors.BOARD_NOT_FOUND;
|
||||
}
|
||||
|
||||
const sourceMembership = await BoardMembership.qm.getOneByBoardIdAndUserId(
|
||||
sourceBoard.id,
|
||||
currentUser.id,
|
||||
);
|
||||
const targetMembership = await BoardMembership.qm.getOneByBoardIdAndUserId(
|
||||
targetBoard.id,
|
||||
currentUser.id,
|
||||
);
|
||||
if (
|
||||
!sourceMembership ||
|
||||
!targetMembership ||
|
||||
sourceMembership.role !== BoardMembership.Roles.EDITOR ||
|
||||
targetMembership.role !== BoardMembership.Roles.EDITOR
|
||||
) {
|
||||
throw Errors.NOT_ENOUGH_RIGHTS;
|
||||
}
|
||||
|
||||
const { updatedList, updatedCards } = await sails.helpers.lists.moveToBoard.with({
|
||||
list,
|
||||
targetBoard,
|
||||
actorUser: currentUser,
|
||||
request: this.req,
|
||||
});
|
||||
|
||||
return {
|
||||
item: updatedList,
|
||||
included: {
|
||||
cards: updatedCards,
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
117
server/api/helpers/lists/move-to-board.js
Normal file
117
server/api/helpers/lists/move-to-board.js
Normal file
|
@ -0,0 +1,117 @@
|
|||
module.exports = {
|
||||
inputs: {
|
||||
list: {
|
||||
type: 'ref',
|
||||
required: true,
|
||||
},
|
||||
targetBoard: {
|
||||
type: 'ref',
|
||||
required: true,
|
||||
},
|
||||
actorUser: {
|
||||
type: 'ref',
|
||||
required: true,
|
||||
},
|
||||
request: {
|
||||
type: 'ref',
|
||||
},
|
||||
},
|
||||
|
||||
async fn(inputs) {
|
||||
const updatedList = await List.updateOne(
|
||||
{ id: inputs.list.id },
|
||||
{ boardId: inputs.targetBoard.id },
|
||||
);
|
||||
|
||||
const updatedCards = await Card.update(
|
||||
{ listId: inputs.list.id },
|
||||
{ boardId: inputs.targetBoard.id },
|
||||
).fetch();
|
||||
|
||||
const migrateLabelsPromises = updatedCards.map(async (card) => {
|
||||
const cardLabels = await CardLabel.find({ cardId: card.id });
|
||||
return Promise.all(
|
||||
cardLabels.map(async (cardLabel) => {
|
||||
const oldLabel = await Label.findOne({ id: cardLabel.labelId });
|
||||
if (!oldLabel) return;
|
||||
let newLabel = await Label.findOne({
|
||||
boardId: inputs.targetBoard.id,
|
||||
name: oldLabel.name,
|
||||
color: oldLabel.color,
|
||||
});
|
||||
if (!newLabel) {
|
||||
const maxPosArr = await Label.find({ boardId: inputs.targetBoard.id })
|
||||
.sort('position DESC')
|
||||
.limit(1);
|
||||
const maxPos = maxPosArr.length > 0 ? maxPosArr[0].position : 0;
|
||||
newLabel = await Label.create({
|
||||
boardId: inputs.targetBoard.id,
|
||||
name: oldLabel.name,
|
||||
color: oldLabel.color,
|
||||
position: maxPos + 65536,
|
||||
}).fetch();
|
||||
}
|
||||
await CardLabel.destroy({ cardId: card.id, labelId: cardLabel.labelId });
|
||||
await CardLabel.create({ cardId: card.id, labelId: newLabel.id });
|
||||
}),
|
||||
);
|
||||
});
|
||||
await Promise.all(migrateLabelsPromises);
|
||||
|
||||
await Promise.all(
|
||||
updatedCards.map(async (card) => {
|
||||
const cardMemberships = await CardMembership.find({ cardId: card.id });
|
||||
await Promise.all(
|
||||
cardMemberships.map(async (membership) => {
|
||||
const userMembership = await BoardMembership.findOne({
|
||||
boardId: inputs.targetBoard.id,
|
||||
userId: membership.userId,
|
||||
});
|
||||
if (!userMembership) {
|
||||
await CardMembership.destroy({ id: membership.id });
|
||||
}
|
||||
}),
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
await Promise.all(
|
||||
updatedCards.map(async (card) => {
|
||||
const customFieldValues = await CustomFieldValue.find({ cardId: card.id });
|
||||
await Promise.all(
|
||||
customFieldValues.map(async (value) => {
|
||||
const group = await CustomFieldGroup.findOne({ id: value.customFieldGroupId });
|
||||
if (group && group.boardId && group.boardId !== inputs.targetBoard.id) {
|
||||
const newGroup = await CustomFieldGroup.create({
|
||||
name: group.name,
|
||||
position: group.position,
|
||||
cardId: card.id,
|
||||
baseCustomFieldGroupId: group.baseCustomFieldGroupId,
|
||||
}).fetch();
|
||||
const field = await CustomField.findOne({ id: value.customFieldId });
|
||||
const newField = await CustomField.create({
|
||||
name: field.name,
|
||||
position: field.position,
|
||||
showOnFrontOfCard: field.showOnFrontOfCard,
|
||||
customFieldGroupId: newGroup.id,
|
||||
baseCustomFieldGroupId: field.baseCustomFieldGroupId,
|
||||
}).fetch();
|
||||
await CustomFieldValue.updateOne(
|
||||
{ id: value.id },
|
||||
{
|
||||
customFieldGroupId: newGroup.id,
|
||||
customFieldId: newField.id,
|
||||
},
|
||||
);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
return {
|
||||
updatedList,
|
||||
updatedCards,
|
||||
};
|
||||
},
|
||||
};
|
|
@ -133,6 +133,7 @@ module.exports.routes = {
|
|||
'DELETE /api/cards/:cardId/card-labels/labelId::labelId': 'card-labels/delete',
|
||||
|
||||
'POST /api/cards/:cardId/task-lists': 'task-lists/create',
|
||||
'POST /api/lists/:id/move-to-board': 'lists/move-to-board',
|
||||
'GET /api/task-lists/:id': 'task-lists/show',
|
||||
'PATCH /api/task-lists/:id': 'task-lists/update',
|
||||
'DELETE /api/task-lists/:id': 'task-lists/delete',
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue