From d64cdfcbe40a013df11121da1e97a66439024cac Mon Sep 17 00:00:00 2001 From: yuval Date: Sun, 6 Jul 2025 10:28:57 +0300 Subject: [PATCH 01/25] GA-134 bugfix sidebar label's menu not showing in mobile UI --- frontend/src/main_page/EmailSideMenu/EmailSideMenu.css | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/main_page/EmailSideMenu/EmailSideMenu.css b/frontend/src/main_page/EmailSideMenu/EmailSideMenu.css index 9c8d0d1e..e752976e 100644 --- a/frontend/src/main_page/EmailSideMenu/EmailSideMenu.css +++ b/frontend/src/main_page/EmailSideMenu/EmailSideMenu.css @@ -123,6 +123,7 @@ transition: transform 0.3s ease-in-out; z-index: 1050; padding-top: 5rem; + overflow: visible; } .sidebar.show { transform: translateX(0); From 3464a9b67cf2b40e810f1ef042fddcd136adff70 Mon Sep 17 00:00:00 2001 From: yuval Date: Sun, 6 Jul 2025 11:40:16 +0300 Subject: [PATCH 02/25] GA-134 basic marking of mails with labels --- frontend/package-lock.json | 237 ++++++++++++++++++++- frontend/package.json | 1 + frontend/src/api/mailApi.js | 27 +++ frontend/src/main_page/MainPage.js | 6 +- frontend/src/main_page/toolbar/ToolBar.jsx | 83 +++++++- web_server/controllers/mails.js | 5 +- web_server/utils/labels.js | 12 +- 7 files changed, 358 insertions(+), 13 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index b0c564dc..af3391f5 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -15,6 +15,7 @@ "bootstrap": "^5.3.6", "bootstrap-icons": "^1.10.5", "react": "^19.1.0", + "react-bootstrap": "^2.10.10", "react-dom": "^19.1.0", "react-router-dom": "^7.6.2", "react-scripts": "5.0.1", @@ -3095,12 +3096,80 @@ "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", "license": "MIT", - "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/popperjs" } }, + "node_modules/@react-aria/ssr": { + "version": "3.9.9", + "resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.9.tgz", + "integrity": "sha512-2P5thfjfPy/np18e5wD4WPt8ydNXhij1jwA8oehxZTFqlgVMGXzcWKxTb4RtJrLFsqPO7RUQTiY8QJk0M4Vy2g==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + }, + "engines": { + "node": ">= 12" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@restart/hooks": { + "version": "0.4.16", + "resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.4.16.tgz", + "integrity": "sha512-f7aCv7c+nU/3mF7NWLtVVr0Ra80RqsO89hO72r+Y/nvQr5+q0UFGkocElTH6MJApvReVh6JHUFYn2cw1WdHF3w==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@restart/ui": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@restart/ui/-/ui-1.9.4.tgz", + "integrity": "sha512-N4C7haUc3vn4LTwVUPlkJN8Ach/+yIMvRuTVIhjilNHqegY60SGLrzud6errOMNJwSnmYFnt1J0H/k8FE3A4KA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.26.0", + "@popperjs/core": "^2.11.8", + "@react-aria/ssr": "^3.5.0", + "@restart/hooks": "^0.5.0", + "@types/warning": "^3.0.3", + "dequal": "^2.0.3", + "dom-helpers": "^5.2.0", + "uncontrollable": "^8.0.4", + "warning": "^4.0.3" + }, + "peerDependencies": { + "react": ">=16.14.0", + "react-dom": ">=16.14.0" + } + }, + "node_modules/@restart/ui/node_modules/@restart/hooks": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.5.1.tgz", + "integrity": "sha512-EMoH04NHS1pbn07iLTjIjgttuqb7qu4+/EyhAx27MHpoENcB2ZdSsLTNxmKD+WEPnZigo62Qc8zjGnNxoSE/5Q==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@restart/ui/node_modules/uncontrollable": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/uncontrollable/-/uncontrollable-8.0.4.tgz", + "integrity": "sha512-ulRWYWHvscPFc0QQXvyJjY6LIXU56f0h8pQFvhxiKk5V1fcI8gp9Ht9leVAhrVjzqMw0BgjspBINx9r6oyJUvQ==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.14.0" + } + }, "node_modules/@rollup/plugin-babel": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", @@ -3449,6 +3518,15 @@ "url": "https://github.com/sponsors/gregberge" } }, + "node_modules/@swc/helpers": { + "version": "0.5.17", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz", + "integrity": "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, "node_modules/@testing-library/dom": { "version": "10.4.0", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz", @@ -3826,6 +3904,12 @@ "integrity": "sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA==", "license": "MIT" }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "license": "MIT" + }, "node_modules/@types/q": { "version": "1.5.8", "resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.8.tgz", @@ -3844,6 +3928,24 @@ "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", "license": "MIT" }, + "node_modules/@types/react": { + "version": "19.1.8", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.8.tgz", + "integrity": "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g==", + "license": "MIT", + "dependencies": { + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-transition-group": { + "version": "4.4.12", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz", + "integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*" + } + }, "node_modules/@types/resolve": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz", @@ -3916,6 +4018,12 @@ "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", "license": "MIT" }, + "node_modules/@types/warning": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/warning/-/warning-3.0.3.tgz", + "integrity": "sha512-D1XC7WK8K+zZEveUPY+cf4+kgauk8N4eHr/XIHXGlGYkHLud6hK9lYfZk1ry1TNh798cZUCgb6MqGEG8DkJt6Q==", + "license": "MIT" + }, "node_modules/@types/ws": { "version": "8.18.1", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", @@ -5651,6 +5759,12 @@ "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", "license": "MIT" }, + "node_modules/classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==", + "license": "MIT" + }, "node_modules/clean-css": { "version": "5.3.3", "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz", @@ -6413,6 +6527,12 @@ "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", "license": "MIT" }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "license": "MIT" + }, "node_modules/damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", @@ -6739,6 +6859,16 @@ "utila": "~0.4" } }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, "node_modules/dom-serializer": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", @@ -9320,6 +9450,15 @@ "node": ">= 0.4" } }, + "node_modules/invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, "node_modules/ipaddr.js": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz", @@ -13664,6 +13803,25 @@ "react-is": "^16.13.1" } }, + "node_modules/prop-types-extra": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/prop-types-extra/-/prop-types-extra-1.1.1.tgz", + "integrity": "sha512-59+AHNnHYCdiC+vMwY52WmvP5dM3QLeoumYuEyceQDi9aEhtwN9zIQ2ZNo25sMyXnbh32h+P1ezDsUpUH3JAew==", + "license": "MIT", + "dependencies": { + "react-is": "^16.3.2", + "warning": "^4.0.0" + }, + "peerDependencies": { + "react": ">=0.14.0" + } + }, + "node_modules/prop-types-extra/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, "node_modules/prop-types/node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -13845,6 +14003,37 @@ "node": ">=14" } }, + "node_modules/react-bootstrap": { + "version": "2.10.10", + "resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-2.10.10.tgz", + "integrity": "sha512-gMckKUqn8aK/vCnfwoBpBVFUGT9SVQxwsYrp9yDHt0arXMamxALerliKBxr1TPbntirK/HGrUAHYbAeQTa9GHQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.24.7", + "@restart/hooks": "^0.4.9", + "@restart/ui": "^1.9.4", + "@types/prop-types": "^15.7.12", + "@types/react-transition-group": "^4.4.6", + "classnames": "^2.3.2", + "dom-helpers": "^5.2.1", + "invariant": "^2.2.4", + "prop-types": "^15.8.1", + "prop-types-extra": "^1.1.0", + "react-transition-group": "^4.4.5", + "uncontrollable": "^7.2.1", + "warning": "^4.0.3" + }, + "peerDependencies": { + "@types/react": ">=16.14.8", + "react": ">=16.14.0", + "react-dom": ">=16.14.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/react-dev-utils": { "version": "12.0.1", "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz", @@ -13974,6 +14163,12 @@ "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "license": "MIT" }, + "node_modules/react-lifecycles-compat": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", + "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==", + "license": "MIT" + }, "node_modules/react-refresh": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz", @@ -14103,6 +14298,22 @@ } } }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -16494,6 +16705,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/uncontrollable": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/uncontrollable/-/uncontrollable-7.2.1.tgz", + "integrity": "sha512-svtcfoTADIB0nT9nltgjujTi7BzVmwjZClOmskKu/E8FW9BXzg9os8OLr4f8Dlnk0rYWJIWr4wv9eKUXiQvQwQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.6.3", + "@types/react": ">=16.9.11", + "invariant": "^2.2.4", + "react-lifecycles-compat": "^3.0.4" + }, + "peerDependencies": { + "react": ">=15.0.0" + } + }, "node_modules/underscore": { "version": "1.12.1", "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.12.1.tgz", @@ -16746,6 +16972,15 @@ "makeerror": "1.0.12" } }, + "node_modules/warning": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", + "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, "node_modules/watchpack": { "version": "2.4.4", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz", diff --git a/frontend/package.json b/frontend/package.json index 5a07a82e..18b90365 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -10,6 +10,7 @@ "bootstrap": "^5.3.6", "bootstrap-icons": "^1.10.5", "react": "^19.1.0", + "react-bootstrap": "^2.10.10", "react-dom": "^19.1.0", "react-router-dom": "^7.6.2", "react-scripts": "5.0.1", diff --git a/frontend/src/api/mailApi.js b/frontend/src/api/mailApi.js index 81c88a6e..94a6764e 100644 --- a/frontend/src/api/mailApi.js +++ b/frontend/src/api/mailApi.js @@ -285,3 +285,30 @@ export async function getMailsByLabel(labelId, page = 1) { } return res.json(); } + +/** + * Edits a mail with the new labels picked for it + * @param {number} mailId id of the mail to edit its labels + * @param {any[]} labels list of label object + * @returns {Promise} + */ +export async function applyLabelsToMail(mailId, labels) { + console.log(labels) + const token = localStorage.getItem("token"); + const url = `${API_BASE}/mails/${mailId}`; + const res = await fetch(url, { + method: "PATCH", + headers: { + "Authorization": `Bearer ${token}`, + 'Content-Type': 'application/json' + + }, + body: JSON.stringify({ + labels: labels, + }) + }) + if (!res.ok) { + throw new Error(`getMailsByLabel failed: ${res.status}`); + } + return res.json(); +} \ No newline at end of file diff --git a/frontend/src/main_page/MainPage.js b/frontend/src/main_page/MainPage.js index 0476a0aa..29bef36a 100644 --- a/frontend/src/main_page/MainPage.js +++ b/frontend/src/main_page/MainPage.js @@ -87,7 +87,7 @@ const MainPage = () => { setInboxType(location.state.inboxType); } }, [location.state?.inboxType]); - + console.log('mails', emails); return (
{ theme={theme} inboxType={inboxType} allSelected={allSelected} + selectedMails={selectedMails} anySelected={anySelected} btnHandlers={handlers} total={total} @@ -122,6 +123,7 @@ const MainPage = () => { hasPrevPage={hasPrevPage} goToNextPage={goToNextPage} goToPrevPage={goToPrevPage} + refreshMails={refreshMails} /> {loading ? ( @@ -149,7 +151,7 @@ const MainPage = () => { offset={c.offset} draftMail={c.draftMail} onCancel={() =>{ handleCloseCompose(c.id); - refreshMails(c.id);}} + refreshMails();}} onSend={() => { handleCloseCompose(c.id); refreshMails(); diff --git a/frontend/src/main_page/toolbar/ToolBar.jsx b/frontend/src/main_page/toolbar/ToolBar.jsx index df8f4cf2..e6e45da0 100644 --- a/frontend/src/main_page/toolbar/ToolBar.jsx +++ b/frontend/src/main_page/toolbar/ToolBar.jsx @@ -2,11 +2,15 @@ import React, {useEffect, useRef, useState} from "react"; import './ToolBar.css' import {MAILS_PER_PAGE} from "../../utils/constants"; import useIsMobile from "../../utils/useIsMobile"; +import {fetchLabels} from "../../api/labelsApi"; +import {applyLabelsToMail} from "../../api/mailApi"; +import {Dropdown} from "react-bootstrap"; const ToolBar = ({ theme, inboxType, allSelected, + selectedMails, anySelected, btnHandlers, total, @@ -15,17 +19,63 @@ const ToolBar = ({ hasPrevPage, goToNextPage, goToPrevPage, + refreshMails, }) => { + // checks if mobile UI or desktop + const isMobile = useIsMobile(); + // for cases where not all mails were selected but some do const selectAllRef = useRef(null); useEffect(() => { if (selectAllRef.current) { - selectAllRef.current.indeterminate = !allSelected && anySelected; + selectAllRef.current.indeterminate = !allSelected && selectedMails.length > 0; } - }, [allSelected, anySelected]); + }, [allSelected, selectedMails]); + + // labels + const [labels, setLabels] = useState([]); + const [showLabelMenu, setShowLabelMenu] = useState(false); + const [selectedLabelId, setSelectedLabelId] = useState(null); + // fetches labels + useEffect(() => { + const loadLabels = async () => { + try { + const response = await fetchLabels(); + setLabels(response); + } catch (e) { + console.error("Failed to fetch labels", e); + } + }; + loadLabels(); + }, []); + + // apply label selection and refresh mails + const handleLabelToggle = async (label) => { + try { + await Promise.all(Array.from(selectedMails).map(async mail => { + const hasLabel = mail.labels.some(l => l.id === label.id); + let newLabels; + + if (hasLabel) { + newLabels = mail.labels.filter(l => l.id !== label.id); // Remove label + } else { + newLabels = [...mail.labels, label]; // Add label + } + // update UI + mail.labels = newLabels; + // Send new label list to backend + await applyLabelsToMail(mail.id, newLabels); + console.log(mail.id); + console.log(newLabels); + })); + await refreshMails(); + } catch (e) { + console.error("Failed to update label:", e); + } + }; + - const isMobile = useIsMobile(); return (
@@ -95,6 +145,33 @@ const ToolBar = ({
)} + {/* label picker for selected mails */} + + + + {labels.map(label => ( +
+ + mail.labels?.some(l => l.id === label.id) + )} + onChange={() => handleLabelToggle(label)} + /> + +
+ ))} +
+
)} {/* paging info and buttons - always shown */} diff --git a/web_server/controllers/mails.js b/web_server/controllers/mails.js index 5b26ba71..0e2e9343 100644 --- a/web_server/controllers/mails.js +++ b/web_server/controllers/mails.js @@ -152,9 +152,12 @@ const updateMail = (req, res) => { return editDraft(req, res, userId, mail); // otherwise it’s a mail already sent - only allow flags & labels - const {isRead, isStarred, isTrashed, isSpam, labels} = req.body; + const {isRead, isStarred, isTrashed, isSpam, labels} = req.body || {}; + console.log('labels', labels); const labelsIds = convertLabelsToIds(userId, labels || []); + console.log('labelsIds', labelsIds); const updated = Mails.editSentMail(mailId, isRead, isStarred, isTrashed, isSpam, labelsIds); + console.log('updated', updated); if (updated) return res.status(200).json(updated); if (updated === 404) diff --git a/web_server/utils/labels.js b/web_server/utils/labels.js index d1026c7e..fa6fe93f 100644 --- a/web_server/utils/labels.js +++ b/web_server/utils/labels.js @@ -3,14 +3,14 @@ const Labels = require("../models/labels"); /** * Converts an array of label names to their ids * @param userId owner of the label - * @param labelsByNames array of labels' names + * @param labelsObjects array of labels' objects * @returns {number[]} array of labels ids */ -function convertLabelsToIds(userId, labelsByNames) { - if (!labelsByNames) +function convertLabelsToIds(userId, labelsObjects) { + if (!labelsObjects) return []; - return labelsByNames.map(label => { - return Labels.getLabelByName(userId, label).id; + return labelsObjects.map(label => { + return label.id; }) } @@ -33,7 +33,7 @@ function labelsToFullElement(userId, labelsIds) { */ const mailLabelNames = (userId, mail) => { return (mail.labels || []) - .map(labelId => Labels.getLabelById(userId, labelId).name) + .map(labelId => Labels.getLabelById(labelId).name) .filter(Boolean) .map(name => name.toLowerCase()); }; From 9df527f9b027e76eafbba1eefa7b17899da911fa Mon Sep 17 00:00:00 2001 From: yuval Date: Sun, 6 Jul 2025 14:24:51 +0300 Subject: [PATCH 03/25] GA-134 added reactive UI for picking labels --- .../src/components/StarButton/StarButton.css | 11 ++-- .../main_page/EmailSideMenu/EmailSideMenu.jsx | 4 +- frontend/src/main_page/MainPage.js | 11 ++-- .../main_page/hooks/useMailToolbarHandlers.js | 7 +++ frontend/src/main_page/mail_row/MailRow.css | 1 + frontend/src/main_page/toolbar/ToolBar.css | 16 ++++++ frontend/src/main_page/toolbar/ToolBar.jsx | 50 ++++++++++--------- web_server/utils/labels.js | 16 +++--- 8 files changed, 75 insertions(+), 41 deletions(-) diff --git a/frontend/src/components/StarButton/StarButton.css b/frontend/src/components/StarButton/StarButton.css index 9ab189e6..a327a03f 100644 --- a/frontend/src/components/StarButton/StarButton.css +++ b/frontend/src/components/StarButton/StarButton.css @@ -1,6 +1,6 @@ /* star button */ .star-icon { - display: flex; + display: inline-flex; align-items: center; justify-content: center; background: none; @@ -8,8 +8,9 @@ border-radius: 4px; border: none; cursor: pointer; - width: 16px; - height: 16px; + width: auto; + height: auto; + line-height: 1; color: #888888; transition: all 0.2s; } @@ -20,6 +21,10 @@ box-shadow: 0 4px 12px rgba(240, 240, 240, 0.15); } +.star-icon.dark { + color: white; +} + .star-icon.starred { color: #ffc107; fill: #ffc107; diff --git a/frontend/src/main_page/EmailSideMenu/EmailSideMenu.jsx b/frontend/src/main_page/EmailSideMenu/EmailSideMenu.jsx index 97166633..4cbf8b93 100644 --- a/frontend/src/main_page/EmailSideMenu/EmailSideMenu.jsx +++ b/frontend/src/main_page/EmailSideMenu/EmailSideMenu.jsx @@ -32,7 +32,8 @@ export default function EmailSidebar({ setCurrentTab, // Callback to change tab onComposeClick, // Callback for "Compose" button showSidebar, // Boolean: is sidebar open (mobile) - setShowSidebar // Callback to toggle sidebar (mobile) + setShowSidebar, // Callback to toggle sidebar (mobile) + clearSelection }) { const isMobile = useIsMobile(); const [labels, setLabels] = useState([]); // Flat list of all labels (from backend) @@ -94,6 +95,7 @@ export default function EmailSidebar({ // Navigate to mailbox tab or label tab const onClickTab = id => { setCurrentTab(id); + clearSelection(); if (isMobile) setShowSidebar(false); }; diff --git a/frontend/src/main_page/MainPage.js b/frontend/src/main_page/MainPage.js index 29bef36a..05d69993 100644 --- a/frontend/src/main_page/MainPage.js +++ b/frontend/src/main_page/MainPage.js @@ -61,14 +61,12 @@ const MainPage = () => { } = useMails(inboxType); const [selectedMails, setSelectedMails] = useState(new Set()); - const allSelected = selectedMails.size === emails.length; - const anySelected = selectedMails.size > 0; const handlers = useMailToolbarHandlers( selectedMails, setSelectedMails, emails, - refreshMails + refreshMails, ); const handleSelect = (mail, checked) => { @@ -85,9 +83,10 @@ const MainPage = () => { useEffect(() => { if (location.state?.inboxType) { setInboxType(location.state.inboxType); + setSelectedMails(new Set()); } }, [location.state?.inboxType]); - console.log('mails', emails); + return (
{ onComposeClick={handleComposeClick} showSidebar={showSidebar} setShowSidebar={setShowSidebar} + clearSelection={handlers.clearSelection} />
@@ -113,9 +113,8 @@ const MainPage = () => { { + setSelectedMails(new Set()); + refreshMails(); + }, []); + return { handleSelectAll, handleRefresh, @@ -98,5 +104,6 @@ export const useMailToolbarHandlers = (selectedMails, setSelectedMails, allEmail handleMarkAsRead, handleMarkSpam, handleRestore, + clearSelection, }; } \ No newline at end of file diff --git a/frontend/src/main_page/mail_row/MailRow.css b/frontend/src/main_page/mail_row/MailRow.css index 00ddb031..3aa89a51 100644 --- a/frontend/src/main_page/mail_row/MailRow.css +++ b/frontend/src/main_page/mail_row/MailRow.css @@ -89,6 +89,7 @@ .row-top-controls { display: flex; + flex-direction: row; gap: 12px; align-items: center; } diff --git a/frontend/src/main_page/toolbar/ToolBar.css b/frontend/src/main_page/toolbar/ToolBar.css index 1f123af4..610ef5f7 100644 --- a/frontend/src/main_page/toolbar/ToolBar.css +++ b/frontend/src/main_page/toolbar/ToolBar.css @@ -61,4 +61,20 @@ } .paging-text.dark { color: #ffffff; +} + +.labels-dropdown { + overflow-y: auto; + max-height: 250px; +} +.labels-dropdown.light { + color: black; + background-color: #fff; +} +.labels-dropdown.dark { + color: white; + background-color: #2c2c2c; +} +.form-check.light .form-check-input { + color: black; } \ No newline at end of file diff --git a/frontend/src/main_page/toolbar/ToolBar.jsx b/frontend/src/main_page/toolbar/ToolBar.jsx index e6e45da0..79bbafe8 100644 --- a/frontend/src/main_page/toolbar/ToolBar.jsx +++ b/frontend/src/main_page/toolbar/ToolBar.jsx @@ -9,9 +9,8 @@ import {Dropdown} from "react-bootstrap"; const ToolBar = ({ theme, inboxType, - allSelected, + mailsAmount, selectedMails, - anySelected, btnHandlers, total, page, @@ -19,24 +18,17 @@ const ToolBar = ({ hasPrevPage, goToNextPage, goToPrevPage, - refreshMails, }) => { // checks if mobile UI or desktop const isMobile = useIsMobile(); - // for cases where not all mails were selected but some do - const selectAllRef = useRef(null); - useEffect(() => { - if (selectAllRef.current) { - selectAllRef.current.indeterminate = !allSelected && selectedMails.length > 0; - } - }, [allSelected, selectedMails]); + let isAllSelected = selectedMails.size === mailsAmount; + let isIndeterminate = selectedMails.size > 0 && selectedMails.size < mailsAmount; // labels const [labels, setLabels] = useState([]); const [showLabelMenu, setShowLabelMenu] = useState(false); - const [selectedLabelId, setSelectedLabelId] = useState(null); // fetches labels useEffect(() => { const loadLabels = async () => { @@ -58,24 +50,35 @@ const ToolBar = ({ let newLabels; if (hasLabel) { - newLabels = mail.labels.filter(l => l.id !== label.id); // Remove label + // Remove label + newLabels = mail.labels.filter(l => l.id !== label.id); } else { - newLabels = [...mail.labels, label]; // Add label + // add label - no duplicates + const labelMap = new Map(mail.labels.map(l => [l.id, l])); + labelMap.set(label.id, label); + newLabels = Array.from(labelMap.values()); } // update UI mail.labels = newLabels; // Send new label list to backend await applyLabelsToMail(mail.id, newLabels); - console.log(mail.id); - console.log(newLabels); + })); - await refreshMails(); + isAllSelected = false; + setShowLabelMenu(false); + btnHandlers.clearSelection(); } catch (e) { console.error("Failed to update label:", e); } }; - + // for cases where not all mails were selected but some do + const selectAllRef = useRef(null); + useEffect(() => { + if (selectAllRef.current) { + selectAllRef.current.indeterminate = isIndeterminate; + } + }, [isIndeterminate]); return (
@@ -84,7 +87,7 @@ const ToolBar = ({ select all @@ -96,7 +99,7 @@ const ToolBar = ({ onClick={btnHandlers.handleRefresh} /> {/* additional buttons - shown when mails selected */} - {anySelected && ( + {selectedMails.size > 0 && (
{/* mark read button */} - + {labels.map(label => (
- mail.labels?.some(l => l.id === label.id) + Array.isArray(mail.labels) && + mail.labels.some(l => l.id === label.id) )} onChange={() => handleLabelToggle(label)} /> diff --git a/web_server/utils/labels.js b/web_server/utils/labels.js index fa6fe93f..e414cfa2 100644 --- a/web_server/utils/labels.js +++ b/web_server/utils/labels.js @@ -2,16 +2,15 @@ const Labels = require("../models/labels"); /** * Converts an array of label names to their ids - * @param userId owner of the label - * @param labelsObjects array of labels' objects + * @param {string} userId owner of the label + * @param {Array} labelsObjects array of labels' objects * @returns {number[]} array of labels ids */ function convertLabelsToIds(userId, labelsObjects) { if (!labelsObjects) return []; - return labelsObjects.map(label => { - return label.id; - }) + const ids = labelsObjects.map(label => label.id); + return [...new Set(ids)]; } /** @@ -21,9 +20,10 @@ function convertLabelsToIds(userId, labelsObjects) { * @returns {*} array of labels elements */ function labelsToFullElement(userId, labelsIds) { - return labelsIds.map(label => { - return Labels.getLabelById(userId, label); - }) + if (!labelsIds) + return []; + const objects = labelsIds.map(id => Labels.getLabelById(id)); + return [...new Set(objects)]; } /** From 0cf42ce6b3707fd3eb2b6f862c5db67fdfaa7528 Mon Sep 17 00:00:00 2001 From: yuval Date: Sun, 6 Jul 2025 14:36:12 +0300 Subject: [PATCH 04/25] GA-134 change labels when reading mail --- .../ReadingHeader/ReadingHeader.jsx | 73 ++++++++++++++++++- 1 file changed, 71 insertions(+), 2 deletions(-) diff --git a/frontend/src/reading_page/ReadingHeader/ReadingHeader.jsx b/frontend/src/reading_page/ReadingHeader/ReadingHeader.jsx index a89115ac..1f171521 100644 --- a/frontend/src/reading_page/ReadingHeader/ReadingHeader.jsx +++ b/frontend/src/reading_page/ReadingHeader/ReadingHeader.jsx @@ -4,8 +4,10 @@ import {APP_NAME, DEFAULT_AVATAR} from "../../utils/constants"; import ProfileMenu from "../../main_page/top_menu/profile/ProfileMenu"; import {useProfile} from "../../main_page/hooks/useProfile"; import {useOutsideClick} from "../../main_page/hooks/useOutsideClick"; -import {useRef, useState} from "react"; -import {deleteMail, restoreMail, toggleSpamReport} from "../../api/mailApi"; +import React, {useEffect, useRef, useState} from "react"; +import {applyLabelsToMail, deleteMail, restoreMail, toggleSpamReport} from "../../api/mailApi"; +import {Dropdown} from "react-bootstrap"; +import {fetchLabels} from "../../api/labelsApi"; const ReadingHeader = ({theme, toggleTheme, email, inboxType}) => { const navigate = useNavigate() @@ -37,6 +39,48 @@ const ReadingHeader = ({theme, toggleTheme, email, inboxType}) => { navigate('/inbox', {state: {inboxType: inboxType}}); } + // labels + const [labels, setLabels] = useState([]); + const [showLabelMenu, setShowLabelMenu] = useState(false); + // fetches labels + useEffect(() => { + const loadLabels = async () => { + try { + const response = await fetchLabels(); + setLabels(response); + } catch (e) { + console.error("Failed to fetch labels", e); + } + }; + loadLabels(); + }, []); + + // Apply label toggle for a single mail object + const handleLabelToggle = async (label) => { + try { + const hasLabel = Array.isArray(email.labels) && email.labels.some(l => l.id === label.id); + let newLabels; + + if (hasLabel) { + // Remove label + newLabels = email.labels.filter(l => l.id !== label.id); + } else { + // Add label without duplicates + const labelMap = new Map(email.labels.map(l => [l.id, l])); + labelMap.set(label.id, label); + newLabels = Array.from(labelMap.values()); + } + // Update UI + email.labels = newLabels; + // Apply to backend + await applyLabelsToMail(email.id, newLabels); + setShowLabelMenu(false); + } catch (e) { + console.error("Failed to update label:", e); + } + }; + + return (
@@ -82,6 +126,31 @@ const ReadingHeader = ({theme, toggleTheme, email, inboxType}) => { Restore mail )} + {/* label picker for current mail */} + + + + {labels.map(label => ( +
+ l.id === label.id)} + onChange={() => handleLabelToggle(label)} + /> + +
+ ))} +
+
{/* profile and profile menu for more actions */} From 0f3fc7af90dcba2e995f5a8d607ba98a745624c2 Mon Sep 17 00:00:00 2001 From: yuval Date: Sun, 6 Jul 2025 15:08:01 +0300 Subject: [PATCH 05/25] GA-134 labels UI indicators --- frontend/src/main_page/mail_row/MailRow.jsx | 14 +++++++++++--- frontend/src/reading_page/ReadingPage.js | 18 ++++++++++++------ 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/frontend/src/main_page/mail_row/MailRow.jsx b/frontend/src/main_page/mail_row/MailRow.jsx index 1e35c01e..9bd24dd4 100644 --- a/frontend/src/main_page/mail_row/MailRow.jsx +++ b/frontend/src/main_page/mail_row/MailRow.jsx @@ -1,6 +1,6 @@ import "./MailRow.css"; -import { formatDate } from "../../utils/formatDate"; -import { useNavigate } from "react-router-dom"; +import {formatDate} from "../../utils/formatDate"; +import {useNavigate} from "react-router-dom"; import StarButton from "../../components/StarButton/StarButton"; import {markMailAsRead, stripHtml} from "../../utils/mailUtils"; //import useIsMobile from "../../utils/useIsMobile"; @@ -51,6 +51,14 @@ const MailRow = ({
{email.from.fullName}
{email.subject}
+
+ {email.labels?.map(label => ( + + {label.name} + + ))} +
{stripHtml(email.body)}
@@ -67,4 +75,4 @@ const MailRow = ({
); } - export default MailRow; +export default MailRow; diff --git a/frontend/src/reading_page/ReadingPage.js b/frontend/src/reading_page/ReadingPage.js index b4019f8f..c928e39b 100644 --- a/frontend/src/reading_page/ReadingPage.js +++ b/frontend/src/reading_page/ReadingPage.js @@ -22,7 +22,7 @@ const ReadingPage = () => { const [composeProps, setComposeProps] = useState(null); // Read inboxType from location state const location = useLocation(); - const { inboxType } = location.state || {}; + const {inboxType} = location.state || {}; // Load the mail when ID changes useEffect(() => { @@ -69,7 +69,7 @@ ${email.body}`, // Allow updating the "starred" state locally const handleStarToggle = (newStarValue) => { - setEmail(prev => ({ ...prev, isStarred: newStarValue })); + setEmail(prev => ({...prev, isStarred: newStarValue})); }; return ( @@ -78,10 +78,16 @@ ${email.body}`, {/* Main content */} {loading ? ( - + ) : (

{email.subject}

+
+ {email.labels?.map(label => ( + {label.name} + ))} +
+ {/* Sender info and date */}
@@ -90,13 +96,13 @@ ${email.body}`, {/* Email body with safe links */}
{/* Attachments if any */} {email.files && email.files.length > 0 && (
-
Attachments:
+
Attachments:
)} @@ -121,7 +127,7 @@ ${email.body}`, onSend={() => setComposeProps(null)} offset={0} draftMail={{ - sentTo: composeProps.recipients.map(mail => ({ mail })), + sentTo: composeProps.recipients.map(mail => ({mail})), subject: composeProps.subject, body: composeProps.body, attachments: composeProps.attachments From 03f59bcb483cecf76d9a4a607d2dfeef1b01bdf6 Mon Sep 17 00:00:00 2001 From: Dor Darmon Date: Wed, 9 Jul 2025 10:18:07 +0300 Subject: [PATCH 06/25] - Enhanced ToolBar with applyLabelsToMail logic inside handleLabelToggle - Modified labelsApi to support fetching updated labels list - Ensured mailApi.applyLabelsToMail is triggered on label changes - Improved user experience by removing need for manual refresh after label modifications --- frontend/src/api/labelsApi.js | 79 ++++++++++++++++----- frontend/src/api/mailApi.js | 10 +-- frontend/src/main_page/mail_row/MailRow.jsx | 10 ++- frontend/src/main_page/toolbar/ToolBar.jsx | 29 +++++++- web_server/controllers/labels.js | 61 ++++++++++------ 5 files changed, 135 insertions(+), 54 deletions(-) diff --git a/frontend/src/api/labelsApi.js b/frontend/src/api/labelsApi.js index c38528a8..89d579f0 100644 --- a/frontend/src/api/labelsApi.js +++ b/frontend/src/api/labelsApi.js @@ -1,17 +1,19 @@ // src/api/labelsApi.js -// Base URL — adjust port/host if needed +import { applyLabelsToMail, getMailsByLabel } from "./mailApi"; + const API_BASE = "http://localhost:3001/api"; const ROOT = `${API_BASE}/labels`; /** - * Fetch all labels + * Fetch all labels for current user * @returns {Promise>} */ export async function fetchLabels() { const token = localStorage.getItem("token"); - const res = await fetch(ROOT, { - method: "GET", + const userEmail = localStorage.getItem("email"); + const res = await fetch(`${ROOT}?email=${encodeURIComponent(userEmail)}`, { + method: "GET", headers: { "Authorization": `Bearer ${token}` } @@ -21,34 +23,52 @@ export async function fetchLabels() { } /** - * Create a new label + * Create a new label, prevent duplicates (client-side check) * @param {string} name */ export async function createLabel(name) { const token = localStorage.getItem("token"); + const userEmail = localStorage.getItem("email"); + + const nameNormalized = name.trim().toLowerCase(); + + // Fetch current user's labels + const existingLabels = await fetchLabels(); + const isDuplicate = existingLabels.some( + label => label.name.trim().toLowerCase() === nameNormalized + ); + + if (isDuplicate) { + alert(`A label named "${name}" already exists.`); + return; + } + + // Send request to server const res = await fetch(ROOT, { - method: "POST", + method: "POST", headers: { - "Authorization": `Bearer ${token}`, - "Content-Type": "application/json" + "Authorization": `Bearer ${token}`, + "Content-Type": "application/json" }, - body: JSON.stringify({ name }) + body: JSON.stringify({ name, email: userEmail }) }); + if (!res.ok) throw new Error(`createLabel failed: ${res.status}`); } + /** - * Rename an existing label + * Rename a label * @param {number} id * @param {string} newName */ export async function editLabel(id, newName) { const token = localStorage.getItem("token"); const res = await fetch(`${ROOT}/${id}`, { - method: "PATCH", + method: "PATCH", headers: { - "Authorization": `Bearer ${token}`, - "Content-Type": "application/json" + "Authorization": `Bearer ${token}`, + "Content-Type": "application/json" }, body: JSON.stringify({ name: newName }) }); @@ -56,35 +76,56 @@ export async function editLabel(id, newName) { } /** - * Delete a label + * Delete label and remove it from all mails for this user * @param {number} id */ export async function deleteLabel(id) { const token = localStorage.getItem("token"); + + try { + // Step 1: get only current user's mails that use this label + const { mails } = await getMailsByLabel(id); + + // Step 2: remove the label from each mail + for (const mail of mails) { + const newLabels = (mail.labels || []).filter( + label => label && label.id !== id && label !== id + ); + await applyLabelsToMail(mail.id, newLabels); + } + } catch (e) { + console.warn("Label cleanup failed", e); + } + + // Step 3: remove label from database const res = await fetch(`${ROOT}/${id}`, { - method: "DELETE", + method: "DELETE", headers: { "Authorization": `Bearer ${token}` } }); + if (!res.ok) throw new Error(`deleteLabel failed: ${res.status}`); } /** - * Create a sublabel under a parent label + * Create a sublabel (child label) under a parent label * @param {number} parentId * @param {string} name */ export async function createLabelUnderParent(parentId, name) { const token = localStorage.getItem("token"); + const userEmail = localStorage.getItem("email"); + const res = await fetch(`${ROOT}/${parentId}/sublabel`, { - method: "POST", + method: "POST", headers: { "Authorization": `Bearer ${token}`, - "Content-Type": "application/json" + "Content-Type": "application/json" }, - body: JSON.stringify({ name }) + body: JSON.stringify({ name, email: userEmail }) }); + if (!res.ok) throw new Error(`createLabelUnderParent failed: ${res.status}`); return res.json(); } diff --git a/frontend/src/api/mailApi.js b/frontend/src/api/mailApi.js index 94a6764e..3fc24fdb 100644 --- a/frontend/src/api/mailApi.js +++ b/frontend/src/api/mailApi.js @@ -296,16 +296,18 @@ export async function applyLabelsToMail(mailId, labels) { console.log(labels) const token = localStorage.getItem("token"); const url = `${API_BASE}/mails/${mailId}`; + // Safely filter out any null or malformed labels + const sanitizedLabels = (labels || []) + .filter(label => label && typeof label.id === 'number'); + const res = await fetch(url, { method: "PATCH", headers: { "Authorization": `Bearer ${token}`, - 'Content-Type': 'application/json' + "Content-Type": "application/json" }, - body: JSON.stringify({ - labels: labels, - }) + body: JSON.stringify({labels: sanitizedLabels}) }) if (!res.ok) { throw new Error(`getMailsByLabel failed: ${res.status}`); diff --git a/frontend/src/main_page/mail_row/MailRow.jsx b/frontend/src/main_page/mail_row/MailRow.jsx index 9bd24dd4..6cb14745 100644 --- a/frontend/src/main_page/mail_row/MailRow.jsx +++ b/frontend/src/main_page/mail_row/MailRow.jsx @@ -24,7 +24,6 @@ const MailRow = ({ onOpenDraft }) => { const navigate = useNavigate(); - const handleMailOpen = async () => { await markMailAsRead(email, onUpdate); if (inboxType === "draft" && onOpenDraft) { @@ -53,10 +52,9 @@ const MailRow = ({
{email.subject}
{email.labels?.map(label => ( - - {label.name} - + + {label.name} + ))}
{stripHtml(email.body)}
@@ -75,4 +73,4 @@ const MailRow = ({
); } -export default MailRow; +export default MailRow; \ No newline at end of file diff --git a/frontend/src/main_page/toolbar/ToolBar.jsx b/frontend/src/main_page/toolbar/ToolBar.jsx index 79bbafe8..fd554284 100644 --- a/frontend/src/main_page/toolbar/ToolBar.jsx +++ b/frontend/src/main_page/toolbar/ToolBar.jsx @@ -41,6 +41,28 @@ const ToolBar = ({ }; loadLabels(); }, []); + // Helper to reload labels after changes + const reload = async () => setLabels(await fetchLabels()); + useEffect(() => { + if (selectedMails.size === 0) return; + + Array.from(selectedMails).forEach((mail) => { + // סנן את התגיות של המייל כך שיכילו רק תגיות שעדיין קיימות + const updatedLabels = mail.labels.filter(label => + labels.some(l => l.id === label.id) + ); + + // אם שם תגית השתנה, עדכן אותו + updatedLabels.forEach(label => { + const fresh = labels.find(l => l.id === label.id); + if (fresh) label.name = fresh.name; + }); + + // שלח עדכון לשרת אם יש שינוי + applyLabelsToMail(mail.id, updatedLabels); + }); + + }, [reload]); // apply label selection and refresh mails const handleLabelToggle = async (label) => { @@ -60,6 +82,7 @@ const ToolBar = ({ } // update UI mail.labels = newLabels; + mail._forceUpdate = Date.now(); // Send new label list to backend await applyLabelsToMail(mail.id, newLabels); @@ -149,7 +172,7 @@ const ToolBar = ({
)} {/* label picker for selected mails */} - + Array.isArray(mail.labels) && - mail.labels.some(l => l.id === label.id) + mail.labels + .filter(l => l && typeof l.id === "number") + .some(l => l.id === label.id) )} onChange={() => handleLabelToggle(label)} /> diff --git a/web_server/controllers/labels.js b/web_server/controllers/labels.js index 6233cdc4..fd7039b5 100644 --- a/web_server/controllers/labels.js +++ b/web_server/controllers/labels.js @@ -2,13 +2,13 @@ const Labels = require('../models/labels'); /** * GET /api/labels - * Return all labels (flat list), including sublabels, for the authenticated user. - * Responds: [{ id, name, parent }] + * Return all labels for the current user only */ exports.getAllLabels = (req, res) => { - const all = Labels.getAllLabels(); - // Only send fields required by frontend; "parent" is id of parent label (or null for root) - res.status(200).json(all.map(l => ({ + const userId = +req.user.id; + const userLabels = Labels.getAllLabels().filter(l => l.owner === userId); + + res.status(200).json(userLabels.map(l => ({ id: l.id, name: l.name, parent: l.parent @@ -17,58 +17,73 @@ exports.getAllLabels = (req, res) => { /** * POST /api/labels - * Create a new root-level label for the user. - * Request body: { name } - * Responds: created label object, 201 status. + * Create a new label for the current user */ exports.createNewLabel = (req, res) => { - const userId = +req.user.id; // User ID from authentication middleware + const userId = +req.user.id; const { name } = req.body; + if (!name) return res.status(400).json({ error: 'Name required' }); + + // Check for duplicate name + const existing = Labels.getAllLabels().find( + l => l.owner === userId && l.name.trim().toLowerCase() === name.trim().toLowerCase() + ); + if (existing) + return res.status(409).json({ error: 'Label name already exists' }); + const lab = Labels.createNewLabel(userId, name); res.status(201).location(`/api/labels/${lab.id}`).json(lab); }; /** * POST /api/labels/:id/sublabel - * Create a new sublabel under a parent label (parent ID in URL). - * Request body: { name } - * Responds: created sublabel object, or 404 if parent not found. + * Create a new sublabel under an existing parent label */ exports.createSublabel = (req, res) => { const userId = +req.user.id; const parentId = +req.params.id; const { name } = req.body; + if (!name) return res.status(400).json({ error: 'Name required' }); - // Model should handle parent existence/ownership + const lab = Labels.createSublabel(userId, parentId, name); - if (!lab) return res.status(404).json({ error: 'Parent not found' }); + if (!lab) return res.status(404).json({ error: 'Parent label not found or not yours' }); + res.status(201).location(`/api/labels/${lab.id}`).json(lab); }; /** * PATCH /api/labels/:id - * Rename an existing label by ID. - * Request body: { name } - * Responds: 204 on success, 404 if not found. + * Rename an existing label (only if owned by user) */ exports.editLabel = (req, res) => { + const userId = +req.user.id; const id = +req.params.id; const { name } = req.body; + if (!name) return res.status(400).json({ error: 'Name required' }); - const ok = Labels.editLabelById(id, name); - if (!ok) return res.status(404).json({ error: 'Not found' }); + + const label = Labels.getLabelById(id); + if (!label || label.owner !== userId) + return res.status(403).json({ error: 'Access denied' }); + + label.name = name; res.status(204).end(); }; /** * DELETE /api/labels/:id - * Delete label (and, usually, its sublabels) by ID. - * Responds: 204 on success, 404 if not found. + * Delete a label owned by the current user */ exports.deleteLabel = (req, res) => { + const userId = +req.user.id; const id = +req.params.id; - if (!Labels.deleteLabelById(id)) - return res.status(404).json({ error: 'Not found' }); + + const label = Labels.getLabelById(id); + if (!label || label.owner !== userId) + return res.status(403).json({ error: 'Access denied' }); + + Labels.deleteLabelById(id); res.status(204).end(); }; From c318e089b9091d0077b701dd45cb7e4b4825fc85 Mon Sep 17 00:00:00 2001 From: Dor Darmon Date: Sun, 13 Jul 2025 01:37:04 +0300 Subject: [PATCH 07/25] - Enhanced ToolBar with applyLabelsToMail logic inside handleLabelToggle - Modified labelsApi to support fetching updated labels list - Ensured mailApi.applyLabelsToMail is triggered on label changes - Improved user experience by removing need for manual refresh after label modifications --- frontend/src/api/labelsApi.js | 81 +++++-------------- .../main_page/EmailSideMenu/EmailSideMenu.jsx | 45 +++++++++-- frontend/src/main_page/MainPage.js | 14 ++++ frontend/src/main_page/mail_row/MailRow.jsx | 21 +++-- frontend/src/main_page/toolbar/ToolBar.jsx | 32 +++----- web_server/controllers/labels.js | 68 ++++++++-------- web_server/controllers/mails.js | 7 +- web_server/models/labels.js | 72 ++++++++++++----- web_server/models/mails.js | 29 ++----- web_server/utils/labels.js | 10 ++- 10 files changed, 194 insertions(+), 185 deletions(-) diff --git a/frontend/src/api/labelsApi.js b/frontend/src/api/labelsApi.js index 89d579f0..25cc2251 100644 --- a/frontend/src/api/labelsApi.js +++ b/frontend/src/api/labelsApi.js @@ -1,19 +1,17 @@ // src/api/labelsApi.js -import { applyLabelsToMail, getMailsByLabel } from "./mailApi"; - +// Base URL — adjust port/host if needed const API_BASE = "http://localhost:3001/api"; const ROOT = `${API_BASE}/labels`; /** - * Fetch all labels for current user + * Fetch all labels * @returns {Promise>} */ export async function fetchLabels() { const token = localStorage.getItem("token"); - const userEmail = localStorage.getItem("email"); - const res = await fetch(`${ROOT}?email=${encodeURIComponent(userEmail)}`, { - method: "GET", + const res = await fetch(ROOT, { + method: "GET", headers: { "Authorization": `Bearer ${token}` } @@ -23,52 +21,34 @@ export async function fetchLabels() { } /** - * Create a new label, prevent duplicates (client-side check) + * Create a new label * @param {string} name */ export async function createLabel(name) { const token = localStorage.getItem("token"); - const userEmail = localStorage.getItem("email"); - - const nameNormalized = name.trim().toLowerCase(); - - // Fetch current user's labels - const existingLabels = await fetchLabels(); - const isDuplicate = existingLabels.some( - label => label.name.trim().toLowerCase() === nameNormalized - ); - - if (isDuplicate) { - alert(`A label named "${name}" already exists.`); - return; - } - - // Send request to server const res = await fetch(ROOT, { - method: "POST", + method: "POST", headers: { - "Authorization": `Bearer ${token}`, - "Content-Type": "application/json" + "Authorization": `Bearer ${token}`, + "Content-Type": "application/json" }, - body: JSON.stringify({ name, email: userEmail }) + body: JSON.stringify({ name }) }); - if (!res.ok) throw new Error(`createLabel failed: ${res.status}`); } - /** - * Rename a label + * Rename an existing label * @param {number} id * @param {string} newName */ export async function editLabel(id, newName) { const token = localStorage.getItem("token"); const res = await fetch(`${ROOT}/${id}`, { - method: "PATCH", + method: "PATCH", headers: { - "Authorization": `Bearer ${token}`, - "Content-Type": "application/json" + "Authorization": `Bearer ${token}`, + "Content-Type": "application/json" }, body: JSON.stringify({ name: newName }) }); @@ -76,56 +56,35 @@ export async function editLabel(id, newName) { } /** - * Delete label and remove it from all mails for this user + * Delete a label * @param {number} id */ export async function deleteLabel(id) { const token = localStorage.getItem("token"); - - try { - // Step 1: get only current user's mails that use this label - const { mails } = await getMailsByLabel(id); - - // Step 2: remove the label from each mail - for (const mail of mails) { - const newLabels = (mail.labels || []).filter( - label => label && label.id !== id && label !== id - ); - await applyLabelsToMail(mail.id, newLabels); - } - } catch (e) { - console.warn("Label cleanup failed", e); - } - - // Step 3: remove label from database const res = await fetch(`${ROOT}/${id}`, { - method: "DELETE", + method: "DELETE", headers: { "Authorization": `Bearer ${token}` } }); - if (!res.ok) throw new Error(`deleteLabel failed: ${res.status}`); } /** - * Create a sublabel (child label) under a parent label + * Create a sublabel under a parent label * @param {number} parentId * @param {string} name */ export async function createLabelUnderParent(parentId, name) { const token = localStorage.getItem("token"); - const userEmail = localStorage.getItem("email"); - const res = await fetch(`${ROOT}/${parentId}/sublabel`, { - method: "POST", + method: "POST", headers: { "Authorization": `Bearer ${token}`, - "Content-Type": "application/json" + "Content-Type": "application/json" }, - body: JSON.stringify({ name, email: userEmail }) + body: JSON.stringify({ name }) }); - if (!res.ok) throw new Error(`createLabelUnderParent failed: ${res.status}`); return res.json(); -} +} \ No newline at end of file diff --git a/frontend/src/main_page/EmailSideMenu/EmailSideMenu.jsx b/frontend/src/main_page/EmailSideMenu/EmailSideMenu.jsx index 4cbf8b93..bb13648c 100644 --- a/frontend/src/main_page/EmailSideMenu/EmailSideMenu.jsx +++ b/frontend/src/main_page/EmailSideMenu/EmailSideMenu.jsx @@ -33,7 +33,8 @@ export default function EmailSidebar({ onComposeClick, // Callback for "Compose" button showSidebar, // Boolean: is sidebar open (mobile) setShowSidebar, // Callback to toggle sidebar (mobile) - clearSelection + clearSelection, + refreshMails }) { const isMobile = useIsMobile(); const [labels, setLabels] = useState([]); // Flat list of all labels (from backend) @@ -56,16 +57,36 @@ export default function EmailSidebar({ const handleAddLabel = async () => { const name = prompt("New label name:"); if (!name) return; - await createLabel(name); - await reload(); + + try { + await createLabel(name); + await reload(); + } catch (err) { + if (err.message.includes("409")) { + alert(`A label named "${name}" already exists.`); + } else { + alert("An error occurred while creating the label."); + console.error(err); + } + } }; // Rename label const handleEditLabel = async lab => { const name = prompt("Rename label:", lab.name); if (!name || name === lab.name) return; - await editLabel(lab.id, name); - await reload(); + try { + await editLabel(lab.id, name); + await reload(); + await refreshMails(); + } catch (err) { + if (err.message.includes("409")) { + alert(`A label named "${name}" already exists.`); + } else { + alert("An error occurred while creating the label."); + console.error(err); + } + } }; // Delete label with confirmation @@ -73,14 +94,24 @@ export default function EmailSidebar({ if (!window.confirm(`Delete "${lab.name}"?`)) return; await deleteLabel(lab.id); await reload(); + await refreshMails(); }; // Add sublabel under parent const handleAddSublabel = async lab => { const name = prompt(`Sublabel name under "${lab.name}":`); if (!name) return; - await createLabelUnderParent(lab.id, name); - await reload(); + try { + await createLabelUnderParent(lab.id, name); + await reload(); + } catch (err) { + if (err.message.includes("409")) { + alert(`A label named "${name}" already exists.`); + } else { + alert("An error occurred while creating the label."); + console.error(err); + } + } }; // Open the context menu at the position of the clicked label diff --git a/frontend/src/main_page/MainPage.js b/frontend/src/main_page/MainPage.js index 05d69993..db0035cb 100644 --- a/frontend/src/main_page/MainPage.js +++ b/frontend/src/main_page/MainPage.js @@ -13,6 +13,7 @@ import { useTheme } from "../utils/useTheme"; import { useRequireAuth } from "../utils/useAutoLogin"; import SkeletonEmail from "../components/loading/SkeletonEmail"; import useIsMobile from "../utils/useIsMobile"; +import { fetchLabels } from "../api/labelsApi"; const MainPage = () => { useRequireAuth(); @@ -87,6 +88,17 @@ const MainPage = () => { } }, [location.state?.inboxType]); + const [labels, setLabels] = useState([]); + + const loadLabels = async () => { + const data = await fetchLabels(); + setLabels(data); + }; + + useEffect(() => { + loadLabels(); + }, []); + return (
{ showSidebar={showSidebar} setShowSidebar={setShowSidebar} clearSelection={handlers.clearSelection} + refreshMails={refreshMails} />
@@ -137,6 +150,7 @@ const MainPage = () => { onSelect={handleSelect} onUpdate={refreshMails} onOpenDraft={handleOpenDraft} + allLabels={labels} /> )) )} diff --git a/frontend/src/main_page/mail_row/MailRow.jsx b/frontend/src/main_page/mail_row/MailRow.jsx index 6cb14745..a4d9f5fb 100644 --- a/frontend/src/main_page/mail_row/MailRow.jsx +++ b/frontend/src/main_page/mail_row/MailRow.jsx @@ -13,6 +13,7 @@ import {markMailAsRead, stripHtml} from "../../utils/mailUtils"; * @prop onSelect (mail,checked)=>void * @prop onUpdate ()=>void * @prop onOpenDraft (mail)=>void + * @prop allLabels list of label */ const MailRow = ({ theme, @@ -21,9 +22,11 @@ const MailRow = ({ isSelected, onSelect, onUpdate, - onOpenDraft + onOpenDraft, + allLabels }) => { const navigate = useNavigate(); + // Handle opening an email (navigate to full view or open draft) const handleMailOpen = async () => { await markMailAsRead(email, onUpdate); if (inboxType === "draft" && onOpenDraft) { @@ -32,7 +35,6 @@ const MailRow = ({ navigate(`/mails/${email.id}`, {state: {inboxType}}); } }; - return (
@@ -47,15 +49,18 @@ const MailRow = ({
-
{email.from.fullName}
+
{email.from?.fullName || "Unknown Sender"}
{email.subject}
- {email.labels?.map(label => ( - - {label.name} - - ))} + {(email.labels || []).map(label => { + const updated = allLabels?.find(l => l.id === label.id); + return ( + + {updated?.name || label.name} + + ); + })}
{stripHtml(email.body)}
diff --git a/frontend/src/main_page/toolbar/ToolBar.jsx b/frontend/src/main_page/toolbar/ToolBar.jsx index fd554284..7d08dde3 100644 --- a/frontend/src/main_page/toolbar/ToolBar.jsx +++ b/frontend/src/main_page/toolbar/ToolBar.jsx @@ -42,27 +42,14 @@ const ToolBar = ({ loadLabels(); }, []); // Helper to reload labels after changes - const reload = async () => setLabels(await fetchLabels()); - useEffect(() => { - if (selectedMails.size === 0) return; - - Array.from(selectedMails).forEach((mail) => { - // סנן את התגיות של המייל כך שיכילו רק תגיות שעדיין קיימות - const updatedLabels = mail.labels.filter(label => - labels.some(l => l.id === label.id) - ); - - // אם שם תגית השתנה, עדכן אותו - updatedLabels.forEach(label => { - const fresh = labels.find(l => l.id === label.id); - if (fresh) label.name = fresh.name; - }); - - // שלח עדכון לשרת אם יש שינוי - applyLabelsToMail(mail.id, updatedLabels); - }); - - }, [reload]); + const reload = async () => { + try { + const data = await fetchLabels(); + setLabels(data); + } catch (e) { + console.error("Failed to reload labels:", e); + } + }; // apply label selection and refresh mails const handleLabelToggle = async (label) => { @@ -81,7 +68,8 @@ const ToolBar = ({ newLabels = Array.from(labelMap.values()); } // update UI - mail.labels = newLabels; + mail.labels = newLabels.filter(label => label && typeof label.id === "number" + && typeof label.name === "string"); mail._forceUpdate = Date.now(); // Send new label list to backend await applyLabelsToMail(mail.id, newLabels); diff --git a/web_server/controllers/labels.js b/web_server/controllers/labels.js index fd7039b5..4c936172 100644 --- a/web_server/controllers/labels.js +++ b/web_server/controllers/labels.js @@ -2,13 +2,14 @@ const Labels = require('../models/labels'); /** * GET /api/labels - * Return all labels for the current user only + * Return all labels (flat list), belonging only to the authenticated user. + * Responds: [{ id, name, parent }] */ exports.getAllLabels = (req, res) => { const userId = +req.user.id; - const userLabels = Labels.getAllLabels().filter(l => l.owner === userId); - - res.status(200).json(userLabels.map(l => ({ + const all = Labels.getAllLabels(userId); + // Only send fields required by frontend; "parent" is id of parent label (or null for root) + res.status(200).json(all.map(l => ({ id: l.id, name: l.name, parent: l.parent @@ -17,73 +18,70 @@ exports.getAllLabels = (req, res) => { /** * POST /api/labels - * Create a new label for the current user + * Create a new root-level label for the user. + * Request body: { name } + * Responds: created label object, 201 status. */ exports.createNewLabel = (req, res) => { + // User ID from authentication middleware const userId = +req.user.id; const { name } = req.body; - if (!name) return res.status(400).json({ error: 'Name required' }); - - // Check for duplicate name - const existing = Labels.getAllLabels().find( - l => l.owner === userId && l.name.trim().toLowerCase() === name.trim().toLowerCase() - ); - if (existing) - return res.status(409).json({ error: 'Label name already exists' }); - const lab = Labels.createNewLabel(userId, name); + if (!lab) return res.status(409).json({ error: 'Label name already exists' }); res.status(201).location(`/api/labels/${lab.id}`).json(lab); }; /** * POST /api/labels/:id/sublabel - * Create a new sublabel under an existing parent label + * Create a new sublabel under a parent label (parent ID in URL). + * Request body: { name } + * Responds: created sublabel object, or 404 if parent not found. */ exports.createSublabel = (req, res) => { const userId = +req.user.id; const parentId = +req.params.id; const { name } = req.body; - if (!name) return res.status(400).json({ error: 'Name required' }); - + // Model should handle parent existence/ownership const lab = Labels.createSublabel(userId, parentId, name); - if (!lab) return res.status(404).json({ error: 'Parent label not found or not yours' }); - + if (!lab) return res.status(404).json({ error: 'Parent not found' }); + else if (!lab) return res.status(409).json({ error: 'Label name already exists' }); res.status(201).location(`/api/labels/${lab.id}`).json(lab); }; /** * PATCH /api/labels/:id - * Rename an existing label (only if owned by user) + * Rename an existing label by ID. + * Request body: { name } + * Responds: 204 on success, 404 if not found. */ exports.editLabel = (req, res) => { - const userId = +req.user.id; const id = +req.params.id; + const userId = +req.user.id; const { name } = req.body; - if (!name) return res.status(400).json({ error: 'Name required' }); + const ok = Labels.editLabelById(id,userId, name); + if (!ok) return res.status(404).json({ error: 'Not found' }); - const label = Labels.getLabelById(id); - if (!label || label.owner !== userId) - return res.status(403).json({ error: 'Access denied' }); - - label.name = name; res.status(204).end(); }; /** * DELETE /api/labels/:id - * Delete a label owned by the current user + * Delete label (and, usually, its sublabels) by ID. + * Responds: 204 on success, 404 if not found. */ exports.deleteLabel = (req, res) => { + const labelId = +req.params.id; const userId = +req.user.id; - const id = +req.params.id; - const label = Labels.getLabelById(id); - if (!label || label.owner !== userId) - return res.status(403).json({ error: 'Access denied' }); + if (!(labelId)) + return res.status(400).json({ error: 'Invalid label ID' }); - Labels.deleteLabelById(id); - res.status(204).end(); -}; + const deleted = Labels.deleteLabelById(labelId, userId); + if (!deleted) + return res.status(404).json({ error: 'Label not found or not owned by user' }); + + return res.status(204).send(); +} \ No newline at end of file diff --git a/web_server/controllers/mails.js b/web_server/controllers/mails.js index 0e2e9343..23b517eb 100644 --- a/web_server/controllers/mails.js +++ b/web_server/controllers/mails.js @@ -2,7 +2,7 @@ const Mails = require('../models/mails'); const Blacklist = require('../models/blacklist'); const {extractUrls} = require("../utils/mails"); const {convertMailsToIds, usersToFullElement} = require("../utils/users"); -const {convertLabelsToIds, labelsToFullElement, mailLabelNames} = require("../utils/labels"); +const {convertLabelsToIds, labelsToFullElement} = require("../utils/labels"); /** * Gets the last 50 mails of a user, ordered by the most recent (first) to least recent (last) @@ -19,7 +19,8 @@ const getLastMailsOrdered = (req, res) => { return res.status(400).json({error: 'User not authenticated - failed fetching last 50 mails'}); const inboxType = req.query.inboxType || 'all'; const page = Number(req.query.page) || 1; - const limit = Number(req.query.limit) || 50; // limit is 50 according to instructions + // limit is 50 according to instructions + const limit = Number(req.query.limit) || 50; const labelIdFilter = Number(req.query.label); let {paged, total} = Mails.getUserMails(userId, limit, inboxType, page); @@ -72,7 +73,7 @@ const getMailById = (req, res) => { if (!mail) return res.status(404).json({error: `No mail found with ID: ${id}`}); // ensure the mail belongs to the user - if (mail.owner != userId) + if (mail.owner !== userId) return res.status(403).json({error: 'mail do not belong to user'}); return res.status(200).json({ ...mail, diff --git a/web_server/models/labels.js b/web_server/models/labels.js index c8ebe822..3a8b7607 100644 --- a/web_server/models/labels.js +++ b/web_server/models/labels.js @@ -1,27 +1,26 @@ // Initial example labels -let labels = [ - { id: 1, name: 'work', owner: 1, parent: null }, - { id: 2, name: 'friends', owner: 1, parent: null }, -]; +let labels = []; // Simple incrementing ID let nextId = labels.length + 1; /** - * Get all labels (no user filter; in a real app, should filter by user) - * @returns {Array} All label objects + * Return all labels owned by a specific user. + * @param {number} userId - ID of the user requesting the labels + * @returns {Array} Filtered label objects owned by the user */ -function getAllLabels() { - return labels; +function getAllLabels(userId) { + return labels.filter(l => l.owner === userId); } /** - * Find a label by its ID - * @param {number} id + * Find a label by its ID and owner + * @param {number} id - ID of label + * @param {number} owner - ID of the user * @returns {object|null} Label object or null if not found */ -function getLabelById(id) { - return labels.find(l => l.id === id); +function getLabelById(id,owner) { + return labels.find(l => l.id === id && l.owner === owner) || null; } /** @@ -31,6 +30,10 @@ function getLabelById(id) { * @returns {object} The created label */ function createNewLabel(owner, name) { + // Check if user already has a label with the same name + const exists = labels.some(l => l.owner === owner && l.name === name && l.parent === null); + if (exists) return null; + const lab = { id: nextId++, owner, name, parent: null }; labels.push(lab); return lab; @@ -45,12 +48,22 @@ function createNewLabel(owner, name) { */ function createSublabel(owner, parent, name) { // Check parent exists (could also check ownership) - if (!labels.find(l => l.id === parent)) return null; + const parentLabel = labels.find(l => l.id === parent && l.owner === owner); + if (!parentLabel) return null; + + // Prevent duplicate sublabel names under the same parent for the same user + const duplicate = labels.some(l => + l.owner === owner && + l.parent === parent && + l.name === name + ); + + if (duplicate) return null const newLabel = { - id: nextId++, // Use global nextId for unique ID + id: nextId++, name, owner, - parent, // Link to parent label by id + parent, }; labels.push(newLabel); return newLabel; @@ -59,24 +72,39 @@ function createSublabel(owner, parent, name) { /** * Rename a label by ID * @param {number} id - * @param {string} name + * @param {string} newName + * @param {number} owner - User ID * @returns {object|null} Updated label or null if not found */ -function editLabelById(id, name) { - const lab = getLabelById(id); +function editLabelById(id, owner,newName) { + const lab = labels.find(l => l.id === id && l.owner === owner); if (!lab) return null; - lab.name = name; + // Check if another label with the same name already exists + const duplicate = labels.find(l => l.name === newName && l.owner === owner && l.id !== id); + if (duplicate) { + throw new Error("409"); + } + // Rename the label + lab.name = newName; return lab; } /** * Delete a label by ID (does not cascade to sublabels) * @param {number} id + * @param {number} owner - User ID * @returns {boolean} True if deleted, false if not found */ -function deleteLabelById(id) { - const idx = labels.findIndex(l => l.id === id); +function deleteLabelById(id,owner) { + const idx = labels.findIndex(l => l.id === id&& l.owner === owner); if (idx < 0) return false; + // Recursively delete children + const childLabels = labels.filter(l => l.parent === id && l.owner === owner); + childLabels.forEach(child => { + deleteLabelById(child.id, owner); + }); + + // Delete the label itself labels.splice(idx, 1); return true; } @@ -89,4 +117,4 @@ module.exports = { createSublabel, editLabelById, deleteLabelById -}; +}; \ No newline at end of file diff --git a/web_server/models/mails.js b/web_server/models/mails.js index 07e3e3a5..2f534a7d 100644 --- a/web_server/models/mails.js +++ b/web_server/models/mails.js @@ -31,7 +31,7 @@ const mails = [ body: "Here’s what we changed in v2.0...", createdAt: new Date('2025-06-03T20:27:11.000Z'), sentAt: new Date('2025-06-04T14:08:36.000Z'), - labels: [1], + labels: [], isDraft: false, isRead: true, isStarred: false, @@ -48,7 +48,7 @@ const mails = [ body: "Here’s what we changed in v2.0...", createdAt: new Date('2025-06-03T20:27:11.000Z'), sentAt: new Date('2025-06-04T14:08:36.000Z'), - labels: [2], + labels: [], isDraft: false, isRead: false, isStarred: false, @@ -82,7 +82,7 @@ const mails = [ body: "Don’t forget the team meeting at 9AM tomorrow.", createdAt: new Date('2025-06-03T03:15:27.000Z'), sentAt: new Date('2025-06-03T11:42:53.000Z'), - labels: [1,2], + labels: [], isDraft: false, isRead: false, isStarred: true, @@ -94,23 +94,6 @@ const mails = [ id: 5, owner: 1, from: 1, - sentTo: [2], - subject: "Your daily digest", - body: "Top tech news today: React 21.0 is out...", - createdAt: new Date('2025-06-02T18:55:04.000Z'), - sentAt: "", - labels: [], - isDraft: true, - isRead: false, - isStarred: false, - isTrashed: false, - isSpam: false, - files: [] - }, - { - id: 6, - owner: 1, - from: 1, sentTo: [1, 2, 3], subject: "My new picture", body: "Whats up guys look at my new picture!", @@ -142,7 +125,7 @@ const mails = [ ] }, { - id: 7, + id: 6, owner: 2, from: 1, sentTo: [1, 2, 3], @@ -164,7 +147,7 @@ const mails = [ ] }, { - id: 8, + id: 7, owner: 3, from: 1, sentTo: [1, 2, 3], @@ -443,4 +426,4 @@ module.exports = { updateDraft, deleteMail, searchInInbox, -}; +}; \ No newline at end of file diff --git a/web_server/utils/labels.js b/web_server/utils/labels.js index e414cfa2..c7c7fce6 100644 --- a/web_server/utils/labels.js +++ b/web_server/utils/labels.js @@ -22,7 +22,9 @@ function convertLabelsToIds(userId, labelsObjects) { function labelsToFullElement(userId, labelsIds) { if (!labelsIds) return []; - const objects = labelsIds.map(id => Labels.getLabelById(id)); + const objects = labelsIds + .map(id => Labels.getLabelById(id, userId)) + .filter(label => label); return [...new Set(objects)]; } @@ -33,9 +35,9 @@ function labelsToFullElement(userId, labelsIds) { */ const mailLabelNames = (userId, mail) => { return (mail.labels || []) - .map(labelId => Labels.getLabelById(labelId).name) - .filter(Boolean) - .map(name => name.toLowerCase()); + .map(labelId => Labels.getLabelById(labelId, userId)) + .filter(label => label && label.name) + .map(label => label.name.toLowerCase()); }; module.exports = {convertLabelsToIds, labelsToFullElement, mailLabelNames} \ No newline at end of file From c2409036ab4511f5f42672b50a6d338ffc9ce3ad Mon Sep 17 00:00:00 2001 From: Dor Darmon Date: Sun, 13 Jul 2025 14:35:46 +0300 Subject: [PATCH 08/25] - Enhanced ToolBar with applyLabelsToMail logic inside handleLabelToggle - Modified labelsApi to support fetching updated labels list - Ensured mailApi.applyLabelsToMail is triggered on label changes - Improved user experience by removing need for manual refresh after label modifications --- frontend/src/api/mailApi.js | 5 +---- frontend/src/main_page/mail_row/MailRow.jsx | 13 +++++-------- frontend/src/main_page/toolbar/ToolBar.jsx | 4 +--- 3 files changed, 7 insertions(+), 15 deletions(-) diff --git a/frontend/src/api/mailApi.js b/frontend/src/api/mailApi.js index 3fc24fdb..1c8903eb 100644 --- a/frontend/src/api/mailApi.js +++ b/frontend/src/api/mailApi.js @@ -296,9 +296,6 @@ export async function applyLabelsToMail(mailId, labels) { console.log(labels) const token = localStorage.getItem("token"); const url = `${API_BASE}/mails/${mailId}`; - // Safely filter out any null or malformed labels - const sanitizedLabels = (labels || []) - .filter(label => label && typeof label.id === 'number'); const res = await fetch(url, { method: "PATCH", @@ -307,7 +304,7 @@ export async function applyLabelsToMail(mailId, labels) { "Content-Type": "application/json" }, - body: JSON.stringify({labels: sanitizedLabels}) + body: JSON.stringify({labels}) }) if (!res.ok) { throw new Error(`getMailsByLabel failed: ${res.status}`); diff --git a/frontend/src/main_page/mail_row/MailRow.jsx b/frontend/src/main_page/mail_row/MailRow.jsx index a4d9f5fb..dd4c8a06 100644 --- a/frontend/src/main_page/mail_row/MailRow.jsx +++ b/frontend/src/main_page/mail_row/MailRow.jsx @@ -53,14 +53,11 @@ const MailRow = ({
{email.subject}
- {(email.labels || []).map(label => { - const updated = allLabels?.find(l => l.id === label.id); - return ( - - {updated?.name || label.name} - - ); - })} + {(email.labels || []).map(label => ( + + {label.name} + + ))}
{stripHtml(email.body)}
diff --git a/frontend/src/main_page/toolbar/ToolBar.jsx b/frontend/src/main_page/toolbar/ToolBar.jsx index 7d08dde3..6eb6ee8c 100644 --- a/frontend/src/main_page/toolbar/ToolBar.jsx +++ b/frontend/src/main_page/toolbar/ToolBar.jsx @@ -176,9 +176,7 @@ const ToolBar = ({ id={`label-check-${label.id}`} checked={Array.from(selectedMails).every(mail => Array.isArray(mail.labels) && - mail.labels - .filter(l => l && typeof l.id === "number") - .some(l => l.id === label.id) + mail.labels.some(l => l.id === label.id) )} onChange={() => handleLabelToggle(label)} /> From c03a51fd4b0cc518842be3ab7ad1b31d2fff2374 Mon Sep 17 00:00:00 2001 From: Dor Darmon Date: Sun, 13 Jul 2025 14:51:30 +0300 Subject: [PATCH 09/25] - Enhanced ToolBar with applyLabelsToMail logic inside handleLabelToggle - Modified labelsApi to support fetching updated labels list - Ensured mailApi.applyLabelsToMail is triggered on label changes - Improved user experience by removing need for manual refresh after label modifications --- frontend/src/main_page/mail_row/MailRow.jsx | 3 +-- web_server/models/labels.js | 3 ++- web_server/models/mails.js | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/frontend/src/main_page/mail_row/MailRow.jsx b/frontend/src/main_page/mail_row/MailRow.jsx index dd4c8a06..73119117 100644 --- a/frontend/src/main_page/mail_row/MailRow.jsx +++ b/frontend/src/main_page/mail_row/MailRow.jsx @@ -49,8 +49,7 @@ const MailRow = ({
-
{email.from?.fullName || "Unknown Sender"}
-
+
{email.from.fullName}
{email.subject}
{(email.labels || []).map(label => ( diff --git a/web_server/models/labels.js b/web_server/models/labels.js index 3a8b7607..b7165cdb 100644 --- a/web_server/models/labels.js +++ b/web_server/models/labels.js @@ -1,5 +1,6 @@ // Initial example labels -let labels = []; +let labels = [ { id: 1, name: 'work', owner: 1, parent: null }, + { id: 2, name: 'friends', owner: 1, parent: null },]; // Simple incrementing ID let nextId = labels.length + 1; diff --git a/web_server/models/mails.js b/web_server/models/mails.js index 2f534a7d..ad230931 100644 --- a/web_server/models/mails.js +++ b/web_server/models/mails.js @@ -31,7 +31,7 @@ const mails = [ body: "Here’s what we changed in v2.0...", createdAt: new Date('2025-06-03T20:27:11.000Z'), sentAt: new Date('2025-06-04T14:08:36.000Z'), - labels: [], + labels: [1], isDraft: false, isRead: true, isStarred: false, @@ -48,7 +48,7 @@ const mails = [ body: "Here’s what we changed in v2.0...", createdAt: new Date('2025-06-03T20:27:11.000Z'), sentAt: new Date('2025-06-04T14:08:36.000Z'), - labels: [], + labels: [2], isDraft: false, isRead: false, isStarred: false, @@ -65,7 +65,7 @@ const mails = [ body: "Don’t forget the team meeting at 9AM tomorrow.", createdAt: new Date('2025-06-03T03:15:27.000Z'), sentAt: new Date('2025-06-03T11:42:53.000Z'), - labels: [], + labels: [1,2], isDraft: false, isRead: true, isStarred: false, From 613026e71f0d7dd7742c5ba33de8dbb043b4a799 Mon Sep 17 00:00:00 2001 From: Dor Darmon Date: Sun, 13 Jul 2025 14:51:30 +0300 Subject: [PATCH 10/25] - Enhanced ToolBar with applyLabelsToMail logic inside handleLabelToggle - Modified labelsApi to support fetching updated labels list - Ensured mailApi.applyLabelsToMail is triggered on label changes - Improved user experience by removing need for manual refresh after label modifications --- frontend/src/main_page/mail_row/MailRow.jsx | 2 +- web_server/models/labels.js | 3 ++- web_server/models/mails.js | 6 +++--- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/frontend/src/main_page/mail_row/MailRow.jsx b/frontend/src/main_page/mail_row/MailRow.jsx index dd4c8a06..5a6489d3 100644 --- a/frontend/src/main_page/mail_row/MailRow.jsx +++ b/frontend/src/main_page/mail_row/MailRow.jsx @@ -49,7 +49,7 @@ const MailRow = ({
-
{email.from?.fullName || "Unknown Sender"}
+
{email.from.fullName}
{email.subject}
diff --git a/web_server/models/labels.js b/web_server/models/labels.js index 3a8b7607..b7165cdb 100644 --- a/web_server/models/labels.js +++ b/web_server/models/labels.js @@ -1,5 +1,6 @@ // Initial example labels -let labels = []; +let labels = [ { id: 1, name: 'work', owner: 1, parent: null }, + { id: 2, name: 'friends', owner: 1, parent: null },]; // Simple incrementing ID let nextId = labels.length + 1; diff --git a/web_server/models/mails.js b/web_server/models/mails.js index 2f534a7d..ad230931 100644 --- a/web_server/models/mails.js +++ b/web_server/models/mails.js @@ -31,7 +31,7 @@ const mails = [ body: "Here’s what we changed in v2.0...", createdAt: new Date('2025-06-03T20:27:11.000Z'), sentAt: new Date('2025-06-04T14:08:36.000Z'), - labels: [], + labels: [1], isDraft: false, isRead: true, isStarred: false, @@ -48,7 +48,7 @@ const mails = [ body: "Here’s what we changed in v2.0...", createdAt: new Date('2025-06-03T20:27:11.000Z'), sentAt: new Date('2025-06-04T14:08:36.000Z'), - labels: [], + labels: [2], isDraft: false, isRead: false, isStarred: false, @@ -65,7 +65,7 @@ const mails = [ body: "Don’t forget the team meeting at 9AM tomorrow.", createdAt: new Date('2025-06-03T03:15:27.000Z'), sentAt: new Date('2025-06-03T11:42:53.000Z'), - labels: [], + labels: [1,2], isDraft: false, isRead: true, isStarred: false, From 519c62c13c6f6331a735a5f6f2d50574b890ab67 Mon Sep 17 00:00:00 2001 From: DOR DARMON <161502302+dor-darmon@users.noreply.github.com> Date: Sun, 13 Jul 2025 14:58:17 +0300 Subject: [PATCH 11/25] Update MailRow.jsx --- frontend/src/main_page/mail_row/MailRow.jsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/frontend/src/main_page/mail_row/MailRow.jsx b/frontend/src/main_page/mail_row/MailRow.jsx index 73119117..39836a90 100644 --- a/frontend/src/main_page/mail_row/MailRow.jsx +++ b/frontend/src/main_page/mail_row/MailRow.jsx @@ -49,7 +49,8 @@ const MailRow = ({
-
{email.from.fullName}
+
{email.from.fullName}
+
{email.subject}
{(email.labels || []).map(label => ( @@ -74,4 +75,4 @@ const MailRow = ({
); } -export default MailRow; \ No newline at end of file +export default MailRow; From 510c5c24d1ab27b0499b210e8b81fc6a2d672fcf Mon Sep 17 00:00:00 2001 From: Dor Darmon Date: Sun, 13 Jul 2025 15:34:03 +0300 Subject: [PATCH 12/25] - Fixed createNewLabel to correctly store both name and parent values - Added isDuplicateLabel to centralize duplicate-check logic across all operations - Refactored createSublabel and editLabelById to use the new validation flow - Removed unused utility function (findLabel) for cleaner and simpler code - Ensured all label operations return well-structured and consistent objects --- web_server/models/labels.js | 53 ++++++++++++++++++++++--------------- web_server/models/mails.js | 6 ++--- 2 files changed, 35 insertions(+), 24 deletions(-) diff --git a/web_server/models/labels.js b/web_server/models/labels.js index b7165cdb..23fc4eb9 100644 --- a/web_server/models/labels.js +++ b/web_server/models/labels.js @@ -31,15 +31,31 @@ function getLabelById(id,owner) { * @returns {object} The created label */ function createNewLabel(owner, name) { - // Check if user already has a label with the same name - const exists = labels.some(l => l.owner === owner && l.name === name && l.parent === null); - if (exists) return null; - + if (isDuplicateLabel(owner, name, null)) return null; const lab = { id: nextId++, owner, name, parent: null }; labels.push(lab); return lab; } +/** + * Check if a label with the same name already exists + * for a given user and (optional) parent. + * Used to prevent duplicates on create/edit. + * @param {number} owner - User ID + * @param {string} name - Label name to check + * @param {number|null} parent - Parent label ID or null + * @param {number|null} excludeId - Optional: label ID to exclude from check (for editing) + * @returns {boolean} True if duplicate exists, false otherwise + */ +function isDuplicateLabel(owner, name, parent = null, excludeId = null) { + return labels.some(l => + l.owner === owner && + l.name === name && + l.parent === parent && + (excludeId === null || l.id !== excludeId) + ); +} + /** * Create a sublabel under an existing label * @param {number} owner - User ID @@ -49,17 +65,12 @@ function createNewLabel(owner, name) { */ function createSublabel(owner, parent, name) { // Check parent exists (could also check ownership) - const parentLabel = labels.find(l => l.id === parent && l.owner === owner); + const parentLabel = getLabelById(parent, owner); if (!parentLabel) return null; // Prevent duplicate sublabel names under the same parent for the same user - const duplicate = labels.some(l => - l.owner === owner && - l.parent === parent && - l.name === name - ); + if (isDuplicateLabel(owner, name, parent)) return null; - if (duplicate) return null const newLabel = { id: nextId++, name, @@ -78,16 +89,16 @@ function createSublabel(owner, parent, name) { * @returns {object|null} Updated label or null if not found */ function editLabelById(id, owner,newName) { - const lab = labels.find(l => l.id === id && l.owner === owner); - if (!lab) return null; + // Check parent exists (could also check ownership) + const Label = getLabelById(id, owner); + if (!Label) return null; // Check if another label with the same name already exists - const duplicate = labels.find(l => l.name === newName && l.owner === owner && l.id !== id); - if (duplicate) { - throw new Error("409"); - } + if (isDuplicateLabel(owner, newName, Label.parent, id)) { + throw new Error("409"); + } // Rename the label - lab.name = newName; - return lab; + Label.name = newName; + return Label; } /** @@ -97,8 +108,8 @@ function editLabelById(id, owner,newName) { * @returns {boolean} True if deleted, false if not found */ function deleteLabelById(id,owner) { - const idx = labels.findIndex(l => l.id === id&& l.owner === owner); - if (idx < 0) return false; + const idx = getLabelById(id, owner); + if (!idx) return false; // Recursively delete children const childLabels = labels.filter(l => l.parent === id && l.owner === owner); childLabels.forEach(child => { diff --git a/web_server/models/mails.js b/web_server/models/mails.js index ad230931..db51d105 100644 --- a/web_server/models/mails.js +++ b/web_server/models/mails.js @@ -82,7 +82,7 @@ const mails = [ body: "Don’t forget the team meeting at 9AM tomorrow.", createdAt: new Date('2025-06-03T03:15:27.000Z'), sentAt: new Date('2025-06-03T11:42:53.000Z'), - labels: [], + labels: [2], isDraft: false, isRead: false, isStarred: true, @@ -99,7 +99,7 @@ const mails = [ body: "Whats up guys look at my new picture!", createdAt: new Date('2025-06-03T20:27:11.000Z'), sentAt: new Date('2025-06-04T14:08:36.000Z'), - labels: [], + labels: [1], isDraft: false, isRead: true, isStarred: false, @@ -155,7 +155,7 @@ const mails = [ body: "Whats up guys look at my new picture!", createdAt: new Date('2025-06-03T20:27:11.000Z'), sentAt: new Date('2025-06-04T14:08:36.000Z'), - labels: [], + labels: [1], isDraft: false, isRead: false, isStarred: false, From 356363cdd01066206abbce3d8ecb9c9c713739c5 Mon Sep 17 00:00:00 2001 From: Dor Darmon Date: Sun, 13 Jul 2025 16:13:08 +0300 Subject: [PATCH 13/25] . --- .../main_page/EmailSideMenu/EmailSideMenu.jsx | 8 ++++---- frontend/src/main_page/MainPage.js | 18 ++---------------- frontend/src/main_page/mail_row/MailRow.jsx | 2 -- frontend/src/main_page/toolbar/ToolBar.jsx | 5 +++-- web_server/models/labels.js | 4 ++-- 5 files changed, 11 insertions(+), 26 deletions(-) diff --git a/frontend/src/main_page/EmailSideMenu/EmailSideMenu.jsx b/frontend/src/main_page/EmailSideMenu/EmailSideMenu.jsx index bb13648c..2200c1e9 100644 --- a/frontend/src/main_page/EmailSideMenu/EmailSideMenu.jsx +++ b/frontend/src/main_page/EmailSideMenu/EmailSideMenu.jsx @@ -208,9 +208,9 @@ export default function EmailSidebar({ return (
{/* Compose button (desktop only) */} - + {/* Folders (Inbox, Sent, etc.) */}
    @@ -267,4 +267,4 @@ export default function EmailSidebar({ )}
); -} +} \ No newline at end of file diff --git a/frontend/src/main_page/MainPage.js b/frontend/src/main_page/MainPage.js index db0035cb..d985d103 100644 --- a/frontend/src/main_page/MainPage.js +++ b/frontend/src/main_page/MainPage.js @@ -13,7 +13,6 @@ import { useTheme } from "../utils/useTheme"; import { useRequireAuth } from "../utils/useAutoLogin"; import SkeletonEmail from "../components/loading/SkeletonEmail"; import useIsMobile from "../utils/useIsMobile"; -import { fetchLabels } from "../api/labelsApi"; const MainPage = () => { useRequireAuth(); @@ -87,18 +86,6 @@ const MainPage = () => { setSelectedMails(new Set()); } }, [location.state?.inboxType]); - - const [labels, setLabels] = useState([]); - - const loadLabels = async () => { - const data = await fetchLabels(); - setLabels(data); - }; - - useEffect(() => { - loadLabels(); - }, []); - return (
{ onSelect={handleSelect} onUpdate={refreshMails} onOpenDraft={handleOpenDraft} - allLabels={labels} /> )) )} @@ -164,7 +150,7 @@ const MainPage = () => { offset={c.offset} draftMail={c.draftMail} onCancel={() =>{ handleCloseCompose(c.id); - refreshMails();}} + refreshMails();}} onSend={() => { handleCloseCompose(c.id); refreshMails(); @@ -176,4 +162,4 @@ const MainPage = () => { ); }; -export default MainPage; +export default MainPage; \ No newline at end of file diff --git a/frontend/src/main_page/mail_row/MailRow.jsx b/frontend/src/main_page/mail_row/MailRow.jsx index dd4c8a06..d65cec9d 100644 --- a/frontend/src/main_page/mail_row/MailRow.jsx +++ b/frontend/src/main_page/mail_row/MailRow.jsx @@ -13,7 +13,6 @@ import {markMailAsRead, stripHtml} from "../../utils/mailUtils"; * @prop onSelect (mail,checked)=>void * @prop onUpdate ()=>void * @prop onOpenDraft (mail)=>void - * @prop allLabels list of label */ const MailRow = ({ theme, @@ -23,7 +22,6 @@ const MailRow = ({ onSelect, onUpdate, onOpenDraft, - allLabels }) => { const navigate = useNavigate(); // Handle opening an email (navigate to full view or open draft) diff --git a/frontend/src/main_page/toolbar/ToolBar.jsx b/frontend/src/main_page/toolbar/ToolBar.jsx index 6eb6ee8c..bd639773 100644 --- a/frontend/src/main_page/toolbar/ToolBar.jsx +++ b/frontend/src/main_page/toolbar/ToolBar.jsx @@ -68,8 +68,9 @@ const ToolBar = ({ newLabels = Array.from(labelMap.values()); } // update UI - mail.labels = newLabels.filter(label => label && typeof label.id === "number" - && typeof label.name === "string"); + mail.labels = newLabels + .map(newLabel => labels.find(l => l.id === newLabel.id)) + .filter(Boolean); mail._forceUpdate = Date.now(); // Send new label list to backend await applyLabelsToMail(mail.id, newLabels); diff --git a/web_server/models/labels.js b/web_server/models/labels.js index 23fc4eb9..44060aaf 100644 --- a/web_server/models/labels.js +++ b/web_server/models/labels.js @@ -108,8 +108,8 @@ function editLabelById(id, owner,newName) { * @returns {boolean} True if deleted, false if not found */ function deleteLabelById(id,owner) { - const idx = getLabelById(id, owner); - if (!idx) return false; + const idx = labels.findIndex(l => l.id === id && l.owner === owner); + if (idx < 0) return false; // Recursively delete children const childLabels = labels.filter(l => l.parent === id && l.owner === owner); childLabels.forEach(child => { From 3d501b30b6904fdcfee2fc2fd2501eb62ca86fc1 Mon Sep 17 00:00:00 2001 From: Dor Darmon Date: Sun, 13 Jul 2025 16:22:33 +0300 Subject: [PATCH 14/25] . --- web_server/models/labels.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/web_server/models/labels.js b/web_server/models/labels.js index 44060aaf..10c8b265 100644 --- a/web_server/models/labels.js +++ b/web_server/models/labels.js @@ -1,6 +1,7 @@ // Initial example labels let labels = [ { id: 1, name: 'work', owner: 1, parent: null }, - { id: 2, name: 'friends', owner: 1, parent: null },]; + { id: 2, name: 'friends', owner: 1, parent: null },{ id: 3, name: 'work', owner: 2, parent: null } + ,{ id: 4, name: 'work', owner: 3, parent: null }]; // Simple incrementing ID let nextId = labels.length + 1; @@ -69,7 +70,7 @@ function createSublabel(owner, parent, name) { if (!parentLabel) return null; // Prevent duplicate sublabel names under the same parent for the same user - if (isDuplicateLabel(owner, name, parent)) return null; + if (isDuplicateLabel(owner, name, parent)) throw new Error("409") const newLabel = { id: nextId++, From 9521600bc680e305ebe38b7f80c2c172deccd211 Mon Sep 17 00:00:00 2001 From: Dor Darmon Date: Sun, 13 Jul 2025 16:27:44 +0300 Subject: [PATCH 15/25] . --- frontend/src/main_page/mail_row/MailRow.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/main_page/mail_row/MailRow.jsx b/frontend/src/main_page/mail_row/MailRow.jsx index d65cec9d..5998dd01 100644 --- a/frontend/src/main_page/mail_row/MailRow.jsx +++ b/frontend/src/main_page/mail_row/MailRow.jsx @@ -47,7 +47,7 @@ const MailRow = ({
-
{email.from?.fullName || "Unknown Sender"}
+
{email.from?.fullName}
{email.subject}
From f7cd5e0ce2f519cadc762654fa97cddbdc185bc8 Mon Sep 17 00:00:00 2001 From: Dor Darmon Date: Sun, 13 Jul 2025 16:28:30 +0300 Subject: [PATCH 16/25] . --- frontend/src/main_page/mail_row/MailRow.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/main_page/mail_row/MailRow.jsx b/frontend/src/main_page/mail_row/MailRow.jsx index 5998dd01..7cd3d65f 100644 --- a/frontend/src/main_page/mail_row/MailRow.jsx +++ b/frontend/src/main_page/mail_row/MailRow.jsx @@ -47,7 +47,7 @@ const MailRow = ({
-
{email.from?.fullName}
+
{email.from.fullName}
{email.subject}
From 43221b45257a15fd9ab0a46f6347c821201092a3 Mon Sep 17 00:00:00 2001 From: DOR DARMON <161502302+dor-darmon@users.noreply.github.com> Date: Sun, 13 Jul 2025 16:34:48 +0300 Subject: [PATCH 17/25] Update mails.js --- web_server/controllers/mails.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web_server/controllers/mails.js b/web_server/controllers/mails.js index 23b517eb..18e9dbd1 100644 --- a/web_server/controllers/mails.js +++ b/web_server/controllers/mails.js @@ -146,7 +146,7 @@ const updateMail = (req, res) => { if (isNaN(mailId)) return res.status(400).json({error: 'error no valid mail id was given'}); const mail = Mails.getMail(mailId); - if (!mailId || mail.owner !== userId) + if (!mailId || mail.owner != userId) return res.status(404).json({error: 'error mail not found'}); // edit it as a draft if (mail.isDraft) @@ -260,4 +260,4 @@ const getMailsByQuery = (req, res) => { return res.status(200).json(fullMails); } -module.exports = {getLastMailsOrdered, getMailById, createNewMail, updateMail, deleteMailById, getMailsByQuery}; \ No newline at end of file +module.exports = {getLastMailsOrdered, getMailById, createNewMail, updateMail, deleteMailById, getMailsByQuery}; From 94c61da7ea058443b47b41503c7fbad9addbee1c Mon Sep 17 00:00:00 2001 From: Dor Darmon Date: Sun, 13 Jul 2025 16:37:25 +0300 Subject: [PATCH 18/25] . --- web_server/controllers/mails.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web_server/controllers/mails.js b/web_server/controllers/mails.js index 23b517eb..5866b4cb 100644 --- a/web_server/controllers/mails.js +++ b/web_server/controllers/mails.js @@ -146,7 +146,7 @@ const updateMail = (req, res) => { if (isNaN(mailId)) return res.status(400).json({error: 'error no valid mail id was given'}); const mail = Mails.getMail(mailId); - if (!mailId || mail.owner !== userId) + if (!mailId || mail.owner != userId) return res.status(404).json({error: 'error mail not found'}); // edit it as a draft if (mail.isDraft) From f3762a7cbefe024c40b7fc770f47442474c21cba Mon Sep 17 00:00:00 2001 From: Yuval Date: Thu, 17 Jul 2025 18:18:38 +0300 Subject: [PATCH 19/25] Removed debug prints in mails controller --- web_server/controllers/mails.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/web_server/controllers/mails.js b/web_server/controllers/mails.js index 18e9dbd1..5f22411f 100644 --- a/web_server/controllers/mails.js +++ b/web_server/controllers/mails.js @@ -73,7 +73,7 @@ const getMailById = (req, res) => { if (!mail) return res.status(404).json({error: `No mail found with ID: ${id}`}); // ensure the mail belongs to the user - if (mail.owner !== userId) + if (mail.owner != userId) return res.status(403).json({error: 'mail do not belong to user'}); return res.status(200).json({ ...mail, @@ -154,11 +154,8 @@ const updateMail = (req, res) => { // otherwise it’s a mail already sent - only allow flags & labels const {isRead, isStarred, isTrashed, isSpam, labels} = req.body || {}; - console.log('labels', labels); const labelsIds = convertLabelsToIds(userId, labels || []); - console.log('labelsIds', labelsIds); const updated = Mails.editSentMail(mailId, isRead, isStarred, isTrashed, isSpam, labelsIds); - console.log('updated', updated); if (updated) return res.status(200).json(updated); if (updated === 404) From a85cb18a0fb44b66ad19d619aae8be02e44a9c16 Mon Sep 17 00:00:00 2001 From: Yuval Date: Thu, 17 Jul 2025 18:22:09 +0300 Subject: [PATCH 20/25] Changed === checks to == --- web_server/models/labels.js | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/web_server/models/labels.js b/web_server/models/labels.js index 10c8b265..44af377b 100644 --- a/web_server/models/labels.js +++ b/web_server/models/labels.js @@ -1,7 +1,10 @@ // Initial example labels -let labels = [ { id: 1, name: 'work', owner: 1, parent: null }, - { id: 2, name: 'friends', owner: 1, parent: null },{ id: 3, name: 'work', owner: 2, parent: null } - ,{ id: 4, name: 'work', owner: 3, parent: null }]; +let labels = [ + { id: 1, name: 'work', owner: 1, parent: null }, + { id: 2, name: 'friends', owner: 1, parent: null }, + { id: 3, name: 'work', owner: 2, parent: null }, + { id: 4, name: 'work', owner: 3, parent: null } +]; // Simple incrementing ID let nextId = labels.length + 1; @@ -12,7 +15,7 @@ let nextId = labels.length + 1; * @returns {Array} Filtered label objects owned by the user */ function getAllLabels(userId) { - return labels.filter(l => l.owner === userId); + return labels.filter(l => l.owner == userId); } /** @@ -22,7 +25,7 @@ function getAllLabels(userId) { * @returns {object|null} Label object or null if not found */ function getLabelById(id,owner) { - return labels.find(l => l.id === id && l.owner === owner) || null; + return labels.find(l => l.id == id && l.owner == owner) || null; } /** @@ -109,10 +112,10 @@ function editLabelById(id, owner,newName) { * @returns {boolean} True if deleted, false if not found */ function deleteLabelById(id,owner) { - const idx = labels.findIndex(l => l.id === id && l.owner === owner); + const idx = labels.findIndex(l => l.id == id && l.owner == owner); if (idx < 0) return false; // Recursively delete children - const childLabels = labels.filter(l => l.parent === id && l.owner === owner); + const childLabels = labels.filter(l => l.parent == id && l.owner == owner); childLabels.forEach(child => { deleteLabelById(child.id, owner); }); @@ -130,4 +133,4 @@ module.exports = { createSublabel, editLabelById, deleteLabelById -}; \ No newline at end of file +}; From 6e576bccfcb427b92d569019ae8076166f476f5d Mon Sep 17 00:00:00 2001 From: DOR DARMON <161502302+dor-darmon@users.noreply.github.com> Date: Fri, 18 Jul 2025 19:25:38 +0300 Subject: [PATCH 21/25] Update labels.js --- web_server/controllers/labels.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web_server/controllers/labels.js b/web_server/controllers/labels.js index 4c936172..00b0c766 100644 --- a/web_server/controllers/labels.js +++ b/web_server/controllers/labels.js @@ -83,5 +83,5 @@ exports.deleteLabel = (req, res) => { if (!deleted) return res.status(404).json({ error: 'Label not found or not owned by user' }); - return res.status(204).send(); -} \ No newline at end of file + return res.status(204).end(); +} From d5df171daae15a36e702b47bde12c0618dfc67c5 Mon Sep 17 00:00:00 2001 From: Dor Darmon Date: Fri, 18 Jul 2025 19:51:29 +0300 Subject: [PATCH 22/25] . --- frontend/src/reading_page/ReadingHeader/ReadingHeader.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/reading_page/ReadingHeader/ReadingHeader.jsx b/frontend/src/reading_page/ReadingHeader/ReadingHeader.jsx index 1f171521..30c7c365 100644 --- a/frontend/src/reading_page/ReadingHeader/ReadingHeader.jsx +++ b/frontend/src/reading_page/ReadingHeader/ReadingHeader.jsx @@ -66,7 +66,7 @@ const ReadingHeader = ({theme, toggleTheme, email, inboxType}) => { newLabels = email.labels.filter(l => l.id !== label.id); } else { // Add label without duplicates - const labelMap = new Map(email.labels.map(l => [l.id, l])); + const labelMap = new Map((email?.labels || []).map(l => [l.id, l])); labelMap.set(label.id, label); newLabels = Array.from(labelMap.values()); } @@ -141,7 +141,7 @@ const ReadingHeader = ({theme, toggleTheme, email, inboxType}) => { className={`form-check-input ${theme}`} type="checkbox" id={`label-check-${label.id}`} - checked={Array.isArray(email.labels) && email.labels.some(l => l.id === label.id)} + checked={Array.isArray(email?.labels) && email.labels.some(l => l.id === label.id)} onChange={() => handleLabelToggle(label)} />