From 2880ffccaa05e4235d0f5410c9a94dc8825134c7 Mon Sep 17 00:00:00 2001 From: Christoph Enne Date: Thu, 8 Dec 2022 17:35:13 +0100 Subject: [PATCH] #27 add trello import to UI --- client/src/api/boards.js | 11 ++ client/src/components/Boards/AddPopup.jsx | 48 +++++- client/src/components/Boards/Boards.jsx | 193 +++++++++++----------- client/src/constants/EntryActionTypes.js | 1 + client/src/containers/BoardsContainer.js | 1 + client/src/entry-actions/boards.js | 8 + client/src/locales/en/core.js | 2 + client/src/sagas/core/services/boards.js | 41 +++++ client/src/sagas/core/watchers/boards.js | 3 + 9 files changed, 206 insertions(+), 102 deletions(-) diff --git a/client/src/api/boards.js b/client/src/api/boards.js index b605ed2e..21488dac 100755 --- a/client/src/api/boards.js +++ b/client/src/api/boards.js @@ -1,4 +1,5 @@ import socket from './socket'; +import http from './http'; import { transformCard } from './cards'; import { transformAttachment } from './attachments'; @@ -7,6 +8,15 @@ import { transformAttachment } from './attachments'; const createBoard = (projectId, data, headers) => socket.post(`/projects/${projectId}/boards`, data, headers); +const importBoard = (projectId, data, headers) => + http.post( + `/projects/${projectId}/imports/boards?name=${data.name}&position=${data.position}`, + { + file: data.file, + }, + headers, + ); + const getBoard = (id, headers) => socket.get(`/boards/${id}`, undefined, headers).then((body) => ({ ...body, @@ -23,6 +33,7 @@ const deleteBoard = (id, headers) => socket.delete(`/boards/${id}`, undefined, h export default { createBoard, + importBoard, getBoard, updateBoard, deleteBoard, diff --git a/client/src/components/Boards/AddPopup.jsx b/client/src/components/Boards/AddPopup.jsx index ff378ac5..7ac4b9db 100755 --- a/client/src/components/Boards/AddPopup.jsx +++ b/client/src/components/Boards/AddPopup.jsx @@ -1,21 +1,24 @@ -import React, { useCallback, useEffect, useRef } from 'react'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; import PropTypes from 'prop-types'; import { useTranslation } from 'react-i18next'; -import { Button, Form } from 'semantic-ui-react'; +import { Button, Form, Divider, Menu } from 'semantic-ui-react'; import { withPopup } from '../../lib/popup'; -import { Input, Popup } from '../../lib/custom-ui'; +import { Input, Popup, FilePicker } from '../../lib/custom-ui'; import { useForm } from '../../hooks'; import styles from './AddPopup.module.scss'; -const AddStep = React.memo(({ onCreate, onClose }) => { +const AddStep = React.memo(({ onCreate, onImport, onClose }) => { const [t] = useTranslation(); const [data, handleFieldChange] = useForm({ name: '', + file: null, }); + const [selectedFile, setSelectedFile] = useState(null); + const nameField = useRef(null); const handleSubmit = useCallback(() => { @@ -30,9 +33,25 @@ const AddStep = React.memo(({ onCreate, onClose }) => { return; } - onCreate(cleanData); + if (data.file) { + onImport(cleanData); + } else { + onCreate(cleanData); + } + onClose(); - }, [onCreate, onClose, data]); + }, [onClose, data, onImport, onCreate]); + + const handleFileSelect = useCallback( + (file) => { + handleFieldChange(null, { + name: 'file', + value: file, + }); + setSelectedFile(file); + }, + [handleFieldChange, setSelectedFile], + ); useEffect(() => { nameField.current.focus(); @@ -55,7 +74,21 @@ const AddStep = React.memo(({ onCreate, onClose }) => { className={styles.field} onChange={handleFieldChange} /> - - - )} - - ) : ( - // eslint-disable-next-line react/jsx-props-no-spreading - - {item.name} - - )} + {item.name} + + {canEdit && ( + handleUpdate(item.id, data)} + onDelete={() => handleDelete(item.id)} + > + + + )} + + ) : ( + // eslint-disable-next-line react/jsx-props-no-spreading + + {item.name} + + )} + - - )} - - )); + )} + + )); - return ( -
-
- - - {({ innerRef, droppableProps, placeholder }) => ( - // eslint-disable-next-line react/jsx-props-no-spreading -
- {itemsNode} - {placeholder} - {canEdit && ( - -
- )} -
-
+ return ( +
+
+ + + {({ innerRef, droppableProps, placeholder }) => ( + // eslint-disable-next-line react/jsx-props-no-spreading +
+ {itemsNode} + {placeholder} + {canEdit && ( + +
+ )} +
+
+
-
- ); -}); + ); + }, +); Boards.propTypes = { items: PropTypes.array.isRequired, // eslint-disable-line react/forbid-prop-types currentId: PropTypes.string, canEdit: PropTypes.bool.isRequired, onCreate: PropTypes.func.isRequired, + onImport: PropTypes.func.isRequired, onUpdate: PropTypes.func.isRequired, onMove: PropTypes.func.isRequired, onDelete: PropTypes.func.isRequired, diff --git a/client/src/constants/EntryActionTypes.js b/client/src/constants/EntryActionTypes.js index 5844a96a..754a007c 100755 --- a/client/src/constants/EntryActionTypes.js +++ b/client/src/constants/EntryActionTypes.js @@ -76,6 +76,7 @@ export default { /* Boards */ BOARD_IN_CURRENT_PROJECT_CREATE: `${PREFIX}/BOARD_IN_CURRENT_PROJECT_CREATE`, + TRELLO_BOARD_IN_CURRENT_PROJECT_IMPORT: `${PREFIX}/TRELLO_BOARD_IN_CURRENT_PROJECT_IMPORT`, BOARD_CREATE_HANDLE: `${PREFIX}/BOARD_CREATE_HANDLE`, BOARD_FETCH: `${PREFIX}/BOARD_FETCH`, BOARD_UPDATE: `${PREFIX}/BOARD_UPDATE`, diff --git a/client/src/containers/BoardsContainer.js b/client/src/containers/BoardsContainer.js index a8bbf2e3..ffee0996 100755 --- a/client/src/containers/BoardsContainer.js +++ b/client/src/containers/BoardsContainer.js @@ -21,6 +21,7 @@ const mapDispatchToProps = (dispatch) => bindActionCreators( { onCreate: entryActions.createBoardInCurrentProject, + onImport: entryActions.importTrelloBoardInCurrentProject, onUpdate: entryActions.updateBoard, onMove: entryActions.moveBoard, onDelete: entryActions.deleteBoard, diff --git a/client/src/entry-actions/boards.js b/client/src/entry-actions/boards.js index 638f49ba..b651878d 100755 --- a/client/src/entry-actions/boards.js +++ b/client/src/entry-actions/boards.js @@ -7,6 +7,13 @@ const createBoardInCurrentProject = (data) => ({ }, }); +const importTrelloBoardInCurrentProject = (data) => ({ + type: EntryActionTypes.TRELLO_BOARD_IN_CURRENT_PROJECT_IMPORT, + payload: { + data, + }, +}); + const handleBoardCreate = (board) => ({ type: EntryActionTypes.BOARD_CREATE_HANDLE, payload: { @@ -60,6 +67,7 @@ const handleBoardDelete = (board) => ({ export default { createBoardInCurrentProject, + importTrelloBoardInCurrentProject, handleBoardCreate, fetchBoard, updateBoard, diff --git a/client/src/locales/en/core.js b/client/src/locales/en/core.js index e43fa667..b74adcf8 100644 --- a/client/src/locales/en/core.js +++ b/client/src/locales/en/core.js @@ -143,6 +143,7 @@ export default { time: 'Time', timer: 'Timer', title: 'Title', + uploadTrelloFile_title: 'Import Trello JSON File', userActions_title: 'User Actions', userAddedThisCardToList: '<0>{{user}}<1> added this card to {{list}}', userLeftNewCommentToCard: '{{user}} left a new comment «{{comment}}» to <2>{{card}}', @@ -201,6 +202,7 @@ export default { editTitle_title: 'Edit Title', editUsername_title: 'Edit Username', hideDetails: 'Hide details', + importTrelloBoard: 'Import Trello Board', leaveBoard: 'Leave board', leaveProject: 'Leave project', logOut_title: 'Log Out', diff --git a/client/src/sagas/core/services/boards.js b/client/src/sagas/core/services/boards.js index cc138810..e1446263 100644 --- a/client/src/sagas/core/services/boards.js +++ b/client/src/sagas/core/services/boards.js @@ -46,6 +46,46 @@ export function* createBoardInCurrentProject(data) { yield call(createBoard, projectId, data); } +export function* importBoard(projectId, data) { + const nextData = { + ...data, + position: yield select(selectors.selectNextBoardPosition, projectId), + }; + + const localId = yield call(createLocalId); + + yield put( + actions.createBoard({ + ...nextData, + projectId, + id: localId, + }), + ); + + let board; + let boardMemberships; + + try { + ({ + item: board, + included: { boardMemberships }, + } = yield call(request, api.importBoard, projectId, nextData)); + } catch (error) { + yield put(actions.createBoard.failure(localId, error)); + return; + } + + yield put(actions.createBoard.success(localId, board, boardMemberships)); + yield put(actions.fetchBoard(board.id)); + yield call(goToBoard, board.id); +} + +export function* importTrelloBoardInCurrentProject(data) { + const { projectId } = yield select(selectors.selectPath); + + yield call(importBoard, projectId, data); +} + export function* handleBoardCreate(board) { yield put(actions.handleBoardCreate(board)); } @@ -163,6 +203,7 @@ export function* handleBoardDelete(board) { export default { createBoard, createBoardInCurrentProject, + importTrelloBoardInCurrentProject, handleBoardCreate, fetchBoard, updateBoard, diff --git a/client/src/sagas/core/watchers/boards.js b/client/src/sagas/core/watchers/boards.js index ad9f2615..06139059 100644 --- a/client/src/sagas/core/watchers/boards.js +++ b/client/src/sagas/core/watchers/boards.js @@ -8,6 +8,9 @@ export default function* boardsWatchers() { takeEvery(EntryActionTypes.BOARD_IN_CURRENT_PROJECT_CREATE, ({ payload: { data } }) => services.createBoardInCurrentProject(data), ), + takeEvery(EntryActionTypes.TRELLO_BOARD_IN_CURRENT_PROJECT_IMPORT, ({ payload: { data } }) => + services.importTrelloBoardInCurrentProject(data), + ), takeEvery(EntryActionTypes.BOARD_CREATE_HANDLE, ({ payload: { board } }) => services.handleBoardCreate(board), ),