1
0
Fork 0
mirror of https://github.com/plankanban/planka.git synced 2025-07-19 13:19:44 +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 c5b44598f9
commit 746e2fe790
44 changed files with 549 additions and 438 deletions

View file

@ -1,26 +1,29 @@
import React from 'react';
import PropTypes from 'prop-types';
import HeaderContainer from '../containers/HeaderContainer';
import ProjectsContainer from '../containers/ProjectsContainer';
import ModalTypes from '../constants/ModalTypes';
import FixedWrapperContainer from '../containers/FixedWrapperContainer';
import StaticWrapperContainer from '../containers/StaticWrapperContainer';
import UsersModalContainer from '../containers/UsersModalContainer';
import UserSettingsModalContainer from '../containers/UserSettingsModalContainer';
import AddProjectModalContainer from '../containers/AddProjectModalContainer';
const App = ({ isUsersModalOpened, isUserSettingsModalOpened, isAddProjectModalOpened }) => (
const App = ({ currentModal }) => (
<>
<HeaderContainer />
<ProjectsContainer />
{isUsersModalOpened && <UsersModalContainer />}
{isUserSettingsModalOpened && <UserSettingsModalContainer />}
{isAddProjectModalOpened && <AddProjectModalContainer />}
<FixedWrapperContainer />
<StaticWrapperContainer />
{currentModal === ModalTypes.USERS && <UsersModalContainer />}
{currentModal === ModalTypes.USER_SETTINGS && <UserSettingsModalContainer />}
{currentModal === ModalTypes.ADD_PROJECT && <AddProjectModalContainer />}
</>
);
App.propTypes = {
isUsersModalOpened: PropTypes.bool.isRequired,
isUserSettingsModalOpened: PropTypes.bool.isRequired,
isAddProjectModalOpened: PropTypes.bool.isRequired,
currentModal: PropTypes.oneOf(Object.values(ModalTypes)),
};
App.defaultProps = {
currentModal: undefined,
};
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 { useTranslation } from 'react-i18next';
import { Button, Form, Input } from 'semantic-ui-react';
@ -12,48 +12,23 @@ const DEFAULT_DATA = {
name: '',
};
const AddList = React.forwardRef(({ children, onCreate }, ref) => {
const AddList = React.memo(({ onCreate, onClose }) => {
const [t] = useTranslation();
const [isOpened, setIsOpened] = useState(false);
const [data, handleFieldChange, setData] = useForm(DEFAULT_DATA);
const [selectNameFieldState, selectNameField] = useToggle();
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(
(event) => {
if (event.key === 'Escape') {
close();
onClose();
}
},
[close],
[onClose],
);
const [handleFieldBlur, handleControlMouseOver, handleControlMouseOut] = useClosableForm(
isOpened,
close,
);
const [handleFieldBlur, handleControlMouseOver, handleControlMouseOut] = useClosableForm(onClose);
const handleSubmit = useCallback(() => {
const cleanData = {
@ -73,21 +48,13 @@ const AddList = React.forwardRef(({ children, onCreate }, ref) => {
}, [onCreate, data, setData, selectNameField]);
useEffect(() => {
if (isOpened) {
nameField.current.select();
}
}, [isOpened]);
}, []);
useDidUpdate(() => {
nameField.current.select();
}, [selectNameFieldState]);
if (!isOpened) {
return React.cloneElement(children, {
onClick: handleChildrenClick,
});
}
return (
<Form className={styles.wrapper} onSubmit={handleSubmit}>
<Input
@ -115,8 +82,8 @@ const AddList = React.forwardRef(({ children, onCreate }, ref) => {
});
AddList.propTypes = {
children: PropTypes.element.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 { useTranslation } from 'react-i18next';
import { DragDropContext, Droppable } from 'react-beautiful-dnd';
@ -35,6 +35,18 @@ const Board = React.memo(
onLabelDelete,
}) => {
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(() => {
closePopup();
@ -66,8 +78,58 @@ const Board = React.memo(
[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 (
<>
{/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */}
<div ref={wrapper} className={styles.wrapper} onMouseDown={handleMouseDown}>
<Filter
users={filterUsers}
labels={filterLabels}
@ -81,31 +143,46 @@ const Board = React.memo(
onLabelUpdate={onLabelUpdate}
onLabelDelete={onLabelDelete}
/>
<div className={styles.wrapper}>
<div>
<DragDropContext onDragStart={handleDragStart} onDragEnd={handleDragEnd}>
<Droppable droppableId="board" type={DroppableTypes.LIST} direction="horizontal">
{({ innerRef, droppableProps, placeholder }) => (
// eslint-disable-next-line react/jsx-props-no-spreading
<div {...droppableProps} data-drag-scroller ref={innerRef} className={styles.lists}>
<div
{...droppableProps} // eslint-disable-line react/jsx-props-no-spreading
data-drag-scroller
ref={innerRef}
className={styles.lists}
>
{listIds.map((listId, index) => (
<ListContainer key={listId} id={listId} index={index} />
))}
{placeholder}
<div data-drag-scroller className={styles.list}>
<AddList onCreate={onListCreate}>
<button type="button" className={styles.addListButton}>
{isAddListOpened ? (
<AddList
isOpened={isAddListOpened}
onCreate={onListCreate}
onClose={handleAddListClose}
/>
) : (
<button
type="button"
className={styles.addListButton}
onClick={handleAddListClick}
>
<PlusMathIcon className={styles.addListButtonIcon} />
<span className={styles.addListButtonText}>
{listIds.length > 0 ? t('action.addAnotherList') : t('action.addList')}
</span>
</button>
</AddList>
)}
</div>
</div>
)}
</Droppable>
</DragDropContext>
</div>
</div>
{isCardModalOpened && <CardModalContainer />}
</>
);

View file

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

View file

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

View file

@ -2,16 +2,13 @@ import pick from 'lodash/pick';
import React, { useCallback } from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { useTranslation, Trans } from 'react-i18next';
import { Link } from 'react-router-dom';
import { DragDropContext, Draggable, Droppable } from 'react-beautiful-dnd';
import { Button, Icon } from 'semantic-ui-react';
import { closePopup } from '../../lib/popup';
import { DragScroller } from '../../lib/custom-ui';
import Paths from '../../constants/Paths';
import DroppableTypes from '../../constants/DroppableTypes';
import BoardWrapperContainer from '../../containers/BoardWrapperContainer';
import AddPopup from './AddPopup';
import EditPopup from './EditPopup';
@ -19,8 +16,6 @@ import styles from './Boards.module.css';
const Boards = React.memo(
({ items, currentId, isEditable, onCreate, onUpdate, onMove, onDelete }) => {
const [t] = useTranslation();
const handleDragStart = useCallback(() => {
closePopup();
}, []);
@ -139,28 +134,6 @@ const Boards = React.memo(
) : (
<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>
);
},

View file

@ -9,15 +9,6 @@
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 {
background: transparent !important;
color: #fff !important;
@ -45,32 +36,6 @@
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 {
border-radius: 3px 3px 0 0;
min-width: 100px;
@ -104,7 +69,6 @@
display: flex;
height: 38px;
flex: 0 0 auto;
margin-bottom: 16px;
white-space: nowrap;
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -61,8 +61,8 @@ const EditName = React.forwardRef(({ children, defaultValue, onUpdate }, ref) =>
);
const [handleFieldBlur, handleControlMouseOver, handleControlMouseOut] = useClosableForm(
isOpened,
close,
isOpened,
);
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,
};
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 [step, openStep, handleBack] = useSteps();
const handleNameEditClick = useCallback(() => {
const handleEditNameClick = useCallback(() => {
onNameEdit();
onClose();
}, [onNameEdit, onClose]);
const handleCardAddClick = useCallback(() => {
const handleAddCardClick = useCallback(() => {
onCardAdd();
onClose();
}, [onCardAdd, onClose]);
@ -55,12 +55,12 @@ const ActionsStep = React.memo(({ onNameEdit, onCardAdd, onDelete, onClose }) =>
</Popup.Header>
<Popup.Content>
<Menu secondary vertical className={styles.menu}>
<Menu.Item className={styles.menuItem} onClick={handleNameEditClick}>
<Menu.Item className={styles.menuItem} onClick={handleEditNameClick}>
{t('action.editTitle', {
context: 'title',
})}
</Menu.Item>
<Menu.Item className={styles.menuItem} onClick={handleCardAddClick}>
<Menu.Item className={styles.menuItem} onClick={handleAddCardClick}>
{t('action.addCard', {
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 classNames from 'classnames';
import { useTranslation } from 'react-i18next';
import TextareaAutosize from 'react-textarea-autosize';
import { Button, Form, TextArea } from 'semantic-ui-react';
@ -13,22 +14,13 @@ const DEFAULT_DATA = {
name: '',
};
const AddCard = React.forwardRef(({ children, onCreate }, ref) => {
const AddCard = React.memo(({ isOpened, onCreate, onClose }) => {
const [t] = useTranslation();
const [isOpened, setIsOpened] = useState(false);
const [data, handleFieldChange, setData] = useForm(DEFAULT_DATA);
const [selectNameFieldState, selectNameField] = useToggle();
const nameField = useRef(null);
const open = useCallback(() => {
setIsOpened(true);
}, []);
const close = useCallback(() => {
setIsOpened(false);
}, []);
const submit = useCallback(() => {
const cleanData = {
...data,
@ -46,19 +38,6 @@ const AddCard = React.forwardRef(({ children, onCreate }, ref) => {
selectNameField();
}, [onCreate, data, setData, selectNameField]);
useImperativeHandle(
ref,
() => ({
open,
close,
}),
[open, close],
);
const handleChildrenClick = useCallback(() => {
open();
}, [open]);
const handleFieldKeyDown = useCallback(
(event) => {
switch (event.key) {
@ -69,19 +48,16 @@ const AddCard = React.forwardRef(({ children, onCreate }, ref) => {
break;
case 'Escape':
close();
onClose();
break;
default:
}
},
[close, submit],
[onClose, submit],
);
const [handleFieldBlur, handleControlMouseOver, handleControlMouseOut] = useClosableForm(
isOpened,
close,
);
const [handleFieldBlur, handleControlMouseOver, handleControlMouseOut] = useClosableForm(onClose);
const handleSubmit = useCallback(() => {
submit();
@ -97,14 +73,11 @@ const AddCard = React.forwardRef(({ children, onCreate }, ref) => {
nameField.current.ref.current.select();
}, [selectNameFieldState]);
if (!isOpened) {
return React.cloneElement(children, {
onClick: handleChildrenClick,
});
}
return (
<Form className={styles.wrapper} onSubmit={handleSubmit}>
<Form
className={classNames(styles.wrapper, !isOpened && styles.wrapperClosed)}
onSubmit={handleSubmit}
>
<div className={styles.fieldWrapper}>
<TextArea
ref={nameField}
@ -135,8 +108,9 @@ const AddCard = React.forwardRef(({ children, onCreate }, ref) => {
});
AddCard.propTypes = {
children: PropTypes.element.isRequired,
isOpened: PropTypes.bool.isRequired,
onCreate: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired,
};
export default React.memo(AddCard);
export default AddCard;

View file

@ -24,3 +24,7 @@
.wrapper {
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 classNames from 'classnames';
import { useTranslation } from 'react-i18next';
@ -17,9 +17,10 @@ import styles from './List.module.css';
const List = React.memo(
({ id, index, name, isPersisted, cardIds, onUpdate, onDelete, onCardCreate }) => {
const [t] = useTranslation();
const [isAddCardOpened, setIsAddCardOpened] = useState(false);
const addCard = useRef(null);
const editName = useRef(null);
const listWrapper = useRef(null);
const handleHeaderClick = useCallback(() => {
if (isPersisted) {
@ -36,14 +37,28 @@ const List = React.memo(
[onUpdate],
);
const handleAddCardClick = useCallback(() => {
setIsAddCardOpened(true);
}, []);
const handleAddCardClose = useCallback(() => {
setIsAddCardOpened(false);
}, []);
const handleNameEdit = useCallback(() => {
editName.current.open();
}, []);
const handleCardAdd = useCallback(() => {
addCard.current.open();
setIsAddCardOpened(true);
}, []);
useEffect(() => {
if (isAddCardOpened) {
listWrapper.current.scrollTop = listWrapper.current.scrollHeight;
}
}, [cardIds, isAddCardOpened]);
const cardsNode = (
<Droppable
droppableId={`list:${id}`}
@ -58,15 +73,12 @@ const List = React.memo(
<CardContainer key={cardId} id={cardId} index={cardIndex} />
))}
{placeholder}
<AddCard
isOpened={isAddCardOpened}
onCreate={onCardCreate}
onClose={handleAddCardClose}
/>
</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>
)}
</Droppable>
@ -99,8 +111,26 @@ const List = React.memo(
</ActionsPopup>
)}
</div>
<div
ref={listWrapper}
className={classNames(styles.listWrapper, isAddCardOpened && styles.listWrapperFull)}
>
<div className={styles.list}>{cardsNode}</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>
);

View file

@ -1,6 +1,7 @@
.addCardButton {
background: none !important;
background: #dfe3e6 !important;
border: none !important;
border-radius: 0 0 3px 3px !important;
color: #6b808c !important;
cursor: pointer;
display: block !important;
@ -8,18 +9,14 @@
flex: 0 0 auto;
font-weight: normal !important;
height: 36px !important;
margin: 0 -8px !important;
outline: none !important;
padding: 8px !important;
text-align: left !important;
width: calc(100% + 16px);
}
.addCardButton:active {
outline: none !important;
width: 100%;
}
.addCardButton:hover {
background-color: #092d4221 !important;
background-color: #c3cbd0 !important;
color: #17394d !important;
fill: #17394d !important;
}
@ -40,6 +37,7 @@
.cards {
flex: 1 1 auto;
min-height: 1px;
}
.header {
@ -96,18 +94,48 @@
.list {
background: #dfe3e6 !important;
border-radius: 0 0 3px 3px !important;
box-sizing: border-box !important;
display: flex;
flex-direction: column;
padding: 0 8px !important;
position: relative !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 {
border-radius: 3px;
flex: 0 0 auto;
margin: 0 4px !important;
overflow: hidden;
vertical-align: top !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 PropTypes from 'prop-types';
import classNames from 'classnames';
@ -7,15 +6,13 @@ import { Link } from 'react-router-dom';
import { Container, Grid } from 'semantic-ui-react';
import Paths from '../../constants/Paths';
import ProjectWrapperContainer from '../../containers/ProjectWrapperContainer';
import { ReactComponent as PlusIcon } from '../../assets/images/plus-icon.svg';
import styles from './Projects.module.css';
const Projects = React.memo(({ items, currentId, isEditable, onAdd }) => {
const Projects = React.memo(({ items, isEditable, onAdd }) => {
const [t] = useTranslation();
if (isUndefined(currentId)) {
return (
<Container className={styles.cardsWrapper}>
<Grid className={styles.gridFix}>
@ -53,26 +50,12 @@ const Projects = React.memo(({ items, currentId, isEditable, onAdd }) => {
</Grid>
</Container>
);
}
return (
<div className={styles.wrapper}>
<div className={styles.project}>
<ProjectWrapperContainer />
</div>
</div>
);
});
Projects.propTypes = {
items: PropTypes.array.isRequired, // eslint-disable-line react/forbid-prop-types
currentId: PropTypes.string,
isEditable: PropTypes.bool.isRequired,
onAdd: PropTypes.func.isRequired,
};
Projects.defaultProps = {
currentId: undefined,
};
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,
};
export default withPopup(UserStep);
export default withPopup(UserStep, {
position: 'bottom right',
});

View file

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

View file

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

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 { connect } from 'react-redux';
import { currentUserSelector, pathSelector, projectsForCurrentUserSelector } from '../selectors';
import { currentUserSelector, projectsForCurrentUserSelector } from '../selectors';
import { openAddProjectModal } from '../actions/entry';
import Projects from '../components/Projects';
const mapStateToProps = (state) => {
const { projectId } = pathSelector(state);
const { isAdmin } = currentUserSelector(state);
const projects = projectsForCurrentUserSelector(state);
return {
items: projects,
currentId: projectId,
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';
export default (isOpened, close) => {
export default (close, isOpened = true) => {
const isClosable = useRef(null);
const handleFieldBlur = useCallback(() => {

View file

@ -5,7 +5,7 @@ body {
#root {
display: flex;
flex-direction: column;
height: 100vh;
height: 100%;
}
.react-datepicker {
@ -100,3 +100,30 @@ body {
top: 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

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

View file

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

View file

@ -53,7 +53,7 @@ module.exports = {
}
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 {
values.boardId = inputs.toBoard.id;
}