feat(notifications): add mark-all-as-read and fix mobile card view#1427
Conversation
|
Need to have the backend merged before to be tested |
ClemRz
left a comment
There was a problem hiding this comment.
Issues (Must Fix)
-
[packages/web-app/src/pages/Notifications/index.jsx:53-56] The
useEffectthat triggers onreadAllStatus === REDUCER_STATUS.SUCCEEDEDwill also fire on mount if the user previously clicked "Mark all as read" in the dropdown (since Redux state persists across navigation). This causes a redundant double-fetch alongside the initialuseEffect([], [])on line 44. Consider either resetting the reducer status back to IDLE after handling success, or tracking a local "has just completed" flag instead.// Option A: Reset status in reducer after handling // Add a RESET_READ_ALL_NOTIFICATIONS action type that sets status back to IDLE // and dispatch it after the re-fetch. // Option B: Use a ref to track previous status const prevReadAllStatus = useRef(readAllStatus); useEffect(() => { if ( prevReadAllStatus.current !== REDUCER_STATUS.SUCCEEDED && readAllStatus === REDUCER_STATUS.SUCCEEDED ) { dispatch(fetchNotifications({ limit: 50, skip: 0 })); dispatch(countUnreadNotifications()); } prevReadAllStatus.current = readAllStatus; }, [dispatch, readAllStatus]); -
[packages/web-app/src/components/appli/NotificationMenu/index.jsx:91-93] After
readAllNotifications()succeeds, the badge resets to 0 but the notification list items in the dropdown are not refreshed — they still appear as unread visually (sincestate.menuNotificationsis not re-fetched). Consider dispatchingfetchMenuNotificationson success, similar to how the Notifications page re-fetches after success. AuseEffectwatchingreadAllStatus(or cross-reducer handling ofREAD_ALL_NOTIFICATIONS_SUCCESSin the menu reducer) would solve this.
Suggestions (Should Consider)
-
[packages/web-app/src/components/common/EntityTable/entitiesConfig.jsx:12-22]
DateTimeCellis a React component but is missing PropTypes validation, which is required by project conventions. Consider adding:import PropTypes from 'prop-types'; // ... after the component definition: DateTimeCell.propTypes = { value: PropTypes.string }; -
[packages/web-app/src/actions/Notifications/ReadAllNotifications.js:40-44] The
readAllNotificationsActionFailurefunction signature accepts a singleerrorparameter, but the dispatch call passes two arguments (makeErrorMessage(...)anderror.message). The second argument is silently ignored. This is a copy-paste fromReadNotification.jswhich has the same issue. Harmless, but worth cleaning up for clarity:dispatch( readAllNotificationsActionFailure( makeErrorMessage(error.message, 'Reading all notifications') ) ); -
[packages/web-app/src/actions/Notifications/ReadAllNotifications.js:3]
makeErrorMessageis imported from the deprecatedhelpers/folder. While the same pattern exists inReadNotification.js, new code ideally shouldn't add more usages of deprecated modules. This is not blocking since it mirrors an existing pattern, but worth noting for future cleanup. -
[packages/web-app/src/pages/Notifications/index.jsx:63-69] The error effect depends on
readAllErrorreference equality. If the action is dispatched multiple times with the same error message, the toast won't re-trigger because themakeErrorMessagecreates a new object each time (so reference does change). This is fine — just noting that error state isn't cleared when a new attempt starts, so navigating away and back could re-show the toast. Consider clearingerrorin the LOADING case of the reducer (which is already done:error: undefinedon LOADING — good).
Nitpicks (Optional)
-
[packages/web-app/src/components/common/EntityTable/entitiesConfig.jsx:75-79] The notifications column definitions now exceed 80 characters per line (e.g.,
{ visible: true, field: 'entityName', label: 'Name', sortable: false, isTitle: true }). Consider breaking these into multi-line objects to stay within Prettier's 80-char width, which would also match the style of other column definitions in the file. -
[packages/web-app/src/components/common/EntityTable/MobileEntityList.jsx:41-42] The
if (onRowClick) onRowClick(doc)is nested inside anelseblock ofif (onToggle). Adding braces for the innerifwould improve readability:} else { if (onRowClick) { onRowClick(doc); } openLink(link(doc)); }
API endpoint ready 🎉The backend for batch mark-as-read is implemented and open for review: GrottoCenter/grottocenter-api#1709 Endpoint contractMark all unread: {}or { "ids": [] }Mark specific notifications: { "ids": [1, 2, 3] }Responses
Notes for front-end integration
|
Code review follow-upMust Fix — both addressed
Fixed in const prevReadAllStatus = useRef(readAllStatus);
useEffect(() => {
if (
readAllStatus === REDUCER_STATUS.SUCCEEDED &&
prevReadAllStatus.current !== REDUCER_STATUS.SUCCEEDED
) {
// ...side effects
}
prevReadAllStatus.current = readAllStatus;
}, [dispatch, readAllStatus]);Dropdown not re-fetching after Fixed in Should Consider — all addressed
Added
The action creator only accepts one argument; the second ( Not Fixed — intentional
This is pre-existing tech debt shared across many action files. Fixing it in isolation would be an unrelated refactor; out of scope for this PR. Column definitions exceeding 80 chars / missing braces on single-line Style nitpicks that conflict with the existing codebase style (Airbnb config allows omitting braces for single-statement bodies, and the 80-char limit is a soft guideline in this project). Not changed. |
532f121 to
24ed54d
Compare
ClemRz
left a comment
There was a problem hiding this comment.
Suggestions (Should Consider)
-
[packages/web-app/src/pages/Notifications/index.jsx:70-74] The error
useEffectdepends onreadAllErrorreference equality. However,makeErrorMessagereturns a new string each time, so this works correctly for repeated errors. The subtle issue is thatonErrorandformatMessageare stable (memoized), but they're still in the dependency array — which is fine for correctness but adds noise. More importantly, if the user navigates away and back whilereadAllErroris still in the store (status is FAILED, error is set), the toast will fire again on mount. Consider clearing the error on the LOADING transition (which is already done:error: undefinedon LOADING — good), but also note that if the user never retries, the stale error persists across navigations. A minor UX concern, not blocking. -
[packages/web-app/src/components/appli/NotificationMenu/index.jsx:157-165] The
Tooltipwrapping theIconButtonalways shows"Mark all as read"as its title, even when the button is disabled (no unread notifications). On the Notifications page, you show a different tooltip ("No unread notifications") when disabled. Consider applying the same pattern here for consistency — showing why the button is disabled improves discoverability. -
[packages/web-app/src/components/common/EntityTable/entitiesConfig.jsx:14-26]
DateTimeCellusesuseIntl()which means it can only be rendered inside a React tree (not as a plain function call). SincecellsRender.dateTimewraps it inside JSX (<DateTimeCell value={value} />), this works. However, the component is defined at module scope alongside plain render functions — consider adding a brief comment clarifying it's a component (vs. the other plain-function renderers) so future contributors don't accidentally try to call it as a function.
Nitpicks (Optional)
-
[packages/web-app/src/components/common/EntityTable/entitiesConfig.jsx:78-97] The notification column definition objects on lines 78, 97, etc. are single-line and exceed 80 characters. The existing pattern in the file uses multi-line objects for columns with
renderfunctions. Consider breaking the longer lines for consistency:{ visible: true, field: 'entityName', label: 'Name', sortable: false, isTitle: true }, -
[packages/web-app/src/pages/Notifications/index.jsx:82-90] The
markAllButtonis defined as a JSX expression stored in a variable. This is fine and readable, but the button'sonClickcreates a new arrow function on every render (() => dispatch(readAllNotifications())). SinceLayoutlikely doesn't memoizeaction, this is harmless in practice — just noting it for awareness. -
[packages/web-app/src/actions/Notifications/ReadAllNotifications.js:3]
makeErrorMessageis imported from the deprecatedhelpers/folder. Pre-existing pattern shared withReadNotification.js— not introduced by this PR, just noting for future cleanup.
Overall this is a clean, well-structured PR. The previous review's must-fix items (useEffect firing on mount, dropdown not re-fetching) have been properly addressed with the useRef pattern. The new action/reducer follow established conventions, translations are sorted and complete across all 15 languages, and the mobile card fix correctly threads onRowClick through the component hierarchy. Good work.
Second code review follow-upSuggestions — all addressedStale error toast on navigation back ( Applied the same ref pattern to the error effect. Tooltip inconsistency in The
Added a one-line comment above the component clarifying it must be rendered as JSX (uses Multi-line column objects (nitpick) Expanded the two single-line column objects ( Not Fixed — intentional
|
|
Azure Static Web Apps: Your stage site is ready! Visit it here: https://orange-rock-0d4f87503-1427.westeurope.azurestaticapps.net |
|
@urien Deployed on staging env, ready to be reviewed :) |
…d when clicked on
207ee9f to
e21f999
Compare
|
J'ai corrigé normalement |
|
Azure Static Web Apps: Your stage site is ready! Visit it here: https://orange-rock-0d4f87503-1427.westeurope.azurestaticapps.net |
1 similar comment
|
Azure Static Web Apps: Your stage site is ready! Visit it here: https://orange-rock-0d4f87503-1427.westeurope.azurestaticapps.net |
|
Je n'ai plus de notification. Est ce que tu peux modifier une fiche de cavité en France ? |
|
Fait ! |
|
tout marche Merci @Paul-AUB |

Closes #1303
🤔 What
/ui/notifications), placed in the page header alongside the title — consistent with other page-level actions in the app (e.g. "New" on Entrances)DD/MM/YYYY HH:MM, card title uses the entity name instead of the raw ISO timestamp>4700 massifsplaceholder on the homepage with a live count from the new API endpoint🤷♂️ Why
Users with many notifications (after a vacation, or a burst of activity in subscribed areas) had no way to clear them efficiently without clicking each one individually. The mobile card view silently never marked notifications as read, which was a regression. The
>4700 massifsvalue on the homepage was stale and would only get more inaccurate over time.🔍 How
ReadAllNotificationscallsPUT /api/v1/notifications/readwith an empty body (marks all unread for the authenticated user)ReadAllNotificationsReducertracks loading/success/failure stateCountUnreadNotificationsReducerhandlesREAD_ALL_NOTIFICATIONS_SUCCESSto reset the badge to 0 immediately, without waiting for a re-fetchonRowClickprop is now threaded throughEntityTable→MobileEntityList→MobileEntityCard, so the callback fires on card tap before navigationBoxwithoverflow-y: auto / maxHeight: 400px; header and footer areflexShrink: 0outside the scroll areamassifskey added todynamicNumbersUrlmap pointing toGET /api/v1/massifs/count;staticValue: '>4700'removed fromHeroStats🔗 Related API PR
GET /api/v1/massifs/countendpoint grottocenter-api#1700 (massifs count endpoint, already closed/merged)🧪 Testing
/ui/notifications→ confirm button is in the page header, disabled when nothing unread>4700)📸 Previews