UI: Replace window.confirm with custom confirmation modal - #12
UI: Replace window.confirm with custom confirmation modal#12tejaavula076 wants to merge 4 commits into
Conversation
|
@tejaavula076 is attempting to deploy a commit to the Yash Pouranik's projects Team on Vercel. A member of the Team first needs to authorize it. |
📝 WalkthroughWalkthroughA reusable Tailwind-styled ConfirmationModal component was added and integrated into Database.jsx and ProjectSettings.jsx to replace native confirm dialogs. Tailwind was integrated into the frontend (deps + Vite plugin), small HTML formatting changes were made, and two CSS utility classes were added for modal spacing and buttons. Changes
Sequence Diagram(s)sequenceDiagram
actor User
participant Page as Database / ProjectSettings
participant Modal as ConfirmationModal
participant API as Backend API
User->>Page: Click "Delete" (record or project)
Page->>Page: set showModal=true, store targetId
Page->>Modal: render(open=true, title, message, onConfirm, onCancel)
Modal->>User: display overlay with Cancel/Delete
alt User clicks Cancel
User->>Modal: Click Cancel
Modal->>Page: onCancel()
Page->>Page: set showModal=false
else User clicks Delete
User->>Modal: Click Delete
Modal->>Page: onConfirm()
Page->>API: DELETE /resource/:id
API-->>Page: 200 OK
Page->>Page: remove item from state, set showModal=false
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
Comment |
There was a problem hiding this comment.
Actionable comments posted: 10
🤖 Fix all issues with AI agents
In @frontend/index.html:
- Line 15: Remove the Tailwind CDN script tag from frontend/index.html (the
script src referencing @tailwindcss/browser) and rely on the Vite plugin
instead; update vite.config.js to import the plugin (referencing the imported
symbol tailwindcss) and add tailwindcss() to the plugins array alongside react()
so the plugin handles Tailwind build-time integration and optimization.
In @frontend/package.json:
- Around line 13-21: package.json currently lists the @tailwindcss/vite
dependency but vite.config.js does not use the plugin and index.html loads
Tailwind from CDN (@tailwindcss/browser@4), so remove the unused dependency by
deleting "@tailwindcss/vite" from package.json and run npm/yarn install;
alternatively, if you prefer build-time Tailwind, add the plugin to
vite.config.js (import and include @tailwindcss/vite in the plugins array) and
remove the CDN script from index.html—pick one approach and update package.json,
vite.config.js, and index.html accordingly to keep configuration and
dependencies consistent.
In @frontend/src/pages/ConfirmationModal.jsx:
- Around line 38-43: The confirm button in ConfirmationModal.jsx is hardcoded to
"Delete"; make it configurable by adding a confirmLabel prop (defaulting to
"Delete") and use that prop for the button text in the component (replace the
literal "Delete" with confirmLabel). Update all call sites (e.g.,
ProjectSettings.jsx) to pass an appropriate confirmLabel like "Delete", "Save",
"Confirm" or "Submit" depending on context so the modal is reusable across
scenarios.
- Line 13: The modal in ConfirmationModal.jsx currently forces a light theme via
the className string (e.g., "bg-white ... text-gray-900 text-gray-600"); update
the root container class in the ConfirmationModal component to use the app's
dark theme CSS variables instead (replace bg-white with a background using
var(--color-bg-main) or the surface variable, replace
text-gray-900/text-gray-600 with var(--color-text-main) and a muted text
variable such as var(--color-text-muted)), and ensure the modal buttons reuse
the app's existing dark-theme button classes so the modal matches the global
dark theme.
- Around line 8-15: The modal markup in ConfirmationModal.jsx lacks keyboard and
ARIA accessibility: add role="dialog", aria-modal="true" and
aria-labelledby/aria-describedby on the inner modal container, set initial focus
to the primary interactive element (e.g., the cancel/confirm button) when the
modal mounts, add an Escape key handler that calls onCancel (attach/remove on
mount/unmount), and implement a focus trap so Tab/Shift+Tab loop inside the
modal (use a small manual trap around the elements or integrate a library like
focus-trap-react/@headlessui and apply it to the element with className
"confirmationModal"). Ensure the click-away outer div still triggers onCancel
and the inner div stops propagation.
In @frontend/src/pages/Database.jsx:
- Around line 117-127: In handleAddDocument, avoid coercing empty/missing
optional fields which currently turn into NaN/false; before converting
formattedData[field.key] for Number or Boolean (iterating
activeCollection.model), only perform Number(...) or === "true" when
formattedData[field.key] is not null/undefined and not an empty string (e.g.,
check formattedData[field.key] != null && formattedData[field.key] !== "") so
absent values stay undefined/empty instead of becoming NaN/false.
- Around line 252-317: The ConfirmationModal is being rendered as a child of the
<table> inside the TableView component which produces invalid DOM; move the
modal out of the table element so it is a sibling (or placed outside the table
container) and keep its props and handlers intact (showModal, selectedId,
handleDelete, setShowModal, onConfirm/onCancel). Locate the TableView component
and remove the ConfirmationModal from inside the <table> JSX, then render it
immediately after the closing </table> (or at a higher-level sibling container)
so the modal overlays correctly without breaking table structure.
- Around line 47-70: The useEffect that defines fetchProject should not depend
on searchParams because updating URL params via setSearchParams causes an
unnecessary refetch loop; remove searchParams from the dependency array so
fetchProject only runs on changes to projectId or token, and keep the current
error handling in fetchProject; additionally, in the effect that runs when
activeCollection changes, guard the setSearchParams call by comparing the
current searchParams.get("collection") to activeCollection.name and only call
setSearchParams when they differ to avoid redundant URL updates (refer to
useEffect, fetchProject, searchParams, setSearchParams, and activeCollection).
In @frontend/src/pages/ProjectSettings.jsx:
- Around line 14-15: Update the inline comment above the state declaration for
showModal in ProjectSettings.jsx to correct the typo: change "model" to "modal"
so the comment reads something like "// used showModal to open the Confirmation
modal"; the unique symbol to locate is the const [showModal, setShowModal] =
useState(false) declaration.
- Around line 267-297: Remove the redundant confirmation modal and its state:
keep the typed-name safety check (deleteConfirm vs project?.name) and call
handleDeleteProject directly from the "Permanently Delete Project" button
onClick; remove showModal state, setShowModal usages, the ConfirmationModal
component usage and its props (open, title, message, onConfirm, onCancel), and
any related imports so the button executes handleDeleteProject immediately when
enabled while still being disabled unless deleteConfirm === project?.name.
🧹 Nitpick comments (12)
frontend/index.html (1)
9-14: Consider using modern font loading strategies.The Google Fonts links are correctly preconnected, but for better performance, consider using
font-display: swapin your CSS or switching to self-hosted fonts to eliminate the external dependency and improve Core Web Vitals.frontend/src/index.css (1)
522-528: Replace custom CSS classes with Tailwind utilities.Adding custom
.confirmationModaland.confirmationModalBtnclasses contradicts the Tailwind CSS approach you've adopted. These simple padding and width styles should use Tailwind utilities directly:
.confirmationModal(padding: 1rem) → usep-4class.confirmationModalBtn(padding: 5px; width: 5rem) → usepx-1.5 py-1 w-20classesMixing custom CSS with Tailwind utilities reduces consistency and makes the codebase harder to maintain.
♻️ Remove custom classes and use Tailwind utilities
Remove these CSS rules:
-} -.confirmationModal{ - padding: 1rem; -} -.confirmationModalBtn{ - padding: 5px; - width: 5rem; - }Then update
ConfirmationModal.jsxto use Tailwind classes directly:- className="bg-white w-[380px] rounded-2xl shadow-lg px-6 py-5 confirmationModal" + className="bg-white w-[380px] rounded-2xl shadow-lg px-6 py-5 p-4"- className="px-4 py-2 text-sm rounded-md border border-gray-300 text-gray-700 hover:bg-gray-100 confirmationModalBtn" + className="px-4 py-2 text-sm rounded-md border border-gray-300 text-gray-700 hover:bg-gray-100 w-20"(Note: Verify exact spacing needs and adjust classes accordingly)
frontend/src/pages/ConfirmationModal.jsx (1)
1-50: Add prop validation for better developer experience.Since you're using React 19 without TypeScript, consider adding runtime prop validation with
prop-typesto catch integration errors early.📝 Add PropTypes
Install
prop-types:npm install prop-typesThen add to the component:
import React, { useEffect, useRef } from "react"; import { AlertTriangle } from "lucide-react"; +import PropTypes from "prop-types"; function ConfirmationModal({ open, title, message, onConfirm, onCancel, confirmLabel, cancelLabel }) { // ... } +ConfirmationModal.propTypes = { + open: PropTypes.bool.isRequired, + title: PropTypes.string.isRequired, + message: PropTypes.string.isRequired, + onConfirm: PropTypes.func.isRequired, + onCancel: PropTypes.func.isRequired, + confirmLabel: PropTypes.string, + cancelLabel: PropTypes.string +}; + export default ConfirmationModal;frontend/src/pages/ProjectSettings.jsx (6)
69-73: Remove commented-out code.Leaving commented code in production degrades maintainability. If you need to preserve this for reference, document it in a commit message or comment explaining why the native
confirm()was removed.🧹 Remove dead code
const handleDeleteProject = async () => { if (deleteConfirm !== project.name) return toast.error("Project name does not match"); - // // if ( - // // !confirm("Final warning: This will delete the project and all its data.") - // // ) - // return; - try {
26-42: Add error handling for network failures.The
fetchProjectfunction logs errors via toast but doesn't handle specific failure scenarios. Consider:
- 401/403: Token expired or unauthorized → redirect to login
- 404: Project not found → redirect to dashboard with message
- Network error: Retry mechanism or offline indicator
🔧 Enhanced error handling
useEffect(() => { const fetchProject = async () => { try { const res = await axios.get(`${API_URL}/api/projects/${projectId}`, { headers: { Authorization: `Bearer ${token}` }, }); setProject(res.data); setNewName(res.data.name); - } catch { - toast.error("Failed to load project"); + } catch (err) { + if (err.response?.status === 401 || err.response?.status === 403) { + toast.error("Session expired. Please log in again."); + navigate("/login"); + } else if (err.response?.status === 404) { + toast.error("Project not found"); + navigate("/dashboard"); + } else { + toast.error("Failed to load project"); + } } finally { setLoading(false); } }; fetchProject(); }, [projectId, token, navigate]);
45-63: Validate project name format and length.The current validation only checks for empty/whitespace names. Consider adding:
- Minimum/maximum length constraints
- Character restrictions (e.g., no special characters if your backend doesn't support them)
- Trim whitespace before submission
✅ Add comprehensive validation
const handleRename = async () => { - if (!newName.trim()) return toast.error("Project name cannot be empty"); + const trimmedName = newName.trim(); + + if (!trimmedName) { + return toast.error("Project name cannot be empty"); + } + + if (trimmedName.length < 3) { + return toast.error("Project name must be at least 3 characters"); + } + + if (trimmedName.length > 50) { + return toast.error("Project name must be less than 50 characters"); + } setRenaming(true); try { await axios.patch( `${API_URL}/api/projects/${projectId}`, - { name: newName }, + { name: trimmedName }, { headers: { Authorization: `Bearer ${token}` } } ); toast.success("Project renamed successfully!"); - setProject((prev) => ({ ...prev, name: newName })); + setProject((prev) => ({ ...prev, name: trimmedName })); + setNewName(trimmedName); // Update state with trimmed version } catch {
304-467: Consider extracting DatabaseConfigForm to a separate file.This component is 160+ lines and handles its own state and API logic. Keeping it in the same file as
ProjectSettingsreduces modularity and makes testing harder.Recommendation: Extract to
frontend/src/components/DatabaseConfigForm.jsx(same forStorageConfigForm).
311-314: useEffect dependency may cause unnecessary rerenders.This effect runs whenever
projectchanges, which happens on every fetch or optimistic update. Ifprojectis a large object, this could cause performance issues.Consider using a more specific dependency or memoizing the check:
⚡ Optimize dependency
useEffect(() => { - const configured = project?.resources?.db?.isExternal || false; - setShowForm(!configured); - }, [project]); + setShowForm(!(project?.resources?.db?.isExternal || false)); + }, [project?.resources?.db?.isExternal]);This way the effect only runs when the actual configuration status changes, not on every project update.
316-336: Add loading state feedback and disable form during submission.The form doesn't disable inputs while
loadingis true, allowing users to modify values mid-submission, potentially causing race conditions.🔒 Disable form during submission
<input type="password" className="input-field" placeholder="mongodb+srv://user:pass@cluster.mongodb.net/..." value={dbUri} onChange={(e) => setDbUri(e.target.value)} + disabled={loading} style={{Also consider showing a loading spinner or progress indicator for better UX.
frontend/src/pages/Database.jsx (3)
99-115: Delete confirmation should await deletion (or show pending state) before closing.Right now
onConfirmfireshandleDelete(selectedId);and immediately closes the modal (Lines 308-311), which can feel “successful” even if the request fails.Proposed fix (await + basic guard)
<ConfirmationModal open={showModal} title="Delete Record" message="Are you sure you want to delete this record? This action cannot be undone." - onConfirm={() => { - handleDelete(selectedId); - setShowModal(false); - }} + onConfirm={async () => { + if (!selectedId) return; + await handleDelete(selectedId); + setShowModal(false); + setSelectedId(null); + }} - onCancel={() => setShowModal(false)} + onCancel={() => { + setShowModal(false); + setSelectedId(null); + }} />Also applies to: 303-313
456-495: Add-document modal: add basic dialog a11y hooks (role/aria + Escape/backdrop behavior).This overlay/modal structure currently lacks obvious
role="dialog",aria-modal="true", and Escape-to-close handling (Lines 456-495). Even a minimal pass would help.
497-861: Consider moving the large inline<style>block to a stylesheet (or Tailwind utilities).Keeping ~300+ lines of CSS inside the component makes the page harder to maintain and increases the chance of global selector collisions.
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
frontend/package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (6)
frontend/index.htmlfrontend/package.jsonfrontend/src/index.cssfrontend/src/pages/ConfirmationModal.jsxfrontend/src/pages/Database.jsxfrontend/src/pages/ProjectSettings.jsx
🔇 Additional comments (2)
frontend/src/pages/ConfirmationModal.jsx (1)
4-5: LGTM—clean early return pattern.The guard clause correctly prevents rendering when the modal is closed.
frontend/src/pages/ProjectSettings.jsx (1)
1-8: LGTM—imports are clean and well-organized.All imports are used and properly structured. The addition of
ConfirmationModalaligns with the PR objectives.
| const handleAddDocument = async (e) => { | ||
| e.preventDefault(); | ||
| try { | ||
| const formattedData = { ...newData }; | ||
| (activeCollection.model || []).forEach((field) => { | ||
| if (field.type === "Number") | ||
| formattedData[field.key] = Number(formattedData[field.key]); | ||
| if (field.type === "Boolean") | ||
| formattedData[field.key] = formattedData[field.key] === "true"; | ||
| }); | ||
|
|
There was a problem hiding this comment.
handleAddDocument: optional Number/Boolean fields can become NaN/false unintentionally.
For missing/empty optional fields, Number(undefined) ⇒ NaN and "" === "true" ⇒ false (Lines 121-126). That can send surprising values to the backend and potentially break validation.
Proposed fix (only coerce when a value is present)
const formattedData = { ...newData };
(activeCollection.model || []).forEach((field) => {
+ const raw = formattedData[field.key];
+ if (raw === "" || raw === undefined || raw === null) {
+ delete formattedData[field.key];
+ return;
+ }
if (field.type === "Number")
- formattedData[field.key] = Number(formattedData[field.key]);
+ formattedData[field.key] = Number(raw);
if (field.type === "Boolean")
- formattedData[field.key] = formattedData[field.key] === "true";
+ formattedData[field.key] = raw === "true";
});🤖 Prompt for AI Agents
In @frontend/src/pages/Database.jsx around lines 117 - 127, In
handleAddDocument, avoid coercing empty/missing optional fields which currently
turn into NaN/false; before converting formattedData[field.key] for Number or
Boolean (iterating activeCollection.model), only perform Number(...) or ===
"true" when formattedData[field.key] is not null/undefined and not an empty
string (e.g., check formattedData[field.key] != null && formattedData[field.key]
!== "") so absent values stay undefined/empty instead of becoming NaN/false.
| const TableView = () => ( | ||
| <div className="table-container fade-in"> | ||
| <table className="modern-table"> | ||
| <thead> | ||
| <tr> | ||
| <th className="w-12">#</th> | ||
| {(activeCollection.model || []).map((field) => ( | ||
| <th key={field.key}> | ||
| <div className="th-content"> | ||
| {field.key} | ||
| <span className="type-badge">{field.type}</span> | ||
| </div> | ||
| </th> | ||
| ))} | ||
| <th>ID</th> | ||
| <th className="w-10">Actions</th> | ||
| </tr> | ||
| </thead> | ||
| <tbody> | ||
| {data.map((row, i) => ( | ||
| <tr key={row._id} className="table-row"> | ||
| <td className="text-muted">{i + 1}</td> | ||
| {(activeCollection.model || []).map((field) => ( | ||
| <td key={field.key}> | ||
| {typeof row[field.key] === "boolean" ? ( | ||
| <span | ||
| className={`status-badge ${ | ||
| row[field.key] ? "success" : "danger" | ||
| }`} | ||
| > | ||
| {String(row[field.key])} | ||
| </span> | ||
| ) : ( | ||
| <span className="cell-text">{String(row[field.key])}</span> | ||
| )} | ||
| </td> | ||
| ))} | ||
| <td className="font-mono text-xs text-muted"> | ||
| {row._id.substring(0, 8)}... | ||
| </td> | ||
| <td> | ||
| <button | ||
| className="btn-icon danger-hover" | ||
| onClick={() => fetchShowModal(row._id)} | ||
| > | ||
| <Trash2 size={15} /> | ||
| </button> | ||
| </td> | ||
| </tr> | ||
| ))} | ||
| </tbody> | ||
| {showModal && ( | ||
| <ConfirmationModal | ||
| open={showModal} | ||
| title="Delete Record" | ||
| message="Are you sure you want to delete this record? This action cannot be undone." | ||
| onConfirm={() => { | ||
| handleDelete(selectedId); | ||
| setShowModal(false); | ||
| }} | ||
| onCancel={() => setShowModal(false)} | ||
| /> | ||
| )} | ||
| </table> | ||
| </div> | ||
| ); |
There was a problem hiding this comment.
Modal is rendered inside <table> (invalid DOM) — move it outside the table element.
ConfirmationModal is a direct child of <table> (Lines 303-314). Even if it visually overlays, this is invalid table structure and can lead to browser DOM “fixups” and weird layout/accessibility behavior.
Proposed fix (render modal as a sibling to the table)
- const TableView = () => (
- <div className="table-container fade-in">
- <table className="modern-table">
+ const TableView = () => (
+ <div className="table-container fade-in">
+ <table className="modern-table">
<thead>
...
</thead>
<tbody>
...
</tbody>
- {showModal && (
- <ConfirmationModal
- open={showModal}
- title="Delete Record"
- message="Are you sure you want to delete this record? This action cannot be undone."
- onConfirm={() => {
- handleDelete(selectedId);
- setShowModal(false);
- }}
- onCancel={() => setShowModal(false)}
- />
- )}
</table>
+ {showModal && (
+ <ConfirmationModal
+ open={showModal}
+ title="Delete Record"
+ message="Are you sure you want to delete this record? This action cannot be undone."
+ onConfirm={async () => {
+ if (!selectedId) return;
+ await handleDelete(selectedId);
+ setShowModal(false);
+ setSelectedId(null);
+ }}
+ onCancel={() => {
+ setShowModal(false);
+ setSelectedId(null);
+ }}
+ />
+ )}
</div>
);🤖 Prompt for AI Agents
In @frontend/src/pages/Database.jsx around lines 252 - 317, The
ConfirmationModal is being rendered as a child of the <table> inside the
TableView component which produces invalid DOM; move the modal out of the table
element so it is a sibling (or placed outside the table container) and keep its
props and handlers intact (showModal, selectedId, handleDelete, setShowModal,
onConfirm/onCancel). Locate the TableView component and remove the
ConfirmationModal from inside the <table> JSX, then render it immediately after
the closing </table> (or at a higher-level sibling container) so the modal
overlays correctly without breaking table structure.
| <button | ||
| onClick={() => { | ||
| setShowModal(true); | ||
| }} | ||
| className="btn btn-danger" | ||
| disabled={deleteConfirm !== project?.name} | ||
| style={{ | ||
| width: "100%", | ||
| justifyContent: "center", | ||
| background: "#ea5455", | ||
| border: "none", | ||
| color: "#fff", | ||
| padding: "12px", | ||
| borderRadius: "8px", | ||
| marginTop: "10px", | ||
| }} | ||
| > | ||
| <Trash2 size={18} /> Permanently Delete Project | ||
| </button> | ||
| {showModal && ( | ||
| <ConfirmationModal | ||
| open={showModal} | ||
| title="Delete Project" | ||
| message="Are you sure you want to delete this project? This action cannot be undone." | ||
| onConfirm={() => { | ||
| handleDeleteProject(); | ||
| setShowModal(false); | ||
| }} | ||
| onCancel={() => setShowModal(false)} | ||
| /> | ||
| )} |
There was a problem hiding this comment.
Improve modal confirmation UX flow.
The current flow requires users to:
- Type the project name in the input (line 254)
- Click "Permanently Delete Project" button (line 267)
- Confirm again in the modal (line 291)
This creates redundant confirmation—the typed name confirmation already provides sufficient friction. The modal adds little value here and frustrates users.
Recommendation: Either remove the modal OR remove the text input requirement. Don't require both.
💡 Suggested UX improvements
Option 1 (Recommended): Keep the text input, remove the modal:
<button
- onClick={() => {
- setShowModal(true);
- }}
+ onClick={handleDeleteProject}
className="btn btn-danger"
disabled={deleteConfirm !== project?.name}
>
<Trash2 size={18} /> Permanently Delete Project
</button>
- {showModal && (
- <ConfirmationModal
- open={showModal}
- title="Delete Project"
- message="Are you sure you want to delete this project? This action cannot be undone."
- onConfirm={() => {
- handleDeleteProject();
- setShowModal(false);
- }}
- onCancel={() => setShowModal(false)}
- />
- )}Option 2: Remove text input, keep modal for visual confirmation:
Remove lines 233-265 (the input field and its label), and keep the modal as the sole confirmation mechanism.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <button | |
| onClick={() => { | |
| setShowModal(true); | |
| }} | |
| className="btn btn-danger" | |
| disabled={deleteConfirm !== project?.name} | |
| style={{ | |
| width: "100%", | |
| justifyContent: "center", | |
| background: "#ea5455", | |
| border: "none", | |
| color: "#fff", | |
| padding: "12px", | |
| borderRadius: "8px", | |
| marginTop: "10px", | |
| }} | |
| > | |
| <Trash2 size={18} /> Permanently Delete Project | |
| </button> | |
| {showModal && ( | |
| <ConfirmationModal | |
| open={showModal} | |
| title="Delete Project" | |
| message="Are you sure you want to delete this project? This action cannot be undone." | |
| onConfirm={() => { | |
| handleDeleteProject(); | |
| setShowModal(false); | |
| }} | |
| onCancel={() => setShowModal(false)} | |
| /> | |
| )} | |
| <button | |
| onClick={handleDeleteProject} | |
| className="btn btn-danger" | |
| disabled={deleteConfirm !== project?.name} | |
| style={{ | |
| width: "100%", | |
| justifyContent: "center", | |
| background: "#ea5455", | |
| border: "none", | |
| color: "#fff", | |
| padding: "12px", | |
| borderRadius: "8px", | |
| marginTop: "10px", | |
| }} | |
| > | |
| <Trash2 size={18} /> Permanently Delete Project | |
| </button> |
🤖 Prompt for AI Agents
In @frontend/src/pages/ProjectSettings.jsx around lines 267 - 297, Remove the
redundant confirmation modal and its state: keep the typed-name safety check
(deleteConfirm vs project?.name) and call handleDeleteProject directly from the
"Permanently Delete Project" button onClick; remove showModal state,
setShowModal usages, the ConfirmationModal component usage and its props (open,
title, message, onConfirm, onCancel), and any related imports so the button
executes handleDeleteProject immediately when enabled while still being disabled
unless deleteConfirm === project?.name.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
@tejaavula076 yes its working properly u can check here - https://ur-backend-git-fork-tejaavula076-9b5337-yash-pouraniks-projects.vercel.app/ but make sure the theme of the delete component should be same as the project theme. and one check is failing of backend test but i think thats from my side, will look into it and comment here. |
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 Fix all issues with AI agents
In @frontend/src/pages/ProjectSettings.jsx:
- Around line 44-63: The catch block in handleRename swallows errors; update it
to catch the error (e.g., catch (err)) and log the actual error details (for
example using console.error with a descriptive message like "Failed to rename
project") in addition to showing the toast, so debugging information is
preserved while keeping the existing toast and state updates.
- Around line 65-83: In handleDeleteProject remove the leftover commented-out
window.confirm block (the lines around the old confirm call) so the modal-only
flow remains clean, and update the catch of the axios.delete call to accept an
error parameter and log it (e.g., console.error or logger.error) and include the
error information when calling toast.error so failures are recorded and easier
to debug; ensure changes are applied inside the handleDeleteProject function
that performs the axios.delete request.
- Around line 304-467: The catch in DatabaseConfigForm.handleUpdate currently
swallows errors; update it to log the caught error for debugging (e.g.,
console.error(err) and/or send to your error tracking service) in addition to
showing the toast (keep the existing toast.error(err.response?.data?.error ||
"Failed to update DB config")). Ensure you reference the err variable inside the
catch before finally and do not remove existing UI behavior.
- Around line 26-42: The catch block in the fetchProject async function (inside
useEffect) swallows errors; update fetchProject to accept the error parameter
(e.g., catch (err)) and log the error (console.error(err) or a centralized
logger) when a request fails, while still calling toast.error("Failed to load
project"), so setProject/setNewName logic stays unchanged and setLoading(false)
remains in finally.
- Around line 469-710: The catch block in StorageConfigForm.handleUpdate only
shows a toast and doesn't log the error; update that catch to log the full error
for debugging (e.g., console.error or a configured logger) including err and
err.response?.data (and err.message) so you capture API response details and
stack traces while still showing the existing toast; locate the catch inside the
async handleUpdate function and add the logging before the toast/error-handling
lines.
🧹 Nitpick comments (1)
frontend/src/pages/ProjectSettings.jsx (1)
286-297: Simplify modal rendering logic.The modal is both conditionally rendered (
{showModal && ...}) and receivesopen={showModal}. This is redundant and prevents smooth animations/transitions.♻️ Recommended approach
Remove the conditional rendering and let the modal's
openprop control visibility:<Trash2 size={18} /> Permanently Delete Project </button> - {showModal && ( - <ConfirmationModal - open={showModal} - title="Delete Project" - message="Are you sure you want to delete this project? This action cannot be undone." - onConfirm={() => { - handleDeleteProject(); - setShowModal(false); - }} - onCancel={() => setShowModal(false)} - /> - )} + <ConfirmationModal + open={showModal} + title="Delete Project" + message="Are you sure you want to delete this project? This action cannot be undone." + onConfirm={() => { + handleDeleteProject(); + setShowModal(false); + }} + onCancel={() => setShowModal(false)} + /> </div> </div>This allows the ConfirmationModal to handle enter/exit transitions smoothly.
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
frontend/src/pages/ProjectSettings.jsx
🔇 Additional comments (2)
frontend/src/pages/ProjectSettings.jsx (2)
1-8: LGTM!Imports are clean and all dependencies are properly used throughout the component.
11-24: LGTM!State declarations are well-organized and clearly commented.
| useEffect(() => { | ||
| const fetchProject = async () => { | ||
| try { | ||
| const res = await axios.get(`${API_URL}/api/projects/${projectId}`, { | ||
| headers: { Authorization: `Bearer ${token}` }, | ||
| }); | ||
| setProject(res.data); | ||
| // Set initial name for renaming | ||
| setNewName(res.data.name); | ||
| } catch { | ||
| toast.error("Failed to load project"); | ||
| } finally { | ||
| setLoading(false); | ||
| } | ||
| }; | ||
| fetchProject(); | ||
| }, [projectId, token]); |
There was a problem hiding this comment.
Log errors in catch blocks for debugging.
The catch block swallows the error after displaying a toast. In production, this makes debugging failed requests difficult.
🔍 Proposed fix
} catch {
toast.error("Failed to load project");
+ console.error("Failed to load project:", error);
} finally {Also update the catch parameter:
- } catch {
+ } catch (error) {
toast.error("Failed to load project");
+ console.error("Failed to load project:", error);🤖 Prompt for AI Agents
In @frontend/src/pages/ProjectSettings.jsx around lines 26 - 42, The catch block
in the fetchProject async function (inside useEffect) swallows errors; update
fetchProject to accept the error parameter (e.g., catch (err)) and log the error
(console.error(err) or a centralized logger) when a request fails, while still
calling toast.error("Failed to load project"), so setProject/setNewName logic
stays unchanged and setLoading(false) remains in finally.
| // --- NEW: HANDLE RENAME --- | ||
| const handleRename = async () => { | ||
| if (!newName.trim()) return toast.error("Project name cannot be empty"); | ||
|
|
||
| {/* General Settings (Rename Feature) */} | ||
| <div className="card" style={{ marginBottom: '2rem' }}> | ||
| <h3 style={{ fontSize: '1.1rem', marginBottom: '1.5rem', fontWeight: 600, display: 'flex', alignItems: 'center', gap: '10px' }}> | ||
| <div style={{ width: '6px', height: '24px', background: 'var(--color-primary)', borderRadius: '4px' }}></div> | ||
| General Information | ||
| </h3> | ||
| <div className="form-group" style={{ display: 'flex', gap: '15px', alignItems: 'flex-end', flexWrap: 'wrap' }}> | ||
| <div style={{ flex: 1, minWidth: '250px' }}> | ||
| <label className="form-label" style={{ display: 'block', marginBottom: '8px', fontSize: '0.9rem', color: 'var(--color-text-muted)' }}>Project Name</label> | ||
| <input | ||
| type="text" | ||
| className="input-field" | ||
| value={newName} | ||
| onChange={(e) => setNewName(e.target.value)} | ||
| style={{ width: '100%', padding: '12px', background: 'var(--color-bg-input)', border: '1px solid var(--color-border)', borderRadius: '8px', color: '#fff' }} | ||
| /> | ||
| </div> | ||
| <button | ||
| onClick={handleRename} | ||
| className="btn btn-primary" | ||
| disabled={renaming || newName === project?.name} | ||
| style={{ padding: '12px 24px', height: '45px' }} | ||
| > | ||
| {renaming ? 'Saving...' : 'Save Changes'} | ||
| </button> | ||
| </div> | ||
| </div> | ||
| setRenaming(true); | ||
| try { | ||
| await axios.patch( | ||
| `${API_URL}/api/projects/${projectId}`, | ||
| { name: newName }, | ||
| { headers: { Authorization: `Bearer ${token}` } } | ||
| ); | ||
| toast.success("Project renamed successfully!"); | ||
| // Update local state to reflect change immediately | ||
| setProject((prev) => ({ ...prev, name: newName })); | ||
| } catch { | ||
| toast.error("Failed to rename project"); | ||
| } finally { | ||
| setRenaming(false); | ||
| } | ||
| }; |
There was a problem hiding this comment.
Log errors in catch blocks for debugging.
Same issue as in the fetch function - the catch block should log the actual error for debugging purposes.
🔍 Proposed fix
- } catch {
+ } catch (error) {
toast.error("Failed to rename project");
+ console.error("Failed to rename project:", error);
} finally {📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // --- NEW: HANDLE RENAME --- | |
| const handleRename = async () => { | |
| if (!newName.trim()) return toast.error("Project name cannot be empty"); | |
| {/* General Settings (Rename Feature) */} | |
| <div className="card" style={{ marginBottom: '2rem' }}> | |
| <h3 style={{ fontSize: '1.1rem', marginBottom: '1.5rem', fontWeight: 600, display: 'flex', alignItems: 'center', gap: '10px' }}> | |
| <div style={{ width: '6px', height: '24px', background: 'var(--color-primary)', borderRadius: '4px' }}></div> | |
| General Information | |
| </h3> | |
| <div className="form-group" style={{ display: 'flex', gap: '15px', alignItems: 'flex-end', flexWrap: 'wrap' }}> | |
| <div style={{ flex: 1, minWidth: '250px' }}> | |
| <label className="form-label" style={{ display: 'block', marginBottom: '8px', fontSize: '0.9rem', color: 'var(--color-text-muted)' }}>Project Name</label> | |
| <input | |
| type="text" | |
| className="input-field" | |
| value={newName} | |
| onChange={(e) => setNewName(e.target.value)} | |
| style={{ width: '100%', padding: '12px', background: 'var(--color-bg-input)', border: '1px solid var(--color-border)', borderRadius: '8px', color: '#fff' }} | |
| /> | |
| </div> | |
| <button | |
| onClick={handleRename} | |
| className="btn btn-primary" | |
| disabled={renaming || newName === project?.name} | |
| style={{ padding: '12px 24px', height: '45px' }} | |
| > | |
| {renaming ? 'Saving...' : 'Save Changes'} | |
| </button> | |
| </div> | |
| </div> | |
| setRenaming(true); | |
| try { | |
| await axios.patch( | |
| `${API_URL}/api/projects/${projectId}`, | |
| { name: newName }, | |
| { headers: { Authorization: `Bearer ${token}` } } | |
| ); | |
| toast.success("Project renamed successfully!"); | |
| // Update local state to reflect change immediately | |
| setProject((prev) => ({ ...prev, name: newName })); | |
| } catch { | |
| toast.error("Failed to rename project"); | |
| } finally { | |
| setRenaming(false); | |
| } | |
| }; | |
| // --- NEW: HANDLE RENAME --- | |
| const handleRename = async () => { | |
| if (!newName.trim()) return toast.error("Project name cannot be empty"); | |
| setRenaming(true); | |
| try { | |
| await axios.patch( | |
| `${API_URL}/api/projects/${projectId}`, | |
| { name: newName }, | |
| { headers: { Authorization: `Bearer ${token}` } } | |
| ); | |
| toast.success("Project renamed successfully!"); | |
| // Update local state to reflect change immediately | |
| setProject((prev) => ({ ...prev, name: newName })); | |
| } catch (error) { | |
| toast.error("Failed to rename project"); | |
| console.error("Failed to rename project:", error); | |
| } finally { | |
| setRenaming(false); | |
| } | |
| }; |
🤖 Prompt for AI Agents
In @frontend/src/pages/ProjectSettings.jsx around lines 44 - 63, The catch block
in handleRename swallows errors; update it to catch the error (e.g., catch
(err)) and log the actual error details (for example using console.error with a
descriptive message like "Failed to rename project") in addition to showing the
toast, so debugging information is preserved while keeping the existing toast
and state updates.
| const handleDeleteProject = async () => { | ||
| if (deleteConfirm !== project.name) | ||
| return toast.error("Project name does not match"); | ||
|
|
||
| {/* Danger Zone */} | ||
| <div className="card" style={{ border: '1px solid rgba(234, 84, 85, 0.3)', background: 'rgba(234, 84, 85, 0.02)' }}> | ||
| <div style={{ display: 'flex', gap: '10px', alignItems: 'center', marginBottom: '1.5rem', color: '#ea5455' }}> | ||
| <AlertTriangle size={24} /> | ||
| <h3 style={{ fontSize: '1.1rem', fontWeight: 600 }}>Danger Zone</h3> | ||
| </div> | ||
|
|
||
| <p style={{ color: 'var(--color-text-muted)', marginBottom: '1.5rem', fontSize: '0.9rem', lineHeight: '1.6' }}> | ||
| This action cannot be undone. This will permanently delete the | ||
| <strong style={{ color: '#fff' }}> {project?.name}</strong> project and all associated data including collections, files, and users. | ||
| </p> | ||
|
|
||
| <div style={{ maxWidth: '500px' }}> | ||
| <div className="form-group" style={{ marginBottom: '1rem' }}> | ||
| <label className="form-label" style={{ color: '#ea5455', fontWeight: 500, marginBottom: '8px', display: 'block' }}> | ||
| Type <strong style={{ textDecoration: 'underline' }}>{project?.name}</strong> to confirm | ||
| </label> | ||
| <input | ||
| type="text" | ||
| className="input-field" | ||
| placeholder={project?.name} | ||
| value={deleteConfirm} | ||
| onChange={(e) => setDeleteConfirm(e.target.value)} | ||
| style={{ width: '100%', padding: '12px', background: 'var(--color-bg-input)', border: '1px solid rgba(234, 84, 85, 0.3)', borderRadius: '8px', color: '#fff' }} | ||
| /> | ||
| </div> | ||
| <button | ||
| onClick={handleDeleteProject} | ||
| className="btn btn-danger" | ||
| disabled={deleteConfirm !== project?.name} | ||
| style={{ width: '100%', justifyContent: 'center', background: '#ea5455', border: 'none', color: '#fff', padding: '12px', borderRadius: '8px', marginTop: '10px' }} | ||
| > | ||
| <Trash2 size={18} /> Permanently Delete Project | ||
| </button> | ||
| </div> | ||
| </div> | ||
| // // if ( | ||
| // // !confirm("Final warning: This will delete the project and all its data.") | ||
| // // ) | ||
| // return; | ||
|
|
||
| try { | ||
| await axios.delete(`${API_URL}/api/projects/${projectId}`, { | ||
| headers: { Authorization: `Bearer ${token}` }, | ||
| }); | ||
| toast.success("Project deleted"); | ||
| navigate("/dashboard"); | ||
| } catch { | ||
| toast.error("Failed to delete project"); | ||
| } | ||
| }; |
There was a problem hiding this comment.
Remove commented-out code and log errors.
Two issues:
- Lines 69-72 contain commented-out code that should be removed since the window.confirm has been replaced with the modal.
- The catch block should log the error for debugging.
🧹 Proposed fix
const handleDeleteProject = async () => {
if (deleteConfirm !== project.name)
return toast.error("Project name does not match");
- // // if (
- // // !confirm("Final warning: This will delete the project and all its data.")
- // // )
- // return;
-
try {
await axios.delete(`${API_URL}/api/projects/${projectId}`, {
headers: { Authorization: `Bearer ${token}` },
});
toast.success("Project deleted");
navigate("/dashboard");
- } catch {
+ } catch (error) {
toast.error("Failed to delete project");
+ console.error("Failed to delete project:", error);
}
};🤖 Prompt for AI Agents
In @frontend/src/pages/ProjectSettings.jsx around lines 65 - 83, In
handleDeleteProject remove the leftover commented-out window.confirm block (the
lines around the old confirm call) so the modal-only flow remains clean, and
update the catch of the axios.delete call to accept an error parameter and log
it (e.g., console.error or logger.error) and include the error information when
calling toast.error so failures are recorded and easier to debug; ensure changes
are applied inside the handleDeleteProject function that performs the
axios.delete request.
| function DatabaseConfigForm({ project, projectId, token }) { | ||
| const [dbUri, setDbUri] = useState(''); | ||
| const [loading, setLoading] = useState(false); | ||
| // Use optional chaining carefully - project might be null initially | ||
| const isConfigured = project?.resources?.db?.isExternal || false; | ||
| const [showForm, setShowForm] = useState(!isConfigured); | ||
|
|
||
| useEffect(() => { | ||
| const configured = project?.resources?.db?.isExternal || false; | ||
| setShowForm(!configured); | ||
| }, [project]); | ||
|
|
||
| const handleUpdate = async () => { | ||
| if (!dbUri) return toast.error("Database URI is required"); | ||
|
|
||
| setLoading(true); | ||
| try { | ||
| await axios.patch(`${API_URL}/api/projects/${projectId}/byod-config`, | ||
| { dbUri }, | ||
| { headers: { Authorization: `Bearer ${token}` } } | ||
| ); | ||
| toast.success("Database configuration updated!"); | ||
| setShowForm(false); | ||
| setDbUri(''); | ||
| // Typically we'd reload project data here, but for now we rely on user refresh or optimistic ui if needed | ||
| // Ideally notify parent to refresh project, but basic flow: | ||
| } catch (err) { | ||
| toast.error(err.response?.data?.error || "Failed to update DB config"); | ||
| } finally { | ||
| setLoading(false); | ||
| } | ||
| }; | ||
| const [dbUri, setDbUri] = useState(""); | ||
| const [loading, setLoading] = useState(false); | ||
| // Use optional chaining carefully - project might be null initially | ||
| const isConfigured = project?.resources?.db?.isExternal || false; | ||
| const [showForm, setShowForm] = useState(!isConfigured); | ||
|
|
||
| return ( | ||
| <div className="card" style={{ position: 'relative', overflow: 'hidden' }}> | ||
| <div style={{ position: 'absolute', top: 0, left: 0, width: '4px', height: '100%', background: 'var(--color-primary)' }}></div> | ||
|
|
||
| <div style={{ marginBottom: '1rem' }}> | ||
| <h3 style={{ fontSize: '1.1rem', fontWeight: 600, display: 'flex', alignItems: 'center', gap: '10px' }}> | ||
| <Save size={20} color="var(--color-primary)" /> Database Configuration (MongoDB) | ||
| </h3> | ||
| <p style={{ color: 'var(--color-text-muted)', fontSize: '0.9rem', marginTop: '5px' }}> | ||
| Connect your own MongoDB cluster. | ||
| </p> | ||
| </div> | ||
| useEffect(() => { | ||
| const configured = project?.resources?.db?.isExternal || false; | ||
| setShowForm(!configured); | ||
| }, [project]); | ||
|
|
||
| const handleUpdate = async () => { | ||
| if (!dbUri) return toast.error("Database URI is required"); | ||
|
|
||
| setLoading(true); | ||
| try { | ||
| await axios.patch( | ||
| `${API_URL}/api/projects/${projectId}/byod-config`, | ||
| { dbUri }, | ||
| { headers: { Authorization: `Bearer ${token}` } } | ||
| ); | ||
| toast.success("Database configuration updated!"); | ||
| setShowForm(false); | ||
| setDbUri(""); | ||
| // Typically we'd reload project data here, but for now we rely on user refresh or optimistic ui if needed | ||
| // Ideally notify parent to refresh project, but basic flow: | ||
| } catch (err) { | ||
| toast.error(err.response?.data?.error || "Failed to update DB config"); | ||
| } finally { | ||
| setLoading(false); | ||
| } | ||
| }; | ||
|
|
||
| return ( | ||
| <div className="card" style={{ position: "relative", overflow: "hidden" }}> | ||
| <div | ||
| style={{ | ||
| position: "absolute", | ||
| top: 0, | ||
| left: 0, | ||
| width: "4px", | ||
| height: "100%", | ||
| background: "var(--color-primary)", | ||
| }} | ||
| ></div> | ||
|
|
||
| {isConfigured && !showForm ? ( | ||
| <div style={{ marginTop: '1rem', background: 'rgba(16, 185, 129, 0.1)', padding: '1rem', borderRadius: '8px', border: '1px solid rgba(16, 185, 129, 0.2)' }}> | ||
| <div style={{ display: 'flex', alignItems: 'center', gap: '10px', color: '#10B981', fontWeight: 600, marginBottom: '0.5rem' }}> | ||
| <CheckCircle size={18} /> | ||
| Connected | ||
| </div> | ||
| <button className="btn btn-secondary btn-sm" onClick={() => setShowForm(true)} style={{ borderColor: 'var(--color-border)', color: 'var(--color-text-main)' }}> | ||
| Update URI | ||
| </button> | ||
| </div> | ||
| ) : ( | ||
| <div style={{ marginTop: '1rem' }}> | ||
| <div className="form-group"> | ||
| <label className="form-label" style={{ marginBottom: '8px', display: 'block', fontSize: '0.9rem' }}>MongoDB Connection URI</label> | ||
| <input | ||
| type="password" | ||
| className="input-field" | ||
| placeholder="mongodb+srv://user:pass@cluster.mongodb.net/..." | ||
| value={dbUri} | ||
| onChange={(e) => setDbUri(e.target.value)} | ||
| style={{ width: '100%', padding: '12px', background: 'var(--color-bg-input)', border: '1px solid var(--color-border)', borderRadius: '8px', color: '#fff', fontFamily: 'monospace' }} | ||
| /> | ||
| </div> | ||
| <div style={{ display: 'flex', justifyContent: 'flex-end', gap: '12px', marginTop: '1rem' }}> | ||
| {isConfigured && <button className="btn btn-ghost" onClick={() => setShowForm(false)}>Cancel</button>} | ||
| <button onClick={handleUpdate} className="btn btn-primary" disabled={loading}> | ||
| {loading ? 'Saving...' : 'Connect Database'} | ||
| </button> | ||
| </div> | ||
| </div> | ||
| <div style={{ marginBottom: "1rem" }}> | ||
| <h3 | ||
| style={{ | ||
| fontSize: "1.1rem", | ||
| fontWeight: 600, | ||
| display: "flex", | ||
| alignItems: "center", | ||
| gap: "10px", | ||
| }} | ||
| > | ||
| <Save size={20} color="var(--color-primary)" /> Database Configuration | ||
| (MongoDB) | ||
| </h3> | ||
| <p | ||
| style={{ | ||
| color: "var(--color-text-muted)", | ||
| fontSize: "0.9rem", | ||
| marginTop: "5px", | ||
| }} | ||
| > | ||
| Connect your own MongoDB cluster. | ||
| </p> | ||
| </div> | ||
|
|
||
| {isConfigured && !showForm ? ( | ||
| <div | ||
| style={{ | ||
| marginTop: "1rem", | ||
| background: "rgba(16, 185, 129, 0.1)", | ||
| padding: "1rem", | ||
| borderRadius: "8px", | ||
| border: "1px solid rgba(16, 185, 129, 0.2)", | ||
| }} | ||
| > | ||
| <div | ||
| style={{ | ||
| display: "flex", | ||
| alignItems: "center", | ||
| gap: "10px", | ||
| color: "#10B981", | ||
| fontWeight: 600, | ||
| marginBottom: "0.5rem", | ||
| }} | ||
| > | ||
| <CheckCircle size={18} /> | ||
| Connected | ||
| </div> | ||
| <button | ||
| className="btn btn-secondary btn-sm" | ||
| onClick={() => setShowForm(true)} | ||
| style={{ | ||
| borderColor: "var(--color-border)", | ||
| color: "var(--color-text-main)", | ||
| }} | ||
| > | ||
| Update URI | ||
| </button> | ||
| </div> | ||
| ) : ( | ||
| <div style={{ marginTop: "1rem" }}> | ||
| <div className="form-group"> | ||
| <label | ||
| className="form-label" | ||
| style={{ | ||
| marginBottom: "8px", | ||
| display: "block", | ||
| fontSize: "0.9rem", | ||
| }} | ||
| > | ||
| MongoDB Connection URI | ||
| </label> | ||
| <input | ||
| type="password" | ||
| className="input-field" | ||
| placeholder="mongodb+srv://user:pass@cluster.mongodb.net/..." | ||
| value={dbUri} | ||
| onChange={(e) => setDbUri(e.target.value)} | ||
| style={{ | ||
| width: "100%", | ||
| padding: "12px", | ||
| background: "var(--color-bg-input)", | ||
| border: "1px solid var(--color-border)", | ||
| borderRadius: "8px", | ||
| color: "#fff", | ||
| fontFamily: "monospace", | ||
| }} | ||
| /> | ||
| </div> | ||
| <div | ||
| style={{ | ||
| display: "flex", | ||
| justifyContent: "flex-end", | ||
| gap: "12px", | ||
| marginTop: "1rem", | ||
| }} | ||
| > | ||
| {isConfigured && ( | ||
| <button | ||
| className="btn btn-ghost" | ||
| onClick={() => setShowForm(false)} | ||
| > | ||
| Cancel | ||
| </button> | ||
| )} | ||
| <button | ||
| onClick={handleUpdate} | ||
| className="btn btn-primary" | ||
| disabled={loading} | ||
| > | ||
| {loading ? "Saving..." : "Connect Database"} | ||
| </button> | ||
| </div> | ||
| </div> | ||
| ); | ||
| )} | ||
| </div> | ||
| ); | ||
| } |
There was a problem hiding this comment.
Log errors in catch blocks for debugging.
The handleUpdate function has an empty catch block (line 331) that swallows the error. Add error logging for production debugging.
🔍 Proposed fix
} catch (err) {
toast.error(err.response?.data?.error || "Failed to update DB config");
+ console.error("Failed to update DB config:", err);
} finally {📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| function DatabaseConfigForm({ project, projectId, token }) { | |
| const [dbUri, setDbUri] = useState(''); | |
| const [loading, setLoading] = useState(false); | |
| // Use optional chaining carefully - project might be null initially | |
| const isConfigured = project?.resources?.db?.isExternal || false; | |
| const [showForm, setShowForm] = useState(!isConfigured); | |
| useEffect(() => { | |
| const configured = project?.resources?.db?.isExternal || false; | |
| setShowForm(!configured); | |
| }, [project]); | |
| const handleUpdate = async () => { | |
| if (!dbUri) return toast.error("Database URI is required"); | |
| setLoading(true); | |
| try { | |
| await axios.patch(`${API_URL}/api/projects/${projectId}/byod-config`, | |
| { dbUri }, | |
| { headers: { Authorization: `Bearer ${token}` } } | |
| ); | |
| toast.success("Database configuration updated!"); | |
| setShowForm(false); | |
| setDbUri(''); | |
| // Typically we'd reload project data here, but for now we rely on user refresh or optimistic ui if needed | |
| // Ideally notify parent to refresh project, but basic flow: | |
| } catch (err) { | |
| toast.error(err.response?.data?.error || "Failed to update DB config"); | |
| } finally { | |
| setLoading(false); | |
| } | |
| }; | |
| const [dbUri, setDbUri] = useState(""); | |
| const [loading, setLoading] = useState(false); | |
| // Use optional chaining carefully - project might be null initially | |
| const isConfigured = project?.resources?.db?.isExternal || false; | |
| const [showForm, setShowForm] = useState(!isConfigured); | |
| return ( | |
| <div className="card" style={{ position: 'relative', overflow: 'hidden' }}> | |
| <div style={{ position: 'absolute', top: 0, left: 0, width: '4px', height: '100%', background: 'var(--color-primary)' }}></div> | |
| <div style={{ marginBottom: '1rem' }}> | |
| <h3 style={{ fontSize: '1.1rem', fontWeight: 600, display: 'flex', alignItems: 'center', gap: '10px' }}> | |
| <Save size={20} color="var(--color-primary)" /> Database Configuration (MongoDB) | |
| </h3> | |
| <p style={{ color: 'var(--color-text-muted)', fontSize: '0.9rem', marginTop: '5px' }}> | |
| Connect your own MongoDB cluster. | |
| </p> | |
| </div> | |
| useEffect(() => { | |
| const configured = project?.resources?.db?.isExternal || false; | |
| setShowForm(!configured); | |
| }, [project]); | |
| const handleUpdate = async () => { | |
| if (!dbUri) return toast.error("Database URI is required"); | |
| setLoading(true); | |
| try { | |
| await axios.patch( | |
| `${API_URL}/api/projects/${projectId}/byod-config`, | |
| { dbUri }, | |
| { headers: { Authorization: `Bearer ${token}` } } | |
| ); | |
| toast.success("Database configuration updated!"); | |
| setShowForm(false); | |
| setDbUri(""); | |
| // Typically we'd reload project data here, but for now we rely on user refresh or optimistic ui if needed | |
| // Ideally notify parent to refresh project, but basic flow: | |
| } catch (err) { | |
| toast.error(err.response?.data?.error || "Failed to update DB config"); | |
| } finally { | |
| setLoading(false); | |
| } | |
| }; | |
| return ( | |
| <div className="card" style={{ position: "relative", overflow: "hidden" }}> | |
| <div | |
| style={{ | |
| position: "absolute", | |
| top: 0, | |
| left: 0, | |
| width: "4px", | |
| height: "100%", | |
| background: "var(--color-primary)", | |
| }} | |
| ></div> | |
| {isConfigured && !showForm ? ( | |
| <div style={{ marginTop: '1rem', background: 'rgba(16, 185, 129, 0.1)', padding: '1rem', borderRadius: '8px', border: '1px solid rgba(16, 185, 129, 0.2)' }}> | |
| <div style={{ display: 'flex', alignItems: 'center', gap: '10px', color: '#10B981', fontWeight: 600, marginBottom: '0.5rem' }}> | |
| <CheckCircle size={18} /> | |
| Connected | |
| </div> | |
| <button className="btn btn-secondary btn-sm" onClick={() => setShowForm(true)} style={{ borderColor: 'var(--color-border)', color: 'var(--color-text-main)' }}> | |
| Update URI | |
| </button> | |
| </div> | |
| ) : ( | |
| <div style={{ marginTop: '1rem' }}> | |
| <div className="form-group"> | |
| <label className="form-label" style={{ marginBottom: '8px', display: 'block', fontSize: '0.9rem' }}>MongoDB Connection URI</label> | |
| <input | |
| type="password" | |
| className="input-field" | |
| placeholder="mongodb+srv://user:pass@cluster.mongodb.net/..." | |
| value={dbUri} | |
| onChange={(e) => setDbUri(e.target.value)} | |
| style={{ width: '100%', padding: '12px', background: 'var(--color-bg-input)', border: '1px solid var(--color-border)', borderRadius: '8px', color: '#fff', fontFamily: 'monospace' }} | |
| /> | |
| </div> | |
| <div style={{ display: 'flex', justifyContent: 'flex-end', gap: '12px', marginTop: '1rem' }}> | |
| {isConfigured && <button className="btn btn-ghost" onClick={() => setShowForm(false)}>Cancel</button>} | |
| <button onClick={handleUpdate} className="btn btn-primary" disabled={loading}> | |
| {loading ? 'Saving...' : 'Connect Database'} | |
| </button> | |
| </div> | |
| </div> | |
| <div style={{ marginBottom: "1rem" }}> | |
| <h3 | |
| style={{ | |
| fontSize: "1.1rem", | |
| fontWeight: 600, | |
| display: "flex", | |
| alignItems: "center", | |
| gap: "10px", | |
| }} | |
| > | |
| <Save size={20} color="var(--color-primary)" /> Database Configuration | |
| (MongoDB) | |
| </h3> | |
| <p | |
| style={{ | |
| color: "var(--color-text-muted)", | |
| fontSize: "0.9rem", | |
| marginTop: "5px", | |
| }} | |
| > | |
| Connect your own MongoDB cluster. | |
| </p> | |
| </div> | |
| {isConfigured && !showForm ? ( | |
| <div | |
| style={{ | |
| marginTop: "1rem", | |
| background: "rgba(16, 185, 129, 0.1)", | |
| padding: "1rem", | |
| borderRadius: "8px", | |
| border: "1px solid rgba(16, 185, 129, 0.2)", | |
| }} | |
| > | |
| <div | |
| style={{ | |
| display: "flex", | |
| alignItems: "center", | |
| gap: "10px", | |
| color: "#10B981", | |
| fontWeight: 600, | |
| marginBottom: "0.5rem", | |
| }} | |
| > | |
| <CheckCircle size={18} /> | |
| Connected | |
| </div> | |
| <button | |
| className="btn btn-secondary btn-sm" | |
| onClick={() => setShowForm(true)} | |
| style={{ | |
| borderColor: "var(--color-border)", | |
| color: "var(--color-text-main)", | |
| }} | |
| > | |
| Update URI | |
| </button> | |
| </div> | |
| ) : ( | |
| <div style={{ marginTop: "1rem" }}> | |
| <div className="form-group"> | |
| <label | |
| className="form-label" | |
| style={{ | |
| marginBottom: "8px", | |
| display: "block", | |
| fontSize: "0.9rem", | |
| }} | |
| > | |
| MongoDB Connection URI | |
| </label> | |
| <input | |
| type="password" | |
| className="input-field" | |
| placeholder="mongodb+srv://user:pass@cluster.mongodb.net/..." | |
| value={dbUri} | |
| onChange={(e) => setDbUri(e.target.value)} | |
| style={{ | |
| width: "100%", | |
| padding: "12px", | |
| background: "var(--color-bg-input)", | |
| border: "1px solid var(--color-border)", | |
| borderRadius: "8px", | |
| color: "#fff", | |
| fontFamily: "monospace", | |
| }} | |
| /> | |
| </div> | |
| <div | |
| style={{ | |
| display: "flex", | |
| justifyContent: "flex-end", | |
| gap: "12px", | |
| marginTop: "1rem", | |
| }} | |
| > | |
| {isConfigured && ( | |
| <button | |
| className="btn btn-ghost" | |
| onClick={() => setShowForm(false)} | |
| > | |
| Cancel | |
| </button> | |
| )} | |
| <button | |
| onClick={handleUpdate} | |
| className="btn btn-primary" | |
| disabled={loading} | |
| > | |
| {loading ? "Saving..." : "Connect Database"} | |
| </button> | |
| </div> | |
| </div> | |
| ); | |
| )} | |
| </div> | |
| ); | |
| } | |
| function DatabaseConfigForm({ project, projectId, token }) { | |
| const [dbUri, setDbUri] = useState(""); | |
| const [loading, setLoading] = useState(false); | |
| // Use optional chaining carefully - project might be null initially | |
| const isConfigured = project?.resources?.db?.isExternal || false; | |
| const [showForm, setShowForm] = useState(!isConfigured); | |
| useEffect(() => { | |
| const configured = project?.resources?.db?.isExternal || false; | |
| setShowForm(!configured); | |
| }, [project]); | |
| const handleUpdate = async () => { | |
| if (!dbUri) return toast.error("Database URI is required"); | |
| setLoading(true); | |
| try { | |
| await axios.patch( | |
| `${API_URL}/api/projects/${projectId}/byod-config`, | |
| { dbUri }, | |
| { headers: { Authorization: `Bearer ${token}` } } | |
| ); | |
| toast.success("Database configuration updated!"); | |
| setShowForm(false); | |
| setDbUri(""); | |
| // Typically we'd reload project data here, but for now we rely on user refresh or optimistic ui if needed | |
| // Ideally notify parent to refresh project, but basic flow: | |
| } catch (err) { | |
| toast.error(err.response?.data?.error || "Failed to update DB config"); | |
| console.error("Failed to update DB config:", err); | |
| } finally { | |
| setLoading(false); | |
| } | |
| }; | |
| return ( | |
| <div className="card" style={{ position: "relative", overflow: "hidden" }}> | |
| <div | |
| style={{ | |
| position: "absolute", | |
| top: 0, | |
| left: 0, | |
| width: "4px", | |
| height: "100%", | |
| background: "var(--color-primary)", | |
| }} | |
| ></div> | |
| <div style={{ marginBottom: "1rem" }}> | |
| <h3 | |
| style={{ | |
| fontSize: "1.1rem", | |
| fontWeight: 600, | |
| display: "flex", | |
| alignItems: "center", | |
| gap: "10px", | |
| }} | |
| > | |
| <Save size={20} color="var(--color-primary)" /> Database Configuration | |
| (MongoDB) | |
| </h3> | |
| <p | |
| style={{ | |
| color: "var(--color-text-muted)", | |
| fontSize: "0.9rem", | |
| marginTop: "5px", | |
| }} | |
| > | |
| Connect your own MongoDB cluster. | |
| </p> | |
| </div> | |
| {isConfigured && !showForm ? ( | |
| <div | |
| style={{ | |
| marginTop: "1rem", | |
| background: "rgba(16, 185, 129, 0.1)", | |
| padding: "1rem", | |
| borderRadius: "8px", | |
| border: "1px solid rgba(16, 185, 129, 0.2)", | |
| }} | |
| > | |
| <div | |
| style={{ | |
| display: "flex", | |
| alignItems: "center", | |
| gap: "10px", | |
| color: "#10B981", | |
| fontWeight: 600, | |
| marginBottom: "0.5rem", | |
| }} | |
| > | |
| <CheckCircle size={18} /> | |
| Connected | |
| </div> | |
| <button | |
| className="btn btn-secondary btn-sm" | |
| onClick={() => setShowForm(true)} | |
| style={{ | |
| borderColor: "var(--color-border)", | |
| color: "var(--color-text-main)", | |
| }} | |
| > | |
| Update URI | |
| </button> | |
| </div> | |
| ) : ( | |
| <div style={{ marginTop: "1rem" }}> | |
| <div className="form-group"> | |
| <label | |
| className="form-label" | |
| style={{ | |
| marginBottom: "8px", | |
| display: "block", | |
| fontSize: "0.9rem", | |
| }} | |
| > | |
| MongoDB Connection URI | |
| </label> | |
| <input | |
| type="password" | |
| className="input-field" | |
| placeholder="mongodb+srv://user:pass@cluster.mongodb.net/..." | |
| value={dbUri} | |
| onChange={(e) => setDbUri(e.target.value)} | |
| style={{ | |
| width: "100%", | |
| padding: "12px", | |
| background: "var(--color-bg-input)", | |
| border: "1px solid var(--color-border)", | |
| borderRadius: "8px", | |
| color: "#fff", | |
| fontFamily: "monospace", | |
| }} | |
| /> | |
| </div> | |
| <div | |
| style={{ | |
| display: "flex", | |
| justifyContent: "flex-end", | |
| gap: "12px", | |
| marginTop: "1rem", | |
| }} | |
| > | |
| {isConfigured && ( | |
| <button | |
| className="btn btn-ghost" | |
| onClick={() => setShowForm(false)} | |
| > | |
| Cancel | |
| </button> | |
| )} | |
| <button | |
| onClick={handleUpdate} | |
| className="btn btn-primary" | |
| disabled={loading} | |
| > | |
| {loading ? "Saving..." : "Connect Database"} | |
| </button> | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| ); | |
| } |
🤖 Prompt for AI Agents
In @frontend/src/pages/ProjectSettings.jsx around lines 304 - 467, The catch in
DatabaseConfigForm.handleUpdate currently swallows errors; update it to log the
caught error for debugging (e.g., console.error(err) and/or send to your error
tracking service) in addition to showing the toast (keep the existing
toast.error(err.response?.data?.error || "Failed to update DB config")). Ensure
you reference the err variable inside the catch before finally and do not remove
existing UI behavior.
| function StorageConfigForm({ project, projectId, token }) { | ||
| const [config, setConfig] = useState({ | ||
| storageUrl: '', | ||
| storageKey: '', | ||
| storageProvider: 'supabase' | ||
| }); | ||
| const [loading, setLoading] = useState(false); | ||
| const isConfigured = project?.resources?.storage?.isExternal || false; | ||
| const [showForm, setShowForm] = useState(!isConfigured); | ||
|
|
||
| useEffect(() => { | ||
| const configured = project?.resources?.storage?.isExternal || false; | ||
| setShowForm(!configured); | ||
| }, [project]); | ||
|
|
||
| const handleChange = (e) => setConfig({ ...config, [e.target.name]: e.target.value }); | ||
|
|
||
| const handleUpdate = async () => { | ||
| if (!config.storageUrl || !config.storageKey) return toast.error("URL and Key are required"); | ||
|
|
||
| setLoading(true); | ||
| try { | ||
| await axios.patch(`${API_URL}/api/projects/${projectId}/byod-config`, | ||
| config, | ||
| { headers: { Authorization: `Bearer ${token}` } } | ||
| ); | ||
| toast.success("Storage configuration updated!"); | ||
| setShowForm(false); | ||
| setConfig({ storageUrl: '', storageKey: '', storageProvider: 'supabase' }); | ||
| } catch (err) { | ||
| toast.error(err.response?.data?.error || "Failed to update Storage config"); | ||
| } finally { | ||
| setLoading(false); | ||
| } | ||
| }; | ||
| const [config, setConfig] = useState({ | ||
| storageUrl: "", | ||
| storageKey: "", | ||
| storageProvider: "supabase", | ||
| }); | ||
| const [loading, setLoading] = useState(false); | ||
| const isConfigured = project?.resources?.storage?.isExternal || false; | ||
| const [showForm, setShowForm] = useState(!isConfigured); | ||
|
|
||
| return ( | ||
| <div className="card" style={{ position: 'relative', overflow: 'hidden' }}> | ||
| <div style={{ position: 'absolute', top: 0, left: 0, width: '4px', height: '100%', background: '#34d399' }}></div> | ||
|
|
||
| <div style={{ marginBottom: '1rem' }}> | ||
| <h3 style={{ fontSize: '1.1rem', fontWeight: 600, display: 'flex', alignItems: 'center', gap: '10px' }}> | ||
| <Save size={20} color="#34d399" /> Storage Configuration | ||
| </h3> | ||
| <p style={{ color: 'var(--color-text-muted)', fontSize: '0.9rem', marginTop: '5px' }}> | ||
| Connect your own S3-compatible storage. | ||
| </p> | ||
| </div> | ||
| useEffect(() => { | ||
| const configured = project?.resources?.storage?.isExternal || false; | ||
| setShowForm(!configured); | ||
| }, [project]); | ||
|
|
||
| const handleChange = (e) => | ||
| setConfig({ ...config, [e.target.name]: e.target.value }); | ||
|
|
||
| {isConfigured && !showForm ? ( | ||
| <div style={{ marginTop: '1rem', background: 'rgba(16, 185, 129, 0.1)', padding: '1rem', borderRadius: '8px', border: '1px solid rgba(16, 185, 129, 0.2)' }}> | ||
| <div style={{ display: 'flex', alignItems: 'center', gap: '10px', color: '#10B981', fontWeight: 600, marginBottom: '0.5rem' }}> | ||
| <CheckCircle size={18} /> | ||
| Connected | ||
| </div> | ||
| <button className="btn btn-secondary btn-sm" onClick={() => setShowForm(true)} style={{ borderColor: 'var(--color-border)', color: 'var(--color-text-main)' }}> | ||
| Update Config | ||
| </button> | ||
| </div> | ||
| ) : ( | ||
| <div style={{ marginTop: '1rem', display: 'grid', gap: '1.5rem' }}> | ||
| <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1.5rem' }}> | ||
| <div className="form-group"> | ||
| <label className="form-label" style={{ marginBottom: '8px', display: 'block', fontSize: '0.9rem' }}>Storage URL</label> | ||
| <input | ||
| type="text" | ||
| name="storageUrl" | ||
| className="input-field" | ||
| value={config.storageUrl} | ||
| onChange={handleChange} | ||
| placeholder="https://..." | ||
| style={{ width: '100%', padding: '12px', background: 'var(--color-bg-input)', border: '1px solid var(--color-border)', borderRadius: '8px', color: '#fff' }} | ||
| /> | ||
| </div> | ||
| <div className="form-group"> | ||
| <label className="form-label" style={{ marginBottom: '8px', display: 'block', fontSize: '0.9rem' }}>Provider</label> | ||
| <select | ||
| name="storageProvider" | ||
| className="input-field" | ||
| value={config.storageProvider} | ||
| onChange={handleChange} | ||
| style={{ width: '100%', padding: '12px', background: 'var(--color-bg-input)', border: '1px solid var(--color-border)', borderRadius: '8px', color: '#fff' }} | ||
| > | ||
| <option value="supabase">Supabase</option> | ||
| <option value="aws" disabled>AWS S3 (Coming Soon)</option> | ||
| </select> | ||
| </div> | ||
| </div> | ||
| <div className="form-group"> | ||
| <label className="form-label" style={{ marginBottom: '8px', display: 'block', fontSize: '0.9rem' }}>Storage Key / Service Role</label> | ||
| <input | ||
| type="password" | ||
| name="storageKey" | ||
| className="input-field" | ||
| value={config.storageKey} | ||
| onChange={handleChange} | ||
| placeholder="Key..." | ||
| style={{ width: '100%', padding: '12px', background: 'var(--color-bg-input)', border: '1px solid var(--color-border)', borderRadius: '8px', color: '#fff', fontFamily: 'monospace' }} | ||
| /> | ||
| </div> | ||
| <div style={{ display: 'flex', justifyContent: 'flex-end', gap: '12px', marginTop: '1rem' }}> | ||
| {isConfigured && <button className="btn btn-ghost" onClick={() => setShowForm(false)}>Cancel</button>} | ||
| <button onClick={handleUpdate} className="btn btn-primary" disabled={loading}> | ||
| {loading ? 'Saving...' : 'Connect Storage'} | ||
| </button> | ||
| </div> | ||
| </div> | ||
| const handleUpdate = async () => { | ||
| if (!config.storageUrl || !config.storageKey) | ||
| return toast.error("URL and Key are required"); | ||
|
|
||
| setLoading(true); | ||
| try { | ||
| await axios.patch( | ||
| `${API_URL}/api/projects/${projectId}/byod-config`, | ||
| config, | ||
| { headers: { Authorization: `Bearer ${token}` } } | ||
| ); | ||
| toast.success("Storage configuration updated!"); | ||
| setShowForm(false); | ||
| setConfig({ | ||
| storageUrl: "", | ||
| storageKey: "", | ||
| storageProvider: "supabase", | ||
| }); | ||
| } catch (err) { | ||
| toast.error( | ||
| err.response?.data?.error || "Failed to update Storage config" | ||
| ); | ||
| } finally { | ||
| setLoading(false); | ||
| } | ||
| }; | ||
|
|
||
| return ( | ||
| <div className="card" style={{ position: "relative", overflow: "hidden" }}> | ||
| <div | ||
| style={{ | ||
| position: "absolute", | ||
| top: 0, | ||
| left: 0, | ||
| width: "4px", | ||
| height: "100%", | ||
| background: "#34d399", | ||
| }} | ||
| ></div> | ||
|
|
||
| <div style={{ marginBottom: "1rem" }}> | ||
| <h3 | ||
| style={{ | ||
| fontSize: "1.1rem", | ||
| fontWeight: 600, | ||
| display: "flex", | ||
| alignItems: "center", | ||
| gap: "10px", | ||
| }} | ||
| > | ||
| <Save size={20} color="#34d399" /> Storage Configuration | ||
| </h3> | ||
| <p | ||
| style={{ | ||
| color: "var(--color-text-muted)", | ||
| fontSize: "0.9rem", | ||
| marginTop: "5px", | ||
| }} | ||
| > | ||
| Connect your own S3-compatible storage. | ||
| </p> | ||
| </div> | ||
|
|
||
| {isConfigured && !showForm ? ( | ||
| <div | ||
| style={{ | ||
| marginTop: "1rem", | ||
| background: "rgba(16, 185, 129, 0.1)", | ||
| padding: "1rem", | ||
| borderRadius: "8px", | ||
| border: "1px solid rgba(16, 185, 129, 0.2)", | ||
| }} | ||
| > | ||
| <div | ||
| style={{ | ||
| display: "flex", | ||
| alignItems: "center", | ||
| gap: "10px", | ||
| color: "#10B981", | ||
| fontWeight: 600, | ||
| marginBottom: "0.5rem", | ||
| }} | ||
| > | ||
| <CheckCircle size={18} /> | ||
| Connected | ||
| </div> | ||
| <button | ||
| className="btn btn-secondary btn-sm" | ||
| onClick={() => setShowForm(true)} | ||
| style={{ | ||
| borderColor: "var(--color-border)", | ||
| color: "var(--color-text-main)", | ||
| }} | ||
| > | ||
| Update Config | ||
| </button> | ||
| </div> | ||
| ) : ( | ||
| <div style={{ marginTop: "1rem", display: "grid", gap: "1.5rem" }}> | ||
| <div | ||
| style={{ | ||
| display: "grid", | ||
| gridTemplateColumns: "1fr 1fr", | ||
| gap: "1.5rem", | ||
| }} | ||
| > | ||
| <div className="form-group"> | ||
| <label | ||
| className="form-label" | ||
| style={{ | ||
| marginBottom: "8px", | ||
| display: "block", | ||
| fontSize: "0.9rem", | ||
| }} | ||
| > | ||
| Storage URL | ||
| </label> | ||
| <input | ||
| type="text" | ||
| name="storageUrl" | ||
| className="input-field" | ||
| value={config.storageUrl} | ||
| onChange={handleChange} | ||
| placeholder="https://..." | ||
| style={{ | ||
| width: "100%", | ||
| padding: "12px", | ||
| background: "var(--color-bg-input)", | ||
| border: "1px solid var(--color-border)", | ||
| borderRadius: "8px", | ||
| color: "#fff", | ||
| }} | ||
| /> | ||
| </div> | ||
| <div className="form-group"> | ||
| <label | ||
| className="form-label" | ||
| style={{ | ||
| marginBottom: "8px", | ||
| display: "block", | ||
| fontSize: "0.9rem", | ||
| }} | ||
| > | ||
| Provider | ||
| </label> | ||
| <select | ||
| name="storageProvider" | ||
| className="input-field" | ||
| value={config.storageProvider} | ||
| onChange={handleChange} | ||
| style={{ | ||
| width: "100%", | ||
| padding: "12px", | ||
| background: "var(--color-bg-input)", | ||
| border: "1px solid var(--color-border)", | ||
| borderRadius: "8px", | ||
| color: "#fff", | ||
| }} | ||
| > | ||
| <option value="supabase">Supabase</option> | ||
| <option value="aws" disabled> | ||
| AWS S3 (Coming Soon) | ||
| </option> | ||
| </select> | ||
| </div> | ||
| </div> | ||
| <div className="form-group"> | ||
| <label | ||
| className="form-label" | ||
| style={{ | ||
| marginBottom: "8px", | ||
| display: "block", | ||
| fontSize: "0.9rem", | ||
| }} | ||
| > | ||
| Storage Key / Service Role | ||
| </label> | ||
| <input | ||
| type="password" | ||
| name="storageKey" | ||
| className="input-field" | ||
| value={config.storageKey} | ||
| onChange={handleChange} | ||
| placeholder="Key..." | ||
| style={{ | ||
| width: "100%", | ||
| padding: "12px", | ||
| background: "var(--color-bg-input)", | ||
| border: "1px solid var(--color-border)", | ||
| borderRadius: "8px", | ||
| color: "#fff", | ||
| fontFamily: "monospace", | ||
| }} | ||
| /> | ||
| </div> | ||
| <div | ||
| style={{ | ||
| display: "flex", | ||
| justifyContent: "flex-end", | ||
| gap: "12px", | ||
| marginTop: "1rem", | ||
| }} | ||
| > | ||
| {isConfigured && ( | ||
| <button | ||
| className="btn btn-ghost" | ||
| onClick={() => setShowForm(false)} | ||
| > | ||
| Cancel | ||
| </button> | ||
| )} | ||
| <button | ||
| onClick={handleUpdate} | ||
| className="btn btn-primary" | ||
| disabled={loading} | ||
| > | ||
| {loading ? "Saving..." : "Connect Storage"} | ||
| </button> | ||
| </div> | ||
| </div> | ||
| ); | ||
| } No newline at end of file | ||
| )} | ||
| </div> | ||
| ); | ||
| } |
There was a problem hiding this comment.
Log errors in catch blocks for debugging.
The handleUpdate function has an empty catch block (line 505) that only shows a toast. Add error logging for production debugging.
🔍 Proposed fix
} catch (err) {
toast.error(
err.response?.data?.error || "Failed to update Storage config"
);
+ console.error("Failed to update Storage config:", err);
} finally {📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| function StorageConfigForm({ project, projectId, token }) { | |
| const [config, setConfig] = useState({ | |
| storageUrl: '', | |
| storageKey: '', | |
| storageProvider: 'supabase' | |
| }); | |
| const [loading, setLoading] = useState(false); | |
| const isConfigured = project?.resources?.storage?.isExternal || false; | |
| const [showForm, setShowForm] = useState(!isConfigured); | |
| useEffect(() => { | |
| const configured = project?.resources?.storage?.isExternal || false; | |
| setShowForm(!configured); | |
| }, [project]); | |
| const handleChange = (e) => setConfig({ ...config, [e.target.name]: e.target.value }); | |
| const handleUpdate = async () => { | |
| if (!config.storageUrl || !config.storageKey) return toast.error("URL and Key are required"); | |
| setLoading(true); | |
| try { | |
| await axios.patch(`${API_URL}/api/projects/${projectId}/byod-config`, | |
| config, | |
| { headers: { Authorization: `Bearer ${token}` } } | |
| ); | |
| toast.success("Storage configuration updated!"); | |
| setShowForm(false); | |
| setConfig({ storageUrl: '', storageKey: '', storageProvider: 'supabase' }); | |
| } catch (err) { | |
| toast.error(err.response?.data?.error || "Failed to update Storage config"); | |
| } finally { | |
| setLoading(false); | |
| } | |
| }; | |
| const [config, setConfig] = useState({ | |
| storageUrl: "", | |
| storageKey: "", | |
| storageProvider: "supabase", | |
| }); | |
| const [loading, setLoading] = useState(false); | |
| const isConfigured = project?.resources?.storage?.isExternal || false; | |
| const [showForm, setShowForm] = useState(!isConfigured); | |
| return ( | |
| <div className="card" style={{ position: 'relative', overflow: 'hidden' }}> | |
| <div style={{ position: 'absolute', top: 0, left: 0, width: '4px', height: '100%', background: '#34d399' }}></div> | |
| <div style={{ marginBottom: '1rem' }}> | |
| <h3 style={{ fontSize: '1.1rem', fontWeight: 600, display: 'flex', alignItems: 'center', gap: '10px' }}> | |
| <Save size={20} color="#34d399" /> Storage Configuration | |
| </h3> | |
| <p style={{ color: 'var(--color-text-muted)', fontSize: '0.9rem', marginTop: '5px' }}> | |
| Connect your own S3-compatible storage. | |
| </p> | |
| </div> | |
| useEffect(() => { | |
| const configured = project?.resources?.storage?.isExternal || false; | |
| setShowForm(!configured); | |
| }, [project]); | |
| const handleChange = (e) => | |
| setConfig({ ...config, [e.target.name]: e.target.value }); | |
| {isConfigured && !showForm ? ( | |
| <div style={{ marginTop: '1rem', background: 'rgba(16, 185, 129, 0.1)', padding: '1rem', borderRadius: '8px', border: '1px solid rgba(16, 185, 129, 0.2)' }}> | |
| <div style={{ display: 'flex', alignItems: 'center', gap: '10px', color: '#10B981', fontWeight: 600, marginBottom: '0.5rem' }}> | |
| <CheckCircle size={18} /> | |
| Connected | |
| </div> | |
| <button className="btn btn-secondary btn-sm" onClick={() => setShowForm(true)} style={{ borderColor: 'var(--color-border)', color: 'var(--color-text-main)' }}> | |
| Update Config | |
| </button> | |
| </div> | |
| ) : ( | |
| <div style={{ marginTop: '1rem', display: 'grid', gap: '1.5rem' }}> | |
| <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1.5rem' }}> | |
| <div className="form-group"> | |
| <label className="form-label" style={{ marginBottom: '8px', display: 'block', fontSize: '0.9rem' }}>Storage URL</label> | |
| <input | |
| type="text" | |
| name="storageUrl" | |
| className="input-field" | |
| value={config.storageUrl} | |
| onChange={handleChange} | |
| placeholder="https://..." | |
| style={{ width: '100%', padding: '12px', background: 'var(--color-bg-input)', border: '1px solid var(--color-border)', borderRadius: '8px', color: '#fff' }} | |
| /> | |
| </div> | |
| <div className="form-group"> | |
| <label className="form-label" style={{ marginBottom: '8px', display: 'block', fontSize: '0.9rem' }}>Provider</label> | |
| <select | |
| name="storageProvider" | |
| className="input-field" | |
| value={config.storageProvider} | |
| onChange={handleChange} | |
| style={{ width: '100%', padding: '12px', background: 'var(--color-bg-input)', border: '1px solid var(--color-border)', borderRadius: '8px', color: '#fff' }} | |
| > | |
| <option value="supabase">Supabase</option> | |
| <option value="aws" disabled>AWS S3 (Coming Soon)</option> | |
| </select> | |
| </div> | |
| </div> | |
| <div className="form-group"> | |
| <label className="form-label" style={{ marginBottom: '8px', display: 'block', fontSize: '0.9rem' }}>Storage Key / Service Role</label> | |
| <input | |
| type="password" | |
| name="storageKey" | |
| className="input-field" | |
| value={config.storageKey} | |
| onChange={handleChange} | |
| placeholder="Key..." | |
| style={{ width: '100%', padding: '12px', background: 'var(--color-bg-input)', border: '1px solid var(--color-border)', borderRadius: '8px', color: '#fff', fontFamily: 'monospace' }} | |
| /> | |
| </div> | |
| <div style={{ display: 'flex', justifyContent: 'flex-end', gap: '12px', marginTop: '1rem' }}> | |
| {isConfigured && <button className="btn btn-ghost" onClick={() => setShowForm(false)}>Cancel</button>} | |
| <button onClick={handleUpdate} className="btn btn-primary" disabled={loading}> | |
| {loading ? 'Saving...' : 'Connect Storage'} | |
| </button> | |
| </div> | |
| </div> | |
| const handleUpdate = async () => { | |
| if (!config.storageUrl || !config.storageKey) | |
| return toast.error("URL and Key are required"); | |
| setLoading(true); | |
| try { | |
| await axios.patch( | |
| `${API_URL}/api/projects/${projectId}/byod-config`, | |
| config, | |
| { headers: { Authorization: `Bearer ${token}` } } | |
| ); | |
| toast.success("Storage configuration updated!"); | |
| setShowForm(false); | |
| setConfig({ | |
| storageUrl: "", | |
| storageKey: "", | |
| storageProvider: "supabase", | |
| }); | |
| } catch (err) { | |
| toast.error( | |
| err.response?.data?.error || "Failed to update Storage config" | |
| ); | |
| } finally { | |
| setLoading(false); | |
| } | |
| }; | |
| return ( | |
| <div className="card" style={{ position: "relative", overflow: "hidden" }}> | |
| <div | |
| style={{ | |
| position: "absolute", | |
| top: 0, | |
| left: 0, | |
| width: "4px", | |
| height: "100%", | |
| background: "#34d399", | |
| }} | |
| ></div> | |
| <div style={{ marginBottom: "1rem" }}> | |
| <h3 | |
| style={{ | |
| fontSize: "1.1rem", | |
| fontWeight: 600, | |
| display: "flex", | |
| alignItems: "center", | |
| gap: "10px", | |
| }} | |
| > | |
| <Save size={20} color="#34d399" /> Storage Configuration | |
| </h3> | |
| <p | |
| style={{ | |
| color: "var(--color-text-muted)", | |
| fontSize: "0.9rem", | |
| marginTop: "5px", | |
| }} | |
| > | |
| Connect your own S3-compatible storage. | |
| </p> | |
| </div> | |
| {isConfigured && !showForm ? ( | |
| <div | |
| style={{ | |
| marginTop: "1rem", | |
| background: "rgba(16, 185, 129, 0.1)", | |
| padding: "1rem", | |
| borderRadius: "8px", | |
| border: "1px solid rgba(16, 185, 129, 0.2)", | |
| }} | |
| > | |
| <div | |
| style={{ | |
| display: "flex", | |
| alignItems: "center", | |
| gap: "10px", | |
| color: "#10B981", | |
| fontWeight: 600, | |
| marginBottom: "0.5rem", | |
| }} | |
| > | |
| <CheckCircle size={18} /> | |
| Connected | |
| </div> | |
| <button | |
| className="btn btn-secondary btn-sm" | |
| onClick={() => setShowForm(true)} | |
| style={{ | |
| borderColor: "var(--color-border)", | |
| color: "var(--color-text-main)", | |
| }} | |
| > | |
| Update Config | |
| </button> | |
| </div> | |
| ) : ( | |
| <div style={{ marginTop: "1rem", display: "grid", gap: "1.5rem" }}> | |
| <div | |
| style={{ | |
| display: "grid", | |
| gridTemplateColumns: "1fr 1fr", | |
| gap: "1.5rem", | |
| }} | |
| > | |
| <div className="form-group"> | |
| <label | |
| className="form-label" | |
| style={{ | |
| marginBottom: "8px", | |
| display: "block", | |
| fontSize: "0.9rem", | |
| }} | |
| > | |
| Storage URL | |
| </label> | |
| <input | |
| type="text" | |
| name="storageUrl" | |
| className="input-field" | |
| value={config.storageUrl} | |
| onChange={handleChange} | |
| placeholder="https://..." | |
| style={{ | |
| width: "100%", | |
| padding: "12px", | |
| background: "var(--color-bg-input)", | |
| border: "1px solid var(--color-border)", | |
| borderRadius: "8px", | |
| color: "#fff", | |
| }} | |
| /> | |
| </div> | |
| <div className="form-group"> | |
| <label | |
| className="form-label" | |
| style={{ | |
| marginBottom: "8px", | |
| display: "block", | |
| fontSize: "0.9rem", | |
| }} | |
| > | |
| Provider | |
| </label> | |
| <select | |
| name="storageProvider" | |
| className="input-field" | |
| value={config.storageProvider} | |
| onChange={handleChange} | |
| style={{ | |
| width: "100%", | |
| padding: "12px", | |
| background: "var(--color-bg-input)", | |
| border: "1px solid var(--color-border)", | |
| borderRadius: "8px", | |
| color: "#fff", | |
| }} | |
| > | |
| <option value="supabase">Supabase</option> | |
| <option value="aws" disabled> | |
| AWS S3 (Coming Soon) | |
| </option> | |
| </select> | |
| </div> | |
| </div> | |
| <div className="form-group"> | |
| <label | |
| className="form-label" | |
| style={{ | |
| marginBottom: "8px", | |
| display: "block", | |
| fontSize: "0.9rem", | |
| }} | |
| > | |
| Storage Key / Service Role | |
| </label> | |
| <input | |
| type="password" | |
| name="storageKey" | |
| className="input-field" | |
| value={config.storageKey} | |
| onChange={handleChange} | |
| placeholder="Key..." | |
| style={{ | |
| width: "100%", | |
| padding: "12px", | |
| background: "var(--color-bg-input)", | |
| border: "1px solid var(--color-border)", | |
| borderRadius: "8px", | |
| color: "#fff", | |
| fontFamily: "monospace", | |
| }} | |
| /> | |
| </div> | |
| <div | |
| style={{ | |
| display: "flex", | |
| justifyContent: "flex-end", | |
| gap: "12px", | |
| marginTop: "1rem", | |
| }} | |
| > | |
| {isConfigured && ( | |
| <button | |
| className="btn btn-ghost" | |
| onClick={() => setShowForm(false)} | |
| > | |
| Cancel | |
| </button> | |
| )} | |
| <button | |
| onClick={handleUpdate} | |
| className="btn btn-primary" | |
| disabled={loading} | |
| > | |
| {loading ? "Saving..." : "Connect Storage"} | |
| </button> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| \ No newline at end of file | |
| )} | |
| </div> | |
| ); | |
| } | |
| function StorageConfigForm({ project, projectId, token }) { | |
| const [config, setConfig] = useState({ | |
| storageUrl: "", | |
| storageKey: "", | |
| storageProvider: "supabase", | |
| }); | |
| const [loading, setLoading] = useState(false); | |
| const isConfigured = project?.resources?.storage?.isExternal || false; | |
| const [showForm, setShowForm] = useState(!isConfigured); | |
| useEffect(() => { | |
| const configured = project?.resources?.storage?.isExternal || false; | |
| setShowForm(!configured); | |
| }, [project]); | |
| const handleChange = (e) => | |
| setConfig({ ...config, [e.target.name]: e.target.value }); | |
| const handleUpdate = async () => { | |
| if (!config.storageUrl || !config.storageKey) | |
| return toast.error("URL and Key are required"); | |
| setLoading(true); | |
| try { | |
| await axios.patch( | |
| `${API_URL}/api/projects/${projectId}/byod-config`, | |
| config, | |
| { headers: { Authorization: `Bearer ${token}` } } | |
| ); | |
| toast.success("Storage configuration updated!"); | |
| setShowForm(false); | |
| setConfig({ | |
| storageUrl: "", | |
| storageKey: "", | |
| storageProvider: "supabase", | |
| }); | |
| } catch (err) { | |
| toast.error( | |
| err.response?.data?.error || "Failed to update Storage config" | |
| ); | |
| console.error("Failed to update Storage config:", err); | |
| } finally { | |
| setLoading(false); | |
| } | |
| }; | |
| return ( | |
| <div className="card" style={{ position: "relative", overflow: "hidden" }}> | |
| <div | |
| style={{ | |
| position: "absolute", | |
| top: 0, | |
| left: 0, | |
| width: "4px", | |
| height: "100%", | |
| background: "#34d399", | |
| }} | |
| ></div> | |
| <div style={{ marginBottom: "1rem" }}> | |
| <h3 | |
| style={{ | |
| fontSize: "1.1rem", | |
| fontWeight: 600, | |
| display: "flex", | |
| alignItems: "center", | |
| gap: "10px", | |
| }} | |
| > | |
| <Save size={20} color="#34d399" /> Storage Configuration | |
| </h3> | |
| <p | |
| style={{ | |
| color: "var(--color-text-muted)", | |
| fontSize: "0.9rem", | |
| marginTop: "5px", | |
| }} | |
| > | |
| Connect your own S3-compatible storage. | |
| </p> | |
| </div> | |
| {isConfigured && !showForm ? ( | |
| <div | |
| style={{ | |
| marginTop: "1rem", | |
| background: "rgba(16, 185, 129, 0.1)", | |
| padding: "1rem", | |
| borderRadius: "8px", | |
| border: "1px solid rgba(16, 185, 129, 0.2)", | |
| }} | |
| > | |
| <div | |
| style={{ | |
| display: "flex", | |
| alignItems: "center", | |
| gap: "10px", | |
| color: "#10B981", | |
| fontWeight: 600, | |
| marginBottom: "0.5rem", | |
| }} | |
| > | |
| <CheckCircle size={18} /> | |
| Connected | |
| </div> | |
| <button | |
| className="btn btn-secondary btn-sm" | |
| onClick={() => setShowForm(true)} | |
| style={{ | |
| borderColor: "var(--color-border)", | |
| color: "var(--color-text-main)", | |
| }} | |
| > | |
| Update Config | |
| </button> | |
| </div> | |
| ) : ( | |
| <div style={{ marginTop: "1rem", display: "grid", gap: "1.5rem" }}> | |
| <div | |
| style={{ | |
| display: "grid", | |
| gridTemplateColumns: "1fr 1fr", | |
| gap: "1.5rem", | |
| }} | |
| > | |
| <div className="form-group"> | |
| <label | |
| className="form-label" | |
| style={{ | |
| marginBottom: "8px", | |
| display: "block", | |
| fontSize: "0.9rem", | |
| }} | |
| > | |
| Storage URL | |
| </label> | |
| <input | |
| type="text" | |
| name="storageUrl" | |
| className="input-field" | |
| value={config.storageUrl} | |
| onChange={handleChange} | |
| placeholder="https://..." | |
| style={{ | |
| width: "100%", | |
| padding: "12px", | |
| background: "var(--color-bg-input)", | |
| border: "1px solid var(--color-border)", | |
| borderRadius: "8px", | |
| color: "#fff", | |
| }} | |
| /> | |
| </div> | |
| <div className="form-group"> | |
| <label | |
| className="form-label" | |
| style={{ | |
| marginBottom: "8px", | |
| display: "block", | |
| fontSize: "0.9rem", | |
| }} | |
| > | |
| Provider | |
| </label> | |
| <select | |
| name="storageProvider" | |
| className="input-field" | |
| value={config.storageProvider} | |
| onChange={handleChange} | |
| style={{ | |
| width: "100%", | |
| padding: "12px", | |
| background: "var(--color-bg-input)", | |
| border: "1px solid var(--color-border)", | |
| borderRadius: "8px", | |
| color: "#fff", | |
| }} | |
| > | |
| <option value="supabase">Supabase</option> | |
| <option value="aws" disabled> | |
| AWS S3 (Coming Soon) | |
| </option> | |
| </select> | |
| </div> | |
| </div> | |
| <div className="form-group"> | |
| <label | |
| className="form-label" | |
| style={{ | |
| marginBottom: "8px", | |
| display: "block", | |
| fontSize: "0.9rem", | |
| }} | |
| > | |
| Storage Key / Service Role | |
| </label> | |
| <input | |
| type="password" | |
| name="storageKey" | |
| className="input-field" | |
| value={config.storageKey} | |
| onChange={handleChange} | |
| placeholder="Key..." | |
| style={{ | |
| width: "100%", | |
| padding: "12px", | |
| background: "var(--color-bg-input)", | |
| border: "1px solid var(--color-border)", | |
| borderRadius: "8px", | |
| color: "#fff", | |
| fontFamily: "monospace", | |
| }} | |
| /> | |
| </div> | |
| <div | |
| style={{ | |
| display: "flex", | |
| justifyContent: "flex-end", | |
| gap: "12px", | |
| marginTop: "1rem", | |
| }} | |
| > | |
| {isConfigured && ( | |
| <button | |
| className="btn btn-ghost" | |
| onClick={() => setShowForm(false)} | |
| > | |
| Cancel | |
| </button> | |
| )} | |
| <button | |
| onClick={handleUpdate} | |
| className="btn btn-primary" | |
| disabled={loading} | |
| > | |
| {loading ? "Saving..." : "Connect Storage"} | |
| </button> | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| ); | |
| } |
🤖 Prompt for AI Agents
In @frontend/src/pages/ProjectSettings.jsx around lines 469 - 710, The catch
block in StorageConfigForm.handleUpdate only shows a toast and doesn't log the
error; update that catch to log the full error for debugging (e.g.,
console.error or a configured logger) including err and err.response?.data (and
err.message) so you capture API response details and stack traces while still
showing the existing toast; locate the catch inside the async handleUpdate
function and add the logging before the toast/error-handling lines.
…tejaavula076/urBackend into tejaswi/9-ui-confirmation-modal
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In @frontend/src/pages/Database.jsx:
- Around line 303-315: The ConfirmationModal component is incorrectly rendered
as a child of the table which causes invalid DOM nesting; move the JSX block
that renders ConfirmationModal (the conditional that uses showModal, onConfirm
calling handleDelete(selectedId) and setShowModal(false), and onCancel calling
setShowModal(false)) out of the <table> element and place it as a sibling (e.g.,
immediately after the closing </table>) so the modal is mounted at the top-level
of the component tree rather than inside the table.
🧹 Nitpick comments (2)
frontend/src/pages/Database.jsx (2)
42-45: Consider renamingfetchShowModalfor clarity.The
fetchprefix typically implies asynchronous data fetching. A name likeopenDeleteModalorconfirmDeletewould better convey the function's purpose.✏️ Suggested rename
- const fetchShowModal = (id) => { + const openDeleteModal = (id) => { setShowModal(true); setSelectedId(id); };And update the call site at line 295:
- onClick={() => fetchShowModal(row._id)} + onClick={() => openDeleteModal(row._id)}
188-250: Consider extracting inline components.
Sidebar,TableView,JsonView, andSkeletonLoaderare defined inside the component body and recreated on every render. For this component size, the impact is minimal, but extracting them to separate files or memoizing them could improve maintainability and performance if the component grows.
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (5)
frontend/index.htmlfrontend/src/index.cssfrontend/src/pages/ConfirmationModal.jsxfrontend/src/pages/Database.jsxfrontend/vite.config.js
🚧 Files skipped from review as they are similar to previous changes (2)
- frontend/src/pages/ConfirmationModal.jsx
- frontend/src/index.css
🧰 Additional context used
🧬 Code graph analysis (1)
frontend/src/pages/Database.jsx (9)
frontend/src/pages/ProjectDetails.jsx (4)
useParams(13-13)navigate(14-14)useAuth(15-15)project(17-17)frontend/src/components/Layout/Sidebar.jsx (2)
useParams(10-10)useAuth(11-11)frontend/src/pages/NotFound.jsx (1)
navigate(6-6)frontend/src/pages/CreateProject.jsx (2)
navigate(17-17)useAuth(16-16)frontend/src/pages/Dashboard.jsx (2)
navigate(13-13)useAuth(12-12)frontend/src/context/AuthContext.jsx (3)
useAuth(38-38)useAuth(38-38)token(6-6)frontend/src/components/Layout/Header.jsx (1)
useAuth(5-5)backend/controllers/project.controller.js (15)
project(67-67)project(84-88)project(143-147)project(170-170)project(196-196)project(230-230)project(272-272)project(310-311)project(352-353)project(399-400)project(444-445)project(485-489)project(501-508)project(561-561)projectId(499-499)frontend/src/config.js (2)
API_URL(2-2)API_URL(2-2)
🔇 Additional comments (5)
frontend/vite.config.js (1)
4-9: LGTM!The Tailwind CSS integration via
@tailwindcss/viteis correctly configured for Tailwind v4. The plugin is properly imported and added to the Vite plugins array.Minor nit: There's an extra blank line at lines 5-6 that could be cleaned up for consistency.
frontend/index.html (1)
1-21: LGTM!The HTML formatting improvements are appropriate. The commented-out Tailwind CDN script (line 15) makes sense since Tailwind is now properly integrated via the Vite plugin.
frontend/src/pages/Database.jsx (3)
99-115: Confirmation modal integration looks good.The
handleDeletefunction correctly removes thewindow.confirmcall per PR objectives. Error handling with toast notifications is in place.Consider adding a loading state to prevent double-clicks during the delete operation, but this is not critical for the current scope.
72-97: LGTM!The
fetchDatacallback is properly memoized withuseCallback, and the effect dependencies are correctly specified.
65-67: LGTM!The optional catch binding (ES2019+) is used correctly. The error toast provides adequate user feedback.
| {showModal && ( | ||
| <ConfirmationModal | ||
| open={showModal} | ||
| title="Delete Record" | ||
| message="Are you sure you want to delete this record? This action cannot be undone." | ||
| onConfirm={() => { | ||
| handleDelete(selectedId); | ||
| setShowModal(false); | ||
| }} | ||
| onCancel={() => setShowModal(false)} | ||
| /> | ||
| )} | ||
| </table> |
There was a problem hiding this comment.
Invalid DOM nesting: ConfirmationModal rendered inside <table> element.
The ConfirmationModal is placed inside the <table> element, which is invalid HTML. Only <thead>, <tbody>, <tfoot>, <tr>, <colgroup>, and <caption> are valid children of <table>. This can cause rendering issues and hydration mismatches in React.
Move the modal outside the table structure.
🛠️ Suggested fix
</tbody>
- {showModal && (
- <ConfirmationModal
- open={showModal}
- title="Delete Record"
- message="Are you sure you want to delete this record? This action cannot be undone."
- onConfirm={() => {
- handleDelete(selectedId);
- setShowModal(false);
- }}
- onCancel={() => setShowModal(false)}
- />
- )}
</table>
+ {showModal && (
+ <ConfirmationModal
+ open={showModal}
+ title="Delete Record"
+ message="Are you sure you want to delete this record? This action cannot be undone."
+ onConfirm={() => {
+ handleDelete(selectedId);
+ setShowModal(false);
+ }}
+ onCancel={() => setShowModal(false)}
+ />
+ )}
</div>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| {showModal && ( | |
| <ConfirmationModal | |
| open={showModal} | |
| title="Delete Record" | |
| message="Are you sure you want to delete this record? This action cannot be undone." | |
| onConfirm={() => { | |
| handleDelete(selectedId); | |
| setShowModal(false); | |
| }} | |
| onCancel={() => setShowModal(false)} | |
| /> | |
| )} | |
| </table> | |
| </table> | |
| {showModal && ( | |
| <ConfirmationModal | |
| open={showModal} | |
| title="Delete Record" | |
| message="Are you sure you want to delete this record? This action cannot be undone." | |
| onConfirm={() => { | |
| handleDelete(selectedId); | |
| setShowModal(false); | |
| }} | |
| onCancel={() => setShowModal(false)} | |
| /> | |
| )} |
🤖 Prompt for AI Agents
In @frontend/src/pages/Database.jsx around lines 303 - 315, The
ConfirmationModal component is incorrectly rendered as a child of the table
which causes invalid DOM nesting; move the JSX block that renders
ConfirmationModal (the conditional that uses showModal, onConfirm calling
handleDelete(selectedId) and setShowModal(false), and onCancel calling
setShowModal(false)) out of the <table> element and place it as a sibling (e.g.,
immediately after the closing </table>) so the modal is mounted at the top-level
of the component tree rather than inside the table.
🚀 Pull Request Description
Fixes #9
🛠️ Type of Change
🐛 Bug fix (non-breaking change which fixes an issue) - Yes
✨ New feature (non-breaking change which adds functionality) - Yes
ui: replace window.confirm with a custom Tailwind CSS modal #9
🧪 Testing & Validation
Backend Verification: (I have not yet touched these components)
npm testin thebackend/directory and all tests passed.Frontend Verification:
npm run lintin thefrontend/directory. - (Yes)📸 Screenshots / Recordings (Optional)
✅ Checklist
Built with ❤️ for urBackend.
Summary by CodeRabbit
New Features
Improvements
Chores
✏️ Tip: You can customize this high-level summary in your review settings.