diff --git a/client/src/api/client.js b/client/src/api/client.js new file mode 100644 index 00000000..04dae615 --- /dev/null +++ b/client/src/api/client.js @@ -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; diff --git a/client/src/api/lists.js b/client/src/api/lists.js index 228fabbe..d342d969 100755 --- a/client/src/api/lists.js +++ b/client/src/api/lists.js @@ -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, }; diff --git a/client/src/api/socket.js b/client/src/api/socket.js index f3a4dd9f..7478d8c9 100755 --- a/client/src/api/socket.js +++ b/client/src/api/socket.js @@ -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; diff --git a/client/src/components/lists/List/ActionsStep.jsx b/client/src/components/lists/List/ActionsStep.jsx index fc1a91f0..e851fe17 100755 --- a/client/src/components/lists/List/ActionsStep.jsx +++ b/client/src/components/lists/List/ActionsStep.jsx @@ -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 ( + + ); default: } } @@ -171,6 +188,9 @@ const ActionsStep = React.memo(({ listId, onNameEdit, onCardAdd, onClose }) => { context: 'title', })} + openStep(StepTypes.MOVE_TO_BOARD)}> + {t('action.moveListToBoard', { context: 'title' })} + diff --git a/client/src/components/lists/List/ActionsStep.module.scss b/client/src/components/lists/List/ActionsStep.module.scss index 6c554199..90872a22 100644 --- a/client/src/components/lists/List/ActionsStep.module.scss +++ b/client/src/components/lists/List/ActionsStep.module.scss @@ -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; +} diff --git a/client/src/components/lists/List/BoardSelectStep.jsx b/client/src/components/lists/List/BoardSelectStep.jsx new file mode 100644 index 00000000..40705a5e --- /dev/null +++ b/client/src/components/lists/List/BoardSelectStep.jsx @@ -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 ( +
+ + {t('action.moveListToBoard', { context: 'title' })} + +
+ {boards + .filter((b) => b && b.id !== currentBoardId) + .map((board) => ( + + ))} + {boards.filter((b) => b && b.id !== currentBoardId).length === 0 && ( +
{t('common.noOtherBoards')}
+ )} +
+
+ ); +} + +BoardSelectStep.propTypes = { + currentBoardId: PropTypes.string.isRequired, + onSelect: PropTypes.func.isRequired, + onBack: PropTypes.func.isRequired, + onClose: PropTypes.func, +}; + +BoardSelectStep.defaultProps = { + onClose: undefined, +}; + +export default BoardSelectStep; diff --git a/client/src/constants/EntryActionTypes.js b/client/src/constants/EntryActionTypes.js index 5acdadc8..4d77a723 100755 --- a/client/src/constants/EntryActionTypes.js +++ b/client/src/constants/EntryActionTypes.js @@ -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`, }; diff --git a/client/src/entry-actions/lists.js b/client/src/entry-actions/lists.js index 2f245d3b..e7a06df6 100755 --- a/client/src/entry-actions/lists.js +++ b/client/src/entry-actions/lists.js @@ -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, }; diff --git a/client/src/locales/ar-YE/core.js b/client/src/locales/ar-YE/core.js index 560742a3..b9e027e2 100644 --- a/client/src/locales/ar-YE/core.js +++ b/client/src/locales/ar-YE/core.js @@ -232,6 +232,7 @@ export default { unsubscribe: 'إلغاء الاشتراك', uploadNewAvatar: 'رفع صورة رمزية جديدة', uploadNewImage: 'رفع صورة جديدة', + moveListToBoard: 'نقل القائمة إلى لوحة أخرى', }, }, }; diff --git a/client/src/locales/cs-CZ/core.js b/client/src/locales/cs-CZ/core.js index 4461ac75..2866e7a8 100644 --- a/client/src/locales/cs-CZ/core.js +++ b/client/src/locales/cs-CZ/core.js @@ -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', }, }, }; diff --git a/client/src/locales/da-DK/core.js b/client/src/locales/da-DK/core.js index 3e7c5c2c..b2cd9f6c 100644 --- a/client/src/locales/da-DK/core.js +++ b/client/src/locales/da-DK/core.js @@ -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', }, }, }; diff --git a/client/src/locales/de-DE/core.js b/client/src/locales/de-DE/core.js index 43bf1cd5..53f215d4 100644 --- a/client/src/locales/de-DE/core.js +++ b/client/src/locales/de-DE/core.js @@ -425,6 +425,7 @@ export default { unsubscribe: 'De-abonnieren', uploadNewAvatar: 'Neuen Avatar hochladen', uploadNewImage: 'Neues Bild hochladen', + moveListToBoard: 'Liste auf andere Arbeitsbereich verschieben', }, }, }; diff --git a/client/src/locales/el-GR/core.js b/client/src/locales/el-GR/core.js index 58e45276..e4805324 100644 --- a/client/src/locales/el-GR/core.js +++ b/client/src/locales/el-GR/core.js @@ -450,6 +450,7 @@ export default { unsubscribe: 'Απεγγραφή', uploadNewAvatar: 'Μεταφόρτωση νέου avatar', uploadNewImage: 'Μεταφόρτωση νέας εικόνας', + moveListToBoard: 'Μετακίνηση λίστας σε άλλο πίνακα', }, }, }; diff --git a/client/src/locales/en-GB/core.js b/client/src/locales/en-GB/core.js index 92f7f85b..06fce5a6 100644 --- a/client/src/locales/en-GB/core.js +++ b/client/src/locales/en-GB/core.js @@ -444,6 +444,7 @@ export default { unsubscribe: 'Unsubscribe', uploadNewAvatar: 'Upload new avatar', uploadNewImage: 'Upload new image', + moveListToBoard: 'Move list to another board', }, }, }; diff --git a/client/src/locales/en-US/core.js b/client/src/locales/en-US/core.js index f7db079d..7f87dbb1 100644 --- a/client/src/locales/en-US/core.js +++ b/client/src/locales/en-US/core.js @@ -439,6 +439,7 @@ export default { unsubscribe: 'Unsubscribe', uploadNewAvatar: 'Upload new avatar', uploadNewImage: 'Upload new image', + moveListToBoard: 'Move list to another board', }, }, }; diff --git a/client/src/locales/es-ES/core.js b/client/src/locales/es-ES/core.js index 84206466..1179b311 100644 --- a/client/src/locales/es-ES/core.js +++ b/client/src/locales/es-ES/core.js @@ -423,6 +423,7 @@ export default { unsubscribe: 'Desuscribirse', uploadNewAvatar: 'Subir un nuevo avatar', uploadNewImage: 'Subir una nueva imagen', + moveListToBoard: 'Mover lista a otro tablero', }, }, }; diff --git a/client/src/locales/fa-IR/core.js b/client/src/locales/fa-IR/core.js index f01aa0cf..22c2b73a 100644 --- a/client/src/locales/fa-IR/core.js +++ b/client/src/locales/fa-IR/core.js @@ -234,6 +234,7 @@ export default { unsubscribe: 'لغو اشتراک', uploadNewAvatar: 'آپلود آواتار جدید', uploadNewImage: 'آپلود تصویر جدید', + moveListToBoard: 'انتقال لیست به برد دیگر', }, }, }; diff --git a/client/src/locales/fi-FI/core.js b/client/src/locales/fi-FI/core.js index 945ce70a..bf033168 100644 --- a/client/src/locales/fi-FI/core.js +++ b/client/src/locales/fi-FI/core.js @@ -432,6 +432,7 @@ export default { unsubscribe: 'Peru tilaus', uploadNewAvatar: 'Lataa uusi avatar', uploadNewImage: 'Lataa uusi kuva', + moveListToBoard: 'Siirrä lista toiselle taululle', }, }, }; diff --git a/client/src/locales/fr-FR/core.js b/client/src/locales/fr-FR/core.js index a88c67b0..934891e4 100644 --- a/client/src/locales/fr-FR/core.js +++ b/client/src/locales/fr-FR/core.js @@ -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', }, }, }; diff --git a/client/src/locales/hu-HU/core.js b/client/src/locales/hu-HU/core.js index fb5a778f..6d17ee50 100644 --- a/client/src/locales/hu-HU/core.js +++ b/client/src/locales/hu-HU/core.js @@ -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', }, }, }; diff --git a/client/src/locales/id-ID/core.js b/client/src/locales/id-ID/core.js index 6ab44b06..798df2b5 100644 --- a/client/src/locales/id-ID/core.js +++ b/client/src/locales/id-ID/core.js @@ -227,6 +227,7 @@ export default { unsubscribe: 'Berhenti berlangganan', uploadNewAvatar: 'Unggah avatar baru', uploadNewImage: 'Unggah gambar baru', + moveListToBoard: 'Pindahkan daftar ke papan lain', }, }, }; diff --git a/client/src/locales/it-IT/core.js b/client/src/locales/it-IT/core.js index 493cff49..00268cec 100644 --- a/client/src/locales/it-IT/core.js +++ b/client/src/locales/it-IT/core.js @@ -429,6 +429,7 @@ export default { unsubscribe: 'Annulla iscrizione', uploadNewAvatar: 'Carica nuovo avatar', uploadNewImage: 'Carica nuova immagine', + moveListToBoard: 'Muovi lista a un altra bacheca', }, }, }; diff --git a/client/src/locales/ja-JP/core.js b/client/src/locales/ja-JP/core.js index df57865b..4bde0874 100644 --- a/client/src/locales/ja-JP/core.js +++ b/client/src/locales/ja-JP/core.js @@ -227,6 +227,7 @@ export default { unsubscribe: '購読解除', uploadNewAvatar: '新しいアバターをアップロード', uploadNewImage: '新しい画像をアップロード', + moveListToBoard: 'リストを別のボードに移動', }, }, }; diff --git a/client/src/locales/ko-KR/core.js b/client/src/locales/ko-KR/core.js index a52e4a05..93b5ec99 100644 --- a/client/src/locales/ko-KR/core.js +++ b/client/src/locales/ko-KR/core.js @@ -237,6 +237,7 @@ export default { unsubscribe: '구독 취소', uploadNewAvatar: '새 아바타 업로드', uploadNewImage: '새 이미지 업로드', + moveListToBoard: '목록을 다른 보드로 이동', }, }, }; diff --git a/client/src/locales/nl-NL/core.js b/client/src/locales/nl-NL/core.js index 24ce91e3..5b6021db 100644 --- a/client/src/locales/nl-NL/core.js +++ b/client/src/locales/nl-NL/core.js @@ -228,6 +228,7 @@ export default { unsubscribe: 'Afmelden', uploadNewAvatar: 'Nieuwe avatar uploaden', uploadNewImage: 'Nieuwe afbeelding uploaden', + moveListToBoard: 'Lijst verplaatsen naar ander bord', }, }, }; diff --git a/client/src/locales/pl-PL/core.js b/client/src/locales/pl-PL/core.js index a26656ec..aa7dcfb1 100644 --- a/client/src/locales/pl-PL/core.js +++ b/client/src/locales/pl-PL/core.js @@ -406,6 +406,7 @@ export default { unsubscribe: 'Odsubskrybuj', uploadNewAvatar: 'Wgraj nowy awatar', uploadNewImage: 'Wgraj nowy obraz', + moveListToBoard: 'Przenieś listę na inną tablicę', }, }, }; diff --git a/client/src/locales/pt-BR/core.js b/client/src/locales/pt-BR/core.js index 14fe2632..77275302 100644 --- a/client/src/locales/pt-BR/core.js +++ b/client/src/locales/pt-BR/core.js @@ -228,6 +228,7 @@ export default { unsubscribe: 'Cancelar inscrição', uploadNewAvatar: 'Enviar novo avatar', uploadNewImage: 'Enviar nova imagem', + moveListToBoard: 'Mover lista para outro quadro', }, }, }; diff --git a/client/src/locales/ru-RU/core.js b/client/src/locales/ru-RU/core.js index d15d76a5..eb1ac881 100644 --- a/client/src/locales/ru-RU/core.js +++ b/client/src/locales/ru-RU/core.js @@ -412,6 +412,7 @@ export default { unsubscribe: 'Отписаться', uploadNewAvatar: 'Загрузить новый аватар', uploadNewImage: 'Загрузить новое изображение', + moveListToBoard: 'Переместить в другую доску', }, }, }; diff --git a/client/src/locales/sk-SK/core.js b/client/src/locales/sk-SK/core.js index 11f67c31..3498276f 100644 --- a/client/src/locales/sk-SK/core.js +++ b/client/src/locales/sk-SK/core.js @@ -209,6 +209,7 @@ export default { unsubscribe: 'Neodoberať', uploadNewAvatar: 'Nahrať nový avatar', uploadNewImage: 'Nahrať nový obrázok', + moveListToBoard: 'Presunúť zoznam na inú tabuľu', }, }, }; diff --git a/client/src/locales/sr-Cyrl-RS/core.js b/client/src/locales/sr-Cyrl-RS/core.js index e6802980..26f235e8 100644 --- a/client/src/locales/sr-Cyrl-RS/core.js +++ b/client/src/locales/sr-Cyrl-RS/core.js @@ -236,6 +236,7 @@ export default { unsubscribe: 'Укини претплату', uploadNewAvatar: 'Постави нови аватар', uploadNewImage: 'Постави нову слику', + moveListToBoard: 'Премести на другу таблу', }, }, }; diff --git a/client/src/locales/sr-Latn-RS/core.js b/client/src/locales/sr-Latn-RS/core.js index 519dbfba..1cefd649 100644 --- a/client/src/locales/sr-Latn-RS/core.js +++ b/client/src/locales/sr-Latn-RS/core.js @@ -233,6 +233,7 @@ export default { unsubscribe: 'Ukini pretplatu', uploadNewAvatar: 'Postavi novi avatar', uploadNewImage: 'Postavi novu sliku', + moveListToBoard: 'Premesti spisak na drugu tablu', }, }, }; diff --git a/client/src/locales/sv-SE/core.js b/client/src/locales/sv-SE/core.js index 78e091df..5d0610c4 100644 --- a/client/src/locales/sv-SE/core.js +++ b/client/src/locales/sv-SE/core.js @@ -207,6 +207,7 @@ export default { unsubscribe: 'Avprenumerera', uploadNewAvatar: 'Ladda upp ny avatar', uploadNewImage: 'Ladda upp ny bild', + moveListToBoard: 'Flytta lista till annan tavla', }, }, }; diff --git a/client/src/locales/tr-TR/core.js b/client/src/locales/tr-TR/core.js index dd782673..c0075be2 100644 --- a/client/src/locales/tr-TR/core.js +++ b/client/src/locales/tr-TR/core.js @@ -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şı', }, }, }; diff --git a/client/src/locales/uk-UA/core.js b/client/src/locales/uk-UA/core.js index 32392c3c..a510ffdb 100644 --- a/client/src/locales/uk-UA/core.js +++ b/client/src/locales/uk-UA/core.js @@ -408,6 +408,7 @@ export default { unsubscribe: 'Відписатися', uploadNewAvatar: 'Завантажити новий аватар', uploadNewImage: 'Завантажити нове зображення', + moveListToBoard: 'Перемістити список на іншу дошку', }, }, }; diff --git a/client/src/locales/uz-UZ/core.js b/client/src/locales/uz-UZ/core.js index 3bee8806..9d04eb09 100644 --- a/client/src/locales/uz-UZ/core.js +++ b/client/src/locales/uz-UZ/core.js @@ -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", }, }, }; diff --git a/client/src/locales/zh-CN/core.js b/client/src/locales/zh-CN/core.js index f7003fb0..76c49e10 100644 --- a/client/src/locales/zh-CN/core.js +++ b/client/src/locales/zh-CN/core.js @@ -422,6 +422,7 @@ export default { unsubscribe: '取消关注', uploadNewAvatar: '上传新头像', uploadNewImage: '上传图片', + moveListToBoard: '移动列表到另一个面板', }, }, }; diff --git a/client/src/locales/zh-TW/core.js b/client/src/locales/zh-TW/core.js index 00107c14..4561a834 100644 --- a/client/src/locales/zh-TW/core.js +++ b/client/src/locales/zh-TW/core.js @@ -220,6 +220,7 @@ export default { unsubscribe: '取消訂閱', uploadNewAvatar: '上傳新頭像', uploadNewImage: '上傳圖片', + moveListToBoard: '移動列表到另一個面板', }, }, }; diff --git a/client/src/sagas/core/services/lists.js b/client/src/sagas/core/services/lists.js index acd6c3e2..2430cfac 100644 --- a/client/src/sagas/core/services/lists.js +++ b/client/src/sagas/core/services/lists.js @@ -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, }; diff --git a/client/src/sagas/core/watchers/lists.js b/client/src/sagas/core/watchers/lists.js index 59f1241a..8ab33e54 100644 --- a/client/src/sagas/core/watchers/lists.js +++ b/client/src/sagas/core/watchers/lists.js @@ -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), ]); } diff --git a/server/api/controllers/lists/move-to-board.js b/server/api/controllers/lists/move-to-board.js new file mode 100644 index 00000000..b3572db9 --- /dev/null +++ b/server/api/controllers/lists/move-to-board.js @@ -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, + }, + }; + }, +}; diff --git a/server/api/helpers/lists/move-to-board.js b/server/api/helpers/lists/move-to-board.js new file mode 100644 index 00000000..708d0337 --- /dev/null +++ b/server/api/helpers/lists/move-to-board.js @@ -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, + }; + }, +}; diff --git a/server/config/routes.js b/server/config/routes.js index acb761ff..a7760634 100644 --- a/server/config/routes.js +++ b/server/config/routes.js @@ -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',