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

Project managers, board members, auto-update after reconnection, refactoring

This commit is contained in:
Maksim Eltyshev 2021-06-24 01:05:22 +05:00
parent 7956503a46
commit fe91b5241e
478 changed files with 21226 additions and 19495 deletions

View file

@ -0,0 +1,78 @@
import React from 'react';
import PropTypes from 'prop-types';
import Filters from './Filters';
import Memberships from '../Memberships';
import styles from './BoardActions.module.scss';
const BoardActions = React.memo(
({
memberships,
labels,
filterUsers,
filterLabels,
allUsers,
canEditMemberships,
onMembershipCreate,
onMembershipDelete,
onUserToFilterAdd,
onUserFromFilterRemove,
onLabelToFilterAdd,
onLabelFromFilterRemove,
onLabelCreate,
onLabelUpdate,
onLabelDelete,
}) => {
return (
<div className={styles.actions}>
<div className={styles.action}>
<Memberships
items={memberships}
allUsers={allUsers}
canEdit={canEditMemberships}
onCreate={onMembershipCreate}
onDelete={onMembershipDelete}
/>
</div>
<div className={styles.action}>
<Filters
users={filterUsers}
labels={filterLabels}
allBoardMemberships={memberships}
allLabels={labels}
onUserAdd={onUserToFilterAdd}
onUserRemove={onUserFromFilterRemove}
onLabelAdd={onLabelToFilterAdd}
onLabelRemove={onLabelFromFilterRemove}
onLabelCreate={onLabelCreate}
onLabelUpdate={onLabelUpdate}
onLabelDelete={onLabelDelete}
/>
</div>
</div>
);
},
);
BoardActions.propTypes = {
/* eslint-disable react/forbid-prop-types */
memberships: PropTypes.array.isRequired,
labels: PropTypes.array.isRequired,
filterUsers: PropTypes.array.isRequired,
filterLabels: PropTypes.array.isRequired,
allUsers: PropTypes.array.isRequired,
/* eslint-enable react/forbid-prop-types */
canEditMemberships: PropTypes.bool.isRequired,
onMembershipCreate: PropTypes.func.isRequired,
onMembershipDelete: PropTypes.func.isRequired,
onUserToFilterAdd: PropTypes.func.isRequired,
onUserFromFilterRemove: PropTypes.func.isRequired,
onLabelToFilterAdd: PropTypes.func.isRequired,
onLabelFromFilterRemove: PropTypes.func.isRequired,
onLabelCreate: PropTypes.func.isRequired,
onLabelUpdate: PropTypes.func.isRequired,
onLabelDelete: PropTypes.func.isRequired,
};
export default BoardActions;

View file

@ -0,0 +1,11 @@
:global(#app) {
.action {
margin-right: 20px;
}
.actions {
align-items: center;
display: flex;
margin: 20px 20px;
}
}

View file

@ -4,16 +4,16 @@ import { useTranslation } from 'react-i18next';
import User from '../User';
import Label from '../Label';
import ProjectMembershipsPopup from '../ProjectMembershipsPopup';
import BoardMembershipsPopup from '../BoardMembershipsPopup';
import LabelsPopup from '../LabelsPopup';
import styles from './Filter.module.scss';
import styles from './Filters.module.scss';
const Filter = React.memo(
const Filters = React.memo(
({
users,
labels,
allProjectMemberships,
allBoardMemberships,
allLabels,
onUserAdd,
onUserRemove,
@ -40,10 +40,10 @@ const Filter = React.memo(
);
return (
<div className={styles.filters}>
<>
<span className={styles.filter}>
<ProjectMembershipsPopup
items={allProjectMemberships}
<BoardMembershipsPopup
items={allBoardMemberships}
currentUserIds={users.map((user) => user.id)}
title={t('common.filterByMembers', {
context: 'title',
@ -55,7 +55,7 @@ const Filter = React.memo(
<span className={styles.filterTitle}>{`${t('common.members')}:`}</span>
{users.length === 0 && <span className={styles.filterLabel}>{t('common.all')}</span>}
</button>
</ProjectMembershipsPopup>
</BoardMembershipsPopup>
{users.map((user) => (
<span key={user.id} className={styles.filterItem}>
<User
@ -96,16 +96,16 @@ const Filter = React.memo(
</span>
))}
</span>
</div>
</>
);
},
);
Filter.propTypes = {
Filters.propTypes = {
/* eslint-disable react/forbid-prop-types */
users: PropTypes.array.isRequired,
labels: PropTypes.array.isRequired,
allProjectMemberships: PropTypes.array.isRequired,
allBoardMemberships: PropTypes.array.isRequired,
allLabels: PropTypes.array.isRequired,
/* eslint-enable react/forbid-prop-types */
onUserAdd: PropTypes.func.isRequired,
@ -117,4 +117,4 @@ Filter.propTypes = {
onLabelDelete: PropTypes.func.isRequired,
};
export default Filter;
export default Filters;

View file

@ -1,8 +1,6 @@
:global(#app) {
.filter {
display: inline-block;
line-height: 0;
margin-right: 16px;
margin-right: 10px;
}
.filterButton {
@ -45,9 +43,4 @@
line-height: 20px;
padding: 2px 12px;
}
.filters {
line-height: 0;
margin-bottom: 12px;
}
}

View file

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

View file

@ -8,7 +8,6 @@ import DroppableTypes from '../../constants/DroppableTypes';
import ListContainer from '../../containers/ListContainer';
import CardModalContainer from '../../containers/CardModalContainer';
import ListAdd from './ListAdd';
import Filter from './Filter';
import { ReactComponent as PlusMathIcon } from '../../assets/images/plus-math-icon.svg';
import styles from './BoardKanban.module.scss';
@ -16,36 +15,19 @@ import styles from './BoardKanban.module.scss';
const parseDndId = (dndId) => dndId.split(':')[1];
const BoardKanban = React.memo(
({
listIds,
filterUsers,
filterLabels,
allProjectMemberships,
allLabels,
isCardModalOpened,
onListCreate,
onListMove,
onCardMove,
onUserToFilterAdd,
onUserFromFilterRemove,
onLabelToFilterAdd,
onLabelFromFilterRemove,
onLabelCreate,
onLabelUpdate,
onLabelDelete,
}) => {
({ listIds, isCardModalOpened, canEdit, onListCreate, onListMove, onCardMove }) => {
const [t] = useTranslation();
const [isAddListOpened, setIsAddListOpened] = useState(false);
const [isListAddOpened, setIsListAddOpened] = useState(false);
const wrapper = useRef(null);
const prevPosition = useRef(null);
const handleAddListClick = useCallback(() => {
setIsAddListOpened(true);
setIsListAddOpened(true);
}, []);
const handleAddListClose = useCallback(() => {
setIsAddListOpened(false);
setIsListAddOpened(false);
}, []);
const handleDragStart = useCallback(() => {
@ -119,10 +101,10 @@ const BoardKanban = React.memo(
}, []);
useEffect(() => {
if (isAddListOpened) {
if (isListAddOpened) {
window.scroll(document.body.scrollWidth, 0);
}
}, [listIds, isAddListOpened]);
}, [listIds, isListAddOpened]);
useEffect(() => {
window.addEventListener('mouseup', handleWindowMouseUp);
@ -138,19 +120,6 @@ const BoardKanban = React.memo(
<>
{/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */}
<div ref={wrapper} className={styles.wrapper} onMouseDown={handleMouseDown}>
<Filter
users={filterUsers}
labels={filterLabels}
allProjectMemberships={allProjectMemberships}
allLabels={allLabels}
onUserAdd={onUserToFilterAdd}
onUserRemove={onUserFromFilterRemove}
onLabelAdd={onLabelToFilterAdd}
onLabelRemove={onLabelFromFilterRemove}
onLabelCreate={onLabelCreate}
onLabelUpdate={onLabelUpdate}
onLabelDelete={onLabelDelete}
/>
<div>
<DragDropContext onDragStart={handleDragStart} onDragEnd={handleDragEnd}>
<Droppable droppableId="board" type={DroppableTypes.LIST} direction="horizontal">
@ -165,26 +134,26 @@ const BoardKanban = React.memo(
<ListContainer key={listId} id={listId} index={index} />
))}
{placeholder}
<div data-drag-scroller className={styles.list}>
{isAddListOpened ? (
<ListAdd
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>
)}
</div>
{canEdit && (
<div data-drag-scroller className={styles.list}>
{isListAddOpened ? (
<ListAdd 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>
)}
</div>
)}
</div>
)}
</Droppable>
@ -198,24 +167,12 @@ const BoardKanban = React.memo(
);
BoardKanban.propTypes = {
/* eslint-disable react/forbid-prop-types */
listIds: PropTypes.array.isRequired,
filterUsers: PropTypes.array.isRequired,
filterLabels: PropTypes.array.isRequired,
allProjectMemberships: PropTypes.array.isRequired,
allLabels: PropTypes.array.isRequired,
/* eslint-enable react/forbid-prop-types */
listIds: PropTypes.array.isRequired, // eslint-disable-line react/forbid-prop-types
isCardModalOpened: PropTypes.bool.isRequired,
canEdit: PropTypes.bool.isRequired,
onListCreate: PropTypes.func.isRequired,
onListMove: PropTypes.func.isRequired,
onCardMove: PropTypes.func.isRequired,
onUserToFilterAdd: PropTypes.func.isRequired,
onUserFromFilterRemove: PropTypes.func.isRequired,
onLabelToFilterAdd: PropTypes.func.isRequired,
onLabelFromFilterRemove: PropTypes.func.isRequired,
onLabelCreate: PropTypes.func.isRequired,
onLabelUpdate: PropTypes.func.isRequired,
onLabelDelete: PropTypes.func.isRequired,
};
export default BoardKanban;

View file

@ -38,7 +38,7 @@
}
.list {
margin: 0 20px 0 4px;
margin-right: 20px;
width: 272px;
}
@ -48,6 +48,16 @@
min-width: 100%;
}
.panel {
align-items: center;
display: flex;
margin-bottom: 20px;
}
.panelItem {
margin-right: 20px;
}
.wrapper {
margin: 0 20px;
}

View file

@ -0,0 +1,5 @@
import { withPopup } from '../lib/popup';
import BoardMembershipsStep from './BoardMembershipsStep';
export default withPopup(BoardMembershipsStep);

View file

@ -6,9 +6,9 @@ import { Popup } from '../../lib/custom-ui';
import Item from './Item';
import styles from './ProjectMembershipsStep.module.scss';
import styles from './BoardMembershipsStep.module.scss';
const ProjectMembershipsStep = React.memo(
const BoardMembershipsStep = React.memo(
({ items, currentUserIds, title, onUserSelect, onUserDeselect, onBack }) => {
const [t] = useTranslation();
@ -48,7 +48,7 @@ const ProjectMembershipsStep = React.memo(
},
);
ProjectMembershipsStep.propTypes = {
BoardMembershipsStep.propTypes = {
/* eslint-disable react/forbid-prop-types */
items: PropTypes.array.isRequired,
currentUserIds: PropTypes.array.isRequired,
@ -59,9 +59,9 @@ ProjectMembershipsStep.propTypes = {
onBack: PropTypes.func,
};
ProjectMembershipsStep.defaultProps = {
BoardMembershipsStep.defaultProps = {
title: 'common.members',
onBack: undefined,
};
export default ProjectMembershipsStep;
export default BoardMembershipsStep;

View file

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

View file

@ -14,145 +14,117 @@ import EditPopup from './EditPopup';
import styles from './Boards.module.scss';
const Boards = React.memo(
({ items, currentId, isEditable, onCreate, onUpdate, onMove, onDelete }) => {
const tabsWrapper = useRef(null);
const Boards = React.memo(({ items, currentId, canEdit, onCreate, onUpdate, onMove, onDelete }) => {
const tabsWrapper = useRef(null);
const handleWheel = useCallback(({ deltaY }) => {
tabsWrapper.current.scrollBy({
left: deltaY,
});
}, []);
const handleWheel = useCallback(({ deltaY }) => {
tabsWrapper.current.scrollBy({
left: deltaY,
});
}, []);
const handleDragStart = useCallback(() => {
closePopup();
}, []);
const handleDragStart = useCallback(() => {
closePopup();
}, []);
const handleDragEnd = useCallback(
({ draggableId, source, destination }) => {
if (!destination || source.index === destination.index) {
return;
}
const handleDragEnd = useCallback(
({ draggableId, source, destination }) => {
if (!destination || source.index === destination.index) {
return;
}
onMove(draggableId, destination.index);
},
[onMove],
);
onMove(draggableId, destination.index);
},
[onMove],
);
const handleUpdate = useCallback(
(id, data) => {
onUpdate(id, data);
},
[onUpdate],
);
const handleUpdate = useCallback(
(id, data) => {
onUpdate(id, data);
},
[onUpdate],
);
const handleDelete = useCallback(
(id) => {
onDelete(id);
},
[onDelete],
);
const handleDelete = useCallback(
(id) => {
onDelete(id);
},
[onDelete],
);
const renderItems = useCallback(
(safeItems) =>
safeItems.map((item) => (
<div key={item.id} className={styles.tabWrapper}>
<div className={classNames(styles.tab, item.id === currentId && styles.tabActive)}>
{item.isPersisted ? (
const itemsNode = items.map((item, index) => (
<Draggable
key={item.id}
draggableId={item.id}
index={index}
isDragDisabled={!item.isPersisted || !canEdit}
>
{({ innerRef, draggableProps, dragHandleProps }) => (
// eslint-disable-next-line react/jsx-props-no-spreading
<div {...draggableProps} ref={innerRef} className={styles.tabWrapper}>
<div className={classNames(styles.tab, item.id === currentId && styles.tabActive)}>
{item.isPersisted ? (
<>
<Link
{...dragHandleProps} // eslint-disable-line react/jsx-props-no-spreading
to={Paths.BOARDS.replace(':id', item.id)}
title={item.name}
className={styles.link}
>
{item.name}
</Link>
) : (
<span className={styles.link}>{item.name}</span>
)}
</div>
</div>
)),
[currentId],
);
const renderEditableItems = useCallback(
(safeItems) =>
safeItems.map((item, index) => (
<Draggable
key={item.id}
draggableId={item.id}
index={index}
isDragDisabled={!item.isPersisted}
>
{({ innerRef, draggableProps, dragHandleProps }) => (
{canEdit && (
<EditPopup
defaultData={pick(item, 'name')}
onUpdate={(data) => handleUpdate(item.id, data)}
onDelete={() => handleDelete(item.id)}
>
<Button className={classNames(styles.editButton, styles.target)}>
<Icon fitted name="pencil" size="small" />
</Button>
</EditPopup>
)}
</>
) : (
// eslint-disable-next-line react/jsx-props-no-spreading
<div {...draggableProps} ref={innerRef} className={styles.tabWrapper}>
<div className={classNames(styles.tab, item.id === currentId && styles.tabActive)}>
{item.isPersisted ? (
<Link
{...dragHandleProps} // eslint-disable-line react/jsx-props-no-spreading
to={Paths.BOARDS.replace(':id', item.id)}
title={item.name}
className={styles.link}
>
{item.name}
</Link>
) : (
// eslint-disable-next-line react/jsx-props-no-spreading
<span {...dragHandleProps} className={styles.link}>
{item.name}
</span>
)}
{item.isPersisted && (
<EditPopup
defaultData={pick(item, 'name')}
onUpdate={(data) => handleUpdate(item.id, data)}
onDelete={() => handleDelete(item.id)}
>
<Button className={classNames(styles.editButton, styles.target)}>
<Icon fitted name="pencil" size="small" />
</Button>
</EditPopup>
)}
</div>
<span {...dragHandleProps} className={styles.link}>
{item.name}
</span>
)}
</div>
</div>
)}
</Draggable>
));
return (
<div className={styles.wrapper} onWheel={handleWheel}>
<div ref={tabsWrapper} className={styles.tabsWrapper}>
<DragDropContext onDragStart={handleDragStart} onDragEnd={handleDragEnd}>
<Droppable droppableId="boards" type={DroppableTypes.BOARD} direction="horizontal">
{({ innerRef, droppableProps, placeholder }) => (
// eslint-disable-next-line react/jsx-props-no-spreading
<div {...droppableProps} ref={innerRef} className={styles.tabs}>
{itemsNode}
{placeholder}
{canEdit && (
<AddPopup onCreate={onCreate}>
<Button icon="plus" className={styles.addButton} />
</AddPopup>
)}
</div>
)}
</Draggable>
)),
[currentId, handleUpdate, handleDelete],
);
return (
<div className={styles.wrapper} onWheel={handleWheel}>
<div ref={tabsWrapper} className={styles.tabsWrapper}>
{isEditable ? (
<DragDropContext onDragStart={handleDragStart} onDragEnd={handleDragEnd}>
<Droppable droppableId="boards" type={DroppableTypes.BOARD} direction="horizontal">
{({ innerRef, droppableProps, placeholder }) => (
// eslint-disable-next-line react/jsx-props-no-spreading
<div {...droppableProps} ref={innerRef} className={styles.tabs}>
{renderEditableItems(items)}
{placeholder}
<AddPopup onCreate={onCreate}>
<Button icon="plus" className={styles.addButton} />
</AddPopup>
</div>
)}
</Droppable>
</DragDropContext>
) : (
<div className={styles.tabs}>{renderItems(items)}</div>
)}
</div>
</Droppable>
</DragDropContext>
</div>
);
},
);
</div>
);
});
Boards.propTypes = {
items: PropTypes.array.isRequired, // eslint-disable-line react/forbid-prop-types
currentId: PropTypes.string,
isEditable: PropTypes.bool.isRequired,
canEdit: PropTypes.bool.isRequired,
onCreate: PropTypes.func.isRequired,
onUpdate: PropTypes.func.isRequired,
onMove: PropTypes.func.isRequired,

View file

@ -30,6 +30,7 @@
.link {
color: #fff;
cursor: pointer;
display: block;
line-height: 20px;
padding: 10px 34px 6px 14px;
@ -99,15 +100,11 @@
}
.wrapper {
border-bottom: 2px solid rgba(0, 0, 0, 0.24);
border-bottom: 1px solid rgba(0, 0, 0, 0.24);
display: flex;
flex: 1 1 auto;
flex-direction: column;
height: 38px;
overflow: hidden;
&:hover {
border-bottom: 0;
}
}
}

View file

@ -7,7 +7,7 @@ import { withPopup } from '../../lib/popup';
import { Popup } from '../../lib/custom-ui';
import { useSteps } from '../../hooks';
import ProjectMembershipsStep from '../ProjectMembershipsStep';
import BoardMembershipsStep from '../BoardMembershipsStep';
import LabelsStep from '../LabelsStep';
import DueDateEditStep from '../DueDateEditStep';
import TimerEditStep from '../TimerEditStep';
@ -29,7 +29,7 @@ const ActionsStep = React.memo(
({
card,
projectsToLists,
projectMemberships,
boardMemberships,
currentUserIds,
labels,
currentLabelIds,
@ -102,8 +102,8 @@ const ActionsStep = React.memo(
switch (step.type) {
case StepTypes.USERS:
return (
<ProjectMembershipsStep
items={projectMemberships}
<BoardMembershipsStep
items={boardMemberships}
currentUserIds={currentUserIds}
onUserSelect={onUserAdd}
onUserDeselect={onUserRemove}
@ -224,7 +224,7 @@ ActionsStep.propTypes = {
/* eslint-disable react/forbid-prop-types */
card: PropTypes.object.isRequired,
projectsToLists: PropTypes.array.isRequired,
projectMemberships: PropTypes.array.isRequired,
boardMemberships: PropTypes.array.isRequired,
currentUserIds: PropTypes.array.isRequired,
labels: PropTypes.array.isRequired,
currentLabelIds: PropTypes.array.isRequired,

View file

@ -33,8 +33,9 @@ const Card = React.memo(
labels,
tasks,
allProjectsToLists,
allProjectMemberships,
allBoardMemberships,
allLabels,
canEdit,
onUpdate,
onMove,
onTransfer,
@ -129,7 +130,7 @@ const Card = React.memo(
);
return (
<Draggable draggableId={`card:${id}`} index={index} isDragDisabled={!isPersisted}>
<Draggable draggableId={`card:${id}`} index={index} isDragDisabled={!isPersisted || !canEdit}>
{({ innerRef, draggableProps, dragHandleProps }) => (
// eslint-disable-next-line react/jsx-props-no-spreading
<div {...draggableProps} {...dragHandleProps} ref={innerRef} className={styles.wrapper}>
@ -144,40 +145,42 @@ const Card = React.memo(
>
{contentNode}
</Link>
<ActionsPopup
card={{
id,
name,
dueDate,
timer,
boardId,
listId,
projectId,
isPersisted,
}}
projectsToLists={allProjectsToLists}
projectMemberships={allProjectMemberships}
currentUserIds={users.map((user) => user.id)}
labels={allLabels}
currentLabelIds={labels.map((label) => label.id)}
onNameEdit={handleNameEdit}
onUpdate={onUpdate}
onMove={onMove}
onTransfer={onTransfer}
onDelete={onDelete}
onUserAdd={onUserAdd}
onUserRemove={onUserRemove}
onBoardFetch={onBoardFetch}
onLabelAdd={onLabelAdd}
onLabelRemove={onLabelRemove}
onLabelCreate={onLabelCreate}
onLabelUpdate={onLabelUpdate}
onLabelDelete={onLabelDelete}
>
<Button className={classNames(styles.actionsButton, styles.target)}>
<Icon fitted name="pencil" size="small" />
</Button>
</ActionsPopup>
{canEdit && (
<ActionsPopup
card={{
id,
name,
dueDate,
timer,
boardId,
listId,
projectId,
isPersisted,
}}
projectsToLists={allProjectsToLists}
boardMemberships={allBoardMemberships}
currentUserIds={users.map((user) => user.id)}
labels={allLabels}
currentLabelIds={labels.map((label) => label.id)}
onNameEdit={handleNameEdit}
onUpdate={onUpdate}
onMove={onMove}
onTransfer={onTransfer}
onDelete={onDelete}
onUserAdd={onUserAdd}
onUserRemove={onUserRemove}
onBoardFetch={onBoardFetch}
onLabelAdd={onLabelAdd}
onLabelRemove={onLabelRemove}
onLabelCreate={onLabelCreate}
onLabelUpdate={onLabelUpdate}
onLabelDelete={onLabelDelete}
>
<Button className={classNames(styles.actionsButton, styles.target)}>
<Icon fitted name="pencil" size="small" />
</Button>
</ActionsPopup>
)}
</>
) : (
<span className={styles.content}>{contentNode}</span>
@ -208,9 +211,10 @@ Card.propTypes = {
labels: PropTypes.array.isRequired,
tasks: PropTypes.array.isRequired,
allProjectsToLists: PropTypes.array.isRequired,
allProjectMemberships: PropTypes.array.isRequired,
allBoardMemberships: PropTypes.array.isRequired,
allLabels: PropTypes.array.isRequired,
/* eslint-enable react/forbid-prop-types */
canEdit: PropTypes.bool.isRequired,
onUpdate: PropTypes.func.isRequired,
onMove: PropTypes.func.isRequired,
onTransfer: PropTypes.func.isRequired,

View file

@ -54,7 +54,6 @@
border-radius: 3px;
box-shadow: 0 1px 0 #ccc;
position: relative;
cursor: pointer;
&:hover {
background: #f5f6f7;
@ -67,7 +66,6 @@
}
.content {
cursor: grab;
display: block;
&:after {

View file

@ -2,7 +2,6 @@
.button {
background: transparent;
border: none;
cursor: pointer;
line-height: 0;
margin: 0 -8px;
outline: none;
@ -12,7 +11,6 @@
.count {
color: #888;
cursor: pointer;
display: inline-block;
font-size: 12px;
line-height: 12px;
@ -77,7 +75,6 @@
.tasks {
color: #333;
cursor: grab;
list-style: none;
margin: -2px 0 0;
padding-left: 0;

View file

@ -14,7 +14,8 @@ const Actions = React.memo(
items,
isFetching,
isAllFetched,
isEditable,
canEdit,
canEditAllComments,
onFetch,
onCommentCreate,
onCommentUpdate,
@ -38,13 +39,15 @@ const Actions = React.memo(
return (
<>
<div className={styles.contentModule}>
<div className={styles.moduleWrapper}>
<Icon name="comment outline" className={styles.moduleIcon} />
<div className={styles.moduleHeader}>{t('common.addComment')}</div>
<CommentAdd onCreate={onCommentCreate} />
{canEdit && (
<div className={styles.contentModule}>
<div className={styles.moduleWrapper}>
<Icon name="comment outline" className={styles.moduleIcon} />
<div className={styles.moduleHeader}>{t('common.addComment')}</div>
<CommentAdd onCreate={onCommentCreate} />
</div>
</div>
</div>
)}
<div className={styles.contentModule}>
<div className={styles.moduleWrapper}>
<Icon name="list ul" className={styles.moduleIcon} />
@ -59,7 +62,7 @@ const Actions = React.memo(
createdAt={item.createdAt}
isPersisted={item.isPersisted}
user={item.user}
isEditable={isEditable}
canEdit={(item.user.isCurrent && canEdit) || canEditAllComments}
onUpdate={(data) => handleCommentUpdate(item.id, data)}
onDelete={() => handleCommentDelete(item.id)}
/>
@ -91,7 +94,8 @@ Actions.propTypes = {
items: PropTypes.array.isRequired, // eslint-disable-line react/forbid-prop-types
isFetching: PropTypes.bool.isRequired,
isAllFetched: PropTypes.bool.isRequired,
isEditable: PropTypes.bool.isRequired,
canEdit: PropTypes.bool.isRequired,
canEditAllComments: PropTypes.bool.isRequired,
onFetch: PropTypes.func.isRequired,
onCommentCreate: PropTypes.func.isRequired,
onCommentUpdate: PropTypes.func.isRequired,

View file

@ -12,7 +12,7 @@ import DeletePopup from '../../DeletePopup';
import styles from './ItemComment.module.scss';
const ItemComment = React.memo(
({ data, createdAt, isPersisted, user, isEditable, onUpdate, onDelete }) => {
({ data, createdAt, isPersisted, user, canEdit, onUpdate, onDelete }) => {
const [t] = useTranslation();
const commentEdit = useRef(null);
@ -38,17 +38,17 @@ const ItemComment = React.memo(
</div>
<CommentEdit ref={commentEdit} defaultData={data} onUpdate={onUpdate}>
<>
<Markdown source={data.text} linkTarget="_blank" className={styles.text} />
<Comment.Actions>
{user.isCurrent && (
<Markdown linkTarget="_blank" className={styles.text}>
{data.text}
</Markdown>
{canEdit && (
<Comment.Actions>
<Comment.Action
as="button"
content={t('action.edit')}
disabled={!isPersisted}
onClick={handleEditClick}
/>
)}
{(user.isCurrent || isEditable) && (
<DeletePopup
title={t('common.deleteComment', {
context: 'title',
@ -63,8 +63,8 @@ const ItemComment = React.memo(
disabled={!isPersisted}
/>
</DeletePopup>
)}
</Comment.Actions>
</Comment.Actions>
)}
</>
</CommentEdit>
</div>
@ -78,7 +78,7 @@ ItemComment.propTypes = {
createdAt: PropTypes.instanceOf(Date).isRequired,
isPersisted: PropTypes.bool.isRequired,
user: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
isEditable: PropTypes.bool.isRequired,
canEdit: PropTypes.bool.isRequired,
onUpdate: PropTypes.func.isRequired,
onDelete: PropTypes.func.isRequired,
};

View file

@ -10,7 +10,7 @@
position: absolute;
text-align: center;
width: 100%;
z-index: 1;
z-index: 2000;
}
.wrapper {

View file

@ -16,7 +16,7 @@ import User from '../User';
import Label from '../Label';
import DueDate from '../DueDate';
import Timer from '../Timer';
import ProjectMembershipsPopup from '../ProjectMembershipsPopup';
import BoardMembershipsPopup from '../BoardMembershipsPopup';
import LabelsPopup from '../LabelsPopup';
import DueDateEditPopup from '../DueDateEditPopup';
import TimerEditPopup from '../TimerEditPopup';
@ -43,9 +43,10 @@ const CardModal = React.memo(
attachments,
actions,
allProjectsToLists,
allProjectMemberships,
allBoardMemberships,
allLabels,
isEditable,
canEdit,
canEditAllCommentActions,
onUpdate,
onMove,
onTransfer,
@ -126,137 +127,163 @@ const CardModal = React.memo(
const userIds = users.map((user) => user.id);
const labelIds = labels.map((label) => label.id);
return (
<Modal open closeIcon size="small" centered={false} onClose={onClose}>
<AttachmentAddZone onCreate={onAttachmentCreate}>
<Grid className={styles.grid}>
<Grid.Row className={styles.headerPadding}>
<Grid.Column width={16} className={styles.headerPadding}>
<div className={styles.headerWrapper}>
<Icon name="list alternate outline" className={styles.moduleIcon} />
<div className={styles.headerTitle}>
<NameField defaultValue={name} onUpdate={handleNameUpdate} />
</div>
</div>
</Grid.Column>
</Grid.Row>
<Grid.Row className={styles.modalPadding}>
<Grid.Column width={12} className={styles.contentPadding}>
{(users.length > 0 || labels.length > 0 || dueDate || timer) && (
<div className={styles.moduleWrapper}>
{users.length > 0 && (
<div className={styles.attachments}>
<div className={styles.text}>
{t('common.members', {
context: 'title',
})}
</div>
{users.map((user) => (
<span key={user.id} className={styles.attachment}>
<ProjectMembershipsPopup
items={allProjectMemberships}
currentUserIds={userIds}
onUserSelect={onUserAdd}
onUserDeselect={onUserRemove}
>
<User name={user.name} avatarUrl={user.avatarUrl} />
</ProjectMembershipsPopup>
</span>
))}
<ProjectMembershipsPopup
items={allProjectMemberships}
currentUserIds={userIds}
onUserSelect={onUserAdd}
onUserDeselect={onUserRemove}
>
<button
type="button"
className={classNames(styles.attachment, styles.dueDate)}
const contentNode = (
<Grid className={styles.grid}>
<Grid.Row className={styles.headerPadding}>
<Grid.Column width={16} className={styles.headerPadding}>
<div className={styles.headerWrapper}>
<Icon name="list alternate outline" className={styles.moduleIcon} />
<div className={styles.headerTitleWrapper}>
{canEdit ? (
<NameField defaultValue={name} onUpdate={handleNameUpdate} />
) : (
<div className={styles.headerTitle}>{name}</div>
)}
</div>
</div>
</Grid.Column>
</Grid.Row>
<Grid.Row className={styles.modalPadding}>
<Grid.Column width={canEdit ? 12 : 16} className={styles.contentPadding}>
{(users.length > 0 || labels.length > 0 || dueDate || timer) && (
<div className={styles.moduleWrapper}>
{users.length > 0 && (
<div className={styles.attachments}>
<div className={styles.text}>
{t('common.members', {
context: 'title',
})}
</div>
{users.map((user) => (
<span key={user.id} className={styles.attachment}>
{canEdit ? (
<BoardMembershipsPopup
items={allBoardMemberships}
currentUserIds={userIds}
onUserSelect={onUserAdd}
onUserDeselect={onUserRemove}
>
<Icon name="add" size="small" className={styles.addAttachment} />
</button>
</ProjectMembershipsPopup>
</div>
)}
{labels.length > 0 && (
<div className={styles.attachments}>
<div className={styles.text}>
{t('common.labels', {
context: 'title',
})}
</div>
{labels.map((label) => (
<span key={label.id} className={styles.attachment}>
<LabelsPopup
key={label.id}
items={allLabels}
currentIds={labelIds}
onSelect={onLabelAdd}
onDeselect={onLabelRemove}
onCreate={onLabelCreate}
onUpdate={onLabelUpdate}
onDelete={onLabelDelete}
>
<Label name={label.name} color={label.color} />
</LabelsPopup>
</span>
))}
<LabelsPopup
items={allLabels}
currentIds={labelIds}
onSelect={onLabelAdd}
onDeselect={onLabelRemove}
onCreate={onLabelCreate}
onUpdate={onLabelUpdate}
onDelete={onLabelDelete}
<User name={user.name} avatarUrl={user.avatarUrl} />
</BoardMembershipsPopup>
) : (
<User name={user.name} avatarUrl={user.avatarUrl} />
)}
</span>
))}
{canEdit && (
<BoardMembershipsPopup
items={allBoardMemberships}
currentUserIds={userIds}
onUserSelect={onUserAdd}
onUserDeselect={onUserRemove}
>
<button
type="button"
className={classNames(styles.attachment, styles.dueDate)}
>
<button
type="button"
className={classNames(styles.attachment, styles.dueDate)}
>
<Icon name="add" size="small" className={styles.addAttachment} />
</button>
</LabelsPopup>
</div>
)}
{dueDate && (
<div className={styles.attachments}>
<div className={styles.text}>
{t('common.dueDate', {
context: 'title',
})}
</div>
<span className={styles.attachment}>
<DueDateEditPopup defaultValue={dueDate} onUpdate={handleDueDateUpdate}>
<DueDate value={dueDate} />
</DueDateEditPopup>
</span>
</div>
)}
{timer && (
<div className={styles.attachments}>
<div className={styles.text}>
{t('common.timer', {
context: 'title',
})}
</div>
<span className={styles.attachment}>
<TimerEditPopup defaultValue={timer} onUpdate={handleTimerUpdate}>
<Timer startedAt={timer.startedAt} total={timer.total} />
</TimerEditPopup>
</span>
</div>
<Icon name="add" size="small" className={styles.addAttachment} />
</button>
</BoardMembershipsPopup>
)}
</div>
)}
<div className={styles.contentModule}>
<div className={styles.moduleWrapper}>
<Icon name="align justify" className={styles.moduleIcon} />
<div className={styles.moduleHeader}>{t('common.description')}</div>
{labels.length > 0 && (
<div className={styles.attachments}>
<div className={styles.text}>
{t('common.labels', {
context: 'title',
})}
</div>
{labels.map((label) => (
<span key={label.id} className={styles.attachment}>
{canEdit ? (
<LabelsPopup
key={label.id}
items={allLabels}
currentIds={labelIds}
onSelect={onLabelAdd}
onDeselect={onLabelRemove}
onCreate={onLabelCreate}
onUpdate={onLabelUpdate}
onDelete={onLabelDelete}
>
<Label name={label.name} color={label.color} />
</LabelsPopup>
) : (
<Label name={label.name} color={label.color} />
)}
</span>
))}
{canEdit && (
<LabelsPopup
items={allLabels}
currentIds={labelIds}
onSelect={onLabelAdd}
onDeselect={onLabelRemove}
onCreate={onLabelCreate}
onUpdate={onLabelUpdate}
onDelete={onLabelDelete}
>
<button
type="button"
className={classNames(styles.attachment, styles.dueDate)}
>
<Icon name="add" size="small" className={styles.addAttachment} />
</button>
</LabelsPopup>
)}
</div>
)}
{dueDate && (
<div className={styles.attachments}>
<div className={styles.text}>
{t('common.dueDate', {
context: 'title',
})}
</div>
<span className={styles.attachment}>
{canEdit ? (
<DueDateEditPopup defaultValue={dueDate} onUpdate={handleDueDateUpdate}>
<DueDate value={dueDate} />
</DueDateEditPopup>
) : (
<DueDate value={dueDate} />
)}
</span>
</div>
)}
{timer && (
<div className={styles.attachments}>
<div className={styles.text}>
{t('common.timer', {
context: 'title',
})}
</div>
<span className={styles.attachment}>
{canEdit ? (
<TimerEditPopup defaultValue={timer} onUpdate={handleTimerUpdate}>
<Timer startedAt={timer.startedAt} total={timer.total} />
</TimerEditPopup>
) : (
<Timer startedAt={timer.startedAt} total={timer.total} />
)}
</span>
</div>
)}
</div>
)}
{(description || canEdit) && (
<div className={styles.contentModule}>
<div className={styles.moduleWrapper}>
<Icon name="align justify" className={styles.moduleIcon} />
<div className={styles.moduleHeader}>{t('common.description')}</div>
{canEdit ? (
<DescriptionEdit defaultValue={description} onUpdate={handleDescriptionUpdate}>
{description ? (
<button type="button" className={styles.descriptionText}>
<Markdown linkStopPropagation source={description} linkTarget="_blank" />
<Markdown linkStopPropagation linkTarget="_blank">
{description}
</Markdown>
</button>
) : (
<button type="button" className={styles.descriptionButton}>
@ -266,142 +293,164 @@ const CardModal = React.memo(
</button>
)}
</DescriptionEdit>
</div>
</div>
<div className={styles.contentModule}>
<div className={styles.moduleWrapper}>
<Icon name="check square outline" className={styles.moduleIcon} />
<div className={styles.moduleHeader}>{t('common.tasks')}</div>
<Tasks
items={tasks}
onCreate={onTaskCreate}
onUpdate={onTaskUpdate}
onDelete={onTaskDelete}
/>
</div>
</div>
{attachments.length > 0 && (
<div className={styles.contentModule}>
<div className={styles.moduleWrapper}>
<Icon name="attach" className={styles.moduleIcon} />
<div className={styles.moduleHeader}>{t('common.attachments')}</div>
<Attachments
items={attachments}
onUpdate={onAttachmentUpdate}
onDelete={onAttachmentDelete}
onCoverUpdate={handleCoverUpdate}
/>
) : (
<div className={styles.descriptionText}>
<Markdown linkStopPropagation linkTarget="_blank">
{description}
</Markdown>
</div>
</div>
)}
<Actions
items={actions}
isFetching={isActionsFetching}
isAllFetched={isAllActionsFetched}
isEditable={isEditable}
onFetch={onActionsFetch}
onCommentCreate={onCommentActionCreate}
onCommentUpdate={onCommentActionUpdate}
onCommentDelete={onCommentActionDelete}
/>
</Grid.Column>
<Grid.Column width={4} className={styles.sidebarPadding}>
<div className={styles.actions}>
<span className={styles.actionsTitle}>{t('action.addToCard')}</span>
<ProjectMembershipsPopup
items={allProjectMemberships}
currentUserIds={userIds}
onUserSelect={onUserAdd}
onUserDeselect={onUserRemove}
>
<Button fluid className={styles.actionButton}>
<Icon name="user outline" className={styles.actionIcon} />
{t('common.members')}
</Button>
</ProjectMembershipsPopup>
<LabelsPopup
items={allLabels}
currentIds={labelIds}
onSelect={onLabelAdd}
onDeselect={onLabelRemove}
onCreate={onLabelCreate}
onUpdate={onLabelUpdate}
onDelete={onLabelDelete}
>
<Button fluid className={styles.actionButton}>
<Icon name="bookmark outline" className={styles.actionIcon} />
{t('common.labels')}
</Button>
</LabelsPopup>
<DueDateEditPopup defaultValue={dueDate} onUpdate={handleDueDateUpdate}>
<Button fluid className={styles.actionButton}>
<Icon name="calendar check outline" className={styles.actionIcon} />
{t('common.dueDate', {
context: 'title',
})}
</Button>
</DueDateEditPopup>
<TimerEditPopup defaultValue={timer} onUpdate={handleTimerUpdate}>
<Button fluid className={styles.actionButton}>
<Icon name="clock outline" className={styles.actionIcon} />
{t('common.timer')}
</Button>
</TimerEditPopup>
<AttachmentAddPopup onCreate={onAttachmentCreate}>
<Button fluid className={styles.actionButton}>
<Icon name="attach" className={styles.actionIcon} />
{t('common.attachment')}
</Button>
</AttachmentAddPopup>
)}
</div>
<div className={styles.actions}>
<span className={styles.actionsTitle}>{t('common.actions')}</span>
</div>
)}
{(tasks.length > 0 || canEdit) && (
<div className={styles.contentModule}>
<div className={styles.moduleWrapper}>
<Icon name="check square outline" className={styles.moduleIcon} />
<div className={styles.moduleHeader}>{t('common.tasks')}</div>
<Tasks
items={tasks}
canEdit={canEdit}
onCreate={onTaskCreate}
onUpdate={onTaskUpdate}
onDelete={onTaskDelete}
/>
</div>
</div>
)}
{attachments.length > 0 && (
<div className={styles.contentModule}>
<div className={styles.moduleWrapper}>
<Icon name="attach" className={styles.moduleIcon} />
<div className={styles.moduleHeader}>{t('common.attachments')}</div>
<Attachments
items={attachments}
onUpdate={onAttachmentUpdate}
onDelete={onAttachmentDelete}
onCoverUpdate={handleCoverUpdate}
/>
</div>
</div>
)}
<Actions
items={actions}
isFetching={isActionsFetching}
isAllFetched={isAllActionsFetched}
canEdit={canEdit}
canEditAllComments={canEditAllCommentActions}
onFetch={onActionsFetch}
onCommentCreate={onCommentActionCreate}
onCommentUpdate={onCommentActionUpdate}
onCommentDelete={onCommentActionDelete}
/>
</Grid.Column>
{canEdit && (
<Grid.Column width={4} className={styles.sidebarPadding}>
<div className={styles.actions}>
<span className={styles.actionsTitle}>{t('action.addToCard')}</span>
<BoardMembershipsPopup
items={allBoardMemberships}
currentUserIds={userIds}
onUserSelect={onUserAdd}
onUserDeselect={onUserRemove}
>
<Button fluid className={styles.actionButton}>
<Icon name="user outline" className={styles.actionIcon} />
{t('common.members')}
</Button>
</BoardMembershipsPopup>
<LabelsPopup
items={allLabels}
currentIds={labelIds}
onSelect={onLabelAdd}
onDeselect={onLabelRemove}
onCreate={onLabelCreate}
onUpdate={onLabelUpdate}
onDelete={onLabelDelete}
>
<Button fluid className={styles.actionButton}>
<Icon name="bookmark outline" className={styles.actionIcon} />
{t('common.labels')}
</Button>
</LabelsPopup>
<DueDateEditPopup defaultValue={dueDate} onUpdate={handleDueDateUpdate}>
<Button fluid className={styles.actionButton}>
<Icon name="calendar check outline" className={styles.actionIcon} />
{t('common.dueDate', {
context: 'title',
})}
</Button>
</DueDateEditPopup>
<TimerEditPopup defaultValue={timer} onUpdate={handleTimerUpdate}>
<Button fluid className={styles.actionButton}>
<Icon name="clock outline" className={styles.actionIcon} />
{t('common.timer')}
</Button>
</TimerEditPopup>
<AttachmentAddPopup onCreate={onAttachmentCreate}>
<Button fluid className={styles.actionButton}>
<Icon name="attach" className={styles.actionIcon} />
{t('common.attachment')}
</Button>
</AttachmentAddPopup>
</div>
<div className={styles.actions}>
<span className={styles.actionsTitle}>{t('common.actions')}</span>
<Button
fluid
className={styles.actionButton}
onClick={handleToggleSubscriptionClick}
>
<Icon name="paper plane outline" className={styles.actionIcon} />
{isSubscribed ? t('action.unsubscribe') : t('action.subscribe')}
</Button>
<CardMovePopup
projectsToLists={allProjectsToLists}
defaultPath={{
projectId,
boardId,
listId,
}}
onMove={onMove}
onTransfer={onTransfer}
onBoardFetch={onBoardFetch}
>
<Button
fluid
className={styles.actionButton}
onClick={handleToggleSubscriptionClick}
>
<Icon name="paper plane outline" className={styles.actionIcon} />
{isSubscribed ? t('action.unsubscribe') : t('action.subscribe')}
<Icon name="share square outline" className={styles.actionIcon} />
{t('action.move')}
</Button>
<CardMovePopup
projectsToLists={allProjectsToLists}
defaultPath={{
projectId,
boardId,
listId,
}}
onMove={onMove}
onTransfer={onTransfer}
onBoardFetch={onBoardFetch}
>
<Button
fluid
className={styles.actionButton}
onClick={handleToggleSubscriptionClick}
>
<Icon name="share square outline" className={styles.actionIcon} />
{t('action.move')}
</Button>
</CardMovePopup>
<DeletePopup
title={t('common.deleteCard', {
context: 'title',
})}
content={t('common.areYouSureYouWantToDeleteThisCard')}
buttonContent={t('action.deleteCard')}
onConfirm={onDelete}
>
<Button fluid className={styles.actionButton}>
<Icon name="trash alternate outline" className={styles.actionIcon} />
{t('action.delete')}
</Button>
</DeletePopup>
</div>
</Grid.Column>
</Grid.Row>
</Grid>
</AttachmentAddZone>
</CardMovePopup>
<DeletePopup
title={t('common.deleteCard', {
context: 'title',
})}
content={t('common.areYouSureYouWantToDeleteThisCard')}
buttonContent={t('action.deleteCard')}
onConfirm={onDelete}
>
<Button fluid className={styles.actionButton}>
<Icon name="trash alternate outline" className={styles.actionIcon} />
{t('action.delete')}
</Button>
</DeletePopup>
</div>
</Grid.Column>
)}
</Grid.Row>
</Grid>
);
return (
<Modal open closeIcon size="small" centered={false} onClose={onClose}>
{canEdit ? (
<AttachmentAddZone onCreate={onAttachmentCreate}>{contentNode}</AttachmentAddZone>
) : (
contentNode
)}
</Modal>
);
},
@ -425,10 +474,11 @@ CardModal.propTypes = {
attachments: PropTypes.array.isRequired,
actions: PropTypes.array.isRequired,
allProjectsToLists: PropTypes.array.isRequired,
allProjectMemberships: PropTypes.array.isRequired,
allBoardMemberships: PropTypes.array.isRequired,
allLabels: PropTypes.array.isRequired,
/* eslint-enable react/forbid-prop-types */
isEditable: PropTypes.bool.isRequired,
canEdit: PropTypes.bool.isRequired,
canEditAllCommentActions: PropTypes.bool.isRequired,
onUpdate: PropTypes.func.isRequired,
onMove: PropTypes.func.isRequired,
onTransfer: PropTypes.func.isRequired,

View file

@ -44,7 +44,6 @@
}
.attachment {
cursor: pointer;
display: inline-block;
margin: 0 4px 4px 0;
max-width: 100%;
@ -70,6 +69,7 @@
border: none;
border-radius: 3px;
color: #6b808c;
cursor: pointer;
line-height: 20px;
outline: none;
padding: 6px 14px;
@ -141,6 +141,13 @@
}
.headerTitle {
color: #17394d;
font-size: 20px;
font-weight: 700;
line-height: 24px;
}
.headerTitleWrapper {
margin: 4px 0;
padding: 6px 0 0;
}
@ -193,8 +200,4 @@
margin: 0 8px 4px 0;
text-transform: uppercase;
}
/* .wrapper {
min-width: 768px;
} */
}

View file

@ -8,14 +8,14 @@ import ActionsPopup from './ActionsPopup';
import styles from './Item.module.scss';
const Item = React.memo(({ name, isCompleted, isPersisted, onUpdate, onDelete }) => {
const Item = React.memo(({ name, isCompleted, isPersisted, canEdit, onUpdate, onDelete }) => {
const nameEdit = useRef(null);
const handleClick = useCallback(() => {
if (isPersisted) {
if (isPersisted && canEdit) {
nameEdit.current.open();
}
}, [isPersisted]);
}, [isPersisted, canEdit]);
const handleNameUpdate = useCallback(
(newName) => {
@ -41,23 +41,26 @@ const Item = React.memo(({ name, isCompleted, isPersisted, onUpdate, onDelete })
<span className={styles.checkboxWrapper}>
<Checkbox
checked={isCompleted}
disabled={!isPersisted}
disabled={!isPersisted || !canEdit}
className={styles.checkbox}
onChange={handleToggleChange}
/>
</span>
<NameEdit ref={nameEdit} defaultValue={name} onUpdate={handleNameUpdate}>
<div className={styles.content}>
<div className={classNames(canEdit && styles.contentHoverable)}>
{/* eslint-disable jsx-a11y/click-events-have-key-events,
jsx-a11y/no-static-element-interactions */}
<span className={styles.text} onClick={handleClick}>
<span
className={classNames(styles.text, canEdit && styles.textEditable)}
onClick={handleClick}
>
{/* eslint-enable jsx-a11y/click-events-have-key-events,
jsx-a11y/no-static-element-interactions */}
<span className={classNames(styles.task, isCompleted && styles.taskCompleted)}>
{name}
</span>
</span>
{isPersisted && (
{isPersisted && canEdit && (
<ActionsPopup onNameEdit={handleNameEdit} onDelete={onDelete}>
<Button className={classNames(styles.button, styles.target)}>
<Icon fitted name="pencil" size="small" />
@ -74,6 +77,7 @@ Item.propTypes = {
name: PropTypes.string.isRequired,
isCompleted: PropTypes.bool.isRequired,
isPersisted: PropTypes.bool.isRequired,
canEdit: PropTypes.bool.isRequired,
onUpdate: PropTypes.func.isRequired,
onDelete: PropTypes.func.isRequired,
};

View file

@ -30,7 +30,7 @@
height: 32px;
}
.content:hover {
.contentHoverable:hover {
background: rgba(9, 30, 66, 0.04);
.target {
@ -55,7 +55,6 @@
background: transparent;
border-radius: 3px;
color: #17394d;
cursor: pointer;
display: inline-block;
font-size: 15px;
line-height: 1.5;
@ -64,6 +63,10 @@
width: 100%;
}
.textEditable {
cursor: pointer;
}
.wrapper {
border-radius: 3px;
margin-left: -40px;

View file

@ -8,7 +8,7 @@ import Add from './Add';
import styles from './Tasks.module.scss';
const Tasks = React.memo(({ items, onCreate, onUpdate, onDelete }) => {
const Tasks = React.memo(({ items, canEdit, onCreate, onUpdate, onDelete }) => {
const [t] = useTranslation();
const handleUpdate = useCallback(
@ -45,23 +45,27 @@ const Tasks = React.memo(({ items, onCreate, onUpdate, onDelete }) => {
name={item.name}
isCompleted={item.isCompleted}
isPersisted={item.isPersisted}
canEdit={canEdit}
onUpdate={(data) => handleUpdate(item.id, data)}
onDelete={() => handleDelete(item.id)}
/>
))}
<Add onCreate={onCreate}>
<button type="button" className={styles.taskButton}>
<span className={styles.taskButtonText}>
{items.length > 0 ? t('action.addAnotherTask') : t('action.addTask')}
</span>
</button>
</Add>
{canEdit && (
<Add onCreate={onCreate}>
<button type="button" className={styles.taskButton}>
<span className={styles.taskButtonText}>
{items.length > 0 ? t('action.addAnotherTask') : t('action.addTask')}
</span>
</button>
</Add>
)}
</>
);
});
Tasks.propTypes = {
items: PropTypes.array.isRequired, // eslint-disable-line react/forbid-prop-types
canEdit: PropTypes.bool.isRequired,
onCreate: PropTypes.func.isRequired,
onUpdate: PropTypes.func.isRequired,
onDelete: PropTypes.func.isRequired,

View file

@ -1,19 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Loader } from 'semantic-ui-react';
import CoreContainer from '../containers/CoreContainer';
import SocketStatusContainer from '../containers/SocketStatusContainer';
const CoreWrapper = React.memo(({ isInitializing }) => (
<>
{isInitializing ? <Loader active size="massive" /> : <CoreContainer />}
<SocketStatusContainer />
</>
));
CoreWrapper.propTypes = {
isInitializing: PropTypes.bool.isRequired,
};
export default CoreWrapper;

View file

@ -0,0 +1,37 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useTranslation, Trans } from 'react-i18next';
import { Loader } from 'semantic-ui-react';
import CoreContainer from '../../containers/CoreContainer';
import styles from './CoreWrapper.module.scss';
const CoreWrapper = React.memo(({ isInitializing, isSocketDisconnected }) => {
const [t] = useTranslation();
return (
<>
{isInitializing ? <Loader active size="massive" /> : <CoreContainer />}
{isSocketDisconnected && (
<div className={styles.message}>
<div className={styles.messageHeader}>{t('common.noConnectionToServer')}</div>
<div className={styles.messageContent}>
<Trans i18nKey="common.allChangesWillBeAutomaticallySavedAfterConnectionRestored">
All changes will be automatically saved
<br />
after connection restored
</Trans>
</div>
</div>
)}
</>
);
});
CoreWrapper.propTypes = {
isInitializing: PropTypes.bool.isRequired,
isSocketDisconnected: PropTypes.bool.isRequired,
};
export default CoreWrapper;

View file

@ -1,29 +1,5 @@
:global(#app) {
.button {
background: none;
border: none;
color: #fff;
cursor: pointer;
outline: none;
padding: 0;
text-decoration: underline;
}
.content {
color: #fff;
font-size: 16px;
line-height: 1.4;
}
.header {
color: #fff;
font-size: 24px;
font-weight: bold;
line-height: 1.2;
margin-bottom: 8px;
}
.wrapper {
.message {
background: #eb5a46;
border-radius: 4px;
bottom: 20px;
@ -34,4 +10,18 @@
width: 390px;
z-index: 10001;
}
.messageContent {
color: #fff;
font-size: 16px;
line-height: 1.4;
}
.messageHeader {
color: #fff;
font-size: 24px;
font-weight: bold;
line-height: 1.2;
margin-bottom: 8px;
}
}

View file

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

View file

@ -3,22 +3,26 @@ import PropTypes from 'prop-types';
import HeaderContainer from '../../containers/HeaderContainer';
import ProjectContainer from '../../containers/ProjectContainer';
import BoardActionsContainer from '../../containers/BoardActionsContainer';
import styles from './Fixed.module.scss';
const Fixed = ({ projectId }) => (
const Fixed = ({ projectId, board }) => (
<div className={styles.wrapper}>
<HeaderContainer />
{projectId && <ProjectContainer />}
{board && !board.isFetching && <BoardActionsContainer />}
</div>
);
Fixed.propTypes = {
projectId: PropTypes.string,
board: PropTypes.object, // eslint-disable-line react/forbid-prop-types
};
Fixed.defaultProps = {
projectId: undefined,
board: undefined,
};
export default Fixed;

View file

@ -1,5 +1,6 @@
import React from 'react';
import React, { useCallback } from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { Link } from 'react-router-dom';
import { Icon, Menu } from 'semantic-ui-react';
@ -11,52 +12,98 @@ import styles from './Header.module.scss';
const Header = React.memo(
({
project,
user,
notifications,
isEditable,
onUsers,
canEditProject,
canEditUsers,
onProjectSettingsClick,
onUsersClick,
onNotificationDelete,
onUserSettings,
onUserSettingsClick,
onLogout,
}) => (
<div className={styles.wrapper}>
<Link to={Paths.ROOT} className={styles.logo}>
Planka
</Link>
<Menu inverted size="large" className={styles.menu}>
<Menu.Menu position="right">
{isEditable && (
<Menu.Item className={styles.item} onClick={onUsers}>
<Icon fitted name="users" />
</Menu.Item>
}) => {
const handleProjectSettingsClick = useCallback(() => {
if (canEditProject) {
onProjectSettingsClick();
}
}, [canEditProject, onProjectSettingsClick]);
return (
<div className={styles.wrapper}>
{!project && (
<Link to={Paths.ROOT} className={classNames(styles.logo, styles.title)}>
Planka
</Link>
)}
<Menu inverted size="large" className={styles.menu}>
{project && (
<Menu.Menu position="left">
<Menu.Item
as={Link}
to={Paths.ROOT}
className={classNames(styles.item, styles.itemHoverable)}
>
<Icon fitted name="arrow left" />
</Menu.Item>
<Menu.Item
className={classNames(
styles.item,
canEditProject && styles.itemHoverable,
styles.title,
)}
onClick={handleProjectSettingsClick}
>
{project.name}
</Menu.Item>
</Menu.Menu>
)}
<NotificationsPopup items={notifications} onDelete={onNotificationDelete}>
<Menu.Item className={styles.item}>
<Icon fitted name="bell" />
{notifications.length > 0 && (
<span className={styles.notification}>{notifications.length}</span>
)}
</Menu.Item>
</NotificationsPopup>
<UserPopup onSettings={onUserSettings} onLogout={onLogout}>
<Menu.Item className={styles.item}>{user.name}</Menu.Item>
</UserPopup>
</Menu.Menu>
</Menu>
</div>
),
<Menu.Menu position="right">
{canEditUsers && (
<Menu.Item
className={classNames(styles.item, styles.itemHoverable)}
onClick={onUsersClick}
>
<Icon fitted name="users" />
</Menu.Item>
)}
<NotificationsPopup items={notifications} onDelete={onNotificationDelete}>
<Menu.Item className={classNames(styles.item, styles.itemHoverable)}>
<Icon fitted name="bell" />
{notifications.length > 0 && (
<span className={styles.notification}>{notifications.length}</span>
)}
</Menu.Item>
</NotificationsPopup>
<UserPopup onSettingsClick={onUserSettingsClick} onLogout={onLogout}>
<Menu.Item className={classNames(styles.item, styles.itemHoverable)}>
{user.name}
</Menu.Item>
</UserPopup>
</Menu.Menu>
</Menu>
</div>
);
},
);
Header.propTypes = {
/* eslint-disable react/forbid-prop-types */
project: PropTypes.object,
user: PropTypes.object.isRequired,
notifications: PropTypes.array.isRequired,
/* eslint-enable react/forbid-prop-types */
isEditable: PropTypes.bool.isRequired,
onUsers: PropTypes.func.isRequired,
canEditProject: PropTypes.bool.isRequired,
canEditUsers: PropTypes.bool.isRequired,
onProjectSettingsClick: PropTypes.func.isRequired,
onUsersClick: PropTypes.func.isRequired,
onNotificationDelete: PropTypes.func.isRequired,
onUserSettings: PropTypes.func.isRequired,
onUserSettingsClick: PropTypes.func.isRequired,
onLogout: PropTypes.func.isRequired,
};
Header.defaultProps = {
project: undefined,
};
export default Header;

View file

@ -1,19 +1,26 @@
:global(#app) {
.item {
cursor: auto;
user-select: auto;
&:before {
background: none;
}
&:hover {
background: rgba(0, 0, 0, 0.32);
&:active, &:hover {
background: transparent;
color: rgba(255, 255, 255, 0.9);
}
}
.itemHoverable:hover {
cursor: pointer;
background: rgba(0, 0, 0, 0.32);
}
.logo {
color: #fff;
flex: 0 0 auto;
font-size: 20px;
font-weight: bold;
letter-spacing: 3.5px;
line-height: 50px;
padding: 0 16px;
@ -54,6 +61,11 @@
width: 16px;
}
.title {
font-size: 20px;
font-weight: bold;
}
.wrapper {
background: rgba(0, 0, 0, 0.24);
display: flex;

View file

@ -65,13 +65,11 @@ const ActionsStep = React.memo(({ onNameEdit, onCardAdd, onDelete, onClose }) =>
context: 'title',
})}
</Menu.Item>
{onDelete && (
<Menu.Item className={styles.menuItem} onClick={handleDeleteClick}>
{t('action.deleteList', {
context: 'title',
})}
</Menu.Item>
)}
<Menu.Item className={styles.menuItem} onClick={handleDeleteClick}>
{t('action.deleteList', {
context: 'title',
})}
</Menu.Item>
</Menu>
</Popup.Content>
</>
@ -81,12 +79,8 @@ const ActionsStep = React.memo(({ onNameEdit, onCardAdd, onDelete, onClose }) =>
ActionsStep.propTypes = {
onNameEdit: PropTypes.func.isRequired,
onCardAdd: PropTypes.func.isRequired,
onDelete: PropTypes.func,
onDelete: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired,
};
ActionsStep.defaultProps = {
onDelete: undefined,
};
export default withPopup(ActionsStep);

View file

@ -15,7 +15,7 @@ import { ReactComponent as PlusMathIcon } from '../../assets/images/plus-math-ic
import styles from './List.module.scss';
const List = React.memo(
({ id, index, name, isPersisted, cardIds, onUpdate, onDelete, onCardCreate }) => {
({ id, index, name, isPersisted, cardIds, canEdit, onUpdate, onDelete, onCardCreate }) => {
const [t] = useTranslation();
const [isAddCardOpened, setIsAddCardOpened] = useState(false);
@ -23,10 +23,10 @@ const List = React.memo(
const listWrapper = useRef(null);
const handleHeaderClick = useCallback(() => {
if (isPersisted) {
if (isPersisted && canEdit) {
nameEdit.current.open();
}
}, [isPersisted]);
}, [isPersisted, canEdit]);
const handleNameUpdate = useCallback(
(newName) => {
@ -73,11 +73,13 @@ const List = React.memo(
<CardContainer key={cardId} id={cardId} index={cardIndex} />
))}
{placeholder}
<CardAdd
isOpened={isAddCardOpened}
onCreate={onCardCreate}
onClose={handleAddCardClose}
/>
{canEdit && (
<CardAdd
isOpened={isAddCardOpened}
onCreate={onCardCreate}
onClose={handleAddCardClose}
/>
)}
</div>
</div>
)}
@ -85,51 +87,64 @@ const List = React.memo(
);
return (
<Draggable draggableId={`list:${id}`} index={index} isDragDisabled={!isPersisted}>
<Draggable draggableId={`list:${id}`} index={index} isDragDisabled={!isPersisted || !canEdit}>
{({ innerRef, draggableProps, dragHandleProps }) => (
// eslint-disable-next-line react/jsx-props-no-spreading
<div {...draggableProps} data-drag-scroller ref={innerRef} className={styles.wrapper}>
<div
{...draggableProps} // eslint-disable-line react/jsx-props-no-spreading
data-drag-scroller
ref={innerRef}
className={styles.innerWrapper}
>
{/* eslint-disable jsx-a11y/click-events-have-key-events,
jsx-a11y/no-static-element-interactions,
react/jsx-props-no-spreading */}
<div {...dragHandleProps} className={styles.header} onClick={handleHeaderClick}>
{/* eslint-enable jsx-a11y/click-events-have-key-events,
<div className={styles.outerWrapper}>
<div
{...dragHandleProps}
className={classNames(styles.header, canEdit && styles.headerEditable)}
onClick={handleHeaderClick}
>
{/* eslint-enable jsx-a11y/click-events-have-key-events,
jsx-a11y/no-static-element-interactions,
react/jsx-props-no-spreading */}
<NameEdit ref={nameEdit} defaultValue={name} onUpdate={handleNameUpdate}>
<div className={styles.headerName}>{name}</div>
</NameEdit>
{isPersisted && (
<ActionsPopup
onNameEdit={handleNameEdit}
onCardAdd={handleCardAdd}
onDelete={onDelete}
<NameEdit ref={nameEdit} defaultValue={name} onUpdate={handleNameUpdate}>
<div className={styles.headerName}>{name}</div>
</NameEdit>
{isPersisted && canEdit && (
<ActionsPopup
onNameEdit={handleNameEdit}
onCardAdd={handleCardAdd}
onDelete={onDelete}
>
<Button className={classNames(styles.headerButton, styles.target)}>
<Icon fitted name="pencil" size="small" />
</Button>
</ActionsPopup>
)}
</div>
<div
ref={listWrapper}
className={classNames(
styles.cardsInnerWrapper,
(isAddCardOpened || !canEdit) && styles.cardsInnerWrapperFull,
)}
>
<div className={styles.cardsOuterWrapper}>{cardsNode}</div>
</div>
{!isAddCardOpened && canEdit && (
<button
type="button"
disabled={!isPersisted}
className={classNames(styles.addCardButton)}
onClick={handleAddCardClick}
>
<Button className={classNames(styles.headerButton, styles.target)}>
<Icon fitted name="pencil" size="small" />
</Button>
</ActionsPopup>
<PlusMathIcon className={styles.addCardButtonIcon} />
<span className={styles.addCardButtonText}>
{cardIds.length > 0 ? t('action.addAnotherCard') : t('action.addCard')}
</span>
</button>
)}
</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>
@ -143,13 +158,10 @@ List.propTypes = {
name: PropTypes.string.isRequired,
isPersisted: PropTypes.bool.isRequired,
cardIds: PropTypes.array.isRequired, // eslint-disable-line react/forbid-prop-types
canEdit: PropTypes.bool.isRequired,
onUpdate: PropTypes.func.isRequired,
onDelete: PropTypes.func,
onDelete: PropTypes.func.isRequired,
onCardCreate: PropTypes.func.isRequired,
};
List.defaultProps = {
onDelete: undefined,
};
export default List;

View file

@ -2,7 +2,6 @@
.addCardButton {
background: #dfe3e6;
border: none;
border-radius: 0 0 3px 3px;
color: #6b808c;
cursor: pointer;
display: block;
@ -37,76 +36,11 @@
}
.cards {
flex: 1 1 auto;
min-height: 1px;
}
.header {
background: #dfe3e6;
border-radius: 3px 3px 0 0;
box-sizing: none;
flex: 0 0 auto;
margin-bottom: 0;
outline: none;
padding: 6px 36px 4px 8px;
position: relative;
&:hover .target {
opacity: 1;
}
}
.headerButton {
background: none;
box-shadow: none;
color: #798d99;
line-height: 32px;
margin: 0;
opacity: 0;
padding: 0;
position: absolute;
right: 2px;
top: 2px;
width: 32px;
&:hover {
background: rgba(9, 30, 66, 0.13);
color: #516b7a;
}
}
.headerName {
background: transparent;
border-color: transparent;
border-radius: 3px;
color: #17394d;
font-weight: bold;
line-height: 20px;
margin: 0;
max-height: 256px;
outline: none;
overflow: hidden;
overflow-wrap: break-word;
padding: 4px 8px;
resize: none;
width: 100%;
word-break: break-word;
}
.list {
background: #dfe3e6;
box-sizing: border-box;
display: flex;
flex-direction: column;
padding: 0 8px;
position: relative;
white-space: normal;
width: 272px;
}
.listWrapper {
background: #dfe3e6;
max-height: calc(100vh - 300px);
.cardsInnerWrapper {
max-height: calc(100vh - 268px);
overflow-x: hidden;
overflow-y: auto;
scrollbar-width: thin;
@ -129,16 +63,69 @@
}
}
.listWrapperFull {
max-height: calc(100vh - 264px);
.cardsInnerWrapperFull {
max-height: calc(100vh - 232px);
}
.wrapper {
border-radius: 3px;
flex: 0 0 auto;
margin: 0 4px;
overflow: hidden;
vertical-align: top;
.cardsOuterWrapper {
padding: 0 8px;
white-space: normal;
width: 272px;
}
.header {
outline: none;
padding: 6px 36px 4px 8px;
position: relative;
&:hover .target {
opacity: 1;
}
}
.headerEditable {
cursor: pointer;
}
.headerButton {
background: none;
box-shadow: none;
color: #798d99;
line-height: 32px;
margin: 0;
opacity: 0;
padding: 0;
position: absolute;
right: 2px;
top: 2px;
width: 32px;
&:hover {
background: rgba(9, 30, 66, 0.13);
color: #516b7a;
}
}
.headerName {
color: #17394d;
font-weight: bold;
line-height: 20px;
max-height: 256px;
outline: none;
overflow: hidden;
overflow-wrap: break-word;
padding: 4px 8px;
word-break: break-word;
}
.innerWrapper {
margin-right: 8px;
width: 272px;
}
.outerWrapper {
background: #dfe3e6;
border-radius: 3px;
overflow: hidden;
}
}

View file

@ -0,0 +1,110 @@
import React, { useCallback } from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import { Button } from 'semantic-ui-react';
import { withPopup } from '../../lib/popup';
import { useSteps } from '../../hooks';
import User from '../User';
import DeleteStep from '../DeleteStep';
import styles from './ActionsPopup.module.scss';
const StepTypes = {
DELETE: 'DELETE',
};
const ActionsStep = React.memo(
({
user,
leaveButtonContent,
leaveConfirmationTitle,
leaveConfirmationContent,
leaveConfirmationButtonContent,
deleteButtonContent,
deleteConfirmationTitle,
deleteConfirmationContent,
deleteConfirmationButtonContent,
canLeave,
canDelete,
onDelete,
}) => {
const [t] = useTranslation();
const [step, openStep, handleBack] = useSteps();
const handleDeleteClick = useCallback(() => {
openStep(StepTypes.DELETE);
}, [openStep]);
if (step && step.type === StepTypes.DELETE) {
return (
<DeleteStep
title={t(user.isCurrent ? leaveConfirmationTitle : deleteConfirmationTitle, {
context: 'title',
})}
content={t(user.isCurrent ? leaveConfirmationContent : deleteConfirmationContent)}
buttonContent={t(
user.isCurrent ? leaveConfirmationButtonContent : deleteConfirmationButtonContent,
)}
onConfirm={onDelete}
onBack={handleBack}
/>
);
}
return (
<>
<span className={styles.user}>
<User name={user.name} avatarUrl={user.avatarUrl} size="large" />
</span>
<span className={styles.content}>
<div className={styles.name}>{user.name}</div>
<div className={styles.email}>{user.email}</div>
{user.isCurrent
? canLeave && (
<Button
content={t(leaveButtonContent)}
className={styles.deleteButton}
onClick={handleDeleteClick}
/>
)
: canDelete && (
<Button
content={t(deleteButtonContent)}
className={styles.deleteButton}
onClick={handleDeleteClick}
/>
)}
</span>
</>
);
},
);
ActionsStep.propTypes = {
user: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
leaveButtonContent: PropTypes.string,
leaveConfirmationTitle: PropTypes.string,
leaveConfirmationContent: PropTypes.string,
leaveConfirmationButtonContent: PropTypes.string,
deleteButtonContent: PropTypes.string,
deleteConfirmationTitle: PropTypes.string,
deleteConfirmationContent: PropTypes.string,
deleteConfirmationButtonContent: PropTypes.string,
canLeave: PropTypes.bool.isRequired,
canDelete: PropTypes.bool.isRequired,
onDelete: PropTypes.func.isRequired,
};
ActionsStep.defaultProps = {
leaveButtonContent: 'action.leaveBoard',
leaveConfirmationTitle: 'common.leaveBoard',
leaveConfirmationContent: 'common.areYouSureYouWantToLeaveBoard',
leaveConfirmationButtonContent: 'action.leaveBoard',
deleteButtonContent: 'action.removeFromBoard',
deleteConfirmationTitle: 'common.removeMember',
deleteConfirmationContent: 'common.areYouSureYouWantToRemoveThisMemberFromBoard',
deleteConfirmationButtonContent: 'action.removeMember',
};
export default withPopup(ActionsStep);

View file

@ -6,9 +6,9 @@ import { Popup } from '../../../lib/custom-ui';
import UserItem from './UserItem';
import styles from './MembershipAddPopup.module.scss';
import styles from './AddPopup.module.scss';
const MembershipAddStep = React.memo(({ users, currentUserIds, onCreate, onClose }) => {
const AddStep = React.memo(({ users, currentUserIds, title, onCreate, onClose }) => {
const [t] = useTranslation();
const handleUserSelect = useCallback(
@ -25,7 +25,7 @@ const MembershipAddStep = React.memo(({ users, currentUserIds, onCreate, onClose
return (
<>
<Popup.Header>
{t('common.addMember', {
{t(title, {
context: 'title',
})}
</Popup.Header>
@ -46,13 +46,18 @@ const MembershipAddStep = React.memo(({ users, currentUserIds, onCreate, onClose
);
});
MembershipAddStep.propTypes = {
AddStep.propTypes = {
/* eslint-disable react/forbid-prop-types */
users: PropTypes.array.isRequired,
currentUserIds: PropTypes.array.isRequired,
/* eslint-disable react/forbid-prop-types */
title: PropTypes.string,
onCreate: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired,
};
export default withPopup(MembershipAddStep);
AddStep.defaultProps = {
title: 'common.addMember',
};
export default withPopup(AddStep);

View file

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

View file

@ -0,0 +1,107 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Button } from 'semantic-ui-react';
import AddPopup from './AddPopup';
import ActionsPopup from './ActionsPopup';
import User from '../User';
import styles from './Memberships.module.scss';
const Memberships = React.memo(
({
items,
allUsers,
addTitle,
leaveButtonContent,
leaveConfirmationTitle,
leaveConfirmationContent,
leaveConfirmationButtonContent,
deleteButtonContent,
deleteConfirmationTitle,
deleteConfirmationContent,
deleteConfirmationButtonContent,
canEdit,
canLeaveIfLast,
onCreate,
onDelete,
}) => {
return (
<>
<span className={styles.users}>
{items.map((item) => (
<span key={item.id} className={styles.user}>
<ActionsPopup
user={item.user}
leaveButtonContent={leaveButtonContent}
leaveConfirmationTitle={leaveConfirmationTitle}
leaveConfirmationContent={leaveConfirmationContent}
leaveConfirmationButtonContent={leaveConfirmationButtonContent}
deleteButtonContent={deleteButtonContent}
deleteConfirmationTitle={deleteConfirmationTitle}
deleteConfirmationContent={deleteConfirmationContent}
deleteConfirmationButtonContent={deleteConfirmationButtonContent}
canLeave={items.length > 1 || canLeaveIfLast}
canDelete={canEdit}
onDelete={() => onDelete(item.id)}
>
<User
name={item.user.name}
avatarUrl={item.user.avatarUrl}
size="large"
isDisabled={!item.isPersisted}
/>
</ActionsPopup>
</span>
))}
</span>
{canEdit && (
<AddPopup
users={allUsers}
currentUserIds={items.map((item) => item.user.id)}
title={addTitle}
onCreate={onCreate}
>
<Button icon="add user" className={styles.addUser} />
</AddPopup>
)}
</>
);
},
);
Memberships.propTypes = {
/* eslint-disable react/forbid-prop-types */
items: PropTypes.array.isRequired,
allUsers: PropTypes.array.isRequired,
/* eslint-enable react/forbid-prop-types */
addTitle: PropTypes.string,
leaveButtonContent: PropTypes.string,
leaveConfirmationTitle: PropTypes.string,
leaveConfirmationContent: PropTypes.string,
leaveConfirmationButtonContent: PropTypes.string,
deleteButtonContent: PropTypes.string,
deleteConfirmationTitle: PropTypes.string,
deleteConfirmationContent: PropTypes.string,
deleteConfirmationButtonContent: PropTypes.string,
canEdit: PropTypes.bool,
canLeaveIfLast: PropTypes.bool,
onCreate: PropTypes.func.isRequired,
onDelete: PropTypes.func.isRequired,
};
Memberships.defaultProps = {
addTitle: undefined,
leaveButtonContent: undefined,
leaveConfirmationTitle: undefined,
leaveConfirmationContent: undefined,
leaveConfirmationButtonContent: undefined,
deleteButtonContent: undefined,
deleteConfirmationTitle: undefined,
deleteConfirmationContent: undefined,
deleteConfirmationButtonContent: undefined,
canEdit: true,
canLeaveIfLast: true,
};
export default Memberships;

View file

@ -0,0 +1,30 @@
:global(#app) {
.addUser {
background: rgba(0, 0, 0, 0.24);
border-radius: 50%;
box-shadow: none;
color: #fff;
line-height: 36px;
margin: 0;
padding: 0;
transition: all 0.1s ease 0s;
vertical-align: top;
width: 36px;
&:hover {
background: rgba(0, 0, 0, 0.32);
}
}
.user {
display: inline-block;
margin: 0 -4px 0 0;
vertical-align: top;
line-height: 0;
}
.users {
display: inline-block;
vertical-align: top;
}
}

View file

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

View file

@ -1,142 +0,0 @@
import React, { useCallback } from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import { Menu } from 'semantic-ui-react';
import { withPopup } from '../../../lib/popup';
import { Popup } from '../../../lib/custom-ui';
import { useSteps } from '../../../hooks';
import NameEditStep from './NameEditStep';
import BackgroundEditStep from './BackgroundEditStep';
import DeleteStep from '../../DeleteStep';
import styles from './ActionsPopup.module.scss';
const StepTypes = {
EDIT_NAME: 'EDIT_NAME',
EDIT_BACKGROUND: 'EDIT_BACKGROUND',
DELETE: 'DELETE',
};
const ActionsStep = React.memo(
({ project, onUpdate, onBackgroundImageUpdate, onDelete, onClose }) => {
const [t] = useTranslation();
const [step, openStep, handleBack] = useSteps();
const handleEditNameClick = useCallback(() => {
openStep(StepTypes.EDIT_NAME);
}, [openStep]);
const handleEditBackgroundClick = useCallback(() => {
openStep(StepTypes.EDIT_BACKGROUND);
}, [openStep]);
const handleDeleteClick = useCallback(() => {
openStep(StepTypes.DELETE);
}, [openStep]);
const handleNameUpdate = useCallback(
(newName) => {
onUpdate({
name: newName,
});
},
[onUpdate],
);
const handleBackgroundUpdate = useCallback(
(newBackground) => {
onUpdate({
background: newBackground,
});
},
[onUpdate],
);
const handleBackgroundImageDelete = useCallback(() => {
onUpdate({
backgroundImage: null,
});
}, [onUpdate]);
if (step) {
if (step) {
switch (step.type) {
case StepTypes.EDIT_NAME:
return (
<NameEditStep
defaultValue={project.name}
onUpdate={handleNameUpdate}
onBack={handleBack}
onClose={onClose}
/>
);
case StepTypes.EDIT_BACKGROUND:
return (
<BackgroundEditStep
defaultValue={project.background}
imageCoverUrl={project.backgroundImage && project.backgroundImage.coverUrl}
isImageUpdating={project.isBackgroundImageUpdating}
onUpdate={handleBackgroundUpdate}
onImageUpdate={onBackgroundImageUpdate}
onImageDelete={handleBackgroundImageDelete}
onBack={handleBack}
/>
);
case StepTypes.DELETE:
return (
<DeleteStep
title={t('common.deleteProject', {
context: 'title',
})}
content={t('common.areYouSureYouWantToDeleteThisProject')}
buttonContent={t('action.deleteProject')}
onConfirm={onDelete}
onBack={handleBack}
/>
);
default:
}
}
}
return (
<>
<Popup.Header>
{t('common.projectActions', {
context: 'title',
})}
</Popup.Header>
<Popup.Content>
<Menu secondary vertical className={styles.menu}>
<Menu.Item className={styles.menuItem} onClick={handleEditNameClick}>
{t('action.editTitle', {
context: 'title',
})}
</Menu.Item>
<Menu.Item className={styles.menuItem} onClick={handleEditBackgroundClick}>
{t('action.editBackground', {
context: 'title',
})}
</Menu.Item>
<Menu.Item className={styles.menuItem} onClick={handleDeleteClick}>
{t('action.deleteProject', {
context: 'title',
})}
</Menu.Item>
</Menu>
</Popup.Content>
</>
);
},
);
ActionsStep.propTypes = {
project: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
onUpdate: PropTypes.func.isRequired,
onBackgroundImageUpdate: PropTypes.func.isRequired,
onDelete: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired,
};
export default withPopup(ActionsStep);

View file

@ -1,11 +0,0 @@
:global(#app) {
.menu {
margin: -7px -12px -5px;
width: calc(100% + 24px);
}
.menuItem {
margin: 0;
padding-left: 14px;
}
}

View file

@ -1,176 +0,0 @@
import { dequal } from 'dequal';
import upperFirst from 'lodash/upperFirst';
import camelCase from 'lodash/camelCase';
import React, { useCallback, useEffect, useRef } from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { useTranslation } from 'react-i18next';
import { Button, Image } from 'semantic-ui-react';
import { FilePicker, Popup } from '../../../lib/custom-ui';
import ProjectBackgroundGradients from '../../../constants/ProjectBackgroundGradients';
import { ProjectBackgroundTypes } from '../../../constants/Enums';
import styles from './BackgroundEditStep.module.scss';
import globalStyles from '../../../styles.module.scss';
const BackgroundEditStep = React.memo(
({
defaultValue,
imageCoverUrl,
isImageUpdating,
onUpdate,
onImageUpdate,
onImageDelete,
onBack,
}) => {
const [t] = useTranslation();
const field = useRef(null);
const handleGradientClick = useCallback(
(_, { value }) => {
const background = {
type: ProjectBackgroundTypes.GRADIENT,
name: value,
};
if (!dequal(background, defaultValue)) {
onUpdate(background);
}
},
[defaultValue, onUpdate],
);
const handleImageClick = useCallback(() => {
const background = {
type: ProjectBackgroundTypes.IMAGE,
};
if (!dequal(background, defaultValue)) {
onUpdate(background);
}
}, [defaultValue, onUpdate]);
const handleFileSelect = useCallback(
(file) => {
onImageUpdate({
file,
});
},
[onImageUpdate],
);
const handleDeleteImageClick = useCallback(() => {
onImageDelete();
}, [onImageDelete]);
const handleRemoveClick = useCallback(() => {
onUpdate(null);
}, [onUpdate]);
useEffect(() => {
field.current.focus();
}, []);
return (
<>
<Popup.Header onBack={onBack}>
{t('common.editBackground', {
context: 'title',
})}
</Popup.Header>
<Popup.Content>
<div className={styles.gradientButtons}>
{ProjectBackgroundGradients.map((gradient) => (
<Button
key={gradient}
type="button"
name="gradient"
value={gradient}
className={classNames(
styles.gradientButton,
defaultValue &&
defaultValue.type === ProjectBackgroundTypes.GRADIENT &&
gradient === defaultValue.name &&
styles.gradientButtonActive,
globalStyles[`background${upperFirst(camelCase(gradient))}`],
)}
onClick={handleGradientClick}
/>
))}
</div>
{imageCoverUrl && (
/* TODO: wrap in button */
<Image
src={imageCoverUrl}
label={
defaultValue &&
defaultValue.type === 'image' && {
corner: 'left',
size: 'small',
icon: {
name: 'star',
color: 'grey',
inverted: true,
},
className: styles.imageLabel,
}
}
className={styles.image}
onClick={handleImageClick}
/>
)}
<div className={styles.action}>
<FilePicker accept="image/*" onSelect={handleFileSelect}>
<Button
ref={field}
content={t('action.uploadNewImage')}
loading={isImageUpdating}
disabled={isImageUpdating}
className={styles.actionButton}
/>
</FilePicker>
</div>
{imageCoverUrl && (
<div className={styles.action}>
<Button
content={t('action.deleteImage')}
disabled={isImageUpdating}
className={styles.actionButton}
onClick={handleDeleteImageClick}
/>
</div>
)}
{defaultValue && (
<div className={styles.action}>
<Button
content={t('action.removeBackground')}
disabled={isImageUpdating}
className={styles.actionButton}
onClick={handleRemoveClick}
/>
</div>
)}
</Popup.Content>
</>
);
},
);
BackgroundEditStep.propTypes = {
defaultValue: PropTypes.object, // eslint-disable-line react/forbid-prop-types
imageCoverUrl: PropTypes.string,
isImageUpdating: PropTypes.bool.isRequired,
onUpdate: PropTypes.func.isRequired,
onImageUpdate: PropTypes.func.isRequired,
onImageDelete: PropTypes.func.isRequired,
onBack: PropTypes.func.isRequired,
};
BackgroundEditStep.defaultProps = {
defaultValue: undefined,
imageCoverUrl: undefined,
};
export default BackgroundEditStep;

View file

@ -1,67 +0,0 @@
import React, { useCallback, useEffect, useRef } from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import { Button, Form } from 'semantic-ui-react';
import { Input, Popup } from '../../../lib/custom-ui';
import { useField } from '../../../hooks';
import styles from './NameEditStep.module.scss';
const NameEditStep = React.memo(({ defaultValue, onUpdate, onBack, onClose }) => {
const [t] = useTranslation();
const [value, handleFieldChange] = useField(defaultValue);
const field = useRef(null);
const handleSubmit = useCallback(() => {
const cleanValue = value.trim();
if (!cleanValue) {
field.current.select();
return;
}
if (cleanValue !== defaultValue) {
onUpdate(cleanValue);
}
onClose();
}, [defaultValue, onUpdate, onClose, value]);
useEffect(() => {
field.current.select();
}, []);
return (
<>
<Popup.Header onBack={onBack}>
{t('common.editTitle', {
context: 'title',
})}
</Popup.Header>
<Popup.Content>
<Form onSubmit={handleSubmit}>
<div className={styles.text}>{t('common.title')}</div>
<Input
fluid
ref={field}
value={value}
className={styles.field}
onChange={handleFieldChange}
/>
<Button positive content={t('action.save')} />
</Form>
</Popup.Content>
</>
);
});
NameEditStep.propTypes = {
defaultValue: PropTypes.string.isRequired,
onUpdate: PropTypes.func.isRequired,
onBack: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired,
};
export default NameEditStep;

View file

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

View file

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

View file

@ -1,65 +0,0 @@
import React, { useCallback } from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import { Button } from 'semantic-ui-react';
import { withPopup } from '../../lib/popup';
import { useSteps } from '../../hooks';
import User from '../User';
import DeleteStep from '../DeleteStep';
import styles from './MembershipEditPopup.module.scss';
const StepTypes = {
DELETE: 'DELETE',
};
const MembershipEditStep = React.memo(({ user, isEditable, onDelete }) => {
const [t] = useTranslation();
const [step, openStep, handleBack] = useSteps();
const handleDeleteClick = useCallback(() => {
openStep(StepTypes.DELETE);
}, [openStep]);
if (step && step.type === StepTypes.DELETE) {
return (
<DeleteStep
title={t('common.removeMember', {
context: 'title',
})}
content={t('common.areYouSureYouWantToRemoveThisMemberFromProject')}
buttonContent={t('action.removeMember')}
onConfirm={onDelete}
onBack={handleBack}
/>
);
}
return (
<>
<span className={styles.user}>
<User name={user.name} avatarUrl={user.avatarUrl} size="large" />
</span>
<span className={styles.content}>
<div className={styles.name}>{user.name}</div>
<div className={styles.email}>{user.email}</div>
{!user.isCurrent && isEditable && (
<Button
content={t('action.removeFromProject')}
className={styles.deleteButton}
onClick={handleDeleteClick}
/>
)}
</span>
</>
);
});
MembershipEditStep.propTypes = {
user: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
isEditable: PropTypes.bool.isRequired,
onDelete: PropTypes.func.isRequired,
};
export default withPopup(MembershipEditStep);

View file

@ -1,123 +1,24 @@
import React, { useCallback, useEffect } from 'react';
import React from 'react';
import PropTypes from 'prop-types';
import { Button, Grid } from 'semantic-ui-react';
import BoardsContainer from '../../containers/BoardsContainer';
import ActionsPopup from './ActionsPopup';
import MembershipAddPopup from './MembershipAddPopup';
import MembershipEditPopup from './MembershipEditPopup';
import User from '../User';
import ProjectSettingsModalContainer from '../../containers/ProjectSettingsModalContainer';
import styles from './Project.module.scss';
const Project = React.memo(
({
name,
background,
backgroundImage,
isBackgroundImageUpdating,
memberships,
allUsers,
isEditable,
onUpdate,
onBackgroundImageUpdate,
onDelete,
onMembershipCreate,
onMembershipDelete,
}) => {
const handleMembershipDelete = useCallback(
(id) => {
onMembershipDelete(id);
},
[onMembershipDelete],
);
useEffect(() => {
return () => {
document.body.style.background = null;
};
}, []);
return (
const Project = React.memo(({ isSettingsModalOpened }) => {
return (
<>
<div className={styles.wrapper}>
<Grid className={styles.header}>
<Grid.Row>
<Grid.Column>
{isEditable ? (
<ActionsPopup
project={{
name,
background,
backgroundImage,
isBackgroundImageUpdating,
}}
onUpdate={onUpdate}
onBackgroundImageUpdate={onBackgroundImageUpdate}
onDelete={onDelete}
>
<Button content={name} disabled={!isEditable} className={styles.name} />
</ActionsPopup>
) : (
<span className={styles.name}>{name}</span>
)}
<span className={styles.users}>
{memberships.map((membership) => (
<span key={membership.id} className={styles.user}>
<MembershipEditPopup
user={membership.user}
isEditable={isEditable}
onDelete={() => handleMembershipDelete(membership.id)}
>
<User
name={membership.user.name}
avatarUrl={membership.user.avatarUrl}
size="large"
isDisabled={!membership.isPersisted}
/>
</MembershipEditPopup>
</span>
))}
</span>
{isEditable && (
<MembershipAddPopup
users={allUsers}
currentUserIds={memberships.map((membership) => membership.user.id)}
onCreate={onMembershipCreate}
>
<Button icon="add user" className={styles.addUser} />
</MembershipAddPopup>
)}
</Grid.Column>
</Grid.Row>
</Grid>
<BoardsContainer />
</div>
);
},
);
{isSettingsModalOpened && <ProjectSettingsModalContainer />}
</>
);
});
Project.propTypes = {
name: PropTypes.string.isRequired,
/* eslint-disable react/forbid-prop-types */
background: PropTypes.object,
backgroundImage: PropTypes.object,
/* eslint-enable react/forbid-prop-types */
isBackgroundImageUpdating: PropTypes.bool.isRequired,
/* eslint-disable react/forbid-prop-types */
memberships: PropTypes.array.isRequired,
allUsers: PropTypes.array.isRequired,
/* eslint-enable react/forbid-prop-types */
isEditable: PropTypes.bool.isRequired,
onUpdate: PropTypes.func.isRequired,
onBackgroundImageUpdate: PropTypes.func.isRequired,
onDelete: PropTypes.func.isRequired,
onMembershipCreate: PropTypes.func.isRequired,
onMembershipDelete: PropTypes.func.isRequired,
};
Project.defaultProps = {
background: undefined,
backgroundImage: undefined,
isSettingsModalOpened: PropTypes.bool.isRequired,
};
export default Project;

View file

@ -1,56 +1,9 @@
:global(#app) {
.addUser {
background: rgba(0, 0, 0, 0.24);
border-radius: 50%;
box-shadow: none;
color: #fff;
line-height: 36px;
margin: 0;
padding: 0;
transition: all 0.1s ease 0s;
vertical-align: top;
width: 36px;
&:hover {
background: rgba(0, 0, 0, 0.32);
}
}
.header {
flex: 0 0 auto;
margin: 0 -1rem;
}
.name {
background: transparent;
box-shadow: none;
color: #fff;
display: inline-block;
font-size: 32px;
font-weight: bold;
line-height: 36px;
margin-right: 8px;
padding: 0;
}
.user {
display: inline-block;
margin: 0 -4px 0 0;
vertical-align: top;
line-height: 0;
}
.users {
display: inline-block;
margin-left: 8px;
vertical-align: top;
}
.wrapper {
background: rgba(0, 0, 0, 0.16);
display: flex;
flex-direction: column;
height: 100%;
padding: 0 20px;
padding: 10px 20px 0;
}
}

View file

@ -1,5 +0,0 @@
import { withPopup } from '../lib/popup';
import ProjectMembershipsStep from './ProjectMembershipsStep';
export default withPopup(ProjectMembershipsStep);

View file

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

View file

@ -0,0 +1,168 @@
import { dequal } from 'dequal';
import upperFirst from 'lodash/upperFirst';
import camelCase from 'lodash/camelCase';
import React, { useCallback, useEffect, useRef } from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { useTranslation } from 'react-i18next';
import { Button, Image } from 'semantic-ui-react';
import { FilePicker } from '../../lib/custom-ui';
import ProjectBackgroundGradients from '../../constants/ProjectBackgroundGradients';
import { ProjectBackgroundTypes } from '../../constants/Enums';
import styles from './BackgroundPane.module.scss';
import globalStyles from '../../styles.module.scss';
const BackgroundPane = React.memo(
({ item, imageCoverUrl, isImageUpdating, onUpdate, onImageUpdate, onImageDelete }) => {
const [t] = useTranslation();
const field = useRef(null);
const handleGradientClick = useCallback(
(_, { value }) => {
const background = {
type: ProjectBackgroundTypes.GRADIENT,
name: value,
};
if (!dequal(background, item)) {
onUpdate(background);
}
},
[item, onUpdate],
);
const handleImageClick = useCallback(() => {
const background = {
type: ProjectBackgroundTypes.IMAGE,
};
if (!dequal(background, item)) {
onUpdate(background);
}
}, [item, onUpdate]);
const handleFileSelect = useCallback(
(file) => {
onImageUpdate({
file,
});
},
[onImageUpdate],
);
const handleDeleteImageClick = useCallback(() => {
onImageDelete();
}, [onImageDelete]);
const handleRemoveClick = useCallback(() => {
onUpdate(null);
}, [onUpdate]);
useEffect(() => {
field.current.focus();
}, []);
return (
<>
<div className={styles.gradientButtons}>
{ProjectBackgroundGradients.map((gradient) => (
<Button
key={gradient}
type="button"
name="gradient"
value={gradient}
className={classNames(
styles.gradientButton,
item &&
item.type === ProjectBackgroundTypes.GRADIENT &&
gradient === item.name &&
styles.gradientButtonActive,
globalStyles[`background${upperFirst(camelCase(gradient))}`],
)}
onClick={handleGradientClick}
/>
))}
</div>
{imageCoverUrl && (
// TODO: wrap in button
<Image
src={imageCoverUrl}
label={
item &&
item.type === 'image' && {
corner: 'left',
size: 'small',
icon: {
name: 'star',
color: 'grey',
inverted: true,
},
className: styles.imageLabel,
}
}
className={styles.image}
onClick={handleImageClick}
/>
)}
<div className={styles.actions}>
<div className={styles.action}>
<FilePicker accept="image/*" onSelect={handleFileSelect}>
<Button
ref={field}
content={t('action.uploadNewImage', {
context: 'title',
})}
loading={isImageUpdating}
disabled={isImageUpdating}
className={styles.actionButton}
/>
</FilePicker>
</div>
{imageCoverUrl && (
<div className={styles.action}>
<Button
content={t('action.deleteImage', {
context: 'title',
})}
disabled={isImageUpdating}
className={styles.actionButton}
onClick={handleDeleteImageClick}
/>
</div>
)}
{item && (
<div className={styles.action}>
<Button
content={t('action.removeBackground', {
context: 'title',
})}
disabled={isImageUpdating}
className={styles.actionButton}
onClick={handleRemoveClick}
/>
</div>
)}
</div>
</>
);
},
);
BackgroundPane.propTypes = {
item: PropTypes.object, // eslint-disable-line react/forbid-prop-types
imageCoverUrl: PropTypes.string,
isImageUpdating: PropTypes.bool.isRequired,
onUpdate: PropTypes.func.isRequired,
onImageUpdate: PropTypes.func.isRequired,
onImageDelete: PropTypes.func.isRequired,
};
BackgroundPane.defaultProps = {
item: undefined,
imageCoverUrl: undefined,
};
export default BackgroundPane;

View file

@ -13,6 +13,10 @@
}
}
.actions {
margin-top: 20px;
}
.actionButton {
background: transparent;
color: #6b808c;
@ -54,8 +58,7 @@
}
.gradientButtons {
margin: -4px;
padding-bottom: 8px;
margin-left: -4px;
&:after {
content: "";
@ -66,7 +69,7 @@
.image {
cursor: pointer;
margin-bottom: 8px;
margin-top: 10px;
&:hover {
opacity: 0.9;

View file

@ -0,0 +1,55 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import { Button, Divider, Header, Tab } from 'semantic-ui-react';
import InformationEdit from './InformationEdit';
import DeletePopup from '../../DeletePopup';
import styles from './GeneralPane.module.scss';
const GeneralPane = React.memo(({ name, onUpdate, onDelete }) => {
const [t] = useTranslation();
return (
<Tab.Pane attached={false} className={styles.wrapper}>
<InformationEdit
defaultData={{
name,
}}
onUpdate={onUpdate}
/>
<Divider horizontal section>
<Header as="h4">
{t('common.dangerZone', {
context: 'title',
})}
</Header>
</Divider>
<div className={styles.action}>
<DeletePopup
title={t('common.deleteProject', {
context: 'title',
})}
content={t('common.areYouSureYouWantToDeleteThisProject')}
buttonContent={t('action.deleteProject')}
onConfirm={onDelete}
>
<Button className={styles.actionButton}>
{t('action.deleteProject', {
context: 'title',
})}
</Button>
</DeletePopup>
</div>
</Tab.Pane>
);
});
GeneralPane.propTypes = {
name: PropTypes.string.isRequired,
onUpdate: PropTypes.func.isRequired,
onDelete: PropTypes.func.isRequired,
};
export default GeneralPane;

View file

@ -0,0 +1,32 @@
:global(#app) {
.action {
border: none;
display: inline-block;
height: 36px;
overflow: hidden;
position: relative;
transition: background 0.3s ease;
width: 100%;
&:hover {
background: #e9e9e9;
}
}
.actionButton {
background: transparent;
color: #6b808c;
font-weight: normal;
height: 36px;
line-height: 24px;
padding: 6px 12px;
text-align: left;
text-decoration: underline;
width: 100%;
}
.wrapper {
border: none;
box-shadow: none;
}
}

View file

@ -0,0 +1,60 @@
import { dequal } from 'dequal';
import pickBy from 'lodash/pickBy';
import React, { useCallback, useMemo, useRef } from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import { Button, Form, Input } from 'semantic-ui-react';
import { useForm } from '../../../hooks';
import styles from './InformationEdit.module.scss';
const InformationEdit = React.memo(({ defaultData, onUpdate }) => {
const [t] = useTranslation();
const [data, handleFieldChange] = useForm(() => ({
name: '',
...pickBy(defaultData),
}));
const cleanData = useMemo(
() => ({
...data,
name: data.name.trim(),
}),
[data],
);
const nameField = useRef(null);
const handleSubmit = useCallback(() => {
if (!cleanData.name) {
nameField.current.select();
return;
}
onUpdate(cleanData);
}, [onUpdate, cleanData]);
return (
<Form onSubmit={handleSubmit}>
<div className={styles.text}>{t('common.title')}</div>
<Input
fluid
ref={nameField}
name="name"
value={data.name}
className={styles.field}
onChange={handleFieldChange}
/>
<Button positive disabled={dequal(cleanData, defaultData)} content={t('action.save')} />
</Form>
);
});
InformationEdit.propTypes = {
defaultData: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
onUpdate: PropTypes.func.isRequired,
};
export default InformationEdit;

View file

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

View file

@ -0,0 +1,41 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Tab } from 'semantic-ui-react';
import Memberships from '../Memberships';
import styles from './ManagersPane.module.scss';
const ManagersPane = React.memo(({ items, allUsers, onCreate, onDelete }) => {
return (
<Tab.Pane attached={false} className={styles.wrapper}>
<Memberships
items={items}
allUsers={allUsers}
addTitle="common.addManager"
leaveButtonContent="action.leaveProject"
leaveConfirmationTitle="common.leaveProject"
leaveConfirmationContent="common.areYouSureYouWantToLeaveProject"
leaveConfirmationButtonContent="action.leaveProject"
deleteButtonContent="action.removeFromProject"
deleteConfirmationTitle="common.removeManager"
deleteConfirmationContent="common.areYouSureYouWantToRemoveThisManagerFromProject"
deleteConfirmationButtonContent="action.removeManager"
canLeaveIfLast={false}
onCreate={onCreate}
onDelete={onDelete}
/>
</Tab.Pane>
);
});
ManagersPane.propTypes = {
/* eslint-disable react/forbid-prop-types */
items: PropTypes.array.isRequired,
allUsers: PropTypes.array.isRequired,
/* eslint-enable react/forbid-prop-types */
onCreate: PropTypes.func.isRequired,
onDelete: PropTypes.func.isRequired,
};
export default ManagersPane;

View file

@ -0,0 +1,6 @@
:global(#app) {
.wrapper {
border: none;
box-shadow: none;
}
}

View file

@ -0,0 +1,119 @@
import React, { useCallback } from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import { Modal, Tab } from 'semantic-ui-react';
import ManagersPane from './ManagersPane';
import BackgroundPane from './BackgroundPane';
import GeneralPane from './GeneralPane';
const ProjectSettingsModal = React.memo(
({
name,
background,
backgroundImage,
isBackgroundImageUpdating,
managers,
allUsers,
onUpdate,
onBackgroundImageUpdate,
onDelete,
onManagerCreate,
onManagerDelete,
onClose,
}) => {
const [t] = useTranslation();
const handleBackgroundUpdate = useCallback(
(newBackground) => {
onUpdate({
background: newBackground,
});
},
[onUpdate],
);
const handleBackgroundImageDelete = useCallback(() => {
onUpdate({
backgroundImage: null,
});
}, [onUpdate]);
const panes = [
{
menuItem: t('common.general', {
context: 'title',
}),
render: () => <GeneralPane name={name} onUpdate={onUpdate} onDelete={onDelete} />,
},
{
menuItem: t('common.managers', {
context: 'title',
}),
render: () => (
<ManagersPane
items={managers}
allUsers={allUsers}
onCreate={onManagerCreate}
onDelete={onManagerDelete}
/>
),
},
{
menuItem: t('common.background', {
context: 'title',
}),
render: () => (
<BackgroundPane
item={background}
imageCoverUrl={backgroundImage && backgroundImage.coverUrl}
isImageUpdating={isBackgroundImageUpdating}
onUpdate={handleBackgroundUpdate}
onImageUpdate={onBackgroundImageUpdate}
onImageDelete={handleBackgroundImageDelete}
/>
),
},
];
return (
<Modal open closeIcon size="small" centered={false} onClose={onClose}>
<Modal.Content>
<Tab
menu={{
secondary: true,
pointing: true,
}}
panes={panes}
/>
</Modal.Content>
</Modal>
);
},
);
ProjectSettingsModal.propTypes = {
name: PropTypes.string.isRequired,
/* eslint-disable react/forbid-prop-types */
background: PropTypes.object,
backgroundImage: PropTypes.object,
/* eslint-enable react/forbid-prop-types */
isBackgroundImageUpdating: PropTypes.bool.isRequired,
/* eslint-disable react/forbid-prop-types */
managers: PropTypes.array.isRequired,
allUsers: PropTypes.array.isRequired,
/* eslint-enable react/forbid-prop-types */
onUpdate: PropTypes.func.isRequired,
onBackgroundImageUpdate: PropTypes.func.isRequired,
onDelete: PropTypes.func.isRequired,
onManagerCreate: PropTypes.func.isRequired,
onManagerDelete: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired,
};
ProjectSettingsModal.defaultProps = {
background: undefined,
backgroundImage: undefined,
};
export default ProjectSettingsModal;

View file

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

View file

@ -14,7 +14,7 @@ import { ReactComponent as PlusIcon } from '../../assets/images/plus-icon.svg';
import styles from './Projects.module.scss';
import globalStyles from '../../styles.module.scss';
const Projects = React.memo(({ items, isEditable, onAdd }) => {
const Projects = React.memo(({ items, canAdd, onAdd }) => {
const [t] = useTranslation();
return (
@ -53,7 +53,7 @@ const Projects = React.memo(({ items, isEditable, onAdd }) => {
</Link>
</Grid.Column>
))}
{isEditable && (
{canAdd && (
<Grid.Column mobile={8} computer={4}>
<button type="button" className={classNames(styles.card, styles.add)} onClick={onAdd}>
<div className={styles.addTitleWrapper}>
@ -72,7 +72,7 @@ const Projects = React.memo(({ items, isEditable, onAdd }) => {
Projects.propTypes = {
items: PropTypes.array.isRequired, // eslint-disable-line react/forbid-prop-types
isEditable: PropTypes.bool.isRequired,
canAdd: PropTypes.bool.isRequired,
onAdd: PropTypes.func.isRequired,
};

View file

@ -1,59 +0,0 @@
import React, { useCallback } from 'react';
import PropTypes from 'prop-types';
import { useTranslation, Trans } from 'react-i18next';
import styles from './SocketStatus.module.scss';
const SocketStatus = React.memo(({ isDisconnected, isReconnected }) => {
const [t] = useTranslation();
const handleReloadClick = useCallback(() => {
window.location.reload(true);
}, []);
if (isDisconnected) {
return (
<div className={styles.wrapper}>
<div className={styles.header}>{t('common.noConnectionToServer')}</div>
<div className={styles.content}>
<Trans i18nKey="common.allChangesWillBeAutomaticallySavedAfterConnectionRestored">
All changes will be automatically saved
<br />
after connection restored
</Trans>
</div>
</div>
);
}
if (isReconnected) {
return (
<div className={styles.wrapper}>
<div className={styles.header}>
{t('common.connectionRestored', {
context: 'title',
})}
</div>
<div className={styles.content}>
<Trans i18nKey="common.refreshPageToLoadLastDataAndReceiveUpdates">
<button type="button" className={styles.button} onClick={handleReloadClick}>
Refresh the page
</button>
to load last data
<br />
and receive updates
</Trans>
</div>
</div>
);
}
return null;
});
SocketStatus.propTypes = {
isDisconnected: PropTypes.bool.isRequired,
isReconnected: PropTypes.bool.isRequired,
};
export default SocketStatus;

View file

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

View file

@ -1,6 +1,6 @@
:global(#app) {
.board {
margin-top: 168px;
margin-top: 174px;
}
.flex {

View file

@ -31,7 +31,7 @@
/* Sizes */
.wrapperTiny {
font-size: 12px;
font-size: 10px;
font-weight: 400;
height: 24px;
line-height: 20px;

View file

@ -7,13 +7,13 @@ import { Popup } from '../../lib/custom-ui';
import styles from './UserPopup.module.scss';
const UserStep = React.memo(({ onSettings, onLogout, onClose }) => {
const UserStep = React.memo(({ onSettingsClick, onLogout, onClose }) => {
const [t] = useTranslation();
const handleSettingsClick = useCallback(() => {
onSettings();
onSettingsClick();
onClose();
}, [onSettings, onClose]);
}, [onSettingsClick, onClose]);
return (
<>
@ -41,7 +41,7 @@ const UserStep = React.memo(({ onSettings, onLogout, onClose }) => {
});
UserStep.propTypes = {
onSettings: PropTypes.func.isRequired,
onSettingsClick: PropTypes.func.isRequired,
onLogout: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired,
};