mirror of
https://github.com/plankanban/planka.git
synced 2025-12-26 09:15:01 +03:00
129
client/src/components/notifications/NotificationsStep/Item.jsx
Normal file
129
client/src/components/notifications/NotificationsStep/Item.jsx
Normal file
@@ -0,0 +1,129 @@
|
||||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
import truncate from 'lodash/truncate';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useTranslation, Trans } from 'react-i18next';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Button } from 'semantic-ui-react';
|
||||
|
||||
import selectors from '../../../selectors';
|
||||
import entryActions from '../../../entry-actions';
|
||||
import Paths from '../../../constants/Paths';
|
||||
import { StaticUserIds } from '../../../constants/StaticUsers';
|
||||
import { NotificationTypes } from '../../../constants/Enums';
|
||||
import UserAvatar from '../../users/UserAvatar';
|
||||
|
||||
import styles from './Item.module.scss';
|
||||
|
||||
const Item = React.memo(({ id, onClose }) => {
|
||||
const selectNotificationById = useMemo(() => selectors.makeSelectNotificationById(), []);
|
||||
const selectCreatorUserById = useMemo(() => selectors.makeSelectUserById(), []);
|
||||
const selectCardById = useMemo(() => selectors.makeSelectCardById(), []);
|
||||
|
||||
const notification = useSelector((state) => selectNotificationById(state, id));
|
||||
|
||||
const creatorUser = useSelector((state) =>
|
||||
selectCreatorUserById(state, notification.creatorUserId),
|
||||
);
|
||||
|
||||
const card = useSelector((state) => selectCardById(state, notification.cardId));
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const [t] = useTranslation();
|
||||
|
||||
const handleDeleteClick = useCallback(() => {
|
||||
dispatch(entryActions.deleteNotification(id));
|
||||
}, [id, dispatch]);
|
||||
|
||||
const creatorUserName =
|
||||
creatorUser.id === StaticUserIds.DELETED
|
||||
? t(`common.${creatorUser.name}`, {
|
||||
context: 'title',
|
||||
})
|
||||
: creatorUser.name;
|
||||
|
||||
const cardName = card ? card.name : notification.data.card.name;
|
||||
|
||||
let contentNode;
|
||||
switch (notification.type) {
|
||||
case NotificationTypes.MOVE_CARD: {
|
||||
const { fromList, toList } = notification.data;
|
||||
|
||||
const fromListName = fromList.name || t(`common.${fromList.type}`);
|
||||
const toListName = toList.name || t(`common.${toList.type}`);
|
||||
|
||||
contentNode = (
|
||||
<Trans
|
||||
i18nKey="common.userMovedCardFromListToList"
|
||||
values={{
|
||||
user: creatorUserName,
|
||||
card: cardName,
|
||||
fromList: fromListName,
|
||||
toList: toListName,
|
||||
}}
|
||||
>
|
||||
{creatorUserName}
|
||||
{' moved '}
|
||||
<Link to={Paths.CARDS.replace(':id', notification.cardId)} onClick={onClose}>
|
||||
{cardName}
|
||||
</Link>
|
||||
{' from '}
|
||||
{fromListName}
|
||||
{' to '}
|
||||
{toListName}
|
||||
</Trans>
|
||||
);
|
||||
|
||||
break;
|
||||
}
|
||||
case NotificationTypes.COMMENT_CARD: {
|
||||
const commentText = truncate(notification.data.text);
|
||||
|
||||
contentNode = (
|
||||
<Trans
|
||||
i18nKey="common.userLeftNewCommentToCard"
|
||||
values={{
|
||||
user: creatorUserName,
|
||||
comment: commentText,
|
||||
card: cardName,
|
||||
}}
|
||||
>
|
||||
{creatorUserName}
|
||||
{` left a new comment «${commentText}» to `}
|
||||
<Link to={Paths.CARDS.replace(':id', notification.cardId)} onClick={onClose}>
|
||||
{cardName}
|
||||
</Link>
|
||||
</Trans>
|
||||
);
|
||||
|
||||
break;
|
||||
}
|
||||
default:
|
||||
contentNode = null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<UserAvatar id={notification.creatorUserId} size="large" />
|
||||
<span className={styles.content}>{contentNode}</span>
|
||||
<Button
|
||||
type="button"
|
||||
icon="trash alternate outline"
|
||||
className={styles.button}
|
||||
onClick={handleDeleteClick}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
Item.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default Item;
|
||||
@@ -0,0 +1,42 @@
|
||||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
:global(#app) {
|
||||
.button {
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
float: right;
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
margin: 0;
|
||||
min-height: auto;
|
||||
padding: 0;
|
||||
transition: background 0.3s ease;
|
||||
width: 20px;
|
||||
|
||||
&:hover {
|
||||
background: #e9e9e9;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
display: inline-block;
|
||||
font-size: 12px;
|
||||
min-height: 36px;
|
||||
overflow: hidden;
|
||||
padding: 0 4px 0 8px;
|
||||
vertical-align: top;
|
||||
width: calc(100% - 56px);
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
padding: 12px;
|
||||
|
||||
&:hover {
|
||||
background: #f0f0f0;
|
||||
}
|
||||
}
|
||||
}
|
||||
65
client/src/components/notifications/NotificationsStep/NotificationsStep.jsx
Executable file
65
client/src/components/notifications/NotificationsStep/NotificationsStep.jsx
Executable file
@@ -0,0 +1,65 @@
|
||||
/*!
|
||||
* 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 PropTypes from 'prop-types';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button } from 'semantic-ui-react';
|
||||
import { Popup } from '../../../lib/custom-ui';
|
||||
|
||||
import selectors from '../../../selectors';
|
||||
import entryActions from '../../../entry-actions';
|
||||
import Item from './Item';
|
||||
|
||||
import styles from './NotificationsStep.module.scss';
|
||||
|
||||
const NotificationsStep = React.memo(({ onClose }) => {
|
||||
const notificationIds = useSelector(selectors.selectNotificationIdsForCurrentUser);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const [t] = useTranslation();
|
||||
|
||||
const handleDeleteAllClick = useCallback(() => {
|
||||
dispatch(entryActions.deleteAllNotifications());
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Popup.Header>
|
||||
{t('common.notifications', {
|
||||
context: 'title',
|
||||
})}
|
||||
</Popup.Header>
|
||||
<Popup.Content>
|
||||
{notificationIds.length > 0 ? (
|
||||
<>
|
||||
<div className={styles.items}>
|
||||
{notificationIds.map((notificationId) => (
|
||||
<Item key={notificationId} id={notificationId} onClose={onClose} />
|
||||
))}
|
||||
</div>
|
||||
{notificationIds.length > 1 && (
|
||||
<Button
|
||||
fluid
|
||||
content={t('action.dismissAll')}
|
||||
className={styles.deleteAllButton}
|
||||
onClick={handleDeleteAllClick}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
t('common.noUnreadNotifications')
|
||||
)}
|
||||
</Popup.Content>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
NotificationsStep.propTypes = {
|
||||
onClose: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default NotificationsStep;
|
||||
@@ -0,0 +1,46 @@
|
||||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
:global(#app) {
|
||||
.deleteAllButton {
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
color: #6b808c;
|
||||
font-weight: normal;
|
||||
margin-top: 8px;
|
||||
padding: 6px 11px;
|
||||
text-decoration: underline;
|
||||
transition: none;
|
||||
|
||||
&:hover {
|
||||
background: rgba(9, 30, 66, 0.08);
|
||||
color: #092d42;
|
||||
}
|
||||
}
|
||||
|
||||
.items {
|
||||
margin: 0 -12px;
|
||||
max-height: 60vh;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
|
||||
@supports (-moz-appearance: none) {
|
||||
scrollbar-color: rgba(0, 0, 0, 0.32) transparent;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 5px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
border-radius: 3px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 NotificationsStep from './NotificationsStep';
|
||||
|
||||
export default NotificationsStep;
|
||||
Reference in New Issue
Block a user