diff --git a/category_rename_test.js b/category_rename_test.js new file mode 100644 index 0000000..98a2dcf --- /dev/null +++ b/category_rename_test.js @@ -0,0 +1,204 @@ +// --- Mocking Area --- +const DEFAULT_CATEGORY = "all"; +let mockSavedFeatures = {}; +let mockContextMenuTab = null; +let mockSelectedTab = null; +let mockAlerts = []; +let mockConsoleErrors = []; + +// Mock alert and console.error +global.alert = (message) => { + console.log(`ALERT: ${message}`); + mockAlerts.push(message); +}; +global.console.error = (message) => { + console.log(`CONSOLE.ERROR: ${message}`); + mockConsoleErrors.push(message); +}; + +// Simplified version of the core logic of useCategoryManagement's handleRenameCategory +// Based on the last known good state of the function. +const setSavedFeaturesMock = (updater) => { + if (typeof updater === 'function') { + mockSavedFeatures = updater(mockSavedFeatures); + } else { + mockSavedFeatures = updater; + } +}; + +const setSelectedTabMock = (tabName) => { + mockSelectedTab = tabName; +}; + +const handleRenameCategory = (newName) => { + // Initial Guard & Default Category Checks + if (!mockContextMenuTab) { + console.error("Rename category called without contextMenuTab"); + return; + } + if (mockContextMenuTab === DEFAULT_CATEGORY) { + alert(`Cannot rename the default category "${DEFAULT_CATEGORY}".`); + return; + } + if (newName === DEFAULT_CATEGORY) { + alert(`Cannot rename category to "${DEFAULT_CATEGORY}". Please choose a different name.`); + return; + } + + // No-op Rename Check + if (newName === mockContextMenuTab) { + return; // It's the same name, do nothing + } + + // Existing Name Check (Data Loss Prevention) + // This check uses the `savedFeatures` state directly. + if (Object.keys(mockSavedFeatures).includes(newName)) { + alert(`Category "${newName}" already exists. Please choose a different name.`); + return; // Prevent renaming + } + + // If all checks pass, then we attempt to save and select. + setSavedFeaturesMock((prev) => { + const orderedKeys = Object.keys(prev); + const newSavedFeatures = {}; + for (const key of orderedKeys) { + if (key === mockContextMenuTab) { + newSavedFeatures[newName] = prev[mockContextMenuTab]; + } else { + newSavedFeatures[key] = prev[key]; + } + } + return newSavedFeatures; + }); + + setSelectedTabMock(newName); +}; + +// --- Test Runner --- +const resetMocks = () => { + mockAlerts = []; + mockConsoleErrors = []; + mockSavedFeatures = {}; + mockContextMenuTab = null; + mockSelectedTab = null; +}; + +const printState = (testName) => { + console.log(`--- ${testName} ---`); + console.log("Alerts:", mockAlerts); + console.log("Console Errors:", mockConsoleErrors); + console.log("Saved Features:", JSON.stringify(mockSavedFeatures, null, 2)); + console.log("Selected Tab:", mockSelectedTab); + console.log("---------------------\n"); +}; + +const verify = (condition, passMessage, failMessage) => { + if (condition) { + console.log(`PASS: ${passMessage}`); + return true; + } else { + console.error(`FAIL: ${failMessage}`); + return false; + } +}; + +// --- Test Scenarios --- + +// 1. Data Loss Prevention +resetMocks(); +console.log("Starting Test 1: Data Loss Prevention"); +mockSavedFeatures = { "all": [], "Category A": ["feat1"], "Category B": ["feat2"] }; +mockContextMenuTab = "Category A"; +const originalSelectedTab1 = "Category A"; // Assume this was the selected tab +mockSelectedTab = originalSelectedTab1; +const originalSavedFeatures1 = JSON.parse(JSON.stringify(mockSavedFeatures)); // Deep copy + +handleRenameCategory("Category B"); + +let test1_passed = true; +test1_passed = test1_passed && verify(mockAlerts.length === 1 && mockAlerts[0].includes('Category "Category B" already exists'), "Alert for existing category name triggered.", "Alert for existing category name NOT triggered or incorrect message."); +test1_passed = test1_passed && verify(JSON.stringify(mockSavedFeatures) === JSON.stringify(originalSavedFeatures1), "savedFeatures remains unchanged.", "savedFeatures was changed!"); +test1_passed = test1_passed && verify(mockSelectedTab === originalSelectedTab1, "selectedTab remains unchanged.", `selectedTab changed to ${mockSelectedTab}!`); +printState("Test 1 Results"); +if(test1_passed) console.log("Test 1: Data Loss Prevention PASSED\n"); else console.error("Test 1: Data Loss Prevention FAILED\n"); + + +// 2. No-Op Rename +resetMocks(); +console.log("Starting Test 2: No-Op Rename"); +mockSavedFeatures = { "all": [], "Category C": ["feat3"] }; +mockContextMenuTab = "Category C"; +const originalSelectedTab2 = "Category C"; +mockSelectedTab = originalSelectedTab2; +const originalSavedFeatures2 = JSON.parse(JSON.stringify(mockSavedFeatures)); + +handleRenameCategory("Category C"); + +let test2_passed = true; +test2_passed = test2_passed && verify(mockAlerts.length === 0, "No alert triggered.", `Alerts triggered: ${mockAlerts.join(", ")}`); +test2_passed = test2_passed && verify(JSON.stringify(mockSavedFeatures) === JSON.stringify(originalSavedFeatures2), "savedFeatures remains unchanged.", "savedFeatures was changed!"); +test2_passed = test2_passed && verify(mockSelectedTab === originalSelectedTab2, "selectedTab remains unchanged.", `selectedTab changed to ${mockSelectedTab}!`); +printState("Test 2 Results"); +if(test2_passed) console.log("Test 2: No-Op Rename PASSED\n"); else console.error("Test 2: No-Op Rename FAILED\n"); + + +// 3. General Rename (Order Preservation & setSelectedTab) +resetMocks(); +console.log("Starting Test 3: General Rename"); +mockSavedFeatures = { "all": [], "Cat X": [], "Cat Y": ["featY"], "Cat Z": [] }; +mockContextMenuTab = "Cat Y"; +mockSelectedTab = "Cat Y"; // Current tab is Cat Y +const expectedSavedFeatures3 = { "all": [], "Cat X": [], "Cat Y New": ["featY"], "Cat Z": [] }; +const expectedKeyOrder3 = ["all", "Cat X", "Cat Y New", "Cat Z"]; + +handleRenameCategory("Cat Y New"); + +let test3_passed = true; +test3_passed = test3_passed && verify(mockAlerts.length === 0, "No alert triggered.", `Alerts triggered: ${mockAlerts.join(", ")}`); +test3_passed = test3_passed && verify(JSON.stringify(mockSavedFeatures) === JSON.stringify(expectedSavedFeatures3), "savedFeatures updated correctly.", `savedFeatures is ${JSON.stringify(mockSavedFeatures)}`); +const actualKeyOrder3 = Object.keys(mockSavedFeatures); +test3_passed = test3_passed && verify(JSON.stringify(actualKeyOrder3) === JSON.stringify(expectedKeyOrder3), `Key order is correct: ${actualKeyOrder3.join(", ")}`, `Key order is incorrect: ${actualKeyOrder3.join(", ")}`); +test3_passed = test3_passed && verify(mockSelectedTab === "Cat Y New", "selectedTab updated to 'Cat Y New'.", `selectedTab is ${mockSelectedTab}`); +printState("Test 3 Results"); +if(test3_passed) console.log("Test 3: General Rename PASSED\n"); else console.error("Test 3: General Rename FAILED\n"); + + +// 4. Renaming involving DEFAULT_CATEGORY ("all") +// 4a. Attempt to rename "all" +resetMocks(); +console.log("Starting Test 4a: Attempt to rename 'all'"); +mockSavedFeatures = { "all": [], "OtherCat": [] }; +mockContextMenuTab = "all"; +const originalSelectedTab4a = "all"; +mockSelectedTab = originalSelectedTab4a; +const originalSavedFeatures4a = JSON.parse(JSON.stringify(mockSavedFeatures)); + +handleRenameCategory("New All Name"); + +let test4a_passed = true; +test4a_passed = test4a_passed && verify(mockAlerts.length === 1 && mockAlerts[0].includes('Cannot rename the default category "all"'), "Alert for trying to rename 'all' triggered.", "Alert for renaming 'all' NOT triggered or incorrect message."); +test4a_passed = test4a_passed && verify(JSON.stringify(mockSavedFeatures) === JSON.stringify(originalSavedFeatures4a), "savedFeatures remains unchanged.", "savedFeatures was changed!"); +test4a_passed = test4a_passed && verify(mockSelectedTab === originalSelectedTab4a, "selectedTab remains unchanged.", `selectedTab changed to ${mockSelectedTab}!`); +printState("Test 4a Results"); +if(test4a_passed) console.log("Test 4a: Attempt to rename 'all' PASSED\n"); else console.error("Test 4a: Attempt to rename 'all' FAILED\n"); + + +// 4b. Attempt to rename a category *to* "all" +resetMocks(); +console.log("Starting Test 4b: Attempt to rename a category TO 'all'"); +mockSavedFeatures = { "all": [], "OtherCat": [] }; +mockContextMenuTab = "OtherCat"; +const originalSelectedTab4b = "OtherCat"; +mockSelectedTab = originalSelectedTab4b; +const originalSavedFeatures4b = JSON.parse(JSON.stringify(mockSavedFeatures)); + +handleRenameCategory("all"); + +let test4b_passed = true; +test4b_passed = test4b_passed && verify(mockAlerts.length === 1 && mockAlerts[0].includes('Cannot rename category to "all"'), "Alert for trying to rename TO 'all' triggered.", "Alert for renaming TO 'all' NOT triggered or incorrect message."); +test4b_passed = test4b_passed && verify(JSON.stringify(mockSavedFeatures) === JSON.stringify(originalSavedFeatures4b), "savedFeatures remains unchanged.", "savedFeatures was changed!"); +test4b_passed = test4b_passed && verify(mockSelectedTab === originalSelectedTab4b, "selectedTab remains unchanged.", `selectedTab changed to ${mockSelectedTab}!`); +printState("Test 4b Results"); +if(test4b_passed) console.log("Test 4b: Attempt to rename a category TO 'all' PASSED\n"); else console.error("Test 4b: Attempt to rename a category TO 'all' FAILED\n"); + +console.log("All tests finished."); diff --git a/package-lock.json b/package-lock.json index 6865059..c2caa64 100644 --- a/package-lock.json +++ b/package-lock.json @@ -38,7 +38,7 @@ "@stylistic/eslint-plugin": "^4.2.0", "@types/file-saver": "^2.0.7", "@types/leaflet": "^1.9.16", - "@types/node": "^22.15.14", + "@types/node": "^22.15.19", "@types/react": "^19.0.12", "@types/react-dom": "^19.0.4", "@types/react-image-gallery": "^1.2.4", @@ -52,6 +52,7 @@ "globals": "^15.15.0", "replace-in-file": "^8.3.0", "rollup-plugin-visualizer": "^5.14.0", + "ts-node": "^10.9.2", "typescript": "~5.6.3", "typescript-eslint": "^8.27.0", "vite": "6.3.4" @@ -352,6 +353,28 @@ "node": ">=6.9.0" } }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, "node_modules/@dnd-kit/accessibility": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz", @@ -2002,6 +2025,30 @@ "react-dom": "^18.0.0 || ^17.0.1 || ^16.7.0" } }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "dev": true + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -2093,11 +2140,10 @@ } }, "node_modules/@types/node": { - "version": "22.15.14", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.14.tgz", - "integrity": "sha512-BL1eyu/XWsFGTtDWOYULQEs4KR0qdtYfCxYAUYRoB7JP7h9ETYLgQTww6kH8Sj2C0pFGgrpM0XKv6/kbIzYJ1g==", + "version": "22.15.19", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.19.tgz", + "integrity": "sha512-3vMNr4TzNQyjHcRZadojpRaD9Ofr6LsonZAoQ+HMUa/9ORTPoxVIw0e0mpqWpdjj8xybyCM+oKOUH2vwFu/oEw==", "dev": true, - "license": "MIT", "dependencies": { "undici-types": "~6.21.0" } @@ -2421,6 +2467,18 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -2464,6 +2522,12 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -2957,6 +3021,12 @@ "node": ">= 6" } }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -3111,6 +3181,15 @@ "node": ">=0.4.0" } }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/doctrine": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", @@ -5012,6 +5091,12 @@ "yallist": "^3.0.2" } }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -6541,6 +6626,49 @@ "typescript": ">=4.8.4" } }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, "node_modules/tsconfig-paths": { "version": "3.15.0", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", @@ -6678,7 +6806,6 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", "dev": true, - "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -6796,6 +6923,12 @@ "uuid": "dist/esm/bin/uuid" } }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true + }, "node_modules/vite": { "version": "6.3.4", "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.4.tgz", @@ -7111,6 +7244,15 @@ "node": ">=12" } }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index 1c63eb3..28bbcb4 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "@stylistic/eslint-plugin": "^4.2.0", "@types/file-saver": "^2.0.7", "@types/leaflet": "^1.9.16", - "@types/node": "^22.15.14", + "@types/node": "^22.15.19", "@types/react": "^19.0.12", "@types/react-dom": "^19.0.4", "@types/react-image-gallery": "^1.2.4", @@ -54,6 +54,7 @@ "globals": "^15.15.0", "replace-in-file": "^8.3.0", "rollup-plugin-visualizer": "^5.14.0", + "ts-node": "^10.9.2", "typescript": "~5.6.3", "typescript-eslint": "^8.27.0", "vite": "6.3.4" diff --git a/src/components/SavedFeaturesDrawer/ContextMenu/CategoryContextMenu.tsx b/src/components/SavedFeaturesDrawer/ContextMenu/CategoryContextMenu.tsx index 6f9bd6a..b88c9ca 100644 --- a/src/components/SavedFeaturesDrawer/ContextMenu/CategoryContextMenu.tsx +++ b/src/components/SavedFeaturesDrawer/ContextMenu/CategoryContextMenu.tsx @@ -54,14 +54,12 @@ export const CategoryContextMenu: React.FC = ({ : undefined } > - {contextMenuTab && contextMenuTab !== DEFAULT_CATEGORY && contextMenuTab !== NULL_TAB ? ( - <> - moveCategory("up"))}>Move Up - moveCategory("down"))}>Move Down - Rename Category - Remove Category - - ) : null} + {contextMenuTab && contextMenuTab !== DEFAULT_CATEGORY && contextMenuTab !== NULL_TAB ? [ + moveCategory("up"))}>Move Up, + moveCategory("down"))}>Move Down, + Rename Category, + Remove Category + ] : null} Add New Category ) diff --git a/src/components/SavedFeaturesDrawer/hooks/useCategoryManagement.tsx b/src/components/SavedFeaturesDrawer/hooks/useCategoryManagement.tsx index dbd5fc7..36312c7 100644 --- a/src/components/SavedFeaturesDrawer/hooks/useCategoryManagement.tsx +++ b/src/components/SavedFeaturesDrawer/hooks/useCategoryManagement.tsx @@ -41,19 +41,54 @@ export const useCategoryManagement = ( setSavedFeatures(newSavedFeatures) }, [contextMenuTab, savedFeatures, setSavedFeatures]) - const handleRenameCategory = useCallback((newName: string) => { - if (contextMenuTab && contextMenuTab !== DEFAULT_CATEGORY && newName !== DEFAULT_CATEGORY) { - setSavedFeatures((prev: SavedFeaturesStateType) => { - const newSavedFeatures = { ...prev } - newSavedFeatures[newName] = newSavedFeatures[contextMenuTab] - delete newSavedFeatures[contextMenuTab] - const keys = Object.keys(newSavedFeatures) - const index = keys.indexOf(contextMenuTab) - if (index !== -1) keys.splice(index, 1, newName) - return Object.fromEntries(keys.map((key) => [key, newSavedFeatures[key]])) - }) +const handleRenameCategory = useCallback((newName: string) => { + // Initial Guard & Default Category Checks + if (!contextMenuTab) { + // This case should ideally not happen if context menu is triggered properly + console.error("Rename category called without contextMenuTab"); + return; + } + if (contextMenuTab === DEFAULT_CATEGORY) { + alert(`Cannot rename the default category "${DEFAULT_CATEGORY}".`); + return; + } + if (newName === DEFAULT_CATEGORY) { + alert(`Cannot rename category to "${DEFAULT_CATEGORY}". Please choose a different name.`); + return; + } + + // No-op Rename Check + if (newName === contextMenuTab) { + return; // It's the same name, do nothing + } + + // Existing Name Check (Data Loss Prevention) + // This check uses the `savedFeatures` state directly from the hook's scope. + // It's done *before* calling `setSavedFeatures`. + // The condition `newName !== contextMenuTab` is removed because the no-op check above ensures they are different. + if (Object.keys(savedFeatures).includes(newName)) { + alert(`Category "${newName}" already exists. Please choose a different name.`); + return; // Prevent renaming + } + + // If all checks pass, then we attempt to save and select. + setSavedFeatures((prev: SavedFeaturesStateType) => { + // This function now assumes the pre-checks for default category, no-op, and existing name have passed. + const orderedKeys = Object.keys(prev); + const newSavedFeatures: SavedFeaturesStateType = {}; + for (const key of orderedKeys) { + if (key === contextMenuTab) { + newSavedFeatures[newName] = prev[contextMenuTab]; + } else { + newSavedFeatures[key] = prev[key]; + } } - }, [contextMenuTab, setSavedFeatures]) + return newSavedFeatures; + }); + + setSelectedTab(newName); // Call if all checks passed and rename proceeded. + +}, [contextMenuTab, savedFeatures, setSavedFeatures, setSelectedTab]); const handleAddCategory = useCallback(() => { let categoryName = prompt("Enter name for category")