feat: Add ability to copy/cut cards with shortcut support

This commit is contained in:
Maksim Eltyshev
2025-12-09 14:58:01 +01:00
parent 52acc9de90
commit 9e6e38fcf7
113 changed files with 1616 additions and 259 deletions
+89
View File
@@ -160,6 +160,72 @@ const handleCardUpdate = (
},
});
const transferCard = (id, data) => ({
type: ActionTypes.CARD_TRANSFER,
payload: {
id,
data,
},
});
transferCard.success = (
card,
users,
cardMemberships,
cardLabels,
taskLists,
tasks,
attachments,
customFieldGroups,
customFields,
customFieldValues,
) => ({
type: ActionTypes.CARD_TRANSFER__SUCCESS,
payload: {
card,
users,
cardMemberships,
cardLabels,
taskLists,
tasks,
attachments,
customFieldGroups,
customFields,
customFieldValues,
},
});
transferCard.failure = (
id,
error,
card,
users,
cardMemberships,
cardLabels,
taskLists,
tasks,
attachments,
customFieldGroups,
customFields,
customFieldValues,
) => ({
type: ActionTypes.CARD_TRANSFER__FAILURE,
payload: {
id,
error,
card,
users,
cardMemberships,
cardLabels,
taskLists,
tasks,
attachments,
customFieldGroups,
customFields,
customFieldValues,
},
});
const duplicateCard = (id, localId, data) => ({
type: ActionTypes.CARD_DUPLICATE,
payload: {
@@ -204,6 +270,25 @@ duplicateCard.failure = (localId, error) => ({
},
});
const copyCard = (id) => ({
type: ActionTypes.CARD_COPY,
payload: {
id,
},
});
const cutCard = (id) => ({
type: ActionTypes.CARD_CUT,
payload: {
id,
},
});
const pasteCard = () => ({
type: ActionTypes.CARD_PASTE,
payload: {},
});
const deleteCard = (id) => ({
type: ActionTypes.CARD_DELETE,
payload: {
@@ -240,7 +325,11 @@ export default {
handleCardCreate,
updateCard,
handleCardUpdate,
transferCard,
duplicateCard,
copyCard,
cutCard,
pasteCard,
deleteCard,
handleCardDelete,
};
@@ -30,12 +30,17 @@ const EndlessContent = React.memo(() => {
[dispatch],
);
const handleCardPaste = useCallback(() => {
dispatch(entryActions.pasteCardInCurrentList());
}, [dispatch]);
const viewProps = {
cardIds,
isCardsFetching,
isAllCardsFetched,
onCardsFetch: handleCardsFetch,
onCardCreate: handleCardCreate,
onCardPaste: handleCardPaste,
};
let View;
@@ -26,6 +26,10 @@ const FiniteContent = React.memo(() => {
[dispatch],
);
const handleCardPaste = useCallback(() => {
dispatch(entryActions.pasteCardInCurrentContext());
}, [dispatch]);
let View;
switch (board.view) {
case BoardViews.GRID:
@@ -39,7 +43,13 @@ const FiniteContent = React.memo(() => {
default:
}
return <View cardIds={cardIds} onCardCreate={canAddCard ? handleCardCreate : undefined} />;
return (
<View
cardIds={cardIds}
onCardCreate={canAddCard ? handleCardCreate : undefined}
onCardPaste={canAddCard ? handleCardPaste : undefined}
/>
);
});
export default FiniteContent;
+41 -17
View File
@@ -5,10 +5,11 @@
import React, { useCallback, useState } from 'react';
import PropTypes from 'prop-types';
import { useSelector } from 'react-redux';
import classNames from 'classnames';
import { shallowEqual, useSelector } from 'react-redux';
import { useInView } from 'react-intersection-observer';
import { useTranslation } from 'react-i18next';
import { Button, Loader } from 'semantic-ui-react';
import { Button, Icon, Loader } from 'semantic-ui-react';
import { useWindowWidth } from '../../../lib/hooks';
import { Masonry } from '../../../lib/custom-ui';
@@ -21,11 +22,18 @@ import PlusMathIcon from '../../../assets/images/plus-math-icon.svg?react';
import styles from './GridView.module.scss';
const GridView = React.memo(
({ cardIds, isCardsFetching, isAllCardsFetched, onCardsFetch, onCardCreate }) => {
const canAddCard = useSelector((state) => {
({ cardIds, isCardsFetching, isAllCardsFetched, onCardsFetch, onCardCreate, onCardPaste }) => {
const clipboard = useSelector(selectors.selectClipboard);
const { canAddCard, canPasteCard } = useSelector((state) => {
const boardMembership = selectors.selectCurrentUserMembershipForCurrentBoard(state);
return !!boardMembership && boardMembership.role === BoardMembershipRoles.EDITOR;
});
const isEditor = !!boardMembership && boardMembership.role === BoardMembershipRoles.EDITOR;
return {
canAddCard: isEditor,
canPasteCard: isEditor,
};
}, shallowEqual);
const [t] = useTranslation();
const [isAddCardOpened, setIsAddCardOpened] = useState(false);
@@ -59,17 +67,31 @@ const GridView = React.memo(
<AddCard onCreate={onCardCreate} onClose={handleAddCardClose} />
</div>
) : (
<Button
type="button"
disabled={!onCardCreate}
className={styles.addCardButton}
onClick={handleAddCardClick}
>
<PlusMathIcon className={styles.addCardButtonIcon} />
<span className={styles.addCardButtonText}>
{onCardCreate ? t('action.addCard') : t('common.atLeastOneListMustBePresent')}
</span>
</Button>
<div>
<div className={styles.addCardButtonWrapper}>
<Button
type="button"
disabled={!onCardCreate}
className={styles.addCardButton}
onClick={handleAddCardClick}
>
<PlusMathIcon className={styles.addCardButtonIcon} />
<span className={styles.addCardButtonText}>
{onCardCreate ? t('action.addCard') : t('common.atLeastOneListMustBePresent')}
</span>
</Button>
{onCardPaste && clipboard && canPasteCard && (
<Button
type="button"
disabled={!onCardCreate}
className={classNames(styles.addCardButton, styles.paste)}
onClick={onCardPaste}
>
<Icon fitted name="paste" />
</Button>
)}
</div>
</div>
))}
{cardIds.map((cardId) => (
<div key={cardId} className={styles.card}>
@@ -97,6 +119,7 @@ GridView.propTypes = {
isAllCardsFetched: PropTypes.bool,
onCardsFetch: PropTypes.func,
onCardCreate: PropTypes.func,
onCardPaste: PropTypes.func,
};
GridView.defaultProps = {
@@ -104,6 +127,7 @@ GridView.defaultProps = {
isAllCardsFetched: undefined,
onCardsFetch: undefined,
onCardCreate: undefined,
onCardPaste: undefined,
};
export default GridView;
@@ -10,16 +10,16 @@
border-radius: 3px;
color: rgba(255, 255, 255, 0.72);
cursor: pointer;
display: block;
fill: rgba(255, 255, 255, 0.72);
flex: 1;
font-weight: normal;
height: 42px;
margin: 0;
min-height: 42px;
padding: 11px;
text-align: left;
transition: background 85ms ease-in, opacity 40ms ease-in,
border-color 85ms ease-in;
width: 100%;
&:active {
outline: none;
@@ -28,6 +28,10 @@
&:hover {
background: rgba(0, 0, 0, 0.32);
}
&.paste {
flex: 0 0 auto;
}
}
.addCardButtonIcon {
@@ -43,6 +47,11 @@
vertical-align: top;
}
.addCardButtonWrapper {
display: flex;
gap: 6px;
}
.card {
background: rgba(223, 227, 230, 0.8);
border-radius: 3px;
+38 -17
View File
@@ -6,10 +6,10 @@
import React, { useCallback, useState } from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { useSelector } from 'react-redux';
import { shallowEqual, useSelector } from 'react-redux';
import { useInView } from 'react-intersection-observer';
import { useTranslation } from 'react-i18next';
import { Button, Loader } from 'semantic-ui-react';
import { Button, Icon, Loader } from 'semantic-ui-react';
import selectors from '../../../selectors';
import { BoardMembershipRoles } from '../../../constants/Enums';
@@ -20,11 +20,18 @@ import PlusMathIcon from '../../../assets/images/plus-math-icon.svg?react';
import styles from './ListView.module.scss';
const ListView = React.memo(
({ cardIds, isCardsFetching, isAllCardsFetched, onCardsFetch, onCardCreate }) => {
const canAddCard = useSelector((state) => {
({ cardIds, isCardsFetching, isAllCardsFetched, onCardsFetch, onCardCreate, onCardPaste }) => {
const clipboard = useSelector(selectors.selectClipboard);
const { canAddCard, canPasteCard } = useSelector((state) => {
const boardMembership = selectors.selectCurrentUserMembershipForCurrentBoard(state);
return !!boardMembership && boardMembership.role === BoardMembershipRoles.EDITOR;
});
const isEditor = !!boardMembership && boardMembership.role === BoardMembershipRoles.EDITOR;
return {
canAddCard: isEditor,
canPasteCard: isEditor,
};
}, shallowEqual);
const [t] = useTranslation();
const [isAddCardOpened, setIsAddCardOpened] = useState(false);
@@ -54,17 +61,29 @@ const ListView = React.memo(
<AddCard onCreate={onCardCreate} onClose={handleAddCardClose} />
</div>
) : (
<Button
type="button"
disabled={!onCardCreate}
className={styles.addCardButton}
onClick={handleAddCardClick}
>
<PlusMathIcon className={styles.addCardButtonIcon} />
<span className={styles.addCardButtonText}>
{onCardCreate ? t('action.addCard') : t('common.atLeastOneListMustBePresent')}
</span>
</Button>
<div className={styles.addCardButtonWrapper}>
<Button
type="button"
disabled={!onCardCreate}
className={styles.addCardButton}
onClick={handleAddCardClick}
>
<PlusMathIcon className={styles.addCardButtonIcon} />
<span className={styles.addCardButtonText}>
{onCardCreate ? t('action.addCard') : t('common.atLeastOneListMustBePresent')}
</span>
</Button>
{onCardPaste && clipboard && canPasteCard && (
<Button
type="button"
disabled={!onCardCreate}
className={classNames(styles.addCardButton, styles.paste)}
onClick={onCardPaste}
>
<Icon fitted name="paste" />
</Button>
)}
</div>
))}
{cardIds.length > 0 && (
<div className={classNames(styles.segment, styles.cards)}>
@@ -95,6 +114,7 @@ ListView.propTypes = {
isAllCardsFetched: PropTypes.bool,
onCardsFetch: PropTypes.func,
onCardCreate: PropTypes.func,
onCardPaste: PropTypes.func,
};
ListView.defaultProps = {
@@ -102,6 +122,7 @@ ListView.defaultProps = {
isAllCardsFetched: undefined,
onCardsFetch: undefined,
onCardCreate: undefined,
onCardPaste: undefined,
};
export default ListView;
@@ -12,14 +12,14 @@
cursor: pointer;
display: block;
fill: rgba(255, 255, 255, 0.72);
flex: 1;
font-weight: normal;
height: 42px;
margin-bottom: 12px;
margin: 0;
padding: 11px;
text-align: left;
transition: background 85ms ease-in, opacity 40ms ease-in,
border-color 85ms ease-in;
width: 100%;
&:active {
outline: none;
@@ -28,6 +28,10 @@
&:hover {
background: rgba(0, 0, 0, 0.32);
}
&.paste {
flex: 0 0 auto;
}
}
.addCardButtonIcon {
@@ -43,6 +47,12 @@
vertical-align: top;
}
.addCardButtonWrapper {
display: flex;
gap: 6px;
margin-bottom: 12px;
}
.card {
margin-bottom: 6px;
}
@@ -18,15 +18,34 @@ import { isActiveTextElement } from '../../../utils/element-helpers';
import { isModifierKeyPressed } from '../../../utils/event-helpers';
import { BoardShortcutsContext } from '../../../contexts';
import Paths from '../../../constants/Paths';
import { BoardMembershipRoles, ListTypes } from '../../../constants/Enums';
import {
BoardContexts,
BoardMembershipRoles,
BoardViews,
ListTypes,
} from '../../../constants/Enums';
import CardActionsStep from '../../cards/CardActionsStep';
const canCopyCard = (isManager, boardMembership) => {
if (isManager) {
return true;
}
return !!boardMembership && boardMembership.role === BoardMembershipRoles.EDITOR;
};
const canCutCard = (boardMembership) =>
!!boardMembership && boardMembership.role === BoardMembershipRoles.EDITOR;
const canPasteCard = (boardMembership) =>
!!boardMembership && boardMembership.role === BoardMembershipRoles.EDITOR;
const canEditCardName = (boardMembership, list) => {
if (isListArchiveOrTrash(list)) {
return false;
}
return boardMembership && boardMembership.role === BoardMembershipRoles.EDITOR;
return !!boardMembership && boardMembership.role === BoardMembershipRoles.EDITOR;
};
const canArchiveCard = (boardMembership, list) => {
@@ -34,7 +53,7 @@ const canArchiveCard = (boardMembership, list) => {
return false;
}
return boardMembership && boardMembership.role === BoardMembershipRoles.EDITOR;
return !!boardMembership && boardMembership.role === BoardMembershipRoles.EDITOR;
};
const canUseCardMembers = (boardMembership, list) => {
@@ -42,7 +61,7 @@ const canUseCardMembers = (boardMembership, list) => {
return false;
}
return boardMembership && boardMembership.role === BoardMembershipRoles.EDITOR;
return !!boardMembership && boardMembership.role === BoardMembershipRoles.EDITOR;
};
const canUseCardLabels = (boardMembership, list) => {
@@ -50,7 +69,7 @@ const canUseCardLabels = (boardMembership, list) => {
return false;
}
return boardMembership && boardMembership.role === BoardMembershipRoles.EDITOR;
return !!boardMembership && boardMembership.role === BoardMembershipRoles.EDITOR;
};
const ShortcutsProvider = React.memo(({ children }) => {
@@ -58,8 +77,20 @@ const ShortcutsProvider = React.memo(({ children }) => {
const dispatch = useDispatch();
const selectedListRef = useRef(null);
const selectedCardRef = useRef(null);
const handleListMouseEnter = useCallback((id, onPaste) => {
selectedListRef.current = {
id,
onPaste,
};
}, []);
const handleListMouseLeave = useCallback(() => {
selectedListRef.current = null;
}, []);
const handleCardMouseEnter = useCallback((id, editName, openActions) => {
selectedCardRef.current = {
id,
@@ -73,15 +104,106 @@ const ShortcutsProvider = React.memo(({ children }) => {
}, []);
const contextValue = useMemo(
() => [handleCardMouseEnter, handleCardMouseLeave],
[handleCardMouseEnter, handleCardMouseLeave],
() => [handleListMouseEnter, handleListMouseLeave, handleCardMouseEnter, handleCardMouseLeave],
[handleListMouseEnter, handleListMouseLeave, handleCardMouseEnter, handleCardMouseLeave],
);
useDidUpdate(() => {
selectedListRef.current = null;
selectedCardRef.current = null;
}, [cardId, boardId]);
useEffect(() => {
const handleCardCopy = (event) => {
if (!selectedCardRef.current) {
return;
}
const state = store.getState();
const card = selectors.selectCardById(state, selectedCardRef.current.id);
if (!card || !card.isPersisted) {
return;
}
const isManager = selectors.selectIsCurrentUserManagerForCurrentProject(state);
const boardMembership = selectors.selectCurrentUserMembershipForCurrentBoard(state);
if (!canCopyCard(isManager, boardMembership)) {
return;
}
event.preventDefault();
dispatch(entryActions.copyCard(card.id));
};
const handleCardCut = (event) => {
if (!selectedCardRef.current) {
return;
}
const state = store.getState();
const card = selectors.selectCardById(state, selectedCardRef.current.id);
if (!card || !card.isPersisted) {
return;
}
const boardMembership = selectors.selectCurrentUserMembershipForCurrentBoard(state);
if (!canCutCard(boardMembership)) {
return;
}
event.preventDefault();
dispatch(entryActions.cutCard(card.id));
};
const handleCardPaste = (event) => {
const state = store.getState();
const clipboard = selectors.selectClipboard(state);
if (!clipboard) {
return;
}
const board = selectors.selectCurrentBoard(state);
let listId;
if (board.context === BoardContexts.BOARD) {
if (board.view === BoardViews.KANBAN) {
listId = selectedListRef.current?.id;
} else {
listId = selectors.selectFirstKanbanListId(state);
}
} else {
listId = selectors.selectCurrentListId(state);
}
if (!listId) {
return;
}
const list = selectors.selectListById(state, listId);
if (!list || !list.isPersisted) {
return;
}
const boardMembership = selectors.selectCurrentUserMembershipForCurrentBoard(state);
if (!canPasteCard(boardMembership)) {
return;
}
event.preventDefault();
dispatch(entryActions.pasteCard(list.id));
if (selectedListRef.current) {
selectedListRef.current.onPaste();
}
};
const handleCardOpen = (event) => {
if (!selectedCardRef.current) {
return;
@@ -234,6 +356,22 @@ const ShortcutsProvider = React.memo(({ children }) => {
}
if (isModifierKeyPressed(event)) {
switch (event.key) {
case 'c':
handleCardCopy(event);
break;
case 'x':
handleCardCut(event);
break;
case 'v':
handleCardPaste(event);
break;
default:
}
return;
}
+18 -2
View File
@@ -16,6 +16,7 @@ import { closePopup, usePopup } from '../../../lib/popup';
import selectors from '../../../selectors';
import { BoardShortcutsContext } from '../../../contexts';
import Paths from '../../../constants/Paths';
import ClipboardTypes from '../../../constants/ClipboardTypes';
import { BoardMembershipRoles, CardTypes } from '../../../constants/Enums';
import ProjectContent from './ProjectContent';
import StoryContent from './StoryContent';
@@ -44,14 +45,25 @@ const Card = React.memo(({ id, isInline }) => {
return selectIsCardWithIdRecent(state, id);
});
const isCut = useSelector((state) => {
const clipboard = selectors.selectClipboard(state);
return clipboard && clipboard.type === ClipboardTypes.CUT && card.id === clipboard.cardId;
});
const canUseActions = useSelector((state) => {
const isManager = selectors.selectIsCurrentUserManagerForCurrentProject(state);
if (isManager) {
return true;
}
const boardMembership = selectors.selectCurrentUserMembershipForCurrentBoard(state);
return !!boardMembership && boardMembership.role === BoardMembershipRoles.EDITOR;
});
const dispatch = useDispatch();
const [isEditNameOpened, setIsEditNameOpened] = useState(false);
const [handleCardMouseEnter, handleCardMouseLeave] = useContext(BoardShortcutsContext);
const [, , handleCardMouseEnter, handleCardMouseLeave] = useContext(BoardShortcutsContext);
const actionsPopupRef = useRef(null);
@@ -139,7 +151,11 @@ const Card = React.memo(({ id, isInline }) => {
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,
jsx-a11y/no-static-element-interactions */}
<div
className={classNames(styles.content, card.isClosed && styles.contentDisabled)}
className={classNames(
styles.content,
card.isClosed && styles.contentDisabled,
isCut && styles.contentCut,
)}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleCardMouseLeave}
onClick={handleClick}
@@ -44,6 +44,10 @@
}
}
.contentCut {
opacity: 0.5;
}
.contentDisabled {
filter: saturate(0.5);
opacity: 0.64;
@@ -60,6 +60,8 @@ const CardActionsStep = React.memo(({ cardId, defaultStep, onNameEdit, onClose }
canEditName,
canEditDueDate,
canEditStopwatch,
canCopy,
canCut,
canDuplicate,
canMove,
canRestore,
@@ -68,6 +70,8 @@ const CardActionsStep = React.memo(({ cardId, defaultStep, onNameEdit, onClose }
canUseMembers,
canUseLabels,
} = useSelector((state) => {
const isManager = selectors.selectIsCurrentUserManagerForCurrentProject(state);
const boardMembership = selectors.selectCurrentUserMembershipForCurrentBoard(state);
const isEditor = !!boardMembership && boardMembership.role === BoardMembershipRoles.EDITOR;
@@ -77,6 +81,8 @@ const CardActionsStep = React.memo(({ cardId, defaultStep, onNameEdit, onClose }
canEditName: false,
canEditDueDate: false,
canEditStopwatch: false,
canCopy: isManager || isEditor,
canCut: isEditor,
canDuplicate: false,
canMove: false,
canRestore: isEditor,
@@ -92,6 +98,8 @@ const CardActionsStep = React.memo(({ cardId, defaultStep, onNameEdit, onClose }
canEditName: isEditor,
canEditDueDate: isEditor,
canEditStopwatch: isEditor,
canCopy: isManager || isEditor,
canCut: isEditor,
canDuplicate: isEditor,
canMove: isEditor,
canRestore: null,
@@ -117,17 +125,20 @@ const CardActionsStep = React.memo(({ cardId, defaultStep, onNameEdit, onClose }
[cardId, dispatch],
);
const handleDuplicateClick = useCallback(() => {
dispatch(
entryActions.duplicateCard(cardId, {
name: `${card.name} (${t('common.copy', {
context: 'inline',
})})`,
}),
);
const handleCopyClick = useCallback(() => {
dispatch(entryActions.copyCard(cardId));
onClose();
}, [cardId, onClose, card.name, dispatch, t]);
}, [cardId, onClose, dispatch]);
const handleCutClick = useCallback(() => {
dispatch(entryActions.cutCard(cardId));
onClose();
}, [cardId, onClose, dispatch]);
const handleDuplicateClick = useCallback(() => {
dispatch(entryActions.duplicateCard(cardId));
onClose();
}, [cardId, onClose, dispatch]);
const handleRestoreClick = useCallback(() => {
dispatch(entryActions.moveCard(cardId, card.prevListId, undefined, true));
@@ -344,6 +355,22 @@ const CardActionsStep = React.memo(({ cardId, defaultStep, onNameEdit, onClose }
})}
</Menu.Item>
)}
{canCopy && (
<Menu.Item className={styles.menuItem} onClick={handleCopyClick}>
<Icon name="copy outline" className={styles.menuItemIcon} />
{t('action.copyCard', {
context: 'title',
})}
</Menu.Item>
)}
{canCut && (
<Menu.Item className={styles.menuItem} onClick={handleCutClick}>
<Icon name="cut" className={styles.menuItemIcon} />
{t('action.cutCard', {
context: 'title',
})}
</Menu.Item>
)}
{canDuplicate && (
<Menu.Item className={styles.menuItem} onClick={handleDuplicateClick}>
<Icon name="copy outline" className={styles.menuItemIcon} />
@@ -34,17 +34,17 @@ const MoreActionsStep = React.memo(({ onClose }) => {
const { canEditType, canDuplicate, canMove } = useSelector((state) => {
const list = selectListById(state, card.listId);
const boardMembership = selectors.selectCurrentUserMembershipForCurrentBoard(state);
const isEditor = !!boardMembership && boardMembership.role === BoardMembershipRoles.EDITOR;
if (isListArchiveOrTrash(list)) {
return {
canEditType: false,
canDuplicate: false,
canMove: false,
canMove: isEditor,
};
}
const boardMembership = selectors.selectCurrentUserMembershipForCurrentBoard(state);
const isEditor = !!boardMembership && boardMembership.role === BoardMembershipRoles.EDITOR;
return {
canEditType: isEditor,
canDuplicate: isEditor,
@@ -68,14 +68,8 @@ const MoreActionsStep = React.memo(({ onClose }) => {
);
const handleDuplicateClick = useCallback(() => {
dispatch(
entryActions.duplicateCurrentCard({
name: `${card.name} (${t('common.copy', {
context: 'inline',
})})`,
}),
);
}, [card.name, dispatch, t]);
dispatch(entryActions.duplicateCurrentCard());
}, [dispatch]);
const handleEditTypeClick = useCallback(() => {
openStep(StepTypes.EDIT_TYPE);
@@ -105,7 +105,7 @@ const ProjectContent = React.memo(() => {
canSubscribe: isMember,
canJoin: false,
canDuplicate: false,
canMove: false,
canMove: isEditor,
canRestore: isEditor,
canArchive: isEditor,
canDelete: isEditor,
@@ -104,7 +104,7 @@ const StoryContent = React.memo(() => {
canSubscribe: isMember,
canJoin: false,
canDuplicate: false,
canMove: false,
canMove: isEditor,
canRestore: isEditor,
canArchive: isEditor,
canDelete: isEditor,
@@ -7,7 +7,7 @@ import React from 'react';
import { useTranslation } from 'react-i18next';
import { Icon, Message } from 'semantic-ui-react';
const FileIsTooBig = React.memo(() => {
const FileIsTooBigToast = React.memo(() => {
const [t] = useTranslation();
return (
@@ -18,4 +18,4 @@ const FileIsTooBig = React.memo(() => {
);
});
export default FileIsTooBig;
export default FileIsTooBigToast;
@@ -7,7 +7,7 @@ import React from 'react';
import { useTranslation } from 'react-i18next';
import { Icon, Message } from 'semantic-ui-react';
const NotEnoughStorage = React.memo(() => {
const NotEnoughStorageToast = React.memo(() => {
const [t] = useTranslation();
return (
@@ -18,4 +18,4 @@ const NotEnoughStorage = React.memo(() => {
);
});
export default NotEnoughStorage;
export default NotEnoughStorageToast;
@@ -0,0 +1,21 @@
/*!
* Copyright (c) 2024 PLANKA Software GmbH
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
import React from 'react';
import { useTranslation } from 'react-i18next';
import { Icon, Message } from 'semantic-ui-react';
const SourceCardNotCopyableToast = React.memo(() => {
const [t] = useTranslation();
return (
<Message visible negative size="tiny">
<Icon name="paste" />
{t('common.sourceCardIsNoLongerAvailableForCopying')}
</Message>
);
});
export default SourceCardNotCopyableToast;
@@ -0,0 +1,21 @@
/*!
* Copyright (c) 2024 PLANKA Software GmbH
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
import React from 'react';
import { useTranslation } from 'react-i18next';
import { Icon, Message } from 'semantic-ui-react';
const SourceCardNotMovableToast = React.memo(() => {
const [t] = useTranslation();
return (
<Message visible negative size="tiny">
<Icon name="paste" />
{t('common.sourceCardIsNoLongerAvailableForMoving')}
</Message>
);
});
export default SourceCardNotMovableToast;
@@ -7,14 +7,18 @@ import React from 'react';
import { Toaster as HotToaster, ToastBar as HotToastBar } from 'react-hot-toast';
import ToastTypes from '../../../constants/ToastTypes';
import FileIsTooBig from './FileIsTooBig';
import NotEnoughStorage from './NotEnoughStorage';
import FileIsTooBigToast from './FileIsTooBigToast';
import NotEnoughStorageToast from './NotEnoughStorageToast';
import EmptyTrashToast from './EmptyTrashToast';
import SourceCardNotCopyableToast from './SourceCardNotCopyableToast';
import SourceCardNotMovableToast from './SourceCardNotMovableToast';
const TOAST_BY_TYPE = {
[ToastTypes.FILE_IS_TOO_BIG]: FileIsTooBig,
[ToastTypes.NOT_ENOUGH_STORAGE]: NotEnoughStorage,
[ToastTypes.FILE_IS_TOO_BIG]: FileIsTooBigToast,
[ToastTypes.NOT_ENOUGH_STORAGE]: NotEnoughStorageToast,
[ToastTypes.EMPTY_TRASH]: EmptyTrashToast,
[ToastTypes.SOURCE_CARD_NOT_COPYABLE]: SourceCardNotCopyableToast,
[ToastTypes.SOURCE_CARD_NOT_MOVABLE]: SourceCardNotMovableToast,
};
const Toaster = React.memo(() => (
+62 -24
View File
@@ -5,18 +5,19 @@
import upperFirst from 'lodash/upperFirst';
import camelCase from 'lodash/camelCase';
import React, { useCallback, useMemo, useRef, useState } from 'react';
import React, { useCallback, useContext, useMemo, useRef, useState } from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import { useTranslation } from 'react-i18next';
import { Draggable, Droppable } from 'react-beautiful-dnd';
import { Button, Icon } from 'semantic-ui-react';
import { useDidUpdate, useTransitioning } from '../../../lib/hooks';
import { useDidUpdate, useToggle, useTransitioning } from '../../../lib/hooks';
import { usePopup } from '../../../lib/popup';
import selectors from '../../../selectors';
import entryActions from '../../../entry-actions';
import { BoardShortcutsContext } from '../../../contexts';
import DroppableTypes from '../../../constants/DroppableTypes';
import { BoardMembershipRoles, ListTypes } from '../../../constants/Enums';
import { ListTypeIcons } from '../../../constants/Icons';
@@ -47,28 +48,36 @@ const List = React.memo(({ id, index }) => {
[],
);
const clipboard = useSelector(selectors.selectClipboard);
const isFavoritesActive = useSelector(selectors.selectIsFavoritesActiveForCurrentUser);
const list = useSelector((state) => selectListById(state, id));
const cardIds = useSelector((state) => selectFilteredCardIdsByListId(state, id));
const { canEdit, canArchiveCards, canAddCard, canDropCard } = useSelector((state) => {
const isEditModeEnabled = selectors.selectIsEditModeEnabled(state); // TODO: move out?
const { canEdit, canArchiveCards, canAddCard, canPasteCard, canDropCard } = useSelector(
(state) => {
const isEditModeEnabled = selectors.selectIsEditModeEnabled(state); // TODO: move out?
const boardMembership = selectors.selectCurrentUserMembershipForCurrentBoard(state);
const isEditor = !!boardMembership && boardMembership.role === BoardMembershipRoles.EDITOR;
const boardMembership = selectors.selectCurrentUserMembershipForCurrentBoard(state);
const isEditor = !!boardMembership && boardMembership.role === BoardMembershipRoles.EDITOR;
return {
canEdit: isEditModeEnabled && isEditor,
canArchiveCards: list.type === ListTypes.CLOSED && isEditor,
canAddCard: isEditor,
canDropCard: isEditor,
};
}, shallowEqual);
return {
canEdit: isEditModeEnabled && isEditor,
canArchiveCards: list.type === ListTypes.CLOSED && isEditor,
canAddCard: isEditor,
canPasteCard: isEditor,
canDropCard: isEditor,
};
},
shallowEqual,
);
const dispatch = useDispatch();
const [t] = useTranslation();
const [isEditNameOpened, setIsEditNameOpened] = useState(false);
const [addCardPosition, setAddCardPosition] = useState(null);
const [scrollBottomState, scrollBottom] = useToggle();
const [handleListMouseEnter, handleListMouseLeave] = useContext(BoardShortcutsContext);
const wrapperRef = useRef(null);
const cardsWrapperRef = useRef(null);
@@ -82,6 +91,17 @@ const List = React.memo(({ id, index }) => {
[id, dispatch, addCardPosition],
);
const handlePasteCardClick = useCallback(() => {
dispatch(entryActions.pasteCard(id));
scrollBottom();
}, [id, dispatch, scrollBottom]);
const handleMouseEnter = useCallback(() => {
handleListMouseEnter(id, () => {
scrollBottom();
});
}, [id, scrollBottom, handleListMouseEnter]);
const handleHeaderClick = useCallback(() => {
if (list.isPersisted && canEdit) {
setIsEditNameOpened(true);
@@ -123,6 +143,10 @@ const List = React.memo(({ id, index }) => {
addCardPosition === AddCardPositions.TOP ? 0 : cardsWrapperRef.current.scrollHeight;
}, [cardIds, addCardPosition]);
useDidUpdate(() => {
cardsWrapperRef.current.scrollTop = cardsWrapperRef.current.scrollHeight;
}, [scrollBottomState]);
const ActionsPopup = usePopup(ActionsStep);
const ArchiveCardsPopup = usePopup(ArchiveCardsStep);
@@ -169,6 +193,8 @@ const List = React.memo(({ id, index }) => {
data-drag-scroller
ref={innerRef}
className={styles.innerWrapper}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleListMouseLeave}
>
<div
ref={wrapperRef}
@@ -231,17 +257,29 @@ const List = React.memo(({ id, index }) => {
<div className={styles.cardsOuterWrapper}>{cardsNode}</div>
</div>
{!addCardPosition && canAddCard && (
<button
type="button"
disabled={!list.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 className={styles.addCardButtonWrapper}>
<button
type="button"
disabled={!list.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>
{clipboard && canPasteCard && (
<button
type="button"
disabled={!list.isPersisted}
className={classNames(styles.addCardButton, styles.paste)}
onClick={handlePasteCardClick}
>
<Icon name="paste" />
</button>
)}
</div>
)}
</div>
</div>
@@ -15,19 +15,22 @@
cursor: pointer;
display: block;
fill: #6b808c;
flex: 0 0 auto;
flex: 1;
font-weight: normal;
height: 36px;
outline: none;
padding: 8px;
text-align: left;
width: 100%;
&:hover {
background: #c3cbd0;
color: #17394d;
fill: #17394d;
}
&.paste {
flex: 0 0 auto;
}
}
.addCardButtonIcon {
@@ -44,6 +47,10 @@
vertical-align: top;
}
.addCardButtonWrapper {
display: flex;
}
.card {
margin-bottom: 8px;
}
+3
View File
@@ -290,6 +290,9 @@ export default {
CARD_DUPLICATE: 'CARD_DUPLICATE',
CARD_DUPLICATE__SUCCESS: 'CARD_DUPLICATE__SUCCESS',
CARD_DUPLICATE__FAILURE: 'CARD_DUPLICATE__FAILURE',
CARD_COPY: 'CARD_COPY',
CARD_CUT: 'CARD_CUT',
CARD_PASTE: 'CARD_PASTE',
CARD_DELETE: 'CARD_DELETE',
CARD_DELETE__SUCCESS: 'CARD_DELETE__SUCCESS',
CARD_DELETE__FAILURE: 'CARD_DELETE__FAILURE',
+12
View File
@@ -0,0 +1,12 @@
/*!
* Copyright (c) 2024 PLANKA Software GmbH
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
const COPY = 'COPY';
const CUT = 'CUT';
export default {
COPY,
CUT,
};
+5
View File
@@ -202,6 +202,11 @@ export default {
CURRENT_CARD_TRANSFER: `${PREFIX}/CURRENT_CARD_TRANSFER`,
CARD_DUPLICATE: `${PREFIX}/CARD_DUPLICATE`,
CURRENT_CARD_DUPLICATE: `${PREFIX}/CURRENT_CARD_DUPLICATE`,
CARD_COPY: `${PREFIX}/CARD_COPY`,
CARD_CUT: `${PREFIX}/CARD_CUT`,
CARD_PASTE: `${PREFIX}/CARD_PASTE`,
CARD_IN_CURRENT_CONTEXT_PASTE: `${PREFIX}/CARD_IN_CURRENT_CONTEXT_PASTE`,
CARD_IN_CURRENT_LIST_PASTE: `${PREFIX}/CARD_IN_CURRENT_LIST_PASTE`,
TO_ADJACENT_CARD_GO: `${PREFIX}/TO_ADJACENT_CARD_GO`,
CARD_DELETE: `${PREFIX}/CARD_DELETE`,
CURRENT_CARD_DELETE: `${PREFIX}/CURRENT_CARD_DELETE`,
+4
View File
@@ -6,9 +6,13 @@
const FILE_IS_TOO_BIG = 'FILE_IS_TOO_BIG';
const NOT_ENOUGH_STORAGE = 'NOT_ENOUGH_STORAGE';
const EMPTY_TRASH = 'EMPTY_TRASH';
const SOURCE_CARD_NOT_COPYABLE = 'SOURCE_CARD_NOT_COPYABLE';
const SOURCE_CARD_NOT_MOVABLE = 'SOURCE_CARD_NOT_MOVABLE';
export default {
FILE_IS_TOO_BIG,
NOT_ENOUGH_STORAGE,
EMPTY_TRASH,
SOURCE_CARD_NOT_COPYABLE,
SOURCE_CARD_NOT_MOVABLE,
};
+1 -1
View File
@@ -5,4 +5,4 @@
import { createContext } from 'react';
export default createContext([null, null]);
export default createContext([null, null, null, null]);
+38 -2
View File
@@ -135,7 +135,7 @@ const transferCurrentCard = (boardId, listId, index = 0) => ({
},
});
const duplicateCard = (id, data) => ({
const duplicateCard = (id, data = {}) => ({
type: EntryActionTypes.CARD_DUPLICATE,
payload: {
id,
@@ -143,13 +143,44 @@ const duplicateCard = (id, data) => ({
},
});
const duplicateCurrentCard = (data) => ({
const duplicateCurrentCard = (data = {}) => ({
type: EntryActionTypes.CURRENT_CARD_DUPLICATE,
payload: {
data,
},
});
const copyCard = (id) => ({
type: EntryActionTypes.CARD_COPY,
payload: {
id,
},
});
const cutCard = (id) => ({
type: EntryActionTypes.CARD_CUT,
payload: {
id,
},
});
const pasteCard = (listId) => ({
type: EntryActionTypes.CARD_PASTE,
payload: {
listId,
},
});
const pasteCardInCurrentContext = () => ({
type: EntryActionTypes.CARD_IN_CURRENT_CONTEXT_PASTE,
payload: {},
});
const pasteCardInCurrentList = () => ({
type: EntryActionTypes.CARD_IN_CURRENT_LIST_PASTE,
payload: {},
});
const goToAdjacentCard = (direction) => ({
type: EntryActionTypes.TO_ADJACENT_CARD_GO,
payload: {
@@ -196,6 +227,11 @@ export default {
transferCurrentCard,
duplicateCard,
duplicateCurrentCard,
copyCard,
cutCard,
pasteCard,
pasteCardInCurrentContext,
pasteCardInCurrentList,
goToAdjacentCard,
deleteCard,
deleteCurrentCard,
+4
View File
@@ -298,6 +298,8 @@ export default {
showOnFrontOfCard: 'عرض في مقدمة البطاقة',
smtp: 'SMTP',
sortList_title: 'فرز القائمة',
sourceCardIsNoLongerAvailableForCopying: 'البطاقة المصدر لم تعد متاحة للنسخ.',
sourceCardIsNoLongerAvailableForMoving: 'البطاقة المصدر لم تعد متاحة للنقل.',
stopwatch: 'المؤقت',
story: 'القصة',
subscribeToCardWhenCommenting: 'الاشتراك في البطاقة عند التعليق',
@@ -384,6 +386,7 @@ export default {
archiveCards_title: 'أرشفة البطاقات',
assignAsOwner: 'تعيين كمالك',
cancel: 'إلغاء',
copyCard_title: 'نسخ البطاقة',
createApiKey: 'إنشاء مفتاح API',
createBoard: 'إنشاء لوحة',
createCustomFieldGroup: 'إنشاء مجموعة حقل مخصص',
@@ -391,6 +394,7 @@ export default {
createLabel: 'إنشاء ملصق',
createNewLabel: 'إنشاء ملصق جديد',
createProject: 'إنشاء مشروع',
cutCard_title: 'قص البطاقة',
deactivateUser: 'إلغاء تفعيل المستخدم',
deactivateUser_title: 'إلغاء تفعيل المستخدم',
delete: 'حذف',
+5
View File
@@ -310,6 +310,9 @@ export default {
showOnFrontOfCard: 'Показване отпред на картата',
smtp: 'SMTP',
sortList_title: 'Сортиране на списък',
sourceCardIsNoLongerAvailableForCopying: 'Оригиналната карта вече не е налична за копиране.',
sourceCardIsNoLongerAvailableForMoving:
'Оригиналната карта вече не е налична за преместване.',
stopwatch: 'Хронометър',
story: 'История',
subscribeToCardWhenCommenting: 'Абониране за карта при коментиране',
@@ -398,6 +401,7 @@ export default {
archiveCards_title: 'Архивиране на карти',
assignAsOwner: 'Назначаване като собственик',
cancel: 'Отказ',
copyCard_title: 'Копиране на карта',
createApiKey: 'Създаване на API ключ',
createBoard: 'Създаване на табло',
createCustomFieldGroup: 'Създаване на група персонализирани полета',
@@ -405,6 +409,7 @@ export default {
createLabel: 'Създаване на етикет',
createNewLabel: 'Създаване на нов етикет',
createProject: 'Създаване на проект',
cutCard_title: 'Изрязване на карта',
deactivateUser: 'Деактивиране на потребител',
deactivateUser_title: 'Деактивиране на потребител',
delete: 'Изтриване',
+6
View File
@@ -309,6 +309,10 @@ export default {
showOnFrontOfCard: 'Mostrar a la part frontal de la targeta',
smtp: 'SMTP',
sortList_title: 'Ordenar llista',
sourceCardIsNoLongerAvailableForCopying:
"La targeta d'origen ja no està disponible per copiar.",
sourceCardIsNoLongerAvailableForMoving:
"La targeta d'origen ja no està disponible per moure.",
stopwatch: 'Cronòmetre',
story: 'Història',
subscribeToCardWhenCommenting: "Subscriure's a la targeta en comentar",
@@ -399,6 +403,7 @@ export default {
archiveCards_title: 'Arxivar targetes',
assignAsOwner: 'Assignar com a propietari',
cancel: 'Cancel·lar',
copyCard_title: 'Copiar targeta',
createApiKey: 'Crear clau API',
createBoard: 'Crear tauler',
createCustomFieldGroup: 'Crear grup de camps personalitzats',
@@ -406,6 +411,7 @@ export default {
createLabel: 'Crear etiqueta',
createNewLabel: 'Crear nova etiqueta',
createProject: 'Crear projecte',
cutCard_title: 'Tallar targeta',
deactivateUser: 'Desactivar usuari',
deactivateUser_title: 'Desactivar usuari',
delete: 'Eliminar',
+5
View File
@@ -301,6 +301,9 @@ export default {
showOnFrontOfCard: 'Zobrazit na přední straně karty',
smtp: 'SMTP',
sortList_title: 'Řadit podle',
sourceCardIsNoLongerAvailableForCopying:
'Zdrojová karta již není k dispozici pro kopírování.',
sourceCardIsNoLongerAvailableForMoving: 'Zdrojová karta již není k dispozici pro přesunutí.',
stopwatch: 'Časovač',
story: 'Příběh',
subscribeToCardWhenCommenting: 'Odebírat karty při komentování',
@@ -388,6 +391,7 @@ export default {
archiveCards_title: 'Archiv karet',
assignAsOwner: 'Přiřadit jako vlastníka',
cancel: 'Zrušit',
copyCard_title: 'Kopírovat kartu',
createApiKey: 'Vytvořit API klíč',
createBoard: 'Vytvořit nástěnku',
createCustomFieldGroup: 'Vytvořit vlastní skupinu polí',
@@ -395,6 +399,7 @@ export default {
createLabel: 'Vytvořit štítek',
createNewLabel: 'Vytvořit nový štítek',
createProject: 'Vytvořit projekt',
cutCard_title: 'Vyjmout kartu',
deactivateUser: 'Deaktivace uživatele',
deactivateUser_title: 'Deaktivace uživatele',
delete: 'Smazat',
+6
View File
@@ -305,6 +305,10 @@ export default {
showOnFrontOfCard: 'Vis på forsiden af kortet',
smtp: 'SMTP',
sortList_title: 'Sortér liste',
sourceCardIsNoLongerAvailableForCopying:
'Kildekortet er ikke længere tilgængeligt til kopiering.',
sourceCardIsNoLongerAvailableForMoving:
'Kildekortet er ikke længere tilgængeligt til flytning.',
stopwatch: 'Stopur',
story: 'Story',
subscribeToCardWhenCommenting: 'Abonnér på kort ved kommentering',
@@ -393,6 +397,7 @@ export default {
archiveCards_title: 'Arkivér kort',
assignAsOwner: 'Sæt som ejer',
cancel: 'Annuller',
copyCard_title: 'Kopiér kort',
createApiKey: 'Opret API-nøgle',
createBoard: 'Opret tavle',
createCustomFieldGroup: 'Opret brugerdefineret feltgruppe',
@@ -400,6 +405,7 @@ export default {
createLabel: 'Opret label',
createNewLabel: 'Opret ny label',
createProject: 'Opret projekt',
cutCard_title: 'Klip kort',
deactivateUser: 'Deaktivér bruger',
deactivateUser_title: 'Deaktivér bruger',
delete: 'Slet',
+5
View File
@@ -321,6 +321,9 @@ export default {
showOnFrontOfCard: 'Auf der Vorderseite der Karte anzeigen',
smtp: 'SMTP',
sortList_title: 'Liste sortieren',
sourceCardIsNoLongerAvailableForCopying: 'Quellkarte ist nicht mehr zum Kopieren verfügbar.',
sourceCardIsNoLongerAvailableForMoving:
'Quellkarte ist nicht mehr zum Verschieben verfügbar.',
stopwatch: 'Stoppuhr',
story: 'Wissen',
subscribeToCardWhenCommenting: 'Karte beim Kommentieren abonnieren',
@@ -410,6 +413,7 @@ export default {
archiveCards_title: 'Karten archivieren',
assignAsOwner: 'Als Eigentümer zuweisen',
cancel: 'Abbrechen',
copyCard_title: 'Karte Kopieren',
createApiKey: 'API-Schlüssel erstellen',
createBoard: 'Arbeitsbereich erstellen',
createCustomFieldGroup: 'Feldgruppe erstellen',
@@ -417,6 +421,7 @@ export default {
createLabel: 'Label erstellen',
createNewLabel: 'Neues Label erstellen',
createProject: 'Projekt erstellen',
cutCard_title: 'Karte Ausschneiden',
deactivateUser: 'Benutzer deaktivieren',
deactivateUser_title: 'Benutzer deaktivieren',
delete: 'Löschen',
+6
View File
@@ -318,6 +318,10 @@ export default {
showOnFrontOfCard: 'Εμφάνιση στο μπροστινό μέρος της κάρτας',
smtp: 'SMTP',
sortList_title: 'Ταξινόμηση λίστας',
sourceCardIsNoLongerAvailableForCopying:
'Η κάρτα πηγής δεν είναι πλέον διαθέσιμη για αντιγραφή.',
sourceCardIsNoLongerAvailableForMoving:
'Η κάρτα πηγής δεν είναι πλέον διαθέσιμη για μετακίνηση.',
stopwatch: 'Χρονόμετρο',
story: 'Ιστορία',
subscribeToCardWhenCommenting: 'Εγγραφή στην κάρτα κατά τη σχολιασμό',
@@ -413,6 +417,7 @@ export default {
archiveCards_title: 'Αρχειοθέτηση καρτών',
assignAsOwner: 'Ορισμός ως ιδιοκτήτης',
cancel: 'Ακύρωση',
copyCard_title: 'Αντιγραφή κάρτας',
createApiKey: 'Δημιουργία κλειδιού API',
createBoard: 'Δημιουργία πίνακα',
createCustomFieldGroup: 'Δημιουργία ομάδας προσαρμοσμένων πεδίων',
@@ -420,6 +425,7 @@ export default {
createLabel: 'Δημιουργία ετικέτας',
createNewLabel: 'Δημιουργία νέας ετικέτας',
createProject: 'Δημιουργία έργου',
cutCard_title: 'Αποκοπή κάρτας',
deactivateUser: 'Απενεργοποίηση χρήστη',
deactivateUser_title: 'Απενεργοποίηση χρήστη',
delete: 'Διαγραφή',
+4
View File
@@ -304,6 +304,8 @@ export default {
showOnFrontOfCard: 'Show on front of card',
smtp: 'SMTP',
sortList_title: 'Sort List',
sourceCardIsNoLongerAvailableForCopying: 'Source card is no longer available for copying.',
sourceCardIsNoLongerAvailableForMoving: 'Source card is no longer available for moving.',
stopwatch: 'Stopwatch',
story: 'Story',
subscribeToCardWhenCommenting: 'Subscribe to card when commenting',
@@ -391,6 +393,7 @@ export default {
archiveCards_title: 'Archive Cards',
assignAsOwner: 'Assign as owner',
cancel: 'Cancel',
copyCard_title: 'Copy Card',
createApiKey: 'Create API key',
createBoard: 'Create board',
createCustomFieldGroup: 'Create custom field group',
@@ -398,6 +401,7 @@ export default {
createLabel: 'Create label',
createNewLabel: 'Create new label',
createProject: 'Create project',
cutCard_title: 'Cut Card',
deactivateUser: 'Deactivate user',
deactivateUser_title: 'Deactivate User',
delete: 'Delete',
+4
View File
@@ -299,6 +299,8 @@ export default {
showOnFrontOfCard: 'Show on front of card',
smtp: 'SMTP',
sortList_title: 'Sort List',
sourceCardIsNoLongerAvailableForCopying: 'Source card is no longer available for copying.',
sourceCardIsNoLongerAvailableForMoving: 'Source card is no longer available for moving.',
stopwatch: 'Stopwatch',
story: 'Story',
subscribeToCardWhenCommenting: 'Subscribe to card when commenting',
@@ -386,6 +388,7 @@ export default {
archiveCards_title: 'Archive Cards',
assignAsOwner: 'Assign as owner',
cancel: 'Cancel',
copyCard_title: 'Copy Card',
createApiKey: 'Create API key',
createBoard: 'Create board',
createCustomFieldGroup: 'Create custom field group',
@@ -393,6 +396,7 @@ export default {
createLabel: 'Create label',
createNewLabel: 'Create new label',
createProject: 'Create project',
cutCard_title: 'Cut Card',
deactivateUser: 'Deactivate user',
deactivateUser_title: 'Deactivate User',
delete: 'Delete',
+6
View File
@@ -310,6 +310,10 @@ export default {
showOnFrontOfCard: 'Mostrar en el frente de la tarjeta',
smtp: 'SMTP',
sortList_title: 'Ordenar lista',
sourceCardIsNoLongerAvailableForCopying:
'La tarjeta de origen ya no está disponible para copiar.',
sourceCardIsNoLongerAvailableForMoving:
'La tarjeta de origen ya no está disponible para mover.',
stopwatch: 'Cronómetro',
story: 'Historia',
subscribeToCardWhenCommenting: 'Suscribirse a la tarjeta al comentar',
@@ -399,6 +403,7 @@ export default {
archiveCards_title: 'Archivar tarjetas',
assignAsOwner: 'Asignar como propietario',
cancel: 'Cancelar',
copyCard_title: 'Copiar tarjeta',
createApiKey: 'Crear clave API',
createBoard: 'Crear tablero',
createCustomFieldGroup: 'Crear grupo de campos personalizados',
@@ -406,6 +411,7 @@ export default {
createLabel: 'Crear etiqueta',
createNewLabel: 'Crear nueva etiqueta',
createProject: 'Crear proyecto',
cutCard_title: 'Cortar tarjeta',
deactivateUser: 'Desactivar usuario',
deactivateUser_title: 'Desactivar usuario',
delete: 'Eliminar',
+4
View File
@@ -305,6 +305,8 @@ export default {
showOnFrontOfCard: 'Kuva kaardi ees',
smtp: 'SMTP',
sortList_title: 'Nimekiri sorteerimine',
sourceCardIsNoLongerAvailableForCopying: 'Lähtekaart ei ole enam kopeerimiseks saadaval.',
sourceCardIsNoLongerAvailableForMoving: 'Lähtekaart ei ole enam liigutamiseks saadaval.',
stopwatch: 'Stopper',
story: 'Kirjeldus',
subscribeToCardWhenCommenting: 'Telli kaart, kui kommenteerida',
@@ -393,6 +395,7 @@ export default {
archiveCards_title: 'Arhiveeri kaardid',
assignAsOwner: 'Määra omanikuks',
cancel: 'Tühista',
copyCard_title: 'Kopeeri kaart',
createApiKey: 'Loo API võti',
createBoard: 'Loo tahvel',
createCustomFieldGroup: 'Loo kohandatud väljade grupp',
@@ -400,6 +403,7 @@ export default {
createLabel: 'Loo silt',
createNewLabel: 'Loo uus silt',
createProject: 'Loo projekt',
cutCard_title: 'Lõika kaart',
deactivateUser: 'Deaktiveeri kasutaja',
deactivateUser_title: 'Deaktiveeri kasutaja',
delete: 'Kustuta',
+4
View File
@@ -308,6 +308,8 @@ export default {
showOnFrontOfCard: 'نمایش در جلوی کارت',
smtp: 'SMTP',
sortList_title: 'مرتب‌سازی لیست',
sourceCardIsNoLongerAvailableForCopying: 'کارت مبدا دیگر برای کپی کردن در دسترس نیست.',
sourceCardIsNoLongerAvailableForMoving: 'کارت مبدا دیگر برای انتقال در دسترس نیست.',
stopwatch: 'کرنومتر',
story: 'داستان',
subscribeToCardWhenCommenting: 'هنگام نظر دادن به کارت مشترک شو',
@@ -395,6 +397,7 @@ export default {
archiveCards_title: 'آرشیو کارت‌ها',
assignAsOwner: 'تعیین به عنوان مالک',
cancel: 'لغو',
copyCard_title: 'کپی کارت',
createApiKey: 'ایجاد کلید API',
createBoard: 'ایجاد برد',
createCustomFieldGroup: 'ایجاد گروه فیلد سفارشی',
@@ -402,6 +405,7 @@ export default {
createLabel: 'ایجاد برچسب',
createNewLabel: 'ایجاد برچسب جدید',
createProject: 'ایجاد پروژه',
cutCard_title: 'برش کارت',
deactivateUser: 'غیرفعال کردن کاربر',
deactivateUser_title: 'غیرفعال کردن کاربر',
delete: 'حذف',
+6
View File
@@ -301,6 +301,10 @@ export default {
showOnFrontOfCard: 'Näytä kortin etupuolella',
smtp: 'SMTP',
sortList_title: 'Lajittele lista',
sourceCardIsNoLongerAvailableForCopying:
'Lähdekortti ei ole enää saatavilla kopiointia varten.',
sourceCardIsNoLongerAvailableForMoving:
'Lähdekortti ei ole enää saatavilla siirtämistä varten.',
stopwatch: 'Ajastin',
story: 'Tarina',
subscribeToCardWhenCommenting: 'Tilaa kortti kommentoidessa',
@@ -392,6 +396,7 @@ export default {
archiveCards_title: 'Arkistoi kortit',
assignAsOwner: 'Aseta omistajaksi',
cancel: 'Peruuta',
copyCard_title: 'Kopioi kortti',
createApiKey: 'Luo API-avain',
createBoard: 'Luo taulu',
createCustomFieldGroup: 'Luo mukautettujen kenttien ryhmä',
@@ -399,6 +404,7 @@ export default {
createLabel: 'Luo tunniste',
createNewLabel: 'Luo uusi tunniste',
createProject: 'Luo projekti',
cutCard_title: 'Leikkaa kortti',
deactivateUser: 'Poista käyttäjä käytöstä',
deactivateUser_title: 'Poista käyttäjä käytöstä',
delete: 'Poista',
+6
View File
@@ -309,6 +309,10 @@ export default {
showOnFrontOfCard: 'Afficher sur le devant de la carte',
smtp: 'SMTP',
sortList_title: 'Trier la liste',
sourceCardIsNoLongerAvailableForCopying:
"La carte source n'est plus disponible pour la copie.",
sourceCardIsNoLongerAvailableForMoving:
"La carte source n'est plus disponible pour le déplacement.",
stopwatch: 'Minuteur',
story: 'Story',
subscribeToCardWhenCommenting: 'S’abonner à la carte lors de la rédaction d’un commentaire',
@@ -397,6 +401,7 @@ export default {
archiveCards_title: 'Archiver les cartes',
assignAsOwner: 'Assigner comme propriétaire',
cancel: 'Annuler',
copyCard_title: 'Copier la carte',
createApiKey: 'Créer une clé API',
createBoard: 'Créer un tableau',
createCustomFieldGroup: 'Créer un groupe de champs personnalisés',
@@ -404,6 +409,7 @@ export default {
createLabel: 'Créer une étiquette',
createNewLabel: 'Créer une nouvelle étiquette',
createProject: 'Créer un projet',
cutCard_title: 'Couper la carte',
deactivateUser: 'Désactiver l’utilisateur',
deactivateUser_title: 'Désactiver l’utilisateur',
delete: 'Supprimer',
+4
View File
@@ -299,6 +299,8 @@ export default {
showOnFrontOfCard: 'Megjelenítés a kártya borítóján',
smtp: 'SMTP',
sortList_title: 'Rendezés listában',
sourceCardIsNoLongerAvailableForCopying: 'A forráskártya már nem érhető el másoláshoz.',
sourceCardIsNoLongerAvailableForMoving: 'A forráskártya már nem érhető el áthelyezéshez.',
stopwatch: 'Stopper',
story: 'Story',
subscribeToCardWhenCommenting: 'Feliratkozás a kártyára kommenteléskor',
@@ -393,6 +395,7 @@ export default {
archiveCards_title: 'Archív kártyák',
assignAsOwner: 'Hozzárendelés tulajdonosnak',
cancel: 'Mégsem',
copyCard_title: 'Kártya másolása',
createApiKey: 'API kulcs létrehozása',
createBoard: 'Tábla létrehozása',
createCustomFieldGroup: 'Egyedi mezőcsoport létrehozása',
@@ -400,6 +403,7 @@ export default {
createLabel: 'Címke létrehozása',
createNewLabel: 'Új címke létrehozása',
createProject: 'Projekt létrehozása',
cutCard_title: 'Kártya kivágása',
deactivateUser: 'Felhasználó inaktiválása',
deactivateUser_title: 'Felhasználó inaktiválása',
delete: 'Törlés',
+4
View File
@@ -306,6 +306,8 @@ export default {
showOnFrontOfCard: 'Tampilkan di depan kartu',
smtp: 'SMTP',
sortList_title: 'Urutkan daftar',
sourceCardIsNoLongerAvailableForCopying: 'Kartu sumber tidak lagi tersedia untuk disalin.',
sourceCardIsNoLongerAvailableForMoving: 'Kartu sumber tidak lagi tersedia untuk dipindahkan.',
stopwatch: 'Stopwatch',
story: 'Cerita',
subscribeToCardWhenCommenting: 'Berlangganan kartu saat berkomentar',
@@ -394,6 +396,7 @@ export default {
archiveCards_title: 'Arsipkan kartu',
assignAsOwner: 'Tetapkan sebagai pemilik',
cancel: 'Batal',
copyCard_title: 'Salin Kartu',
createApiKey: 'Buat kunci API',
createBoard: 'Tambah papan',
createCustomFieldGroup: 'Buat grup bidang kustom',
@@ -401,6 +404,7 @@ export default {
createLabel: 'Tambah label',
createNewLabel: 'Tambah label baru',
createProject: 'Tambah proyek',
cutCard_title: 'Potong Kartu',
deactivateUser: 'Nonaktifkan pengguna',
deactivateUser_title: 'Nonaktifkan pengguna',
delete: 'Hapus',
+6
View File
@@ -306,6 +306,10 @@ export default {
showOnFrontOfCard: 'Mostra davanti alla scheda',
smtp: 'SMTP',
sortList_title: 'Ordina',
sourceCardIsNoLongerAvailableForCopying:
'La scheda sorgente non è più disponibile per la copia.',
sourceCardIsNoLongerAvailableForMoving:
'La scheda sorgente non è più disponibile per lo spostamento.',
stopwatch: 'Timer',
story: 'Storia',
subscribeToCardWhenCommenting: 'Iscrivimi alla scheda quando commento',
@@ -395,6 +399,7 @@ export default {
archiveCards_title: 'Archivia schede',
assignAsOwner: 'Assegna come proprietario',
cancel: 'Annulla',
copyCard_title: 'Copia scheda',
createApiKey: 'Crea chiave API',
createBoard: 'Crea bacheca',
createCustomFieldGroup: 'Crea campi personalizzati',
@@ -402,6 +407,7 @@ export default {
createLabel: 'Crea etichetta',
createNewLabel: 'Crea nuova etichetta',
createProject: 'Crea progetto',
cutCard_title: 'Taglia scheda',
deactivateUser: 'Disattiva utente',
deactivateUser_title: 'Disattiva utente',
delete: 'Elimina',
+4
View File
@@ -302,6 +302,8 @@ export default {
showOnFrontOfCard: 'カードの前面に表示',
smtp: 'SMTP',
sortList_title: 'リストを並び替え',
sourceCardIsNoLongerAvailableForCopying: 'ソースカードはコピーできなくなりました。',
sourceCardIsNoLongerAvailableForMoving: 'ソースカードは移動できなくなりました。',
stopwatch: 'タイマー',
story: 'ストーリー',
subscribeToCardWhenCommenting: 'コメント時にカードを購読',
@@ -391,6 +393,7 @@ export default {
archiveCards_title: 'カードをアーカイブ',
assignAsOwner: 'オーナーとして割り当て',
cancel: 'キャンセル',
copyCard_title: 'カードをコピー',
createApiKey: 'APIキーを作成',
createBoard: 'ボードを作成',
createCustomFieldGroup: 'カスタムフィールドグループを作成',
@@ -398,6 +401,7 @@ export default {
createLabel: 'ラベルを作成',
createNewLabel: '新しいラベルを作成',
createProject: 'プロジェクトを作成',
cutCard_title: 'カードを切り取り',
deactivateUser: 'ユーザーを非アクティブにする',
deactivateUser_title: 'ユーザーを非アクティブにする',
delete: '削除',
+4
View File
@@ -296,6 +296,8 @@ export default {
showOnFrontOfCard: '카드 앞면에 표시',
smtp: 'SMTP',
sortList_title: '목록 정렬',
sourceCardIsNoLongerAvailableForCopying: '원본 카드를 더 이상 복사할 수 없습니다.',
sourceCardIsNoLongerAvailableForMoving: '원본 카드를 더 이상 이동할 수 없습니다.',
stopwatch: '스톱워치',
story: '스토리',
subscribeToCardWhenCommenting: '댓글 작성 시 카드 구독',
@@ -388,6 +390,7 @@ export default {
archiveCards_title: '카드들 보관',
assignAsOwner: '소유자로 지정',
cancel: '취소',
copyCard_title: '카드 복사',
createApiKey: 'API 키 생성',
createBoard: '보드 생성',
createCustomFieldGroup: '사용자 정의 필드 그룹 생성',
@@ -395,6 +398,7 @@ export default {
createLabel: '라벨 생성',
createNewLabel: '새 라벨 생성',
createProject: '프로젝트 생성',
cutCard_title: '카드 잘라내기',
deactivateUser: '사용자 비활성화',
deactivateUser_title: '사용자 비활성화',
delete: '삭제',
+5
View File
@@ -305,6 +305,9 @@ export default {
showOnFrontOfCard: 'Tonen op voorkant van kaart',
smtp: 'SMTP',
sortList_title: 'Lijst sorteren',
sourceCardIsNoLongerAvailableForCopying: 'Bronkaart is niet meer beschikbaar voor kopiëren.',
sourceCardIsNoLongerAvailableForMoving:
'Bronkaart is niet meer beschikbaar voor verplaatsen.',
stopwatch: 'Stopwatch',
story: 'Verhaal',
subscribeToCardWhenCommenting: 'Abonneren op kaart bij het plaatsen van commentaar',
@@ -396,6 +399,7 @@ export default {
archiveCards_title: 'Kaarten archiveren',
assignAsOwner: 'Toewijzen als eigenaar',
cancel: 'Annuleren',
copyCard_title: 'Kaart kopiëren',
createApiKey: 'API-sleutel aanmaken',
createBoard: 'Bord aanmaken',
createCustomFieldGroup: 'Aangepaste veldgroep aanmaken',
@@ -403,6 +407,7 @@ export default {
createLabel: 'Label aanmaken',
createNewLabel: 'Nieuw label aanmaken',
createProject: 'Project aanmaken',
cutCard_title: 'Kaart knippen',
deactivateUser: 'Gebruiker deactiveren',
deactivateUser_title: 'Gebruiker deactiveren',
delete: 'Verwijderen',
+6
View File
@@ -304,6 +304,10 @@ export default {
showOnFrontOfCard: 'Pokazuj na przodzie karty',
smtp: 'SMTP',
sortList_title: 'Sortowanie listy',
sourceCardIsNoLongerAvailableForCopying:
'Źródłowa karta nie jest już dostępna do skopiowania.',
sourceCardIsNoLongerAvailableForMoving:
'Źródłowa karta nie jest już dostępna do przeniesienia.',
stopwatch: 'Stoper',
story: 'Scenorys',
subscribeToCardWhenCommenting: 'Subskrybuj kartę przy komentowaniu',
@@ -392,6 +396,7 @@ export default {
archiveCards_title: 'Archiwizuj karty',
assignAsOwner: 'Przypisz jako właściciela',
cancel: 'Anuluj',
copyCard_title: 'Kopiuj kartę',
createApiKey: 'Utwórz klucz API',
createBoard: 'Utwórz tablicę',
createCustomFieldGroup: 'Utwórz grupę pól własnych',
@@ -399,6 +404,7 @@ export default {
createLabel: 'Utwórz oznaczenie',
createNewLabel: 'Utwórz nowe oznaczenie',
createProject: 'Utwórz projekt',
cutCard_title: 'Wytnij kartę',
deactivateUser: 'Dezaktywuj użytkownika',
deactivateUser_title: 'Dezaktywuj użytkownika',
delete: 'Usuń',
+6
View File
@@ -307,6 +307,10 @@ export default {
showOnFrontOfCard: 'Mostrar na frente do cartão',
smtp: 'SMTP',
sortList_title: 'Ordenar lista',
sourceCardIsNoLongerAvailableForCopying:
'O cartão de origem não está mais disponível para cópia.',
sourceCardIsNoLongerAvailableForMoving:
'O cartão de origem não está mais disponível para mover.',
stopwatch: 'Cronômetro',
story: 'História',
subscribeToCardWhenCommenting: 'Inscrever-se no cartão ao comentar',
@@ -395,6 +399,7 @@ export default {
archiveCards_title: 'Arquivar cartões',
assignAsOwner: 'Atribuir como proprietário',
cancel: 'Cancelar',
copyCard_title: 'Copiar cartão',
createApiKey: 'Criar chave API',
createBoard: 'Criar quadro',
createCustomFieldGroup: 'Criar grupo de campos personalizados',
@@ -402,6 +407,7 @@ export default {
createLabel: 'Criar rótulo',
createNewLabel: 'Criar novo rótulo',
createProject: 'Criar projeto',
cutCard_title: 'Cortar cartão',
deactivateUser: 'Desativar usuário',
deactivateUser_title: 'Desativar usuário',
delete: 'Excluir',
+6
View File
@@ -309,6 +309,10 @@ export default {
showOnFrontOfCard: 'Mostrar na frente do cartão',
smtp: 'SMTP',
sortList_title: 'Ordenar lista',
sourceCardIsNoLongerAvailableForCopying:
'O cartão de origem já não está disponível para cópia.',
sourceCardIsNoLongerAvailableForMoving:
'O cartão de origem já não está disponível para mover.',
stopwatch: 'Cronómetro',
story: 'História',
subscribeToCardWhenCommenting: 'Subscrever cartão ao comentar',
@@ -398,6 +402,7 @@ export default {
archiveCards_title: 'Arquivar cartões',
assignAsOwner: 'Atribuir como proprietário',
cancel: 'Cancelar',
copyCard_title: 'Copiar cartão',
createApiKey: 'Criar chave API',
createBoard: 'Criar quadro',
createCustomFieldGroup: 'Criar grupo de campos personalizados',
@@ -405,6 +410,7 @@ export default {
createLabel: 'Criar etiqueta',
createNewLabel: 'Criar nova etiqueta',
createProject: 'Criar projeto',
cutCard_title: 'Cortar cartão',
deactivateUser: 'Desativar utilizador',
deactivateUser_title: 'Desativar utilizador',
delete: 'Eliminar',
+5
View File
@@ -303,6 +303,9 @@ export default {
showOnFrontOfCard: 'Afișează pe fața cardului',
smtp: 'SMTP',
sortList_title: 'Sortează lista',
sourceCardIsNoLongerAvailableForCopying:
'Cardul sursă nu mai este disponibil pentru copiere.',
sourceCardIsNoLongerAvailableForMoving: 'Cardul sursă nu mai este disponibil pentru mutare.',
stopwatch: 'Cronometru',
story: 'Poveste',
subscribeToCardWhenCommenting: 'Abonează-te la card când comentezi',
@@ -393,6 +396,7 @@ export default {
archiveCards_title: 'Arhivează cardurile',
assignAsOwner: 'Atribuie ca proprietar',
cancel: 'Anulează',
copyCard_title: 'Copiază cardul',
createApiKey: 'Creează cheie API',
createBoard: 'Creați tablă',
createCustomFieldGroup: 'Creați grup de câmpuri personalizate',
@@ -400,6 +404,7 @@ export default {
createLabel: 'Creați eticheta',
createNewLabel: 'Creați o nouă etichetă',
createProject: 'Creați proiect',
cutCard_title: 'Taie cardul',
deactivateUser: 'Dezactivați utilizatorul',
deactivateUser_title: 'Dezactivați utilizatorul',
delete: 'Ștergeți',
+6
View File
@@ -306,6 +306,10 @@ export default {
showOnFrontOfCard: 'Показать на лицевой стороне карточки',
smtp: 'SMTP',
sortList_title: 'Сортировка списка',
sourceCardIsNoLongerAvailableForCopying:
'Исходная карточка больше не доступна для копирования.',
sourceCardIsNoLongerAvailableForMoving:
'Исходная карточка больше не доступна для перемещения.',
stopwatch: 'Секундомер',
story: 'История',
subscribeToCardWhenCommenting: 'Подписаться на карточку при комментировании',
@@ -394,6 +398,7 @@ export default {
archiveCards_title: 'Архивировать карточки',
assignAsOwner: 'Назначить владельцем',
cancel: 'Отменить',
copyCard_title: 'Копировать карточку',
createApiKey: 'Создать ключ API',
createBoard: 'Создать доску',
createCustomFieldGroup: 'Создать группу настраиваемых полей',
@@ -401,6 +406,7 @@ export default {
createLabel: 'Создать метку',
createNewLabel: 'Создать новую метку',
createProject: 'Создать проект',
cutCard_title: 'Вырезать карточку',
deactivateUser: 'Деактивировать пользователя',
deactivateUser_title: 'Деактивировать пользователя',
delete: 'Удалить',
+4
View File
@@ -300,6 +300,8 @@ export default {
showOnFrontOfCard: 'Zobraziť na prednej strane karty',
smtp: 'SMTP',
sortList_title: 'Zoradiť zoznam',
sourceCardIsNoLongerAvailableForCopying: 'Zdrojová karta už nie je dostupná na kopírovanie.',
sourceCardIsNoLongerAvailableForMoving: 'Zdrojová karta už nie je dostupná na presunutie.',
stopwatch: 'Časovač',
story: 'Príbeh',
subscribeToCardWhenCommenting: 'Odoberať kartu pri komentovaní',
@@ -387,6 +389,7 @@ export default {
archiveCards_title: 'Archivovať karty',
assignAsOwner: 'Prideliť ako vlastníka',
cancel: 'Zrušiť',
copyCard_title: 'Kopírovať kartu',
createApiKey: 'Vytvoriť API kľúč',
createBoard: 'Vytvoriť tabuľu',
createCustomFieldGroup: 'Vytvoriť skupinu vlastných polí',
@@ -394,6 +397,7 @@ export default {
createLabel: 'Vytvoriť štítok',
createNewLabel: 'Vytvoriť nový štítok',
createProject: 'Vytvoriť projekt',
cutCard_title: 'Vystrihnúť kartu',
deactivateUser: 'Deaktivovať používateľa',
deactivateUser_title: 'Deaktivovať používateľa',
delete: 'Zmazať',
+4
View File
@@ -303,6 +303,8 @@ export default {
showOnFrontOfCard: 'Прикажи на предњој страни картице',
smtp: 'SMTP',
sortList_title: 'Сложи списак',
sourceCardIsNoLongerAvailableForCopying: 'Изворна картица више није доступна за копирање.',
sourceCardIsNoLongerAvailableForMoving: 'Изворна картица више није доступна за премештање.',
stopwatch: 'Штоперица',
story: 'Прича',
subscribeToCardWhenCommenting: 'Претплати се на картицу при коментарисању',
@@ -390,6 +392,7 @@ export default {
archiveCards_title: 'Архивирај картице',
assignAsOwner: 'Додели као власника',
cancel: 'Откажи',
copyCard_title: 'Копирај картицу',
createApiKey: 'Креирај API кључ',
createBoard: 'Направи таблу',
createCustomFieldGroup: 'Направи групу прилагођених поља',
@@ -397,6 +400,7 @@ export default {
createLabel: 'Направи ознаку',
createNewLabel: 'Направи нову ознаку',
createProject: 'Направи пројекат',
cutCard_title: 'Исеци картицу',
deactivateUser: 'Деактивирај корисника',
deactivateUser_title: 'Деактивирај корисника',
delete: 'Обриши',
+4
View File
@@ -304,6 +304,8 @@ export default {
showOnFrontOfCard: 'Prikaži na prednjoj strani kartice',
smtp: 'SMTP',
sortList_title: 'Složi spisak',
sourceCardIsNoLongerAvailableForCopying: 'Izvorna kartica više nije dostupna za kopiranje.',
sourceCardIsNoLongerAvailableForMoving: 'Izvorna kartica više nije dostupna za premeštanje.',
stopwatch: 'Štoperica',
story: 'Priča',
subscribeToCardWhenCommenting: 'Pretplati se na karticu pri komentarisanju',
@@ -392,6 +394,7 @@ export default {
archiveCards_title: 'Arhiviraj kartice',
assignAsOwner: 'Dodeli kao vlasnika',
cancel: 'Otkaži',
copyCard_title: 'Kopiraj karticu',
createApiKey: 'Kreiraj API ključ',
createBoard: 'Napravi tablu',
createCustomFieldGroup: 'Napravi grupu prilagođenih polja',
@@ -399,6 +402,7 @@ export default {
createLabel: 'Napravi oznaku',
createNewLabel: 'Napravi novu oznaku',
createProject: 'Napravi projekat',
cutCard_title: 'Iseci karticu',
deactivateUser: 'Deaktiviraj korisnika',
deactivateUser_title: 'Deaktiviraj korisnika',
delete: 'Obriši',
+6
View File
@@ -310,6 +310,10 @@ export default {
showOnFrontOfCard: 'Visa på framsidan av kort',
smtp: 'SMTP',
sortList_title: 'Sortera lista',
sourceCardIsNoLongerAvailableForCopying:
'Källkortet är inte längre tillgängligt för kopiering.',
sourceCardIsNoLongerAvailableForMoving:
'Källkortet är inte längre tillgängligt för flyttning.',
stopwatch: 'Timer',
story: 'Berättelse',
subscribeToCardWhenCommenting: 'Prenumerera på kort vid kommentering',
@@ -399,6 +403,7 @@ export default {
archiveCards_title: 'Arkivera kort',
assignAsOwner: 'Tilldela som ägare',
cancel: 'Avbryt',
copyCard_title: 'Kopiera kort',
createApiKey: 'Skapa API-nyckel',
createBoard: 'Skapa tavla',
createCustomFieldGroup: 'Skapa anpassad fältgrupp',
@@ -406,6 +411,7 @@ export default {
createLabel: 'Skapa etikett',
createNewLabel: 'Skapa ny etikett',
createProject: 'Skapa projekt',
cutCard_title: 'Klipp ut kort',
deactivateUser: 'Inaktivera användare',
deactivateUser_title: 'Inaktivera användare',
delete: 'Ta bort',
+4
View File
@@ -306,6 +306,8 @@ export default {
showOnFrontOfCard: 'Kartın ön yüzünde göster',
smtp: 'SMTP',
sortList_title: 'Listeyi sırala',
sourceCardIsNoLongerAvailableForCopying: 'Kaynak kart artık kopyalama için kullanılamıyor.',
sourceCardIsNoLongerAvailableForMoving: 'Kaynak kart artık taşıma için kullanılamıyor.',
stopwatch: 'kronometre',
story: 'Hikaye',
subscribeToCardWhenCommenting: 'Yorum yaparken karta abone ol',
@@ -397,6 +399,7 @@ export default {
archiveCards_title: 'Kartları arşivle',
assignAsOwner: 'Sahip olarak ata',
cancel: 'İptal',
copyCard_title: 'Kartı kopyala',
createApiKey: 'API anahtarı oluştur',
createBoard: 'Pano oluştur',
createCustomFieldGroup: 'Özel alan grubu oluştur',
@@ -404,6 +407,7 @@ export default {
createLabel: 'Etiket oluştur',
createNewLabel: 'Yeni etiket oluştur',
createProject: 'Proje oluştur',
cutCard_title: 'Kartı kes',
deactivateUser: 'Kullanıcıyı devre dışı bırak',
deactivateUser_title: 'Kullanıcıyı devre dışı bırak',
delete: 'Sil',
+4
View File
@@ -305,6 +305,8 @@ export default {
showOnFrontOfCard: 'Показати на лицьовій стороні картки',
smtp: 'SMTP',
sortList_title: 'Сортувати список',
sourceCardIsNoLongerAvailableForCopying: 'Вихідна картка більше недоступна для копіювання.',
sourceCardIsNoLongerAvailableForMoving: 'Вихідна картка більше недоступна для переміщення.',
stopwatch: 'Секундомір',
story: 'Історія',
subscribeToCardWhenCommenting: 'Підпишіться на картку при коментуванні',
@@ -392,6 +394,7 @@ export default {
archiveCards_title: 'Архівувати картки',
assignAsOwner: 'Призначити власником',
cancel: 'Скасувати',
copyCard_title: 'Копіювати картку',
createApiKey: 'Створити ключ API',
createBoard: 'Створити дошку',
createCustomFieldGroup: 'Створити групу користувацьких полів',
@@ -399,6 +402,7 @@ export default {
createLabel: 'Створити мітку',
createNewLabel: 'Створити нову мітку',
createProject: 'Створити проект',
cutCard_title: 'Вирізати картку',
deactivateUser: 'Деактивувати користувача',
deactivateUser_title: 'Деактивувати користувача',
delete: 'Видалити',
+4
View File
@@ -301,6 +301,8 @@ export default {
showOnFrontOfCard: "Karta old tomonida ko'rsatish",
smtp: 'SMTP',
sortList_title: "Ro'yxatni saralash",
sourceCardIsNoLongerAvailableForCopying: 'Manba karta nusxalash uchun endi mavjud emas.',
sourceCardIsNoLongerAvailableForMoving: 'Manba karta koʻchirish uchun endi mavjud emas.',
stopwatch: 'Taymer',
story: 'Hikoya',
subscribeToCardWhenCommenting: "Izoh qoldirganida kartaga obuna bo'lish",
@@ -390,6 +392,7 @@ export default {
archiveCards_title: 'Kartalarni arxivlash',
assignAsOwner: 'Egasi sifatida tayinlash',
cancel: 'Bekor qilish',
copyCard_title: 'Kartani nusxalash',
createApiKey: 'API kalitini yaratish',
createBoard: 'Doska yaratish',
createCustomFieldGroup: 'Maxsus maydon guruhi yaratish',
@@ -397,6 +400,7 @@ export default {
createLabel: 'Yorliq yaratish',
createNewLabel: 'Yangi yorliq yaratish',
createProject: 'Loyiha yaratish',
cutCard_title: 'Kartani kesish',
deactivateUser: 'Foydalanuvchini faolsizlantirish',
deactivateUser_title: 'Foydalanuvchini faolsizlantirish',
delete: "O'chirish",
+4
View File
@@ -283,6 +283,8 @@ export default {
showOnFrontOfCard: '在卡片正面显示',
smtp: 'SMTP',
sortList_title: '排序列表',
sourceCardIsNoLongerAvailableForCopying: '源卡片不再可供复制。',
sourceCardIsNoLongerAvailableForMoving: '源卡片不再可供移动。',
stopwatch: '计时器',
story: '故事',
subscribeToCardWhenCommenting: '评论时自动关注卡片',
@@ -367,6 +369,7 @@ export default {
archiveCards_title: '归档多个卡片',
assignAsOwner: '指定为所有者',
cancel: '取消',
copyCard_title: '复制卡片',
createApiKey: '创建API密钥',
createBoard: '创建面板',
createCustomFieldGroup: '创建自定义字段组',
@@ -374,6 +377,7 @@ export default {
createLabel: '创建标签',
createNewLabel: '创建新标签',
createProject: '创建项目',
cutCard_title: '剪切卡片',
deactivateUser: '停用用户',
deactivateUser_title: '停用用户',
delete: '删除',
+4
View File
@@ -283,6 +283,8 @@ export default {
showOnFrontOfCard: '在卡片正面顯示',
smtp: 'SMTP',
sortList_title: '排序列表',
sourceCardIsNoLongerAvailableForCopying: '來源卡片不再可供複製。',
sourceCardIsNoLongerAvailableForMoving: '來源卡片不再可供移動。',
stopwatch: '碼表',
story: '故事',
subscribeToCardWhenCommenting: '評論時訂閱卡片',
@@ -367,6 +369,7 @@ export default {
archiveCards_title: '封存卡片',
assignAsOwner: '指派為擁有者',
cancel: '取消',
copyCard_title: '複製卡片',
createApiKey: '建立API金鑰',
createBoard: '創建看板',
createCustomFieldGroup: '創建自定義欄位群組',
@@ -374,6 +377,7 @@ export default {
createLabel: '創建標籤',
createNewLabel: '創建新標籤',
createProject: '創建專案',
cutCard_title: '剪下卡片',
deactivateUser: '停用使用者',
deactivateUser_title: '停用使用者',
delete: '刪除',
+2
View File
@@ -60,6 +60,7 @@ export default class extends BaseModel {
case ActionTypes.BOARD_MEMBERSHIP_CREATE_HANDLE:
case ActionTypes.LIST_UPDATE_HANDLE:
case ActionTypes.CARD_UPDATE_HANDLE:
case ActionTypes.CARD_TRANSFER__FAILURE:
if (payload.attachments) {
payload.attachments.forEach((attachment) => {
Attachment.upsert(prepareAttachment(attachment));
@@ -80,6 +81,7 @@ export default class extends BaseModel {
case ActionTypes.BOARD_FETCH__SUCCESS:
case ActionTypes.CARDS_FETCH__SUCCESS:
case ActionTypes.CARD_CREATE_HANDLE:
case ActionTypes.CARD_TRANSFER__SUCCESS:
case ActionTypes.CARD_DUPLICATE__SUCCESS:
payload.attachments.forEach((attachment) => {
Attachment.upsert(prepareAttachment(attachment));
+146 -32
View File
@@ -3,6 +3,7 @@
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
import keyBy from 'lodash/keyBy';
import { attr, fk, many, oneToOne } from 'redux-orm';
import BaseModel from './BaseModel';
@@ -316,33 +317,28 @@ export default class extends BaseModel {
case ActionTypes.CARD_UPDATE: {
const cardModel = Card.withId(payload.id);
// TODO: introduce separate action?
if (payload.data.boardId && payload.data.boardId !== cardModel.boardId) {
cardModel.deleteWithRelated();
} else {
if (payload.data.listId && payload.data.listId !== cardModel.listId) {
payload.data.listChangedAt = new Date(); // eslint-disable-line no-param-reassign
}
if (payload.data.dueDate !== undefined) {
if (payload.data.dueDate) {
if (!cardModel.dueDate) {
payload.data.isDueCompleted = false; // eslint-disable-line no-param-reassign
}
} else {
payload.data.isDueCompleted = null; // eslint-disable-line no-param-reassign
}
}
if (payload.data.isClosed !== undefined && payload.data.isClosed !== cardModel.isClosed) {
cardModel.linkedTasks.update({
isCompleted: payload.data.isClosed,
});
}
cardModel.update(payload.data);
if (payload.data.listId && payload.data.listId !== cardModel.listId) {
payload.data.listChangedAt = new Date(); // eslint-disable-line no-param-reassign
}
if (payload.data.dueDate !== undefined) {
if (payload.data.dueDate) {
if (!cardModel.dueDate) {
payload.data.isDueCompleted = false; // eslint-disable-line no-param-reassign
}
} else {
payload.data.isDueCompleted = null; // eslint-disable-line no-param-reassign
}
}
if (payload.data.isClosed !== undefined && payload.data.isClosed !== cardModel.isClosed) {
cardModel.linkedTasks.update({
isCompleted: payload.data.isClosed,
});
}
cardModel.update(payload.data);
break;
}
case ActionTypes.CARD_UPDATE_HANDLE: {
@@ -378,14 +374,86 @@ export default class extends BaseModel {
break;
}
case ActionTypes.CARD_DUPLICATE:
Card.withId(payload.id).duplicate(payload.localId, payload.data);
case ActionTypes.CARD_TRANSFER: {
const cardModel = Card.withId(payload.id);
if (cardModel) {
cardModel.update(payload.data);
cardModel.syncAfterBoardChange();
}
break;
case ActionTypes.CARD_DUPLICATE__SUCCESS: {
Card.withId(payload.localId).deleteWithRelated();
}
case ActionTypes.CARD_TRANSFER__SUCCESS: {
const cardModel = Card.withId(payload.card.id);
const cardModel = Card.upsert(payload.card);
if (cardModel) {
cardModel.deleteWithRelated();
}
Card.upsert(payload.card);
if (payload.cardMemberships) {
payload.cardMemberships.forEach(({ cardId, userId }) => {
Card.withId(cardId).users.add(userId);
});
}
if (payload.cardLabels) {
payload.cardLabels.forEach(({ cardId, labelId }) => {
Card.withId(cardId).labels.add(labelId);
});
}
break;
}
case ActionTypes.CARD_TRANSFER__FAILURE: {
const cardModel = Card.withId(payload.id);
if (cardModel) {
cardModel.deleteWithRelated();
}
if (payload.card) {
Card.upsert(payload.card);
}
if (payload.cardMemberships) {
payload.cardMemberships.forEach(({ cardId, userId }) => {
Card.withId(cardId).users.add(userId);
});
}
if (payload.cardLabels) {
payload.cardLabels.forEach(({ cardId, labelId }) => {
Card.withId(cardId).labels.add(labelId);
});
}
break;
}
case ActionTypes.CARD_DUPLICATE: {
let cardModel = Card.withId(payload.id);
if (cardModel) {
cardModel = cardModel.duplicate(payload.localId, {
...payload.data,
listChangedAt: new Date(),
});
cardModel.syncAfterBoardChange();
}
break;
}
case ActionTypes.CARD_DUPLICATE__SUCCESS: {
let cardModel = Card.withId(payload.localId);
if (cardModel) {
cardModel.deleteWithRelated();
}
cardModel = Card.upsert(payload.card);
payload.cardMemberships.forEach(({ userId }) => {
cardModel.users.add(userId);
@@ -397,10 +465,15 @@ export default class extends BaseModel {
break;
}
case ActionTypes.CARD_DUPLICATE__FAILURE:
Card.withId(payload.localId).deleteWithRelated();
case ActionTypes.CARD_DUPLICATE__FAILURE: {
const cardModel = Card.withId(payload.localId);
if (cardModel) {
cardModel.deleteWithRelated();
}
break;
}
case ActionTypes.CARD_DELETE:
Card.withId(payload.id).deleteWithRelated();
@@ -637,6 +710,47 @@ export default class extends BaseModel {
return cardModel;
}
syncAfterBoardChange() {
if (!this.board) {
return;
}
const boardMemberships = this.board.memberships.toRefArray();
const userIdsSet = new Set(boardMemberships.map(({ userId }) => userId));
this.users.toRefArray().forEach((user) => {
if (userIdsSet.has(user.id)) {
return;
}
this.users.remove(user.id);
});
this.taskLists.toModelArray().forEach((taskListModel) => {
taskListModel.tasks.toModelArray().forEach((taskModel) => {
if (!taskModel.assigneeUserId || userIdsSet.has(taskModel.assigneeUserId)) {
return;
}
taskModel.update({
assigneeUserId: null,
});
});
});
const labels = this.board.labels.toRefArray();
const labelByName = keyBy(labels, 'name');
this.labels.toRefArray().forEach((label) => {
if (!labelByName[label.name]) {
return;
}
this.labels.remove(label.id);
this.labels.add(labelByName[label.name].id);
});
}
deleteClearable() {
this.users.clear();
this.labels.clear();
+2
View File
@@ -38,6 +38,7 @@ export default class extends BaseModel {
case ActionTypes.BOARD_MEMBERSHIP_CREATE_HANDLE:
case ActionTypes.LIST_UPDATE_HANDLE:
case ActionTypes.CARD_UPDATE_HANDLE:
case ActionTypes.CARD_TRANSFER__FAILURE:
if (payload.customFields) {
payload.customFields.forEach((customField) => {
CustomField.upsert(customField);
@@ -59,6 +60,7 @@ export default class extends BaseModel {
case ActionTypes.BOARD_FETCH__SUCCESS:
case ActionTypes.CARDS_FETCH__SUCCESS:
case ActionTypes.CARD_CREATE_HANDLE:
case ActionTypes.CARD_TRANSFER__SUCCESS:
case ActionTypes.CARD_DUPLICATE__SUCCESS:
payload.customFields.forEach((customField) => {
CustomField.upsert(customField);
+2
View File
@@ -42,6 +42,7 @@ export default class extends BaseModel {
case ActionTypes.BOARD_MEMBERSHIP_CREATE_HANDLE:
case ActionTypes.LIST_UPDATE_HANDLE:
case ActionTypes.CARD_UPDATE_HANDLE:
case ActionTypes.CARD_TRANSFER__FAILURE:
if (payload.customFieldGroups) {
payload.customFieldGroups.forEach((customFieldGroup) => {
CustomFieldGroup.upsert(customFieldGroup);
@@ -62,6 +63,7 @@ export default class extends BaseModel {
case ActionTypes.BOARD_FETCH__SUCCESS:
case ActionTypes.CARDS_FETCH__SUCCESS:
case ActionTypes.CARD_CREATE_HANDLE:
case ActionTypes.CARD_TRANSFER__SUCCESS:
case ActionTypes.CARD_DUPLICATE__SUCCESS:
payload.customFieldGroups.forEach((customFieldGroup) => {
CustomFieldGroup.upsert(customFieldGroup);
+2
View File
@@ -53,6 +53,7 @@ export default class extends BaseModel {
case ActionTypes.BOARD_MEMBERSHIP_CREATE_HANDLE:
case ActionTypes.LIST_UPDATE_HANDLE:
case ActionTypes.CARD_UPDATE_HANDLE:
case ActionTypes.CARD_TRANSFER__FAILURE:
if (payload.customFieldValues) {
payload.customFieldValues.forEach((customFieldValue) => {
CustomFieldValue.upsert(prepareCustomFieldValue(customFieldValue));
@@ -73,6 +74,7 @@ export default class extends BaseModel {
case ActionTypes.BOARD_FETCH__SUCCESS:
case ActionTypes.CARDS_FETCH__SUCCESS:
case ActionTypes.CARD_CREATE_HANDLE:
case ActionTypes.CARD_TRANSFER__SUCCESS:
case ActionTypes.CARD_DUPLICATE__SUCCESS:
payload.customFieldValues.forEach((customFieldValue) => {
CustomFieldValue.upsert(prepareCustomFieldValue(customFieldValue));
+2
View File
@@ -45,6 +45,7 @@ export default class extends BaseModel {
case ActionTypes.BOARD_MEMBERSHIP_CREATE_HANDLE:
case ActionTypes.LIST_UPDATE_HANDLE:
case ActionTypes.CARD_UPDATE_HANDLE:
case ActionTypes.CARD_TRANSFER__FAILURE:
if (payload.tasks) {
payload.tasks.forEach((task) => {
Task.upsert(task);
@@ -65,6 +66,7 @@ export default class extends BaseModel {
case ActionTypes.BOARD_FETCH__SUCCESS:
case ActionTypes.CARDS_FETCH__SUCCESS:
case ActionTypes.CARD_CREATE_HANDLE:
case ActionTypes.CARD_TRANSFER__SUCCESS:
case ActionTypes.CARD_DUPLICATE__SUCCESS:
payload.tasks.forEach((task) => {
Task.upsert(task);
+2
View File
@@ -34,6 +34,7 @@ export default class extends BaseModel {
case ActionTypes.BOARD_MEMBERSHIP_CREATE_HANDLE:
case ActionTypes.LIST_UPDATE_HANDLE:
case ActionTypes.CARD_UPDATE_HANDLE:
case ActionTypes.CARD_TRANSFER__FAILURE:
if (payload.taskLists) {
payload.taskLists.forEach((taskList) => {
TaskList.upsert(taskList);
@@ -54,6 +55,7 @@ export default class extends BaseModel {
case ActionTypes.BOARD_FETCH__SUCCESS:
case ActionTypes.CARDS_FETCH__SUCCESS:
case ActionTypes.CARD_CREATE_HANDLE:
case ActionTypes.CARD_TRANSFER__SUCCESS:
case ActionTypes.CARD_DUPLICATE__SUCCESS:
payload.taskLists.forEach((taskList) => {
TaskList.upsert(taskList);
+2
View File
@@ -105,6 +105,7 @@ export default class extends BaseModel {
case ActionTypes.BOARD_MEMBERSHIP_CREATE_HANDLE:
case ActionTypes.LIST_UPDATE_HANDLE:
case ActionTypes.CARD_UPDATE_HANDLE:
case ActionTypes.CARD_TRANSFER__FAILURE:
if (payload.users) {
payload.users.forEach((user) => {
User.upsert(user);
@@ -351,6 +352,7 @@ export default class extends BaseModel {
case ActionTypes.BOARD_FETCH__SUCCESS:
case ActionTypes.CARDS_FETCH__SUCCESS:
case ActionTypes.CARD_CREATE_HANDLE:
case ActionTypes.CARD_TRANSFER__SUCCESS:
case ActionTypes.COMMENTS_FETCH__SUCCESS:
case ActionTypes.COMMENT_CREATE_HANDLE:
case ActionTypes.ACTIVITIES_IN_BOARD_FETCH__SUCCESS:
+41
View File
@@ -7,6 +7,7 @@ import { LOCATION_CHANGE_HANDLE } from '../lib/redux-router';
import ActionTypes from '../constants/ActionTypes';
import ModalTypes from '../constants/ModalTypes';
import ClipboardTypes from '../constants/ClipboardTypes';
import { HomeViews, ProjectOrders } from '../constants/Enums';
const initialState = {
@@ -15,6 +16,7 @@ const initialState = {
isFavoritesEnabled: false,
isEditModeEnabled: false,
modal: null,
clipboard: null,
config: null,
boardId: null,
cardId: null,
@@ -175,6 +177,45 @@ export default (state = initialState, { type, payload }) => {
}
return state;
case ActionTypes.CARD_DELETE:
if (payload.clipboard && payload.id === payload.clipboard.cardId) {
return {
...state,
clipboard: null,
};
}
return state;
case ActionTypes.CARD_DELETE_HANDLE:
if (payload.clipboard && payload.card.id === payload.clipboard.cardId) {
return {
...state,
clipboard: null,
};
}
return state;
case ActionTypes.CARD_COPY:
return {
...state,
clipboard: {
type: ClipboardTypes.COPY,
cardId: payload.id,
},
};
case ActionTypes.CARD_CUT:
return {
...state,
clipboard: {
type: ClipboardTypes.CUT,
cardId: payload.id,
},
};
case ActionTypes.CARD_PASTE:
return {
...state,
clipboard: null,
};
default:
return state;
}
+170 -8
View File
@@ -4,6 +4,7 @@
*/
import { call, fork, join, put, race, select, take } from 'redux-saga/effects';
import toast from 'react-hot-toast';
import { LOCATION_CHANGE_HANDLE } from '../../../lib/redux-router';
import { goToBoard, goToCard } from './router';
@@ -11,9 +12,12 @@ import request from '../request';
import selectors from '../../../selectors';
import actions from '../../../actions';
import api from '../../../api';
import i18n from '../../../i18n';
import { createLocalId } from '../../../utils/local-id';
import { isListArchiveOrTrash, isListFinite } from '../../../utils/record-helpers';
import ActionTypes from '../../../constants/ActionTypes';
import ClipboardTypes from '../../../constants/ClipboardTypes';
import ToastTypes from '../../../constants/ToastTypes';
import { BoardViews, ListTypes, ListTypeStates } from '../../../constants/Enums';
import LIST_TYPE_STATE_BY_TYPE from '../../../constants/ListTypeStateByType';
@@ -419,7 +423,92 @@ export function* transferCard(id, boardId, listId, index) {
data.position = yield select(selectors.selectNextCardPosition, listId, index, id);
}
yield call(updateCard, id, data);
const typeState = LIST_TYPE_STATE_BY_TYPE[list.type];
yield put(
actions.transferCard(id, {
...data,
isClosed: typeState === ListTypeStates.CLOSED,
}),
);
let card;
let updateError;
try {
({ item: card } = yield call(request, api.updateCard, id, data));
} catch (error) {
updateError = error;
}
let users;
let cardMemberships;
let cardLabels;
let taskLists;
let tasks;
let attachments;
let customFieldGroups;
let customFields;
let customFieldValues;
try {
({
item: card,
included: {
users,
cardMemberships,
cardLabels,
taskLists,
tasks,
attachments,
customFieldGroups,
customFields,
customFieldValues,
},
} = yield call(request, api.getCard, id));
} catch (error) {
yield put(actions.transferCard.failure(id, error));
}
if (updateError) {
yield put(
actions.transferCard.failure(
id,
updateError,
card,
users,
cardMemberships,
cardLabels,
taskLists,
tasks,
attachments,
customFieldGroups,
customFields,
customFieldValues,
),
);
yield call(toast, {
type: ToastTypes.SOURCE_CARD_NOT_MOVABLE,
});
return;
}
yield put(
actions.transferCard.success(
card,
users,
cardMemberships,
cardLabels,
taskLists,
tasks,
attachments,
customFieldGroups,
customFields,
customFieldValues,
),
);
}
export function* transferCurrentCard(boardId, listId, index) {
@@ -431,23 +520,38 @@ export function* transferCurrentCard(boardId, listId, index) {
export function* duplicateCard(id, data) {
const localId = yield call(createLocalId);
const { cardId: currentCardId } = yield select(selectors.selectPath);
const { boardId, listId } = yield select(selectors.selectCardById, id);
const index = yield select(selectors.selectCardIndexById, id);
const sourceCard = yield select(selectors.selectCardById, id);
const boardId = data.boardId || sourceCard.boardId;
const listId = data.listId || sourceCard.listId;
const list = yield select(selectors.selectListById, listId);
const typeState = LIST_TYPE_STATE_BY_TYPE[list.type];
const nextData = {
...data,
};
if (!nextData.position && isListFinite(list)) {
const index = yield select(selectors.selectCardIndexById, id);
nextData.position = yield select(selectors.selectNextCardPosition, listId, index + 1);
}
const currentUserMembership = yield select(
selectors.selectCurrentUserMembershipByBoardId,
boardId,
);
const nextData = {
...data,
position: yield select(selectors.selectNextCardPosition, listId, index + 1),
};
yield put(
actions.duplicateCard(id, localId, {
...nextData,
creatorUserId: currentUserMembership.userId,
isClosed: typeState === ListTypeStates.CLOSED,
...(sourceCard && {
name: `${sourceCard.name} (${i18n.t('common.copy', {
context: 'inline',
})})`,
}),
}),
);
@@ -481,6 +585,11 @@ export function* duplicateCard(id, data) {
} = yield call(request, api.duplicateCard, id, nextData));
} catch (error) {
yield put(actions.duplicateCard.failure(localId, error));
yield call(toast, {
type: ToastTypes.SOURCE_CARD_NOT_COPYABLE,
});
return;
}
@@ -516,6 +625,54 @@ export function* duplicateCurrentCard(data) {
yield call(duplicateCard, cardId, data);
}
export function* copyCard(id) {
yield put(actions.copyCard(id));
}
export function* cutCard(id) {
yield put(actions.cutCard(id));
}
export function* pasteCard(listId) {
const list = yield select(selectors.selectListById, listId);
const clipboard = yield select(selectors.selectClipboard);
const sourceCard = yield select(selectors.selectCardById, clipboard.cardId);
yield put(actions.pasteCard());
if (clipboard.type === ClipboardTypes.COPY) {
const data = {
listId,
};
if (!sourceCard || list.boardId !== sourceCard.boardId) {
data.boardId = list.boardId;
}
if (isListFinite(list)) {
data.position = yield select(selectors.selectNextCardPosition, listId);
}
yield call(duplicateCard, clipboard.cardId, data);
} else if (clipboard.type === ClipboardTypes.CUT) {
if (sourceCard && listId === sourceCard.listId) {
return;
}
yield call(transferCard, clipboard.cardId, list.boardId, list.id);
}
}
export function* pasteCardInCurrentContext() {
const listId = yield select(selectors.selectFirstKanbanListId);
yield call(pasteCard, listId);
}
export function* pasteCardInCurrentList() {
const currentListId = yield select(selectors.selectCurrentListId);
yield call(pasteCard, currentListId);
}
export function* goToAdjacentCard(direction) {
const card = yield select(selectors.selectCurrentCard);
const list = yield select(selectors.selectListById, card.listId);
@@ -617,6 +774,11 @@ export default {
transferCurrentCard,
duplicateCard,
duplicateCurrentCard,
copyCard,
cutCard,
pasteCard,
pasteCardInCurrentContext,
pasteCardInCurrentList,
goToAdjacentCard,
deleteCard,
deleteCurrentCard,
+7
View File
@@ -67,6 +67,13 @@ export default function* cardsWatchers() {
takeEvery(EntryActionTypes.CURRENT_CARD_DUPLICATE, ({ payload: { data } }) =>
services.duplicateCurrentCard(data),
),
takeEvery(EntryActionTypes.CARD_COPY, ({ payload: { id } }) => services.copyCard(id)),
takeEvery(EntryActionTypes.CARD_CUT, ({ payload: { id } }) => services.cutCard(id)),
takeEvery(EntryActionTypes.CARD_PASTE, ({ payload: { listId } }) => services.pasteCard(listId)),
takeEvery(EntryActionTypes.CARD_IN_CURRENT_CONTEXT_PASTE, () =>
services.pasteCardInCurrentContext(),
),
takeEvery(EntryActionTypes.CARD_IN_CURRENT_LIST_PASTE, () => services.pasteCardInCurrentList()),
takeEvery(EntryActionTypes.TO_ADJACENT_CARD_GO, ({ payload: { direction } }) =>
services.goToAdjacentCard(direction),
),
+3
View File
@@ -11,6 +11,8 @@ export const selectIsFavoritesEnabled = ({ core: { isFavoritesEnabled } }) => is
export const selectIsEditModeEnabled = ({ core: { isEditModeEnabled } }) => isEditModeEnabled;
export const selectClipboard = ({ core: { clipboard } }) => clipboard;
export const selectConfig = ({ core: { config } }) => config;
export const selectRecentCardId = ({ core: { recentCardId } }) => recentCardId;
@@ -31,6 +33,7 @@ export default {
selectIsLogouting,
selectIsFavoritesEnabled,
selectIsEditModeEnabled,
selectClipboard,
selectConfig,
selectRecentCardId,
selectPrevCardId,