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