mirror of
https://github.com/plankanban/planka.git
synced 2025-07-18 20:59:44 +02:00
feat: add page to view all cards assigned to current user #264
This commit is contained in:
parent
e6ab4e2370
commit
8c2de439d4
15 changed files with 329 additions and 3 deletions
|
@ -77,6 +77,13 @@ const ProjectContent = React.memo(({ cardId }) => {
|
|||
|
||||
const { listName, withCreator } = useSelector((state) => {
|
||||
const board = selectors.selectCurrentBoard(state);
|
||||
|
||||
if (!board) {
|
||||
return {
|
||||
listName: null,
|
||||
withCreator: false,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
listName: list.name && (board.view === BoardViews.KANBAN ? null : list.name),
|
||||
|
|
|
@ -45,7 +45,7 @@ const CardModal = React.memo(() => {
|
|||
const dispatch = useDispatch();
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
dispatch(push(Paths.BOARDS.replace(':id', card.boardId)));
|
||||
window.history.back();
|
||||
}, [card.boardId, dispatch]);
|
||||
|
||||
const [ClosableModal, isClosableActiveRef] = useClosableModal();
|
||||
|
|
|
@ -16,6 +16,7 @@ import Paths from '../../constants/Paths';
|
|||
import Login from './Login';
|
||||
import Core from './Core';
|
||||
import NotFound from './NotFound';
|
||||
import UserCardsPage from "../users/UserCardsPage";
|
||||
|
||||
import 'react-datepicker/dist/react-datepicker.css';
|
||||
import 'photoswipe/dist/photoswipe.css';
|
||||
|
@ -37,6 +38,7 @@ function Root({ store, history }) {
|
|||
<Route path={Paths.PROJECTS} element={<Core />} />
|
||||
<Route path={Paths.BOARDS} element={<Core />} />
|
||||
<Route path={Paths.CARDS} element={<Core />} />
|
||||
<Route path={Paths.USERCARDS} element={<UserCardsPage />} />
|
||||
<Route path="*" element={<NotFound />} />
|
||||
</Routes>
|
||||
</ToasterProvider>
|
||||
|
|
136
client/src/components/users/UserCardsPage/UserCardsPage.jsx
Executable file
136
client/src/components/users/UserCardsPage/UserCardsPage.jsx
Executable file
|
@ -0,0 +1,136 @@
|
|||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Loader } from 'semantic-ui-react';
|
||||
import { DragDropContext, Droppable } from 'react-beautiful-dnd';
|
||||
|
||||
import DroppableTypes from '../../../constants/DroppableTypes';
|
||||
import DraggableCard from '../../cards/DraggableCard';
|
||||
import Header from '../../common/Header';
|
||||
import selectors from '../../../selectors';
|
||||
import entryActions from '../../../entry-actions';
|
||||
|
||||
import styles from './UserCardsPage.module.scss';
|
||||
|
||||
function UserCardsPage() {
|
||||
const [t] = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
const loading = useRef(false);
|
||||
|
||||
const currentUserId = useSelector(selectors.selectCurrentUserId);
|
||||
const user = useSelector(selectors.selectCurrentUser);
|
||||
const projectsToCard = useSelector(selectors.selectProjectsToCardsWithEditorRightsForCurrentUser);
|
||||
|
||||
console.log('Projects to Cards Data:', projectsToCard);
|
||||
|
||||
const isDataReady = currentUserId && user && projectsToCard !== null;
|
||||
|
||||
const handleDragEnd = (result) => {
|
||||
if (!result.destination) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { source, destination, draggableId } = result;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!currentUserId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (loading.current || !isDataReady) return;
|
||||
|
||||
if (projectsToCard && projectsToCard.length > 0) {
|
||||
projectsToCard.forEach((project) => {
|
||||
project.boards?.forEach((board) => {
|
||||
dispatch(entryActions.fetchBoard(board.id));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
loading.current = true;
|
||||
}, [currentUserId, projectsToCard, dispatch, isDataReady]);
|
||||
|
||||
if (!isDataReady) {
|
||||
return (
|
||||
<>
|
||||
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100vh' }}>
|
||||
<Loader active size="massive" />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<div className={styles.container}>
|
||||
<h1 className={styles.title}>{t('common.myCards')}</h1>
|
||||
{projectsToCard && projectsToCard.length > 0 ? (
|
||||
<DragDropContext onDragEnd={handleDragEnd}>
|
||||
<div className={styles.projectsContainer}>
|
||||
{projectsToCard.map((project) => (
|
||||
<div key={project.id} className={styles.projectSection}>
|
||||
<h2 className={styles.projectTitle}>{project.name}</h2>
|
||||
|
||||
{project.boards && project.boards.length > 0 ? (
|
||||
project.boards.map((board) => (
|
||||
<div key={board.id} className={styles.boardSection}>
|
||||
<h3 className={styles.boardTitle}>{board.name}</h3>
|
||||
|
||||
{board.cards && board.cards.length > 0 ? (
|
||||
<Droppable droppableId={`board:${board.id}`} type={DroppableTypes.CARD}>
|
||||
{(provided) => (
|
||||
<div
|
||||
ref={provided.innerRef}
|
||||
data-rbd-droppable-id={`board:${board.id}`}
|
||||
data-rbd-droppable-context-id={
|
||||
provided.droppableProps['data-rbd-droppable-context-id']
|
||||
}
|
||||
className={styles.cardsGrid}
|
||||
>
|
||||
{board.cards.map((card, index) => {
|
||||
const cardId = card.id || card._fields?.id;
|
||||
if (!cardId) {
|
||||
console.warn('Card missing ID:', card);
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={cardId} className={styles.card}>
|
||||
<DraggableCard id={cardId} index={index} />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{provided.placeholder}
|
||||
</div>
|
||||
)}
|
||||
</Droppable>
|
||||
) : (
|
||||
<p className={styles.emptyMessage}>{t('common.noCards')}</p>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<p className={styles.emptyMessage}>{t('common.noBoards')}</p>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</DragDropContext>
|
||||
) : (
|
||||
<div className={styles.emptyState}>
|
||||
<p>{t('common.noProjects')}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default UserCardsPage;
|
96
client/src/components/users/UserCardsPage/UserCardsPage.module.scss
Executable file
96
client/src/components/users/UserCardsPage/UserCardsPage.module.scss
Executable file
|
@ -0,0 +1,96 @@
|
|||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
.container {
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.title {
|
||||
color: #fff;
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
.projectsContainer {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.projectSection {
|
||||
background-color: #333333;
|
||||
border-radius: 4px;
|
||||
padding: 2rem;
|
||||
margin: 1rem;
|
||||
font-family: "Nunitoga", "Helvetica Neue", Arial, Helvetica, sans-serif;
|
||||
}
|
||||
|
||||
.projectTitle {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.boardSection {
|
||||
width: 300px;
|
||||
color: #000;
|
||||
background-color: #dfe3e6;
|
||||
border-radius: 4px;
|
||||
padding: 1rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.boardTitle {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.cardsGrid {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.card {
|
||||
margin-top: 1rem;
|
||||
align-items: center;
|
||||
border-radius: 4px;
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
transform: translateY(-8px);
|
||||
box-shadow: 0 8px 16px rgba(255, 255, 255, 0.562);
|
||||
}
|
||||
|
||||
.cardName {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.emptyMessage {
|
||||
width: 300px;
|
||||
padding: 1rem;
|
||||
margin-top: 1rem;
|
||||
color: #9e0000;
|
||||
font-weight:bold;
|
||||
font-size:large;
|
||||
}
|
||||
|
||||
.emptyState {
|
||||
width: 300px;
|
||||
padding: 1rem;
|
||||
margin-top: 1rem;
|
||||
color: #9e0000;
|
||||
font-weight:bold;
|
||||
font-size: xx-large;
|
||||
}
|
||||
|
||||
/* Media Queries for Responsiveness */
|
||||
/* tablet */
|
||||
@media (max-width: 768px) {
|
||||
|
||||
.projectsContainer{
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/* phone */
|
||||
@media (max-width: 480px) {
|
||||
|
||||
}
|
8
client/src/components/users/UserCardsPage/index.js
Executable file
8
client/src/components/users/UserCardsPage/index.js
Executable 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 UserCards from './UserCardsPage';
|
||||
|
||||
export default UserCards;
|
|
@ -7,6 +7,7 @@ import React, { useCallback } from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Button, Menu } from 'semantic-ui-react';
|
||||
import { Popup } from '../../../lib/custom-ui';
|
||||
|
||||
|
@ -40,6 +41,10 @@ const UserStep = React.memo(({ onClose }) => {
|
|||
onClose();
|
||||
}, [onClose, dispatch]);
|
||||
|
||||
const handleCardsClick = useCallback(() => {
|
||||
onClose();
|
||||
}, [onClose]);
|
||||
|
||||
let logoutMenuItemProps;
|
||||
if (isLogouting) {
|
||||
logoutMenuItemProps = {
|
||||
|
@ -60,6 +65,16 @@ const UserStep = React.memo(({ onClose }) => {
|
|||
</Popup.Header>
|
||||
<Popup.Content>
|
||||
<Menu secondary vertical className={styles.menu}>
|
||||
<Menu.Item
|
||||
as={Link}
|
||||
to="/user-cards"
|
||||
className={styles.menuItem}
|
||||
onClick={handleCardsClick}
|
||||
>
|
||||
{t('common.myCards', {
|
||||
context: 'title',
|
||||
})}
|
||||
</Menu.Item>
|
||||
<Menu.Item className={styles.menuItem} onClick={handleSettingsClick}>
|
||||
{t('common.settings', {
|
||||
context: 'title',
|
||||
|
|
|
@ -9,6 +9,7 @@ const OIDC_CALLBACK = '/oidc-callback';
|
|||
const PROJECTS = '/projects/:id';
|
||||
const BOARDS = '/boards/:id';
|
||||
const CARDS = '/cards/:id';
|
||||
const USERCARDS = '/user-cards';
|
||||
|
||||
export default {
|
||||
ROOT,
|
||||
|
@ -17,4 +18,5 @@ export default {
|
|||
PROJECTS,
|
||||
BOARDS,
|
||||
CARDS,
|
||||
USERCARDS,
|
||||
};
|
||||
|
|
|
@ -209,6 +209,7 @@ export default {
|
|||
memberActions_title: 'Member Actions',
|
||||
minutes: 'Minutes',
|
||||
moveCard_title: 'Move Card',
|
||||
myCards: 'My cards',
|
||||
myOwn_title: 'My Own',
|
||||
name: 'Name',
|
||||
newestFirst: 'Newest first',
|
||||
|
@ -218,6 +219,7 @@ export default {
|
|||
newVersionAvailable: 'New version available',
|
||||
noConnectionToServer: 'No connection to server',
|
||||
noBoards: 'No boards',
|
||||
noCards: 'No cards',
|
||||
noLists: 'No lists',
|
||||
noProjects: 'No projects',
|
||||
notifications: 'Notifications',
|
||||
|
|
|
@ -204,6 +204,7 @@ export default {
|
|||
memberActions_title: 'Member Actions',
|
||||
minutes: 'Minutes',
|
||||
moveCard_title: 'Move Card',
|
||||
myCards: 'My cards',
|
||||
myOwn_title: 'My Own',
|
||||
name: 'Name',
|
||||
newestFirst: 'Newest first',
|
||||
|
@ -213,6 +214,7 @@ export default {
|
|||
newVersionAvailable: 'New version available',
|
||||
noConnectionToServer: 'No connection to server',
|
||||
noBoards: 'No boards',
|
||||
noCards: 'No cards',
|
||||
noLists: 'No lists',
|
||||
noProjects: 'No projects',
|
||||
notifications: 'Notifications',
|
||||
|
|
|
@ -111,6 +111,7 @@ export default {
|
|||
memberActions_title: 'Actions des membres',
|
||||
minutes: 'Minutes',
|
||||
moveCard_title: 'Déplacer la carte',
|
||||
myCards: 'Mes cartes',
|
||||
name: 'Nom',
|
||||
newestFirst: 'Le plus récent en premier',
|
||||
newEmail: 'Nouvel e-mail',
|
||||
|
@ -118,6 +119,7 @@ export default {
|
|||
newUsername: "Nouveau nom d'utilisateur",
|
||||
noConnectionToServer: 'Pas de connexion au serveur',
|
||||
noBoards: 'Pas de tableau',
|
||||
noCards: 'Pas de cartes',
|
||||
noLists: 'Pas de liste',
|
||||
noProjects: 'Pas de projet',
|
||||
notifications: 'Notifications',
|
||||
|
|
|
@ -296,6 +296,10 @@ export default class extends BaseModel {
|
|||
return this.activities.orderBy(['id.length', 'id'], ['desc', 'desc']);
|
||||
}
|
||||
|
||||
getOrderedCardsQuerySet() {
|
||||
return this.cards.orderBy(['position', 'id.length', 'id']);
|
||||
}
|
||||
|
||||
getUnreadNotificationsQuerySet() {
|
||||
return this.notifications.filter({
|
||||
isRead: false,
|
||||
|
@ -313,6 +317,11 @@ export default class extends BaseModel {
|
|||
})
|
||||
.first();
|
||||
}
|
||||
getOrderedCardsModelArrayForUser(userId) {
|
||||
return this.getOrderedCardsQuerySet()
|
||||
.toModelArray()
|
||||
.filter((cardModel) => cardModel.hasMembershipForUser(userId));
|
||||
}
|
||||
|
||||
getCardsModelArray() {
|
||||
return this.getFiniteListsQuerySet()
|
||||
|
|
|
@ -167,7 +167,9 @@ export default class extends BaseModel {
|
|||
});
|
||||
|
||||
payload.cardMemberships.forEach(({ cardId, userId }) => {
|
||||
Card.withId(cardId).users.add(userId);
|
||||
try {
|
||||
Card.withId(cardId).users.add(userId);
|
||||
} catch {} // eslint-disable-line no-empty
|
||||
});
|
||||
|
||||
payload.cardLabels.forEach(({ cardId, labelId }) => {
|
||||
|
@ -510,6 +512,14 @@ export default class extends BaseModel {
|
|||
.exists();
|
||||
}
|
||||
|
||||
hasMembershipForUser(userId) {
|
||||
return this.users
|
||||
.filter({
|
||||
id: userId,
|
||||
})
|
||||
.exists();
|
||||
}
|
||||
|
||||
isAvailableForUser(userModel) {
|
||||
return !!this.list && this.list.isAvailableForUser(userModel);
|
||||
}
|
||||
|
|
|
@ -235,6 +235,41 @@ export const selectProjectsToListsWithEditorRightsForCurrentUser = createSelecto
|
|||
},
|
||||
);
|
||||
|
||||
export const selectProjectsToCardsWithEditorRightsForCurrentUser = createSelector(
|
||||
orm,
|
||||
(state) => selectCurrentUserId(state),
|
||||
({ User }, id) => {
|
||||
if (!id) {
|
||||
return id;
|
||||
}
|
||||
|
||||
const userModel = User.withId(id);
|
||||
|
||||
if (!userModel) {
|
||||
return userModel;
|
||||
}
|
||||
|
||||
return userModel.getMembershipProjectsModelArray().map((projectModel) => ({
|
||||
...projectModel.ref,
|
||||
boards: projectModel.getBoardsModelArrayForUserWithId(id).flatMap((boardModel) => {
|
||||
const boardMembersipModel = boardModel.getMembershipModelByUserId(id);
|
||||
|
||||
if (boardMembersipModel.role !== BoardMembershipRoles.EDITOR) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return {
|
||||
...boardModel.ref,
|
||||
cards: boardModel.getOrderedCardsModelArrayForUser(id).map((card) => ({
|
||||
...card,
|
||||
isPersisted: !isLocalId(card.id),
|
||||
})),
|
||||
};
|
||||
}),
|
||||
}));
|
||||
},
|
||||
);
|
||||
|
||||
export const selectBoardIdsForCurrentUser = createSelector(
|
||||
orm,
|
||||
(state) => selectCurrentUserId(state),
|
||||
|
@ -335,6 +370,7 @@ export default {
|
|||
selectFilteredProjctIdsByGroupForCurrentUser,
|
||||
selectFavoriteProjectIdsForCurrentUser,
|
||||
selectProjectsToListsWithEditorRightsForCurrentUser,
|
||||
selectProjectsToCardsWithEditorRightsForCurrentUser,
|
||||
selectBoardIdsForCurrentUser,
|
||||
selectNotificationIdsForCurrentUser,
|
||||
selectNotificationServiceIdsForCurrentUser,
|
||||
|
|
1
server/package-lock.json
generated
1
server/package-lock.json
generated
|
@ -9455,7 +9455,6 @@
|
|||
},
|
||||
"node_modules/string-width": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue