From 8c2de439d4cf7c559d15aaf88c4cb3ec866f696d Mon Sep 17 00:00:00 2001 From: joakimbaynaud Date: Fri, 30 May 2025 12:29:40 +0200 Subject: [PATCH] feat: add page to view all cards assigned to current user #264 --- .../components/cards/Card/ProjectContent.jsx | 7 + .../components/cards/CardModal/CardModal.jsx | 2 +- client/src/components/common/Root.jsx | 2 + .../users/UserCardsPage/UserCardsPage.jsx | 136 ++++++++++++++++++ .../UserCardsPage/UserCardsPage.module.scss | 96 +++++++++++++ .../components/users/UserCardsPage/index.js | 8 ++ .../components/users/UserStep/UserStep.jsx | 15 ++ client/src/constants/Paths.js | 2 + client/src/locales/en-GB/core.js | 2 + client/src/locales/en-US/core.js | 2 + client/src/locales/fr-FR/core.js | 2 + client/src/models/Board.js | 9 ++ client/src/models/Card.js | 12 +- client/src/selectors/users.js | 36 +++++ server/package-lock.json | 1 - 15 files changed, 329 insertions(+), 3 deletions(-) create mode 100755 client/src/components/users/UserCardsPage/UserCardsPage.jsx create mode 100755 client/src/components/users/UserCardsPage/UserCardsPage.module.scss create mode 100755 client/src/components/users/UserCardsPage/index.js diff --git a/client/src/components/cards/Card/ProjectContent.jsx b/client/src/components/cards/Card/ProjectContent.jsx index 0f74693a..2cf7fac1 100755 --- a/client/src/components/cards/Card/ProjectContent.jsx +++ b/client/src/components/cards/Card/ProjectContent.jsx @@ -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), diff --git a/client/src/components/cards/CardModal/CardModal.jsx b/client/src/components/cards/CardModal/CardModal.jsx index 3d86c94c..ec5a4bfc 100755 --- a/client/src/components/cards/CardModal/CardModal.jsx +++ b/client/src/components/cards/CardModal/CardModal.jsx @@ -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(); diff --git a/client/src/components/common/Root.jsx b/client/src/components/common/Root.jsx index 03ea0e0f..516f6eed 100755 --- a/client/src/components/common/Root.jsx +++ b/client/src/components/common/Root.jsx @@ -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 }) { } /> } /> } /> + } /> } /> diff --git a/client/src/components/users/UserCardsPage/UserCardsPage.jsx b/client/src/components/users/UserCardsPage/UserCardsPage.jsx new file mode 100755 index 00000000..1a8dfd5b --- /dev/null +++ b/client/src/components/users/UserCardsPage/UserCardsPage.jsx @@ -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 ( + <> +
+ +
+ + ); + } + + return ( + <> +
+
+

{t('common.myCards')}

+ {projectsToCard && projectsToCard.length > 0 ? ( + +
+ {projectsToCard.map((project) => ( +
+

{project.name}

+ + {project.boards && project.boards.length > 0 ? ( + project.boards.map((board) => ( +
+

{board.name}

+ + {board.cards && board.cards.length > 0 ? ( + + {(provided) => ( +
+ {board.cards.map((card, index) => { + const cardId = card.id || card._fields?.id; + if (!cardId) { + console.warn('Card missing ID:', card); + return null; + } + + return ( +
+ +
+ ); + })} + {provided.placeholder} +
+ )} +
+ ) : ( +

{t('common.noCards')}

+ )} +
+ )) + ) : ( +

{t('common.noBoards')}

+ )} +
+ ))} +
+
+ ) : ( +
+

{t('common.noProjects')}

+
+ )} +
+ + ); +} + +export default UserCardsPage; diff --git a/client/src/components/users/UserCardsPage/UserCardsPage.module.scss b/client/src/components/users/UserCardsPage/UserCardsPage.module.scss new file mode 100755 index 00000000..f0be4f79 --- /dev/null +++ b/client/src/components/users/UserCardsPage/UserCardsPage.module.scss @@ -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) { + +} diff --git a/client/src/components/users/UserCardsPage/index.js b/client/src/components/users/UserCardsPage/index.js new file mode 100755 index 00000000..1d8aa232 --- /dev/null +++ b/client/src/components/users/UserCardsPage/index.js @@ -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; diff --git a/client/src/components/users/UserStep/UserStep.jsx b/client/src/components/users/UserStep/UserStep.jsx index 1d943c19..11d79913 100755 --- a/client/src/components/users/UserStep/UserStep.jsx +++ b/client/src/components/users/UserStep/UserStep.jsx @@ -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 }) => { + + {t('common.myCards', { + context: 'title', + })} + {t('common.settings', { context: 'title', diff --git a/client/src/constants/Paths.js b/client/src/constants/Paths.js index 91bd78d0..03dad4a6 100755 --- a/client/src/constants/Paths.js +++ b/client/src/constants/Paths.js @@ -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, }; diff --git a/client/src/locales/en-GB/core.js b/client/src/locales/en-GB/core.js index ae9d0d4e..6441e5ad 100755 --- a/client/src/locales/en-GB/core.js +++ b/client/src/locales/en-GB/core.js @@ -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', diff --git a/client/src/locales/en-US/core.js b/client/src/locales/en-US/core.js index a42ac99d..f3d8bd9e 100755 --- a/client/src/locales/en-US/core.js +++ b/client/src/locales/en-US/core.js @@ -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', diff --git a/client/src/locales/fr-FR/core.js b/client/src/locales/fr-FR/core.js index f690c019..3db82894 100755 --- a/client/src/locales/fr-FR/core.js +++ b/client/src/locales/fr-FR/core.js @@ -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', diff --git a/client/src/models/Board.js b/client/src/models/Board.js index 429178b7..3f532b73 100755 --- a/client/src/models/Board.js +++ b/client/src/models/Board.js @@ -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() diff --git a/client/src/models/Card.js b/client/src/models/Card.js index 8fe5b4bb..0efa4a59 100755 --- a/client/src/models/Card.js +++ b/client/src/models/Card.js @@ -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); } diff --git a/client/src/selectors/users.js b/client/src/selectors/users.js index 1da85bf8..b8e9329f 100755 --- a/client/src/selectors/users.js +++ b/client/src/selectors/users.js @@ -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, diff --git a/server/package-lock.json b/server/package-lock.json index 86330359..9745f1a0 100755 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -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",