pull/705/merge
Jens Frost 2 years ago committed by GitHub
commit aae1a21c2c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -121,6 +121,33 @@ const handleCardDelete = (card) => ({
}, },
}); });
const copyCard = (id) => ({
type: ActionTypes.CARD_COPY,
payload: {
id,
},
});
copyCard.success = (card) => ({
type: ActionTypes.CARD_COPY__SUCCESS,
payload: {
card,
},
});
copyCard.failure = (id, error) => ({
type: ActionTypes.CARD_COPY__FAILURE,
payload: {
id,
error,
},
});
const handleCardCopy = (card) => ({
type: ActionTypes.CARD_COPY_HANDLE,
payload: {
card,
const filterText = (boardId, text) => ({ const filterText = (boardId, text) => ({
type: ActionTypes.TEXT_FILTER_IN_CURRENT_BOARD, type: ActionTypes.TEXT_FILTER_IN_CURRENT_BOARD,
payload: { payload: {
@ -137,5 +164,7 @@ export default {
duplicateCard, duplicateCard,
deleteCard, deleteCard,
handleCardDelete, handleCardDelete,
copyCard,
handleCardCopy,
filterText, filterText,
}; };

@ -121,6 +121,35 @@ const handleListDelete = (list) => ({
}, },
}); });
const sortList = (id) => ({
type: ActionTypes.LIST_SORT,
payload: {
id,
},
});
sortList.success = (list) => ({
type: ActionTypes.LIST_SORT__SUCCESS,
payload: {
list,
},
});
sortList.failure = (id, error) => ({
type: ActionTypes.LIST_SORT__FAILURE,
payload: {
id,
error,
},
});
const handleListSort = (list) => ({
type: ActionTypes.LIST_SORT_HANDLE,
payload: {
list,
},
});
export default { export default {
createList, createList,
handleListCreate, handleListCreate,
@ -130,4 +159,6 @@ export default {
handleListSort, handleListSort,
deleteList, deleteList,
handleListDelete, handleListDelete,
sortList,
handleListSort,
}; };

@ -41,6 +41,12 @@ const createCard = (listId, data, headers) =>
item: transformCard(body.item), item: transformCard(body.item),
})); }));
const copyCard = (listId, data, headers) =>
socket.post(`/lists/${listId}/cards`, transformCardData(data), headers).then((body) => ({
...body,
item: transformCard(body.item),
}));
const getCard = (id, headers) => const getCard = (id, headers) =>
socket.get(`/cards/${id}`, undefined, headers).then((body) => ({ socket.get(`/cards/${id}`, undefined, headers).then((body) => ({
...body, ...body,
@ -91,4 +97,5 @@ export default {
makeHandleCardCreate, makeHandleCardCreate,
makeHandleCardUpdate, makeHandleCardUpdate,
makeHandleCardDelete, makeHandleCardDelete,
copyCard,
}; };

@ -12,6 +12,7 @@ import DueDateEditStep from '../DueDateEditStep';
import StopwatchEditStep from '../StopwatchEditStep'; import StopwatchEditStep from '../StopwatchEditStep';
import CardMoveStep from '../CardMoveStep'; import CardMoveStep from '../CardMoveStep';
import DeleteStep from '../DeleteStep'; import DeleteStep from '../DeleteStep';
import CardCopyStep from '../CardCopyStep';
import styles from './ActionsStep.module.scss'; import styles from './ActionsStep.module.scss';
@ -22,6 +23,7 @@ const StepTypes = {
EDIT_STOPWATCH: 'EDIT_STOPWATCH', EDIT_STOPWATCH: 'EDIT_STOPWATCH',
MOVE: 'MOVE', MOVE: 'MOVE',
DELETE: 'DELETE', DELETE: 'DELETE',
COPY: 'COPY',
}; };
const ActionsStep = React.memo( const ActionsStep = React.memo(
@ -48,9 +50,35 @@ const ActionsStep = React.memo(
onLabelMove, onLabelMove,
onLabelDelete, onLabelDelete,
onClose, onClose,
onCopyCard,
}) => { }) => {
const [t] = useTranslation(); const [t] = useTranslation();
const [step, openStep, handleBack] = useSteps(); const [step, openStep, handleBack] = useSteps();
// prepare defaultPath data for copying card
const defaultPath = {};
defaultPath.id = card.id;
defaultPath.boardId = card.boardId;
defaultPath.projectId = card.projectId;
defaultPath.listId = card.listId;
defaultPath.cardId = card.id;
defaultPath.name = card.name;
if (card.dueDate !== null) {
defaultPath.dueDate = card.dueDate;
}
if (card.dueDate === null || card.dueDate === undefined) {
// eslint-disable-next-line no-param-reassign
if (defaultPath.dueDate) {
delete defaultPath.dueDate;
}
}
defaultPath.stopwatch = card.stopwatch;
defaultPath.labels = card.labels;
defaultPath.boardMemberships = boardMemberships;
defaultPath.currentLabelIds = currentLabelIds;
defaultPath.currentUserIds = currentUserIds;
defaultPath.tasks = card.tasks;
defaultPath.users = card.users;
defaultPath.description = card.description;
const handleEditNameClick = useCallback(() => { const handleEditNameClick = useCallback(() => {
onNameEdit(); onNameEdit();
@ -77,11 +105,16 @@ const ActionsStep = React.memo(
openStep(StepTypes.MOVE); openStep(StepTypes.MOVE);
}, [openStep]); }, [openStep]);
const handleCopyClick = useCallback(() => {
openStep(StepTypes.COPY);
}, [openStep]);
const handleDuplicateClick = useCallback(() => { const handleDuplicateClick = useCallback(() => {
onDuplicate(); onDuplicate();
onClose(); onClose();
}, [onDuplicate, onClose]); }, [onDuplicate, onClose]);
const handleDeleteClick = useCallback(() => { const handleDeleteClick = useCallback(() => {
openStep(StepTypes.DELETE); openStep(StepTypes.DELETE);
}, [openStep]); }, [openStep]);
@ -170,6 +203,18 @@ const ActionsStep = React.memo(
onBack={handleBack} onBack={handleBack}
/> />
); );
case StepTypes.COPY:
return (
<CardCopyStep
projectsToLists={projectsToLists}
defaultPath={defaultPath}
onCopyCard={onCopyCard}
onTransfer={onTransfer}
onBoardFetch={onBoardFetch}
onBack={handleBack}
onClose={onClose}
/>
);
default: default:
} }
} }
@ -223,6 +268,11 @@ const ActionsStep = React.memo(
context: 'title', context: 'title',
})} })}
</Menu.Item> </Menu.Item>
<Menu.Item className={styles.menuItem} onClick={handleCopyClick}>
{t('action.copyCard', {
context: 'title',
})}
</Menu.Item>
</Menu> </Menu>
</Popup.Content> </Popup.Content>
</> </>
@ -255,6 +305,7 @@ ActionsStep.propTypes = {
onLabelMove: PropTypes.func.isRequired, onLabelMove: PropTypes.func.isRequired,
onLabelDelete: PropTypes.func.isRequired, onLabelDelete: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired, onClose: PropTypes.func.isRequired,
onCopyCard: PropTypes.func.isRequired,
}; };
export default ActionsStep; export default ActionsStep;

@ -21,6 +21,7 @@ import styles from './Card.module.scss';
const Card = React.memo( const Card = React.memo(
({ ({
id, id,
description,
index, index,
name, name,
dueDate, dueDate,
@ -52,6 +53,7 @@ const Card = React.memo(
onLabelUpdate, onLabelUpdate,
onLabelMove, onLabelMove,
onLabelDelete, onLabelDelete,
onCopyCard,
}) => { }) => {
const nameEdit = useRef(null); const nameEdit = useRef(null);
@ -171,6 +173,13 @@ const Card = React.memo(
{canEdit && ( {canEdit && (
<ActionsPopup <ActionsPopup
card={{ card={{
id,
description,
index,
name,
tasks,
labels,
users,
dueDate, dueDate,
stopwatch, stopwatch,
boardId, boardId,
@ -197,6 +206,7 @@ const Card = React.memo(
onLabelUpdate={onLabelUpdate} onLabelUpdate={onLabelUpdate}
onLabelMove={onLabelMove} onLabelMove={onLabelMove}
onLabelDelete={onLabelDelete} onLabelDelete={onLabelDelete}
onCopyCard={onCopyCard}
> >
<Button className={classNames(styles.actionsButton, styles.target)}> <Button className={classNames(styles.actionsButton, styles.target)}>
<Icon fitted name="pencil" size="small" /> <Icon fitted name="pencil" size="small" />
@ -218,6 +228,7 @@ const Card = React.memo(
Card.propTypes = { Card.propTypes = {
id: PropTypes.string.isRequired, id: PropTypes.string.isRequired,
description: PropTypes.string,
index: PropTypes.number.isRequired, index: PropTypes.number.isRequired,
name: PropTypes.string.isRequired, name: PropTypes.string.isRequired,
dueDate: PropTypes.instanceOf(Date), dueDate: PropTypes.instanceOf(Date),
@ -251,12 +262,16 @@ Card.propTypes = {
onLabelUpdate: PropTypes.func.isRequired, onLabelUpdate: PropTypes.func.isRequired,
onLabelMove: PropTypes.func.isRequired, onLabelMove: PropTypes.func.isRequired,
onLabelDelete: PropTypes.func.isRequired, onLabelDelete: PropTypes.func.isRequired,
// onSortTitleAsc: PropTypes.func.isRequired,
// onSortTitleDesc: PropTypes.func.isRequired,
onCopyCard: PropTypes.func.isRequired,
}; };
Card.defaultProps = { Card.defaultProps = {
dueDate: undefined, dueDate: undefined,
stopwatch: undefined, stopwatch: undefined,
coverUrl: undefined, coverUrl: undefined,
description: undefined,
}; };
export default Card; export default Card;

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

@ -0,0 +1,184 @@
import React, { useMemo, useCallback } from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import { Button, Dropdown, Form } from 'semantic-ui-react';
import { Popup } from '../../lib/custom-ui';
import { useForm } from '../../hooks';
import styles from './CardCopyStep.module.scss';
import store from '../../store';
const CardCopyStep = React.memo(
({ projectsToLists, defaultPath, onBoardFetch, onBack, onClose, onCopyCard }) => {
const [t] = useTranslation();
// Get store to get value for description string
const st = store.getState();
const keys = Object.keys(st.orm.Card.itemsById);
if (defaultPath.description === undefined) {
keys.forEach((key) => {
if (key === defaultPath.id) {
// eslint-disable-next-line no-param-reassign
defaultPath.description = st.orm.Card.itemsById[key].description;
}
});
}
if (defaultPath.dueDate === null || defaultPath.dueDate === undefined) {
// eslint-disable-next-line no-param-reassign
delete defaultPath.dueDate;
}
const [path, handleFieldChange] = useForm(() => ({
projectId: null,
boardId: null,
listId: null,
name: defaultPath.name,
description: defaultPath.description,
tasks: defaultPath.tasks,
attachments: defaultPath.attachments,
labels: defaultPath.labels,
users: defaultPath.users,
...defaultPath,
}));
const selectedProject = useMemo(
() => projectsToLists.find((project) => project.id === path.projectId) || null,
[projectsToLists, path.projectId],
);
const selectedBoard = useMemo(
() =>
(selectedProject && selectedProject.boards.find((board) => board.id === path.boardId)) ||
null,
[selectedProject, path.boardId],
);
const selectedList = useMemo(
() => (selectedBoard && selectedBoard.lists.find((list) => list.id === path.listId)) || null,
[selectedBoard, path.listId],
);
const handleBoardIdChange = useCallback(
(event, data) => {
if (selectedProject.boards.find((board) => board.id === data.value).isFetching === null) {
onBoardFetch(data.value);
}
handleFieldChange(event, data);
},
[onBoardFetch, handleFieldChange, selectedProject],
);
const handleSubmit = useCallback(() => {
onCopyCard(selectedList.id, path);
onClose();
}, [onCopyCard, selectedList.id, path, onClose]);
return (
<>
<Popup.Header onBack={onBack}>
{t('action.copyCard', {
context: 'title',
})}
</Popup.Header>
<Popup.Content>
<Form onSubmit={handleSubmit}>
<div className={styles.text}>{t('common.project')}</div>
<Dropdown
fluid
selection
name="projectId"
options={projectsToLists.map((project) => ({
text: project.name,
value: project.id,
}))}
value={selectedProject && selectedProject.id}
placeholder={
projectsToLists.length === 0 ? t('common.noProjects') : t('common.selectProject')
}
disabled={projectsToLists.length === 0}
className={styles.field}
onChange={handleFieldChange}
/>
{selectedProject && (
<>
<div className={styles.text}>{t('common.board')}</div>
<Dropdown
fluid
selection
name="boardId"
options={selectedProject.boards.map((board) => ({
text: board.name,
value: board.id,
}))}
value={selectedBoard && selectedBoard.id}
placeholder={
selectedProject.boards.length === 0
? t('common.noBoards')
: t('common.selectBoard')
}
disabled={selectedProject.boards.length === 0}
className={styles.field}
onChange={handleBoardIdChange}
/>
</>
)}
{selectedBoard && (
<>
<div className={styles.text}>{t('common.list')}</div>
<Dropdown
fluid
selection
name="listId"
options={selectedBoard.lists.map((list) => ({
text: list.name,
value: list.id,
}))}
value={selectedList && selectedList.id}
placeholder={
selectedBoard.isFetching === false && selectedBoard.lists.length === 0
? t('common.noLists')
: t('common.selectList')
}
loading={selectedBoard.isFetching !== false}
disabled={selectedBoard.isFetching !== false || selectedBoard.lists.length === 0}
className={styles.field}
onChange={handleFieldChange}
/>
</>
)}
<Button
positive
content={t('action.copy')} // change this action.copy
disabled={(selectedBoard && selectedBoard.isFetching !== false) || !selectedList}
/>
</Form>
</Popup.Content>
</>
);
},
);
CardCopyStep.propTypes = {
// eslint-disable-next-line react/forbid-prop-types
description: PropTypes.string,
/* eslint-disable react/forbid-prop-types */
projectsToLists: PropTypes.array.isRequired,
defaultPath: PropTypes.object.isRequired,
/* eslint-enable react/forbid-prop-types */
// onMove: PropTypes.func.isRequired,
// onTransfer: PropTypes.func.isRequired,
onBoardFetch: PropTypes.func.isRequired,
onBack: PropTypes.func,
onClose: PropTypes.func.isRequired,
onCopyCard: PropTypes.func.isRequired,
};
CardCopyStep.defaultProps = {
onBack: undefined,
description: undefined,
};
export default CardCopyStep;

@ -0,0 +1,12 @@
:global(#app) {
.field {
margin-bottom: 8px;
}
.text {
color: #444444;
font-size: 12px;
font-weight: bold;
padding-bottom: 6px;
}
}

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

@ -25,10 +25,13 @@ import StopwatchEditStep from '../StopwatchEditStep';
import CardMoveStep from '../CardMoveStep'; import CardMoveStep from '../CardMoveStep';
import DeleteStep from '../DeleteStep'; import DeleteStep from '../DeleteStep';
import CardCopyPopup from '../CardCopyPopup';
import styles from './CardModal.module.scss'; import styles from './CardModal.module.scss';
const CardModal = React.memo( const CardModal = React.memo(
({ ({
id,
name, name,
description, description,
dueDate, dueDate,
@ -79,9 +82,63 @@ const CardModal = React.memo(
onCommentActivityUpdate, onCommentActivityUpdate,
onCommentActivityDelete, onCommentActivityDelete,
onClose, onClose,
onCopyCard,
}) => { }) => {
const [t] = useTranslation(); const [t] = useTranslation();
const card = {};
card.id = id;
card.name = name;
card.description = description;
card.dueDate = dueDate;
card.stopwatch = stopwatch;
card.isSubscribed = isSubscribed;
card.isActivitiesFetching = isActivitiesFetching;
card.isAllActivitiesFetched = isAllActivitiesFetched;
card.isActivitiesDetailsVisible = isActivitiesDetailsVisible;
card.isActivitiesDetailsFetching = isActivitiesDetailsFetching;
card.listId = listId;
card.boardId = boardId;
card.projectId = projectId;
card.users = users;
card.labels = labels;
card.tasks = tasks;
card.attachments = attachments;
card.activities = activities;
card.allProjectsToLists = allProjectsToLists;
card.allBoardMemberships = allBoardMemberships;
card.allLabels = allLabels;
card.canEdit = canEdit;
card.canEditCommentActivities = canEditCommentActivities;
card.canEditAllCommentActivities = canEditAllCommentActivities;
card.onUpdate = onUpdate;
card.onMove = onMove;
card.onTransfer = onTransfer;
card.onDelete = onDelete;
card.onUserAdd = onUserAdd;
card.onUserRemove = onUserRemove;
card.onBoardFetch = onBoardFetch;
card.onLabelAdd = onLabelAdd;
card.onLabelRemove = onLabelRemove;
card.onLabelCreate = onLabelCreate;
card.onLabelUpdate = onLabelUpdate;
card.onLabelMove = onLabelMove;
card.onLabelDelete = onLabelDelete;
card.onTaskCreate = onTaskCreate;
card.onTaskUpdate = onTaskUpdate;
card.onTaskMove = onTaskMove;
card.onTaskDelete = onTaskDelete;
card.onAttachmentCreate = onAttachmentCreate;
card.onAttachmentUpdate = onAttachmentUpdate;
card.onAttachmentDelete = onAttachmentDelete;
card.onActivitiesFetch = onActivitiesFetch;
card.onActivitiesDetailsToggle = onActivitiesDetailsToggle;
card.onCommentActivityCreate = onCommentActivityCreate;
card.onCommentActivityUpdate = onCommentActivityUpdate;
card.onCommentActivityDelete = onCommentActivityDelete;
card.onClose = onClose;
card.onCopyCard = onCopyCard;
const isGalleryOpened = useRef(false); const isGalleryOpened = useRef(false);
const handleToggleStopwatchClick = useCallback(() => { const handleToggleStopwatchClick = useCallback(() => {
@ -502,10 +559,30 @@ const CardModal = React.memo(
{t('action.move')} {t('action.move')}
</Button> </Button>
</CardMovePopup> </CardMovePopup>
<CardCopyPopup
projectsToLists={allProjectsToLists}
defaultPath={card}
onMove={onMove}
onTransfer={onTransfer}
onBoardFetch={onBoardFetch}
onCopyCard={onCopyCard}
>
<Button
fluid
className={styles.actionButton}
onClick={handleToggleSubscriptionClick}
>
<Icon name="copy outline" className={styles.actionIcon} />
{t('action.copy')}
</Button>
</CardCopyPopup>
<Button fluid className={styles.actionButton} onClick={handleDuplicateClick}> <Button fluid className={styles.actionButton} onClick={handleDuplicateClick}>
<Icon name="copy outline" className={styles.actionIcon} /> <Icon name="copy outline" className={styles.actionIcon} />
{t('action.duplicate')} {t('action.duplicate')}
</Button> </Button>
<DeletePopup <DeletePopup
title="common.deleteCard" title="common.deleteCard"
content="common.areYouSureYouWantToDeleteThisCard" content="common.areYouSureYouWantToDeleteThisCard"
@ -537,6 +614,7 @@ const CardModal = React.memo(
); );
CardModal.propTypes = { CardModal.propTypes = {
id: PropTypes.string,
name: PropTypes.string.isRequired, name: PropTypes.string.isRequired,
description: PropTypes.string, description: PropTypes.string,
dueDate: PropTypes.instanceOf(Date), dueDate: PropTypes.instanceOf(Date),
@ -589,12 +667,14 @@ CardModal.propTypes = {
onCommentActivityUpdate: PropTypes.func.isRequired, onCommentActivityUpdate: PropTypes.func.isRequired,
onCommentActivityDelete: PropTypes.func.isRequired, onCommentActivityDelete: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired, onClose: PropTypes.func.isRequired,
onCopyCard: PropTypes.func.isRequired,
}; };
CardModal.defaultProps = { CardModal.defaultProps = {
description: undefined, description: undefined,
dueDate: undefined, dueDate: undefined,
stopwatch: undefined, stopwatch: undefined,
id: undefined,
}; };
export default CardModal; export default CardModal;

@ -11,7 +11,6 @@ import styles from './CardMoveStep.module.scss';
const CardMoveStep = React.memo( const CardMoveStep = React.memo(
({ projectsToLists, defaultPath, onMove, onTransfer, onBoardFetch, onBack, onClose }) => { ({ projectsToLists, defaultPath, onMove, onTransfer, onBoardFetch, onBack, onClose }) => {
const [t] = useTranslation(); const [t] = useTranslation();
const [path, handleFieldChange] = useForm(() => ({ const [path, handleFieldChange] = useForm(() => ({
projectId: null, projectId: null,
boardId: null, boardId: null,

@ -7,6 +7,7 @@ import { Popup } from '../../lib/custom-ui';
import { useSteps } from '../../hooks'; import { useSteps } from '../../hooks';
import ListSortStep from '../ListSortStep'; import ListSortStep from '../ListSortStep';
import DeleteStep from '../DeleteStep'; import DeleteStep from '../DeleteStep';
import SortStep from '../SortStep';
import styles from './ActionsStep.module.scss'; import styles from './ActionsStep.module.scss';
@ -15,19 +16,90 @@ const StepTypes = {
SORT: 'SORT', SORT: 'SORT',
}; };
const ActionsStep = React.memo(({ onNameEdit, onCardAdd, onSort, onDelete, onClose }) => { const ActionsStep = React.memo(
const [t] = useTranslation(); ({ onNameEdit, onCardAdd, onDelete, onClose, onSort, selectedOption, setSelectedOption }) => {
const [step, openStep, handleBack] = useSteps(); const [t] = useTranslation();
const [step, openStep, handleBack] = useSteps();
const handleEditNameClick = useCallback(() => { const handleEditNameClick = useCallback(() => {
onNameEdit(); onNameEdit();
onClose(); onClose();
}, [onNameEdit, onClose]); }, [onNameEdit, onClose]);
const handleAddCardClick = useCallback(() => {
onCardAdd();
onClose();
}, [onCardAdd, onClose]);
const handleDeleteClick = useCallback(() => {
openStep(StepTypes.DELETE);
}, [openStep]);
const handleSortClick = useCallback(() => {
openStep(StepTypes.SORT);
}, [openStep]);
const handleAddCardClick = useCallback(() => { if (step && step.type === StepTypes.DELETE) {
onCardAdd(); return (
onClose(); <DeleteStep
}, [onCardAdd, onClose]); title="common.deleteList"
content="common.areYouSureYouWantToDeleteThisList"
buttonContent="action.deleteList"
onConfirm={onDelete}
onBack={handleBack}
/>
);
}
if (step && step.type === StepTypes.SORT) {
return (
<SortStep
title="common.sortList"
content="common.areYouSureYouWantToSortThisList"
buttonContent="action.sortList"
onConfirm={onSort}
onBack={handleBack}
selectedOption={selectedOption}
setSelectedOption={setSelectedOption}
/>
);
}
return (
<>
<Popup.Header>
{t('common.listActions', {
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={handleAddCardClick}>
{t('action.addCard', {
context: 'title',
})}
</Menu.Item>
<Menu.Item className={styles.menuItem} onClick={handleDeleteClick}>
{t('action.deleteList', {
context: 'title',
})}
</Menu.Item>
<Menu.Item className={styles.menuItem} onClick={handleSortClick}>
{t('action.sortList', {
context: 'title',
})}
</Menu.Item>
</Menu>
</Popup.Content>
</>
);
},
);
const handleSortClick = useCallback(() => { const handleSortClick = useCallback(() => {
openStep(StepTypes.SORT); openStep(StepTypes.SORT);
@ -107,6 +179,9 @@ ActionsStep.propTypes = {
onDelete: PropTypes.func.isRequired, onDelete: PropTypes.func.isRequired,
onSort: PropTypes.func.isRequired, onSort: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired, onClose: PropTypes.func.isRequired,
onSort: PropTypes.func.isRequired,
selectedOption: PropTypes.string.isRequired,
setSelectedOption: PropTypes.func.isRequired,
}; };
export default ActionsStep; export default ActionsStep;

@ -4,7 +4,7 @@ import classNames from 'classnames';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Draggable, Droppable } from 'react-beautiful-dnd'; import { Draggable, Droppable } from 'react-beautiful-dnd';
import { Button, Icon } from 'semantic-ui-react'; import { Button, Icon } from 'semantic-ui-react';
import { usePopup } from '../../lib/popup'; import { usePopup, closePopup } from '../../lib/popup';
import DroppableTypes from '../../constants/DroppableTypes'; import DroppableTypes from '../../constants/DroppableTypes';
import CardContainer from '../../containers/CardContainer'; import CardContainer from '../../containers/CardContainer';
@ -30,6 +30,7 @@ const List = React.memo(
}) => { }) => {
const [t] = useTranslation(); const [t] = useTranslation();
const [isAddCardOpened, setIsAddCardOpened] = useState(false); const [isAddCardOpened, setIsAddCardOpened] = useState(false);
const [selectedOption, setSelectedOption] = useState('name');
const nameEdit = useRef(null); const nameEdit = useRef(null);
const listWrapper = useRef(null); const listWrapper = useRef(null);
@ -51,11 +52,11 @@ const List = React.memo(
const handleAddCardClick = useCallback(() => { const handleAddCardClick = useCallback(() => {
setIsAddCardOpened(true); setIsAddCardOpened(true);
}, []); }, [setIsAddCardOpened]);
const handleAddCardClose = useCallback(() => { const handleAddCardClose = useCallback(() => {
setIsAddCardOpened(false); setIsAddCardOpened(false);
}, []); }, [setIsAddCardOpened]);
const handleNameEdit = useCallback(() => { const handleNameEdit = useCallback(() => {
nameEdit.current.open(); nameEdit.current.open();
@ -63,7 +64,12 @@ const List = React.memo(
const handleCardAdd = useCallback(() => { const handleCardAdd = useCallback(() => {
setIsAddCardOpened(true); setIsAddCardOpened(true);
}, []); }, [setIsAddCardOpened]);
const onSort = useCallback(() => {
onUpdate({ selectedOption: document.querySelector('input[name="sort"]:checked').value });
closePopup();
}, [onUpdate]);
useEffect(() => { useEffect(() => {
if (isAddCardOpened) { if (isAddCardOpened) {
@ -126,6 +132,9 @@ const List = React.memo(
onCardAdd={handleCardAdd} onCardAdd={handleCardAdd}
onDelete={onDelete} onDelete={onDelete}
onSort={onSort} onSort={onSort}
selectedOption={selectedOption}
setSelectedOption={setSelectedOption}
> >
<Button className={classNames(styles.headerButton, styles.target)}> <Button className={classNames(styles.headerButton, styles.target)}>
<Icon fitted name="pencil" size="small" /> <Icon fitted name="pencil" size="small" />

@ -0,0 +1,164 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import { Button } from 'semantic-ui-react';
import { Popup } from '../../lib/custom-ui';
import styles from './SortStep.module.scss';
const SortStep = React.memo(
({ title, content, buttonContent, onBack, onConfirm, selectedOption, setSelectedOption }) => {
const [t] = useTranslation();
const [checked, setChecked] = useState(false);
function handleChange(event) {
setSelectedOption(event.target.id);
}
const updateState = () => {
if (checked) {
setChecked(false);
} else {
setChecked(true);
}
};
function changeEvent(event) {
handleChange(event);
updateState();
}
return (
<>
<Popup.Header onBack={onBack}>
{t(title, {
context: 'title',
})}
</Popup.Header>
<Popup.Content>
<div className={styles.content}>{t(content)}</div>
<div>
<label htmlFor="name">
<input
type="radio"
id="name"
name="sort"
value="name"
onChange={changeEvent}
checked={selectedOption === 'name'}
/>
{t('action.sortByTitleList', {
context: 'title',
})}
</label>
<br />
<label htmlFor="id">
<input
type="radio"
id="id"
name="sort"
value="id"
onChange={changeEvent}
checked={selectedOption === 'id'}
/>
{t('action.sortByIdList', {
context: 'title',
})}
</label>
<br />
<label htmlFor="position">
<input
type="radio"
id="position"
name="sort"
value="position"
onChange={changeEvent}
checked={selectedOption === 'position'}
/>
{t('action.sortByPositionList', {
context: 'title',
})}
</label>
<br />
<label htmlFor="createdAt">
<input
type="radio"
id="createdAt"
name="sort"
value="createdAt"
onChange={changeEvent}
checked={selectedOption === 'createdAt'}
/>
{t('action.sortByCreatedAt', {
context: 'title',
})}
</label>
<br />
<label htmlFor="updatedAt">
<input
type="radio"
id="updatedAt"
name="sort"
value="updatedAt"
onChange={changeEvent}
checked={selectedOption === 'updatedAt'}
/>
{t('action.sortByUpdatedAtList', {
context: 'title',
})}
</label>
<br />
<label htmlFor="dueDate">
<input
type="radio"
id="dueDate"
name="sort"
value="dueDate"
onChange={changeEvent}
checked={selectedOption === 'dueDate'}
/>
{t('action.sortByDueDateList', {
context: 'title',
})}
</label>
<br />
<label htmlFor="creatorUserId">
<input
type="radio"
id="creatorUserId"
name="sort"
value="creatorUserId"
onChange={changeEvent}
checked={selectedOption === 'creatorUserId'}
/>
{t('action.sortByCreatorUserIdList', {
context: 'title',
})}
</label>
<br />
</div>
<Button fluid negative content={t(buttonContent)} onClick={onConfirm} />
</Popup.Content>
</>
);
},
);
SortStep.propTypes = {
title: PropTypes.string.isRequired,
content: PropTypes.string.isRequired,
buttonContent: PropTypes.string.isRequired,
onConfirm: PropTypes.func.isRequired,
onBack: PropTypes.func,
selectedOption: PropTypes.string.isRequired,
setSelectedOption: PropTypes.func.isRequired,
};
SortStep.defaultProps = {
onBack: undefined,
};
export default SortStep;

@ -0,0 +1,7 @@
:global(#app) {
.content {
color: #212121;
padding-bottom: 6px;
padding-left: 2px;
}
}

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

@ -181,6 +181,10 @@ export default {
LIST_DELETE__SUCCESS: 'LIST_DELETE__SUCCESS', LIST_DELETE__SUCCESS: 'LIST_DELETE__SUCCESS',
LIST_DELETE__FAILURE: 'LIST_DELETE__FAILURE', LIST_DELETE__FAILURE: 'LIST_DELETE__FAILURE',
LIST_DELETE_HANDLE: 'LIST_DELETE_HANDLE', LIST_DELETE_HANDLE: 'LIST_DELETE_HANDLE',
LIST_SORT: 'LIST_SORT',
LIST_SORT__SUCCESS: 'LIST_SORT__SUCCESS',
LIST_SORT__FAILURE: 'LIST_SORT__FAILURE',
LIST_SORT_HANDLE: 'LIST_SORT_HANDLE',
/* Cards */ /* Cards */
@ -205,8 +209,15 @@ export default {
CARD_DELETE__SUCCESS: 'CARD_DELETE__SUCCESS', CARD_DELETE__SUCCESS: 'CARD_DELETE__SUCCESS',
CARD_DELETE__FAILURE: 'CARD_DELETE__FAILURE', CARD_DELETE__FAILURE: 'CARD_DELETE__FAILURE',
CARD_DELETE_HANDLE: 'CARD_DELETE_HANDLE', CARD_DELETE_HANDLE: 'CARD_DELETE_HANDLE',
CARD_COPY_HANDLE: 'CARD_COPY_HANDLE',
CARD_COPY: 'CARD_COPY',
CARD_COPY__SUCCESS: 'CARD_COPY__SUCCESS',
CARD_COPY__FAILURE: 'CARD_COPY__FAILURE',
TEXT_FILTER_IN_CURRENT_BOARD: 'TEXT_FILTER_IN_CURRENT_BOARD', TEXT_FILTER_IN_CURRENT_BOARD: 'TEXT_FILTER_IN_CURRENT_BOARD',
/* Tasks */ /* Tasks */
TASK_CREATE: 'TASK_CREATE', TASK_CREATE: 'TASK_CREATE',

@ -139,6 +139,8 @@ export default {
CARD_DELETE: `${PREFIX}/CARD_DELETE`, CARD_DELETE: `${PREFIX}/CARD_DELETE`,
CURRENT_CARD_DELETE: `${PREFIX}/CURRENT_CARD_DELETE`, CURRENT_CARD_DELETE: `${PREFIX}/CURRENT_CARD_DELETE`,
CARD_DELETE_HANDLE: `${PREFIX}/CARD_DELETE_HANDLE`, CARD_DELETE_HANDLE: `${PREFIX}/CARD_DELETE_HANDLE`,
CARD_COPY: `${PREFIX}/CARD_COPY`,
CARD_COPY_HANDLE: `${PREFIX}/CARD_COPY_HANDLE`,
TEXT_FILTER_IN_CURRENT_BOARD: `${PREFIX}/FILTER_TEXT_HANDLE`, TEXT_FILTER_IN_CURRENT_BOARD: `${PREFIX}/FILTER_TEXT_HANDLE`,
/* Tasks */ /* Tasks */

@ -19,4 +19,5 @@ export const ActivityTypes = {
CREATE_CARD: 'createCard', CREATE_CARD: 'createCard',
MOVE_CARD: 'moveCard', MOVE_CARD: 'moveCard',
COMMENT_CARD: 'commentCard', COMMENT_CARD: 'commentCard',
COPY_CARD: 'copyCard',
}; };

@ -27,6 +27,7 @@ const mapDispatchToProps = (dispatch) =>
onListCreate: entryActions.createListInCurrentBoard, onListCreate: entryActions.createListInCurrentBoard,
onListMove: entryActions.moveList, onListMove: entryActions.moveList,
onCardMove: entryActions.moveCard, onCardMove: entryActions.moveCard,
onCardCopy: entryActions.copyCard,
}, },
dispatch, dispatch,
); );

@ -24,6 +24,7 @@ const mapDispatchToProps = (dispatch) =>
onUpdate: entryActions.updateBoard, onUpdate: entryActions.updateBoard,
onMove: entryActions.moveBoard, onMove: entryActions.moveBoard,
onDelete: entryActions.deleteBoard, onDelete: entryActions.deleteBoard,
onSort: entryActions.sortBoard,
}, },
dispatch, dispatch,
); );

@ -73,6 +73,7 @@ const mapDispatchToProps = (dispatch, { id }) =>
onLabelUpdate: (labelId, data) => entryActions.updateLabel(labelId, data), onLabelUpdate: (labelId, data) => entryActions.updateLabel(labelId, data),
onLabelMove: (labelId, index) => entryActions.moveLabel(labelId, index), onLabelMove: (labelId, index) => entryActions.moveLabel(labelId, index),
onLabelDelete: (labelId) => entryActions.deleteLabel(labelId), onLabelDelete: (labelId) => entryActions.deleteLabel(labelId),
onCopyCard: (listId, data) => entryActions.copyCard(listId, data, false),
}, },
dispatch, dispatch,
); );

@ -18,6 +18,7 @@ const mapStateToProps = (state) => {
const currentUserMembership = selectors.selectCurrentUserMembershipForCurrentBoard(state); const currentUserMembership = selectors.selectCurrentUserMembershipForCurrentBoard(state);
const { const {
id,
name, name,
description, description,
dueDate, dueDate,
@ -46,6 +47,7 @@ const mapStateToProps = (state) => {
} }
return { return {
id,
name, name,
description, description,
dueDate, dueDate,
@ -101,6 +103,7 @@ const mapDispatchToProps = (dispatch) =>
onCommentActivityCreate: entryActions.createCommentActivityInCurrentCard, onCommentActivityCreate: entryActions.createCommentActivityInCurrentCard,
onCommentActivityUpdate: entryActions.updateCommentActivity, onCommentActivityUpdate: entryActions.updateCommentActivity,
onCommentActivityDelete: entryActions.deleteCommentActivity, onCommentActivityDelete: entryActions.deleteCommentActivity,
onCopyCard: entryActions.copyCard,
push, push,
}, },
dispatch, dispatch,

@ -36,6 +36,7 @@ const mapDispatchToProps = (dispatch, { id }) =>
onSort: (data) => entryActions.sortList(id, data), onSort: (data) => entryActions.sortList(id, data),
onDelete: () => entryActions.deleteList(id), onDelete: () => entryActions.deleteList(id),
onCardCreate: (data, autoOpen) => entryActions.createCard(id, data, autoOpen), onCardCreate: (data, autoOpen) => entryActions.createCard(id, data, autoOpen),
onSort: (data) => entryActions.sortList(id, data),
}, },
dispatch, dispatch,
); );

@ -47,7 +47,16 @@ const moveCard = (id, listId, index) => ({
}, },
}); });
const moveCurrentCard = (listId, index) => ({ const copyCard = (listId, data, autoOpen) => ({
type: EntryActionTypes.CARD_COPY,
payload: {
listId,
data,
autoOpen,
},
});
const moveCurrentCard = (listId, index = 0) => ({
type: EntryActionTypes.CURRENT_CARD_MOVE, type: EntryActionTypes.CURRENT_CARD_MOVE,
payload: { payload: {
listId, listId,
@ -127,5 +136,6 @@ export default {
deleteCard, deleteCard,
deleteCurrentCard, deleteCurrentCard,
handleCardDelete, handleCardDelete,
copyCard,
filterText, filterText,
}; };

@ -69,6 +69,13 @@ const handleListDelete = (list) => ({
}, },
}); });
const handleListSort = (list) => ({
type: EntryActionTypes.LIST_SORT_HANDLE,
payload: {
list,
},
});
export default { export default {
createListInCurrentBoard, createListInCurrentBoard,
handleListCreate, handleListCreate,
@ -79,4 +86,5 @@ export default {
handleListSort, handleListSort,
deleteList, deleteList,
handleListDelete, handleListDelete,
handleListSort,
}; };

@ -1,4 +1,5 @@
import usePopup from './use-popup'; import usePopup from './use-popup';
import closePopup from './close-popup'; import closePopup from './close-popup';
import withPopup from './with-popup';
export { usePopup, closePopup }; export { usePopup, closePopup, withPopup };

@ -0,0 +1,109 @@
import { ResizeObserver } from '@juggle/resize-observer';
import React, { useCallback, useRef, useState } from 'react';
import PropTypes from 'prop-types';
import { Button, Popup as SemanticUIPopup } from 'semantic-ui-react';
import styles from './Popup.module.css';
export default (WrappedComponent, defaultProps) => {
const Popup = React.memo(({ children, ...props }) => {
const [isOpened, setIsOpened] = useState(false);
const wrapper = useRef(null);
const resizeObserver = useRef(null);
const handleOpen = useCallback(() => {
setIsOpened(true);
}, []);
const handleClose = useCallback(() => {
setIsOpened(false);
}, []);
const handleMouseDown = useCallback((event) => {
event.stopPropagation();
}, []);
const handleClick = useCallback((event) => {
event.stopPropagation();
}, []);
const handleTriggerClick = useCallback(
(event) => {
event.stopPropagation();
const { onClick } = children;
if (onClick) {
onClick(event);
}
},
[children],
);
const handleContentRef = useCallback((element) => {
if (resizeObserver.current) {
resizeObserver.current.disconnect();
}
if (!element) {
resizeObserver.current = null;
return;
}
resizeObserver.current = new ResizeObserver(() => {
if (resizeObserver.current.isInitial) {
resizeObserver.current.isInitial = false;
return;
}
wrapper.current.positionUpdate();
});
resizeObserver.current.isInitial = true;
resizeObserver.current.observe(element);
}, []);
const tigger = React.cloneElement(children, {
onClick: handleTriggerClick,
});
return (
<SemanticUIPopup
basic
wide
ref={wrapper}
trigger={tigger}
on="click"
open={isOpened}
position="bottom left"
popperModifiers={[
{
name: 'preventOverflow',
options: {
boundariesElement: 'window',
},
},
]}
className={styles.wrapper}
onOpen={handleOpen}
onClose={handleClose}
onMouseDown={handleMouseDown}
onClick={handleClick}
{...defaultProps} // eslint-disable-line react/jsx-props-no-spreading
>
<div ref={handleContentRef}>
<Button icon="close" onClick={handleClose} className={styles.closeButton} />
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
<WrappedComponent {...props} onClose={handleClose} />
</div>
</SemanticUIPopup>
);
});
Popup.propTypes = {
children: PropTypes.node.isRequired,
};
return Popup;
};

@ -22,7 +22,8 @@ export default {
administrator: 'Administrator', administrator: 'Administrator',
all: 'All', all: 'All',
allChangesWillBeAutomaticallySavedAfterConnectionRestored: allChangesWillBeAutomaticallySavedAfterConnectionRestored:
'All changes will be automatically saved<br />after connection restored.', 'All changes will be automatically saved<br />after connection restored',
areYouSureYouWantToCopyThisCard: 'Are you sure you want to copy this card',
areYouSureYouWantToDeleteThisAttachment: 'Are you sure you want to delete this attachment?', areYouSureYouWantToDeleteThisAttachment: 'Are you sure you want to delete this attachment?',
areYouSureYouWantToDeleteThisBoard: 'Are you sure you want to delete this board?', areYouSureYouWantToDeleteThisBoard: 'Are you sure you want to delete this board?',
areYouSureYouWantToDeleteThisCard: 'Are you sure you want to delete this card?', areYouSureYouWantToDeleteThisCard: 'Are you sure you want to delete this card?',
@ -38,6 +39,7 @@ export default {
'Are you sure you want to remove this manager from the project?', 'Are you sure you want to remove this manager from the project?',
areYouSureYouWantToRemoveThisMemberFromBoard: areYouSureYouWantToRemoveThisMemberFromBoard:
'Are you sure you want to remove this member from the board?', 'Are you sure you want to remove this member from the board?',
areYouSureYouWantToSortThisList: 'Are you sure you want to sort this list?',
attachment: 'Attachment', attachment: 'Attachment',
attachments: 'Attachments', attachments: 'Attachments',
authentication: 'Authentication', authentication: 'Authentication',
@ -51,6 +53,7 @@ export default {
cardNotFound_title: 'Card Not Found', cardNotFound_title: 'Card Not Found',
cardOrActionAreDeleted: 'Card or action are deleted.', cardOrActionAreDeleted: 'Card or action are deleted.',
color: 'Color', color: 'Color',
copyCard: 'Copy card',
copy_inline: 'copy', copy_inline: 'copy',
createBoard_title: 'Create Board', createBoard_title: 'Create Board',
createLabel_title: 'Create Label', createLabel_title: 'Create Label',
@ -147,6 +150,7 @@ export default {
selectPermissions_title: 'Select Permissions', selectPermissions_title: 'Select Permissions',
selectProject: 'Select project', selectProject: 'Select project',
settings: 'Settings', settings: 'Settings',
sortList: 'Sort list',
sortList_title: 'Sort List', sortList_title: 'Sort List',
stopwatch: 'Stopwatch', stopwatch: 'Stopwatch',
subscribeToMyOwnCardsByDefault: 'Subscribe to my own cards by default', subscribeToMyOwnCardsByDefault: 'Subscribe to my own cards by default',
@ -183,6 +187,8 @@ export default {
addTask: 'Add task', addTask: 'Add task',
addToCard: 'Add to card', addToCard: 'Add to card',
addUser: 'Add user', addUser: 'Add user',
copy: 'Copy',
copyCard: 'Copy card',
createBoard: 'Create board', createBoard: 'Create board',
createFile: 'Create file', createFile: 'Create file',
createLabel: 'Create label', createLabel: 'Create label',
@ -235,6 +241,14 @@ export default {
showAllAttachments: 'Show all attachments ({{hidden}} hidden)', showAllAttachments: 'Show all attachments ({{hidden}} hidden)',
showDetails: 'Show details', showDetails: 'Show details',
showFewerAttachments: 'Show fewer attachments', showFewerAttachments: 'Show fewer attachments',
sortByCreatedAt: ' Sort by created at',
sortByCreatorUserIdList: ' Sort by creator',
sortByDueDateList: ' Sort by due date',
sortByIdList: ' Sort by id',
sortByPositionList: ' Sort by position',
sortByTitleList: ' Sort by title',
sortByUpdatedAtList: ' Sort by updated at',
sortList: 'Sort list',
sortList_title: 'Sort List', sortList_title: 'Sort List',
start: 'Start', start: 'Start',
stop: 'Stop', stop: 'Stop',

@ -15,6 +15,7 @@ export default {
translation: { translation: {
common: { common: {
aboutPlanka: 'Om Planka',
account: 'Konto', account: 'Konto',
actions: 'Åtgärder', actions: 'Åtgärder',
addAttachment_title: 'Bifoga', addAttachment_title: 'Bifoga',
@ -25,7 +26,8 @@ export default {
administrator: 'Administratör', administrator: 'Administratör',
all: 'Alla', all: 'Alla',
allChangesWillBeAutomaticallySavedAfterConnectionRestored: allChangesWillBeAutomaticallySavedAfterConnectionRestored:
'Alla ändringar kommer att sparas automatiskt<br />så fort anslutningen är återställd.', 'Alla ändringar kommer att sparas automatiskt<br />så fort anslutningen är återställd',
areYouSureYouWantToCopyThisCard: 'Är du säker på att du vill kopiera det här kortet',
areYouSureYouWantToDeleteThisAttachment: areYouSureYouWantToDeleteThisAttachment:
'Är du säker på att du vill ta bort den här bilagan?', 'Är du säker på att du vill ta bort den här bilagan?',
areYouSureYouWantToDeleteThisBoard: 'Är du säker på att du vill ta bort den här tavlan?', areYouSureYouWantToDeleteThisBoard: 'Är du säker på att du vill ta bort den här tavlan?',
@ -34,6 +36,7 @@ export default {
'Är du säker på att du vill ta bort den här kommentaren?', 'Är du säker på att du vill ta bort den här kommentaren?',
areYouSureYouWantToDeleteThisLabel: 'Är du säker på att du vill ta bort den här etiketten?', areYouSureYouWantToDeleteThisLabel: 'Är du säker på att du vill ta bort den här etiketten?',
areYouSureYouWantToDeleteThisList: 'Är du säker på att du vill ta bort den här listan?', areYouSureYouWantToDeleteThisList: 'Är du säker på att du vill ta bort den här listan?',
areYouSureYouWantToSortThisList: 'Är du säker på att du vill sortera den här listan?',
areYouSureYouWantToDeleteThisProject: 'Är du säker på att du vill ta bort det här projektet?', areYouSureYouWantToDeleteThisProject: 'Är du säker på att du vill ta bort det här projektet?',
areYouSureYouWantToDeleteThisTask: 'Är du säker på att du vill ta bort den här uppgiften?', areYouSureYouWantToDeleteThisTask: 'Är du säker på att du vill ta bort den här uppgiften?',
areYouSureYouWantToDeleteThisUser: 'Är du säker på att du vill ta bort den här användaren?', areYouSureYouWantToDeleteThisUser: 'Är du säker på att du vill ta bort den här användaren?',
@ -49,10 +52,14 @@ export default {
background: 'Bakgrund', background: 'Bakgrund',
board: 'Tavla', board: 'Tavla',
boardNotFound_title: 'Tavla Ej Funnen', boardNotFound_title: 'Tavla Ej Funnen',
canComment: 'Kan kommentera',
canEditContentOfBoard: 'Kan ändra innehåll för tavla',
canOnlyViewBoard: 'Kan bara titta på tavla',
cardActions_title: 'Kortåtgärder', cardActions_title: 'Kortåtgärder',
cardNotFound_title: 'Kort Ej Funnet', cardNotFound_title: 'Kort Ej Funnet',
cardOrActionAreDeleted: 'Kort eller åtgärd är borttagen.', cardOrActionAreDeleted: 'Kort eller åtgärd är borttagen.',
color: 'Färg', color: 'Färg',
copyCard: 'Kopiera kort',
createBoard_title: 'Skapa Tavla', createBoard_title: 'Skapa Tavla',
createLabel_title: 'Skapa Etikett', createLabel_title: 'Skapa Etikett',
createNewOneOrSelectExistingOne: 'Skapa en ny eller välj<br />en redan existerande.', createNewOneOrSelectExistingOne: 'Skapa en ny eller välj<br />en redan existerande.',
@ -72,14 +79,18 @@ export default {
deleteTask_title: 'Ta Bort Uppgift', deleteTask_title: 'Ta Bort Uppgift',
deleteUser_title: 'Ta Bort Användare', deleteUser_title: 'Ta Bort Användare',
description: 'Beskrivning', description: 'Beskrivning',
detectAutomatically: 'Upptäck automatiskt',
dropFileToUpload: 'Släpp en fil för att ladda upp', dropFileToUpload: 'Släpp en fil för att ladda upp',
editor: 'Fileditor',
editAttachment_title: 'Redigera Bilaga', editAttachment_title: 'Redigera Bilaga',
editAvatar_title: 'Redigera Avatar', editAvatar_title: 'Redigera Avatar',
editBoard_title: 'Redigera Tavla', editBoard_title: 'Redigera Tavla',
editDueDate_title: 'Redigera Förfallodatum', editDueDate_title: 'Redigera Förfallodatum',
editEmail_title: 'Redigera E-mail', editEmail_title: 'Redigera E-mail',
editInformation_title: 'Ändra Information',
editLabel_title: 'Redigera Etikett', editLabel_title: 'Redigera Etikett',
editPassword_title: 'Redigera Lösenord', editPassword_title: 'Redigera Lösenord',
editPermissions_title: 'Ändra behörigheter',
editStopwatch_title: 'Redigera Timer', editStopwatch_title: 'Redigera Timer',
editUsername_title: 'Redigera Användarnamn', editUsername_title: 'Redigera Användarnamn',
email: 'E-mail', email: 'E-mail',
@ -93,10 +104,13 @@ export default {
filterByLabels_title: 'Filtrera Efter Etiketter', filterByLabels_title: 'Filtrera Efter Etiketter',
filterByMembers_title: 'Filtrera Efter Medlemmar', filterByMembers_title: 'Filtrera Efter Medlemmar',
fromComputer_title: 'Från dator', fromComputer_title: 'Från dator',
fromTrello: 'Från Trello',
general: 'Allmänt', general: 'Allmänt',
hours: 'Timmar', hours: 'Timmar',
importBoard_title: 'Importera Tavla',
invalidCurrentPassword: 'Ogiltigt nuvarande lösenord', invalidCurrentPassword: 'Ogiltigt nuvarande lösenord',
labels: 'Etiketter', labels: 'Etiketter',
language: 'Språk',
leaveBoard_title: 'Lämna Tavla', leaveBoard_title: 'Lämna Tavla',
leaveProject_title: 'Lämna Projekt', leaveProject_title: 'Lämna Projekt',
list: 'Lista', list: 'Lista',
@ -126,11 +140,16 @@ export default {
projectNotFound_title: 'Projekt Ej Funnet', projectNotFound_title: 'Projekt Ej Funnet',
removeManager_title: 'Ta Bort Projektledare', removeManager_title: 'Ta Bort Projektledare',
removeMember_title: 'Ta Bort Medlem', removeMember_title: 'Ta Bort Medlem',
searchLabels: 'Sök etiketter...',
searchMembers: 'Sök medlemmar...',
searchUsers: 'Sök användare...',
seconds: 'Sekunder', seconds: 'Sekunder',
selectBoard: 'Välj tavla', selectBoard: 'Välj tavla',
selectList: 'Välj lista', selectList: 'Välj lista',
selectPermissions_title: 'Välj behörigheter',
selectProject: 'Välj projekt', selectProject: 'Välj projekt',
settings: 'Inställningar', settings: 'Inställningar',
sortList: 'Sortera listan',
stopwatch: 'Timer', stopwatch: 'Timer',
subscribeToMyOwnCardsByDefault: 'Prenumerera på mina egna kort som standard', subscribeToMyOwnCardsByDefault: 'Prenumerera på mina egna kort som standard',
taskActions_title: 'Uppgiftsåtgärder', taskActions_title: 'Uppgiftsåtgärder',
@ -147,6 +166,8 @@ export default {
username: 'Användarnamn', username: 'Användarnamn',
usernameAlreadyInUse: 'Användarnamnet används redan', usernameAlreadyInUse: 'Användarnamnet används redan',
users: 'Användare', users: 'Användare',
version: 'Version',
viewer: 'Visare',
writeComment: 'Skriv en kommentar...', writeComment: 'Skriv en kommentar...',
}, },
@ -158,10 +179,13 @@ export default {
addCard_title: 'Lägg Till Kort', addCard_title: 'Lägg Till Kort',
addComment: 'Lägg till kommentar', addComment: 'Lägg till kommentar',
addList: 'Lägg till lista', addList: 'Lägg till lista',
addMember: 'Lääg till medlem',
addMoreDetailedDescription: 'Lägg till en mer detaljerad beskrivning', addMoreDetailedDescription: 'Lägg till en mer detaljerad beskrivning',
addTask: 'Lägg till uppgift', addTask: 'Lägg till uppgift',
addToCard: 'Lägg till i kort', addToCard: 'Lägg till i kort',
addUser: 'Lägg till användare', addUser: 'Lägg till användare',
copy: 'Kopiera',
copyCard: 'Kopiera kort',
createBoard: 'Skapa tavla', createBoard: 'Skapa tavla',
createFile: 'Skapa fil', createFile: 'Skapa fil',
createLabel: 'Skapa etikett', createLabel: 'Skapa etikett',
@ -188,9 +212,12 @@ export default {
editDescription_title: 'Redigera Beskrivning', editDescription_title: 'Redigera Beskrivning',
editEmail_title: 'Redigera E-mail', editEmail_title: 'Redigera E-mail',
editPassword_title: 'Redigera Lösenord', editPassword_title: 'Redigera Lösenord',
editPermissions: 'Ändra behörigheter',
editStopwatch_title: 'Redigera Timer', editStopwatch_title: 'Redigera Timer',
editTitle_title: 'Redigera Titel', editTitle_title: 'Redigera Titel',
editUsername_title: 'Redigera Användarnamn', editUsername_title: 'Redigera Användarnamn',
hideDetails: 'Dölj detaljer',
import: 'Importera',
leaveBoard: 'Lämna tavla', leaveBoard: 'Lämna tavla',
leaveProject: 'Lämna projekt', leaveProject: 'Lämna projekt',
logOut_title: 'Logga ut', logOut_title: 'Logga ut',
@ -206,7 +233,16 @@ export default {
removeMember: 'Ta bort medlem', removeMember: 'Ta bort medlem',
save: 'Spara', save: 'Spara',
showAllAttachments: 'Visa alla bilagor ({{hidden}} dolda)', showAllAttachments: 'Visa alla bilagor ({{hidden}} dolda)',
showDetails: 'Visa detaljer',
showFewerAttachments: 'Visa färre bilagor', showFewerAttachments: 'Visa färre bilagor',
sortByCreatedAt: ' Sortera på skapad',
sortByCreatorUserIdList: ' Sortera på skapad av',
sortByDueDateList: ' Sortera på förfallodatum',
sortByIdList: ' Sortera på id',
sortByPositionList: ' Sortera på position',
sortByTitleList: ' Sortera på titel',
sortByUpdatedAtList: ' Sortera på uppdaterad',
sortList: 'Sortera listan',
start: 'Starta', start: 'Starta',
stop: 'Stoppa', stop: 'Stoppa',
subscribe: 'Prenumerera', subscribe: 'Prenumerera',

@ -189,6 +189,13 @@ export default class extends BaseModel {
Card.upsert(payload.card); Card.upsert(payload.card);
break; break;
case ActionTypes.CARD_COPY__SUCCESS:
Card.withId(payload.localId).delete();
Card.upsert(payload.card);
break;
case ActionTypes.CARD_CREATE_HANDLE: { case ActionTypes.CARD_CREATE_HANDLE: {
const cardModel = Card.upsert(payload.card); const cardModel = Card.upsert(payload.card);
@ -202,6 +209,7 @@ export default class extends BaseModel {
break; break;
} }
case ActionTypes.CARD_UPDATE: case ActionTypes.CARD_UPDATE:
Card.withId(payload.id).update(payload.data); Card.withId(payload.id).update(payload.data);

@ -79,16 +79,77 @@ export default class extends BaseModel {
break; break;
} }
case ActionTypes.LIST_SORT:
List.withId(payload.id).sortList();
break;
case ActionTypes.LIST_SORT__SUCCESS:
case ActionTypes.LIST_SORT_HANDLE: {
const listModel = List.withId(payload.list.id);
if (listModel) {
listModel.sortList();
}
break;
}
default: default:
} }
} }
getOrderedCardsQuerySet() { getOrderedByIdCardsQuerySet() {
return this.cards.orderBy('id');
}
getOrderedByPositionCardsQuerySet() {
return this.cards.orderBy('position'); return this.cards.orderBy('position');
} }
getOrderedByTitelCardsQuerySet() {
return this.cards.orderBy('name');
}
getOrderedByCreatedAtCardsQuerySet() {
return this.cards.orderBy('createdAt');
}
getOrderedByUpdatedAtCardsQuerySet() {
return this.cards.orderBy('updatedAt');
}
getOrderedByDueDateCardsQuerySet() {
return this.cards.orderBy('dueDate');
}
getFilteredOrderedCardsModelArray() { getFilteredOrderedCardsModelArray() {
let cardModels = this.getOrderedCardsQuerySet().toModelArray(); const sortby = this.selectedOption;
let cardModels = null;
switch (sortby) {
case 'name':
cardModels = this.getOrderedByTitelCardsQuerySet().toModelArray();
break;
case 'id':
cardModels = this.getOrderedByIdCardsQuerySet().toModelArray();
break;
case 'position':
cardModels = this.getOrderedByPositionCardsQuerySet().toModelArray();
break;
case 'createdAt':
cardModels = this.getOrderedByCreatedAtCardsQuerySet().toModelArray();
break;
case 'updatedAt':
cardModels = this.getOrderedByUpdatedAtCardsQuerySet().toModelArray();
break;
case 'dueDate':
cardModels = this.getOrderedByDueDateCardsQuerySet().toModelArray();
break;
case 'creatorUserId':
cardModels = this.getOrderedByDueDateCardsQuerySet().toModelArray();
break;
default:
cardModels = this.getOrderedByPositionCardsQuerySet().toModelArray();
}
const { filterText } = this.board; const { filterText } = this.board;

@ -72,7 +72,9 @@ export default class extends BaseModel {
break; break;
case ActionTypes.TASK_CREATE__SUCCESS: case ActionTypes.TASK_CREATE__SUCCESS:
Task.withId(payload.localId).delete(); if (Task.withId(payload.localId)) {
Task.withId(payload.localId).delete();
}
Task.upsert(payload.task); Task.upsert(payload.task);
break; break;

@ -1,4 +1,4 @@
import { call, put, select } from 'redux-saga/effects'; import { call, put, select, all } from 'redux-saga/effects';
import { goToBoard, goToCard } from './router'; import { goToBoard, goToCard } from './router';
import request from '../request'; import request from '../request';
@ -7,6 +7,9 @@ import actions from '../../../actions';
import api from '../../../api'; import api from '../../../api';
import i18n from '../../../i18n'; import i18n from '../../../i18n';
import { createLocalId } from '../../../utils/local-id'; import { createLocalId } from '../../../utils/local-id';
import { addLabelToCard } from './labels';
import { createTask } from './tasks';
import { addUserToCard } from './users';
export function* createCard(listId, data, autoOpen) { export function* createCard(listId, data, autoOpen) {
const { boardId } = yield select(selectors.selectListById, listId); const { boardId } = yield select(selectors.selectListById, listId);
@ -40,6 +43,30 @@ export function* createCard(listId, data, autoOpen) {
if (autoOpen) { if (autoOpen) {
yield call(goToCard, card.id); yield call(goToCard, card.id);
} }
// Add labels to card //
if (nextData.labels) {
const arr = [];
Object.keys(nextData.labels).map((key) => arr.push(nextData.labels[key].id));
yield all(arr?.map((label) => call(addLabelToCard, label, card.id)));
}
// Add tasks to card //
if (nextData.tasks) {
const tasks = [];
Object.keys(nextData.tasks)?.map((key) => tasks.push(nextData.tasks[key]));
tasks.forEach((task) => {
// eslint-disable-next-line no-param-reassign
task.id = `local:${task.id}`;
});
yield all(tasks?.map((task) => call(createTask, card.id, task)));
}
// Add users to card //
if (nextData.users) {
const users = [];
Object.keys(nextData.users)?.map((key) => users.push(nextData.users[key].id));
yield all(users?.map((user) => call(addUserToCard, user, card.id)));
}
} }
export function* handleCardCreate({ id }) { export function* handleCardCreate({ id }) {

@ -61,6 +61,24 @@ export function* handleListUpdate(list) {
yield put(actions.handleListUpdate(list)); yield put(actions.handleListUpdate(list));
} }
export function* sortList(id, data) {
yield put(actions.sortList(id, data));
let list;
try {
({ item: list } = yield call(request, api.sortList, id, data));
} catch (error) {
yield put(actions.sortList.failure(id, error));
return;
}
yield put(actions.sortList.success(list));
}
export function* handleListSort(list) {
yield put(actions.handleListSort(list));
}
export function* moveList(id, index) { export function* moveList(id, index) {
const { boardId } = yield select(selectors.selectListById, id); const { boardId } = yield select(selectors.selectListById, id);
const position = yield select(selectors.selectNextListPosition, boardId, index, id); const position = yield select(selectors.selectNextListPosition, boardId, index, id);

@ -7,10 +7,18 @@ import api from '../../../api';
import { createLocalId } from '../../../utils/local-id'; import { createLocalId } from '../../../utils/local-id';
export function* createTask(cardId, data) { export function* createTask(cardId, data) {
const nextData = { let nextData = {};
...data, if (data.position) {
position: yield select(selectors.selectNextTaskPosition, cardId), nextData = {
}; ...data,
position: data.position,
};
} else {
nextData = {
...data,
position: yield select(selectors.selectNextTaskPosition, cardId),
};
}
const localId = yield call(createLocalId); const localId = yield call(createLocalId);

@ -39,8 +39,13 @@ export default function* cardsWatchers() {
takeEvery(EntryActionTypes.CARD_DELETE_HANDLE, ({ payload: { card } }) => takeEvery(EntryActionTypes.CARD_DELETE_HANDLE, ({ payload: { card } }) =>
services.handleCardDelete(card), services.handleCardDelete(card),
), ),
takeEvery(EntryActionTypes.CARD_COPY, ({ payload: { listId, data, autoOpen } }) =>
services.createCard(listId, data, autoOpen),
takeEvery(EntryActionTypes.TEXT_FILTER_IN_CURRENT_BOARD, ({ payload: { text } }) => takeEvery(EntryActionTypes.TEXT_FILTER_IN_CURRENT_BOARD, ({ payload: { text } }) =>
services.handleTextFilter(text), services.handleTextFilter(text),
), ),
]); ]);
} }

@ -17,6 +17,9 @@ export default function* listsWatchers() {
takeEvery(EntryActionTypes.LIST_UPDATE_HANDLE, ({ payload: { list } }) => takeEvery(EntryActionTypes.LIST_UPDATE_HANDLE, ({ payload: { list } }) =>
services.handleListUpdate(list), services.handleListUpdate(list),
), ),
takeEvery(EntryActionTypes.LIST_SORT_HANDLE, ({ payload: { list } }) =>
services.handleListSort(list),
),
takeEvery(EntryActionTypes.LIST_MOVE, ({ payload: { id, index } }) => takeEvery(EntryActionTypes.LIST_MOVE, ({ payload: { id, index } }) =>
services.moveList(id, index), services.moveList(id, index),
), ),

@ -92,6 +92,10 @@ const createSocketEventsChannel = () =>
emit(entryActions.handleListDelete(item)); emit(entryActions.handleListDelete(item));
}; };
const handleListSort = ({ item }) => {
emit(entryActions.handleListSort(item));
};
const handleLabelCreate = ({ item }) => { const handleLabelCreate = ({ item }) => {
emit(entryActions.handleLabelCreate(item)); emit(entryActions.handleLabelCreate(item));
}; };
@ -204,6 +208,7 @@ const createSocketEventsChannel = () =>
socket.on('listUpdate', handleListUpdate); socket.on('listUpdate', handleListUpdate);
socket.on('listSort', handleListSort); socket.on('listSort', handleListSort);
socket.on('listDelete', handleListDelete); socket.on('listDelete', handleListDelete);
socket.on('listSort', handleListSort);
socket.on('labelCreate', handleLabelCreate); socket.on('labelCreate', handleLabelCreate);
socket.on('labelUpdate', handleLabelUpdate); socket.on('labelUpdate', handleLabelUpdate);
@ -263,6 +268,7 @@ const createSocketEventsChannel = () =>
socket.off('listUpdate', handleListUpdate); socket.off('listUpdate', handleListUpdate);
socket.off('listSort', handleListSort); socket.off('listSort', handleListSort);
socket.off('listDelete', handleListDelete); socket.off('listDelete', handleListDelete);
socket.off('listSort', handleListSort);
socket.off('labelCreate', handleLabelCreate); socket.off('labelCreate', handleLabelCreate);
socket.off('labelUpdate', handleLabelUpdate); socket.off('labelUpdate', handleLabelUpdate);

1452
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -9,6 +9,7 @@ const Types = {
CREATE_CARD: 'createCard', CREATE_CARD: 'createCard',
MOVE_CARD: 'moveCard', MOVE_CARD: 'moveCard',
COMMENT_CARD: 'commentCard', COMMENT_CARD: 'commentCard',
COPY_CARD: 'copyCard',
}; };
module.exports = { module.exports = {

Loading…
Cancel
Save