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

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) => ({ const duplicateCard = (id, localId, data) => ({
type: ActionTypes.CARD_DUPLICATE, type: ActionTypes.CARD_DUPLICATE,
payload: { 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) => ({ const deleteCard = (id) => ({
type: ActionTypes.CARD_DELETE, type: ActionTypes.CARD_DELETE,
payload: { payload: {
@@ -240,7 +325,11 @@ export default {
handleCardCreate, handleCardCreate,
updateCard, updateCard,
handleCardUpdate, handleCardUpdate,
transferCard,
duplicateCard, duplicateCard,
copyCard,
cutCard,
pasteCard,
deleteCard, deleteCard,
handleCardDelete, handleCardDelete,
}; };

View File

@@ -30,12 +30,17 @@ const EndlessContent = React.memo(() => {
[dispatch], [dispatch],
); );
const handleCardPaste = useCallback(() => {
dispatch(entryActions.pasteCardInCurrentList());
}, [dispatch]);
const viewProps = { const viewProps = {
cardIds, cardIds,
isCardsFetching, isCardsFetching,
isAllCardsFetched, isAllCardsFetched,
onCardsFetch: handleCardsFetch, onCardsFetch: handleCardsFetch,
onCardCreate: handleCardCreate, onCardCreate: handleCardCreate,
onCardPaste: handleCardPaste,
}; };
let View; let View;

View File

@@ -26,6 +26,10 @@ const FiniteContent = React.memo(() => {
[dispatch], [dispatch],
); );
const handleCardPaste = useCallback(() => {
dispatch(entryActions.pasteCardInCurrentContext());
}, [dispatch]);
let View; let View;
switch (board.view) { switch (board.view) {
case BoardViews.GRID: case BoardViews.GRID:
@@ -39,7 +43,13 @@ const FiniteContent = React.memo(() => {
default: default:
} }
return <View cardIds={cardIds} onCardCreate={canAddCard ? handleCardCreate : undefined} />; return (
<View
cardIds={cardIds}
onCardCreate={canAddCard ? handleCardCreate : undefined}
onCardPaste={canAddCard ? handleCardPaste : undefined}
/>
);
}); });
export default FiniteContent; export default FiniteContent;

View File

@@ -5,10 +5,11 @@
import React, { useCallback, useState } from 'react'; import React, { useCallback, useState } from 'react';
import PropTypes from 'prop-types'; 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 { useInView } from 'react-intersection-observer';
import { useTranslation } from 'react-i18next'; 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 { useWindowWidth } from '../../../lib/hooks';
import { Masonry } from '../../../lib/custom-ui'; 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'; import styles from './GridView.module.scss';
const GridView = React.memo( const GridView = React.memo(
({ cardIds, isCardsFetching, isAllCardsFetched, onCardsFetch, onCardCreate }) => { ({ cardIds, isCardsFetching, isAllCardsFetched, onCardsFetch, onCardCreate, onCardPaste }) => {
const canAddCard = useSelector((state) => { const clipboard = useSelector(selectors.selectClipboard);
const { canAddCard, canPasteCard } = useSelector((state) => {
const boardMembership = selectors.selectCurrentUserMembershipForCurrentBoard(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 [t] = useTranslation();
const [isAddCardOpened, setIsAddCardOpened] = useState(false); const [isAddCardOpened, setIsAddCardOpened] = useState(false);
@@ -59,17 +67,31 @@ const GridView = React.memo(
<AddCard onCreate={onCardCreate} onClose={handleAddCardClose} /> <AddCard onCreate={onCardCreate} onClose={handleAddCardClose} />
</div> </div>
) : ( ) : (
<Button <div>
type="button" <div className={styles.addCardButtonWrapper}>
disabled={!onCardCreate} <Button
className={styles.addCardButton} type="button"
onClick={handleAddCardClick} disabled={!onCardCreate}
> className={styles.addCardButton}
<PlusMathIcon className={styles.addCardButtonIcon} /> onClick={handleAddCardClick}
<span className={styles.addCardButtonText}> >
{onCardCreate ? t('action.addCard') : t('common.atLeastOneListMustBePresent')} <PlusMathIcon className={styles.addCardButtonIcon} />
</span> <span className={styles.addCardButtonText}>
</Button> {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) => ( {cardIds.map((cardId) => (
<div key={cardId} className={styles.card}> <div key={cardId} className={styles.card}>
@@ -97,6 +119,7 @@ GridView.propTypes = {
isAllCardsFetched: PropTypes.bool, isAllCardsFetched: PropTypes.bool,
onCardsFetch: PropTypes.func, onCardsFetch: PropTypes.func,
onCardCreate: PropTypes.func, onCardCreate: PropTypes.func,
onCardPaste: PropTypes.func,
}; };
GridView.defaultProps = { GridView.defaultProps = {
@@ -104,6 +127,7 @@ GridView.defaultProps = {
isAllCardsFetched: undefined, isAllCardsFetched: undefined,
onCardsFetch: undefined, onCardsFetch: undefined,
onCardCreate: undefined, onCardCreate: undefined,
onCardPaste: undefined,
}; };
export default GridView; export default GridView;

View File

@@ -10,16 +10,16 @@
border-radius: 3px; border-radius: 3px;
color: rgba(255, 255, 255, 0.72); color: rgba(255, 255, 255, 0.72);
cursor: pointer; cursor: pointer;
display: block;
fill: rgba(255, 255, 255, 0.72); fill: rgba(255, 255, 255, 0.72);
flex: 1;
font-weight: normal; font-weight: normal;
height: 42px; height: 42px;
margin: 0;
min-height: 42px; min-height: 42px;
padding: 11px; padding: 11px;
text-align: left; text-align: left;
transition: background 85ms ease-in, opacity 40ms ease-in, transition: background 85ms ease-in, opacity 40ms ease-in,
border-color 85ms ease-in; border-color 85ms ease-in;
width: 100%;
&:active { &:active {
outline: none; outline: none;
@@ -28,6 +28,10 @@
&:hover { &:hover {
background: rgba(0, 0, 0, 0.32); background: rgba(0, 0, 0, 0.32);
} }
&.paste {
flex: 0 0 auto;
}
} }
.addCardButtonIcon { .addCardButtonIcon {
@@ -43,6 +47,11 @@
vertical-align: top; vertical-align: top;
} }
.addCardButtonWrapper {
display: flex;
gap: 6px;
}
.card { .card {
background: rgba(223, 227, 230, 0.8); background: rgba(223, 227, 230, 0.8);
border-radius: 3px; border-radius: 3px;

View File

@@ -6,10 +6,10 @@
import React, { useCallback, useState } from 'react'; import React, { useCallback, useState } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classNames from 'classnames'; import classNames from 'classnames';
import { useSelector } from 'react-redux'; import { shallowEqual, useSelector } from 'react-redux';
import { useInView } from 'react-intersection-observer'; import { useInView } from 'react-intersection-observer';
import { useTranslation } from 'react-i18next'; 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 selectors from '../../../selectors';
import { BoardMembershipRoles } from '../../../constants/Enums'; 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'; import styles from './ListView.module.scss';
const ListView = React.memo( const ListView = React.memo(
({ cardIds, isCardsFetching, isAllCardsFetched, onCardsFetch, onCardCreate }) => { ({ cardIds, isCardsFetching, isAllCardsFetched, onCardsFetch, onCardCreate, onCardPaste }) => {
const canAddCard = useSelector((state) => { const clipboard = useSelector(selectors.selectClipboard);
const { canAddCard, canPasteCard } = useSelector((state) => {
const boardMembership = selectors.selectCurrentUserMembershipForCurrentBoard(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 [t] = useTranslation();
const [isAddCardOpened, setIsAddCardOpened] = useState(false); const [isAddCardOpened, setIsAddCardOpened] = useState(false);
@@ -54,17 +61,29 @@ const ListView = React.memo(
<AddCard onCreate={onCardCreate} onClose={handleAddCardClose} /> <AddCard onCreate={onCardCreate} onClose={handleAddCardClose} />
</div> </div>
) : ( ) : (
<Button <div className={styles.addCardButtonWrapper}>
type="button" <Button
disabled={!onCardCreate} type="button"
className={styles.addCardButton} disabled={!onCardCreate}
onClick={handleAddCardClick} className={styles.addCardButton}
> onClick={handleAddCardClick}
<PlusMathIcon className={styles.addCardButtonIcon} /> >
<span className={styles.addCardButtonText}> <PlusMathIcon className={styles.addCardButtonIcon} />
{onCardCreate ? t('action.addCard') : t('common.atLeastOneListMustBePresent')} <span className={styles.addCardButtonText}>
</span> {onCardCreate ? t('action.addCard') : t('common.atLeastOneListMustBePresent')}
</Button> </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 && ( {cardIds.length > 0 && (
<div className={classNames(styles.segment, styles.cards)}> <div className={classNames(styles.segment, styles.cards)}>
@@ -95,6 +114,7 @@ ListView.propTypes = {
isAllCardsFetched: PropTypes.bool, isAllCardsFetched: PropTypes.bool,
onCardsFetch: PropTypes.func, onCardsFetch: PropTypes.func,
onCardCreate: PropTypes.func, onCardCreate: PropTypes.func,
onCardPaste: PropTypes.func,
}; };
ListView.defaultProps = { ListView.defaultProps = {
@@ -102,6 +122,7 @@ ListView.defaultProps = {
isAllCardsFetched: undefined, isAllCardsFetched: undefined,
onCardsFetch: undefined, onCardsFetch: undefined,
onCardCreate: undefined, onCardCreate: undefined,
onCardPaste: undefined,
}; };
export default ListView; export default ListView;

View File

@@ -12,14 +12,14 @@
cursor: pointer; cursor: pointer;
display: block; display: block;
fill: rgba(255, 255, 255, 0.72); fill: rgba(255, 255, 255, 0.72);
flex: 1;
font-weight: normal; font-weight: normal;
height: 42px; height: 42px;
margin-bottom: 12px; margin: 0;
padding: 11px; padding: 11px;
text-align: left; text-align: left;
transition: background 85ms ease-in, opacity 40ms ease-in, transition: background 85ms ease-in, opacity 40ms ease-in,
border-color 85ms ease-in; border-color 85ms ease-in;
width: 100%;
&:active { &:active {
outline: none; outline: none;
@@ -28,6 +28,10 @@
&:hover { &:hover {
background: rgba(0, 0, 0, 0.32); background: rgba(0, 0, 0, 0.32);
} }
&.paste {
flex: 0 0 auto;
}
} }
.addCardButtonIcon { .addCardButtonIcon {
@@ -43,6 +47,12 @@
vertical-align: top; vertical-align: top;
} }
.addCardButtonWrapper {
display: flex;
gap: 6px;
margin-bottom: 12px;
}
.card { .card {
margin-bottom: 6px; margin-bottom: 6px;
} }

View File

@@ -18,15 +18,34 @@ import { isActiveTextElement } from '../../../utils/element-helpers';
import { isModifierKeyPressed } from '../../../utils/event-helpers'; import { isModifierKeyPressed } from '../../../utils/event-helpers';
import { BoardShortcutsContext } from '../../../contexts'; import { BoardShortcutsContext } from '../../../contexts';
import Paths from '../../../constants/Paths'; import Paths from '../../../constants/Paths';
import { BoardMembershipRoles, ListTypes } from '../../../constants/Enums'; import {
BoardContexts,
BoardMembershipRoles,
BoardViews,
ListTypes,
} from '../../../constants/Enums';
import CardActionsStep from '../../cards/CardActionsStep'; 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) => { const canEditCardName = (boardMembership, list) => {
if (isListArchiveOrTrash(list)) { if (isListArchiveOrTrash(list)) {
return false; return false;
} }
return boardMembership && boardMembership.role === BoardMembershipRoles.EDITOR; return !!boardMembership && boardMembership.role === BoardMembershipRoles.EDITOR;
}; };
const canArchiveCard = (boardMembership, list) => { const canArchiveCard = (boardMembership, list) => {
@@ -34,7 +53,7 @@ const canArchiveCard = (boardMembership, list) => {
return false; return false;
} }
return boardMembership && boardMembership.role === BoardMembershipRoles.EDITOR; return !!boardMembership && boardMembership.role === BoardMembershipRoles.EDITOR;
}; };
const canUseCardMembers = (boardMembership, list) => { const canUseCardMembers = (boardMembership, list) => {
@@ -42,7 +61,7 @@ const canUseCardMembers = (boardMembership, list) => {
return false; return false;
} }
return boardMembership && boardMembership.role === BoardMembershipRoles.EDITOR; return !!boardMembership && boardMembership.role === BoardMembershipRoles.EDITOR;
}; };
const canUseCardLabels = (boardMembership, list) => { const canUseCardLabels = (boardMembership, list) => {
@@ -50,7 +69,7 @@ const canUseCardLabels = (boardMembership, list) => {
return false; return false;
} }
return boardMembership && boardMembership.role === BoardMembershipRoles.EDITOR; return !!boardMembership && boardMembership.role === BoardMembershipRoles.EDITOR;
}; };
const ShortcutsProvider = React.memo(({ children }) => { const ShortcutsProvider = React.memo(({ children }) => {
@@ -58,8 +77,20 @@ const ShortcutsProvider = React.memo(({ children }) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const selectedListRef = useRef(null);
const selectedCardRef = 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) => { const handleCardMouseEnter = useCallback((id, editName, openActions) => {
selectedCardRef.current = { selectedCardRef.current = {
id, id,
@@ -73,15 +104,106 @@ const ShortcutsProvider = React.memo(({ children }) => {
}, []); }, []);
const contextValue = useMemo( const contextValue = useMemo(
() => [handleCardMouseEnter, handleCardMouseLeave], () => [handleListMouseEnter, handleListMouseLeave, handleCardMouseEnter, handleCardMouseLeave],
[handleCardMouseEnter, handleCardMouseLeave], [handleListMouseEnter, handleListMouseLeave, handleCardMouseEnter, handleCardMouseLeave],
); );
useDidUpdate(() => { useDidUpdate(() => {
selectedListRef.current = null;
selectedCardRef.current = null; selectedCardRef.current = null;
}, [cardId, boardId]); }, [cardId, boardId]);
useEffect(() => { 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) => { const handleCardOpen = (event) => {
if (!selectedCardRef.current) { if (!selectedCardRef.current) {
return; return;
@@ -234,6 +356,22 @@ const ShortcutsProvider = React.memo(({ children }) => {
} }
if (isModifierKeyPressed(event)) { if (isModifierKeyPressed(event)) {
switch (event.key) {
case 'c':
handleCardCopy(event);
break;
case 'x':
handleCardCut(event);
break;
case 'v':
handleCardPaste(event);
break;
default:
}
return; return;
} }

View File

@@ -16,6 +16,7 @@ import { closePopup, usePopup } from '../../../lib/popup';
import selectors from '../../../selectors'; import selectors from '../../../selectors';
import { BoardShortcutsContext } from '../../../contexts'; import { BoardShortcutsContext } from '../../../contexts';
import Paths from '../../../constants/Paths'; import Paths from '../../../constants/Paths';
import ClipboardTypes from '../../../constants/ClipboardTypes';
import { BoardMembershipRoles, CardTypes } from '../../../constants/Enums'; import { BoardMembershipRoles, CardTypes } from '../../../constants/Enums';
import ProjectContent from './ProjectContent'; import ProjectContent from './ProjectContent';
import StoryContent from './StoryContent'; import StoryContent from './StoryContent';
@@ -44,14 +45,25 @@ const Card = React.memo(({ id, isInline }) => {
return selectIsCardWithIdRecent(state, id); 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 canUseActions = useSelector((state) => {
const isManager = selectors.selectIsCurrentUserManagerForCurrentProject(state);
if (isManager) {
return true;
}
const boardMembership = selectors.selectCurrentUserMembershipForCurrentBoard(state); const boardMembership = selectors.selectCurrentUserMembershipForCurrentBoard(state);
return !!boardMembership && boardMembership.role === BoardMembershipRoles.EDITOR; return !!boardMembership && boardMembership.role === BoardMembershipRoles.EDITOR;
}); });
const dispatch = useDispatch(); const dispatch = useDispatch();
const [isEditNameOpened, setIsEditNameOpened] = useState(false); const [isEditNameOpened, setIsEditNameOpened] = useState(false);
const [handleCardMouseEnter, handleCardMouseLeave] = useContext(BoardShortcutsContext); const [, , handleCardMouseEnter, handleCardMouseLeave] = useContext(BoardShortcutsContext);
const actionsPopupRef = useRef(null); 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, {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,
jsx-a11y/no-static-element-interactions */} jsx-a11y/no-static-element-interactions */}
<div <div
className={classNames(styles.content, card.isClosed && styles.contentDisabled)} className={classNames(
styles.content,
card.isClosed && styles.contentDisabled,
isCut && styles.contentCut,
)}
onMouseEnter={handleMouseEnter} onMouseEnter={handleMouseEnter}
onMouseLeave={handleCardMouseLeave} onMouseLeave={handleCardMouseLeave}
onClick={handleClick} onClick={handleClick}

View File

@@ -44,6 +44,10 @@
} }
} }
.contentCut {
opacity: 0.5;
}
.contentDisabled { .contentDisabled {
filter: saturate(0.5); filter: saturate(0.5);
opacity: 0.64; opacity: 0.64;

View File

@@ -60,6 +60,8 @@ const CardActionsStep = React.memo(({ cardId, defaultStep, onNameEdit, onClose }
canEditName, canEditName,
canEditDueDate, canEditDueDate,
canEditStopwatch, canEditStopwatch,
canCopy,
canCut,
canDuplicate, canDuplicate,
canMove, canMove,
canRestore, canRestore,
@@ -68,6 +70,8 @@ const CardActionsStep = React.memo(({ cardId, defaultStep, onNameEdit, onClose }
canUseMembers, canUseMembers,
canUseLabels, canUseLabels,
} = useSelector((state) => { } = useSelector((state) => {
const isManager = selectors.selectIsCurrentUserManagerForCurrentProject(state);
const boardMembership = selectors.selectCurrentUserMembershipForCurrentBoard(state); const boardMembership = selectors.selectCurrentUserMembershipForCurrentBoard(state);
const isEditor = !!boardMembership && boardMembership.role === BoardMembershipRoles.EDITOR; const isEditor = !!boardMembership && boardMembership.role === BoardMembershipRoles.EDITOR;
@@ -77,6 +81,8 @@ const CardActionsStep = React.memo(({ cardId, defaultStep, onNameEdit, onClose }
canEditName: false, canEditName: false,
canEditDueDate: false, canEditDueDate: false,
canEditStopwatch: false, canEditStopwatch: false,
canCopy: isManager || isEditor,
canCut: isEditor,
canDuplicate: false, canDuplicate: false,
canMove: false, canMove: false,
canRestore: isEditor, canRestore: isEditor,
@@ -92,6 +98,8 @@ const CardActionsStep = React.memo(({ cardId, defaultStep, onNameEdit, onClose }
canEditName: isEditor, canEditName: isEditor,
canEditDueDate: isEditor, canEditDueDate: isEditor,
canEditStopwatch: isEditor, canEditStopwatch: isEditor,
canCopy: isManager || isEditor,
canCut: isEditor,
canDuplicate: isEditor, canDuplicate: isEditor,
canMove: isEditor, canMove: isEditor,
canRestore: null, canRestore: null,
@@ -117,17 +125,20 @@ const CardActionsStep = React.memo(({ cardId, defaultStep, onNameEdit, onClose }
[cardId, dispatch], [cardId, dispatch],
); );
const handleDuplicateClick = useCallback(() => { const handleCopyClick = useCallback(() => {
dispatch( dispatch(entryActions.copyCard(cardId));
entryActions.duplicateCard(cardId, {
name: `${card.name} (${t('common.copy', {
context: 'inline',
})})`,
}),
);
onClose(); 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(() => { const handleRestoreClick = useCallback(() => {
dispatch(entryActions.moveCard(cardId, card.prevListId, undefined, true)); dispatch(entryActions.moveCard(cardId, card.prevListId, undefined, true));
@@ -344,6 +355,22 @@ const CardActionsStep = React.memo(({ cardId, defaultStep, onNameEdit, onClose }
})} })}
</Menu.Item> </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 && ( {canDuplicate && (
<Menu.Item className={styles.menuItem} onClick={handleDuplicateClick}> <Menu.Item className={styles.menuItem} onClick={handleDuplicateClick}>
<Icon name="copy outline" className={styles.menuItemIcon} /> <Icon name="copy outline" className={styles.menuItemIcon} />

View File

@@ -34,17 +34,17 @@ const MoreActionsStep = React.memo(({ onClose }) => {
const { canEditType, canDuplicate, canMove } = useSelector((state) => { const { canEditType, canDuplicate, canMove } = useSelector((state) => {
const list = selectListById(state, card.listId); const list = selectListById(state, card.listId);
const boardMembership = selectors.selectCurrentUserMembershipForCurrentBoard(state);
const isEditor = !!boardMembership && boardMembership.role === BoardMembershipRoles.EDITOR;
if (isListArchiveOrTrash(list)) { if (isListArchiveOrTrash(list)) {
return { return {
canEditType: false, canEditType: false,
canDuplicate: false, canDuplicate: false,
canMove: false, canMove: isEditor,
}; };
} }
const boardMembership = selectors.selectCurrentUserMembershipForCurrentBoard(state);
const isEditor = !!boardMembership && boardMembership.role === BoardMembershipRoles.EDITOR;
return { return {
canEditType: isEditor, canEditType: isEditor,
canDuplicate: isEditor, canDuplicate: isEditor,
@@ -68,14 +68,8 @@ const MoreActionsStep = React.memo(({ onClose }) => {
); );
const handleDuplicateClick = useCallback(() => { const handleDuplicateClick = useCallback(() => {
dispatch( dispatch(entryActions.duplicateCurrentCard());
entryActions.duplicateCurrentCard({ }, [dispatch]);
name: `${card.name} (${t('common.copy', {
context: 'inline',
})})`,
}),
);
}, [card.name, dispatch, t]);
const handleEditTypeClick = useCallback(() => { const handleEditTypeClick = useCallback(() => {
openStep(StepTypes.EDIT_TYPE); openStep(StepTypes.EDIT_TYPE);

View File

@@ -105,7 +105,7 @@ const ProjectContent = React.memo(() => {
canSubscribe: isMember, canSubscribe: isMember,
canJoin: false, canJoin: false,
canDuplicate: false, canDuplicate: false,
canMove: false, canMove: isEditor,
canRestore: isEditor, canRestore: isEditor,
canArchive: isEditor, canArchive: isEditor,
canDelete: isEditor, canDelete: isEditor,

View File

@@ -104,7 +104,7 @@ const StoryContent = React.memo(() => {
canSubscribe: isMember, canSubscribe: isMember,
canJoin: false, canJoin: false,
canDuplicate: false, canDuplicate: false,
canMove: false, canMove: isEditor,
canRestore: isEditor, canRestore: isEditor,
canArchive: isEditor, canArchive: isEditor,
canDelete: isEditor, canDelete: isEditor,

View File

@@ -7,7 +7,7 @@ import React from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Icon, Message } from 'semantic-ui-react'; import { Icon, Message } from 'semantic-ui-react';
const FileIsTooBig = React.memo(() => { const FileIsTooBigToast = React.memo(() => {
const [t] = useTranslation(); const [t] = useTranslation();
return ( return (
@@ -18,4 +18,4 @@ const FileIsTooBig = React.memo(() => {
); );
}); });
export default FileIsTooBig; export default FileIsTooBigToast;

View File

@@ -7,7 +7,7 @@ import React from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Icon, Message } from 'semantic-ui-react'; import { Icon, Message } from 'semantic-ui-react';
const NotEnoughStorage = React.memo(() => { const NotEnoughStorageToast = React.memo(() => {
const [t] = useTranslation(); const [t] = useTranslation();
return ( return (
@@ -18,4 +18,4 @@ const NotEnoughStorage = React.memo(() => {
); );
}); });
export default NotEnoughStorage; export default NotEnoughStorageToast;

View File

@@ -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;

View File

@@ -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;

View File

@@ -7,14 +7,18 @@ import React from 'react';
import { Toaster as HotToaster, ToastBar as HotToastBar } from 'react-hot-toast'; import { Toaster as HotToaster, ToastBar as HotToastBar } from 'react-hot-toast';
import ToastTypes from '../../../constants/ToastTypes'; import ToastTypes from '../../../constants/ToastTypes';
import FileIsTooBig from './FileIsTooBig'; import FileIsTooBigToast from './FileIsTooBigToast';
import NotEnoughStorage from './NotEnoughStorage'; import NotEnoughStorageToast from './NotEnoughStorageToast';
import EmptyTrashToast from './EmptyTrashToast'; import EmptyTrashToast from './EmptyTrashToast';
import SourceCardNotCopyableToast from './SourceCardNotCopyableToast';
import SourceCardNotMovableToast from './SourceCardNotMovableToast';
const TOAST_BY_TYPE = { const TOAST_BY_TYPE = {
[ToastTypes.FILE_IS_TOO_BIG]: FileIsTooBig, [ToastTypes.FILE_IS_TOO_BIG]: FileIsTooBigToast,
[ToastTypes.NOT_ENOUGH_STORAGE]: NotEnoughStorage, [ToastTypes.NOT_ENOUGH_STORAGE]: NotEnoughStorageToast,
[ToastTypes.EMPTY_TRASH]: EmptyTrashToast, [ToastTypes.EMPTY_TRASH]: EmptyTrashToast,
[ToastTypes.SOURCE_CARD_NOT_COPYABLE]: SourceCardNotCopyableToast,
[ToastTypes.SOURCE_CARD_NOT_MOVABLE]: SourceCardNotMovableToast,
}; };
const Toaster = React.memo(() => ( const Toaster = React.memo(() => (

View File

@@ -5,18 +5,19 @@
import upperFirst from 'lodash/upperFirst'; import upperFirst from 'lodash/upperFirst';
import camelCase from 'lodash/camelCase'; 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 PropTypes from 'prop-types';
import classNames from 'classnames'; import classNames from 'classnames';
import { shallowEqual, useDispatch, useSelector } from 'react-redux'; import { shallowEqual, useDispatch, useSelector } from 'react-redux';
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 { useDidUpdate, useTransitioning } from '../../../lib/hooks'; import { useDidUpdate, useToggle, useTransitioning } from '../../../lib/hooks';
import { usePopup } from '../../../lib/popup'; import { usePopup } from '../../../lib/popup';
import selectors from '../../../selectors'; import selectors from '../../../selectors';
import entryActions from '../../../entry-actions'; import entryActions from '../../../entry-actions';
import { BoardShortcutsContext } from '../../../contexts';
import DroppableTypes from '../../../constants/DroppableTypes'; import DroppableTypes from '../../../constants/DroppableTypes';
import { BoardMembershipRoles, ListTypes } from '../../../constants/Enums'; import { BoardMembershipRoles, ListTypes } from '../../../constants/Enums';
import { ListTypeIcons } from '../../../constants/Icons'; 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 isFavoritesActive = useSelector(selectors.selectIsFavoritesActiveForCurrentUser);
const list = useSelector((state) => selectListById(state, id)); const list = useSelector((state) => selectListById(state, id));
const cardIds = useSelector((state) => selectFilteredCardIdsByListId(state, id)); const cardIds = useSelector((state) => selectFilteredCardIdsByListId(state, id));
const { canEdit, canArchiveCards, canAddCard, canDropCard } = useSelector((state) => { const { canEdit, canArchiveCards, canAddCard, canPasteCard, canDropCard } = useSelector(
const isEditModeEnabled = selectors.selectIsEditModeEnabled(state); // TODO: move out? (state) => {
const isEditModeEnabled = selectors.selectIsEditModeEnabled(state); // TODO: move out?
const boardMembership = selectors.selectCurrentUserMembershipForCurrentBoard(state); const boardMembership = selectors.selectCurrentUserMembershipForCurrentBoard(state);
const isEditor = !!boardMembership && boardMembership.role === BoardMembershipRoles.EDITOR; const isEditor = !!boardMembership && boardMembership.role === BoardMembershipRoles.EDITOR;
return { return {
canEdit: isEditModeEnabled && isEditor, canEdit: isEditModeEnabled && isEditor,
canArchiveCards: list.type === ListTypes.CLOSED && isEditor, canArchiveCards: list.type === ListTypes.CLOSED && isEditor,
canAddCard: isEditor, canAddCard: isEditor,
canDropCard: isEditor, canPasteCard: isEditor,
}; canDropCard: isEditor,
}, shallowEqual); };
},
shallowEqual,
);
const dispatch = useDispatch(); const dispatch = useDispatch();
const [t] = useTranslation(); const [t] = useTranslation();
const [isEditNameOpened, setIsEditNameOpened] = useState(false); const [isEditNameOpened, setIsEditNameOpened] = useState(false);
const [addCardPosition, setAddCardPosition] = useState(null); const [addCardPosition, setAddCardPosition] = useState(null);
const [scrollBottomState, scrollBottom] = useToggle();
const [handleListMouseEnter, handleListMouseLeave] = useContext(BoardShortcutsContext);
const wrapperRef = useRef(null); const wrapperRef = useRef(null);
const cardsWrapperRef = useRef(null); const cardsWrapperRef = useRef(null);
@@ -82,6 +91,17 @@ const List = React.memo(({ id, index }) => {
[id, dispatch, addCardPosition], [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(() => { const handleHeaderClick = useCallback(() => {
if (list.isPersisted && canEdit) { if (list.isPersisted && canEdit) {
setIsEditNameOpened(true); setIsEditNameOpened(true);
@@ -123,6 +143,10 @@ const List = React.memo(({ id, index }) => {
addCardPosition === AddCardPositions.TOP ? 0 : cardsWrapperRef.current.scrollHeight; addCardPosition === AddCardPositions.TOP ? 0 : cardsWrapperRef.current.scrollHeight;
}, [cardIds, addCardPosition]); }, [cardIds, addCardPosition]);
useDidUpdate(() => {
cardsWrapperRef.current.scrollTop = cardsWrapperRef.current.scrollHeight;
}, [scrollBottomState]);
const ActionsPopup = usePopup(ActionsStep); const ActionsPopup = usePopup(ActionsStep);
const ArchiveCardsPopup = usePopup(ArchiveCardsStep); const ArchiveCardsPopup = usePopup(ArchiveCardsStep);
@@ -169,6 +193,8 @@ const List = React.memo(({ id, index }) => {
data-drag-scroller data-drag-scroller
ref={innerRef} ref={innerRef}
className={styles.innerWrapper} className={styles.innerWrapper}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleListMouseLeave}
> >
<div <div
ref={wrapperRef} ref={wrapperRef}
@@ -231,17 +257,29 @@ const List = React.memo(({ id, index }) => {
<div className={styles.cardsOuterWrapper}>{cardsNode}</div> <div className={styles.cardsOuterWrapper}>{cardsNode}</div>
</div> </div>
{!addCardPosition && canAddCard && ( {!addCardPosition && canAddCard && (
<button <div className={styles.addCardButtonWrapper}>
type="button" <button
disabled={!list.isPersisted} type="button"
className={styles.addCardButton} disabled={!list.isPersisted}
onClick={handleAddCardClick} className={styles.addCardButton}
> onClick={handleAddCardClick}
<PlusMathIcon className={styles.addCardButtonIcon} /> >
<span className={styles.addCardButtonText}> <PlusMathIcon className={styles.addCardButtonIcon} />
{cardIds.length > 0 ? t('action.addAnotherCard') : t('action.addCard')} <span className={styles.addCardButtonText}>
</span> {cardIds.length > 0 ? t('action.addAnotherCard') : t('action.addCard')}
</button> </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>
</div> </div>

View File

@@ -15,19 +15,22 @@
cursor: pointer; cursor: pointer;
display: block; display: block;
fill: #6b808c; fill: #6b808c;
flex: 0 0 auto; flex: 1;
font-weight: normal; font-weight: normal;
height: 36px; height: 36px;
outline: none; outline: none;
padding: 8px; padding: 8px;
text-align: left; text-align: left;
width: 100%;
&:hover { &:hover {
background: #c3cbd0; background: #c3cbd0;
color: #17394d; color: #17394d;
fill: #17394d; fill: #17394d;
} }
&.paste {
flex: 0 0 auto;
}
} }
.addCardButtonIcon { .addCardButtonIcon {
@@ -44,6 +47,10 @@
vertical-align: top; vertical-align: top;
} }
.addCardButtonWrapper {
display: flex;
}
.card { .card {
margin-bottom: 8px; margin-bottom: 8px;
} }

View File

@@ -290,6 +290,9 @@ export default {
CARD_DUPLICATE: 'CARD_DUPLICATE', CARD_DUPLICATE: 'CARD_DUPLICATE',
CARD_DUPLICATE__SUCCESS: 'CARD_DUPLICATE__SUCCESS', CARD_DUPLICATE__SUCCESS: 'CARD_DUPLICATE__SUCCESS',
CARD_DUPLICATE__FAILURE: 'CARD_DUPLICATE__FAILURE', CARD_DUPLICATE__FAILURE: 'CARD_DUPLICATE__FAILURE',
CARD_COPY: 'CARD_COPY',
CARD_CUT: 'CARD_CUT',
CARD_PASTE: 'CARD_PASTE',
CARD_DELETE: 'CARD_DELETE', CARD_DELETE: 'CARD_DELETE',
CARD_DELETE__SUCCESS: 'CARD_DELETE__SUCCESS', CARD_DELETE__SUCCESS: 'CARD_DELETE__SUCCESS',
CARD_DELETE__FAILURE: 'CARD_DELETE__FAILURE', CARD_DELETE__FAILURE: 'CARD_DELETE__FAILURE',

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,
};

View File

@@ -202,6 +202,11 @@ export default {
CURRENT_CARD_TRANSFER: `${PREFIX}/CURRENT_CARD_TRANSFER`, CURRENT_CARD_TRANSFER: `${PREFIX}/CURRENT_CARD_TRANSFER`,
CARD_DUPLICATE: `${PREFIX}/CARD_DUPLICATE`, CARD_DUPLICATE: `${PREFIX}/CARD_DUPLICATE`,
CURRENT_CARD_DUPLICATE: `${PREFIX}/CURRENT_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`, TO_ADJACENT_CARD_GO: `${PREFIX}/TO_ADJACENT_CARD_GO`,
CARD_DELETE: `${PREFIX}/CARD_DELETE`, CARD_DELETE: `${PREFIX}/CARD_DELETE`,
CURRENT_CARD_DELETE: `${PREFIX}/CURRENT_CARD_DELETE`, CURRENT_CARD_DELETE: `${PREFIX}/CURRENT_CARD_DELETE`,

View File

@@ -6,9 +6,13 @@
const FILE_IS_TOO_BIG = 'FILE_IS_TOO_BIG'; const FILE_IS_TOO_BIG = 'FILE_IS_TOO_BIG';
const NOT_ENOUGH_STORAGE = 'NOT_ENOUGH_STORAGE'; const NOT_ENOUGH_STORAGE = 'NOT_ENOUGH_STORAGE';
const EMPTY_TRASH = 'EMPTY_TRASH'; 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 { export default {
FILE_IS_TOO_BIG, FILE_IS_TOO_BIG,
NOT_ENOUGH_STORAGE, NOT_ENOUGH_STORAGE,
EMPTY_TRASH, EMPTY_TRASH,
SOURCE_CARD_NOT_COPYABLE,
SOURCE_CARD_NOT_MOVABLE,
}; };

View File

@@ -5,4 +5,4 @@
import { createContext } from 'react'; import { createContext } from 'react';
export default createContext([null, null]); export default createContext([null, null, null, null]);

View File

@@ -135,7 +135,7 @@ const transferCurrentCard = (boardId, listId, index = 0) => ({
}, },
}); });
const duplicateCard = (id, data) => ({ const duplicateCard = (id, data = {}) => ({
type: EntryActionTypes.CARD_DUPLICATE, type: EntryActionTypes.CARD_DUPLICATE,
payload: { payload: {
id, id,
@@ -143,13 +143,44 @@ const duplicateCard = (id, data) => ({
}, },
}); });
const duplicateCurrentCard = (data) => ({ const duplicateCurrentCard = (data = {}) => ({
type: EntryActionTypes.CURRENT_CARD_DUPLICATE, type: EntryActionTypes.CURRENT_CARD_DUPLICATE,
payload: { payload: {
data, 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) => ({ const goToAdjacentCard = (direction) => ({
type: EntryActionTypes.TO_ADJACENT_CARD_GO, type: EntryActionTypes.TO_ADJACENT_CARD_GO,
payload: { payload: {
@@ -196,6 +227,11 @@ export default {
transferCurrentCard, transferCurrentCard,
duplicateCard, duplicateCard,
duplicateCurrentCard, duplicateCurrentCard,
copyCard,
cutCard,
pasteCard,
pasteCardInCurrentContext,
pasteCardInCurrentList,
goToAdjacentCard, goToAdjacentCard,
deleteCard, deleteCard,
deleteCurrentCard, deleteCurrentCard,

View File

@@ -298,6 +298,8 @@ export default {
showOnFrontOfCard: 'عرض في مقدمة البطاقة', showOnFrontOfCard: 'عرض في مقدمة البطاقة',
smtp: 'SMTP', smtp: 'SMTP',
sortList_title: 'فرز القائمة', sortList_title: 'فرز القائمة',
sourceCardIsNoLongerAvailableForCopying: 'البطاقة المصدر لم تعد متاحة للنسخ.',
sourceCardIsNoLongerAvailableForMoving: 'البطاقة المصدر لم تعد متاحة للنقل.',
stopwatch: 'المؤقت', stopwatch: 'المؤقت',
story: 'القصة', story: 'القصة',
subscribeToCardWhenCommenting: 'الاشتراك في البطاقة عند التعليق', subscribeToCardWhenCommenting: 'الاشتراك في البطاقة عند التعليق',
@@ -384,6 +386,7 @@ export default {
archiveCards_title: 'أرشفة البطاقات', archiveCards_title: 'أرشفة البطاقات',
assignAsOwner: 'تعيين كمالك', assignAsOwner: 'تعيين كمالك',
cancel: 'إلغاء', cancel: 'إلغاء',
copyCard_title: 'نسخ البطاقة',
createApiKey: 'إنشاء مفتاح API', createApiKey: 'إنشاء مفتاح API',
createBoard: 'إنشاء لوحة', createBoard: 'إنشاء لوحة',
createCustomFieldGroup: 'إنشاء مجموعة حقل مخصص', createCustomFieldGroup: 'إنشاء مجموعة حقل مخصص',
@@ -391,6 +394,7 @@ export default {
createLabel: 'إنشاء ملصق', createLabel: 'إنشاء ملصق',
createNewLabel: 'إنشاء ملصق جديد', createNewLabel: 'إنشاء ملصق جديد',
createProject: 'إنشاء مشروع', createProject: 'إنشاء مشروع',
cutCard_title: 'قص البطاقة',
deactivateUser: 'إلغاء تفعيل المستخدم', deactivateUser: 'إلغاء تفعيل المستخدم',
deactivateUser_title: 'إلغاء تفعيل المستخدم', deactivateUser_title: 'إلغاء تفعيل المستخدم',
delete: 'حذف', delete: 'حذف',

View File

@@ -310,6 +310,9 @@ export default {
showOnFrontOfCard: 'Показване отпред на картата', showOnFrontOfCard: 'Показване отпред на картата',
smtp: 'SMTP', smtp: 'SMTP',
sortList_title: 'Сортиране на списък', sortList_title: 'Сортиране на списък',
sourceCardIsNoLongerAvailableForCopying: 'Оригиналната карта вече не е налична за копиране.',
sourceCardIsNoLongerAvailableForMoving:
'Оригиналната карта вече не е налична за преместване.',
stopwatch: 'Хронометър', stopwatch: 'Хронометър',
story: 'История', story: 'История',
subscribeToCardWhenCommenting: 'Абониране за карта при коментиране', subscribeToCardWhenCommenting: 'Абониране за карта при коментиране',
@@ -398,6 +401,7 @@ export default {
archiveCards_title: 'Архивиране на карти', archiveCards_title: 'Архивиране на карти',
assignAsOwner: 'Назначаване като собственик', assignAsOwner: 'Назначаване като собственик',
cancel: 'Отказ', cancel: 'Отказ',
copyCard_title: 'Копиране на карта',
createApiKey: 'Създаване на API ключ', createApiKey: 'Създаване на API ключ',
createBoard: 'Създаване на табло', createBoard: 'Създаване на табло',
createCustomFieldGroup: 'Създаване на група персонализирани полета', createCustomFieldGroup: 'Създаване на група персонализирани полета',
@@ -405,6 +409,7 @@ export default {
createLabel: 'Създаване на етикет', createLabel: 'Създаване на етикет',
createNewLabel: 'Създаване на нов етикет', createNewLabel: 'Създаване на нов етикет',
createProject: 'Създаване на проект', createProject: 'Създаване на проект',
cutCard_title: 'Изрязване на карта',
deactivateUser: 'Деактивиране на потребител', deactivateUser: 'Деактивиране на потребител',
deactivateUser_title: 'Деактивиране на потребител', deactivateUser_title: 'Деактивиране на потребител',
delete: 'Изтриване', delete: 'Изтриване',

View File

@@ -309,6 +309,10 @@ export default {
showOnFrontOfCard: 'Mostrar a la part frontal de la targeta', showOnFrontOfCard: 'Mostrar a la part frontal de la targeta',
smtp: 'SMTP', smtp: 'SMTP',
sortList_title: 'Ordenar llista', 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', stopwatch: 'Cronòmetre',
story: 'Història', story: 'Història',
subscribeToCardWhenCommenting: "Subscriure's a la targeta en comentar", subscribeToCardWhenCommenting: "Subscriure's a la targeta en comentar",
@@ -399,6 +403,7 @@ export default {
archiveCards_title: 'Arxivar targetes', archiveCards_title: 'Arxivar targetes',
assignAsOwner: 'Assignar com a propietari', assignAsOwner: 'Assignar com a propietari',
cancel: 'Cancel·lar', cancel: 'Cancel·lar',
copyCard_title: 'Copiar targeta',
createApiKey: 'Crear clau API', createApiKey: 'Crear clau API',
createBoard: 'Crear tauler', createBoard: 'Crear tauler',
createCustomFieldGroup: 'Crear grup de camps personalitzats', createCustomFieldGroup: 'Crear grup de camps personalitzats',
@@ -406,6 +411,7 @@ export default {
createLabel: 'Crear etiqueta', createLabel: 'Crear etiqueta',
createNewLabel: 'Crear nova etiqueta', createNewLabel: 'Crear nova etiqueta',
createProject: 'Crear projecte', createProject: 'Crear projecte',
cutCard_title: 'Tallar targeta',
deactivateUser: 'Desactivar usuari', deactivateUser: 'Desactivar usuari',
deactivateUser_title: 'Desactivar usuari', deactivateUser_title: 'Desactivar usuari',
delete: 'Eliminar', delete: 'Eliminar',

View File

@@ -301,6 +301,9 @@ export default {
showOnFrontOfCard: 'Zobrazit na přední straně karty', showOnFrontOfCard: 'Zobrazit na přední straně karty',
smtp: 'SMTP', smtp: 'SMTP',
sortList_title: 'Řadit podle', 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č', stopwatch: 'Časovač',
story: 'Příběh', story: 'Příběh',
subscribeToCardWhenCommenting: 'Odebírat karty při komentování', subscribeToCardWhenCommenting: 'Odebírat karty při komentování',
@@ -388,6 +391,7 @@ export default {
archiveCards_title: 'Archiv karet', archiveCards_title: 'Archiv karet',
assignAsOwner: 'Přiřadit jako vlastníka', assignAsOwner: 'Přiřadit jako vlastníka',
cancel: 'Zrušit', cancel: 'Zrušit',
copyCard_title: 'Kopírovat kartu',
createApiKey: 'Vytvořit API klíč', createApiKey: 'Vytvořit API klíč',
createBoard: 'Vytvořit nástěnku', createBoard: 'Vytvořit nástěnku',
createCustomFieldGroup: 'Vytvořit vlastní skupinu polí', createCustomFieldGroup: 'Vytvořit vlastní skupinu polí',
@@ -395,6 +399,7 @@ export default {
createLabel: 'Vytvořit štítek', createLabel: 'Vytvořit štítek',
createNewLabel: 'Vytvořit nový štítek', createNewLabel: 'Vytvořit nový štítek',
createProject: 'Vytvořit projekt', createProject: 'Vytvořit projekt',
cutCard_title: 'Vyjmout kartu',
deactivateUser: 'Deaktivace uživatele', deactivateUser: 'Deaktivace uživatele',
deactivateUser_title: 'Deaktivace uživatele', deactivateUser_title: 'Deaktivace uživatele',
delete: 'Smazat', delete: 'Smazat',

View File

@@ -305,6 +305,10 @@ export default {
showOnFrontOfCard: 'Vis på forsiden af kortet', showOnFrontOfCard: 'Vis på forsiden af kortet',
smtp: 'SMTP', smtp: 'SMTP',
sortList_title: 'Sortér liste', 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', stopwatch: 'Stopur',
story: 'Story', story: 'Story',
subscribeToCardWhenCommenting: 'Abonnér på kort ved kommentering', subscribeToCardWhenCommenting: 'Abonnér på kort ved kommentering',
@@ -393,6 +397,7 @@ export default {
archiveCards_title: 'Arkivér kort', archiveCards_title: 'Arkivér kort',
assignAsOwner: 'Sæt som ejer', assignAsOwner: 'Sæt som ejer',
cancel: 'Annuller', cancel: 'Annuller',
copyCard_title: 'Kopiér kort',
createApiKey: 'Opret API-nøgle', createApiKey: 'Opret API-nøgle',
createBoard: 'Opret tavle', createBoard: 'Opret tavle',
createCustomFieldGroup: 'Opret brugerdefineret feltgruppe', createCustomFieldGroup: 'Opret brugerdefineret feltgruppe',
@@ -400,6 +405,7 @@ export default {
createLabel: 'Opret label', createLabel: 'Opret label',
createNewLabel: 'Opret ny label', createNewLabel: 'Opret ny label',
createProject: 'Opret projekt', createProject: 'Opret projekt',
cutCard_title: 'Klip kort',
deactivateUser: 'Deaktivér bruger', deactivateUser: 'Deaktivér bruger',
deactivateUser_title: 'Deaktivér bruger', deactivateUser_title: 'Deaktivér bruger',
delete: 'Slet', delete: 'Slet',

View File

@@ -321,6 +321,9 @@ export default {
showOnFrontOfCard: 'Auf der Vorderseite der Karte anzeigen', showOnFrontOfCard: 'Auf der Vorderseite der Karte anzeigen',
smtp: 'SMTP', smtp: 'SMTP',
sortList_title: 'Liste sortieren', sortList_title: 'Liste sortieren',
sourceCardIsNoLongerAvailableForCopying: 'Quellkarte ist nicht mehr zum Kopieren verfügbar.',
sourceCardIsNoLongerAvailableForMoving:
'Quellkarte ist nicht mehr zum Verschieben verfügbar.',
stopwatch: 'Stoppuhr', stopwatch: 'Stoppuhr',
story: 'Wissen', story: 'Wissen',
subscribeToCardWhenCommenting: 'Karte beim Kommentieren abonnieren', subscribeToCardWhenCommenting: 'Karte beim Kommentieren abonnieren',
@@ -410,6 +413,7 @@ export default {
archiveCards_title: 'Karten archivieren', archiveCards_title: 'Karten archivieren',
assignAsOwner: 'Als Eigentümer zuweisen', assignAsOwner: 'Als Eigentümer zuweisen',
cancel: 'Abbrechen', cancel: 'Abbrechen',
copyCard_title: 'Karte Kopieren',
createApiKey: 'API-Schlüssel erstellen', createApiKey: 'API-Schlüssel erstellen',
createBoard: 'Arbeitsbereich erstellen', createBoard: 'Arbeitsbereich erstellen',
createCustomFieldGroup: 'Feldgruppe erstellen', createCustomFieldGroup: 'Feldgruppe erstellen',
@@ -417,6 +421,7 @@ export default {
createLabel: 'Label erstellen', createLabel: 'Label erstellen',
createNewLabel: 'Neues Label erstellen', createNewLabel: 'Neues Label erstellen',
createProject: 'Projekt erstellen', createProject: 'Projekt erstellen',
cutCard_title: 'Karte Ausschneiden',
deactivateUser: 'Benutzer deaktivieren', deactivateUser: 'Benutzer deaktivieren',
deactivateUser_title: 'Benutzer deaktivieren', deactivateUser_title: 'Benutzer deaktivieren',
delete: 'Löschen', delete: 'Löschen',

View File

@@ -318,6 +318,10 @@ export default {
showOnFrontOfCard: 'Εμφάνιση στο μπροστινό μέρος της κάρτας', showOnFrontOfCard: 'Εμφάνιση στο μπροστινό μέρος της κάρτας',
smtp: 'SMTP', smtp: 'SMTP',
sortList_title: 'Ταξινόμηση λίστας', sortList_title: 'Ταξινόμηση λίστας',
sourceCardIsNoLongerAvailableForCopying:
'Η κάρτα πηγής δεν είναι πλέον διαθέσιμη για αντιγραφή.',
sourceCardIsNoLongerAvailableForMoving:
'Η κάρτα πηγής δεν είναι πλέον διαθέσιμη για μετακίνηση.',
stopwatch: 'Χρονόμετρο', stopwatch: 'Χρονόμετρο',
story: 'Ιστορία', story: 'Ιστορία',
subscribeToCardWhenCommenting: 'Εγγραφή στην κάρτα κατά τη σχολιασμό', subscribeToCardWhenCommenting: 'Εγγραφή στην κάρτα κατά τη σχολιασμό',
@@ -413,6 +417,7 @@ export default {
archiveCards_title: 'Αρχειοθέτηση καρτών', archiveCards_title: 'Αρχειοθέτηση καρτών',
assignAsOwner: 'Ορισμός ως ιδιοκτήτης', assignAsOwner: 'Ορισμός ως ιδιοκτήτης',
cancel: 'Ακύρωση', cancel: 'Ακύρωση',
copyCard_title: 'Αντιγραφή κάρτας',
createApiKey: 'Δημιουργία κλειδιού API', createApiKey: 'Δημιουργία κλειδιού API',
createBoard: 'Δημιουργία πίνακα', createBoard: 'Δημιουργία πίνακα',
createCustomFieldGroup: 'Δημιουργία ομάδας προσαρμοσμένων πεδίων', createCustomFieldGroup: 'Δημιουργία ομάδας προσαρμοσμένων πεδίων',
@@ -420,6 +425,7 @@ export default {
createLabel: 'Δημιουργία ετικέτας', createLabel: 'Δημιουργία ετικέτας',
createNewLabel: 'Δημιουργία νέας ετικέτας', createNewLabel: 'Δημιουργία νέας ετικέτας',
createProject: 'Δημιουργία έργου', createProject: 'Δημιουργία έργου',
cutCard_title: 'Αποκοπή κάρτας',
deactivateUser: 'Απενεργοποίηση χρήστη', deactivateUser: 'Απενεργοποίηση χρήστη',
deactivateUser_title: 'Απενεργοποίηση χρήστη', deactivateUser_title: 'Απενεργοποίηση χρήστη',
delete: 'Διαγραφή', delete: 'Διαγραφή',

View File

@@ -304,6 +304,8 @@ export default {
showOnFrontOfCard: 'Show on front of card', showOnFrontOfCard: 'Show on front of card',
smtp: 'SMTP', smtp: 'SMTP',
sortList_title: 'Sort List', sortList_title: 'Sort List',
sourceCardIsNoLongerAvailableForCopying: 'Source card is no longer available for copying.',
sourceCardIsNoLongerAvailableForMoving: 'Source card is no longer available for moving.',
stopwatch: 'Stopwatch', stopwatch: 'Stopwatch',
story: 'Story', story: 'Story',
subscribeToCardWhenCommenting: 'Subscribe to card when commenting', subscribeToCardWhenCommenting: 'Subscribe to card when commenting',
@@ -391,6 +393,7 @@ export default {
archiveCards_title: 'Archive Cards', archiveCards_title: 'Archive Cards',
assignAsOwner: 'Assign as owner', assignAsOwner: 'Assign as owner',
cancel: 'Cancel', cancel: 'Cancel',
copyCard_title: 'Copy Card',
createApiKey: 'Create API key', createApiKey: 'Create API key',
createBoard: 'Create board', createBoard: 'Create board',
createCustomFieldGroup: 'Create custom field group', createCustomFieldGroup: 'Create custom field group',
@@ -398,6 +401,7 @@ export default {
createLabel: 'Create label', createLabel: 'Create label',
createNewLabel: 'Create new label', createNewLabel: 'Create new label',
createProject: 'Create project', createProject: 'Create project',
cutCard_title: 'Cut Card',
deactivateUser: 'Deactivate user', deactivateUser: 'Deactivate user',
deactivateUser_title: 'Deactivate User', deactivateUser_title: 'Deactivate User',
delete: 'Delete', delete: 'Delete',

View File

@@ -299,6 +299,8 @@ export default {
showOnFrontOfCard: 'Show on front of card', showOnFrontOfCard: 'Show on front of card',
smtp: 'SMTP', smtp: 'SMTP',
sortList_title: 'Sort List', sortList_title: 'Sort List',
sourceCardIsNoLongerAvailableForCopying: 'Source card is no longer available for copying.',
sourceCardIsNoLongerAvailableForMoving: 'Source card is no longer available for moving.',
stopwatch: 'Stopwatch', stopwatch: 'Stopwatch',
story: 'Story', story: 'Story',
subscribeToCardWhenCommenting: 'Subscribe to card when commenting', subscribeToCardWhenCommenting: 'Subscribe to card when commenting',
@@ -386,6 +388,7 @@ export default {
archiveCards_title: 'Archive Cards', archiveCards_title: 'Archive Cards',
assignAsOwner: 'Assign as owner', assignAsOwner: 'Assign as owner',
cancel: 'Cancel', cancel: 'Cancel',
copyCard_title: 'Copy Card',
createApiKey: 'Create API key', createApiKey: 'Create API key',
createBoard: 'Create board', createBoard: 'Create board',
createCustomFieldGroup: 'Create custom field group', createCustomFieldGroup: 'Create custom field group',
@@ -393,6 +396,7 @@ export default {
createLabel: 'Create label', createLabel: 'Create label',
createNewLabel: 'Create new label', createNewLabel: 'Create new label',
createProject: 'Create project', createProject: 'Create project',
cutCard_title: 'Cut Card',
deactivateUser: 'Deactivate user', deactivateUser: 'Deactivate user',
deactivateUser_title: 'Deactivate User', deactivateUser_title: 'Deactivate User',
delete: 'Delete', delete: 'Delete',

View File

@@ -310,6 +310,10 @@ export default {
showOnFrontOfCard: 'Mostrar en el frente de la tarjeta', showOnFrontOfCard: 'Mostrar en el frente de la tarjeta',
smtp: 'SMTP', smtp: 'SMTP',
sortList_title: 'Ordenar lista', 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', stopwatch: 'Cronómetro',
story: 'Historia', story: 'Historia',
subscribeToCardWhenCommenting: 'Suscribirse a la tarjeta al comentar', subscribeToCardWhenCommenting: 'Suscribirse a la tarjeta al comentar',
@@ -399,6 +403,7 @@ export default {
archiveCards_title: 'Archivar tarjetas', archiveCards_title: 'Archivar tarjetas',
assignAsOwner: 'Asignar como propietario', assignAsOwner: 'Asignar como propietario',
cancel: 'Cancelar', cancel: 'Cancelar',
copyCard_title: 'Copiar tarjeta',
createApiKey: 'Crear clave API', createApiKey: 'Crear clave API',
createBoard: 'Crear tablero', createBoard: 'Crear tablero',
createCustomFieldGroup: 'Crear grupo de campos personalizados', createCustomFieldGroup: 'Crear grupo de campos personalizados',
@@ -406,6 +411,7 @@ export default {
createLabel: 'Crear etiqueta', createLabel: 'Crear etiqueta',
createNewLabel: 'Crear nueva etiqueta', createNewLabel: 'Crear nueva etiqueta',
createProject: 'Crear proyecto', createProject: 'Crear proyecto',
cutCard_title: 'Cortar tarjeta',
deactivateUser: 'Desactivar usuario', deactivateUser: 'Desactivar usuario',
deactivateUser_title: 'Desactivar usuario', deactivateUser_title: 'Desactivar usuario',
delete: 'Eliminar', delete: 'Eliminar',

View File

@@ -305,6 +305,8 @@ export default {
showOnFrontOfCard: 'Kuva kaardi ees', showOnFrontOfCard: 'Kuva kaardi ees',
smtp: 'SMTP', smtp: 'SMTP',
sortList_title: 'Nimekiri sorteerimine', sortList_title: 'Nimekiri sorteerimine',
sourceCardIsNoLongerAvailableForCopying: 'Lähtekaart ei ole enam kopeerimiseks saadaval.',
sourceCardIsNoLongerAvailableForMoving: 'Lähtekaart ei ole enam liigutamiseks saadaval.',
stopwatch: 'Stopper', stopwatch: 'Stopper',
story: 'Kirjeldus', story: 'Kirjeldus',
subscribeToCardWhenCommenting: 'Telli kaart, kui kommenteerida', subscribeToCardWhenCommenting: 'Telli kaart, kui kommenteerida',
@@ -393,6 +395,7 @@ export default {
archiveCards_title: 'Arhiveeri kaardid', archiveCards_title: 'Arhiveeri kaardid',
assignAsOwner: 'Määra omanikuks', assignAsOwner: 'Määra omanikuks',
cancel: 'Tühista', cancel: 'Tühista',
copyCard_title: 'Kopeeri kaart',
createApiKey: 'Loo API võti', createApiKey: 'Loo API võti',
createBoard: 'Loo tahvel', createBoard: 'Loo tahvel',
createCustomFieldGroup: 'Loo kohandatud väljade grupp', createCustomFieldGroup: 'Loo kohandatud väljade grupp',
@@ -400,6 +403,7 @@ export default {
createLabel: 'Loo silt', createLabel: 'Loo silt',
createNewLabel: 'Loo uus silt', createNewLabel: 'Loo uus silt',
createProject: 'Loo projekt', createProject: 'Loo projekt',
cutCard_title: 'Lõika kaart',
deactivateUser: 'Deaktiveeri kasutaja', deactivateUser: 'Deaktiveeri kasutaja',
deactivateUser_title: 'Deaktiveeri kasutaja', deactivateUser_title: 'Deaktiveeri kasutaja',
delete: 'Kustuta', delete: 'Kustuta',

View File

@@ -308,6 +308,8 @@ export default {
showOnFrontOfCard: 'نمایش در جلوی کارت', showOnFrontOfCard: 'نمایش در جلوی کارت',
smtp: 'SMTP', smtp: 'SMTP',
sortList_title: 'مرتب‌سازی لیست', sortList_title: 'مرتب‌سازی لیست',
sourceCardIsNoLongerAvailableForCopying: 'کارت مبدا دیگر برای کپی کردن در دسترس نیست.',
sourceCardIsNoLongerAvailableForMoving: 'کارت مبدا دیگر برای انتقال در دسترس نیست.',
stopwatch: 'کرنومتر', stopwatch: 'کرنومتر',
story: 'داستان', story: 'داستان',
subscribeToCardWhenCommenting: 'هنگام نظر دادن به کارت مشترک شو', subscribeToCardWhenCommenting: 'هنگام نظر دادن به کارت مشترک شو',
@@ -395,6 +397,7 @@ export default {
archiveCards_title: 'آرشیو کارت‌ها', archiveCards_title: 'آرشیو کارت‌ها',
assignAsOwner: 'تعیین به عنوان مالک', assignAsOwner: 'تعیین به عنوان مالک',
cancel: 'لغو', cancel: 'لغو',
copyCard_title: 'کپی کارت',
createApiKey: 'ایجاد کلید API', createApiKey: 'ایجاد کلید API',
createBoard: 'ایجاد برد', createBoard: 'ایجاد برد',
createCustomFieldGroup: 'ایجاد گروه فیلد سفارشی', createCustomFieldGroup: 'ایجاد گروه فیلد سفارشی',
@@ -402,6 +405,7 @@ export default {
createLabel: 'ایجاد برچسب', createLabel: 'ایجاد برچسب',
createNewLabel: 'ایجاد برچسب جدید', createNewLabel: 'ایجاد برچسب جدید',
createProject: 'ایجاد پروژه', createProject: 'ایجاد پروژه',
cutCard_title: 'برش کارت',
deactivateUser: 'غیرفعال کردن کاربر', deactivateUser: 'غیرفعال کردن کاربر',
deactivateUser_title: 'غیرفعال کردن کاربر', deactivateUser_title: 'غیرفعال کردن کاربر',
delete: 'حذف', delete: 'حذف',

View File

@@ -301,6 +301,10 @@ export default {
showOnFrontOfCard: 'Näytä kortin etupuolella', showOnFrontOfCard: 'Näytä kortin etupuolella',
smtp: 'SMTP', smtp: 'SMTP',
sortList_title: 'Lajittele lista', 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', stopwatch: 'Ajastin',
story: 'Tarina', story: 'Tarina',
subscribeToCardWhenCommenting: 'Tilaa kortti kommentoidessa', subscribeToCardWhenCommenting: 'Tilaa kortti kommentoidessa',
@@ -392,6 +396,7 @@ export default {
archiveCards_title: 'Arkistoi kortit', archiveCards_title: 'Arkistoi kortit',
assignAsOwner: 'Aseta omistajaksi', assignAsOwner: 'Aseta omistajaksi',
cancel: 'Peruuta', cancel: 'Peruuta',
copyCard_title: 'Kopioi kortti',
createApiKey: 'Luo API-avain', createApiKey: 'Luo API-avain',
createBoard: 'Luo taulu', createBoard: 'Luo taulu',
createCustomFieldGroup: 'Luo mukautettujen kenttien ryhmä', createCustomFieldGroup: 'Luo mukautettujen kenttien ryhmä',
@@ -399,6 +404,7 @@ export default {
createLabel: 'Luo tunniste', createLabel: 'Luo tunniste',
createNewLabel: 'Luo uusi tunniste', createNewLabel: 'Luo uusi tunniste',
createProject: 'Luo projekti', createProject: 'Luo projekti',
cutCard_title: 'Leikkaa kortti',
deactivateUser: 'Poista käyttäjä käytöstä', deactivateUser: 'Poista käyttäjä käytöstä',
deactivateUser_title: 'Poista käyttäjä käytöstä', deactivateUser_title: 'Poista käyttäjä käytöstä',
delete: 'Poista', delete: 'Poista',

View File

@@ -309,6 +309,10 @@ export default {
showOnFrontOfCard: 'Afficher sur le devant de la carte', showOnFrontOfCard: 'Afficher sur le devant de la carte',
smtp: 'SMTP', smtp: 'SMTP',
sortList_title: 'Trier la liste', 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', stopwatch: 'Minuteur',
story: 'Story', story: 'Story',
subscribeToCardWhenCommenting: 'Sabonner à la carte lors de la rédaction dun commentaire', subscribeToCardWhenCommenting: 'Sabonner à la carte lors de la rédaction dun commentaire',
@@ -397,6 +401,7 @@ export default {
archiveCards_title: 'Archiver les cartes', archiveCards_title: 'Archiver les cartes',
assignAsOwner: 'Assigner comme propriétaire', assignAsOwner: 'Assigner comme propriétaire',
cancel: 'Annuler', cancel: 'Annuler',
copyCard_title: 'Copier la carte',
createApiKey: 'Créer une clé API', createApiKey: 'Créer une clé API',
createBoard: 'Créer un tableau', createBoard: 'Créer un tableau',
createCustomFieldGroup: 'Créer un groupe de champs personnalisés', createCustomFieldGroup: 'Créer un groupe de champs personnalisés',
@@ -404,6 +409,7 @@ export default {
createLabel: 'Créer une étiquette', createLabel: 'Créer une étiquette',
createNewLabel: 'Créer une nouvelle étiquette', createNewLabel: 'Créer une nouvelle étiquette',
createProject: 'Créer un projet', createProject: 'Créer un projet',
cutCard_title: 'Couper la carte',
deactivateUser: 'Désactiver lutilisateur', deactivateUser: 'Désactiver lutilisateur',
deactivateUser_title: 'Désactiver lutilisateur', deactivateUser_title: 'Désactiver lutilisateur',
delete: 'Supprimer', delete: 'Supprimer',

View File

@@ -299,6 +299,8 @@ export default {
showOnFrontOfCard: 'Megjelenítés a kártya borítóján', showOnFrontOfCard: 'Megjelenítés a kártya borítóján',
smtp: 'SMTP', smtp: 'SMTP',
sortList_title: 'Rendezés listában', 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', stopwatch: 'Stopper',
story: 'Story', story: 'Story',
subscribeToCardWhenCommenting: 'Feliratkozás a kártyára kommenteléskor', subscribeToCardWhenCommenting: 'Feliratkozás a kártyára kommenteléskor',
@@ -393,6 +395,7 @@ export default {
archiveCards_title: 'Archív kártyák', archiveCards_title: 'Archív kártyák',
assignAsOwner: 'Hozzárendelés tulajdonosnak', assignAsOwner: 'Hozzárendelés tulajdonosnak',
cancel: 'Mégsem', cancel: 'Mégsem',
copyCard_title: 'Kártya másolása',
createApiKey: 'API kulcs létrehozása', createApiKey: 'API kulcs létrehozása',
createBoard: 'Tábla létrehozása', createBoard: 'Tábla létrehozása',
createCustomFieldGroup: 'Egyedi mezőcsoport létrehozása', createCustomFieldGroup: 'Egyedi mezőcsoport létrehozása',
@@ -400,6 +403,7 @@ export default {
createLabel: 'Címke létrehozása', createLabel: 'Címke létrehozása',
createNewLabel: 'Új címke létrehozása', createNewLabel: 'Új címke létrehozása',
createProject: 'Projekt létrehozása', createProject: 'Projekt létrehozása',
cutCard_title: 'Kártya kivágása',
deactivateUser: 'Felhasználó inaktiválása', deactivateUser: 'Felhasználó inaktiválása',
deactivateUser_title: 'Felhasználó inaktiválása', deactivateUser_title: 'Felhasználó inaktiválása',
delete: 'Törlés', delete: 'Törlés',

View File

@@ -306,6 +306,8 @@ export default {
showOnFrontOfCard: 'Tampilkan di depan kartu', showOnFrontOfCard: 'Tampilkan di depan kartu',
smtp: 'SMTP', smtp: 'SMTP',
sortList_title: 'Urutkan daftar', sortList_title: 'Urutkan daftar',
sourceCardIsNoLongerAvailableForCopying: 'Kartu sumber tidak lagi tersedia untuk disalin.',
sourceCardIsNoLongerAvailableForMoving: 'Kartu sumber tidak lagi tersedia untuk dipindahkan.',
stopwatch: 'Stopwatch', stopwatch: 'Stopwatch',
story: 'Cerita', story: 'Cerita',
subscribeToCardWhenCommenting: 'Berlangganan kartu saat berkomentar', subscribeToCardWhenCommenting: 'Berlangganan kartu saat berkomentar',
@@ -394,6 +396,7 @@ export default {
archiveCards_title: 'Arsipkan kartu', archiveCards_title: 'Arsipkan kartu',
assignAsOwner: 'Tetapkan sebagai pemilik', assignAsOwner: 'Tetapkan sebagai pemilik',
cancel: 'Batal', cancel: 'Batal',
copyCard_title: 'Salin Kartu',
createApiKey: 'Buat kunci API', createApiKey: 'Buat kunci API',
createBoard: 'Tambah papan', createBoard: 'Tambah papan',
createCustomFieldGroup: 'Buat grup bidang kustom', createCustomFieldGroup: 'Buat grup bidang kustom',
@@ -401,6 +404,7 @@ export default {
createLabel: 'Tambah label', createLabel: 'Tambah label',
createNewLabel: 'Tambah label baru', createNewLabel: 'Tambah label baru',
createProject: 'Tambah proyek', createProject: 'Tambah proyek',
cutCard_title: 'Potong Kartu',
deactivateUser: 'Nonaktifkan pengguna', deactivateUser: 'Nonaktifkan pengguna',
deactivateUser_title: 'Nonaktifkan pengguna', deactivateUser_title: 'Nonaktifkan pengguna',
delete: 'Hapus', delete: 'Hapus',

View File

@@ -306,6 +306,10 @@ export default {
showOnFrontOfCard: 'Mostra davanti alla scheda', showOnFrontOfCard: 'Mostra davanti alla scheda',
smtp: 'SMTP', smtp: 'SMTP',
sortList_title: 'Ordina', 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', stopwatch: 'Timer',
story: 'Storia', story: 'Storia',
subscribeToCardWhenCommenting: 'Iscrivimi alla scheda quando commento', subscribeToCardWhenCommenting: 'Iscrivimi alla scheda quando commento',
@@ -395,6 +399,7 @@ export default {
archiveCards_title: 'Archivia schede', archiveCards_title: 'Archivia schede',
assignAsOwner: 'Assegna come proprietario', assignAsOwner: 'Assegna come proprietario',
cancel: 'Annulla', cancel: 'Annulla',
copyCard_title: 'Copia scheda',
createApiKey: 'Crea chiave API', createApiKey: 'Crea chiave API',
createBoard: 'Crea bacheca', createBoard: 'Crea bacheca',
createCustomFieldGroup: 'Crea campi personalizzati', createCustomFieldGroup: 'Crea campi personalizzati',
@@ -402,6 +407,7 @@ export default {
createLabel: 'Crea etichetta', createLabel: 'Crea etichetta',
createNewLabel: 'Crea nuova etichetta', createNewLabel: 'Crea nuova etichetta',
createProject: 'Crea progetto', createProject: 'Crea progetto',
cutCard_title: 'Taglia scheda',
deactivateUser: 'Disattiva utente', deactivateUser: 'Disattiva utente',
deactivateUser_title: 'Disattiva utente', deactivateUser_title: 'Disattiva utente',
delete: 'Elimina', delete: 'Elimina',

View File

@@ -302,6 +302,8 @@ export default {
showOnFrontOfCard: 'カードの前面に表示', showOnFrontOfCard: 'カードの前面に表示',
smtp: 'SMTP', smtp: 'SMTP',
sortList_title: 'リストを並び替え', sortList_title: 'リストを並び替え',
sourceCardIsNoLongerAvailableForCopying: 'ソースカードはコピーできなくなりました。',
sourceCardIsNoLongerAvailableForMoving: 'ソースカードは移動できなくなりました。',
stopwatch: 'タイマー', stopwatch: 'タイマー',
story: 'ストーリー', story: 'ストーリー',
subscribeToCardWhenCommenting: 'コメント時にカードを購読', subscribeToCardWhenCommenting: 'コメント時にカードを購読',
@@ -391,6 +393,7 @@ export default {
archiveCards_title: 'カードをアーカイブ', archiveCards_title: 'カードをアーカイブ',
assignAsOwner: 'オーナーとして割り当て', assignAsOwner: 'オーナーとして割り当て',
cancel: 'キャンセル', cancel: 'キャンセル',
copyCard_title: 'カードをコピー',
createApiKey: 'APIキーを作成', createApiKey: 'APIキーを作成',
createBoard: 'ボードを作成', createBoard: 'ボードを作成',
createCustomFieldGroup: 'カスタムフィールドグループを作成', createCustomFieldGroup: 'カスタムフィールドグループを作成',
@@ -398,6 +401,7 @@ export default {
createLabel: 'ラベルを作成', createLabel: 'ラベルを作成',
createNewLabel: '新しいラベルを作成', createNewLabel: '新しいラベルを作成',
createProject: 'プロジェクトを作成', createProject: 'プロジェクトを作成',
cutCard_title: 'カードを切り取り',
deactivateUser: 'ユーザーを非アクティブにする', deactivateUser: 'ユーザーを非アクティブにする',
deactivateUser_title: 'ユーザーを非アクティブにする', deactivateUser_title: 'ユーザーを非アクティブにする',
delete: '削除', delete: '削除',

View File

@@ -296,6 +296,8 @@ export default {
showOnFrontOfCard: '카드 앞면에 표시', showOnFrontOfCard: '카드 앞면에 표시',
smtp: 'SMTP', smtp: 'SMTP',
sortList_title: '목록 정렬', sortList_title: '목록 정렬',
sourceCardIsNoLongerAvailableForCopying: '원본 카드를 더 이상 복사할 수 없습니다.',
sourceCardIsNoLongerAvailableForMoving: '원본 카드를 더 이상 이동할 수 없습니다.',
stopwatch: '스톱워치', stopwatch: '스톱워치',
story: '스토리', story: '스토리',
subscribeToCardWhenCommenting: '댓글 작성 시 카드 구독', subscribeToCardWhenCommenting: '댓글 작성 시 카드 구독',
@@ -388,6 +390,7 @@ export default {
archiveCards_title: '카드들 보관', archiveCards_title: '카드들 보관',
assignAsOwner: '소유자로 지정', assignAsOwner: '소유자로 지정',
cancel: '취소', cancel: '취소',
copyCard_title: '카드 복사',
createApiKey: 'API 키 생성', createApiKey: 'API 키 생성',
createBoard: '보드 생성', createBoard: '보드 생성',
createCustomFieldGroup: '사용자 정의 필드 그룹 생성', createCustomFieldGroup: '사용자 정의 필드 그룹 생성',
@@ -395,6 +398,7 @@ export default {
createLabel: '라벨 생성', createLabel: '라벨 생성',
createNewLabel: '새 라벨 생성', createNewLabel: '새 라벨 생성',
createProject: '프로젝트 생성', createProject: '프로젝트 생성',
cutCard_title: '카드 잘라내기',
deactivateUser: '사용자 비활성화', deactivateUser: '사용자 비활성화',
deactivateUser_title: '사용자 비활성화', deactivateUser_title: '사용자 비활성화',
delete: '삭제', delete: '삭제',

View File

@@ -305,6 +305,9 @@ export default {
showOnFrontOfCard: 'Tonen op voorkant van kaart', showOnFrontOfCard: 'Tonen op voorkant van kaart',
smtp: 'SMTP', smtp: 'SMTP',
sortList_title: 'Lijst sorteren', sortList_title: 'Lijst sorteren',
sourceCardIsNoLongerAvailableForCopying: 'Bronkaart is niet meer beschikbaar voor kopiëren.',
sourceCardIsNoLongerAvailableForMoving:
'Bronkaart is niet meer beschikbaar voor verplaatsen.',
stopwatch: 'Stopwatch', stopwatch: 'Stopwatch',
story: 'Verhaal', story: 'Verhaal',
subscribeToCardWhenCommenting: 'Abonneren op kaart bij het plaatsen van commentaar', subscribeToCardWhenCommenting: 'Abonneren op kaart bij het plaatsen van commentaar',
@@ -396,6 +399,7 @@ export default {
archiveCards_title: 'Kaarten archiveren', archiveCards_title: 'Kaarten archiveren',
assignAsOwner: 'Toewijzen als eigenaar', assignAsOwner: 'Toewijzen als eigenaar',
cancel: 'Annuleren', cancel: 'Annuleren',
copyCard_title: 'Kaart kopiëren',
createApiKey: 'API-sleutel aanmaken', createApiKey: 'API-sleutel aanmaken',
createBoard: 'Bord aanmaken', createBoard: 'Bord aanmaken',
createCustomFieldGroup: 'Aangepaste veldgroep aanmaken', createCustomFieldGroup: 'Aangepaste veldgroep aanmaken',
@@ -403,6 +407,7 @@ export default {
createLabel: 'Label aanmaken', createLabel: 'Label aanmaken',
createNewLabel: 'Nieuw label aanmaken', createNewLabel: 'Nieuw label aanmaken',
createProject: 'Project aanmaken', createProject: 'Project aanmaken',
cutCard_title: 'Kaart knippen',
deactivateUser: 'Gebruiker deactiveren', deactivateUser: 'Gebruiker deactiveren',
deactivateUser_title: 'Gebruiker deactiveren', deactivateUser_title: 'Gebruiker deactiveren',
delete: 'Verwijderen', delete: 'Verwijderen',

View File

@@ -304,6 +304,10 @@ export default {
showOnFrontOfCard: 'Pokazuj na przodzie karty', showOnFrontOfCard: 'Pokazuj na przodzie karty',
smtp: 'SMTP', smtp: 'SMTP',
sortList_title: 'Sortowanie listy', 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', stopwatch: 'Stoper',
story: 'Scenorys', story: 'Scenorys',
subscribeToCardWhenCommenting: 'Subskrybuj kartę przy komentowaniu', subscribeToCardWhenCommenting: 'Subskrybuj kartę przy komentowaniu',
@@ -392,6 +396,7 @@ export default {
archiveCards_title: 'Archiwizuj karty', archiveCards_title: 'Archiwizuj karty',
assignAsOwner: 'Przypisz jako właściciela', assignAsOwner: 'Przypisz jako właściciela',
cancel: 'Anuluj', cancel: 'Anuluj',
copyCard_title: 'Kopiuj kartę',
createApiKey: 'Utwórz klucz API', createApiKey: 'Utwórz klucz API',
createBoard: 'Utwórz tablicę', createBoard: 'Utwórz tablicę',
createCustomFieldGroup: 'Utwórz grupę pól własnych', createCustomFieldGroup: 'Utwórz grupę pól własnych',
@@ -399,6 +404,7 @@ export default {
createLabel: 'Utwórz oznaczenie', createLabel: 'Utwórz oznaczenie',
createNewLabel: 'Utwórz nowe oznaczenie', createNewLabel: 'Utwórz nowe oznaczenie',
createProject: 'Utwórz projekt', createProject: 'Utwórz projekt',
cutCard_title: 'Wytnij kartę',
deactivateUser: 'Dezaktywuj użytkownika', deactivateUser: 'Dezaktywuj użytkownika',
deactivateUser_title: 'Dezaktywuj użytkownika', deactivateUser_title: 'Dezaktywuj użytkownika',
delete: 'Usuń', delete: 'Usuń',

View File

@@ -307,6 +307,10 @@ export default {
showOnFrontOfCard: 'Mostrar na frente do cartão', showOnFrontOfCard: 'Mostrar na frente do cartão',
smtp: 'SMTP', smtp: 'SMTP',
sortList_title: 'Ordenar lista', 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', stopwatch: 'Cronômetro',
story: 'História', story: 'História',
subscribeToCardWhenCommenting: 'Inscrever-se no cartão ao comentar', subscribeToCardWhenCommenting: 'Inscrever-se no cartão ao comentar',
@@ -395,6 +399,7 @@ export default {
archiveCards_title: 'Arquivar cartões', archiveCards_title: 'Arquivar cartões',
assignAsOwner: 'Atribuir como proprietário', assignAsOwner: 'Atribuir como proprietário',
cancel: 'Cancelar', cancel: 'Cancelar',
copyCard_title: 'Copiar cartão',
createApiKey: 'Criar chave API', createApiKey: 'Criar chave API',
createBoard: 'Criar quadro', createBoard: 'Criar quadro',
createCustomFieldGroup: 'Criar grupo de campos personalizados', createCustomFieldGroup: 'Criar grupo de campos personalizados',
@@ -402,6 +407,7 @@ export default {
createLabel: 'Criar rótulo', createLabel: 'Criar rótulo',
createNewLabel: 'Criar novo rótulo', createNewLabel: 'Criar novo rótulo',
createProject: 'Criar projeto', createProject: 'Criar projeto',
cutCard_title: 'Cortar cartão',
deactivateUser: 'Desativar usuário', deactivateUser: 'Desativar usuário',
deactivateUser_title: 'Desativar usuário', deactivateUser_title: 'Desativar usuário',
delete: 'Excluir', delete: 'Excluir',

View File

@@ -309,6 +309,10 @@ export default {
showOnFrontOfCard: 'Mostrar na frente do cartão', showOnFrontOfCard: 'Mostrar na frente do cartão',
smtp: 'SMTP', smtp: 'SMTP',
sortList_title: 'Ordenar lista', 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', stopwatch: 'Cronómetro',
story: 'História', story: 'História',
subscribeToCardWhenCommenting: 'Subscrever cartão ao comentar', subscribeToCardWhenCommenting: 'Subscrever cartão ao comentar',
@@ -398,6 +402,7 @@ export default {
archiveCards_title: 'Arquivar cartões', archiveCards_title: 'Arquivar cartões',
assignAsOwner: 'Atribuir como proprietário', assignAsOwner: 'Atribuir como proprietário',
cancel: 'Cancelar', cancel: 'Cancelar',
copyCard_title: 'Copiar cartão',
createApiKey: 'Criar chave API', createApiKey: 'Criar chave API',
createBoard: 'Criar quadro', createBoard: 'Criar quadro',
createCustomFieldGroup: 'Criar grupo de campos personalizados', createCustomFieldGroup: 'Criar grupo de campos personalizados',
@@ -405,6 +410,7 @@ export default {
createLabel: 'Criar etiqueta', createLabel: 'Criar etiqueta',
createNewLabel: 'Criar nova etiqueta', createNewLabel: 'Criar nova etiqueta',
createProject: 'Criar projeto', createProject: 'Criar projeto',
cutCard_title: 'Cortar cartão',
deactivateUser: 'Desativar utilizador', deactivateUser: 'Desativar utilizador',
deactivateUser_title: 'Desativar utilizador', deactivateUser_title: 'Desativar utilizador',
delete: 'Eliminar', delete: 'Eliminar',

View File

@@ -303,6 +303,9 @@ export default {
showOnFrontOfCard: 'Afișează pe fața cardului', showOnFrontOfCard: 'Afișează pe fața cardului',
smtp: 'SMTP', smtp: 'SMTP',
sortList_title: 'Sortează lista', sortList_title: 'Sortează lista',
sourceCardIsNoLongerAvailableForCopying:
'Cardul sursă nu mai este disponibil pentru copiere.',
sourceCardIsNoLongerAvailableForMoving: 'Cardul sursă nu mai este disponibil pentru mutare.',
stopwatch: 'Cronometru', stopwatch: 'Cronometru',
story: 'Poveste', story: 'Poveste',
subscribeToCardWhenCommenting: 'Abonează-te la card când comentezi', subscribeToCardWhenCommenting: 'Abonează-te la card când comentezi',
@@ -393,6 +396,7 @@ export default {
archiveCards_title: 'Arhivează cardurile', archiveCards_title: 'Arhivează cardurile',
assignAsOwner: 'Atribuie ca proprietar', assignAsOwner: 'Atribuie ca proprietar',
cancel: 'Anulează', cancel: 'Anulează',
copyCard_title: 'Copiază cardul',
createApiKey: 'Creează cheie API', createApiKey: 'Creează cheie API',
createBoard: 'Creați tablă', createBoard: 'Creați tablă',
createCustomFieldGroup: 'Creați grup de câmpuri personalizate', createCustomFieldGroup: 'Creați grup de câmpuri personalizate',
@@ -400,6 +404,7 @@ export default {
createLabel: 'Creați eticheta', createLabel: 'Creați eticheta',
createNewLabel: 'Creați o nouă etichetă', createNewLabel: 'Creați o nouă etichetă',
createProject: 'Creați proiect', createProject: 'Creați proiect',
cutCard_title: 'Taie cardul',
deactivateUser: 'Dezactivați utilizatorul', deactivateUser: 'Dezactivați utilizatorul',
deactivateUser_title: 'Dezactivați utilizatorul', deactivateUser_title: 'Dezactivați utilizatorul',
delete: 'Ștergeți', delete: 'Ștergeți',

View File

@@ -306,6 +306,10 @@ export default {
showOnFrontOfCard: 'Показать на лицевой стороне карточки', showOnFrontOfCard: 'Показать на лицевой стороне карточки',
smtp: 'SMTP', smtp: 'SMTP',
sortList_title: 'Сортировка списка', sortList_title: 'Сортировка списка',
sourceCardIsNoLongerAvailableForCopying:
'Исходная карточка больше не доступна для копирования.',
sourceCardIsNoLongerAvailableForMoving:
'Исходная карточка больше не доступна для перемещения.',
stopwatch: 'Секундомер', stopwatch: 'Секундомер',
story: 'История', story: 'История',
subscribeToCardWhenCommenting: 'Подписаться на карточку при комментировании', subscribeToCardWhenCommenting: 'Подписаться на карточку при комментировании',
@@ -394,6 +398,7 @@ export default {
archiveCards_title: 'Архивировать карточки', archiveCards_title: 'Архивировать карточки',
assignAsOwner: 'Назначить владельцем', assignAsOwner: 'Назначить владельцем',
cancel: 'Отменить', cancel: 'Отменить',
copyCard_title: 'Копировать карточку',
createApiKey: 'Создать ключ API', createApiKey: 'Создать ключ API',
createBoard: 'Создать доску', createBoard: 'Создать доску',
createCustomFieldGroup: 'Создать группу настраиваемых полей', createCustomFieldGroup: 'Создать группу настраиваемых полей',
@@ -401,6 +406,7 @@ export default {
createLabel: 'Создать метку', createLabel: 'Создать метку',
createNewLabel: 'Создать новую метку', createNewLabel: 'Создать новую метку',
createProject: 'Создать проект', createProject: 'Создать проект',
cutCard_title: 'Вырезать карточку',
deactivateUser: 'Деактивировать пользователя', deactivateUser: 'Деактивировать пользователя',
deactivateUser_title: 'Деактивировать пользователя', deactivateUser_title: 'Деактивировать пользователя',
delete: 'Удалить', delete: 'Удалить',

View File

@@ -300,6 +300,8 @@ export default {
showOnFrontOfCard: 'Zobraziť na prednej strane karty', showOnFrontOfCard: 'Zobraziť na prednej strane karty',
smtp: 'SMTP', smtp: 'SMTP',
sortList_title: 'Zoradiť zoznam', 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č', stopwatch: 'Časovač',
story: 'Príbeh', story: 'Príbeh',
subscribeToCardWhenCommenting: 'Odoberať kartu pri komentovaní', subscribeToCardWhenCommenting: 'Odoberať kartu pri komentovaní',
@@ -387,6 +389,7 @@ export default {
archiveCards_title: 'Archivovať karty', archiveCards_title: 'Archivovať karty',
assignAsOwner: 'Prideliť ako vlastníka', assignAsOwner: 'Prideliť ako vlastníka',
cancel: 'Zrušiť', cancel: 'Zrušiť',
copyCard_title: 'Kopírovať kartu',
createApiKey: 'Vytvoriť API kľúč', createApiKey: 'Vytvoriť API kľúč',
createBoard: 'Vytvoriť tabuľu', createBoard: 'Vytvoriť tabuľu',
createCustomFieldGroup: 'Vytvoriť skupinu vlastných polí', createCustomFieldGroup: 'Vytvoriť skupinu vlastných polí',
@@ -394,6 +397,7 @@ export default {
createLabel: 'Vytvoriť štítok', createLabel: 'Vytvoriť štítok',
createNewLabel: 'Vytvoriť nový štítok', createNewLabel: 'Vytvoriť nový štítok',
createProject: 'Vytvoriť projekt', createProject: 'Vytvoriť projekt',
cutCard_title: 'Vystrihnúť kartu',
deactivateUser: 'Deaktivovať používateľa', deactivateUser: 'Deaktivovať používateľa',
deactivateUser_title: 'Deaktivovať používateľa', deactivateUser_title: 'Deaktivovať používateľa',
delete: 'Zmazať', delete: 'Zmazať',

View File

@@ -303,6 +303,8 @@ export default {
showOnFrontOfCard: 'Прикажи на предњој страни картице', showOnFrontOfCard: 'Прикажи на предњој страни картице',
smtp: 'SMTP', smtp: 'SMTP',
sortList_title: 'Сложи списак', sortList_title: 'Сложи списак',
sourceCardIsNoLongerAvailableForCopying: 'Изворна картица више није доступна за копирање.',
sourceCardIsNoLongerAvailableForMoving: 'Изворна картица више није доступна за премештање.',
stopwatch: 'Штоперица', stopwatch: 'Штоперица',
story: 'Прича', story: 'Прича',
subscribeToCardWhenCommenting: 'Претплати се на картицу при коментарисању', subscribeToCardWhenCommenting: 'Претплати се на картицу при коментарисању',
@@ -390,6 +392,7 @@ export default {
archiveCards_title: 'Архивирај картице', archiveCards_title: 'Архивирај картице',
assignAsOwner: 'Додели као власника', assignAsOwner: 'Додели као власника',
cancel: 'Откажи', cancel: 'Откажи',
copyCard_title: 'Копирај картицу',
createApiKey: 'Креирај API кључ', createApiKey: 'Креирај API кључ',
createBoard: 'Направи таблу', createBoard: 'Направи таблу',
createCustomFieldGroup: 'Направи групу прилагођених поља', createCustomFieldGroup: 'Направи групу прилагођених поља',
@@ -397,6 +400,7 @@ export default {
createLabel: 'Направи ознаку', createLabel: 'Направи ознаку',
createNewLabel: 'Направи нову ознаку', createNewLabel: 'Направи нову ознаку',
createProject: 'Направи пројекат', createProject: 'Направи пројекат',
cutCard_title: 'Исеци картицу',
deactivateUser: 'Деактивирај корисника', deactivateUser: 'Деактивирај корисника',
deactivateUser_title: 'Деактивирај корисника', deactivateUser_title: 'Деактивирај корисника',
delete: 'Обриши', delete: 'Обриши',

View File

@@ -304,6 +304,8 @@ export default {
showOnFrontOfCard: 'Prikaži na prednjoj strani kartice', showOnFrontOfCard: 'Prikaži na prednjoj strani kartice',
smtp: 'SMTP', smtp: 'SMTP',
sortList_title: 'Složi spisak', 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', stopwatch: 'Štoperica',
story: 'Priča', story: 'Priča',
subscribeToCardWhenCommenting: 'Pretplati se na karticu pri komentarisanju', subscribeToCardWhenCommenting: 'Pretplati se na karticu pri komentarisanju',
@@ -392,6 +394,7 @@ export default {
archiveCards_title: 'Arhiviraj kartice', archiveCards_title: 'Arhiviraj kartice',
assignAsOwner: 'Dodeli kao vlasnika', assignAsOwner: 'Dodeli kao vlasnika',
cancel: 'Otkaži', cancel: 'Otkaži',
copyCard_title: 'Kopiraj karticu',
createApiKey: 'Kreiraj API ključ', createApiKey: 'Kreiraj API ključ',
createBoard: 'Napravi tablu', createBoard: 'Napravi tablu',
createCustomFieldGroup: 'Napravi grupu prilagođenih polja', createCustomFieldGroup: 'Napravi grupu prilagođenih polja',
@@ -399,6 +402,7 @@ export default {
createLabel: 'Napravi oznaku', createLabel: 'Napravi oznaku',
createNewLabel: 'Napravi novu oznaku', createNewLabel: 'Napravi novu oznaku',
createProject: 'Napravi projekat', createProject: 'Napravi projekat',
cutCard_title: 'Iseci karticu',
deactivateUser: 'Deaktiviraj korisnika', deactivateUser: 'Deaktiviraj korisnika',
deactivateUser_title: 'Deaktiviraj korisnika', deactivateUser_title: 'Deaktiviraj korisnika',
delete: 'Obriši', delete: 'Obriši',

View File

@@ -310,6 +310,10 @@ export default {
showOnFrontOfCard: 'Visa på framsidan av kort', showOnFrontOfCard: 'Visa på framsidan av kort',
smtp: 'SMTP', smtp: 'SMTP',
sortList_title: 'Sortera lista', 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', stopwatch: 'Timer',
story: 'Berättelse', story: 'Berättelse',
subscribeToCardWhenCommenting: 'Prenumerera på kort vid kommentering', subscribeToCardWhenCommenting: 'Prenumerera på kort vid kommentering',
@@ -399,6 +403,7 @@ export default {
archiveCards_title: 'Arkivera kort', archiveCards_title: 'Arkivera kort',
assignAsOwner: 'Tilldela som ägare', assignAsOwner: 'Tilldela som ägare',
cancel: 'Avbryt', cancel: 'Avbryt',
copyCard_title: 'Kopiera kort',
createApiKey: 'Skapa API-nyckel', createApiKey: 'Skapa API-nyckel',
createBoard: 'Skapa tavla', createBoard: 'Skapa tavla',
createCustomFieldGroup: 'Skapa anpassad fältgrupp', createCustomFieldGroup: 'Skapa anpassad fältgrupp',
@@ -406,6 +411,7 @@ export default {
createLabel: 'Skapa etikett', createLabel: 'Skapa etikett',
createNewLabel: 'Skapa ny etikett', createNewLabel: 'Skapa ny etikett',
createProject: 'Skapa projekt', createProject: 'Skapa projekt',
cutCard_title: 'Klipp ut kort',
deactivateUser: 'Inaktivera användare', deactivateUser: 'Inaktivera användare',
deactivateUser_title: 'Inaktivera användare', deactivateUser_title: 'Inaktivera användare',
delete: 'Ta bort', delete: 'Ta bort',

View File

@@ -306,6 +306,8 @@ export default {
showOnFrontOfCard: 'Kartın ön yüzünde göster', showOnFrontOfCard: 'Kartın ön yüzünde göster',
smtp: 'SMTP', smtp: 'SMTP',
sortList_title: 'Listeyi sırala', 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', stopwatch: 'kronometre',
story: 'Hikaye', story: 'Hikaye',
subscribeToCardWhenCommenting: 'Yorum yaparken karta abone ol', subscribeToCardWhenCommenting: 'Yorum yaparken karta abone ol',
@@ -397,6 +399,7 @@ export default {
archiveCards_title: 'Kartları arşivle', archiveCards_title: 'Kartları arşivle',
assignAsOwner: 'Sahip olarak ata', assignAsOwner: 'Sahip olarak ata',
cancel: 'İptal', cancel: 'İptal',
copyCard_title: 'Kartı kopyala',
createApiKey: 'API anahtarı oluştur', createApiKey: 'API anahtarı oluştur',
createBoard: 'Pano oluştur', createBoard: 'Pano oluştur',
createCustomFieldGroup: 'Özel alan grubu oluştur', createCustomFieldGroup: 'Özel alan grubu oluştur',
@@ -404,6 +407,7 @@ export default {
createLabel: 'Etiket oluştur', createLabel: 'Etiket oluştur',
createNewLabel: 'Yeni etiket oluştur', createNewLabel: 'Yeni etiket oluştur',
createProject: 'Proje oluştur', createProject: 'Proje oluştur',
cutCard_title: 'Kartı kes',
deactivateUser: 'Kullanıcıyı devre dışı bırak', deactivateUser: 'Kullanıcıyı devre dışı bırak',
deactivateUser_title: 'Kullanıcıyı devre dışı bırak', deactivateUser_title: 'Kullanıcıyı devre dışı bırak',
delete: 'Sil', delete: 'Sil',

View File

@@ -305,6 +305,8 @@ export default {
showOnFrontOfCard: 'Показати на лицьовій стороні картки', showOnFrontOfCard: 'Показати на лицьовій стороні картки',
smtp: 'SMTP', smtp: 'SMTP',
sortList_title: 'Сортувати список', sortList_title: 'Сортувати список',
sourceCardIsNoLongerAvailableForCopying: 'Вихідна картка більше недоступна для копіювання.',
sourceCardIsNoLongerAvailableForMoving: 'Вихідна картка більше недоступна для переміщення.',
stopwatch: 'Секундомір', stopwatch: 'Секундомір',
story: 'Історія', story: 'Історія',
subscribeToCardWhenCommenting: 'Підпишіться на картку при коментуванні', subscribeToCardWhenCommenting: 'Підпишіться на картку при коментуванні',
@@ -392,6 +394,7 @@ export default {
archiveCards_title: 'Архівувати картки', archiveCards_title: 'Архівувати картки',
assignAsOwner: 'Призначити власником', assignAsOwner: 'Призначити власником',
cancel: 'Скасувати', cancel: 'Скасувати',
copyCard_title: 'Копіювати картку',
createApiKey: 'Створити ключ API', createApiKey: 'Створити ключ API',
createBoard: 'Створити дошку', createBoard: 'Створити дошку',
createCustomFieldGroup: 'Створити групу користувацьких полів', createCustomFieldGroup: 'Створити групу користувацьких полів',
@@ -399,6 +402,7 @@ export default {
createLabel: 'Створити мітку', createLabel: 'Створити мітку',
createNewLabel: 'Створити нову мітку', createNewLabel: 'Створити нову мітку',
createProject: 'Створити проект', createProject: 'Створити проект',
cutCard_title: 'Вирізати картку',
deactivateUser: 'Деактивувати користувача', deactivateUser: 'Деактивувати користувача',
deactivateUser_title: 'Деактивувати користувача', deactivateUser_title: 'Деактивувати користувача',
delete: 'Видалити', delete: 'Видалити',

View File

@@ -301,6 +301,8 @@ export default {
showOnFrontOfCard: "Karta old tomonida ko'rsatish", showOnFrontOfCard: "Karta old tomonida ko'rsatish",
smtp: 'SMTP', smtp: 'SMTP',
sortList_title: "Ro'yxatni saralash", sortList_title: "Ro'yxatni saralash",
sourceCardIsNoLongerAvailableForCopying: 'Manba karta nusxalash uchun endi mavjud emas.',
sourceCardIsNoLongerAvailableForMoving: 'Manba karta koʻchirish uchun endi mavjud emas.',
stopwatch: 'Taymer', stopwatch: 'Taymer',
story: 'Hikoya', story: 'Hikoya',
subscribeToCardWhenCommenting: "Izoh qoldirganida kartaga obuna bo'lish", subscribeToCardWhenCommenting: "Izoh qoldirganida kartaga obuna bo'lish",
@@ -390,6 +392,7 @@ export default {
archiveCards_title: 'Kartalarni arxivlash', archiveCards_title: 'Kartalarni arxivlash',
assignAsOwner: 'Egasi sifatida tayinlash', assignAsOwner: 'Egasi sifatida tayinlash',
cancel: 'Bekor qilish', cancel: 'Bekor qilish',
copyCard_title: 'Kartani nusxalash',
createApiKey: 'API kalitini yaratish', createApiKey: 'API kalitini yaratish',
createBoard: 'Doska yaratish', createBoard: 'Doska yaratish',
createCustomFieldGroup: 'Maxsus maydon guruhi yaratish', createCustomFieldGroup: 'Maxsus maydon guruhi yaratish',
@@ -397,6 +400,7 @@ export default {
createLabel: 'Yorliq yaratish', createLabel: 'Yorliq yaratish',
createNewLabel: 'Yangi yorliq yaratish', createNewLabel: 'Yangi yorliq yaratish',
createProject: 'Loyiha yaratish', createProject: 'Loyiha yaratish',
cutCard_title: 'Kartani kesish',
deactivateUser: 'Foydalanuvchini faolsizlantirish', deactivateUser: 'Foydalanuvchini faolsizlantirish',
deactivateUser_title: 'Foydalanuvchini faolsizlantirish', deactivateUser_title: 'Foydalanuvchini faolsizlantirish',
delete: "O'chirish", delete: "O'chirish",

View File

@@ -283,6 +283,8 @@ export default {
showOnFrontOfCard: '在卡片正面显示', showOnFrontOfCard: '在卡片正面显示',
smtp: 'SMTP', smtp: 'SMTP',
sortList_title: '排序列表', sortList_title: '排序列表',
sourceCardIsNoLongerAvailableForCopying: '源卡片不再可供复制。',
sourceCardIsNoLongerAvailableForMoving: '源卡片不再可供移动。',
stopwatch: '计时器', stopwatch: '计时器',
story: '故事', story: '故事',
subscribeToCardWhenCommenting: '评论时自动关注卡片', subscribeToCardWhenCommenting: '评论时自动关注卡片',
@@ -367,6 +369,7 @@ export default {
archiveCards_title: '归档多个卡片', archiveCards_title: '归档多个卡片',
assignAsOwner: '指定为所有者', assignAsOwner: '指定为所有者',
cancel: '取消', cancel: '取消',
copyCard_title: '复制卡片',
createApiKey: '创建API密钥', createApiKey: '创建API密钥',
createBoard: '创建面板', createBoard: '创建面板',
createCustomFieldGroup: '创建自定义字段组', createCustomFieldGroup: '创建自定义字段组',
@@ -374,6 +377,7 @@ export default {
createLabel: '创建标签', createLabel: '创建标签',
createNewLabel: '创建新标签', createNewLabel: '创建新标签',
createProject: '创建项目', createProject: '创建项目',
cutCard_title: '剪切卡片',
deactivateUser: '停用用户', deactivateUser: '停用用户',
deactivateUser_title: '停用用户', deactivateUser_title: '停用用户',
delete: '删除', delete: '删除',

View File

@@ -283,6 +283,8 @@ export default {
showOnFrontOfCard: '在卡片正面顯示', showOnFrontOfCard: '在卡片正面顯示',
smtp: 'SMTP', smtp: 'SMTP',
sortList_title: '排序列表', sortList_title: '排序列表',
sourceCardIsNoLongerAvailableForCopying: '來源卡片不再可供複製。',
sourceCardIsNoLongerAvailableForMoving: '來源卡片不再可供移動。',
stopwatch: '碼表', stopwatch: '碼表',
story: '故事', story: '故事',
subscribeToCardWhenCommenting: '評論時訂閱卡片', subscribeToCardWhenCommenting: '評論時訂閱卡片',
@@ -367,6 +369,7 @@ export default {
archiveCards_title: '封存卡片', archiveCards_title: '封存卡片',
assignAsOwner: '指派為擁有者', assignAsOwner: '指派為擁有者',
cancel: '取消', cancel: '取消',
copyCard_title: '複製卡片',
createApiKey: '建立API金鑰', createApiKey: '建立API金鑰',
createBoard: '創建看板', createBoard: '創建看板',
createCustomFieldGroup: '創建自定義欄位群組', createCustomFieldGroup: '創建自定義欄位群組',
@@ -374,6 +377,7 @@ export default {
createLabel: '創建標籤', createLabel: '創建標籤',
createNewLabel: '創建新標籤', createNewLabel: '創建新標籤',
createProject: '創建專案', createProject: '創建專案',
cutCard_title: '剪下卡片',
deactivateUser: '停用使用者', deactivateUser: '停用使用者',
deactivateUser_title: '停用使用者', deactivateUser_title: '停用使用者',
delete: '刪除', delete: '刪除',

View File

@@ -60,6 +60,7 @@ export default class extends BaseModel {
case ActionTypes.BOARD_MEMBERSHIP_CREATE_HANDLE: case ActionTypes.BOARD_MEMBERSHIP_CREATE_HANDLE:
case ActionTypes.LIST_UPDATE_HANDLE: case ActionTypes.LIST_UPDATE_HANDLE:
case ActionTypes.CARD_UPDATE_HANDLE: case ActionTypes.CARD_UPDATE_HANDLE:
case ActionTypes.CARD_TRANSFER__FAILURE:
if (payload.attachments) { if (payload.attachments) {
payload.attachments.forEach((attachment) => { payload.attachments.forEach((attachment) => {
Attachment.upsert(prepareAttachment(attachment)); Attachment.upsert(prepareAttachment(attachment));
@@ -80,6 +81,7 @@ export default class extends BaseModel {
case ActionTypes.BOARD_FETCH__SUCCESS: case ActionTypes.BOARD_FETCH__SUCCESS:
case ActionTypes.CARDS_FETCH__SUCCESS: case ActionTypes.CARDS_FETCH__SUCCESS:
case ActionTypes.CARD_CREATE_HANDLE: case ActionTypes.CARD_CREATE_HANDLE:
case ActionTypes.CARD_TRANSFER__SUCCESS:
case ActionTypes.CARD_DUPLICATE__SUCCESS: case ActionTypes.CARD_DUPLICATE__SUCCESS:
payload.attachments.forEach((attachment) => { payload.attachments.forEach((attachment) => {
Attachment.upsert(prepareAttachment(attachment)); Attachment.upsert(prepareAttachment(attachment));

View File

@@ -3,6 +3,7 @@
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md * 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 { attr, fk, many, oneToOne } from 'redux-orm';
import BaseModel from './BaseModel'; import BaseModel from './BaseModel';
@@ -316,33 +317,28 @@ export default class extends BaseModel {
case ActionTypes.CARD_UPDATE: { case ActionTypes.CARD_UPDATE: {
const cardModel = Card.withId(payload.id); const cardModel = Card.withId(payload.id);
// TODO: introduce separate action? if (payload.data.listId && payload.data.listId !== cardModel.listId) {
if (payload.data.boardId && payload.data.boardId !== cardModel.boardId) { payload.data.listChangedAt = new Date(); // eslint-disable-line no-param-reassign
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.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; break;
} }
case ActionTypes.CARD_UPDATE_HANDLE: { case ActionTypes.CARD_UPDATE_HANDLE: {
@@ -378,14 +374,86 @@ export default class extends BaseModel {
break; break;
} }
case ActionTypes.CARD_DUPLICATE: case ActionTypes.CARD_TRANSFER: {
Card.withId(payload.id).duplicate(payload.localId, payload.data); const cardModel = Card.withId(payload.id);
if (cardModel) {
cardModel.update(payload.data);
cardModel.syncAfterBoardChange();
}
break; 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 }) => { payload.cardMemberships.forEach(({ userId }) => {
cardModel.users.add(userId); cardModel.users.add(userId);
@@ -397,10 +465,15 @@ export default class extends BaseModel {
break; break;
} }
case ActionTypes.CARD_DUPLICATE__FAILURE: case ActionTypes.CARD_DUPLICATE__FAILURE: {
Card.withId(payload.localId).deleteWithRelated(); const cardModel = Card.withId(payload.localId);
if (cardModel) {
cardModel.deleteWithRelated();
}
break; break;
}
case ActionTypes.CARD_DELETE: case ActionTypes.CARD_DELETE:
Card.withId(payload.id).deleteWithRelated(); Card.withId(payload.id).deleteWithRelated();
@@ -637,6 +710,47 @@ export default class extends BaseModel {
return cardModel; 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() { deleteClearable() {
this.users.clear(); this.users.clear();
this.labels.clear(); this.labels.clear();

View File

@@ -38,6 +38,7 @@ export default class extends BaseModel {
case ActionTypes.BOARD_MEMBERSHIP_CREATE_HANDLE: case ActionTypes.BOARD_MEMBERSHIP_CREATE_HANDLE:
case ActionTypes.LIST_UPDATE_HANDLE: case ActionTypes.LIST_UPDATE_HANDLE:
case ActionTypes.CARD_UPDATE_HANDLE: case ActionTypes.CARD_UPDATE_HANDLE:
case ActionTypes.CARD_TRANSFER__FAILURE:
if (payload.customFields) { if (payload.customFields) {
payload.customFields.forEach((customField) => { payload.customFields.forEach((customField) => {
CustomField.upsert(customField); CustomField.upsert(customField);
@@ -59,6 +60,7 @@ export default class extends BaseModel {
case ActionTypes.BOARD_FETCH__SUCCESS: case ActionTypes.BOARD_FETCH__SUCCESS:
case ActionTypes.CARDS_FETCH__SUCCESS: case ActionTypes.CARDS_FETCH__SUCCESS:
case ActionTypes.CARD_CREATE_HANDLE: case ActionTypes.CARD_CREATE_HANDLE:
case ActionTypes.CARD_TRANSFER__SUCCESS:
case ActionTypes.CARD_DUPLICATE__SUCCESS: case ActionTypes.CARD_DUPLICATE__SUCCESS:
payload.customFields.forEach((customField) => { payload.customFields.forEach((customField) => {
CustomField.upsert(customField); CustomField.upsert(customField);

View File

@@ -42,6 +42,7 @@ export default class extends BaseModel {
case ActionTypes.BOARD_MEMBERSHIP_CREATE_HANDLE: case ActionTypes.BOARD_MEMBERSHIP_CREATE_HANDLE:
case ActionTypes.LIST_UPDATE_HANDLE: case ActionTypes.LIST_UPDATE_HANDLE:
case ActionTypes.CARD_UPDATE_HANDLE: case ActionTypes.CARD_UPDATE_HANDLE:
case ActionTypes.CARD_TRANSFER__FAILURE:
if (payload.customFieldGroups) { if (payload.customFieldGroups) {
payload.customFieldGroups.forEach((customFieldGroup) => { payload.customFieldGroups.forEach((customFieldGroup) => {
CustomFieldGroup.upsert(customFieldGroup); CustomFieldGroup.upsert(customFieldGroup);
@@ -62,6 +63,7 @@ export default class extends BaseModel {
case ActionTypes.BOARD_FETCH__SUCCESS: case ActionTypes.BOARD_FETCH__SUCCESS:
case ActionTypes.CARDS_FETCH__SUCCESS: case ActionTypes.CARDS_FETCH__SUCCESS:
case ActionTypes.CARD_CREATE_HANDLE: case ActionTypes.CARD_CREATE_HANDLE:
case ActionTypes.CARD_TRANSFER__SUCCESS:
case ActionTypes.CARD_DUPLICATE__SUCCESS: case ActionTypes.CARD_DUPLICATE__SUCCESS:
payload.customFieldGroups.forEach((customFieldGroup) => { payload.customFieldGroups.forEach((customFieldGroup) => {
CustomFieldGroup.upsert(customFieldGroup); CustomFieldGroup.upsert(customFieldGroup);

View File

@@ -53,6 +53,7 @@ export default class extends BaseModel {
case ActionTypes.BOARD_MEMBERSHIP_CREATE_HANDLE: case ActionTypes.BOARD_MEMBERSHIP_CREATE_HANDLE:
case ActionTypes.LIST_UPDATE_HANDLE: case ActionTypes.LIST_UPDATE_HANDLE:
case ActionTypes.CARD_UPDATE_HANDLE: case ActionTypes.CARD_UPDATE_HANDLE:
case ActionTypes.CARD_TRANSFER__FAILURE:
if (payload.customFieldValues) { if (payload.customFieldValues) {
payload.customFieldValues.forEach((customFieldValue) => { payload.customFieldValues.forEach((customFieldValue) => {
CustomFieldValue.upsert(prepareCustomFieldValue(customFieldValue)); CustomFieldValue.upsert(prepareCustomFieldValue(customFieldValue));
@@ -73,6 +74,7 @@ export default class extends BaseModel {
case ActionTypes.BOARD_FETCH__SUCCESS: case ActionTypes.BOARD_FETCH__SUCCESS:
case ActionTypes.CARDS_FETCH__SUCCESS: case ActionTypes.CARDS_FETCH__SUCCESS:
case ActionTypes.CARD_CREATE_HANDLE: case ActionTypes.CARD_CREATE_HANDLE:
case ActionTypes.CARD_TRANSFER__SUCCESS:
case ActionTypes.CARD_DUPLICATE__SUCCESS: case ActionTypes.CARD_DUPLICATE__SUCCESS:
payload.customFieldValues.forEach((customFieldValue) => { payload.customFieldValues.forEach((customFieldValue) => {
CustomFieldValue.upsert(prepareCustomFieldValue(customFieldValue)); CustomFieldValue.upsert(prepareCustomFieldValue(customFieldValue));

View File

@@ -45,6 +45,7 @@ export default class extends BaseModel {
case ActionTypes.BOARD_MEMBERSHIP_CREATE_HANDLE: case ActionTypes.BOARD_MEMBERSHIP_CREATE_HANDLE:
case ActionTypes.LIST_UPDATE_HANDLE: case ActionTypes.LIST_UPDATE_HANDLE:
case ActionTypes.CARD_UPDATE_HANDLE: case ActionTypes.CARD_UPDATE_HANDLE:
case ActionTypes.CARD_TRANSFER__FAILURE:
if (payload.tasks) { if (payload.tasks) {
payload.tasks.forEach((task) => { payload.tasks.forEach((task) => {
Task.upsert(task); Task.upsert(task);
@@ -65,6 +66,7 @@ export default class extends BaseModel {
case ActionTypes.BOARD_FETCH__SUCCESS: case ActionTypes.BOARD_FETCH__SUCCESS:
case ActionTypes.CARDS_FETCH__SUCCESS: case ActionTypes.CARDS_FETCH__SUCCESS:
case ActionTypes.CARD_CREATE_HANDLE: case ActionTypes.CARD_CREATE_HANDLE:
case ActionTypes.CARD_TRANSFER__SUCCESS:
case ActionTypes.CARD_DUPLICATE__SUCCESS: case ActionTypes.CARD_DUPLICATE__SUCCESS:
payload.tasks.forEach((task) => { payload.tasks.forEach((task) => {
Task.upsert(task); Task.upsert(task);

View File

@@ -34,6 +34,7 @@ export default class extends BaseModel {
case ActionTypes.BOARD_MEMBERSHIP_CREATE_HANDLE: case ActionTypes.BOARD_MEMBERSHIP_CREATE_HANDLE:
case ActionTypes.LIST_UPDATE_HANDLE: case ActionTypes.LIST_UPDATE_HANDLE:
case ActionTypes.CARD_UPDATE_HANDLE: case ActionTypes.CARD_UPDATE_HANDLE:
case ActionTypes.CARD_TRANSFER__FAILURE:
if (payload.taskLists) { if (payload.taskLists) {
payload.taskLists.forEach((taskList) => { payload.taskLists.forEach((taskList) => {
TaskList.upsert(taskList); TaskList.upsert(taskList);
@@ -54,6 +55,7 @@ export default class extends BaseModel {
case ActionTypes.BOARD_FETCH__SUCCESS: case ActionTypes.BOARD_FETCH__SUCCESS:
case ActionTypes.CARDS_FETCH__SUCCESS: case ActionTypes.CARDS_FETCH__SUCCESS:
case ActionTypes.CARD_CREATE_HANDLE: case ActionTypes.CARD_CREATE_HANDLE:
case ActionTypes.CARD_TRANSFER__SUCCESS:
case ActionTypes.CARD_DUPLICATE__SUCCESS: case ActionTypes.CARD_DUPLICATE__SUCCESS:
payload.taskLists.forEach((taskList) => { payload.taskLists.forEach((taskList) => {
TaskList.upsert(taskList); TaskList.upsert(taskList);

View File

@@ -105,6 +105,7 @@ export default class extends BaseModel {
case ActionTypes.BOARD_MEMBERSHIP_CREATE_HANDLE: case ActionTypes.BOARD_MEMBERSHIP_CREATE_HANDLE:
case ActionTypes.LIST_UPDATE_HANDLE: case ActionTypes.LIST_UPDATE_HANDLE:
case ActionTypes.CARD_UPDATE_HANDLE: case ActionTypes.CARD_UPDATE_HANDLE:
case ActionTypes.CARD_TRANSFER__FAILURE:
if (payload.users) { if (payload.users) {
payload.users.forEach((user) => { payload.users.forEach((user) => {
User.upsert(user); User.upsert(user);
@@ -351,6 +352,7 @@ export default class extends BaseModel {
case ActionTypes.BOARD_FETCH__SUCCESS: case ActionTypes.BOARD_FETCH__SUCCESS:
case ActionTypes.CARDS_FETCH__SUCCESS: case ActionTypes.CARDS_FETCH__SUCCESS:
case ActionTypes.CARD_CREATE_HANDLE: case ActionTypes.CARD_CREATE_HANDLE:
case ActionTypes.CARD_TRANSFER__SUCCESS:
case ActionTypes.COMMENTS_FETCH__SUCCESS: case ActionTypes.COMMENTS_FETCH__SUCCESS:
case ActionTypes.COMMENT_CREATE_HANDLE: case ActionTypes.COMMENT_CREATE_HANDLE:
case ActionTypes.ACTIVITIES_IN_BOARD_FETCH__SUCCESS: case ActionTypes.ACTIVITIES_IN_BOARD_FETCH__SUCCESS:

View File

@@ -7,6 +7,7 @@ import { LOCATION_CHANGE_HANDLE } from '../lib/redux-router';
import ActionTypes from '../constants/ActionTypes'; import ActionTypes from '../constants/ActionTypes';
import ModalTypes from '../constants/ModalTypes'; import ModalTypes from '../constants/ModalTypes';
import ClipboardTypes from '../constants/ClipboardTypes';
import { HomeViews, ProjectOrders } from '../constants/Enums'; import { HomeViews, ProjectOrders } from '../constants/Enums';
const initialState = { const initialState = {
@@ -15,6 +16,7 @@ const initialState = {
isFavoritesEnabled: false, isFavoritesEnabled: false,
isEditModeEnabled: false, isEditModeEnabled: false,
modal: null, modal: null,
clipboard: null,
config: null, config: null,
boardId: null, boardId: null,
cardId: null, cardId: null,
@@ -175,6 +177,45 @@ export default (state = initialState, { type, payload }) => {
} }
return state; 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: default:
return state; return state;
} }

View File

@@ -4,6 +4,7 @@
*/ */
import { call, fork, join, put, race, select, take } from 'redux-saga/effects'; 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 { LOCATION_CHANGE_HANDLE } from '../../../lib/redux-router';
import { goToBoard, goToCard } from './router'; import { goToBoard, goToCard } from './router';
@@ -11,9 +12,12 @@ import request from '../request';
import selectors from '../../../selectors'; import selectors from '../../../selectors';
import actions from '../../../actions'; import actions from '../../../actions';
import api from '../../../api'; import api from '../../../api';
import i18n from '../../../i18n';
import { createLocalId } from '../../../utils/local-id'; import { createLocalId } from '../../../utils/local-id';
import { isListArchiveOrTrash, isListFinite } from '../../../utils/record-helpers'; import { isListArchiveOrTrash, isListFinite } from '../../../utils/record-helpers';
import ActionTypes from '../../../constants/ActionTypes'; import ActionTypes from '../../../constants/ActionTypes';
import ClipboardTypes from '../../../constants/ClipboardTypes';
import ToastTypes from '../../../constants/ToastTypes';
import { BoardViews, ListTypes, ListTypeStates } from '../../../constants/Enums'; import { BoardViews, ListTypes, ListTypeStates } from '../../../constants/Enums';
import LIST_TYPE_STATE_BY_TYPE from '../../../constants/ListTypeStateByType'; 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); 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) { export function* transferCurrentCard(boardId, listId, index) {
@@ -431,23 +520,38 @@ export function* transferCurrentCard(boardId, listId, index) {
export function* duplicateCard(id, data) { export function* duplicateCard(id, data) {
const localId = yield call(createLocalId); const localId = yield call(createLocalId);
const { cardId: currentCardId } = yield select(selectors.selectPath); const { cardId: currentCardId } = yield select(selectors.selectPath);
const { boardId, listId } = yield select(selectors.selectCardById, id); const sourceCard = yield select(selectors.selectCardById, id);
const index = yield select(selectors.selectCardIndexById, 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( const currentUserMembership = yield select(
selectors.selectCurrentUserMembershipByBoardId, selectors.selectCurrentUserMembershipByBoardId,
boardId, boardId,
); );
const nextData = {
...data,
position: yield select(selectors.selectNextCardPosition, listId, index + 1),
};
yield put( yield put(
actions.duplicateCard(id, localId, { actions.duplicateCard(id, localId, {
...nextData, ...nextData,
creatorUserId: currentUserMembership.userId, 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)); } = yield call(request, api.duplicateCard, id, nextData));
} catch (error) { } catch (error) {
yield put(actions.duplicateCard.failure(localId, error)); yield put(actions.duplicateCard.failure(localId, error));
yield call(toast, {
type: ToastTypes.SOURCE_CARD_NOT_COPYABLE,
});
return; return;
} }
@@ -516,6 +625,54 @@ export function* duplicateCurrentCard(data) {
yield call(duplicateCard, cardId, 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) { export function* goToAdjacentCard(direction) {
const card = yield select(selectors.selectCurrentCard); const card = yield select(selectors.selectCurrentCard);
const list = yield select(selectors.selectListById, card.listId); const list = yield select(selectors.selectListById, card.listId);
@@ -617,6 +774,11 @@ export default {
transferCurrentCard, transferCurrentCard,
duplicateCard, duplicateCard,
duplicateCurrentCard, duplicateCurrentCard,
copyCard,
cutCard,
pasteCard,
pasteCardInCurrentContext,
pasteCardInCurrentList,
goToAdjacentCard, goToAdjacentCard,
deleteCard, deleteCard,
deleteCurrentCard, deleteCurrentCard,

View File

@@ -67,6 +67,13 @@ export default function* cardsWatchers() {
takeEvery(EntryActionTypes.CURRENT_CARD_DUPLICATE, ({ payload: { data } }) => takeEvery(EntryActionTypes.CURRENT_CARD_DUPLICATE, ({ payload: { data } }) =>
services.duplicateCurrentCard(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 } }) => takeEvery(EntryActionTypes.TO_ADJACENT_CARD_GO, ({ payload: { direction } }) =>
services.goToAdjacentCard(direction), services.goToAdjacentCard(direction),
), ),

View File

@@ -11,6 +11,8 @@ export const selectIsFavoritesEnabled = ({ core: { isFavoritesEnabled } }) => is
export const selectIsEditModeEnabled = ({ core: { isEditModeEnabled } }) => isEditModeEnabled; export const selectIsEditModeEnabled = ({ core: { isEditModeEnabled } }) => isEditModeEnabled;
export const selectClipboard = ({ core: { clipboard } }) => clipboard;
export const selectConfig = ({ core: { config } }) => config; export const selectConfig = ({ core: { config } }) => config;
export const selectRecentCardId = ({ core: { recentCardId } }) => recentCardId; export const selectRecentCardId = ({ core: { recentCardId } }) => recentCardId;
@@ -31,6 +33,7 @@ export default {
selectIsLogouting, selectIsLogouting,
selectIsFavoritesEnabled, selectIsFavoritesEnabled,
selectIsEditModeEnabled, selectIsEditModeEnabled,
selectClipboard,
selectConfig, selectConfig,
selectRecentCardId, selectRecentCardId,
selectPrevCardId, selectPrevCardId,

View File

@@ -26,18 +26,25 @@
* application/json: * application/json:
* schema: * schema:
* type: object * type: object
* required:
* - position
* - name
* properties: * properties:
* boardId:
* type: string
* description: ID of the board to duplicate the card to
* example: "1357158568008091265"
* listId:
* type: string
* description: ID of the list to duplicate the card to
* example: "1357158568008091266"
* position: * position:
* type: number * type: number
* minimum: 0 * minimum: 0
* nullable: true
* description: Position for the duplicated card within the list * description: Position for the duplicated card within the list
* example: 65536 * example: 65536
* name: * name:
* type: string * type: string
* maxLength: 1024 * maxLength: 1024
* nullable: true
* description: Name/title for the duplicated card * description: Name/title for the duplicated card
* example: Implement user authentication (copy) * example: Implement user authentication (copy)
* responses: * responses:
@@ -113,6 +120,8 @@
* $ref: '#/components/responses/Forbidden' * $ref: '#/components/responses/Forbidden'
* 404: * 404:
* $ref: '#/components/responses/NotFound' * $ref: '#/components/responses/NotFound'
* 422:
* $ref: '#/components/responses/UnprocessableEntity'
*/ */
const { idInput } = require('../../../utils/inputs'); const { idInput } = require('../../../utils/inputs');
@@ -124,6 +133,18 @@ const Errors = {
CARD_NOT_FOUND: { CARD_NOT_FOUND: {
cardNotFound: 'Card not found', cardNotFound: 'Card not found',
}, },
BOARD_NOT_FOUND: {
boardNotFound: 'Board not found',
},
LIST_NOT_FOUND: {
listNotFound: 'List not found',
},
LIST_MUST_BE_PRESENT: {
listMustBePresent: 'List must be present',
},
POSITION_MUST_BE_PRESENT: {
positionMustBePresent: 'Position must be present',
},
}; };
module.exports = { module.exports = {
@@ -132,15 +153,17 @@ module.exports = {
...idInput, ...idInput,
required: true, required: true,
}, },
boardId: idInput,
listId: idInput,
position: { position: {
type: 'number', type: 'number',
min: 0, min: 0,
required: true, allowNull: true,
}, },
name: { name: {
type: 'string', type: 'string',
maxLength: 1024, maxLength: 1024,
required: true, allowNull: true,
}, },
}, },
@@ -151,6 +174,18 @@ module.exports = {
cardNotFound: { cardNotFound: {
responseType: 'notFound', responseType: 'notFound',
}, },
boardNotFound: {
responseType: 'notFound',
},
listNotFound: {
responseType: 'notFound',
},
listMustBePresent: {
responseType: 'unprocessableEntity',
},
positionMustBePresent: {
responseType: 'unprocessableEntity',
},
}, },
async fn(inputs) { async fn(inputs) {
@@ -160,24 +195,60 @@ module.exports = {
.getPathToProjectById(inputs.id) .getPathToProjectById(inputs.id)
.intercept('pathNotFound', () => Errors.CARD_NOT_FOUND); .intercept('pathNotFound', () => Errors.CARD_NOT_FOUND);
const boardMembership = await BoardMembership.qm.getOneByBoardIdAndUserId( const isProjectManager = await sails.helpers.users.isProjectManager(currentUser.id, project.id);
let boardMembership = await BoardMembership.qm.getOneByBoardIdAndUserId(
board.id, board.id,
currentUser.id, currentUser.id,
); );
if (!boardMembership) { if (!isProjectManager) {
throw Errors.CARD_NOT_FOUND; // Forbidden if (!boardMembership) {
throw Errors.CARD_NOT_FOUND; // Forbidden
}
if (boardMembership.role !== BoardMembership.Roles.EDITOR) {
throw Errors.NOT_ENOUGH_RIGHTS;
}
} }
// TODO: allow for endless lists? let nextProject;
if (!sails.helpers.lists.isFinite(list)) { let nextBoard;
throw Errors.NOT_ENOUGH_RIGHTS;
if (!_.isUndefined(inputs.boardId)) {
({ board: nextBoard, project: nextProject } = await sails.helpers.boards
.getPathToProjectById(inputs.boardId)
.intercept('pathNotFound', () => Errors.BOARD_NOT_FOUND));
boardMembership = await BoardMembership.qm.getOneByBoardIdAndUserId(
nextBoard.id,
currentUser.id,
);
if (!boardMembership) {
throw Errors.BOARD_NOT_FOUND; // Forbidden
}
}
if (!boardMembership) {
throw Errors.LIST_NOT_FOUND; // Forbidden
} }
if (boardMembership.role !== BoardMembership.Roles.EDITOR) { if (boardMembership.role !== BoardMembership.Roles.EDITOR) {
throw Errors.NOT_ENOUGH_RIGHTS; throw Errors.NOT_ENOUGH_RIGHTS;
} }
let nextList;
if (!_.isUndefined(inputs.listId)) {
nextList = await List.qm.getOneById(inputs.listId, {
boardId: (nextBoard || board).id,
});
if (!nextList) {
throw Errors.LIST_NOT_FOUND;
}
}
const values = _.pick(inputs, ['position', 'name']); const values = _.pick(inputs, ['position', 'name']);
const { const {
@@ -190,17 +261,23 @@ module.exports = {
customFieldGroups, customFieldGroups,
customFields, customFields,
customFieldValues, customFieldValues,
} = await sails.helpers.cards.duplicateOne.with({ } = await sails.helpers.cards.duplicateOne
project, .with({
board, project,
list, board,
record: card, list,
values: { record: card,
...values, values: {
creatorUser: currentUser, ...values,
}, project: nextProject,
request: this.req, board: nextBoard,
}); list: nextList,
creatorUser: currentUser,
},
request: this.req,
})
.intercept('positionMustBeInValues', () => Errors.POSITION_MUST_BE_PRESENT)
.intercept('listMustBeInValues', () => Errors.LIST_MUST_BE_PRESENT);
return { return {
item: nextCard, item: nextCard,

View File

@@ -0,0 +1,185 @@
/*!
* Copyright (c) 2024 PLANKA Software GmbH
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
const { POSITION_GAP } = require('../../../constants');
module.exports = {
inputs: {
fromRecord: {
type: 'ref',
required: true,
},
toRecord: {
type: 'ref',
required: true,
},
detachBoardCustomFieldGroups: {
type: 'boolean',
defaultsTo: true,
},
detachBaseCustomFieldGroups: {
type: 'boolean',
defaultsTo: true,
},
},
async fn(inputs) {
const boardCustomFieldGroups = inputs.detachBoardCustomFieldGroups
? await CustomFieldGroup.qm.getByBoardId(inputs.fromRecord.boardId)
: [];
const cardCustomFieldGroups = await CustomFieldGroup.qm.getByCardId(inputs.fromRecord.id);
const customFieldGroups = [...boardCustomFieldGroups, ...cardCustomFieldGroups];
const customFieldGroupIds = sails.helpers.utils.mapRecords(customFieldGroups);
const customFields = await CustomField.qm.getByCustomFieldGroupIds(customFieldGroupIds);
let customFieldGroupsByBaseCustomFieldGroupId;
let baseCustomFieldGroupById;
let customFieldsByBaseCustomFieldGroupId;
let nextCustomFieldsTotal = customFields.length;
if (inputs.detachBaseCustomFieldGroups) {
customFieldGroupsByBaseCustomFieldGroupId = _.groupBy(
customFieldGroups.filter(({ baseCustomFieldGroupId }) => baseCustomFieldGroupId),
'baseCustomFieldGroupId',
);
const baseCustomFieldGroupIds = Object.keys(customFieldGroupsByBaseCustomFieldGroupId);
if (baseCustomFieldGroupIds.length > 0) {
const baseCustomFieldGroups =
await BaseCustomFieldGroup.qm.getByIds(baseCustomFieldGroupIds);
baseCustomFieldGroupById = _.keyBy(baseCustomFieldGroups, 'id');
const baseCustomFields = await CustomField.qm.getByBaseCustomFieldGroupIds(
Object.keys(baseCustomFieldGroupById),
);
customFieldsByBaseCustomFieldGroupId = _.groupBy(
baseCustomFields,
'baseCustomFieldGroupId',
);
nextCustomFieldsTotal += Object.entries(customFieldGroupsByBaseCustomFieldGroupId).reduce(
(result, [baseCustomFieldGroupId, customFieldGroupsItem]) => {
const customFieldsItem = customFieldsByBaseCustomFieldGroupId[baseCustomFieldGroupId];
if (!customFieldsItem) {
return result;
}
return result + customFieldsItem.length * customFieldGroupsItem.length;
},
0,
);
}
}
const customFieldValues = await CustomFieldValue.qm.getByCardId(inputs.fromRecord.id);
const ids = await sails.helpers.utils.generateIds(
customFieldGroups.length + nextCustomFieldsTotal,
);
const nextCustomFieldGroupIdByCustomFieldGroupId = {};
const nextCustomFieldGroupsValues = customFieldGroups.map((customFieldGroup, index) => {
const id = ids.shift();
nextCustomFieldGroupIdByCustomFieldGroupId[customFieldGroup.id] = id;
const values = {
..._.pick(customFieldGroup, ['position']),
id,
cardId: inputs.toRecord.id,
position: customFieldGroup.boardId
? POSITION_GAP * (index + 1)
: customFieldGroup.position + POSITION_GAP * boardCustomFieldGroups.length,
};
if (inputs.detachBaseCustomFieldGroups) {
values.name =
customFieldGroup.name ||
baseCustomFieldGroupById[customFieldGroup.baseCustomFieldGroupId].name;
} else {
Object.assign(values, {
name: customFieldGroup.name,
baseCustomFieldGroupId: customFieldGroup.baseCustomFieldGroupId,
});
}
return values;
});
const nextCustomFieldGroups = await CustomFieldGroup.qm.create(nextCustomFieldGroupsValues);
const nextCustomFieldIdByCustomFieldId = {};
const nextCustomFieldsValues = customFields.map((customField) => {
const id = ids.shift();
nextCustomFieldIdByCustomFieldId[customField.id] = id;
return {
..._.pick(customField, ['position', 'name', 'showOnFrontOfCard']),
id,
customFieldGroupId:
nextCustomFieldGroupIdByCustomFieldGroupId[customField.customFieldGroupId],
};
});
if (inputs.detachBaseCustomFieldGroups) {
Object.entries(customFieldGroupsByBaseCustomFieldGroupId).forEach(
([baseCustomFieldGroupId, customFieldGroupsItem]) => {
const customFieldsItem = customFieldsByBaseCustomFieldGroupId[baseCustomFieldGroupId];
if (!customFieldsItem) {
return;
}
customFieldGroupsItem.forEach((customFieldGroup) => {
customFieldsItem.forEach((customField) => {
const groupedId = `${customFieldGroup.id}:${customField.id}`;
const id = ids.shift();
nextCustomFieldIdByCustomFieldId[groupedId] = id;
nextCustomFieldsValues.push({
..._.pick(customField, ['position', 'name', 'showOnFrontOfCard']),
id,
customFieldGroupId: nextCustomFieldGroupIdByCustomFieldGroupId[customFieldGroup.id],
});
});
});
},
);
}
const nextCustomFields = await CustomField.qm.create(nextCustomFieldsValues);
const nextCustomFieldValuesValues = customFieldValues.map((customFieldValue) => {
const groupedId = `${customFieldValue.customFieldGroupId}:${customFieldValue.customFieldId}`;
return {
..._.pick(customFieldValue, ['content']),
cardId: inputs.toRecord.id,
customFieldGroupId:
nextCustomFieldGroupIdByCustomFieldGroupId[customFieldValue.customFieldGroupId] ||
customFieldValue.customFieldGroupId,
customFieldId:
nextCustomFieldIdByCustomFieldId[groupedId] ||
nextCustomFieldIdByCustomFieldId[customFieldValue.customFieldId] ||
customFieldValue.customFieldId,
};
});
const nextCustomFieldValues = await CustomFieldValue.qm.create(nextCustomFieldValuesValues);
return {
customFieldGroups: nextCustomFieldGroups,
customFields: nextCustomFields,
customFieldValues: nextCustomFieldValues,
};
},
};

View File

@@ -198,11 +198,10 @@ module.exports = {
} }
customFieldsItem.forEach((customField) => { customFieldsItem.forEach((customField) => {
const groupedId = `${customFieldGroup.id}:${customField.id}`;
const id = ids.shift(); const id = ids.shift();
nextCustomFieldIdByCustomFieldIdByCardId[cardId][ nextCustomFieldIdByCustomFieldIdByCardId[cardId][groupedId] = id;
`${customFieldGroup.id}:${customField.id}`
] = id;
nextCustomFieldsValues.push({ nextCustomFieldsValues.push({
..._.pick(customField, ['name', 'showOnFrontOfCard', 'position']), ..._.pick(customField, ['name', 'showOnFrontOfCard', 'position']),
@@ -223,11 +222,10 @@ module.exports = {
} }
customFieldsItem.forEach((customField) => { customFieldsItem.forEach((customField) => {
const groupedId = `${customFieldGroup.id}:${customField.id}`;
const id = ids.shift(); const id = ids.shift();
nextCustomFieldIdByCustomFieldIdByCardId[customFieldGroup.cardId][ nextCustomFieldIdByCustomFieldIdByCardId[customFieldGroup.cardId][groupedId] = id;
`${customFieldGroup.id}:${customField.id}`
] = id;
nextCustomFieldsValues.push({ nextCustomFieldsValues.push({
..._.pick(customField, ['name', 'showOnFrontOfCard', 'position']), ..._.pick(customField, ['name', 'showOnFrontOfCard', 'position']),
@@ -262,10 +260,11 @@ module.exports = {
nextCustomFieldIdByCustomFieldIdByCardId[customFieldValue.cardId]; nextCustomFieldIdByCustomFieldIdByCardId[customFieldValue.cardId];
if (nextCustomFieldIdByCustomFieldId) { if (nextCustomFieldIdByCustomFieldId) {
const groupedId = `${customFieldValue.customFieldGroupId}:${customFieldValue.customFieldId}`;
const nextCustomFieldId = const nextCustomFieldId =
nextCustomFieldIdByCustomFieldId[ nextCustomFieldIdByCustomFieldId[groupedId] ||
`${customFieldValue.customFieldGroupId}:${customFieldValue.customFieldId}` nextCustomFieldIdByCustomFieldId[customFieldValue.customFieldId];
] || nextCustomFieldIdByCustomFieldId[customFieldValue.customFieldId];
if (nextCustomFieldId) { if (nextCustomFieldId) {
updateValues.customFieldId = nextCustomFieldId; updateValues.customFieldId = nextCustomFieldId;

View File

@@ -25,10 +25,6 @@ module.exports = {
type: 'ref', type: 'ref',
required: true, required: true,
}, },
join: {
type: 'boolean',
defaultsTo: false,
},
request: { request: {
type: 'ref', type: 'ref',
}, },
@@ -36,25 +32,58 @@ module.exports = {
exits: { exits: {
positionMustBeInValues: {}, positionMustBeInValues: {},
boardInValuesMustBelongToProject: {},
listMustBeInValues: {},
listInValuesMustBelongToBoard: {},
}, },
async fn(inputs) { async fn(inputs) {
const { values } = inputs; const { values } = inputs;
if (values.list) { if (values.project && values.project.id === inputs.project.id) {
const typeState = List.TYPE_STATE_BY_TYPE[values.list.type]; delete values.project;
}
if (inputs.record.isClosed) { const project = values.project || inputs.project;
if (typeState === List.TypeStates.OPENED) {
values.isClosed = false; if (values.board) {
} if (values.board.projectId !== project.id) {
} else if (typeState === List.TypeStates.CLOSED) { throw 'boardInValuesMustBelongToProject';
values.isClosed = true; }
if (values.board.id === inputs.board.id) {
delete values.board;
} else {
values.boardId = values.board.id;
} }
} }
const board = values.board || inputs.board;
if (values.list) {
if (values.list.boardId !== board.id) {
throw 'listInValuesMustBelongToBoard';
}
if (values.list.id === inputs.list.id) {
delete values.list;
} else {
values.listId = values.list.id;
}
} else if (values.board) {
throw 'listMustBeInValues';
}
const list = values.list || inputs.list; const list = values.list || inputs.list;
if (sails.helpers.lists.isFinite(list)) {
if (values.list && _.isUndefined(values.position)) {
throw 'positionMustBeInValues';
}
} else {
values.position = null;
}
if (sails.helpers.lists.isFinite(list)) { if (sails.helpers.lists.isFinite(list)) {
if (_.isUndefined(values.position)) { if (_.isUndefined(values.position)) {
throw 'positionMustBeInValues'; throw 'positionMustBeInValues';
@@ -95,9 +124,54 @@ module.exports = {
} }
} }
let labelIds;
if (values.board) {
const prevLabels = await sails.helpers.cards.getLabels(inputs.record.id);
const labels = await Label.qm.getByBoardId(values.board.id);
const labelByName = _.keyBy(labels, 'name');
labelIds = await Promise.all(
prevLabels.map(async (label) => {
if (labelByName[label.name]) {
return labelByName[label.name].id;
}
const { id } = await sails.helpers.labels.createOne.with({
project,
values: {
..._.omit(label, ['id', 'boardId', 'createdAt', 'updatedAt']),
board,
},
actorUser: values.creatorUser,
});
return id;
}),
);
}
if (values.list) {
const typeState = List.TYPE_STATE_BY_TYPE[values.list.type];
if (inputs.record.isClosed) {
if (typeState === List.TypeStates.OPENED) {
values.isClosed = false;
}
} else if (typeState === List.TypeStates.CLOSED) {
values.isClosed = true;
}
}
if (!values.name) {
const t = sails.helpers.utils.makeTranslator(values.creatorUser.language);
values.name = `${inputs.record.name} (${t('copy')})`;
}
let card = await Card.qm.createOne({ let card = await Card.qm.createOne({
..._.pick(inputs.record, [ ..._.pick(inputs.record, [
'boardId', 'boardId',
'listId',
'prevListId', 'prevListId',
'type', 'type',
'name', 'name',
@@ -108,12 +182,16 @@ module.exports = {
'isClosed', 'isClosed',
]), ]),
...values, ...values,
listId: list.id,
creatorUserId: values.creatorUser.id, creatorUserId: values.creatorUser.id,
listChangedAt: new Date().toISOString(), listChangedAt: new Date().toISOString(),
}); });
const cardMemberships = await CardMembership.qm.getByCardId(inputs.record.id); const boardMemberUserIds = await sails.helpers.boards.getMemberUserIds(card.boardId);
const boardMemberUserIdsSet = new Set(boardMemberUserIds);
const cardMemberships = await CardMembership.qm.getByCardId(inputs.record.id, {
userIdOrIds: boardMemberUserIds,
});
const cardMembershipsValues = cardMemberships.map((cardMembership) => ({ const cardMembershipsValues = cardMemberships.map((cardMembership) => ({
..._.pick(cardMembership, ['userId']), ..._.pick(cardMembership, ['userId']),
@@ -122,10 +200,13 @@ module.exports = {
const nextCardMemberships = await CardMembership.qm.create(cardMembershipsValues); const nextCardMemberships = await CardMembership.qm.create(cardMembershipsValues);
const cardLabels = await CardLabel.qm.getByCardId(inputs.record.id); if (!values.board) {
const cardLabels = await CardLabel.qm.getByCardId(inputs.record.id);
labelIds = sails.helpers.utils.mapRecords(cardLabels, 'labelId');
}
const cardLabelsValues = cardLabels.map((cardLabel) => ({ const cardLabelsValues = labelIds.map((labelId) => ({
..._.pick(cardLabel, ['labelId']), labelId,
cardId: card.id, cardId: card.id,
})); }));
@@ -137,15 +218,7 @@ module.exports = {
const tasks = await Task.qm.getByTaskListIds(taskListIds); const tasks = await Task.qm.getByTaskListIds(taskListIds);
const attachments = await Attachment.qm.getByCardId(inputs.record.id); const attachments = await Attachment.qm.getByCardId(inputs.record.id);
const customFieldGroups = await CustomFieldGroup.qm.getByCardId(inputs.record.id); const ids = await sails.helpers.utils.generateIds(taskLists.length + attachments.length);
const customFieldGroupIds = sails.helpers.utils.mapRecords(customFieldGroups);
const customFields = await CustomField.qm.getByCustomFieldGroupIds(customFieldGroupIds);
const customFieldValues = await CustomFieldValue.qm.getByCardId(inputs.record.id);
const ids = await sails.helpers.utils.generateIds(
taskLists.length + attachments.length + customFieldGroups.length + customFields.length,
);
const nextTaskListIdByTaskListId = {}; const nextTaskListIdByTaskListId = {};
const nextTaskListsValues = await taskLists.map((taskList) => { const nextTaskListsValues = await taskLists.map((taskList) => {
@@ -162,8 +235,9 @@ module.exports = {
const nextTaskLists = await TaskList.qm.create(nextTaskListsValues); const nextTaskLists = await TaskList.qm.create(nextTaskListsValues);
const nextTasksValues = tasks.map((task) => ({ const nextTasksValues = tasks.map((task) => ({
..._.pick(task, ['linkedCardId', 'assigneeUserId', 'position', 'name', 'isCompleted']), ..._.pick(task, ['linkedCardId', 'position', 'name', 'isCompleted']),
taskListId: nextTaskListIdByTaskListId[task.taskListId], taskListId: nextTaskListIdByTaskListId[task.taskListId],
assigneeUserId: boardMemberUserIdsSet.has(task.assigneeUserId) ? task.assigneeUserId : null,
})); }));
const nextTasks = await Task.qm.create(nextTasksValues); const nextTasks = await Task.qm.create(nextTasksValues);
@@ -193,47 +267,16 @@ module.exports = {
} }
} }
const nextCustomFieldGroupIdByCustomFieldGroupId = {}; const {
const nextCustomFieldGroupsValues = customFieldGroups.map((customFieldGroup) => { customFieldGroups: nextCustomFieldGroups,
const id = ids.shift(); customFields: nextCustomFields,
nextCustomFieldGroupIdByCustomFieldGroupId[customFieldGroup.id] = id; customFieldValues: nextCustomFieldValues,
} = await sails.helpers.cards.copyCustomFields(
return { inputs.record,
..._.pick(customFieldGroup, ['baseCustomFieldGroupId', 'position', 'name']), card,
id, !!values.board,
cardId: card.id, !!values.project,
}; );
});
const nextCustomFieldGroups = await CustomFieldGroup.qm.create(nextCustomFieldGroupsValues);
const nextCustomFieldIdByCustomFieldId = {};
const nextCustomFieldsValues = customFields.map((customField) => {
const id = ids.shift();
nextCustomFieldIdByCustomFieldId[customField.id] = id;
return {
..._.pick(customField, ['position', 'name', 'showOnFrontOfCard']),
id,
customFieldGroupId:
nextCustomFieldGroupIdByCustomFieldGroupId[customField.customFieldGroupId],
};
});
const nextCustomFields = await CustomField.qm.create(nextCustomFieldsValues);
const nextCustomFieldValuesValues = customFieldValues.map((customFieldValue) => ({
..._.pick(customFieldValue, ['content']),
cardId: card.id,
customFieldGroupId:
nextCustomFieldGroupIdByCustomFieldGroupId[customFieldValue.customFieldGroupId] ||
customFieldValue.customFieldGroupId,
customFieldId:
nextCustomFieldIdByCustomFieldId[customFieldValue.customFieldId] ||
customFieldValue.customFieldId,
}));
const nextCustomFieldValues = await CustomFieldValue.qm.create(nextCustomFieldValuesValues);
sails.sockets.broadcast( sails.sockets.broadcast(
`board:${card.boardId}`, `board:${card.boardId}`,
@@ -252,8 +295,8 @@ module.exports = {
buildData: () => ({ buildData: () => ({
item: card, item: card,
included: { included: {
projects: [inputs.project], projects: [project],
boards: [inputs.board], boards: [board],
lists: [list], lists: [list],
cardMemberships: nextCardMemberships, cardMemberships: nextCardMemberships,
cardLabels: nextCardLabels, cardLabels: nextCardLabels,
@@ -291,6 +334,8 @@ module.exports = {
} }
await sails.helpers.actions.createOne.with({ await sails.helpers.actions.createOne.with({
project,
board,
list, list,
webhooks, webhooks,
values: { values: {
@@ -302,8 +347,6 @@ module.exports = {
}, },
user: values.creatorUser, user: values.creatorUser,
}, },
project: inputs.project,
board: inputs.board,
}); });
return { return {

View File

@@ -98,7 +98,11 @@ module.exports = {
throw 'coverAttachmentInValuesMustContainImage'; throw 'coverAttachmentInValuesMustContainImage';
} }
values.coverAttachmentId = values.coverAttachment.id; if (values.coverAttachment.id === inputs.record.coverAttachmentId) {
delete values.coverAttachment;
} else {
values.coverAttachmentId = values.coverAttachment.id;
}
} }
const dueDate = _.isUndefined(values.dueDate) ? inputs.record.dueDate : values.dueDate; const dueDate = _.isUndefined(values.dueDate) ? inputs.record.dueDate : values.dueDate;
@@ -289,9 +293,14 @@ module.exports = {
inputs.request, inputs.request,
); );
sails.sockets.broadcast(`board:${card.boardId}`, 'cardUpdate', { sails.sockets.broadcast(
item: card, `board:${card.boardId}`,
}); 'cardUpdate',
{
item: card,
},
inputs.request,
);
// TODO: add transfer action // TODO: add transfer action
} else { } else {

View File

@@ -13,10 +13,16 @@ const createOne = (values) => CardMembership.create({ ...values }).fetch();
const getByIds = (ids) => defaultFind(ids); const getByIds = (ids) => defaultFind(ids);
const getByCardId = (cardId) => const getByCardId = (cardId, { userIdOrIds } = {}) => {
defaultFind({ const criteria = {
cardId, cardId,
}); };
if (userIdOrIds) {
criteria.userId = userIdOrIds;
}
return defaultFind(criteria);
};
const getByCardIds = (cardIds) => const getByCardIds = (cardIds) =>
defaultFind({ defaultFind({

View File

@@ -2,6 +2,7 @@
"Archive": "أرشيف", "Archive": "أرشيف",
"Card Created": "تم إنشاء البطاقة", "Card Created": "تم إنشاء البطاقة",
"Card Moved": "تم نقل البطاقة", "Card Moved": "تم نقل البطاقة",
"copy": "نسخة",
"New Comment": "تعليق جديد", "New Comment": "تعليق جديد",
"Test Title": "عنوان تجريبي", "Test Title": "عنوان تجريبي",
"This is a test text message!": "هذه رسالة نصية تجريبية!", "This is a test text message!": "هذه رسالة نصية تجريبية!",

View File

@@ -2,6 +2,7 @@
"Archive": "Архив", "Archive": "Архив",
"Card Created": "Картата е създадена", "Card Created": "Картата е създадена",
"Card Moved": "Картата е преместена", "Card Moved": "Картата е преместена",
"copy": "копие",
"New Comment": "Нов коментар", "New Comment": "Нов коментар",
"Test Title": "Тестово заглавие", "Test Title": "Тестово заглавие",
"This is a test text message!": "Това е тестово текстово съобщение!", "This is a test text message!": "Това е тестово текстово съобщение!",

View File

@@ -2,6 +2,7 @@
"Archive": "Arxivar", "Archive": "Arxivar",
"Card Created": "Targeta creada", "Card Created": "Targeta creada",
"Card Moved": "Targeta moguda", "Card Moved": "Targeta moguda",
"copy": "còpia",
"New Comment": "Comentari nou", "New Comment": "Comentari nou",
"Test Title": "Títol de prova", "Test Title": "Títol de prova",
"This is a test text message!": "Aquest és un missatge de text de prova!", "This is a test text message!": "Aquest és un missatge de text de prova!",

View File

@@ -2,6 +2,7 @@
"Archive": "Archiv", "Archive": "Archiv",
"Card Created": "Karta vytvořena", "Card Created": "Karta vytvořena",
"Card Moved": "Karta přesunuta", "Card Moved": "Karta přesunuta",
"copy": "kopie",
"New Comment": "Nový komentář", "New Comment": "Nový komentář",
"Test Title": "Testovací název", "Test Title": "Testovací název",
"This is a test text message!": "Toto je testovací textová zpráva!", "This is a test text message!": "Toto je testovací textová zpráva!",

View File

@@ -2,6 +2,7 @@
"Archive": "Arkiv", "Archive": "Arkiv",
"Card Created": "Kort oprettet", "Card Created": "Kort oprettet",
"Card Moved": "Kort flyttet", "Card Moved": "Kort flyttet",
"copy": "kopi",
"New Comment": "Ny kommentar", "New Comment": "Ny kommentar",
"Test Title": "Test titel", "Test Title": "Test titel",
"This is a test text message!": "Dette er en test tekstbesked!", "This is a test text message!": "Dette er en test tekstbesked!",

View File

@@ -2,6 +2,7 @@
"Archive": "Archiv", "Archive": "Archiv",
"Card Created": "Karte erstellt", "Card Created": "Karte erstellt",
"Card Moved": "Karte verschoben", "Card Moved": "Karte verschoben",
"copy": "Kopie",
"New Comment": "Neuer Kommentar", "New Comment": "Neuer Kommentar",
"Test Title": "Testtitel", "Test Title": "Testtitel",
"This is a test text message!": "Dies ist eine Test-Textnachricht!", "This is a test text message!": "Dies ist eine Test-Textnachricht!",

View File

@@ -2,6 +2,7 @@
"Archive": "Αρχείο", "Archive": "Αρχείο",
"Card Created": "Η κάρτα δημιουργήθηκε", "Card Created": "Η κάρτα δημιουργήθηκε",
"Card Moved": "Η κάρτα μετακινήθηκε", "Card Moved": "Η κάρτα μετακινήθηκε",
"copy": "αντίγραφο",
"New Comment": "Νέο σχόλιο", "New Comment": "Νέο σχόλιο",
"Test Title": "Τίτλος δοκιμής", "Test Title": "Τίτλος δοκιμής",
"This is a test text message!": "Αυτό είναι ένα δοκιμαστικό μήνυμα!", "This is a test text message!": "Αυτό είναι ένα δοκιμαστικό μήνυμα!",

View File

@@ -2,6 +2,7 @@
"Archive": "Archive", "Archive": "Archive",
"Card Created": "Card Created", "Card Created": "Card Created",
"Card Moved": "Card Moved", "Card Moved": "Card Moved",
"copy": "copy",
"New Comment": "New Comment", "New Comment": "New Comment",
"Test Title": "Test Title", "Test Title": "Test Title",
"This is a test text message!": "This is a test text message!", "This is a test text message!": "This is a test text message!",

View File

@@ -2,6 +2,7 @@
"Archive": "Archive", "Archive": "Archive",
"Card Created": "Card Created", "Card Created": "Card Created",
"Card Moved": "Card Moved", "Card Moved": "Card Moved",
"copy": "copy",
"New Comment": "New Comment", "New Comment": "New Comment",
"Test Title": "Test Title", "Test Title": "Test Title",
"This is a test text message!": "This is a test text message!", "This is a test text message!": "This is a test text message!",

View File

@@ -2,6 +2,7 @@
"Archive": "Archivo", "Archive": "Archivo",
"Card Created": "Tarjeta creada", "Card Created": "Tarjeta creada",
"Card Moved": "Tarjeta movida", "Card Moved": "Tarjeta movida",
"copy": "copia",
"New Comment": "Nuevo comentario", "New Comment": "Nuevo comentario",
"Test Title": "Título de prueba", "Test Title": "Título de prueba",
"This is a test text message!": "¡Este es un mensaje de texto de prueba!", "This is a test text message!": "¡Este es un mensaje de texto de prueba!",

View File

@@ -2,6 +2,7 @@
"Archive": "Arhiiv", "Archive": "Arhiiv",
"Card Created": "Kaart loodud", "Card Created": "Kaart loodud",
"Card Moved": "Kaart liigutatud", "Card Moved": "Kaart liigutatud",
"copy": "koopia",
"New Comment": "Uus kommentaar", "New Comment": "Uus kommentaar",
"Test Title": "Testi pealkiri", "Test Title": "Testi pealkiri",
"This is a test text message!": "See on testi tekstisõnum!", "This is a test text message!": "See on testi tekstisõnum!",

View File

@@ -2,6 +2,7 @@
"Archive": "بایگانی", "Archive": "بایگانی",
"Card Created": "کارت ایجاد شد", "Card Created": "کارت ایجاد شد",
"Card Moved": "کارت منتقل شد", "Card Moved": "کارت منتقل شد",
"copy": "کپی",
"New Comment": "نظر جدید", "New Comment": "نظر جدید",
"Test Title": "عنوان آزمایشی", "Test Title": "عنوان آزمایشی",
"This is a test text message!": "این یک پیام متنی آزمایشی است!", "This is a test text message!": "این یک پیام متنی آزمایشی است!",

View File

@@ -2,6 +2,7 @@
"Archive": "Arkisto", "Archive": "Arkisto",
"Card Created": "Kortti luotu", "Card Created": "Kortti luotu",
"Card Moved": "Kortti siirretty", "Card Moved": "Kortti siirretty",
"copy": "kopio",
"New Comment": "Uusi kommentti", "New Comment": "Uusi kommentti",
"Test Title": "Testin otsikko", "Test Title": "Testin otsikko",
"This is a test text message!": "Tämä on testiviesti!", "This is a test text message!": "Tämä on testiviesti!",

View File

@@ -2,6 +2,7 @@
"Archive": "Archive", "Archive": "Archive",
"Card Created": "Carte créée", "Card Created": "Carte créée",
"Card Moved": "Carte déplacée", "Card Moved": "Carte déplacée",
"copy": "copie",
"New Comment": "Nouveau commentaire", "New Comment": "Nouveau commentaire",
"Test Title": "Titre de test", "Test Title": "Titre de test",
"This is a test text message!": "Ceci est un message texte de test !", "This is a test text message!": "Ceci est un message texte de test !",

View File

@@ -2,6 +2,7 @@
"Archive": "Archívum", "Archive": "Archívum",
"Card Created": "Kártya létrehozva", "Card Created": "Kártya létrehozva",
"Card Moved": "Kártya áthelyezve", "Card Moved": "Kártya áthelyezve",
"copy": "másolat",
"New Comment": "Új hozzászólás", "New Comment": "Új hozzászólás",
"Test Title": "Teszt cím", "Test Title": "Teszt cím",
"This is a test text message!": "Ez itt egy szöveges teszt üzenet!", "This is a test text message!": "Ez itt egy szöveges teszt üzenet!",

View File

@@ -2,6 +2,7 @@
"Archive": "Arsip", "Archive": "Arsip",
"Card Created": "Kartu dibuat", "Card Created": "Kartu dibuat",
"Card Moved": "Kartu dipindahkan", "Card Moved": "Kartu dipindahkan",
"copy": "salinan",
"New Comment": "Komentar baru", "New Comment": "Komentar baru",
"Test Title": "Judul tes", "Test Title": "Judul tes",
"This is a test text message!": "Ini adalah pesan teks tes!", "This is a test text message!": "Ini adalah pesan teks tes!",

View File

@@ -2,6 +2,7 @@
"Archive": "Archivio", "Archive": "Archivio",
"Card Created": "Nuova task creata", "Card Created": "Nuova task creata",
"Card Moved": "Task spostata", "Card Moved": "Task spostata",
"copy": "copia",
"New Comment": "Nuovo commento", "New Comment": "Nuovo commento",
"Test Title": "Titolo di test", "Test Title": "Titolo di test",
"This is a test text message!": "Questo è un messaggio di testo di test!", "This is a test text message!": "Questo è un messaggio di testo di test!",

View File

@@ -2,6 +2,7 @@
"Archive": "アーカイブ", "Archive": "アーカイブ",
"Card Created": "カードが作成されました", "Card Created": "カードが作成されました",
"Card Moved": "カードが移動されました", "Card Moved": "カードが移動されました",
"copy": "コピー",
"New Comment": "新しいコメント", "New Comment": "新しいコメント",
"Test Title": "テストタイトル", "Test Title": "テストタイトル",
"This is a test text message!": "これはテストテキストメッセージです!", "This is a test text message!": "これはテストテキストメッセージです!",

View File

@@ -2,6 +2,7 @@
"Archive": "보관함", "Archive": "보관함",
"Card Created": "카드가 생성됨", "Card Created": "카드가 생성됨",
"Card Moved": "카드가 이동됨", "Card Moved": "카드가 이동됨",
"copy": "복사본",
"New Comment": "새 댓글", "New Comment": "새 댓글",
"Test Title": "테스트 제목", "Test Title": "테스트 제목",
"This is a test text message!": "이것은 테스트 텍스트 메시지입니다!", "This is a test text message!": "이것은 테스트 텍스트 메시지입니다!",

View File

@@ -2,6 +2,7 @@
"Archive": "Archief", "Archive": "Archief",
"Card Created": "Kaart aangemaakt", "Card Created": "Kaart aangemaakt",
"Card Moved": "Kaart verplaatst", "Card Moved": "Kaart verplaatst",
"copy": "kopie",
"New Comment": "Nieuwe reactie", "New Comment": "Nieuwe reactie",
"Test Title": "Test titel", "Test Title": "Test titel",
"This is a test text message!": "Dit is een test tekstbericht!", "This is a test text message!": "Dit is een test tekstbericht!",

View File

@@ -2,6 +2,7 @@
"Archive": "Archiwum", "Archive": "Archiwum",
"Card Created": "Karta utworzona", "Card Created": "Karta utworzona",
"Card Moved": "Karta przeniesiona", "Card Moved": "Karta przeniesiona",
"copy": "kopia",
"New Comment": "Nowy komentarz", "New Comment": "Nowy komentarz",
"Test Title": "Tytuł testowy", "Test Title": "Tytuł testowy",
"This is a test text message!": "To jest testowa wiadomość tekstowa!", "This is a test text message!": "To jest testowa wiadomość tekstowa!",

Some files were not shown because too many files have changed in this diff Show More