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 66b63c3c..88b6bf39 100755 --- a/client/src/components/lists/List/ActionsStep.jsx +++ b/client/src/components/lists/List/ActionsStep.jsx @@ -8,6 +8,7 @@ import PropTypes from 'prop-types'; import { useDispatch, useSelector } from 'react-redux'; import { useTranslation } from 'react-i18next'; import { Menu } from 'semantic-ui-react'; +import { useNavigate } from 'react-router-dom'; import { Popup } from '../../../lib/custom-ui'; import selectors from '../../../selectors'; @@ -19,6 +20,8 @@ import SortStep from './SortStep'; import SelectListTypeStep from '../SelectListTypeStep'; import ConfirmationStep from '../../common/ConfirmationStep'; import ArchiveCardsStep from '../../cards/ArchiveCardsStep'; +import BoardSelectStep from './BoardSelectStep'; +import api from '../../../api/lists'; import styles from './ActionsStep.module.scss'; @@ -28,6 +31,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 }) => { @@ -38,6 +42,7 @@ const ActionsStep = React.memo(({ listId, onNameEdit, onCardAdd, onClose }) => { const dispatch = useDispatch(); const [t] = useTranslation(); const [step, openStep, handleBack] = useSteps(); + const navigate = useNavigate(); const handleTypeSelect = useCallback( (type) => { @@ -84,6 +89,25 @@ const ActionsStep = React.memo(({ listId, onNameEdit, onCardAdd, onClose }) => { openStep(StepTypes.DELETE); }, [openStep]); + const handleMoveToBoard = useCallback( + async (targetBoardId) => { + try { + const { item: updatedList, included } = await api.moveToBoard(listId, { targetBoardId }); + dispatch(entryActions.handleListUpdate(updatedList)); + if (included && included.cards) { + dispatch(entryActions.handleCardsUpdate(included.cards, [])); + } + sessionStorage.setItem('movedListId', listId); + onClose(); + navigate(`/boards/${targetBoardId}`); + } catch (err) { + // eslint-disable-next-line no-console + console.error(err); + } + }, + [listId, onClose, dispatch, navigate], + ); + if (step) { switch (step.type) { case StepTypes.EDIT_TYPE: @@ -114,6 +138,14 @@ const ActionsStep = React.memo(({ listId, onNameEdit, onCardAdd, onClose }) => { onBack={handleBack} /> ); + case StepTypes.MOVE_TO_BOARD: + return ( + + ); default: } } @@ -164,6 +196,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 10966717..4b635b79 100644 --- a/client/src/components/lists/List/ActionsStep.module.scss +++ b/client/src/components/lists/List/ActionsStep.module.scss @@ -14,3 +14,34 @@ padding-left: 14px; } } + +.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..40c99dc5 --- /dev/null +++ b/client/src/components/lists/List/BoardSelectStep.jsx @@ -0,0 +1,52 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { useSelector } from 'react-redux'; +import { useTranslation } from 'react-i18next'; +import PopupHeader from '../../../lib/custom-ui/components/Popup/PopupHeader'; +import styles from './ActionsStep.module.scss'; +import selectors from '../../../selectors'; + +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 boards = useSelector((state) => boardIds.map((id) => selectors.selectBoardById(state, id))); + + 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/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 af3e3537..a9c241f3 100644 --- a/client/src/locales/de-DE/core.js +++ b/client/src/locales/de-DE/core.js @@ -423,6 +423,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 21643d5f..014357b1 100644 --- a/client/src/locales/en-GB/core.js +++ b/client/src/locales/en-GB/core.js @@ -431,6 +431,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 23c7c899..159731af 100644 --- a/client/src/locales/en-US/core.js +++ b/client/src/locales/en-US/core.js @@ -426,6 +426,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 51312a4e..7852f7db 100644 --- a/client/src/locales/fr-FR/core.js +++ b/client/src/locales/fr-FR/core.js @@ -239,6 +239,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 3fcbddee..d9ec1016 100644 --- a/client/src/locales/it-IT/core.js +++ b/client/src/locales/it-IT/core.js @@ -426,6 +426,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 f99f465b..2ca8000b 100644 --- a/client/src/locales/ja-JP/core.js +++ b/client/src/locales/ja-JP/core.js @@ -224,6 +224,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 4249a3aa..593b332f 100644 --- a/client/src/locales/ko-KR/core.js +++ b/client/src/locales/ko-KR/core.js @@ -234,6 +234,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 3d9c976e..3bcd159c 100644 --- a/client/src/locales/nl-NL/core.js +++ b/client/src/locales/nl-NL/core.js @@ -225,6 +225,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 384cbfc3..544a2ca2 100644 --- a/client/src/locales/pt-BR/core.js +++ b/client/src/locales/pt-BR/core.js @@ -225,6 +225,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 a11fafd6..b2d3dd4e 100644 --- a/client/src/locales/sk-SK/core.js +++ b/client/src/locales/sk-SK/core.js @@ -206,6 +206,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 4a1803f9..7711e42f 100644 --- a/client/src/locales/sr-Cyrl-RS/core.js +++ b/client/src/locales/sr-Cyrl-RS/core.js @@ -233,6 +233,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 ede7abce..1cefd649 100644 --- a/client/src/locales/sr-Latn-RS/core.js +++ b/client/src/locales/sr-Latn-RS/core.js @@ -1,12 +1,9 @@ import dateFns from 'date-fns/locale/sr-Latn'; import timeAgo from 'javascript-time-ago/locale/sr-Latn'; -import markdownEditor from './markdown-editor.json'; - export default { dateFns, timeAgo, - markdownEditor, format: { date: 'd.M.yyyy.', @@ -236,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/sr-Latn-RS/markdown-editor.json b/client/src/locales/sr-Latn-RS/markdown-editor.json deleted file mode 100644 index 1d8ff203..00000000 --- a/client/src/locales/sr-Latn-RS/markdown-editor.json +++ /dev/null @@ -1,165 +0,0 @@ -{ - "action-previews": { - "text": "Ovo je tekst bez naslova.\nI naslov i tekst\nmogu biti istaknuti podebljano, kurzivom, bojom,\nprecrtan i podvučen.", - "text-with-head": "Ovo je tekst sa naslovom.\nI naslov i tekst\nmogu biti istaknuti podebljano, kurzivom, bojom,\nprecrtan i podvučen.", - "heading": "Naslov" - }, - "bundle": { - "error-title": "Greška u markdown editoru", - "settings_wysiwyg": "Vizuelni editor (wysiwyg)", - "settings_markup": "Markdown oznake", - "markup_placeholder": "Unesite markdown oznake..." - }, - "codeblock": { - "remove": "Ukloni", - "empty_option": "Nema pronađenih podudaranja" - }, - "common": { - "delete": "Ukloni", - "edit": "Uredi", - "toolbar_action_disabled": "Neusaglašen element oznake" - }, - "forms": { - "common_action_cancel": "Otkaži", - "common_action_submit": "Pošalji", - "common_action_upload": "Izaberi", - "common_tab_attach": "Dodaj sa uređaja", - "common_tab_link": "Dodaj preko linka", - "common_link": "Link", - "common_sizes": "Veličina, px", - "image_name": "Naslov", - "image_link_href": "Link slike", - "image_link_href_help": "Adresa na koju vodi link slike.", - "image_alt": "Alternativni tekst", - "image_alt_help": "Alternativni tekst se prikazuje ako slika ne može biti učitana.", - "image_upload_help": "JPEG, GIF ili PNG slika do 1 MB.", - "image_upload_failed": "Neuspešno dodavanje slike", - "image_size_width": "Širina", - "image_size_height": "Visina", - "link_url_help": "Adresa na koju vodi link.", - "link_text": "Tekst linka", - "link_text_help": "Tekst koji se prikazuje kao link.", - "link_open_help": "Otvori link u novom tabu" - }, - "md-hints": { - "header_title": "Zaglavlje", - "header_hint": "# Vaš tekst", - "italic_title": "Kurziv", - "italic_hint": "_Vaš tekst_", - "bold_title": "Podebljano", - "bold_hint": "**Vaš tekst**", - "strikethrough_title": "Precrtano", - "strikethrough_hint": "~~Vaš tekst~~", - "blockquote_title": "Citat", - "blockquote_hint": "> Vaš tekst", - "code_title": "Kod", - "code_hint": "```Vaš tekst```", - "link_title": "Link", - "link_hint": "[Vaš tekst](url)", - "image_title": "Slika", - "image_hint": "![Vaš tekst](url)", - "list_title": "Stavka liste", - "list_hint": "- Vaš tekst", - "numbered-list_title": "Numerisana lista", - "numbered-list_hint": "1. Vaš tekst", - "documentation": "Dokumentacija", - "documentation_link": "https://diplodoc.com/docs/sr/syntax/" - }, - "menubar": { - "bold": "Podebljano", - "code": "Kod", - "code_inline": "Inline kod", - "codeblock": "Blok koda", - "colorify": "Boja teksta", - "colorify__color_blue": "Plava", - "colorify__color_default": "Podrazumevano", - "colorify__color_gray": "Siva", - "colorify__color_green": "Zelena", - "colorify__color_orange": "Narandžasta", - "colorify__color_red": "Crvena", - "colorify__color_violet": "Ljubičasta", - "colorify__color_yellow": "Žuta", - "colorify__group_text": "Tekst", - "cut": "Iseci", - "emoji": "Emodži", - "emoji__hint": "Emodžiji se mogu dodati u WYSIWYG ili ručno preko oznaka", - "heading": "Zaglavlje", - "heading1": "Zaglavlje 1", - "heading2": "Zaglavlje 2", - "heading3": "Zaglavlje 3", - "heading4": "Zaglavlje 4", - "heading5": "Zaglavlje 5", - "heading6": "Zaglavlje 6", - "hrule": "Razdvajač", - "image": "Slika", - "italic": "Kurziv", - "link": "Link", - "list": "Lista", - "list__action_lift": "Podigni stavku", - "list__action_sink": "Spusti stavku", - "list_action_disabled": "Suprotno logici liste", - "mark": "Označeno", - "mono": "Monospace", - "more_action": "Više radnji", - "note": "Beleška", - "olist": "Numerisana lista", - "quote": "Citat", - "redo": "Ponovi", - "strike": "Precrtano", - "table": "Tabela", - "text": "Tekst", - "ulist": "Markirana lista", - "underline": "Podvučeno", - "undo": "Opozovi" - }, - "placeholder": { - "doc_empty": "Ukucajte / za korišćenje komandi...", - "checkbox": "Unesite opis zadatka...", - "deflist_term": "Termin", - "deflist_desc": "Opis definicije", - "heading": "Zaglavlje", - "cut_title": "Naslov", - "cut_content": "Sadržaj koji se prikazuje na klik", - "note_title": "Naslov", - "note_content": "Sadržaj beleške", - "table_cell": "Sadržaj ćelije", - "select_filter": "Pretraži jezike..." - }, - "search": { - "label_case-sensitive": "Razlikuj velika/mala slova", - "label_whole-word": "Cela reč", - "title": "Pretraži u kodu" - }, - "suggest": { - "empty-msg": "Nije pronađeno" - }, - "widgets": { - "image": "Dodaj sliku", - "link": "Dodaj link" - }, - "yfm-note": { - "info": "Beleška", - "tip": "Savet", - "warning": "Upozorenje", - "alert": "Alarm", - "remove": "Ukloni" - }, - "yfm-table": { - "column.add.before": "Dodaj kolonu pre", - "column.add.after": "Dodaj kolonu posle", - "column.remove": "Ukloni kolonu", - "row.add.before": "Dodaj red pre", - "row.add.after": "Dodaj red posle", - "row.remove": "Ukloni red", - "table.remove": "Ukloni tabelu", - "table.menu.cell.align.left": "Poravnaj sadržaj ćelije levo", - "table.menu.cell.align.right": "Poravnaj sadržaj ćelije desno", - "table.menu.cell.align.center": "Centriraj sadržaj ćelije", - "table.menu.row.add": "Dodaj red posle", - "table.menu.row.remove": "Ukloni red", - "table.menu.column.add": "Dodaj kolonu posle", - "table.menu.column.remove": "Ukloni kolonu", - "table.menu.convert.yfm": "Konvertuj u YFM tabelu", - "table.menu.table.remove": "Ukloni tabelu" - } -} 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 8ccccb02..0b14bd56 100644 --- a/client/src/locales/zh-CN/core.js +++ b/client/src/locales/zh-CN/core.js @@ -220,6 +220,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/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..434c235a --- /dev/null +++ b/server/api/helpers/lists/move-to-board.js @@ -0,0 +1,66 @@ +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); + + return { + updatedList, + updatedCards, + }; + }, +}; diff --git a/server/config/routes.js b/server/config/routes.js index ba2292bb..5cda7a6b 100644 --- a/server/config/routes.js +++ b/server/config/routes.js @@ -128,6 +128,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',