1
0
Fork 0
mirror of https://github.com/plankanban/planka.git synced 2025-07-18 20:59:44 +02:00

feat: Add board activity log

This commit is contained in:
Maksim Eltyshev 2025-05-22 23:14:46 +02:00
parent 777ff467f3
commit 86cfd155f2
72 changed files with 833 additions and 169 deletions

View file

@ -5,15 +5,39 @@
import ActionTypes from '../constants/ActionTypes'; import ActionTypes from '../constants/ActionTypes';
const fetchActivities = (cardId) => ({ const fetchActivitiesInBoard = (boardId) => ({
type: ActionTypes.ACTIVITIES_FETCH, type: ActionTypes.ACTIVITIES_IN_BOARD_FETCH,
payload: {
boardId,
},
});
fetchActivitiesInBoard.success = (boardId, activities, users) => ({
type: ActionTypes.ACTIVITIES_IN_BOARD_FETCH__SUCCESS,
payload: {
boardId,
activities,
users,
},
});
fetchActivitiesInBoard.failure = (boardId, error) => ({
type: ActionTypes.ACTIVITIES_IN_BOARD_FETCH__FAILURE,
payload: {
boardId,
error,
},
});
const fetchActivitiesInCard = (cardId) => ({
type: ActionTypes.ACTIVITIES_IN_CARD_FETCH,
payload: { payload: {
cardId, cardId,
}, },
}); });
fetchActivities.success = (cardId, activities, users) => ({ fetchActivitiesInCard.success = (cardId, activities, users) => ({
type: ActionTypes.ACTIVITIES_FETCH__SUCCESS, type: ActionTypes.ACTIVITIES_IN_CARD_FETCH__SUCCESS,
payload: { payload: {
cardId, cardId,
activities, activities,
@ -21,8 +45,8 @@ fetchActivities.success = (cardId, activities, users) => ({
}, },
}); });
fetchActivities.failure = (cardId, error) => ({ fetchActivitiesInCard.failure = (cardId, error) => ({
type: ActionTypes.ACTIVITIES_FETCH__FAILURE, type: ActionTypes.ACTIVITIES_IN_CARD_FETCH__FAILURE,
payload: { payload: {
cardId, cardId,
error, error,
@ -37,6 +61,7 @@ const handleActivityCreate = (activity) => ({
}); });
export default { export default {
fetchActivities, fetchActivitiesInBoard,
fetchActivitiesInCard,
handleActivityCreate, handleActivityCreate,
}; };

View file

@ -16,7 +16,13 @@ export const transformActivity = (activity) => ({
/* Actions */ /* Actions */
const getActivities = (cardId, data, headers) => const getActivitiesInBoard = (boardId, data, headers) =>
socket.get(`/boards/${boardId}/actions`, data, headers).then((body) => ({
...body,
items: body.items.map(transformActivity),
}));
const getActivitiesInCard = (cardId, data, headers) =>
socket.get(`/cards/${cardId}/actions`, data, headers).then((body) => ({ socket.get(`/cards/${cardId}/actions`, data, headers).then((body) => ({
...body, ...body,
items: body.items.map(transformActivity), items: body.items.map(transformActivity),
@ -32,6 +38,7 @@ const makeHandleActivityCreate = (next) => (body) => {
}; };
export default { export default {
getActivities, getActivitiesInBoard,
getActivitiesInCard,
makeHandleActivityCreate, makeHandleActivityCreate,
}; };

View file

@ -0,0 +1,73 @@
/*!
* Copyright (c) 2024 PLANKA Software GmbH
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
import React, { useCallback } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useTranslation } from 'react-i18next';
import { useInView } from 'react-intersection-observer';
import { Comment, Loader } from 'semantic-ui-react';
import selectors from '../../../selectors';
import entryActions from '../../../entry-actions';
import { useClosableModal } from '../../../hooks';
import Item from './Item';
import styles from './BoardActivitiesModal.module.scss';
const BoardActivitiesModal = React.memo(() => {
const activityIds = useSelector(selectors.selectActivityIdsForCurrentBoard);
const { isActivitiesFetching, isAllActivitiesFetched } = useSelector(
selectors.selectCurrentBoard,
);
const dispatch = useDispatch();
const [t] = useTranslation();
const handleClose = useCallback(() => {
dispatch(entryActions.closeModal());
}, [dispatch]);
const [inViewRef] = useInView({
threshold: 1,
onChange: (inView) => {
if (inView) {
dispatch(entryActions.fetchActivitiesInCurrentBoard());
}
},
});
const [ClosableModal] = useClosableModal();
return (
<ClosableModal closeIcon size="small" centered={false} onClose={handleClose}>
<ClosableModal.Header>
{t('common.boardActions', {
context: 'title',
})}
</ClosableModal.Header>
<ClosableModal.Content>
<div className={styles.itemsWrapper}>
<Comment.Group className={styles.items}>
{activityIds.map((activityId) => (
<Item key={activityId} id={activityId} />
))}
</Comment.Group>
</div>
{isActivitiesFetching !== undefined && isAllActivitiesFetched !== undefined && (
<div className={styles.loaderWrapper}>
{isActivitiesFetching ? (
<Loader active inverted inline="centered" size="small" />
) : (
!isAllActivitiesFetched && <div ref={inViewRef} />
)}
</div>
)}
</ClosableModal.Content>
</ClosableModal>
);
});
export default BoardActivitiesModal;

View file

@ -0,0 +1,18 @@
/*!
* Copyright (c) 2024 PLANKA Software GmbH
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
:global(#app) {
.items {
max-width: none;
}
.itemsWrapper {
margin-top: 12px;
}
.loaderWrapper {
margin-top: 10px;
}
}

View file

@ -0,0 +1,220 @@
/*!
* Copyright (c) 2024 PLANKA Software GmbH
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
import React, { useMemo } from 'react';
import PropTypes from 'prop-types';
import { useSelector } from 'react-redux';
import { useTranslation, Trans } from 'react-i18next';
import { Link } from 'react-router-dom';
import { Comment } from 'semantic-ui-react';
import selectors from '../../../selectors';
import Paths from '../../../constants/Paths';
import { StaticUserIds } from '../../../constants/StaticUsers';
import { ActivityTypes } from '../../../constants/Enums';
import TimeAgo from '../../common/TimeAgo';
import UserAvatar from '../../users/UserAvatar';
import styles from './Item.module.scss';
const Item = React.memo(({ id }) => {
const selectActivityById = useMemo(() => selectors.makeSelectActivityById(), []);
const selectUserById = useMemo(() => selectors.makeSelectUserById(), []);
const selectCardById = useMemo(() => selectors.makeSelectCardById(), []);
const activity = useSelector((state) => selectActivityById(state, id));
const user = useSelector((state) => selectUserById(state, activity.userId));
const card = useSelector((state) => selectCardById(state, activity.cardId));
const [t] = useTranslation();
const userName =
user.id === StaticUserIds.DELETED
? t(`common.${user.name}`, {
context: 'title',
})
: user.name;
const cardName = card ? card.name : activity.data.card.name;
let contentNode;
switch (activity.type) {
case ActivityTypes.CREATE_CARD: {
const { list } = activity.data;
const listName = list.name || t(`common.${list.type}`);
contentNode = (
<Trans
i18nKey="common.userAddedCardToList"
values={{
user: userName,
card: cardName,
list: listName,
}}
>
<span className={styles.author}>{userName}</span>
{' added '}
<Link to={Paths.CARDS.replace(':id', activity.cardId)}>{cardName}</Link>
{' to '}
{listName}
</Trans>
);
break;
}
case ActivityTypes.MOVE_CARD: {
const { fromList, toList } = activity.data;
const fromListName = fromList.name || t(`common.${fromList.type}`);
const toListName = toList.name || t(`common.${toList.type}`);
contentNode = (
<Trans
i18nKey="common.userMovedCardFromListToList"
values={{
user: userName,
card: cardName,
fromList: fromListName,
toList: toListName,
}}
>
<span className={styles.author}>{userName}</span>
{' moved '}
<Link to={Paths.CARDS.replace(':id', activity.cardId)}>{cardName}</Link>
{' from '}
{fromListName}
{' to '}
{toListName}
</Trans>
);
break;
}
case ActivityTypes.ADD_MEMBER_TO_CARD:
contentNode =
user.id === activity.data.user.id ? (
<Trans
i18nKey="common.userJoinedCard"
values={{
user: userName,
card: cardName,
}}
>
<span className={styles.author}>{userName}</span>
{' joined '}
<Link to={Paths.CARDS.replace(':id', activity.cardId)}>{cardName}</Link>
</Trans>
) : (
<Trans
i18nKey="common.userAddedUserToCard"
values={{
actorUser: userName,
addedUser: activity.data.user.name,
card: cardName,
}}
>
<span className={styles.author}>{userName}</span>
{' added '}
{activity.data.user.name}
{' to '}
<Link to={Paths.CARDS.replace(':id', activity.cardId)}>{cardName}</Link>
</Trans>
);
break;
case ActivityTypes.REMOVE_MEMBER_FROM_CARD:
contentNode =
user.id === activity.data.user.id ? (
<Trans
i18nKey="common.userLeftCard"
values={{
user: userName,
card: cardName,
}}
>
<span className={styles.author}>{userName}</span>
{' left '}
<Link to={Paths.CARDS.replace(':id', activity.cardId)}>{cardName}</Link>
</Trans>
) : (
<Trans
i18nKey="common.userRemovedUserFromCard"
values={{
actorUser: userName,
removedUser: activity.data.user.name,
card: cardName,
}}
>
<span className={styles.author}>{userName}</span>
{' removed '}
{activity.data.user.name}
{' from '}
<Link to={Paths.CARDS.replace(':id', activity.cardId)}>{cardName}</Link>
</Trans>
);
break;
case ActivityTypes.COMPLETE_TASK:
contentNode = (
<Trans
i18nKey="common.userCompletedTaskOnCard"
values={{
user: userName,
task: activity.data.task.name,
card: cardName,
}}
>
<span className={styles.author}>{userName}</span>
{' completed '}
{activity.data.task.name}
{' on '}
<Link to={Paths.CARDS.replace(':id', activity.cardId)}>{cardName}</Link>
</Trans>
);
break;
case ActivityTypes.UNCOMPLETE_TASK:
contentNode = (
<Trans
i18nKey="common.userMarkedTaskIncompleteOnCard"
values={{
user: userName,
task: activity.data.task.name,
card: cardName,
}}
>
<span className={styles.author}>{userName}</span>
{' marked '}
{activity.data.task.name}
{' incomplete on '}
<Link to={Paths.CARDS.replace(':id', activity.cardId)}>{cardName}</Link>
</Trans>
);
break;
default:
contentNode = null;
}
return (
<Comment>
<span className={styles.user}>
<UserAvatar id={activity.userId} />
</span>
<div className={styles.content}>
<div>{contentNode}</div>
<span className={styles.date}>
<TimeAgo date={activity.createdAt} />
</span>
</div>
</Comment>
);
});
Item.propTypes = {
id: PropTypes.string.isRequired,
};
export default Item;

View file

@ -7,12 +7,12 @@
.author { .author {
color: #17394d; color: #17394d;
font-weight: bold; font-weight: bold;
line-height: 20px;
} }
.content { .content {
border-bottom: 1px solid #092d4221; border-bottom: 1px solid #092d4221;
display: inline-block; display: inline-block;
line-height: 20px;
padding-bottom: 14px; padding-bottom: 14px;
vertical-align: top; vertical-align: top;
width: calc(100% - 40px); width: calc(100% - 40px);
@ -21,11 +21,6 @@
.date { .date {
color: #6b808c; color: #6b808c;
font-size: 12px; font-size: 12px;
line-height: 20px;
}
.text {
line-height: 20px;
} }
.user { .user {

View file

@ -0,0 +1,8 @@
/*!
* Copyright (c) 2024 PLANKA Software GmbH
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
import BoardActivitiesModal from './BoardActivitiesModal';
export default BoardActivitiesModal;

View file

@ -12,9 +12,9 @@ import selectors from '../../../selectors';
import entryActions from '../../../entry-actions'; import entryActions from '../../../entry-actions';
import Item from './Item'; import Item from './Item';
import styles from './Activities.module.scss'; import styles from './CardActivities.module.scss';
const Activities = React.memo(() => { const CardActivities = React.memo(() => {
const activityIds = useSelector(selectors.selectActivityIdsForCurrentCard); const activityIds = useSelector(selectors.selectActivityIdsForCurrentCard);
const { isActivitiesFetching, isAllActivitiesFetched } = useSelector(selectors.selectCurrentCard); const { isActivitiesFetching, isAllActivitiesFetched } = useSelector(selectors.selectCurrentCard);
@ -51,4 +51,4 @@ const Activities = React.memo(() => {
); );
}); });
export default Activities; export default CardActivities;

View file

@ -48,10 +48,8 @@ const Item = React.memo(({ id }) => {
}} }}
> >
<span className={styles.author}>{userName}</span> <span className={styles.author}>{userName}</span>
<span className={styles.text}> {' added this card to '}
{' added this card to '} {listName}
{listName}
</span>
</Trans> </Trans>
); );
@ -73,12 +71,10 @@ const Item = React.memo(({ id }) => {
}} }}
> >
<span className={styles.author}>{userName}</span> <span className={styles.author}>{userName}</span>
<span className={styles.text}> {' moved this card from '}
{' moved this card from '} {fromListName}
{fromListName} {' to '}
{' to '} {toListName}
{toListName}
</span>
</Trans> </Trans>
); );
@ -94,7 +90,7 @@ const Item = React.memo(({ id }) => {
}} }}
> >
<span className={styles.author}>{userName}</span> <span className={styles.author}>{userName}</span>
<span className={styles.text}>{' joined this card'}</span> {' joined this card'}
</Trans> </Trans>
) : ( ) : (
<Trans <Trans
@ -105,11 +101,9 @@ const Item = React.memo(({ id }) => {
}} }}
> >
<span className={styles.author}>{userName}</span> <span className={styles.author}>{userName}</span>
<span className={styles.text}> {' added '}
{' added '} {activity.data.user.name}
{activity.data.user.name} {' to this card'}
{' to this card'}
</span>
</Trans> </Trans>
); );
@ -124,7 +118,7 @@ const Item = React.memo(({ id }) => {
}} }}
> >
<span className={styles.author}>{userName}</span> <span className={styles.author}>{userName}</span>
<span className={styles.text}>{' left this card'}</span> {' left this card'}
</Trans> </Trans>
) : ( ) : (
<Trans <Trans
@ -135,11 +129,9 @@ const Item = React.memo(({ id }) => {
}} }}
> >
<span className={styles.author}>{userName}</span> <span className={styles.author}>{userName}</span>
<span className={styles.text}> {' removed '}
{' removed '} {activity.data.user.name}
{activity.data.user.name} {' from this card'}
{' from this card'}
</span>
</Trans> </Trans>
); );
@ -154,11 +146,9 @@ const Item = React.memo(({ id }) => {
}} }}
> >
<span className={styles.author}>{userName}</span> <span className={styles.author}>{userName}</span>
<span className={styles.text}> {' completed '}
{' completed '} {activity.data.task.name}
{activity.data.task.name} {' on this card'}
{' on this card'}
</span>
</Trans> </Trans>
); );
@ -173,11 +163,9 @@ const Item = React.memo(({ id }) => {
}} }}
> >
<span className={styles.author}>{userName}</span> <span className={styles.author}>{userName}</span>
<span className={styles.text}> {' marked '}
{' marked '} {activity.data.task.name}
{activity.data.task.name} {' incomplete on this card'}
{' incomplete on this card'}
</span>
</Trans> </Trans>
); );

View file

@ -0,0 +1,31 @@
/*!
* Copyright (c) 2024 PLANKA Software GmbH
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
:global(#app) {
.author {
color: #17394d;
font-weight: bold;
}
.content {
border-bottom: 1px solid #092d4221;
display: inline-block;
line-height: 20px;
padding-bottom: 14px;
vertical-align: top;
width: calc(100% - 40px);
}
.date {
color: #6b808c;
font-size: 12px;
}
.user {
display: inline-block;
padding: 4px 8px 0 0;
vertical-align: top;
}
}

View file

@ -3,6 +3,6 @@
* 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 Activities from './Activities'; import CardActivities from './CardActivities';
export default Activities; export default CardActivities;

View file

@ -7,14 +7,17 @@ import React from 'react';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import selectors from '../../../selectors'; import selectors from '../../../selectors';
import ModalTypes from '../../../constants/ModalTypes';
import { BoardContexts, BoardViews } from '../../../constants/Enums'; import { BoardContexts, BoardViews } from '../../../constants/Enums';
import KanbanContent from './KanbanContent'; import KanbanContent from './KanbanContent';
import FiniteContent from './FiniteContent'; import FiniteContent from './FiniteContent';
import EndlessContent from './EndlessContent'; import EndlessContent from './EndlessContent';
import CardModal from '../../cards/CardModal'; import CardModal from '../../cards/CardModal';
import BoardActivitiesModal from '../../activities/BoardActivitiesModal';
const Board = React.memo(() => { const Board = React.memo(() => {
const board = useSelector(selectors.selectCurrentBoard); const board = useSelector(selectors.selectCurrentBoard);
const modal = useSelector(selectors.selectCurrentModal);
const isCardModalOpened = useSelector((state) => !!selectors.selectPath(state).cardId); const isCardModalOpened = useSelector((state) => !!selectors.selectPath(state).cardId);
let Content; let Content;
@ -35,10 +38,23 @@ const Board = React.memo(() => {
} }
} }
let modalNode = null;
if (isCardModalOpened) {
modalNode = <CardModal />;
} else if (modal) {
switch (modal.type) {
case ModalTypes.BOARD_ACTIVITIES:
modalNode = <BoardActivitiesModal />;
break;
default:
}
}
return ( return (
<> <>
<Content /> <Content />
{isCardModalOpened && <CardModal />} {modalNode}
</> </>
); );
}); });

View file

@ -21,14 +21,14 @@ import CustomFieldGroupsStep from '../../../custom-field-groups/CustomFieldGroup
import styles from './ActionsStep.module.scss'; import styles from './ActionsStep.module.scss';
const StepTypes = { const StepTypes = {
EMPTY_TRASH: 'EMPTY_TRASH',
CUSTOM_FIELD_GROUPS: 'CUSTOM_FIELD_GROUPS', CUSTOM_FIELD_GROUPS: 'CUSTOM_FIELD_GROUPS',
EMPTY_TRASH: 'EMPTY_TRASH',
}; };
const ActionsStep = React.memo(({ onClose }) => { const ActionsStep = React.memo(({ onClose }) => {
const board = useSelector(selectors.selectCurrentBoard); const board = useSelector(selectors.selectCurrentBoard);
const { withSubscribe, withTrashEmptier, withCustomFieldGroups } = useSelector((state) => { const { withSubscribe, withCustomFieldGroups, withTrashEmptier } = useSelector((state) => {
const isManager = selectors.selectIsCurrentUserManagerForCurrentProject(state); const isManager = selectors.selectIsCurrentUserManagerForCurrentProject(state);
const boardMembership = selectors.selectCurrentUserMembershipForCurrentBoard(state); const boardMembership = selectors.selectCurrentUserMembershipForCurrentBoard(state);
@ -42,8 +42,8 @@ const ActionsStep = React.memo(({ onClose }) => {
return { return {
withSubscribe: isMember, // TODO: rename? withSubscribe: isMember, // TODO: rename?
withTrashEmptier: board.context === BoardContexts.TRASH && (isManager || isEditor),
withCustomFieldGroups: isEditor, withCustomFieldGroups: isEditor,
withTrashEmptier: board.context === BoardContexts.TRASH && (isManager || isEditor),
}; };
}, shallowEqual); }, shallowEqual);
@ -69,21 +69,28 @@ const ActionsStep = React.memo(({ onClose }) => {
[onClose, dispatch], [onClose, dispatch],
); );
const handleActivitiesClick = useCallback(() => {
dispatch(entryActions.openBoardActivitiesModal());
onClose();
}, [onClose, dispatch]);
const handleEmptyTrashConfirm = useCallback(() => { const handleEmptyTrashConfirm = useCallback(() => {
dispatch(entryActions.clearTrashListInCurrentBoard()); dispatch(entryActions.clearTrashListInCurrentBoard());
onClose(); onClose();
}, [onClose, dispatch]); }, [onClose, dispatch]);
const handleEmptyTrashClick = useCallback(() => {
openStep(StepTypes.EMPTY_TRASH);
}, [openStep]);
const handleCustomFieldsClick = useCallback(() => { const handleCustomFieldsClick = useCallback(() => {
openStep(StepTypes.CUSTOM_FIELD_GROUPS); openStep(StepTypes.CUSTOM_FIELD_GROUPS);
}, [openStep]); }, [openStep]);
const handleEmptyTrashClick = useCallback(() => {
openStep(StepTypes.EMPTY_TRASH);
}, [openStep]);
if (step) { if (step) {
switch (step.type) { switch (step.type) {
case StepTypes.CUSTOM_FIELD_GROUPS:
return <CustomFieldGroupsStep onBack={handleBack} onClose={onClose} />;
case StepTypes.EMPTY_TRASH: case StepTypes.EMPTY_TRASH:
return ( return (
<ConfirmationStep <ConfirmationStep
@ -94,8 +101,6 @@ const ActionsStep = React.memo(({ onClose }) => {
onBack={handleBack} onBack={handleBack}
/> />
); );
case StepTypes.CUSTOM_FIELD_GROUPS:
return <CustomFieldGroupsStep onBack={handleBack} onClose={onClose} />;
default: default:
} }
} }
@ -128,6 +133,12 @@ const ActionsStep = React.memo(({ onClose }) => {
})} })}
</Menu.Item> </Menu.Item>
)} )}
<Menu.Item className={styles.menuItem} onClick={handleActivitiesClick}>
<Icon name="list ul" className={styles.menuItemIcon} />
{t('common.actions', {
context: 'title',
})}
</Menu.Item>
{withTrashEmptier && ( {withTrashEmptier && (
<> <>
{(withSubscribe || withCustomFieldGroups) && <hr className={styles.divider} />} {(withSubscribe || withCustomFieldGroups) && <hr className={styles.divider} />}

View file

@ -8,7 +8,7 @@ import { useTranslation } from 'react-i18next';
import { Menu, Tab } from 'semantic-ui-react'; import { Menu, Tab } from 'semantic-ui-react';
import Comments from '../../comments/Comments'; import Comments from '../../comments/Comments';
import Activities from '../../activities/Activities'; import CardActivities from '../../activities/CardActivities';
import styles from './Communication.module.scss'; import styles from './Communication.module.scss';
@ -34,7 +34,7 @@ const Communication = React.memo(() => {
})} })}
</Menu.Item> </Menu.Item>
), ),
render: () => <Activities />, render: () => <CardActivities />,
}, },
]; ];

View file

@ -29,6 +29,7 @@
.content { .content {
display: inline-block; display: inline-block;
font-size: 13px; font-size: 13px;
line-height: 20px;
min-height: 36px; min-height: 36px;
overflow: hidden; overflow: hidden;
padding: 0 4px 0 8px; padding: 0 4px 0 8px;
@ -40,7 +41,6 @@
.date { .date {
color: #6b808c; color: #6b808c;
font-size: 12px; font-size: 12px;
line-height: 20px;
} }
.wrapper { .wrapper {

View file

@ -360,9 +360,12 @@ export default {
/* Activities */ /* Activities */
ACTIVITIES_FETCH: 'ACTIVITIES_FETCH', ACTIVITIES_IN_BOARD_FETCH: 'ACTIVITIES_IN_BOARD_FETCH',
ACTIVITIES_FETCH__SUCCESS: 'ACTIVITIES_FETCH__SUCCESS', ACTIVITIES_IN_BOARD_FETCH__SUCCESS: 'ACTIVITIES_IN_BOARD_FETCH__SUCCESS',
ACTIVITIES_FETCH__FAILURE: 'ACTIVITIES_FETCH__FAILURE', ACTIVITIES_IN_BOARD_FETCH__FAILURE: 'ACTIVITIES_IN_BOARD_FETCH__FAILURE',
ACTIVITIES_IN_CARD_FETCH: 'ACTIVITIES_IN_CARD_FETCH',
ACTIVITIES_IN_CARD_FETCH__SUCCESS: 'ACTIVITIES_IN_CARD_FETCH__SUCCESS',
ACTIVITIES_IN_CARD_FETCH__FAILURE: 'ACTIVITIES_IN_CARD_FETCH__FAILURE',
ACTIVITY_CREATE_HANDLE: 'ACTIVITY_CREATE_HANDLE', ACTIVITY_CREATE_HANDLE: 'ACTIVITY_CREATE_HANDLE',
/* Notifications */ /* Notifications */

View file

@ -255,6 +255,7 @@ export default {
/* Activities */ /* Activities */
ACTIVITIES_IN_CURRENT_BOARD_FETCH: `${PREFIX}/ACTIVITIES_IN_CURRENT_BOARD_FETCH`,
ACTIVITIES_IN_CURRENT_CARD_FETCH: `${PREFIX}/ACTIVITIES_IN_CURRENT_CARD_FETCH`, ACTIVITIES_IN_CURRENT_CARD_FETCH: `${PREFIX}/ACTIVITIES_IN_CURRENT_CARD_FETCH`,
ACTIVITY_CREATE_HANDLE: `${PREFIX}/ACTIVITY_CREATE_HANDLE`, ACTIVITY_CREATE_HANDLE: `${PREFIX}/ACTIVITY_CREATE_HANDLE`,

View file

@ -8,6 +8,7 @@ const USER_SETTINGS = 'USER_SETTINGS';
const ADD_PROJECT = 'ADD_PROJECT'; const ADD_PROJECT = 'ADD_PROJECT';
const PROJECT_SETTINGS = 'PROJECT_SETTINGS'; const PROJECT_SETTINGS = 'PROJECT_SETTINGS';
const BOARD_SETTINGS = 'BOARD_SETTINGS'; const BOARD_SETTINGS = 'BOARD_SETTINGS';
const BOARD_ACTIVITIES = 'BOARD_ACTIVITIES';
export default { export default {
ADMINISTRATION, ADMINISTRATION,
@ -15,4 +16,5 @@ export default {
ADD_PROJECT, ADD_PROJECT,
PROJECT_SETTINGS, PROJECT_SETTINGS,
BOARD_SETTINGS, BOARD_SETTINGS,
BOARD_ACTIVITIES,
}; };

View file

@ -5,6 +5,11 @@
import EntryActionTypes from '../constants/EntryActionTypes'; import EntryActionTypes from '../constants/EntryActionTypes';
const fetchActivitiesInCurrentBoard = () => ({
type: EntryActionTypes.ACTIVITIES_IN_CURRENT_BOARD_FETCH,
payload: {},
});
const fetchActivitiesInCurrentCard = () => ({ const fetchActivitiesInCurrentCard = () => ({
type: EntryActionTypes.ACTIVITIES_IN_CURRENT_CARD_FETCH, type: EntryActionTypes.ACTIVITIES_IN_CURRENT_CARD_FETCH,
payload: {}, payload: {},
@ -18,6 +23,7 @@ const handleActivityCreate = (activity) => ({
}); });
export default { export default {
fetchActivitiesInCurrentBoard,
fetchActivitiesInCurrentCard, fetchActivitiesInCurrentCard,
handleActivityCreate, handleActivityCreate,
}; };

View file

@ -47,6 +47,13 @@ const openBoardSettingsModal = (boardId) => ({
}, },
}); });
const openBoardActivitiesModal = () => ({
type: EntryActionTypes.MODAL_OPEN,
payload: {
type: ModalTypes.BOARD_ACTIVITIES,
},
});
const closeModal = () => ({ const closeModal = () => ({
type: EntryActionTypes.MODAL_CLOSE, type: EntryActionTypes.MODAL_CLOSE,
payload: {}, payload: {},
@ -58,5 +65,6 @@ export default {
openAddProjectModal, openAddProjectModal,
openProjectSettingsModal, openProjectSettingsModal,
openBoardSettingsModal, openBoardSettingsModal,
openBoardActivitiesModal,
closeModal, closeModal,
}; };

View file

@ -59,6 +59,7 @@ export default (initialClosableValue) => {
onClose: undefined, onClose: undefined,
}; };
ClosableModal.Header = Modal.Header;
ClosableModal.Content = Modal.Content; ClosableModal.Content = Modal.Content;
ClosableModal.Actions = Modal.Actions; ClosableModal.Actions = Modal.Actions;

View file

@ -152,12 +152,12 @@ export default {
time: 'الوقت', time: 'الوقت',
title: 'العنوان', title: 'العنوان',
userActions_title: 'إجراءات المستخدم', userActions_title: 'إجراءات المستخدم',
userAddedThisCardToList: '<0>{{user}}</0><1> تمت إضافة هذه البطاقة إلى {{list}}</1>', userAddedThisCardToList: '<0>{{user}}</0> تمت إضافة هذه البطاقة إلى {{list}}',
userLeftNewCommentToCard: '<0>{{user}}</0> ترك تعليق جديد «{{comment}}» إلى <2>{{card}}</2>', userLeftNewCommentToCard: '<0>{{user}}</0> ترك تعليق جديد «{{comment}}» إلى <2>{{card}}</2>',
userMovedCardFromListToList: userMovedCardFromListToList:
'<0>{{user}}</0> انتقل <2>{{card}}</2> من {{fromList}} إلى {{toList}}', '<0>{{user}}</0> انتقل <2>{{card}}</2> من {{fromList}} إلى {{toList}}',
userMovedThisCardFromListToList: userMovedThisCardFromListToList:
'<0>{{user}}</0><1> نُقلت هذه البطاقة من {{fromList}} إلى {{toList}}</1>', '<0>{{user}}</0> نُقلت هذه البطاقة من {{fromList}} إلى {{toList}}',
username: 'اسم المستخدم', username: 'اسم المستخدم',
users: 'المستخدمين', users: 'المستخدمين',
viewer: 'مشاهد', viewer: 'مشاهد',

View file

@ -153,13 +153,13 @@ export default {
time: 'Време', time: 'Време',
title: 'Заглавие', title: 'Заглавие',
userActions_title: 'Потребителски действия', userActions_title: 'Потребителски действия',
userAddedThisCardToList: '<0>{{user}}</0><1> добави тази карта в {{list}}</1>', userAddedThisCardToList: '<0>{{user}}</0> добави тази карта в {{list}}',
userLeftNewCommentToCard: userLeftNewCommentToCard:
'<0>{{user}}</0> остави нов коментар «{{comment}}» в <2>{{card}}</2>', '<0>{{user}}</0> остави нов коментар «{{comment}}» в <2>{{card}}</2>',
userMovedCardFromListToList: userMovedCardFromListToList:
'<0>{{user}}</0> премести <2>{{card}}</2> от {{fromList}} към {{toList}}', '<0>{{user}}</0> премести <2>{{card}}</2> от {{fromList}} към {{toList}}',
userMovedThisCardFromListToList: userMovedThisCardFromListToList:
'<0>{{user}}</0><1> премести тази карта от {{fromList}} към {{toList}}</1>', '<0>{{user}}</0> премести тази карта от {{fromList}} към {{toList}}',
username: 'Потребителско име', username: 'Потребителско име',
users: 'Потребители', users: 'Потребители',
viewer: 'Зрител', viewer: 'Зрител',

View file

@ -276,13 +276,13 @@ export default {
unsavedChanges: 'Neuložené změny', unsavedChanges: 'Neuložené změny',
uploadedImages: 'Nahrané obrázky', uploadedImages: 'Nahrané obrázky',
userActions_title: 'Akce uživatele', userActions_title: 'Akce uživatele',
userAddedThisCardToList: '<0>{{user}}</0><1> přidal kartu do {{list}}</1>', userAddedThisCardToList: '<0>{{user}}</0> přidal kartu do {{list}}',
userLeftNewCommentToCard: userLeftNewCommentToCard:
'<0>{{user}}</0> zanechal nový komentář «{{comment}}» k <2>{{card}}</2>', '<0>{{user}}</0> zanechal nový komentář «{{comment}}» k <2>{{card}}</2>',
userMovedCardFromListToList: userMovedCardFromListToList:
'<0>{{user}}</0> přesunul <2>{{card}}</2> z {{fromList}} do {{toList}}', '<0>{{user}}</0> přesunul <2>{{card}}</2> z {{fromList}} do {{toList}}',
userMovedThisCardFromListToList: userMovedThisCardFromListToList:
'<0>{{user}}</0><1> přesunul tuto kartu z {{fromList}} do {{toList}}</1>', '<0>{{user}}</0> přesunul tuto kartu z {{fromList}} do {{toList}}',
username: 'Uživatelské jméno', username: 'Uživatelské jméno',
users: 'Uživatelé', users: 'Uživatelé',
viewer: 'Prohlížeč', viewer: 'Prohlížeč',

View file

@ -154,13 +154,13 @@ export default {
time: 'Tid', time: 'Tid',
title: 'Overskrift', title: 'Overskrift',
userActions_title: 'Brugerhandlinger', userActions_title: 'Brugerhandlinger',
userAddedThisCardToList: '<0>{{user}}</0><1> tilføjede kortet til {{list}}</1>', userAddedThisCardToList: '<0>{{user}}</0> tilføjede kortet til {{list}}',
userLeftNewCommentToCard: userLeftNewCommentToCard:
'<0>{{user}}</0> skrevet en ny kommentar «{{comment}}» på <2>{{card}}</2>', '<0>{{user}}</0> skrevet en ny kommentar «{{comment}}» på <2>{{card}}</2>',
userMovedCardFromListToList: userMovedCardFromListToList:
'<0>{{user}}</0> flyttede <2>{{card}}</2> fra {{fromList}} til {{toList}}', '<0>{{user}}</0> flyttede <2>{{card}}</2> fra {{fromList}} til {{toList}}',
userMovedThisCardFromListToList: userMovedThisCardFromListToList:
'<0>{{user}}</0><1> flyttede kortet fra {{fromList}} til {{toList}}</1>', '<0>{{user}}</0> flyttede kortet fra {{fromList}} til {{toList}}',
username: 'Brugernavn', username: 'Brugernavn',
users: 'Brugere', users: 'Brugere',
viewer: 'Læser', viewer: 'Læser',

View file

@ -296,13 +296,13 @@ export default {
unsavedChanges: 'Ungespeicherte Änderungen', unsavedChanges: 'Ungespeicherte Änderungen',
uploadedImages: 'Hochgeladene Bilder', uploadedImages: 'Hochgeladene Bilder',
userActions_title: 'Benutzeraktionen', userActions_title: 'Benutzeraktionen',
userAddedThisCardToList: '<0>{{user}}</0><1> hat diese Karte hinzugefügt zu {{list}}</1>', userAddedThisCardToList: '<0>{{user}}</0> hat diese Karte hinzugefügt zu {{list}}',
userLeftNewCommentToCard: userLeftNewCommentToCard:
'<0>{{user}}</0> hat einen neuen Kommentar verfasst: «{{comment}}» in <2>{{card}}</2>', '<0>{{user}}</0> hat einen neuen Kommentar verfasst: «{{comment}}» in <2>{{card}}</2>',
userMovedCardFromListToList: userMovedCardFromListToList:
'<0>{{user}}</0> bewegte <2>{{card}}</2> von {{fromList}} nach {{toList}}', '<0>{{user}}</0> bewegte <2>{{card}}</2> von {{fromList}} nach {{toList}}',
userMovedThisCardFromListToList: userMovedThisCardFromListToList:
'<0>{{user}}</0><1> bewegte diese Karte von {{fromList}} nach {{toList}}</1>', '<0>{{user}}</0> bewegte diese Karte von {{fromList}} nach {{toList}}',
username: 'Benutzername', username: 'Benutzername',
users: 'Benutzer', users: 'Benutzer',
viewer: 'Betrachter', viewer: 'Betrachter',

View file

@ -286,22 +286,28 @@ export default {
unsavedChanges: 'Unsaved changes', unsavedChanges: 'Unsaved changes',
uploadedImages: 'Uploaded images', uploadedImages: 'Uploaded images',
userActions_title: 'User Actions', userActions_title: 'User Actions',
userAddedThisCardToList: '<0>{{user}}</0><1> added this card to {{list}}</1>', userAddedCardToList: '<0>{{user}}</0> added <2>{{card}}</2> to {{list}}',
userAddedUserToThisCard: '<0>{{actorUser}}</0><1> added {{addedUser}} to this card</1>', userAddedThisCardToList: '<0>{{user}}</0> added this card to {{list}}',
userAddedUserToCard: '<0>{{actorUser}}</0> added {{addedUser}} to <4>{{card}}</4>',
userAddedUserToThisCard: '<0>{{actorUser}}</0> added {{addedUser}} to this card',
userAddedYouToCard: '<0>{{user}}</0> added you to <2>{{card}}</2>', userAddedYouToCard: '<0>{{user}}</0> added you to <2>{{card}}</2>',
userCompletedTaskOnThisCard: '<0>{{user}}</0><1> completed {{task}} on this card</1>', userCompletedTaskOnCard: '<0>{{user}}</0> completed {{task}} on <4>{{card}}</4>',
userJoinedThisCard: `<0>{{user}}</0><1> joined this card</1>`, userCompletedTaskOnThisCard: '<0>{{user}}</0> completed {{task}} on this card',
userJoinedCard: `<0>{{user}}</0> joined <2>{{card}}</2>`,
userJoinedThisCard: `<0>{{user}}</0> joined this card`,
userLeftNewCommentToCard: userLeftNewCommentToCard:
'<0>{{user}}</0> left a new comment «{{comment}}» to <2>{{card}}</2>', '<0>{{user}}</0> left a new comment «{{comment}}» to <2>{{card}}</2>',
userLeftThisCard: '<0>{{user}}</0><1> left this card</1>', userLeftCard: '<0>{{user}}</0> left <2>{{card}}</2>',
userMarkedTaskIncompleteOnThisCard: userLeftThisCard: '<0>{{user}}</0> left this card',
'<0>{{user}}</0><1> marked {{task}} incomplete on this card</1>', userMarkedTaskIncompleteOnCard:
'<0>{{user}}</0> marked {{task}} incomplete on <4>{{card}}</4>',
userMarkedTaskIncompleteOnThisCard: '<0>{{user}}</0> marked {{task}} incomplete on this card',
userMovedCardFromListToList: userMovedCardFromListToList:
'<0>{{user}}</0> moved <2>{{card}}</2> from {{fromList}} to {{toList}}', '<0>{{user}}</0> moved <2>{{card}}</2> from {{fromList}} to {{toList}}',
userMovedThisCardFromListToList: userMovedThisCardFromListToList:
'<0>{{user}}</0><1> moved this card from {{fromList}} to {{toList}}</1>', '<0>{{user}}</0> moved this card from {{fromList}} to {{toList}}',
userRemovedUserFromThisCard: userRemovedUserFromCard: '<0>{{actorUser}}</0> removed {{removedUser}} from <4>{{card}}</4>',
'<0>{{actorUser}}</0><1> removed {{removedUser}} from this card</1>', userRemovedUserFromThisCard: '<0>{{actorUser}}</0> removed {{removedUser}} from this card',
username: 'Username', username: 'Username',
users: 'Users', users: 'Users',
viewer: 'Viewer', viewer: 'Viewer',

View file

@ -281,22 +281,28 @@ export default {
unsavedChanges: 'Unsaved changes', unsavedChanges: 'Unsaved changes',
uploadedImages: 'Uploaded images', uploadedImages: 'Uploaded images',
userActions_title: 'User Actions', userActions_title: 'User Actions',
userAddedThisCardToList: '<0>{{user}}</0><1> added this card to {{list}}</1>', userAddedCardToList: '<0>{{user}}</0> added <2>{{card}}</2> to {{list}}',
userAddedUserToThisCard: '<0>{{actorUser}}</0><1> added {{addedUser}} to this card</1>', userAddedThisCardToList: '<0>{{user}}</0> added this card to {{list}}',
userAddedUserToCard: '<0>{{actorUser}}</0> added {{addedUser}} to <4>{{card}}</4>',
userAddedUserToThisCard: '<0>{{actorUser}}</0> added {{addedUser}} to this card',
userAddedYouToCard: '<0>{{user}}</0> added you to <2>{{card}}</2>', userAddedYouToCard: '<0>{{user}}</0> added you to <2>{{card}}</2>',
userCompletedTaskOnThisCard: '<0>{{user}}</0><1> completed {{task}} on this card</1>', userCompletedTaskOnCard: '<0>{{user}}</0> completed {{task}} on <4>{{card}}</4>',
userJoinedThisCard: `<0>{{user}}</0><1> joined this card</1>`, userCompletedTaskOnThisCard: '<0>{{user}}</0> completed {{task}} on this card',
userJoinedCard: `<0>{{user}}</0> joined <2>{{card}}</2>`,
userJoinedThisCard: `<0>{{user}}</0> joined this card`,
userLeftNewCommentToCard: userLeftNewCommentToCard:
'<0>{{user}}</0> left a new comment «{{comment}}» to <2>{{card}}</2>', '<0>{{user}}</0> left a new comment «{{comment}}» to <2>{{card}}</2>',
userLeftThisCard: '<0>{{user}}</0><1> left this card</1>', userLeftCard: '<0>{{user}}</0> left <2>{{card}}</2>',
userMarkedTaskIncompleteOnThisCard: userLeftThisCard: '<0>{{user}}</0> left this card',
'<0>{{user}}</0><1> marked {{task}} incomplete on this card</1>', userMarkedTaskIncompleteOnCard:
'<0>{{user}}</0> marked {{task}} incomplete on <4>{{card}}</4>',
userMarkedTaskIncompleteOnThisCard: '<0>{{user}}</0> marked {{task}} incomplete on this card',
userMovedCardFromListToList: userMovedCardFromListToList:
'<0>{{user}}</0> moved <2>{{card}}</2> from {{fromList}} to {{toList}}', '<0>{{user}}</0> moved <2>{{card}}</2> from {{fromList}} to {{toList}}',
userMovedThisCardFromListToList: userMovedThisCardFromListToList:
'<0>{{user}}</0><1> moved this card from {{fromList}} to {{toList}}</1>', '<0>{{user}}</0> moved this card from {{fromList}} to {{toList}}',
userRemovedUserFromThisCard: userRemovedUserFromCard: '<0>{{actorUser}}</0> removed {{removedUser}} from <4>{{card}}</4>',
'<0>{{actorUser}}</0><1> removed {{removedUser}} from this card</1>', userRemovedUserFromThisCard: '<0>{{actorUser}}</0> removed {{removedUser}} from this card',
username: 'Username', username: 'Username',
users: 'Users', users: 'Users',
viewer: 'Viewer', viewer: 'Viewer',

View file

@ -117,13 +117,13 @@ export default {
time: 'Tiempo', time: 'Tiempo',
title: 'Título', title: 'Título',
userActions_title: 'Acciones de Usuario', userActions_title: 'Acciones de Usuario',
userAddedThisCardToList: '<0>{{user}}</0><1> añadido a esta tarjeta en {{list}}</1>', userAddedThisCardToList: '<0>{{user}}</0> añadido a esta tarjeta en {{list}}',
userLeftNewCommentToCard: userLeftNewCommentToCard:
'<0>{{user}}</0> dejó un nuevo comentario «{{comment}}» en <2>{{card}}</2>', '<0>{{user}}</0> dejó un nuevo comentario «{{comment}}» en <2>{{card}}</2>',
userMovedCardFromListToList: userMovedCardFromListToList:
'<0>{{user}}</0> movió <2>{{card}}</2> de {{fromList}} a {{toList}}', '<0>{{user}}</0> movió <2>{{card}}</2> de {{fromList}} a {{toList}}',
userMovedThisCardFromListToList: userMovedThisCardFromListToList:
'<0>{{user}}</0><1> movió esta tarjeta de {{fromList}} a {{toList}}</1>', '<0>{{user}}</0> movió esta tarjeta de {{fromList}} a {{toList}}',
username: 'Nombre de usuario', username: 'Nombre de usuario',
users: 'Usuarios', users: 'Usuarios',
writeComment: 'Escribir un comentario...', writeComment: 'Escribir un comentario...',

View file

@ -153,13 +153,13 @@ export default {
time: 'زمان', time: 'زمان',
title: 'عنوان', title: 'عنوان',
userActions_title: 'اقدامات کاربر', userActions_title: 'اقدامات کاربر',
userAddedThisCardToList: '<0>{{user}}</0><1> این کارت را به {{list}} اضافه کرد</1>', userAddedThisCardToList: '<0>{{user}}</0> این کارت را به {{list}} اضافه کرد',
userLeftNewCommentToCard: userLeftNewCommentToCard:
'<0>{{user}}</0> نظر جدید «{{comment}}» را به <2>{{card}}</2> اضافه کرد', '<0>{{user}}</0> نظر جدید «{{comment}}» را به <2>{{card}}</2> اضافه کرد',
userMovedCardFromListToList: userMovedCardFromListToList:
'<0>{{user}}</0> <2>{{card}}</2> را از {{fromList}} به {{toList}} منتقل کرد', '<0>{{user}}</0> <2>{{card}}</2> را از {{fromList}} به {{toList}} منتقل کرد',
userMovedThisCardFromListToList: userMovedThisCardFromListToList:
'<0>{{user}}</0><1> این کارت را از {{fromList}} به {{toList}} منتقل کرد</1>', '<0>{{user}}</0> این کارت را از {{fromList}} به {{toList}} منتقل کرد',
username: 'نام کاربری', username: 'نام کاربری',
users: 'کاربران', users: 'کاربران',
viewer: 'بیننده', viewer: 'بیننده',

View file

@ -153,13 +153,13 @@ export default {
time: 'Temps', time: 'Temps',
title: 'Titre', title: 'Titre',
userActions_title: "Actions de l'utilisateur", userActions_title: "Actions de l'utilisateur",
userAddedThisCardToList: '<0>{{user}}</0><1> a ajouté cette carte à {{list}}</1>', userAddedThisCardToList: '<0>{{user}}</0> a ajouté cette carte à {{list}}',
userLeftNewCommentToCard: userLeftNewCommentToCard:
'<0>{{user}}</0> a laissé un nouveau commentaire {{comment}} à <2>{{card}}</2>', '<0>{{user}}</0> a laissé un nouveau commentaire {{comment}} à <2>{{card}}</2>',
userMovedCardFromListToList: userMovedCardFromListToList:
'<0>{{user}}</0> a déplacé <2>{{card}}</2> de {{fromList}} vers {{toList}}', '<0>{{user}}</0> a déplacé <2>{{card}}</2> de {{fromList}} vers {{toList}}',
userMovedThisCardFromListToList: userMovedThisCardFromListToList:
'<0>{{user}}</0><1> a déplacé cette carte de {{fromList}} vers {{toList}}</1>', '<0>{{user}}</0> a déplacé cette carte de {{fromList}} vers {{toList}}',
username: "Nom d'utilisateur", username: "Nom d'utilisateur",
users: 'Utilisateurs', users: 'Utilisateurs',
viewer: 'Spectateur', viewer: 'Spectateur',

View file

@ -151,14 +151,13 @@ export default {
time: 'Idő', time: 'Idő',
title: 'Cím', title: 'Cím',
userActions_title: 'Felhasználói műveletek', userActions_title: 'Felhasználói műveletek',
userAddedThisCardToList: userAddedThisCardToList: '<0>{{user}}</0> hozzáadta ezt a kártyát a következőhöz: {{list}}',
'<0>{{user}}</0><1> hozzáadta ezt a kártyát a következőhöz: {{list}}</1>',
userLeftNewCommentToCard: userLeftNewCommentToCard:
'<0>{{user}}</0> új kommentet hagyott itt: «{{comment}}» a következő kártyán: <2>{{card}}</2>', '<0>{{user}}</0> új kommentet hagyott itt: «{{comment}}» a következő kártyán: <2>{{card}}</2>',
userMovedCardFromListToList: userMovedCardFromListToList:
'<0>{{user}}</0> áthelyezte ezt a kártyát innen: {{fromList}} ide: {{toList}}', '<0>{{user}}</0> áthelyezte ezt a kártyát innen: {{fromList}} ide: {{toList}}',
userMovedThisCardFromListToList: userMovedThisCardFromListToList:
'<0>{{user}}</0><1> áthelyezte ezt a kártyát innen: {{fromList}} ide: {{toList}}</1>', '<0>{{user}}</0> áthelyezte ezt a kártyát innen: {{fromList}} ide: {{toList}}',
username: 'Felhasználónév', username: 'Felhasználónév',
users: 'Felhasználók', users: 'Felhasználók',
viewer: 'Néző', viewer: 'Néző',

View file

@ -147,12 +147,12 @@ export default {
time: 'Waktu', time: 'Waktu',
title: 'Judul', title: 'Judul',
userActions_title: 'Aksi Pengguna', userActions_title: 'Aksi Pengguna',
userAddedThisCardToList: '<0>{{user}}</0><1> menambahkan kartu ini ke {{list}}</1>', userAddedThisCardToList: '<0>{{user}}</0> menambahkan kartu ini ke {{list}}',
userLeftNewCommentToCard: '<0>{{user}}</0> mengomentari «{{comment}}» di <2>{{card}}</2>', userLeftNewCommentToCard: '<0>{{user}}</0> mengomentari «{{comment}}» di <2>{{card}}</2>',
userMovedCardFromListToList: userMovedCardFromListToList:
'<0>{{user}}</0> memindahkan <2>{{card}}</2> dari {{fromList}} ke {{toList}}', '<0>{{user}}</0> memindahkan <2>{{card}}</2> dari {{fromList}} ke {{toList}}',
userMovedThisCardFromListToList: userMovedThisCardFromListToList:
'<0>{{user}}</0><1> memindahkan kartu ini dari {{fromList}} ke {{toList}}</1>', '<0>{{user}}</0> memindahkan kartu ini dari {{fromList}} ke {{toList}}',
username: 'Username', username: 'Username',
users: 'Pengguna', users: 'Pengguna',
viewer: 'Penglihat', viewer: 'Penglihat',

View file

@ -284,13 +284,13 @@ export default {
unsavedChanges: 'Modifiche non salvate', unsavedChanges: 'Modifiche non salvate',
uploadedImages: 'Immagini caricate', uploadedImages: 'Immagini caricate',
userActions_title: 'Azioni utente', userActions_title: 'Azioni utente',
userAddedThisCardToList: '<0>{{user}}</0><1> ha aggiunto questa scheda a {{list}}</1>', userAddedThisCardToList: '<0>{{user}}</0> ha aggiunto questa scheda a {{list}}',
userLeftNewCommentToCard: userLeftNewCommentToCard:
'<0>{{user}}</0> ha lasciato un commento «{{comment}}» a <2>{{card}}</2>', '<0>{{user}}</0> ha lasciato un commento «{{comment}}» a <2>{{card}}</2>',
userMovedCardFromListToList: userMovedCardFromListToList:
'<0>{{user}}</0> ha spostato <2>{{card}}</2> da {{fromList}} a {{toList}}', '<0>{{user}}</0> ha spostato <2>{{card}}</2> da {{fromList}} a {{toList}}',
userMovedThisCardFromListToList: userMovedThisCardFromListToList:
'<0>{{user}}</0><1> ha spostato questa scheda da {{fromList}} a {{toList}}</1>', '<0>{{user}}</0> ha spostato questa scheda da {{fromList}} a {{toList}}',
username: 'Username', username: 'Username',
users: 'Utenti', users: 'Utenti',
viewer: 'Visualizzatore', viewer: 'Visualizzatore',

View file

@ -146,13 +146,13 @@ export default {
time: '時間', time: '時間',
title: 'タイトル', title: 'タイトル',
userActions_title: 'ユーザーのアクション', userActions_title: 'ユーザーのアクション',
userAddedThisCardToList: '<0>{{user}}</0> 様が <1>{{list}} をこのカードに追加しました</1>', userAddedThisCardToList: '<0>{{user}}</0> 様が {{list}} をこのカードに追加しました',
userLeftNewCommentToCard: userLeftNewCommentToCard:
'<0>{{user}}</0> 様が <2>{{card}}</2> に新しいコメント «{{comment}}» を残しました', '<0>{{user}}</0> 様が <2>{{card}}</2> に新しいコメント «{{comment}}» を残しました',
userMovedCardFromListToList: userMovedCardFromListToList:
'<0>{{user}}</0> 様が <2>{{card}}</2> を {{fromList}} から {{toList}} に移動しました', '<0>{{user}}</0> 様が <2>{{card}}</2> を {{fromList}} から {{toList}} に移動しました',
userMovedThisCardFromListToList: userMovedThisCardFromListToList:
'<0>{{user}}</0><1> 様がこのカードを {{fromList}} から {{toList}} に移動しました</1>', '<0>{{user}}</0> 様がこのカードを {{fromList}} から {{toList}} に移動しました',
username: 'ユーザー名', username: 'ユーザー名',
users: 'ユーザー', users: 'ユーザー',
viewer: 'ビューア', viewer: 'ビューア',

View file

@ -151,13 +151,13 @@ export default {
time: '시간', time: '시간',
title: '제목', title: '제목',
userActions_title: '사용자 작업', userActions_title: '사용자 작업',
userAddedThisCardToList: '<0>{{user}}</0><1>님이 이 카드를 {{list}}에 추가했습니다</1>', userAddedThisCardToList: '<0>{{user}}</0>님이 이 카드를 {{list}}에 추가했습니다',
userLeftNewCommentToCard: userLeftNewCommentToCard:
'<0>{{user}}</0>님이 <2>{{card}}</2>에 새 댓글 «{{comment}}»을 남겼습니다', '<0>{{user}}</0>님이 <2>{{card}}</2>에 새 댓글 «{{comment}}»을 남겼습니다',
userMovedCardFromListToList: userMovedCardFromListToList:
'<0>{{user}}</0>님이 <2>{{card}}</2>를 {{fromList}}에서 {{toList}}로 이동했습니다', '<0>{{user}}</0>님이 <2>{{card}}</2>를 {{fromList}}에서 {{toList}}로 이동했습니다',
userMovedThisCardFromListToList: userMovedThisCardFromListToList:
'<0>{{user}}</0><1> 님이 {{fromList}}에서 {{toList}}로 이 카드를 옮겼습니다</1>', '<0>{{user}}</0> 님이 {{fromList}}에서 {{toList}}로 이 카드를 옮겼습니다',
username: '사용자 이름', username: '사용자 이름',
users: '사용자들', users: '사용자들',
viewer: '뷰어', viewer: '뷰어',

View file

@ -147,13 +147,13 @@ export default {
time: 'Tijd', time: 'Tijd',
title: 'Titel', title: 'Titel',
userActions_title: 'Gebruikersacties', userActions_title: 'Gebruikersacties',
userAddedThisCardToList: '<0>{{user}}</0><1> heeft deze kaart toegevoegd aan {{list}}</1>', userAddedThisCardToList: '<0>{{user}}</0> heeft deze kaart toegevoegd aan {{list}}',
userLeftNewCommentToCard: userLeftNewCommentToCard:
'<0>{{user}}</0> heeft een nieuwe opmerking achtergelaten «{{comment}}» bij <2>{{card}}</2>', '<0>{{user}}</0> heeft een nieuwe opmerking achtergelaten «{{comment}}» bij <2>{{card}}</2>',
userMovedCardFromListToList: userMovedCardFromListToList:
'<0>{{user}}</0> heeft <2>{{card}}</2> verplaatst van {{fromList}} naar {{toList}}', '<0>{{user}}</0> heeft <2>{{card}}</2> verplaatst van {{fromList}} naar {{toList}}',
userMovedThisCardFromListToList: userMovedThisCardFromListToList:
'<0>{{user}}</0><1> heeft deze kaart verplaatst van {{fromList}} naar {{toList}}</1>', '<0>{{user}}</0> heeft deze kaart verplaatst van {{fromList}} naar {{toList}}',
username: 'Gebruikersnaam', username: 'Gebruikersnaam',
users: 'Gebruikers', users: 'Gebruikers',
viewer: 'Kijker', viewer: 'Kijker',

View file

@ -148,13 +148,13 @@ export default {
time: 'Czas', time: 'Czas',
title: 'Tytuł', title: 'Tytuł',
userActions_title: 'Akcje użytkownika', userActions_title: 'Akcje użytkownika',
userAddedThisCardToList: '<0>{{user}}</0><1> dodał tę kartę w {{list}}</1>', userAddedThisCardToList: '<0>{{user}}</0> dodał tę kartę w {{list}}',
userLeftNewCommentToCard: userLeftNewCommentToCard:
'<0>{{user}}</0> zamieścił nowy komentarz «{{comment}}» w <2>{{card}}</2>', '<0>{{user}}</0> zamieścił nowy komentarz «{{comment}}» w <2>{{card}}</2>',
userMovedCardFromListToList: userMovedCardFromListToList:
'<0>{{user}}</0> przeniósł <2>{{card}}</2> z {{fromList}} do {{toList}}', '<0>{{user}}</0> przeniósł <2>{{card}}</2> z {{fromList}} do {{toList}}',
userMovedThisCardFromListToList: userMovedThisCardFromListToList:
'<0>{{user}}</0><1> przeniósł tę kartę z {{fromList}} do {{toList}}</1>', '<0>{{user}}</0> przeniósł tę kartę z {{fromList}} do {{toList}}',
username: 'Nazwa Użytkownika', username: 'Nazwa Użytkownika',
users: 'Użytkownicy', users: 'Użytkownicy',
viewer: 'Odwiedzający', viewer: 'Odwiedzający',

View file

@ -147,13 +147,13 @@ export default {
time: 'Tempo', time: 'Tempo',
title: 'Título', title: 'Título',
userActions_title: 'Ações do Usuário', userActions_title: 'Ações do Usuário',
userAddedThisCardToList: '<0>{{user}}</0><1> adicionou este cartão a {{list}}</1>', userAddedThisCardToList: '<0>{{user}}</0> adicionou este cartão a {{list}}',
userLeftNewCommentToCard: userLeftNewCommentToCard:
'<0>{{user}}</0> deixou um novo comentário «{{comment}}» em <2>{{card}}</2>', '<0>{{user}}</0> deixou um novo comentário «{{comment}}» em <2>{{card}}</2>',
userMovedCardFromListToList: userMovedCardFromListToList:
'<0>{{user}}</0> moveu <2>{{card}}</2> de {{fromList}} para {{toList}}', '<0>{{user}}</0> moveu <2>{{card}}</2> de {{fromList}} para {{toList}}',
userMovedThisCardFromListToList: userMovedThisCardFromListToList:
'<0>{{user}}</0><1> moveu este cartão de {{fromList}} para {{toList}}</1>', '<0>{{user}}</0> moveu este cartão de {{fromList}} para {{toList}}',
username: 'Nome de usuário', username: 'Nome de usuário',
users: 'Usuários', users: 'Usuários',
viewer: 'Visualizador', viewer: 'Visualizador',

View file

@ -147,13 +147,13 @@ export default {
time: 'Timp', time: 'Timp',
title: 'Titlu', title: 'Titlu',
userActions_title: 'Acțiunile utilizatorului', userActions_title: 'Acțiunile utilizatorului',
userAddedThisCardToList: '<0>{{user}}</0><1> a adăugat acest card în {{list}}</1>', userAddedThisCardToList: '<0>{{user}}</0> a adăugat acest card în {{list}}',
userLeftNewCommentToCard: userLeftNewCommentToCard:
'<0>{{user}}</0> a lăsat un nou comentariu «{{comment}}» în <2>{{card}}</2>', '<0>{{user}}</0> a lăsat un nou comentariu «{{comment}}» în <2>{{card}}</2>',
userMovedCardFromListToList: userMovedCardFromListToList:
'<0>{{user}}</0> a mutat <2>{{card}}</2> din {{fromList}} în {{toList}}', '<0>{{user}}</0> a mutat <2>{{card}}</2> din {{fromList}} în {{toList}}',
userMovedThisCardFromListToList: userMovedThisCardFromListToList:
'<0>{{user}}</0><1> a mutat aceast card din {{fromList}} în {{toList}}</1>', '<0>{{user}}</0> a mutat aceast card din {{fromList}} în {{toList}}',
username: 'Nume utilizator', username: 'Nume utilizator',
users: 'Utilizatori', users: 'Utilizatori',
viewer: 'Vizualizator', viewer: 'Vizualizator',

View file

@ -284,13 +284,13 @@ export default {
unsavedChanges: 'Несохранённые изменения', unsavedChanges: 'Несохранённые изменения',
uploadedImages: 'Загруженные изображения', uploadedImages: 'Загруженные изображения',
userActions_title: 'Действия с пользователем', userActions_title: 'Действия с пользователем',
userAddedThisCardToList: '<0>{{user}}</0><1> добавил(а) эту карточку в {{list}}</1>', userAddedThisCardToList: '<0>{{user}}</0> добавил(а) эту карточку в {{list}}',
userLeftNewCommentToCard: userLeftNewCommentToCard:
'<0>{{user}}</0> оставил(а) комментарий «{{comment}}» к <2>{{card}}</2>', '<0>{{user}}</0> оставил(а) комментарий «{{comment}}» к <2>{{card}}</2>',
userMovedCardFromListToList: userMovedCardFromListToList:
'<0>{{user}}</0> переместил(а) <2>{{card}}</2> из {{fromList}} в {{toList}}', '<0>{{user}}</0> переместил(а) <2>{{card}}</2> из {{fromList}} в {{toList}}',
userMovedThisCardFromListToList: userMovedThisCardFromListToList:
'<0>{{user}}</0><1> переместил(а) эту карточку из {{fromList}} в {{toList}}</1>', '<0>{{user}}</0> переместил(а) эту карточку из {{fromList}} в {{toList}}',
username: 'Имя пользователя', username: 'Имя пользователя',
users: 'Пользователи', users: 'Пользователи',
viewer: 'Читатель', viewer: 'Читатель',

View file

@ -132,13 +132,13 @@ export default {
time: 'Čas', time: 'Čas',
title: 'Názov', title: 'Názov',
userActions_title: 'Akcie na používateľovi', userActions_title: 'Akcie na používateľovi',
userAddedThisCardToList: '<0>{{user}}</0><1> pridal kartu do {{list}}</1>', userAddedThisCardToList: '<0>{{user}}</0> pridal kartu do {{list}}',
userLeftNewCommentToCard: userLeftNewCommentToCard:
'<0>{{user}}</0> zanechal nový komentár «{{comment}}» k <2>{{card}}</2>', '<0>{{user}}</0> zanechal nový komentár «{{comment}}» k <2>{{card}}</2>',
userMovedCardFromListToList: userMovedCardFromListToList:
'<0>{{user}}</0> presunul <2>{{card}}</2> z {{fromList}} do {{toList}}', '<0>{{user}}</0> presunul <2>{{card}}</2> z {{fromList}} do {{toList}}',
userMovedThisCardFromListToList: userMovedThisCardFromListToList:
'<0>{{user}}</0><1> presunul túto kartu z {{fromList}} do {{toList}}</1>', '<0>{{user}}</0> presunul túto kartu z {{fromList}} do {{toList}}',
username: 'Používateľské meno', username: 'Používateľské meno',
users: 'Používatelia', users: 'Používatelia',
writeComment: 'Napísať komentár...', writeComment: 'Napísať komentár...',

View file

@ -152,13 +152,13 @@ export default {
time: 'Време', time: 'Време',
title: 'Наслов', title: 'Наслов',
userActions_title: 'Корисничке радње', userActions_title: 'Корисничке радње',
userAddedThisCardToList: '<0>{{user}}</0><1> је додао ову картицу на {{list}}</1>', userAddedThisCardToList: '<0>{{user}}</0> је додао ову картицу на {{list}}',
userLeftNewCommentToCard: userLeftNewCommentToCard:
'<0>{{user}}</0> је оставио нови коментар «{{comment}}» у <2>{{card}}</2>', '<0>{{user}}</0> је оставио нови коментар «{{comment}}» у <2>{{card}}</2>',
userMovedCardFromListToList: userMovedCardFromListToList:
'<0>{{user}}</0> је преместио <2>{{card}}</2> са {{fromList}} у {{toList}}', '<0>{{user}}</0> је преместио <2>{{card}}</2> са {{fromList}} у {{toList}}',
userMovedThisCardFromListToList: userMovedThisCardFromListToList:
'<0>{{user}}</0><1> је преместио ову картицу са {{fromList}} на {{toList}}</1>', '<0>{{user}}</0> је преместио ову картицу са {{fromList}} на {{toList}}',
username: 'Корисничко име', username: 'Корисничко име',
users: 'Корисници', users: 'Корисници',
viewer: 'Прегледач', viewer: 'Прегледач',

View file

@ -152,13 +152,13 @@ export default {
time: 'Vreme', time: 'Vreme',
title: 'Naslov', title: 'Naslov',
userActions_title: 'Korisničke radnje', userActions_title: 'Korisničke radnje',
userAddedThisCardToList: '<0>{{user}}</0><1> je dodao ovu karticu na {{list}}</1>', userAddedThisCardToList: '<0>{{user}}</0> je dodao ovu karticu na {{list}}',
userLeftNewCommentToCard: userLeftNewCommentToCard:
'<0>{{user}}</0> je ostavio novi komentar «{{comment}}» u <2>{{card}}</2>', '<0>{{user}}</0> je ostavio novi komentar «{{comment}}» u <2>{{card}}</2>',
userMovedCardFromListToList: userMovedCardFromListToList:
'<0>{{user}}</0> je premestio <2>{{card}}</2> sa {{fromList}} u {{toList}}', '<0>{{user}}</0> je premestio <2>{{card}}</2> sa {{fromList}} u {{toList}}',
userMovedThisCardFromListToList: userMovedThisCardFromListToList:
'<0>{{user}}</0><1> je premestio ovu karticu sa {{fromList}} na {{toList}}</1>', '<0>{{user}}</0> je premestio ovu karticu sa {{fromList}} na {{toList}}',
username: 'Korisničko ime', username: 'Korisničko ime',
users: 'Korisnici', users: 'Korisnici',
viewer: 'Pregledač', viewer: 'Pregledač',

View file

@ -133,13 +133,13 @@ export default {
time: 'Tid', time: 'Tid',
title: 'Titel', title: 'Titel',
userActions_title: 'Användaråtgärder', userActions_title: 'Användaråtgärder',
userAddedThisCardToList: '<0>{{user}}</0><1> lade till detta kort i {{list}}</1>', userAddedThisCardToList: '<0>{{user}}</0> lade till detta kort i {{list}}',
userLeftNewCommentToCard: userLeftNewCommentToCard:
'<0>{{user}}</0> lämnade en ny kommentar «{{comment}}» på <2>{{card}}</2>', '<0>{{user}}</0> lämnade en ny kommentar «{{comment}}» på <2>{{card}}</2>',
userMovedCardFromListToList: userMovedCardFromListToList:
'<0>{{user}}</0> flyttade <2>{{card}}</2> från {{fromList}} till {{toList}}', '<0>{{user}}</0> flyttade <2>{{card}}</2> från {{fromList}} till {{toList}}',
userMovedThisCardFromListToList: userMovedThisCardFromListToList:
'<0>{{user}}</0><1> flyttade detta kort från {{fromList}} till {{toList}}</1>', '<0>{{user}}</0> flyttade detta kort från {{fromList}} till {{toList}}',
username: 'Användarnamn', username: 'Användarnamn',
users: 'Användare', users: 'Användare',
writeComment: 'Skriv en kommentar...', writeComment: 'Skriv en kommentar...',

View file

@ -134,13 +134,13 @@ export default {
time: 'zaman', time: 'zaman',
title: 'başlık', title: 'başlık',
userActions_title: 'Kullanıcı İşlemleri', userActions_title: 'Kullanıcı İşlemleri',
userAddedThisCardToList: '<0>{{user}}</0><1> bu kartı {{list}</1> listesine ekledi', userAddedThisCardToList: '<0>{{user}}</0> bu kartı {{list}} listesine ekledi',
userLeftNewCommentToCard: userLeftNewCommentToCard:
'<0>{{user}}</0> yeni bir yorum yazdı: <2>{{card}</2> kartına «{{comment}}»', '<0>{{user}}</0> yeni bir yorum yazdı: <2>{{card}</2> kartına «{{comment}}»',
userMovedCardFromListToList: userMovedCardFromListToList:
'<0>{{user}}</0>, <2>{{card}></2> kartını {{fromList}} listesinden {{toList}} listesine taşıdı', '<0>{{user}}</0>, <2>{{card}></2> kartını {{fromList}} listesinden {{toList}} listesine taşıdı',
userMovedThisCardFromListToList: userMovedThisCardFromListToList:
'<0>{{user}}</0><1> bu kartı {{fromList}} konumundan {{toList}}</1> konumuna taşıdı', '<0>{{user}}</0> bu kartı {{fromList}} konumundan {{toList}} konumuna taşıdı',
username: 'kullanıcı adı', username: 'kullanıcı adı',
users: 'kullanıcı', users: 'kullanıcı',
writeComment: 'Yorum yazın...', writeComment: 'Yorum yazın...',

View file

@ -281,13 +281,13 @@ export default {
unsavedChanges: 'Незбережені зміни', unsavedChanges: 'Незбережені зміни',
uploadedImages: 'Завантажені зображення', uploadedImages: 'Завантажені зображення',
userActions_title: 'Дії користувача', userActions_title: 'Дії користувача',
userAddedThisCardToList: '<0>{{user}}</0><1> додав(ла) цю картку до {{list}}</1>', userAddedThisCardToList: '<0>{{user}}</0> додав(ла) цю картку до {{list}}',
userLeftNewCommentToCard: userLeftNewCommentToCard:
'<0>{{user}}</0> залишив(ла) новий коментар «{{comment}}» до <2>{{card}}</2>', '<0>{{user}}</0> залишив(ла) новий коментар «{{comment}}» до <2>{{card}}</2>',
userMovedCardFromListToList: userMovedCardFromListToList:
'<0>{{user}}</0> перемістив(ла) <2>{{card}}</2> з {{fromList}} в {{toList}}', '<0>{{user}}</0> перемістив(ла) <2>{{card}}</2> з {{fromList}} в {{toList}}',
userMovedThisCardFromListToList: userMovedThisCardFromListToList:
'<0>{{user}}</0><1> перемістив(ла) цю картку з {{fromList}} в {{toList}}</1>', '<0>{{user}}</0> перемістив(ла) цю картку з {{fromList}} в {{toList}}',
username: "Ім'я користувача", username: "Ім'я користувача",
users: 'Користувачі', users: 'Користувачі',
viewer: 'Переглядач', viewer: 'Переглядач',

View file

@ -130,13 +130,13 @@ export default {
time: 'Vaqt', time: 'Vaqt',
title: 'Sarlavha', title: 'Sarlavha',
userActions_title: 'Foydalanuvchi Amallari', userActions_title: 'Foydalanuvchi Amallari',
userAddedThisCardToList: "<1>Ushbu kartani {{list}} ga</1><0>{{user}}</0> qo'shdi", userAddedThisCardToList: "Ushbu kartani {{list}} ga<0>{{user}}</0> qo'shdi",
userLeftNewCommentToCard: userLeftNewCommentToCard:
'<0>{{user}}</0> <2>{{card}}</2> ga yangi izoh qoldirdi «{{comment}}»', '<0>{{user}}</0> <2>{{card}}</2> ga yangi izoh qoldirdi «{{comment}}»',
userMovedCardFromListToList: userMovedCardFromListToList:
"<0>{{user}}</0> <2>{{card}}</2> ni {{fromList}} dan {{toList}} ga ko'chirdi", "<0>{{user}}</0> <2>{{card}}</2> ni {{fromList}} dan {{toList}} ga ko'chirdi",
userMovedThisCardFromListToList: userMovedThisCardFromListToList:
"<0>{{user}}</0><1> ushbu kartani {{fromList}} dan {{toList}}</1> ga ko'chirdi", "<0>{{user}}</0> ushbu kartani {{fromList}} dan {{toList}} ga ko'chirdi",
username: 'Foydalanuvchi nomi', username: 'Foydalanuvchi nomi',
users: 'Foydalanuvchilar', users: 'Foydalanuvchilar',
writeComment: 'Izoh yozish...', writeComment: 'Izoh yozish...',

View file

@ -143,12 +143,11 @@ export default {
time: '时间', time: '时间',
title: '标题', title: '标题',
userActions_title: '用户操作', userActions_title: '用户操作',
userAddedThisCardToList: '<0>{{user}}</0><1> 向列表 {{list}} 添加了该卡片</1>', userAddedThisCardToList: '<0>{{user}}</0> 向列表 {{list}} 添加了该卡片',
userLeftNewCommentToCard: '<0>{{user}}</0> 给 {{card}} 添加了一个新评论 «{{comment}}»', userLeftNewCommentToCard: '<0>{{user}}</0> 给 <2>{{card}}</2> 添加了一个新评论 «{{comment}}»',
userMovedCardFromListToList: userMovedCardFromListToList:
'<0>{{user}}</0> 将卡片 <2>{{card}}</2> 从 {{fromList}} 移动到 {{toList}}', '<0>{{user}}</0> 将卡片 <2>{{card}}</2> 从 {{fromList}} 移动到 {{toList}}',
userMovedThisCardFromListToList: userMovedThisCardFromListToList: '<0>{{user}}</0> 将该卡片从 {{fromList}} 移动到 {{toList}}',
'<0>{{user}}</0><1> 将该卡片从 {{fromList}} 移动到 {{toList}}</1>',
username: '用户名', username: '用户名',
users: '用户', users: '用户',
viewer: '视图', viewer: '视图',

View file

@ -143,12 +143,12 @@ export default {
time: '時間', time: '時間',
title: '標題', title: '標題',
userActions_title: '使用者操作', userActions_title: '使用者操作',
userAddedThisCardToList: '<0>{{user}}</0><1> 向列表 {{list}} 添加了該卡片</1>', userAddedThisCardToList: '<0>{{user}}</0> 向列表 {{list}} 添加了該卡片',
userLeftNewCommentToCard: '<0>{{user}}</0> 給 {{card}} 添加了一條新評論 「{{comment}}」', userLeftNewCommentToCard:
'<0>{{user}}</0> 給 <2>{{card}}</2> 添加了一條新評論 「{{comment}}」',
userMovedCardFromListToList: userMovedCardFromListToList:
'<0>{{user}}</0> 將卡片 <2>{{card}}</2> 從 {{fromList}} 移動到 {{toList}}', '<0>{{user}}</0> 將卡片 <2>{{card}}</2> 從 {{fromList}} 移動到 {{toList}}',
userMovedThisCardFromListToList: userMovedThisCardFromListToList: '<0>{{user}}</0> 將該卡片從 {{fromList}} 移動到 {{toList}}',
'<0>{{user}}</0><1> 將該卡片從 {{fromList}} 移動到 {{toList}}</1>',
username: '使用者名稱', username: '使用者名稱',
users: '使用者', users: '使用者',
viewer: '檢視', viewer: '檢視',

View file

@ -18,6 +18,11 @@ export default class extends BaseModel {
createdAt: attr({ createdAt: attr({
getDefault: () => new Date(), getDefault: () => new Date(),
}), }),
boardId: fk({
to: 'Board',
as: 'board',
relatedName: 'activities',
}),
cardId: fk({ cardId: fk({
to: 'Card', to: 'Card',
as: 'card', as: 'card',
@ -37,7 +42,8 @@ export default class extends BaseModel {
break; break;
case ActionTypes.LIST_CARDS_MOVE__SUCCESS: case ActionTypes.LIST_CARDS_MOVE__SUCCESS:
case ActionTypes.ACTIVITIES_FETCH__SUCCESS: case ActionTypes.ACTIVITIES_IN_BOARD_FETCH__SUCCESS:
case ActionTypes.ACTIVITIES_IN_CARD_FETCH__SUCCESS:
payload.activities.forEach((activity) => { payload.activities.forEach((activity) => {
Activity.upsert(activity); Activity.upsert(activity);
}); });

View file

@ -9,6 +9,7 @@ import BaseModel from './BaseModel';
import buildSearchParts from '../utils/build-search-parts'; import buildSearchParts from '../utils/build-search-parts';
import { isListFinite } from '../utils/record-helpers'; import { isListFinite } from '../utils/record-helpers';
import ActionTypes from '../constants/ActionTypes'; import ActionTypes from '../constants/ActionTypes';
import Config from '../constants/Config';
import { BoardContexts, BoardViews } from '../constants/Enums'; import { BoardContexts, BoardViews } from '../constants/Enums';
const prepareFetchedBoard = (board) => ({ const prepareFetchedBoard = (board) => ({
@ -39,6 +40,15 @@ export default class extends BaseModel {
isFetching: attr({ isFetching: attr({
getDefault: () => null, getDefault: () => null,
}), }),
lastActivityId: attr({
getDefault: () => null,
}),
isActivitiesFetching: attr({
getDefault: () => false,
}),
isAllActivitiesFetched: attr({
getDefault: () => null,
}),
projectId: fk({ projectId: fk({
to: 'Project', to: 'Project',
as: 'project', as: 'project',
@ -241,6 +251,22 @@ export default class extends BaseModel {
case ActionTypes.LABEL_FROM_BOARD_FILTER_REMOVE: case ActionTypes.LABEL_FROM_BOARD_FILTER_REMOVE:
Board.withId(payload.boardId).filterLabels.remove(payload.id); Board.withId(payload.boardId).filterLabels.remove(payload.id);
break;
case ActionTypes.ACTIVITIES_IN_BOARD_FETCH:
Board.withId(payload.boardId).update({
isActivitiesFetching: true,
});
break;
case ActionTypes.ACTIVITIES_IN_BOARD_FETCH__SUCCESS:
Board.withId(payload.boardId).update({
isActivitiesFetching: false,
isAllActivitiesFetched: payload.activities.length < Config.ACTIVITIES_LIMIT,
...(payload.activities.length > 0 && {
lastActivityId: payload.activities[payload.activities.length - 1].id,
}),
});
break; break;
default: default:
} }
@ -266,6 +292,10 @@ export default class extends BaseModel {
return this.customFieldGroups.orderBy(['position', 'id.length', 'id']); return this.customFieldGroups.orderBy(['position', 'id.length', 'id']);
} }
getActivitiesQuerySet() {
return this.activities.orderBy(['id.length', 'id'], ['desc', 'desc']);
}
getUnreadNotificationsQuerySet() { getUnreadNotificationsQuerySet() {
return this.notifications.filter({ return this.notifications.filter({
isRead: false, isRead: false,
@ -347,6 +377,30 @@ export default class extends BaseModel {
return cardModels; return cardModels;
} }
getActivitiesModelArray() {
if (this.isAllActivitiesFetched === null) {
return [];
}
const activityModels = this.getActivitiesQuerySet().toModelArray();
if (this.lastActivityId && this.isAllActivitiesFetched === false) {
return activityModels.filter((activityModel) => {
if (activityModel.id.length > this.lastActivityId.length) {
return true;
}
if (activityModel.id.length < this.lastActivityId.length) {
return false;
}
return activityModel.id >= this.lastActivityId;
});
}
return activityModels;
}
hasMembershipWithUserId(userId) { hasMembershipWithUserId(userId) {
return this.memberships return this.memberships
.filter({ .filter({

View file

@ -402,13 +402,13 @@ export default class extends BaseModel {
}); });
break; break;
case ActionTypes.ACTIVITIES_FETCH: case ActionTypes.ACTIVITIES_IN_CARD_FETCH:
Card.withId(payload.cardId).update({ Card.withId(payload.cardId).update({
isActivitiesFetching: true, isActivitiesFetching: true,
}); });
break; break;
case ActionTypes.ACTIVITIES_FETCH__SUCCESS: case ActionTypes.ACTIVITIES_IN_CARD_FETCH__SUCCESS:
Card.withId(payload.cardId).update({ Card.withId(payload.cardId).update({
isActivitiesFetching: false, isActivitiesFetching: false,
isAllActivitiesFetched: payload.activities.length < Config.ACTIVITIES_LIMIT, isAllActivitiesFetched: payload.activities.length < Config.ACTIVITIES_LIMIT,
@ -607,7 +607,6 @@ export default class extends BaseModel {
this.customFieldValues.delete(); this.customFieldValues.delete();
this.comments.delete(); this.comments.delete();
this.activities.delete();
} }
deleteWithClearable() { deleteWithClearable() {

View file

@ -295,7 +295,8 @@ export default class extends BaseModel {
case ActionTypes.CARD_CREATE_HANDLE: case ActionTypes.CARD_CREATE_HANDLE:
case ActionTypes.COMMENTS_FETCH__SUCCESS: case ActionTypes.COMMENTS_FETCH__SUCCESS:
case ActionTypes.COMMENT_CREATE_HANDLE: case ActionTypes.COMMENT_CREATE_HANDLE:
case ActionTypes.ACTIVITIES_FETCH__SUCCESS: case ActionTypes.ACTIVITIES_IN_BOARD_FETCH__SUCCESS:
case ActionTypes.ACTIVITIES_IN_CARD_FETCH__SUCCESS:
case ActionTypes.NOTIFICATION_CREATE_HANDLE: case ActionTypes.NOTIFICATION_CREATE_HANDLE:
payload.users.forEach((user) => { payload.users.forEach((user) => {
User.upsert(user); User.upsert(user);

View file

@ -10,10 +10,10 @@ import selectors from '../../../selectors';
import actions from '../../../actions'; import actions from '../../../actions';
import api from '../../../api'; import api from '../../../api';
export function* fetchActivities(cardId) { export function* fetchActivitiesInBoard(boardId) {
const { lastActivityId } = yield select(selectors.selectCardById, cardId); const { lastActivityId } = yield select(selectors.selectBoardById, boardId);
yield put(actions.fetchActivities(cardId)); yield put(actions.fetchActivitiesInBoard(boardId));
let activities; let activities;
let users; let users;
@ -22,21 +22,50 @@ export function* fetchActivities(cardId) {
({ ({
items: activities, items: activities,
included: { users }, included: { users },
} = yield call(request, api.getActivities, cardId, { } = yield call(request, api.getActivitiesInBoard, boardId, {
beforeId: lastActivityId || undefined, beforeId: lastActivityId || undefined,
})); }));
} catch (error) { } catch (error) {
yield put(actions.fetchActivities.failure(cardId, error)); yield put(actions.fetchActivitiesInBoard.failure(boardId, error));
return; return;
} }
yield put(actions.fetchActivities.success(cardId, activities, users)); yield put(actions.fetchActivitiesInBoard.success(boardId, activities, users));
}
export function* fetchActivitiesInCurrentBoard() {
const { boardId } = yield select(selectors.selectPath);
yield call(fetchActivitiesInBoard, boardId);
}
export function* fetchActivitiesInCard(cardId) {
const { lastActivityId } = yield select(selectors.selectCardById, cardId);
yield put(actions.fetchActivitiesInCard(cardId));
let activities;
let users;
try {
({
items: activities,
included: { users },
} = yield call(request, api.getActivitiesInCard, cardId, {
beforeId: lastActivityId || undefined,
}));
} catch (error) {
yield put(actions.fetchActivitiesInCard.failure(cardId, error));
return;
}
yield put(actions.fetchActivitiesInCard.success(cardId, activities, users));
} }
export function* fetchActivitiesInCurrentCard() { export function* fetchActivitiesInCurrentCard() {
const { cardId } = yield select(selectors.selectPath); const { cardId } = yield select(selectors.selectPath);
yield call(fetchActivities, cardId); yield call(fetchActivitiesInCard, cardId);
} }
export function* handleActivityCreate(activity) { export function* handleActivityCreate(activity) {
@ -44,7 +73,9 @@ export function* handleActivityCreate(activity) {
} }
export default { export default {
fetchActivities, fetchActivitiesInBoard,
fetchActivitiesInCurrentBoard,
fetchActivitiesInCard,
fetchActivitiesInCurrentCard, fetchActivitiesInCurrentCard,
handleActivityCreate, handleActivityCreate,
}; };

View file

@ -10,6 +10,9 @@ import EntryActionTypes from '../../../constants/EntryActionTypes';
export default function* activitiesWatchers() { export default function* activitiesWatchers() {
yield all([ yield all([
takeEvery(EntryActionTypes.ACTIVITIES_IN_CURRENT_BOARD_FETCH, () =>
services.fetchActivitiesInCurrentBoard(),
),
takeEvery(EntryActionTypes.ACTIVITIES_IN_CURRENT_CARD_FETCH, () => takeEvery(EntryActionTypes.ACTIVITIES_IN_CURRENT_CARD_FETCH, () =>
services.fetchActivitiesInCurrentCard(), services.fetchActivitiesInCurrentCard(),
), ),

View file

@ -383,6 +383,24 @@ export const selectCustomFieldGroupsForCurrentBoard = createSelector(
}, },
); );
export const selectActivityIdsForCurrentBoard = createSelector(
orm,
(state) => selectPath(state).boardId,
({ Board }, id) => {
if (!id) {
return id;
}
const boardModel = Board.withId(id);
if (!boardModel) {
return boardModel;
}
return boardModel.getActivitiesModelArray().map((activity) => activity.id);
},
);
export const selectFilterUserIdsForCurrentBoard = createSelector( export const selectFilterUserIdsForCurrentBoard = createSelector(
orm, orm,
(state) => selectPath(state).boardId, (state) => selectPath(state).boardId,
@ -447,6 +465,7 @@ export default {
selectFilteredCardIdsForCurrentBoard, selectFilteredCardIdsForCurrentBoard,
selectCustomFieldGroupIdsForCurrentBoard, selectCustomFieldGroupIdsForCurrentBoard,
selectCustomFieldGroupsForCurrentBoard, selectCustomFieldGroupsForCurrentBoard,
selectActivityIdsForCurrentBoard,
selectFilterUserIdsForCurrentBoard, selectFilterUserIdsForCurrentBoard,
selectFilterLabelIdsForCurrentBoard, selectFilterLabelIdsForCurrentBoard,
selectIsBoardWithIdExists, selectIsBoardWithIdExists,

View file

@ -0,0 +1,68 @@
/*!
* Copyright (c) 2024 PLANKA Software GmbH
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
const { idInput } = require('../../../utils/inputs');
const Errors = {
BOARD_NOT_FOUND: {
boardNotFound: 'Board not found',
},
};
module.exports = {
inputs: {
boardId: {
...idInput,
required: true,
},
beforeId: idInput,
},
exits: {
boardNotFound: {
responseType: 'notFound',
},
},
async fn(inputs) {
const { currentUser } = this.req;
const { board, project } = await sails.helpers.boards
.getPathToProjectById(inputs.boardId)
.intercept('pathNotFound', () => Errors.BOARD_NOT_FOUND);
const boardMembership = await BoardMembership.qm.getOneByBoardIdAndUserId(
board.id,
currentUser.id,
);
if (!boardMembership) {
if (currentUser.role !== User.Roles.ADMIN || project.ownerProjectManagerId) {
const isProjectManager = await sails.helpers.users.isProjectManager(
currentUser.id,
project.id,
);
if (!isProjectManager) {
throw Errors.BOARD_NOT_FOUND; // Forbidden
}
}
}
const actions = await Action.qm.getByBoardId(board.id, {
beforeId: inputs.beforeId,
});
const userIds = sails.helpers.utils.mapRecords(actions, 'userId', true, true);
const users = await User.qm.getByIds(userIds);
return {
items: actions,
included: {
users: sails.helpers.users.presentMany(users, currentUser),
},
};
},
};

View file

@ -115,6 +115,7 @@ module.exports = {
const action = await Action.qm.createOne({ const action = await Action.qm.createOne({
...values, ...values,
boardId: values.card.boardId,
cardId: values.card.id, cardId: values.card.id,
userId: values.user.id, userId: values.user.id,
}); });
@ -149,10 +150,7 @@ module.exports = {
values: { values: {
action, action,
type: action.type, type: action.type,
data: { data: action.data,
...action.data,
card: _.pick(values.card, ['name']),
},
userId: action.data.user.id, userId: action.data.user.id,
creatorUser: values.user, creatorUser: values.user,
card: values.card, card: values.card,
@ -182,10 +180,7 @@ module.exports = {
userId, userId,
action, action,
type: action.type, type: action.type,
data: { data: action.data,
...action.data,
card: _.pick(values.card, ['name']),
},
creatorUser: values.user, creatorUser: values.user,
card: values.card, card: values.card,
}, },

View file

@ -35,6 +35,15 @@ module.exports = {
await sails.helpers.lists.deleteRelated(lists); await sails.helpers.lists.deleteRelated(lists);
await Action.qm.update(
{
boardId: boardIdOrIds,
},
{
boardId: null,
},
);
await NotificationService.qm.delete({ await NotificationService.qm.delete({
boardId: boardIdOrIds, boardId: boardIdOrIds,
}); });

View file

@ -110,6 +110,7 @@ module.exports = {
type: Action.Types.ADD_MEMBER_TO_CARD, type: Action.Types.ADD_MEMBER_TO_CARD,
data: { data: {
user: _.pick(values.user, ['id', 'name']), user: _.pick(values.user, ['id', 'name']),
card: _.pick(values.card, ['name']),
}, },
user: inputs.actorUser, user: inputs.actorUser,
card: values.card, card: values.card,

View file

@ -86,6 +86,7 @@ module.exports = {
type: Action.Types.REMOVE_MEMBER_FROM_CARD, type: Action.Types.REMOVE_MEMBER_FROM_CARD,
data: { data: {
user: _.pick(inputs.user, ['id', 'name']), user: _.pick(inputs.user, ['id', 'name']),
card: _.pick(inputs.card, ['name']),
}, },
user: inputs.actorUser, user: inputs.actorUser,
card: inputs.card, card: inputs.card,

View file

@ -124,6 +124,7 @@ module.exports = {
card, card,
type: Action.Types.CREATE_CARD, type: Action.Types.CREATE_CARD,
data: { data: {
card: _.pick(card, ['name']),
list: _.pick(values.list, ['id', 'type', 'name']), list: _.pick(values.list, ['id', 'type', 'name']),
}, },
user: values.creatorUser, user: values.creatorUser,

View file

@ -276,6 +276,7 @@ module.exports = {
card, card,
type: Action.Types.CREATE_CARD, // TODO: introduce separate type? type: Action.Types.CREATE_CARD, // TODO: introduce separate type?
data: { data: {
card: _.pick(card, ['name']),
list: _.pick(inputs.list, ['id', 'type', 'name']), list: _.pick(inputs.list, ['id', 'type', 'name']),
}, },
user: values.creatorUser, user: values.creatorUser,

View file

@ -463,6 +463,7 @@ module.exports = {
card, card,
type: Action.Types.MOVE_CARD, type: Action.Types.MOVE_CARD,
data: { data: {
card: _.pick(card, ['name']),
fromList: _.pick(inputs.list, ['id', 'type', 'name']), fromList: _.pick(inputs.list, ['id', 'type', 'name']),
toList: _.pick(values.list, ['id', 'type', 'name']), toList: _.pick(values.list, ['id', 'type', 'name']),
}, },

View file

@ -138,6 +138,7 @@ module.exports = {
values: { values: {
type: task.isCompleted ? Action.Types.COMPLETE_TASK : Action.Types.UNCOMPLETE_TASK, type: task.isCompleted ? Action.Types.COMPLETE_TASK : Action.Types.UNCOMPLETE_TASK,
data: { data: {
card: _.pick(inputs.card, ['name']),
task: _.pick(task, ['id', 'name']), task: _.pick(task, ['id', 'name']),
}, },
user: inputs.actorUser, user: inputs.actorUser,

View file

@ -11,6 +11,20 @@ const create = (arrayOfValues) => Action.createEach(arrayOfValues).fetch();
const createOne = (values) => Action.create({ ...values }).fetch(); const createOne = (values) => Action.create({ ...values }).fetch();
const getByBoardId = (boardId, { beforeId } = {}) => {
const criteria = {
boardId,
};
if (beforeId) {
criteria.id = {
'<': beforeId,
};
}
return Action.find(criteria).sort('id DESC').limit(LIMIT);
};
const getByCardId = (cardId, { beforeId } = {}) => { const getByCardId = (cardId, { beforeId } = {}) => {
const criteria = { const criteria = {
cardId, cardId,
@ -33,6 +47,7 @@ const delete_ = (criteria) => Action.destroy(criteria).fetch();
module.exports = { module.exports = {
create, create,
createOne, createOne,
getByBoardId,
getByCardId, getByCardId,
update, update,
delete: delete_, delete: delete_,

View file

@ -52,6 +52,10 @@ module.exports = {
// ╠═╣╚═╗╚═╗║ ║║ ║╠═╣ ║ ║║ ║║║║╚═╗ // ╠═╣╚═╗╚═╗║ ║║ ║╠═╣ ║ ║║ ║║║║╚═╗
// ╩ ╩╚═╝╚═╝╚═╝╚═╝╩╩ ╩ ╩ ╩╚═╝╝╚╝╚═╝ // ╩ ╩╚═╝╚═╝╚═╝╚═╝╩╩ ╩ ╩ ╩╚═╝╝╚╝╚═╝
boardId: {
model: 'Board',
columnName: 'board_id',
},
cardId: { cardId: {
model: 'Card', model: 'Card',
required: true, required: true,

View file

@ -163,7 +163,8 @@ module.exports.routes = {
'PATCH /api/comments/:id': 'comments/update', 'PATCH /api/comments/:id': 'comments/update',
'DELETE /api/comments/:id': 'comments/delete', 'DELETE /api/comments/:id': 'comments/delete',
'GET /api/cards/:cardId/actions': 'actions/index', 'GET /api/boards/:boardId/actions': 'actions/index-in-board',
'GET /api/cards/:cardId/actions': 'actions/index-in-card',
'GET /api/notifications': 'notifications/index', 'GET /api/notifications': 'notifications/index',
'GET /api/notifications/:id': 'notifications/show', 'GET /api/notifications/:id': 'notifications/show',

View file

@ -0,0 +1,30 @@
/*!
* Copyright (c) 2024 PLANKA Software GmbH
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
exports.up = async (knex) => {
await knex.schema.alterTable('action', (table) => {
/* Columns */
table.bigInteger('board_id');
/* Indexes */
table.index('board_id');
});
return knex.raw(`
UPDATE action
SET
board_id = card.board_id,
data = data || jsonb_build_object('card', jsonb_build_object('name', card.name))
FROM card
WHERE action.card_id = card.id;
`);
};
exports.down = (knex) =>
knex.schema.table('action', (table) => {
table.dropColumn('board_id');
});