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:
parent
7956503a46
commit
fe91b5241e
478 changed files with 21226 additions and 19495 deletions
78
client/src/components/BoardActions/BoardActions.jsx
Normal file
78
client/src/components/BoardActions/BoardActions.jsx
Normal 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;
|
11
client/src/components/BoardActions/BoardActions.module.scss
Normal file
11
client/src/components/BoardActions/BoardActions.module.scss
Normal file
|
@ -0,0 +1,11 @@
|
|||
:global(#app) {
|
||||
.action {
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
margin: 20px 20px;
|
||||
}
|
||||
}
|
|
@ -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;
|
|
@ -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;
|
||||
}
|
||||
}
|
3
client/src/components/BoardActions/index.js
Normal file
3
client/src/components/BoardActions/index.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
import BoardActions from './BoardActions';
|
||||
|
||||
export default BoardActions;
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
5
client/src/components/BoardMembershipsPopup.jsx
Normal file
5
client/src/components/BoardMembershipsPopup.jsx
Normal file
|
@ -0,0 +1,5 @@
|
|||
import { withPopup } from '../lib/popup';
|
||||
|
||||
import BoardMembershipsStep from './BoardMembershipsStep';
|
||||
|
||||
export default withPopup(BoardMembershipsStep);
|
|
@ -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;
|
3
client/src/components/BoardMembershipsStep/index.js
Executable file
3
client/src/components/BoardMembershipsStep/index.js
Executable file
|
@ -0,0 +1,3 @@
|
|||
import BoardMembershipsStep from './BoardMembershipsStep';
|
||||
|
||||
export default BoardMembershipsStep;
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
position: absolute;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
z-index: 1;
|
||||
z-index: 2000;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
} */
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
37
client/src/components/CoreWrapper/CoreWrapper.jsx
Executable file
37
client/src/components/CoreWrapper/CoreWrapper.jsx
Executable 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;
|
|
@ -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;
|
||||
}
|
||||
}
|
3
client/src/components/CoreWrapper/index.js
Normal file
3
client/src/components/CoreWrapper/index.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
import CoreWrapper from './CoreWrapper';
|
||||
|
||||
export default CoreWrapper;
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
110
client/src/components/Memberships/ActionsPopup.jsx
Executable file
110
client/src/components/Memberships/ActionsPopup.jsx
Executable 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);
|
|
@ -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);
|
3
client/src/components/Memberships/AddPopup/index.js
Normal file
3
client/src/components/Memberships/AddPopup/index.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
import AddPopup from './AddPopup';
|
||||
|
||||
export default AddPopup;
|
107
client/src/components/Memberships/Memberships.jsx
Normal file
107
client/src/components/Memberships/Memberships.jsx
Normal 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;
|
30
client/src/components/Memberships/Memberships.module.scss
Normal file
30
client/src/components/Memberships/Memberships.module.scss
Normal 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;
|
||||
}
|
||||
}
|
3
client/src/components/Memberships/index.js
Normal file
3
client/src/components/Memberships/index.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
import Memberships from './Memberships';
|
||||
|
||||
export default Memberships;
|
|
@ -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);
|
|
@ -1,11 +0,0 @@
|
|||
:global(#app) {
|
||||
.menu {
|
||||
margin: -7px -12px -5px;
|
||||
width: calc(100% + 24px);
|
||||
}
|
||||
|
||||
.menuItem {
|
||||
margin: 0;
|
||||
padding-left: 14px;
|
||||
}
|
||||
}
|
|
@ -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;
|
|
@ -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;
|
|
@ -1,3 +0,0 @@
|
|||
import ActionsPopup from './ActionsPopup';
|
||||
|
||||
export default ActionsPopup;
|
|
@ -1,3 +0,0 @@
|
|||
import MembershipAddPopup from './MembershipAddPopup';
|
||||
|
||||
export default MembershipAddPopup;
|
|
@ -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);
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
import { withPopup } from '../lib/popup';
|
||||
|
||||
import ProjectMembershipsStep from './ProjectMembershipsStep';
|
||||
|
||||
export default withPopup(ProjectMembershipsStep);
|
|
@ -1,3 +0,0 @@
|
|||
import ProjectMembershipsStep from './ProjectMembershipsStep';
|
||||
|
||||
export default ProjectMembershipsStep;
|
168
client/src/components/ProjectSettingsModal/BackgroundPane.jsx
Normal file
168
client/src/components/ProjectSettingsModal/BackgroundPane.jsx
Normal 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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
|
@ -0,0 +1,3 @@
|
|||
import GeneralPane from './GeneralPane';
|
||||
|
||||
export default GeneralPane;
|
41
client/src/components/ProjectSettingsModal/ManagersPane.jsx
Normal file
41
client/src/components/ProjectSettingsModal/ManagersPane.jsx
Normal 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;
|
|
@ -0,0 +1,6 @@
|
|||
:global(#app) {
|
||||
.wrapper {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
|
@ -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;
|
3
client/src/components/ProjectSettingsModal/index.js
Normal file
3
client/src/components/ProjectSettingsModal/index.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
import ProjectSettingsModal from './ProjectSettingsModal';
|
||||
|
||||
export default ProjectSettingsModal;
|
|
@ -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,
|
||||
};
|
||||
|
||||
|
|
|
@ -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;
|
|
@ -1,3 +0,0 @@
|
|||
import SocketStatus from './SocketStatus';
|
||||
|
||||
export default SocketStatus;
|
|
@ -1,6 +1,6 @@
|
|||
:global(#app) {
|
||||
.board {
|
||||
margin-top: 168px;
|
||||
margin-top: 174px;
|
||||
}
|
||||
|
||||
.flex {
|
||||
|
|
|
@ -31,7 +31,7 @@
|
|||
/* Sizes */
|
||||
|
||||
.wrapperTiny {
|
||||
font-size: 12px;
|
||||
font-size: 10px;
|
||||
font-weight: 400;
|
||||
height: 24px;
|
||||
line-height: 20px;
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue