1
0
Fork 0
mirror of https://github.com/plankanban/planka.git synced 2025-07-19 05:09:43 +02:00

Make columns itself scrollable, fix action creation when moving card, little refactoring

This commit is contained in:
Maksim Eltyshev 2020-05-16 04:09:46 +05:00
parent 66c570f234
commit 3a1929efba
44 changed files with 549 additions and 438 deletions

View file

@ -1,26 +1,29 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import HeaderContainer from '../containers/HeaderContainer'; import ModalTypes from '../constants/ModalTypes';
import ProjectsContainer from '../containers/ProjectsContainer'; import FixedWrapperContainer from '../containers/FixedWrapperContainer';
import StaticWrapperContainer from '../containers/StaticWrapperContainer';
import UsersModalContainer from '../containers/UsersModalContainer'; import UsersModalContainer from '../containers/UsersModalContainer';
import UserSettingsModalContainer from '../containers/UserSettingsModalContainer'; import UserSettingsModalContainer from '../containers/UserSettingsModalContainer';
import AddProjectModalContainer from '../containers/AddProjectModalContainer'; import AddProjectModalContainer from '../containers/AddProjectModalContainer';
const App = ({ isUsersModalOpened, isUserSettingsModalOpened, isAddProjectModalOpened }) => ( const App = ({ currentModal }) => (
<> <>
<HeaderContainer /> <FixedWrapperContainer />
<ProjectsContainer /> <StaticWrapperContainer />
{isUsersModalOpened && <UsersModalContainer />} {currentModal === ModalTypes.USERS && <UsersModalContainer />}
{isUserSettingsModalOpened && <UserSettingsModalContainer />} {currentModal === ModalTypes.USER_SETTINGS && <UserSettingsModalContainer />}
{isAddProjectModalOpened && <AddProjectModalContainer />} {currentModal === ModalTypes.ADD_PROJECT && <AddProjectModalContainer />}
</> </>
); );
App.propTypes = { App.propTypes = {
isUsersModalOpened: PropTypes.bool.isRequired, currentModal: PropTypes.oneOf(Object.values(ModalTypes)),
isUserSettingsModalOpened: PropTypes.bool.isRequired, };
isAddProjectModalOpened: PropTypes.bool.isRequired,
App.defaultProps = {
currentModal: undefined,
}; };
export default App; export default App;

View file

@ -1,4 +1,4 @@
import React, { useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react'; import React, { useCallback, useEffect, useRef } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Button, Form, Input } from 'semantic-ui-react'; import { Button, Form, Input } from 'semantic-ui-react';
@ -12,48 +12,23 @@ const DEFAULT_DATA = {
name: '', name: '',
}; };
const AddList = React.forwardRef(({ children, onCreate }, ref) => { const AddList = React.memo(({ onCreate, onClose }) => {
const [t] = useTranslation(); const [t] = useTranslation();
const [isOpened, setIsOpened] = useState(false);
const [data, handleFieldChange, setData] = useForm(DEFAULT_DATA); const [data, handleFieldChange, setData] = useForm(DEFAULT_DATA);
const [selectNameFieldState, selectNameField] = useToggle(); const [selectNameFieldState, selectNameField] = useToggle();
const nameField = useRef(null); const nameField = useRef(null);
const open = useCallback(() => {
setIsOpened(true);
}, []);
const close = useCallback(() => {
setIsOpened(false);
}, []);
useImperativeHandle(
ref,
() => ({
open,
close,
}),
[open, close],
);
const handleChildrenClick = useCallback(() => {
open();
}, [open]);
const handleFieldKeyDown = useCallback( const handleFieldKeyDown = useCallback(
(event) => { (event) => {
if (event.key === 'Escape') { if (event.key === 'Escape') {
close(); onClose();
} }
}, },
[close], [onClose],
); );
const [handleFieldBlur, handleControlMouseOver, handleControlMouseOut] = useClosableForm( const [handleFieldBlur, handleControlMouseOver, handleControlMouseOut] = useClosableForm(onClose);
isOpened,
close,
);
const handleSubmit = useCallback(() => { const handleSubmit = useCallback(() => {
const cleanData = { const cleanData = {
@ -73,21 +48,13 @@ const AddList = React.forwardRef(({ children, onCreate }, ref) => {
}, [onCreate, data, setData, selectNameField]); }, [onCreate, data, setData, selectNameField]);
useEffect(() => { useEffect(() => {
if (isOpened) {
nameField.current.select(); nameField.current.select();
} }, []);
}, [isOpened]);
useDidUpdate(() => { useDidUpdate(() => {
nameField.current.select(); nameField.current.select();
}, [selectNameFieldState]); }, [selectNameFieldState]);
if (!isOpened) {
return React.cloneElement(children, {
onClick: handleChildrenClick,
});
}
return ( return (
<Form className={styles.wrapper} onSubmit={handleSubmit}> <Form className={styles.wrapper} onSubmit={handleSubmit}>
<Input <Input
@ -115,8 +82,8 @@ const AddList = React.forwardRef(({ children, onCreate }, ref) => {
}); });
AddList.propTypes = { AddList.propTypes = {
children: PropTypes.element.isRequired,
onCreate: PropTypes.func.isRequired, onCreate: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired,
}; };
export default React.memo(AddList); export default AddList;

View file

@ -1,4 +1,4 @@
import React, { useCallback } from 'react'; import React, { useCallback, useEffect, useRef, useState } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { DragDropContext, Droppable } from 'react-beautiful-dnd'; import { DragDropContext, Droppable } from 'react-beautiful-dnd';
@ -35,6 +35,18 @@ const Board = React.memo(
onLabelDelete, onLabelDelete,
}) => { }) => {
const [t] = useTranslation(); const [t] = useTranslation();
const [isAddListOpened, setIsAddListOpened] = useState(false);
const wrapper = useRef(null);
const prevPosition = useRef(null);
const handleAddListClick = useCallback(() => {
setIsAddListOpened(true);
}, []);
const handleAddListClose = useCallback(() => {
setIsAddListOpened(false);
}, []);
const handleDragStart = useCallback(() => { const handleDragStart = useCallback(() => {
closePopup(); closePopup();
@ -66,8 +78,58 @@ const Board = React.memo(
[onListMove, onCardMove], [onListMove, onCardMove],
); );
const handleMouseDown = useCallback(
(event) => {
if (event.target !== wrapper.current && !event.target.dataset.dragScroller) {
return;
}
prevPosition.current = event.clientX;
},
[wrapper],
);
const handleWindowMouseMove = useCallback(
(event) => {
if (!prevPosition.current) {
return;
}
event.preventDefault();
window.scrollBy({
left: prevPosition.current - event.clientX,
});
prevPosition.current = event.clientX;
},
[prevPosition],
);
const handleWindowMouseUp = useCallback(() => {
prevPosition.current = null;
}, [prevPosition]);
useEffect(() => {
if (isAddListOpened) {
window.scroll(document.body.scrollWidth, 0);
}
}, [listIds, isAddListOpened]);
useEffect(() => {
window.addEventListener('mouseup', handleWindowMouseUp);
window.addEventListener('mousemove', handleWindowMouseMove);
return () => {
window.removeEventListener('mouseup', handleWindowMouseUp);
window.removeEventListener('mousemove', handleWindowMouseMove);
};
}, [handleWindowMouseUp, handleWindowMouseMove]);
return ( return (
<> <>
{/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */}
<div ref={wrapper} className={styles.wrapper} onMouseDown={handleMouseDown}>
<Filter <Filter
users={filterUsers} users={filterUsers}
labels={filterLabels} labels={filterLabels}
@ -81,31 +143,46 @@ const Board = React.memo(
onLabelUpdate={onLabelUpdate} onLabelUpdate={onLabelUpdate}
onLabelDelete={onLabelDelete} onLabelDelete={onLabelDelete}
/> />
<div className={styles.wrapper}> <div>
<DragDropContext onDragStart={handleDragStart} onDragEnd={handleDragEnd}> <DragDropContext onDragStart={handleDragStart} onDragEnd={handleDragEnd}>
<Droppable droppableId="board" type={DroppableTypes.LIST} direction="horizontal"> <Droppable droppableId="board" type={DroppableTypes.LIST} direction="horizontal">
{({ innerRef, droppableProps, placeholder }) => ( {({ innerRef, droppableProps, placeholder }) => (
// eslint-disable-next-line react/jsx-props-no-spreading <div
<div {...droppableProps} data-drag-scroller ref={innerRef} className={styles.lists}> {...droppableProps} // eslint-disable-line react/jsx-props-no-spreading
data-drag-scroller
ref={innerRef}
className={styles.lists}
>
{listIds.map((listId, index) => ( {listIds.map((listId, index) => (
<ListContainer key={listId} id={listId} index={index} /> <ListContainer key={listId} id={listId} index={index} />
))} ))}
{placeholder} {placeholder}
<div data-drag-scroller className={styles.list}> <div data-drag-scroller className={styles.list}>
<AddList onCreate={onListCreate}> {isAddListOpened ? (
<button type="button" className={styles.addListButton}> <AddList
isOpened={isAddListOpened}
onCreate={onListCreate}
onClose={handleAddListClose}
/>
) : (
<button
type="button"
className={styles.addListButton}
onClick={handleAddListClick}
>
<PlusMathIcon className={styles.addListButtonIcon} /> <PlusMathIcon className={styles.addListButtonIcon} />
<span className={styles.addListButtonText}> <span className={styles.addListButtonText}>
{listIds.length > 0 ? t('action.addAnotherList') : t('action.addList')} {listIds.length > 0 ? t('action.addAnotherList') : t('action.addList')}
</span> </span>
</button> </button>
</AddList> )}
</div> </div>
</div> </div>
)} )}
</Droppable> </Droppable>
</DragDropContext> </DragDropContext>
</div> </div>
</div>
{isCardModalOpened && <CardModalContainer />} {isCardModalOpened && <CardModalContainer />}
</> </>
); );

View file

@ -48,5 +48,5 @@
} }
.wrapper { .wrapper {
height: 100%; margin: 0 20px;
} }

View file

@ -25,14 +25,14 @@ const Filter = React.memo(
}) => { }) => {
const [t] = useTranslation(); const [t] = useTranslation();
const handleUserRemoveClick = useCallback( const handleRemoveUserClick = useCallback(
(id) => { (id) => {
onUserRemove(id); onUserRemove(id);
}, },
[onUserRemove], [onUserRemove],
); );
const handleLabelRemoveClick = useCallback( const handleRemoveLabelClick = useCallback(
(id) => { (id) => {
onLabelRemove(id); onLabelRemove(id);
}, },
@ -62,7 +62,7 @@ const Filter = React.memo(
name={user.name} name={user.name}
avatarUrl={user.avatarUrl} avatarUrl={user.avatarUrl}
size="tiny" size="tiny"
onClick={() => handleUserRemoveClick(user.id)} onClick={() => handleRemoveUserClick(user.id)}
/> />
</span> </span>
))} ))}
@ -91,7 +91,7 @@ const Filter = React.memo(
name={label.name} name={label.name}
color={label.color} color={label.color}
size="small" size="small"
onClick={() => handleLabelRemoveClick(label.id)} onClick={() => handleRemoveLabelClick(label.id)}
/> />
</span> </span>
))} ))}

View file

@ -2,16 +2,13 @@ import pick from 'lodash/pick';
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classNames from 'classnames'; import classNames from 'classnames';
import { useTranslation, Trans } from 'react-i18next';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { DragDropContext, Draggable, Droppable } from 'react-beautiful-dnd'; import { DragDropContext, Draggable, Droppable } from 'react-beautiful-dnd';
import { Button, Icon } from 'semantic-ui-react'; import { Button, Icon } from 'semantic-ui-react';
import { closePopup } from '../../lib/popup'; import { closePopup } from '../../lib/popup';
import { DragScroller } from '../../lib/custom-ui';
import Paths from '../../constants/Paths'; import Paths from '../../constants/Paths';
import DroppableTypes from '../../constants/DroppableTypes'; import DroppableTypes from '../../constants/DroppableTypes';
import BoardWrapperContainer from '../../containers/BoardWrapperContainer';
import AddPopup from './AddPopup'; import AddPopup from './AddPopup';
import EditPopup from './EditPopup'; import EditPopup from './EditPopup';
@ -19,8 +16,6 @@ import styles from './Boards.module.css';
const Boards = React.memo( const Boards = React.memo(
({ items, currentId, isEditable, onCreate, onUpdate, onMove, onDelete }) => { ({ items, currentId, isEditable, onCreate, onUpdate, onMove, onDelete }) => {
const [t] = useTranslation();
const handleDragStart = useCallback(() => { const handleDragStart = useCallback(() => {
closePopup(); closePopup();
}, []); }, []);
@ -139,28 +134,6 @@ const Boards = React.memo(
) : ( ) : (
<div className={styles.tabs}>{renderItems(items)}</div> <div className={styles.tabs}>{renderItems(items)}</div>
)} )}
<DragScroller className={styles.board}>
{currentId ? (
<BoardWrapperContainer />
) : (
<div className={styles.message}>
<Icon
inverted
name="hand point up outline"
size="huge"
className={styles.messageIcon}
/>
<h1 className={styles.messageTitle}>
{t('common.openBoard', {
context: 'title',
})}
</h1>
<div className={styles.messageContent}>
<Trans i18nKey="common.createNewOneOrSelectExistingOne" />
</div>
</div>
)}
</DragScroller>
</div> </div>
); );
}, },

View file

@ -9,15 +9,6 @@
background: rgba(34, 36, 38, 0.3) !important; background: rgba(34, 36, 38, 0.3) !important;
} }
.board {
display: flex;
flex: 1 1 auto;
flex-direction: column;
margin: 0 -20px 8px;
overflow: auto;
padding: 0 20px;
}
.editButton { .editButton {
background: transparent !important; background: transparent !important;
color: #fff !important; color: #fff !important;
@ -45,32 +36,6 @@
overflow: hidden; overflow: hidden;
} }
.message {
align-content: space-between;
align-items: center;
color: #fff;
display: flex;
flex: 1 1 auto;
flex-direction: column;
justify-content: center;
}
.messageIcon {
margin-top: -84px;
}
.messageTitle {
font-size: 32px;
margin: 24px 0 8px;
}
.messageContent {
font-size: 18px;
line-height: 1.4;
margin: 4px 0 0;
text-align: center;
}
.tab { .tab {
border-radius: 3px 3px 0 0; border-radius: 3px 3px 0 0;
min-width: 100px; min-width: 100px;
@ -104,7 +69,6 @@
display: flex; display: flex;
height: 38px; height: 38px;
flex: 0 0 auto; flex: 0 0 auto;
margin-bottom: 16px;
white-space: nowrap; white-space: nowrap;
} }

View file

@ -51,7 +51,7 @@ const ActionsStep = React.memo(
const [t] = useTranslation(); const [t] = useTranslation();
const [step, openStep, handleBack] = useSteps(); const [step, openStep, handleBack] = useSteps();
const handleNameEditClick = useCallback(() => { const handleEditNameClick = useCallback(() => {
onNameEdit(); onNameEdit();
onClose(); onClose();
}, [onNameEdit, onClose]); }, [onNameEdit, onClose]);
@ -178,7 +178,7 @@ const ActionsStep = React.memo(
</Popup.Header> </Popup.Header>
<Popup.Content> <Popup.Content>
<Menu secondary vertical className={styles.menu}> <Menu secondary vertical className={styles.menu}>
<Menu.Item className={styles.menuItem} onClick={handleNameEditClick}> <Menu.Item className={styles.menuItem} onClick={handleEditNameClick}>
{t('action.editTitle', { {t('action.editTitle', {
context: 'title', context: 'title',
})} })}

View file

@ -69,8 +69,8 @@ const EditName = React.forwardRef(({ children, defaultValue, onUpdate }, ref) =>
); );
const [handleFieldBlur, handleControlMouseOver, handleControlMouseOut] = useClosableForm( const [handleFieldBlur, handleControlMouseOver, handleControlMouseOut] = useClosableForm(
isOpened,
close, close,
isOpened,
); );
const handleSubmit = useCallback(() => { const handleSubmit = useCallback(() => {

View file

@ -66,8 +66,8 @@ const EditComment = React.forwardRef(({ children, defaultData, onUpdate }, ref)
); );
const [handleFieldBlur, handleControlMouseOver, handleControlMouseOut] = useClosableForm( const [handleFieldBlur, handleControlMouseOver, handleControlMouseOut] = useClosableForm(
isOpened,
close, close,
isOpened,
); );
const handleSubmit = useCallback(() => { const handleSubmit = useCallback(() => {

View file

@ -117,7 +117,7 @@ const CardModal = React.memo(
[onUpdate], [onUpdate],
); );
const handleToggleSubscribeClick = useCallback(() => { const handleToggleSubscriptionClick = useCallback(() => {
onUpdate({ onUpdate({
isSubscribed: !isSubscribed, isSubscribed: !isSubscribed,
}); });
@ -359,7 +359,7 @@ const CardModal = React.memo(
<Button <Button
fluid fluid
className={styles.actionButton} className={styles.actionButton}
onClick={handleToggleSubscribeClick} onClick={handleToggleSubscriptionClick}
> >
<Icon name="paper plane outline" className={styles.actionIcon} /> <Icon name="paper plane outline" className={styles.actionIcon} />
{isSubscribed ? t('action.unsubscribe') : t('action.subscribe')} {isSubscribed ? t('action.unsubscribe') : t('action.subscribe')}
@ -378,7 +378,7 @@ const CardModal = React.memo(
<Button <Button
fluid fluid
className={styles.actionButton} className={styles.actionButton}
onClick={handleToggleSubscribeClick} onClick={handleToggleSubscriptionClick}
> >
<Icon name="share square outline" className={styles.actionIcon} /> <Icon name="share square outline" className={styles.actionIcon} />
{t('action.move')} {t('action.move')}

View file

@ -56,8 +56,8 @@ const EditDescription = React.forwardRef(({ children, defaultValue, onUpdate },
); );
const [handleFieldBlur, handleControlMouseOver, handleControlMouseOut] = useClosableForm( const [handleFieldBlur, handleControlMouseOver, handleControlMouseOut] = useClosableForm(
isOpened,
close, close,
isOpened,
); );
const handleSubmit = useCallback(() => { const handleSubmit = useCallback(() => {

View file

@ -18,7 +18,7 @@ const ActionsStep = React.memo(({ onNameEdit, onDelete, onClose }) => {
const [t] = useTranslation(); const [t] = useTranslation();
const [step, openStep, handleBack] = useSteps(); const [step, openStep, handleBack] = useSteps();
const handleNameEditClick = useCallback(() => { const handleEditNameClick = useCallback(() => {
onNameEdit(); onNameEdit();
onClose(); onClose();
}, [onNameEdit, onClose]); }, [onNameEdit, onClose]);
@ -50,7 +50,7 @@ const ActionsStep = React.memo(({ onNameEdit, onDelete, onClose }) => {
</Popup.Header> </Popup.Header>
<Popup.Content> <Popup.Content>
<Menu secondary vertical className={styles.menu}> <Menu secondary vertical className={styles.menu}>
<Menu.Item className={styles.menuItem} onClick={handleNameEditClick}> <Menu.Item className={styles.menuItem} onClick={handleEditNameClick}>
{t('action.editDescription', { {t('action.editDescription', {
context: 'title', context: 'title',
})} })}

View file

@ -71,8 +71,8 @@ const Add = React.forwardRef(({ children, onCreate }, ref) => {
); );
const [handleFieldBlur, handleControlMouseOver, handleControlMouseOut] = useClosableForm( const [handleFieldBlur, handleControlMouseOver, handleControlMouseOut] = useClosableForm(
isOpened,
close, close,
isOpened,
); );
const handleSubmit = useCallback(() => { const handleSubmit = useCallback(() => {

View file

@ -61,8 +61,8 @@ const EditName = React.forwardRef(({ children, defaultValue, onUpdate }, ref) =>
); );
const [handleFieldBlur, handleControlMouseOver, handleControlMouseOut] = useClosableForm( const [handleFieldBlur, handleControlMouseOver, handleControlMouseOut] = useClosableForm(
isOpened,
close, close,
isOpened,
); );
const handleSubmit = useCallback(() => { const handleSubmit = useCallback(() => {

View file

@ -0,0 +1,24 @@
import React from 'react';
import PropTypes from 'prop-types';
import HeaderContainer from '../../containers/HeaderContainer';
import ProjectContainer from '../../containers/ProjectContainer';
import styles from './FixedWrapper.module.css';
const FixedWrapper = ({ projectId }) => (
<div className={styles.wrapper}>
<HeaderContainer />
{projectId && <ProjectContainer />}
</div>
);
FixedWrapper.propTypes = {
projectId: PropTypes.string,
};
FixedWrapper.defaultProps = {
projectId: undefined,
};
export default FixedWrapper;

View file

@ -0,0 +1,5 @@
.wrapper {
position: fixed;
width: 100%;
z-index: 1;
}

View file

@ -0,0 +1,3 @@
import FixedWrapper from './FixedWrapper';
export default FixedWrapper;

View file

@ -111,4 +111,6 @@ NotificationsStep.propTypes = {
onClose: PropTypes.func.isRequired, onClose: PropTypes.func.isRequired,
}; };
export default withPopup(NotificationsStep); export default withPopup(NotificationsStep, {
position: 'bottom right',
});

View file

@ -18,12 +18,12 @@ const ActionsStep = React.memo(({ onNameEdit, onCardAdd, onDelete, onClose }) =>
const [t] = useTranslation(); const [t] = useTranslation();
const [step, openStep, handleBack] = useSteps(); const [step, openStep, handleBack] = useSteps();
const handleNameEditClick = useCallback(() => { const handleEditNameClick = useCallback(() => {
onNameEdit(); onNameEdit();
onClose(); onClose();
}, [onNameEdit, onClose]); }, [onNameEdit, onClose]);
const handleCardAddClick = useCallback(() => { const handleAddCardClick = useCallback(() => {
onCardAdd(); onCardAdd();
onClose(); onClose();
}, [onCardAdd, onClose]); }, [onCardAdd, onClose]);
@ -55,12 +55,12 @@ const ActionsStep = React.memo(({ onNameEdit, onCardAdd, onDelete, onClose }) =>
</Popup.Header> </Popup.Header>
<Popup.Content> <Popup.Content>
<Menu secondary vertical className={styles.menu}> <Menu secondary vertical className={styles.menu}>
<Menu.Item className={styles.menuItem} onClick={handleNameEditClick}> <Menu.Item className={styles.menuItem} onClick={handleEditNameClick}>
{t('action.editTitle', { {t('action.editTitle', {
context: 'title', context: 'title',
})} })}
</Menu.Item> </Menu.Item>
<Menu.Item className={styles.menuItem} onClick={handleCardAddClick}> <Menu.Item className={styles.menuItem} onClick={handleAddCardClick}>
{t('action.addCard', { {t('action.addCard', {
context: 'title', context: 'title',
})} })}

View file

@ -1,5 +1,6 @@
import React, { useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react'; import React, { useCallback, useEffect, useRef } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classNames from 'classnames';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import TextareaAutosize from 'react-textarea-autosize'; import TextareaAutosize from 'react-textarea-autosize';
import { Button, Form, TextArea } from 'semantic-ui-react'; import { Button, Form, TextArea } from 'semantic-ui-react';
@ -13,22 +14,13 @@ const DEFAULT_DATA = {
name: '', name: '',
}; };
const AddCard = React.forwardRef(({ children, onCreate }, ref) => { const AddCard = React.memo(({ isOpened, onCreate, onClose }) => {
const [t] = useTranslation(); const [t] = useTranslation();
const [isOpened, setIsOpened] = useState(false);
const [data, handleFieldChange, setData] = useForm(DEFAULT_DATA); const [data, handleFieldChange, setData] = useForm(DEFAULT_DATA);
const [selectNameFieldState, selectNameField] = useToggle(); const [selectNameFieldState, selectNameField] = useToggle();
const nameField = useRef(null); const nameField = useRef(null);
const open = useCallback(() => {
setIsOpened(true);
}, []);
const close = useCallback(() => {
setIsOpened(false);
}, []);
const submit = useCallback(() => { const submit = useCallback(() => {
const cleanData = { const cleanData = {
...data, ...data,
@ -46,19 +38,6 @@ const AddCard = React.forwardRef(({ children, onCreate }, ref) => {
selectNameField(); selectNameField();
}, [onCreate, data, setData, selectNameField]); }, [onCreate, data, setData, selectNameField]);
useImperativeHandle(
ref,
() => ({
open,
close,
}),
[open, close],
);
const handleChildrenClick = useCallback(() => {
open();
}, [open]);
const handleFieldKeyDown = useCallback( const handleFieldKeyDown = useCallback(
(event) => { (event) => {
switch (event.key) { switch (event.key) {
@ -69,19 +48,16 @@ const AddCard = React.forwardRef(({ children, onCreate }, ref) => {
break; break;
case 'Escape': case 'Escape':
close(); onClose();
break; break;
default: default:
} }
}, },
[close, submit], [onClose, submit],
); );
const [handleFieldBlur, handleControlMouseOver, handleControlMouseOut] = useClosableForm( const [handleFieldBlur, handleControlMouseOver, handleControlMouseOut] = useClosableForm(onClose);
isOpened,
close,
);
const handleSubmit = useCallback(() => { const handleSubmit = useCallback(() => {
submit(); submit();
@ -97,14 +73,11 @@ const AddCard = React.forwardRef(({ children, onCreate }, ref) => {
nameField.current.ref.current.select(); nameField.current.ref.current.select();
}, [selectNameFieldState]); }, [selectNameFieldState]);
if (!isOpened) {
return React.cloneElement(children, {
onClick: handleChildrenClick,
});
}
return ( return (
<Form className={styles.wrapper} onSubmit={handleSubmit}> <Form
className={classNames(styles.wrapper, !isOpened && styles.wrapperClosed)}
onSubmit={handleSubmit}
>
<div className={styles.fieldWrapper}> <div className={styles.fieldWrapper}>
<TextArea <TextArea
ref={nameField} ref={nameField}
@ -135,8 +108,9 @@ const AddCard = React.forwardRef(({ children, onCreate }, ref) => {
}); });
AddCard.propTypes = { AddCard.propTypes = {
children: PropTypes.element.isRequired, isOpened: PropTypes.bool.isRequired,
onCreate: PropTypes.func.isRequired, onCreate: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired,
}; };
export default React.memo(AddCard); export default AddCard;

View file

@ -24,3 +24,7 @@
.wrapper { .wrapper {
padding-bottom: 8px !important; padding-bottom: 8px !important;
} }
.wrapperClosed {
display: none;
}

View file

@ -1,4 +1,4 @@
import React, { useCallback, useRef } from 'react'; import React, { useCallback, useEffect, useRef, useState } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classNames from 'classnames'; import classNames from 'classnames';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@ -17,9 +17,10 @@ import styles from './List.module.css';
const List = React.memo( const List = React.memo(
({ id, index, name, isPersisted, cardIds, onUpdate, onDelete, onCardCreate }) => { ({ id, index, name, isPersisted, cardIds, onUpdate, onDelete, onCardCreate }) => {
const [t] = useTranslation(); const [t] = useTranslation();
const [isAddCardOpened, setIsAddCardOpened] = useState(false);
const addCard = useRef(null);
const editName = useRef(null); const editName = useRef(null);
const listWrapper = useRef(null);
const handleHeaderClick = useCallback(() => { const handleHeaderClick = useCallback(() => {
if (isPersisted) { if (isPersisted) {
@ -36,14 +37,28 @@ const List = React.memo(
[onUpdate], [onUpdate],
); );
const handleAddCardClick = useCallback(() => {
setIsAddCardOpened(true);
}, []);
const handleAddCardClose = useCallback(() => {
setIsAddCardOpened(false);
}, []);
const handleNameEdit = useCallback(() => { const handleNameEdit = useCallback(() => {
editName.current.open(); editName.current.open();
}, []); }, []);
const handleCardAdd = useCallback(() => { const handleCardAdd = useCallback(() => {
addCard.current.open(); setIsAddCardOpened(true);
}, []); }, []);
useEffect(() => {
if (isAddCardOpened) {
listWrapper.current.scrollTop = listWrapper.current.scrollHeight;
}
}, [cardIds, isAddCardOpened]);
const cardsNode = ( const cardsNode = (
<Droppable <Droppable
droppableId={`list:${id}`} droppableId={`list:${id}`}
@ -58,15 +73,12 @@ const List = React.memo(
<CardContainer key={cardId} id={cardId} index={cardIndex} /> <CardContainer key={cardId} id={cardId} index={cardIndex} />
))} ))}
{placeholder} {placeholder}
<AddCard
isOpened={isAddCardOpened}
onCreate={onCardCreate}
onClose={handleAddCardClose}
/>
</div> </div>
<AddCard ref={addCard} onCreate={onCardCreate}>
<button type="button" disabled={!isPersisted} className={styles.addCardButton}>
<PlusMathIcon className={styles.addCardButtonIcon} />
<span className={styles.addCardButtonText}>
{cardIds.length > 0 ? t('action.addAnotherCard') : t('action.addCard')}
</span>
</button>
</AddCard>
</div> </div>
)} )}
</Droppable> </Droppable>
@ -99,8 +111,26 @@ const List = React.memo(
</ActionsPopup> </ActionsPopup>
)} )}
</div> </div>
<div
ref={listWrapper}
className={classNames(styles.listWrapper, isAddCardOpened && styles.listWrapperFull)}
>
<div className={styles.list}>{cardsNode}</div> <div className={styles.list}>{cardsNode}</div>
</div> </div>
{!isAddCardOpened && (
<button
type="button"
disabled={!isPersisted}
className={styles.addCardButton}
onClick={handleAddCardClick}
>
<PlusMathIcon className={styles.addCardButtonIcon} />
<span className={styles.addCardButtonText}>
{cardIds.length > 0 ? t('action.addAnotherCard') : t('action.addCard')}
</span>
</button>
)}
</div>
)} )}
</Draggable> </Draggable>
); );

View file

@ -1,6 +1,7 @@
.addCardButton { .addCardButton {
background: none !important; background: #dfe3e6 !important;
border: none !important; border: none !important;
border-radius: 0 0 3px 3px !important;
color: #6b808c !important; color: #6b808c !important;
cursor: pointer; cursor: pointer;
display: block !important; display: block !important;
@ -8,18 +9,14 @@
flex: 0 0 auto; flex: 0 0 auto;
font-weight: normal !important; font-weight: normal !important;
height: 36px !important; height: 36px !important;
margin: 0 -8px !important; outline: none !important;
padding: 8px !important; padding: 8px !important;
text-align: left !important; text-align: left !important;
width: calc(100% + 16px); width: 100%;
}
.addCardButton:active {
outline: none !important;
} }
.addCardButton:hover { .addCardButton:hover {
background-color: #092d4221 !important; background-color: #c3cbd0 !important;
color: #17394d !important; color: #17394d !important;
fill: #17394d !important; fill: #17394d !important;
} }
@ -40,6 +37,7 @@
.cards { .cards {
flex: 1 1 auto; flex: 1 1 auto;
min-height: 1px;
} }
.header { .header {
@ -96,18 +94,48 @@
.list { .list {
background: #dfe3e6 !important; background: #dfe3e6 !important;
border-radius: 0 0 3px 3px !important;
box-sizing: border-box !important; box-sizing: border-box !important;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding: 0 8px !important; padding: 0 8px !important;
position: relative !important; position: relative !important;
white-space: normal !important; white-space: normal !important;
width: 272px;
}
.listWrapper {
background: #dfe3e6;
max-height: calc(100vh - 300px);
overflow-x: hidden;
overflow-y: auto;
width: 290px;
}
.listWrapper:hover {
width: 272px;
}
.listWrapper::-webkit-scrollbar {
width: 5px;
}
.listWrapper::-webkit-scrollbar-track {
background: transparent;
}
.listWrapper::-webkit-scrollbar-thumb {
border-radius: 3px;
}
.listWrapperFull {
max-height: calc(100vh - 264px);
} }
.wrapper { .wrapper {
border-radius: 3px;
flex: 0 0 auto; flex: 0 0 auto;
margin: 0 4px !important; margin: 0 4px !important;
overflow: hidden;
vertical-align: top !important; vertical-align: top !important;
width: 272px !important; width: 272px !important;
} }

View file

@ -1,49 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import ProjectContainer from '../containers/ProjectContainer';
const ProjectWrapper = React.memo(({ isProjectNotFound, isBoardNotFound, isCardNotFound }) => {
const [t] = useTranslation();
if (isCardNotFound) {
return (
<h1>
{t('common.cardNotFound', {
context: 'title',
})}
</h1>
);
}
if (isBoardNotFound) {
return (
<h1>
{t('common.boardNotFound', {
context: 'title',
})}
</h1>
);
}
if (isProjectNotFound) {
return (
<h1>
{t('common.projectNotFound', {
context: 'title',
})}
</h1>
);
}
return <ProjectContainer />;
});
ProjectWrapper.propTypes = {
isProjectNotFound: PropTypes.bool.isRequired,
isBoardNotFound: PropTypes.bool.isRequired,
isCardNotFound: PropTypes.bool.isRequired,
};
export default ProjectWrapper;

View file

@ -1,4 +1,3 @@
import isUndefined from 'lodash/isUndefined';
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classNames from 'classnames'; import classNames from 'classnames';
@ -7,15 +6,13 @@ import { Link } from 'react-router-dom';
import { Container, Grid } from 'semantic-ui-react'; import { Container, Grid } from 'semantic-ui-react';
import Paths from '../../constants/Paths'; import Paths from '../../constants/Paths';
import ProjectWrapperContainer from '../../containers/ProjectWrapperContainer';
import { ReactComponent as PlusIcon } from '../../assets/images/plus-icon.svg'; import { ReactComponent as PlusIcon } from '../../assets/images/plus-icon.svg';
import styles from './Projects.module.css'; import styles from './Projects.module.css';
const Projects = React.memo(({ items, currentId, isEditable, onAdd }) => { const Projects = React.memo(({ items, isEditable, onAdd }) => {
const [t] = useTranslation(); const [t] = useTranslation();
if (isUndefined(currentId)) {
return ( return (
<Container className={styles.cardsWrapper}> <Container className={styles.cardsWrapper}>
<Grid className={styles.gridFix}> <Grid className={styles.gridFix}>
@ -53,26 +50,12 @@ const Projects = React.memo(({ items, currentId, isEditable, onAdd }) => {
</Grid> </Grid>
</Container> </Container>
); );
}
return (
<div className={styles.wrapper}>
<div className={styles.project}>
<ProjectWrapperContainer />
</div>
</div>
);
}); });
Projects.propTypes = { Projects.propTypes = {
items: PropTypes.array.isRequired, // eslint-disable-line react/forbid-prop-types items: PropTypes.array.isRequired, // eslint-disable-line react/forbid-prop-types
currentId: PropTypes.string,
isEditable: PropTypes.bool.isRequired, isEditable: PropTypes.bool.isRequired,
onAdd: PropTypes.func.isRequired, onAdd: PropTypes.func.isRequired,
}; };
Projects.defaultProps = {
currentId: undefined,
};
export default Projects; export default Projects;

View file

@ -0,0 +1,106 @@
import React, { useEffect } from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { useTranslation, Trans } from 'react-i18next';
import { Icon } from 'semantic-ui-react';
import ProjectsContainer from '../../containers/ProjectsContainer';
import BoardWrapperContainer from '../../containers/BoardWrapperContainer';
import styles from './StaticWrapper.module.css';
const StaticWrapper = ({ cardId, boardId, projectId }) => {
const [t] = useTranslation();
useEffect(() => {
document.body.style.overflowX = 'auto'; // TODO: only for board
}, []);
if (projectId === undefined) {
return (
<div className={styles.root}>
<ProjectsContainer />
</div>
);
}
if (cardId === null) {
return (
<div className={classNames(styles.root, styles.flex)}>
<div className={styles.message}>
<h1>
{t('common.cardNotFound', {
context: 'title',
})}
</h1>
</div>
</div>
);
}
if (boardId === null) {
return (
<div className={classNames(styles.root, styles.flex)}>
<div className={styles.message}>
<h1>
{t('common.boardNotFound', {
context: 'title',
})}
</h1>
</div>
</div>
);
}
if (projectId === null) {
return (
<div className={classNames(styles.root, styles.flex)}>
<div className={styles.message}>
<h1>
{t('common.projectNotFound', {
context: 'title',
})}
</h1>
</div>
</div>
);
}
if (boardId === undefined) {
return (
<div className={classNames(styles.board, styles.flex)}>
<div className={styles.message}>
<Icon inverted name="hand point up outline" size="huge" className={styles.messageIcon} />
<h1 className={styles.messageTitle}>
{t('common.openBoard', {
context: 'title',
})}
</h1>
<div className={styles.messageContent}>
<Trans i18nKey="common.createNewOneOrSelectExistingOne" />
</div>
</div>
</div>
);
}
return (
<div className={classNames(styles.board, styles.flex)}>
<BoardWrapperContainer />
</div>
);
};
StaticWrapper.propTypes = {
cardId: PropTypes.string,
boardId: PropTypes.string,
projectId: PropTypes.string,
};
StaticWrapper.defaultProps = {
cardId: undefined,
boardId: undefined,
projectId: undefined,
};
export default StaticWrapper;

View file

@ -0,0 +1,38 @@
.board {
margin-top: 168px;
}
.flex {
display: flex;
height: 100%;
}
.message {
align-content: space-between;
align-items: center;
color: #fff;
display: flex;
flex: 1 1 auto;
flex-direction: column;
justify-content: center;
}
.messageIcon {
margin-top: -84px;
}
.messageTitle {
font-size: 32px;
margin: 24px 0 8px;
}
.messageContent {
font-size: 18px;
line-height: 1.4;
margin: 4px 0 0;
text-align: center;
}
.root {
margin-top: 50px;
}

View file

@ -0,0 +1,3 @@
import StaticWrapper from './StaticWrapper';
export default StaticWrapper;

View file

@ -46,4 +46,6 @@ UserStep.propTypes = {
onClose: PropTypes.func.isRequired, onClose: PropTypes.func.isRequired,
}; };
export default withPopup(UserStep); export default withPopup(UserStep, {
position: 'bottom right',
});

View file

@ -1,7 +1,5 @@
const USERS = 'USERS'; const USERS = 'USERS';
const USER_SETTINGS = 'USER_SETTINGS'; const USER_SETTINGS = 'USER_SETTINGS';
const ADD_PROJECT = 'ADD_PROJECT'; const ADD_PROJECT = 'ADD_PROJECT';
export default { export default {

View file

@ -1,16 +1,13 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { currentModalSelector } from '../selectors'; import { currentModalSelector } from '../selectors';
import ModalTypes from '../constants/ModalTypes';
import App from '../components/App'; import App from '../components/App';
const mapStateToProps = (state) => { const mapStateToProps = (state) => {
const currentModal = currentModalSelector(state); const currentModal = currentModalSelector(state);
return { return {
isUsersModalOpened: currentModal === ModalTypes.USERS, currentModal,
isUserSettingsModalOpened: currentModal === ModalTypes.USER_SETTINGS,
isAddProjectModalOpened: currentModal === ModalTypes.ADD_PROJECT,
}; };
}; };

View file

@ -0,0 +1,14 @@
import { connect } from 'react-redux';
import { pathSelector } from '../selectors';
import FixedWrapper from '../components/FixedWrapper';
const mapStateToProps = (state) => {
const { projectId } = pathSelector(state);
return {
projectId,
};
};
export default connect(mapStateToProps)(FixedWrapper);

View file

@ -1,16 +0,0 @@
import { connect } from 'react-redux';
import { pathSelector } from '../selectors';
import ProjectWrapper from '../components/ProjectWrapper';
const mapStateToProps = (state) => {
const { cardId, boardId, projectId } = pathSelector(state);
return {
isProjectNotFound: projectId === null,
isBoardNotFound: boardId === null,
isCardNotFound: cardId === null,
};
};
export default connect(mapStateToProps)(ProjectWrapper);

View file

@ -1,18 +1,16 @@
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { currentUserSelector, pathSelector, projectsForCurrentUserSelector } from '../selectors'; import { currentUserSelector, projectsForCurrentUserSelector } from '../selectors';
import { openAddProjectModal } from '../actions/entry'; import { openAddProjectModal } from '../actions/entry';
import Projects from '../components/Projects'; import Projects from '../components/Projects';
const mapStateToProps = (state) => { const mapStateToProps = (state) => {
const { projectId } = pathSelector(state);
const { isAdmin } = currentUserSelector(state); const { isAdmin } = currentUserSelector(state);
const projects = projectsForCurrentUserSelector(state); const projects = projectsForCurrentUserSelector(state);
return { return {
items: projects, items: projects,
currentId: projectId,
isEditable: isAdmin, isEditable: isAdmin,
}; };
}; };

View file

@ -0,0 +1,16 @@
import { connect } from 'react-redux';
import { pathSelector } from '../selectors';
import StaticWrapper from '../components/StaticWrapper';
const mapStateToProps = (state) => {
const { cardId, boardId, projectId } = pathSelector(state);
return {
cardId,
boardId,
projectId,
};
};
export default connect(mapStateToProps)(StaticWrapper);

View file

@ -1,6 +1,6 @@
import { useCallback, useEffect, useRef } from 'react'; import { useCallback, useEffect, useRef } from 'react';
export default (isOpened, close) => { export default (close, isOpened = true) => {
const isClosable = useRef(null); const isClosable = useRef(null);
const handleFieldBlur = useCallback(() => { const handleFieldBlur = useCallback(() => {

View file

@ -5,7 +5,7 @@ body {
#root { #root {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 100vh; height: 100%;
} }
.react-datepicker { .react-datepicker {
@ -100,3 +100,30 @@ body {
top: 0; top: 0;
margin: 9px 0; margin: 9px 0;
} }
::-webkit-scrollbar {
height: 10px;
width: 10px;
-webkit-appearance: none;
}
::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.1);
border-radius: 0px;
}
::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.25);
border-radius: 5px;
cursor: pointer;
transition: color 0.2s ease;
-webkit-transition: color 0.2s ease;
}
::-webkit-scrollbar-thumb:window-inactive {
background: rgba(0, 0, 0, 0.15);
}
::-webkit-scrollbar-thumb:hover {
background: rgba(128, 135, 139, 0.8);
}

View file

@ -1,62 +0,0 @@
import React, { useCallback, useEffect, useRef } from 'react';
import PropTypes from 'prop-types';
const DragScroller = React.memo(({ children, ...props }) => {
const wrapper = useRef(null);
const prevPosition = useRef(null);
const handleMouseDown = useCallback(
(event) => {
if (event.target !== wrapper.current && !event.target.dataset.dragScroller) {
return;
}
prevPosition.current = event.clientX;
},
[wrapper],
);
const handleWindowMouseMove = useCallback(
(event) => {
if (!prevPosition.current) {
return;
}
event.preventDefault();
const position = event.clientX;
wrapper.current.scrollLeft -= -prevPosition.current + position;
prevPosition.current = position;
},
[wrapper, prevPosition],
);
const handleWindowMouseUp = useCallback(() => {
prevPosition.current = null;
}, [prevPosition]);
useEffect(() => {
window.addEventListener('mouseup', handleWindowMouseUp);
window.addEventListener('mousemove', handleWindowMouseMove);
return () => {
window.removeEventListener('mouseup', handleWindowMouseUp);
window.removeEventListener('mousemove', handleWindowMouseMove);
};
}, [handleWindowMouseUp, handleWindowMouseMove]);
return (
/* eslint-disable jsx-a11y/no-static-element-interactions, react/jsx-props-no-spreading */
<div {...props} ref={wrapper} onMouseDown={handleMouseDown}>
{/* eslint-enable jsx-a11y/no-static-element-interactions, react/jsx-props-no-spreading */}
{children}
</div>
);
});
DragScroller.propTypes = {
children: PropTypes.node.isRequired,
};
export default DragScroller;

View file

@ -1,3 +0,0 @@
import DragScroller from './DragScroller';
export default DragScroller;

View file

@ -707,7 +707,7 @@ input::selection {
/* Force Simple Scrollbars */ /* Force Simple Scrollbars */
body ::-webkit-scrollbar { /* body ::-webkit-scrollbar {
-webkit-appearance: none; -webkit-appearance: none;
width: 10px; width: 10px;
height: 10px; height: 10px;
@ -732,11 +732,11 @@ body ::-webkit-scrollbar-thumb:window-inactive {
body ::-webkit-scrollbar-thumb:hover { body ::-webkit-scrollbar-thumb:hover {
background: rgba(128, 135, 139, 0.8); background: rgba(128, 135, 139, 0.8);
} } */
/* Inverted UI */ /* Inverted UI */
body .ui.inverted::-webkit-scrollbar-track { /* body .ui.inverted::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.1); background: rgba(255, 255, 255, 0.1);
} }
@ -750,7 +750,7 @@ body .ui.inverted::-webkit-scrollbar-thumb:window-inactive {
body .ui.inverted::-webkit-scrollbar-thumb:hover { body .ui.inverted::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.35); background: rgba(255, 255, 255, 0.35);
} } */
/******************************* /*******************************
Global Overrides Global Overrides

View file

@ -2,6 +2,5 @@ import Input from './components/Input';
import Popup from './components/Popup'; import Popup from './components/Popup';
import Markdown from './components/Markdown'; import Markdown from './components/Markdown';
import FilePicker from './components/FilePicker'; import FilePicker from './components/FilePicker';
import DragScroller from './components/DragScroller';
export { Input, Popup, Markdown, FilePicker, DragScroller }; export { Input, Popup, Markdown, FilePicker };

View file

@ -4,7 +4,7 @@ import { Button, Popup as SemanticUIPopup } from 'semantic-ui-react';
import styles from './Popup.module.css'; import styles from './Popup.module.css';
export default (WrappedComponent) => { export default (WrappedComponent, defaultProps) => {
const Popup = React.memo(({ children, ...props }) => { const Popup = React.memo(({ children, ...props }) => {
const [isOpened, setIsOpened] = useState(false); const [isOpened, setIsOpened] = useState(false);
@ -49,11 +49,17 @@ export default (WrappedComponent) => {
on="click" on="click"
open={isOpened} open={isOpened}
position="bottom left" position="bottom left"
popperModifiers={{
preventOverflow: {
boundariesElement: 'window',
},
}}
className={styles.wrapper} className={styles.wrapper}
onOpen={handleOpen} onOpen={handleOpen}
onClose={handleClose} onClose={handleClose}
onMouseDown={handleMouseDown} onMouseDown={handleMouseDown}
onClick={handleClick} onClick={handleClick}
{...defaultProps} // eslint-disable-line react/jsx-props-no-spreading
> >
<Button icon="close" onClick={handleClose} className={styles.closeButton} /> <Button icon="close" onClick={handleClose} className={styles.closeButton} />
{/* eslint-disable-next-line react/jsx-props-no-spreading */} {/* eslint-disable-next-line react/jsx-props-no-spreading */}

View file

@ -53,7 +53,7 @@ module.exports = {
} }
if (inputs.toBoard.id === inputs.board.id) { if (inputs.toBoard.id === inputs.board.id) {
delete inputs.toList; // eslint-disable-line no-param-reassign delete inputs.toBoard; // eslint-disable-line no-param-reassign
} else { } else {
values.boardId = inputs.toBoard.id; values.boardId = inputs.toBoard.id;
} }