Skip to content

feat(grids): Replace all toast-based feedback in grid management components#3534

Merged
Baalmart merged 13 commits into
airqo-platform:stagingfrom
BwanikaRobert:Update-Grid-Management-Module-to-use-InfoBanners
May 27, 2026
Merged

feat(grids): Replace all toast-based feedback in grid management components#3534
Baalmart merged 13 commits into
airqo-platform:stagingfrom
BwanikaRobert:Update-Grid-Management-Module-to-use-InfoBanners

Conversation

@BwanikaRobert

@BwanikaRobert BwanikaRobert commented May 25, 2026

Copy link
Copy Markdown
Contributor

Summary of Changes (What does this PR do?)

Migrates all toast-based notifications in the Grid Management module to the
centralized InfoBanner system (useBanner hook). Since most grid management
actions occur inside dialogs, banner scoping has been carefully applied:

  • scoped: true — error banners rendered inside the active dialog's
    built-in BannerSlot (dialog stays open on error).
  • scoped: false + setTimeout(100ms) — success banners shown after a
    dialog closes, giving the dialog time to unmount before the global banner
    renders.

Files changed:

  • src/vertex/core/hooks/useGrids.ts — Confirmed hooks already used the
    callback options pattern with no toast calls. Minor cleanup only.
  • src/vertex/components/features/grids/create-grid.tsx — Replaced toast
    with useBanner; success scoped: false, error scoped: true.
  • src/vertex/components/features/grids/edit-grid-details-dialog.tsx
    Replaced toast with useBanner; same scoping pattern.
  • src/vertex/components/features/grids/admin-levels-modal.tsx — Replaced
    toast with useBanner; all actions scoped: true (dialog stays open).
  • src/vertex/components/features/grids/create-admin-level.tsx — Added
    missing banner feedback (previously silent on error).
  • src/vertex/components/features/grids/grid-details-card.tsx — Copy action
    uses scoped: false; hardened with async/await and error fallback.
  • src/vertex/components/features/grids/grid-measurements-api-card.tsx
    Copy actions use scoped: false.

Known limitations:

  • The Create Grid flow was not fully tested locally because the polygon map
    (MiniMap) does not render in the current development environment.
  • The shapefile copy button inside Create Grid still uses a toast — it is
    triggered internally by ReusableInputField's showCopyButton prop, which
    is outside the scope of this PR.
  • When updating an admin level name, the server returns:
    unexpected field(s): name — only 'description' may be updated.
    This is a pre-existing API mismatch, not introduced by this PR.
    Screenshots attached.

Status of maturity (all need to be checked before merging):


How should this be manually tested?

  1. Navigate to the Grids page.
  2. Edit Grid Details (success) — open the edit dialog, change the grid
    name or visibility and save. Confirm a success banner appears at the top
    of the page after the dialog closes.
  3. Edit Grid Details (error) — submit with an empty name field. Confirm an
    error banner appears inside the dialog.
  4. Create Admin Level (error) — submit a name that already exists (409).
    Confirm an error banner appears inside the dialog.
  5. Admin Levels — copy ID — hover over a row and click the copy icon.
    Confirm a global success banner appears.
  6. Admin Levels — rename — edit a level name and save. (Note: currently
    returns a server-side parameter error — see Known Limitations above.)
  7. Grid Details card — copy Grid ID — click the copy icon. Confirm a
    global success banner appears.
  8. Grid Measurements API card — copy URL — click either copy button.
    Confirm a global success banner appears.

What are the relevant tickets?

Screenshots (optional)

gird7 grid1 grid2 grid3 grid4 grid6 grid7

Summary by CodeRabbit

  • Refactor
    • Migrated grid/admin-level notifications from toasts to the unified banner system for consistent success/error feedback.
    • Deferred success banners briefly after dialogs close to ensure proper UI cleanup.
  • Bug Fixes
    • Sanitized API error messages to avoid malformed output and improved scoped dialog error displays.
    • Clipboard copy actions now surface success/failure via banners.

Review Change Stack

@coderabbitai

coderabbitai Bot commented May 25, 2026

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Migrates grid module notifications from ReusableToast to the centralized useBanner system, refactors mutation hooks to accept optional onSuccess/onError callbacks, updates dialog components to show scoped/delayed banners, converts clipboard copy handlers to banner feedback, and sanitizes API error messages.

Changes

Grid Management Banner Migration

Layer / File(s) Summary
Hook refactoring: callback-based mutation options
src/vertex/core/hooks/useGrids.ts
useUpdateGridDetails, useCreateGrid, useCreateAdminLevel, and useUpdateAdminLevel now accept optional onSuccess/onError callbacks. Hooks invalidate relevant queries on success, then invoke caller callbacks instead of rendering toasts internally. New option types exported.
Grid creation & edit dialogs
src/vertex/components/features/grids/create-grid.tsx, src/vertex/components/features/grids/edit-grid-details-dialog.tsx
Dialogs use useBanner and new hook options: success handlers close/reset dialogs and schedule a delayed success banner (AFTER_DIALOG_CLOSE_MS); errors show scoped banners with sanitized API messages. Validation errors show scoped banners. Form submissions call mutations without inline callbacks.
Admin level modal & creators
src/vertex/components/features/grids/create-admin-level.tsx, src/vertex/components/features/grids/admin-levels-modal.tsx
Create/update admin-level flows now use hook-level callbacks to close dialogs and show delayed success / scoped error banners. Clipboard-copy and save flows were updated to rely on hook callbacks and show banners for copy success/failure.
Clipboard handlers in detail cards
src/vertex/components/features/grids/grid-details-card.tsx, src/vertex/components/features/grids/grid-measurements-api-card.tsx
Clipboard copy handlers converted to async try/catch with navigator.clipboard.writeText; success/failure reported via useBanner. Legacy execCommand fallback removed.
Error sanitization, timing constant, and documentation
src/vertex/core/constants/ui.ts, src/vertex/core/utils/getApiErrorMessage.ts, src/vertex/app/changelog.md
Added AFTER_DIALOG_CLOSE_MS = 100 for delayed global banners post-dialog close. getApiErrorMessage now replaces HTML-like payloads with a generic message. Changelog documents Version 1.23.49 migration.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

ready for review

Suggested reviewers

  • Baalmart
  • OchiengPaul442
  • Codebmk

Poem

Banners rise where toasts once played,
Dialogs hush, then flags cascade.
Copies sing with gentle cheer,
Errors washed of markup smear.
Small delays make finish neat — hooray! 🎉

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately summarizes the main change: replacing toast-based feedback with the banner system across grid management components.
Linked Issues check ✅ Passed The PR successfully implements all core requirements from #3494: replacing ReusableToast with useBanner, adding BannerSlot to dialogs, using scoped:true for modal errors and scoped:false for global actions, and refactoring hooks to accept optional callbacks.
Out of Scope Changes check ✅ Passed All changes are within scope of the toast-to-banner migration. The PR includes banner system integration, hook refactoring, error message utilities, and a changelog entry—all directly supporting the migration objective.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

Review ran into problems

🔥 Problems

Errors were encountered while retrieving linked issues.

Errors (1)
  • JIRA integration encountered authorization issues. Please disconnect and reconnect the integration in the CodeRabbit UI.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/vertex/components/features/grids/admin-levels-modal.tsx (1)

71-74: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Give feedback when save is blocked by an empty name.

The early return on empty input is silent, so users get no guidance inside the modal.

Proposed fix
   const saveEdit = (levelId: string) => {
     const name = editValue.trim();
     if (!name) {
+      showBanner({
+        message: "Admin level name cannot be empty.",
+        severity: "error",
+        scoped: true,
+      });
       return;
     }
     updateAdminLevel({ levelId, data: { name } });
   };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/vertex/components/features/grids/admin-levels-modal.tsx` around lines 71
- 74, The code silently returns when the trimmed name is empty (const name =
editValue.trim();) which provides no user feedback; modify the save handler
(e.g., handleSave/onSave) to validate and, instead of silently returning, set a
validation state (e.g., add const [nameError, setNameError] =
useState<string|undefined>(undefined)) and call setNameError('Name is required')
when name is empty, ensure the modal renders this message near the input and/or
disables the Save button while nameError is set, and clear nameError when
editValue changes or on successful save.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/vertex/components/features/grids/admin-levels-modal.tsx`:
- Around line 31-36: The onError handler in admin-levels-modal.tsx currently
injects the full getApiErrorMessage(error) into showBanner which can surface raw
backend HTML/payloads to users; change the onError to present a generic,
non-sensitive banner text (e.g. "Unable to update admin level. Please try again
or contact support.") and move the detailed backend/error payload into a
developer log (console.error or processLogger.error) or store it in a
non-user-facing monitoring call; update the showBanner call in the onError block
(and any callers of getApiErrorMessage within that block) to use the generic
message and optionally truncate/strip HTML from any user-visible text while
preserving full error details only in logs/telemetry.

In `@src/vertex/components/features/grids/edit-grid-details-dialog.tsx`:
- Around line 85-88: The call to updateGridDetails (the mutateAsync returned by
useUpdateGridDetails) in edit-grid-details-dialog.tsx is not awaited, so the
surrounding try/catch cannot catch async rejections; change the call to await
updateGridDetails(updates) inside the existing try block (or use
updateGridDetails(...).catch(...) if you prefer) so errors are caught locally
and avoid unhandled promise rejections, keeping the hook’s onError/banner
behavior intact.

In `@src/vertex/components/features/grids/grid-details-card.tsx`:
- Around line 24-32: The handleCopy function in grid-details-card.tsx only uses
navigator.clipboard.writeText and lacks the older-browser fallback used in
grid-measurements-api-card.tsx; update handleCopy to first try
navigator.clipboard.writeText (when available) and if not supported or it
throws, fall back to creating a temporary textarea, set its value to text,
append/select it and call document.execCommand("copy"), then remove the
textarea; preserve the existing showBanner success/failure calls and ensure both
success paths trigger the same "Copied to clipboard" banner and failures trigger
the error banner.

---

Outside diff comments:
In `@src/vertex/components/features/grids/admin-levels-modal.tsx`:
- Around line 71-74: The code silently returns when the trimmed name is empty
(const name = editValue.trim();) which provides no user feedback; modify the
save handler (e.g., handleSave/onSave) to validate and, instead of silently
returning, set a validation state (e.g., add const [nameError, setNameError] =
useState<string|undefined>(undefined)) and call setNameError('Name is required')
when name is empty, ensure the modal renders this message near the input and/or
disables the Save button while nameError is set, and clear nameError when
editValue changes or on successful save.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d40cb695-d3be-450d-aed3-e1f99a665f68

📥 Commits

Reviewing files that changed from the base of the PR and between 05247eb and e900e7a.

📒 Files selected for processing (7)
  • src/vertex/components/features/grids/admin-levels-modal.tsx
  • src/vertex/components/features/grids/create-admin-level.tsx
  • src/vertex/components/features/grids/create-grid.tsx
  • src/vertex/components/features/grids/edit-grid-details-dialog.tsx
  • src/vertex/components/features/grids/grid-details-card.tsx
  • src/vertex/components/features/grids/grid-measurements-api-card.tsx
  • src/vertex/core/hooks/useGrids.ts

Comment on lines +31 to +36
onError: (error) => {
showBanner({
message: `Failed to update admin level: ${getApiErrorMessage(error)}`,
severity: "error",
scoped: true,
});

@coderabbitai coderabbitai Bot May 25, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Do not render raw backend payloads in user-facing banners.

At Line 33, the banner can display raw HTML/error-page bodies from the server. This leaks internal payload details and creates a broken modal UX.

Proposed fix
     onError: (error) => {
+      const apiMessage = getApiErrorMessage(error);
+      const safeMessage = /<\/?[a-z][\s\S]*>/i.test(apiMessage)
+        ? "Server error. Please try again."
+        : apiMessage;
       showBanner({
-        message: `Failed to update admin level: ${getApiErrorMessage(error)}`,
+        message: `Failed to update admin level: ${safeMessage}`,
         severity: "error",
         scoped: true,
       });
     }
📝 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.

Suggested change
onError: (error) => {
showBanner({
message: `Failed to update admin level: ${getApiErrorMessage(error)}`,
severity: "error",
scoped: true,
});
onError: (error) => {
const apiMessage = getApiErrorMessage(error);
const safeMessage = /<\/?[a-z][\s\S]*>/i.test(apiMessage)
? "Server error. Please try again."
: apiMessage;
showBanner({
message: `Failed to update admin level: ${safeMessage}`,
severity: "error",
scoped: true,
});
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/vertex/components/features/grids/admin-levels-modal.tsx` around lines 31
- 36, The onError handler in admin-levels-modal.tsx currently injects the full
getApiErrorMessage(error) into showBanner which can surface raw backend
HTML/payloads to users; change the onError to present a generic, non-sensitive
banner text (e.g. "Unable to update admin level. Please try again or contact
support.") and move the detailed backend/error payload into a developer log
(console.error or processLogger.error) or store it in a non-user-facing
monitoring call; update the showBanner call in the onError block (and any
callers of getApiErrorMessage within that block) to use the generic message and
optionally truncate/strip HTML from any user-visible text while preserving full
error details only in logs/telemetry.

@Codebmk Codebmk May 26, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please apply coderabbit feedback @BwanikaRobert , thanks

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

Comment thread src/vertex/components/features/grids/edit-grid-details-dialog.tsx
Comment on lines +24 to 32
const handleCopy = async (text: string) => {
if (!text) return;
try {
await navigator.clipboard.writeText(text);
showBanner({ message: "Copied to clipboard", severity: "success", scoped: false });
} catch {
showBanner({ message: "Failed to copy to clipboard", severity: "error", scoped: false });
}
};

@coderabbitai coderabbitai Bot May 25, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Browser compatibility inconsistency: Missing fallback for older browsers.

The handleCopy implementation here uses only navigator.clipboard.writeText, while grid-measurements-api-card.tsx (lines 22-41) includes a fallback to document.execCommand("copy") for browsers that don't support the Clipboard API. This inconsistency means users on older browsers will see "Failed to copy to clipboard" for grid IDs but might succeed copying API URLs.

🔧 Proposed fix: Add browser compatibility fallback
 const handleCopy = async (text: string) => {
     if (!text) return;
     try {
-        await navigator.clipboard.writeText(text);
+        if (navigator?.clipboard?.writeText) {
+            await navigator.clipboard.writeText(text);
+        } else {
+            const el = document.createElement("textarea");
+            el.value = text;
+            el.style.position = "fixed";
+            el.style.opacity = "0";
+            document.body.appendChild(el);
+            el.select();
+            document.execCommand("copy");
+            document.body.removeChild(el);
+        }
         showBanner({ message: "Copied to clipboard", severity: "success", scoped: false });
     } catch {
         showBanner({ message: "Failed to copy to clipboard", severity: "error", scoped: false });
     }
 };
📝 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.

Suggested change
const handleCopy = async (text: string) => {
if (!text) return;
try {
await navigator.clipboard.writeText(text);
showBanner({ message: "Copied to clipboard", severity: "success", scoped: false });
} catch {
showBanner({ message: "Failed to copy to clipboard", severity: "error", scoped: false });
}
};
const handleCopy = async (text: string) => {
if (!text) return;
try {
if (navigator?.clipboard?.writeText) {
await navigator.clipboard.writeText(text);
} else {
const el = document.createElement("textarea");
el.value = text;
el.style.position = "fixed";
el.style.opacity = "0";
document.body.appendChild(el);
el.select();
document.execCommand("copy");
document.body.removeChild(el);
}
showBanner({ message: "Copied to clipboard", severity: "success", scoped: false });
} catch {
showBanner({ message: "Failed to copy to clipboard", severity: "error", scoped: false });
}
};
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/vertex/components/features/grids/grid-details-card.tsx` around lines 24 -
32, The handleCopy function in grid-details-card.tsx only uses
navigator.clipboard.writeText and lacks the older-browser fallback used in
grid-measurements-api-card.tsx; update handleCopy to first try
navigator.clipboard.writeText (when available) and if not supported or it
throws, fall back to creating a temporary textarea, set its value to text,
append/select it and call document.execCommand("copy"), then remove the
textarea; preserve the existing showBanner success/failure calls and ensure both
success paths trigger the same "Copied to clipboard" banner and failures trigger
the error banner.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Outside the scope of the PR

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@BwanikaRobert, fair enough — that's a valid call. Would you like me to open a follow-up issue to track the browser compatibility fallback for handleCopy in grid-details-card.tsx, so it doesn't get lost?

(´• ω •`) 🐇

@Codebmk Codebmk May 26, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice actionable feedback from coderabbit, please address this within current pr @BwanikaRobert

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

@BwanikaRobert

Copy link
Copy Markdown
Contributor Author

@Codebmk This is ready for review when you have a moment.

Comment on lines +24 to 32
const handleCopy = async (text: string) => {
if (!text) return;
try {
await navigator.clipboard.writeText(text);
showBanner({ message: "Copied to clipboard", severity: "success", scoped: false });
} catch {
showBanner({ message: "Failed to copy to clipboard", severity: "error", scoped: false });
}
};

@Codebmk Codebmk May 26, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice actionable feedback from coderabbit, please address this within current pr @BwanikaRobert

Comment on lines +31 to +36
onError: (error) => {
showBanner({
message: `Failed to update admin level: ${getApiErrorMessage(error)}`,
severity: "error",
scoped: true,
});

@Codebmk Codebmk May 26, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please apply coderabbit feedback @BwanikaRobert , thanks

@Codebmk

Codebmk commented May 26, 2026

Copy link
Copy Markdown
Member

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 26, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/vertex/components/features/grids/edit-grid-details-dialog.tsx (1)

92-106: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Add <BannerSlot /> to render scoped error banners inside the dialog.

The dialog triggers scoped banners for validation errors (line 68) and API errors (line 41), but the JSX does not include <BannerSlot />. Per the banner context implementation, scoped banners only render when BannerSlot is explicitly placed in the component tree. Without it, users will see no feedback when validation fails or the API returns an error, even though the dialog remains open.

🔧 Add BannerSlot at the top of dialog children
+import React, { useEffect, useState } from "react";
+import ReusableDialog from "`@/components/shared/dialog/ReusableDialog`";
+import ReusableInputField from "`@/components/shared/inputfield/ReusableInputField`";
+import ReusableSelectInput from "`@/components/shared/select/ReusableSelectInput`";
+import { useUpdateGridDetails } from "`@/core/hooks/useGrids`";
+import { usePermission } from "`@/core/hooks/usePermissions`";
+import { PERMISSIONS } from "`@/core/permissions/constants`";
+import { Grid } from "`@/app/types/grids`";
+import { useBanner } from "`@/context/banner-context`";
+import { getApiErrorMessage } from "`@/core/utils/getApiErrorMessage`";

Then update the dialog JSX:

     return (
         <ReusableDialog isOpen={open} onClose={onClose} title="Edit Grid Details" size="lg" 
             primaryAction={{ label: isLoading ? "Saving..." : "Save", onClick: handleSave, disabled: isLoading || !canUpdate || !form.name.trim() || !form.admin_level.trim() }}
             secondaryAction={{ label: "Cancel", onClick: onClose, disabled: isLoading, variant: "outline" }}
         >
+            <BannerSlot />
             <div className="space-y-4">
                 <ReusableInputField label="Grid Name" value={form.name} onChange={(e) => setForm((s) => ({ ...s, name: e.target.value }))} placeholder="Enter grid name" disabled={isLoading} required />
                 <ReusableInputField label="Admin Level" value={form.admin_level} onChange={(e) => setForm((s) => ({ ...s, admin_level: e.target.value }))} placeholder="e.g. Country, Region" disabled={isLoading} required />
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/vertex/components/features/grids/edit-grid-details-dialog.tsx` around
lines 92 - 106, The dialog renders scoped validation and API error banners but
never places a BannerSlot, so those banners won't appear; update the JSX inside
ReusableDialog in EditGridDetailsDialog to include a BannerSlot component
(imported from your banner/context module) as the first child of the dialog
content (i.e., place <BannerSlot /> above the existing <div
className="space-y-4">) so scoped banners triggered by the form validation and
API errors will render.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/vertex/components/features/grids/edit-grid-details-dialog.tsx`:
- Around line 92-106: The dialog renders scoped validation and API error banners
but never places a BannerSlot, so those banners won't appear; update the JSX
inside ReusableDialog in EditGridDetailsDialog to include a BannerSlot component
(imported from your banner/context module) as the first child of the dialog
content (i.e., place <BannerSlot /> above the existing <div
className="space-y-4">) so scoped banners triggered by the form validation and
API errors will render.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1a880e90-1085-4064-a7d8-79b8b6fc7664

📥 Commits

Reviewing files that changed from the base of the PR and between e900e7a and 7a72f9a.

📒 Files selected for processing (1)
  • src/vertex/components/features/grids/edit-grid-details-dialog.tsx

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Migrates Grid Management user feedback from toast notifications to the centralized useBanner/InfoBanner system, using scoped banners inside dialogs and global banners for page-level actions (e.g., copy operations).

Changes:

  • Refactored grid/admin-level mutations in useGrids to accept optional onSuccess/onError callbacks instead of firing toasts directly.
  • Updated grid/admin-level dialogs and cards to use showBanner with appropriate scoped behavior (scoped in dialogs, global for copy/success-after-close).
  • Added a changelog entry documenting the banner migration.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
src/vertex/core/hooks/useGrids.ts Removes toast side-effects from mutations and adds optional callbacks for components to drive banner feedback.
src/vertex/components/features/grids/create-grid.tsx Uses useBanner for create-grid success/error feedback with dialog-scoped vs global banners.
src/vertex/components/features/grids/edit-grid-details-dialog.tsx Uses useBanner with scoped error banners and deferred global success banner after dialog close.
src/vertex/components/features/grids/admin-levels-modal.tsx Uses scoped banners for admin-level update and copy ID feedback within the modal.
src/vertex/components/features/grids/create-admin-level.tsx Adds banner feedback for admin-level creation success/error (previously missing/silent).
src/vertex/components/features/grids/grid-details-card.tsx Uses global banners for clipboard copy success/failure and hardens copy with async/await.
src/vertex/components/features/grids/grid-measurements-api-card.tsx Uses global banners for API URL copy success/failure.
src/vertex/app/changelog.md Documents the banner migration in the Vertex changelog.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/vertex/app/changelog.md Outdated
</details>

<details>
<summary><strong>Files Updated (6)</strong></summary>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/platform/src/shared/lib/auth.ts (1)

103-132: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Persist the backend-issued OAuth token here.

This branch still stores oauthToken even though fetchEnhancedUserProfile() now returns profile.accessToken. If the backend rotates or canonicalizes the token during verification, the NextAuth session keeps the stale handoff token and downstream authenticated calls drift.

Proposed fix
           return {
             id: profile._id,
             email: profile.email,
             name: fullName || profile.email,
             firstName: profile.firstName,
             lastName: profile.lastName,
             image: profile.profilePicture,
             _id: profile._id,
-            accessToken: oauthToken,
+            accessToken: profile.accessToken ?? oauthToken,
             authMethods: normalizeAuthMethods(profile.authMethods),
           };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/platform/src/shared/lib/auth.ts` around lines 103 - 132, The authorize
function returns the stale client-provided oauthToken instead of the
backend-issued token; update the returned session object in authorize (the block
that calls fetchOAuthProfile) to set accessToken to profile.accessToken if
present, falling back to the original oauthToken otherwise (i.e., accessToken:
profile?.accessToken || oauthToken), ensuring the backend-rotated/canonical
token from fetchOAuthProfile is persisted in the session.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/platform/src/modules/billing/components/SubscriptionSection.tsx`:
- Around line 588-595: The effectiveFrom fallback logic in
SubscriptionSection.tsx currently infers upgrade vs downgrade by comparing
selectedPlan.price to currentPlan.price; replace that fallback to use the
dialog's planActionMode instead: set effectiveFrom = payload.data?.effectiveFrom
|| (planActionMode === 'upgrade' ? 'immediately' : 'next_billing_period'),
making sure to still preserve the existing getPaidTier usage for previousTier
and to handle missing currentPlan safely when reading selectedPlan and
planActionMode.

In `@src/platform/src/shared/components/auth/SetPasswordPromptDialog.tsx`:
- Around line 149-165: The current console.warn calls in SetPasswordPromptDialog
(around variables errorName, isMountedRef, update, fallbackAuthMethods) log
whole error objects which may contain sensitive tokens; change them to log a
sanitized error payload containing only safe fields (e.g., name, message,
status/code) instead of the raw error for both the initial refresh error and the
updateError in the catch block; implement this by extracting name/message/status
(or code) into a small object (or a helper sanitizeError) and pass that object
to console.warn along with a concise context string.

In `@src/vertex/core/utils/getApiErrorMessage.ts`:
- Around line 16-20: getMessageFromApiData currently only sanitizes when data is
a plain string, which lets raw HTML through for ApiErrorResponse shapes; update
getMessageFromApiData to inspect ApiErrorResponse.message and each entry of
ApiErrorResponse.errors (if present), run the same HTML-detection used for
strings on those values, and replace any HTML-containing value with the generic
fallback ("An unexpected error occurred. Please try again.") or strip/sanitize
appropriately so no raw HTML is returned to the UI; reference the
getMessageFromApiData function and the ApiErrorResponse.message and
ApiErrorResponse.errors fields when making this change.

---

Outside diff comments:
In `@src/platform/src/shared/lib/auth.ts`:
- Around line 103-132: The authorize function returns the stale client-provided
oauthToken instead of the backend-issued token; update the returned session
object in authorize (the block that calls fetchOAuthProfile) to set accessToken
to profile.accessToken if present, falling back to the original oauthToken
otherwise (i.e., accessToken: profile?.accessToken || oauthToken), ensuring the
backend-rotated/canonical token from fetchOAuthProfile is persisted in the
session.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 77a3e1b1-bb3b-4111-8e5a-9ab1fcd2e4c1

📥 Commits

Reviewing files that changed from the base of the PR and between 7a72f9a and 816393a.

📒 Files selected for processing (30)
  • k8s/platform/k8s-aks/airqo-platform/values-stage.yaml
  • src/platform/middleware.ts
  • src/platform/src/app/(dashboard)/request-organization/layout.tsx
  • src/platform/src/app/(dashboard)/system/feedback/page.tsx
  • src/platform/src/modules/api-client/ApiClientPage.tsx
  • src/platform/src/modules/billing/BillingPage.tsx
  • src/platform/src/modules/billing/components/CheckoutDialog.tsx
  • src/platform/src/modules/billing/components/SubscriptionSection.tsx
  • src/platform/src/modules/feedback/components/FeedbackLauncher.tsx
  • src/platform/src/next-auth.d.ts
  • src/platform/src/shared/components/auth/SetPasswordPromptDialog.tsx
  • src/platform/src/shared/components/auth/SocialAuthSection.tsx
  • src/platform/src/shared/components/header/index.tsx
  • src/platform/src/shared/components/header/types/index.ts
  • src/platform/src/shared/hooks/index.ts
  • src/platform/src/shared/hooks/useUser.ts
  • src/platform/src/shared/layouts/MainLayout.tsx
  • src/platform/src/shared/lib/auth.ts
  • src/platform/src/shared/lib/oauth-session.ts
  • src/platform/src/shared/lib/validators.ts
  • src/platform/src/shared/providers/auth-provider.tsx
  • src/platform/src/shared/services/apiClient.ts
  • src/platform/src/shared/services/sessionAuthToken.ts
  • src/platform/src/shared/services/subscriptionService.ts
  • src/platform/src/shared/services/userService.ts
  • src/platform/src/shared/types/api.ts
  • src/platform/src/shared/types/global.d.ts
  • src/vertex/app/changelog.md
  • src/vertex/components/features/grids/grid-measurements-api-card.tsx
  • src/vertex/core/utils/getApiErrorMessage.ts
💤 Files with no reviewable changes (3)
  • src/platform/src/modules/api-client/ApiClientPage.tsx
  • src/platform/src/shared/types/global.d.ts
  • src/platform/src/shared/components/auth/SocialAuthSection.tsx
✅ Files skipped from review due to trivial changes (4)
  • src/platform/src/shared/components/header/types/index.ts
  • k8s/platform/k8s-aks/airqo-platform/values-stage.yaml
  • src/platform/middleware.ts
  • src/vertex/app/changelog.md

Comment on lines +588 to +595
const previousTier =
payload.data?.previousTier || getPaidTier(currentTier);
const effectiveFrom =
payload.data?.effectiveFrom ||
(currentPlan && selectedPlan.price > currentPlan.price
? 'immediately'
: 'next_billing_period');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use planActionMode as the effectiveFrom fallback.

Lines 590-594 currently infer upgrade vs downgrade from a price comparison. That breaks when prices are equal, currencies differ, or currentPlan is missing, and it can leave the success message plus scheduled-state UI wrong. The dialog already knows the intended mode, so that is the safer fallback.

Proposed fix
       const effectiveFrom =
         payload.data?.effectiveFrom ||
-        (currentPlan && selectedPlan.price > currentPlan.price
-          ? 'immediately'
-          : 'next_billing_period');
+        (planActionMode === 'upgrade'
+          ? 'immediately'
+          : 'next_billing_period');
📝 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.

Suggested change
const previousTier =
payload.data?.previousTier || getPaidTier(currentTier);
const effectiveFrom =
payload.data?.effectiveFrom ||
(currentPlan && selectedPlan.price > currentPlan.price
? 'immediately'
: 'next_billing_period');
const previousTier =
payload.data?.previousTier || getPaidTier(currentTier);
const effectiveFrom =
payload.data?.effectiveFrom ||
(planActionMode === 'upgrade'
? 'immediately'
: 'next_billing_period');
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/platform/src/modules/billing/components/SubscriptionSection.tsx` around
lines 588 - 595, The effectiveFrom fallback logic in SubscriptionSection.tsx
currently infers upgrade vs downgrade by comparing selectedPlan.price to
currentPlan.price; replace that fallback to use the dialog's planActionMode
instead: set effectiveFrom = payload.data?.effectiveFrom || (planActionMode ===
'upgrade' ? 'immediately' : 'next_billing_period'), making sure to still
preserve the existing getPaidTier usage for previousTier and to handle missing
currentPlan safely when reading selectedPlan and planActionMode.

Comment on lines +149 to +165
const errorName = (error as { name?: string })?.name;
if (errorName !== 'AbortError') {
console.warn('Failed to refresh session after setting password', error);
}

if (!isMountedRef.current || errorName === 'AbortError') {
return 'failed';
}

try {
await update({ authMethods: fallbackAuthMethods });
return 'fallback';
} catch (updateError) {
console.warn(
'Failed to apply fallback auth methods after setting password',
updateError
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Sanitize auth refresh errors before logging.

Raw error objects here can carry sensitive request metadata. Log only sanitized fields (name/message/status) instead of entire error payloads.

Suggested fix
+  const toSafeErrorLog = (value: unknown) => {
+    if (value instanceof Error) {
+      return { name: value.name, message: value.message };
+    }
+    if (value && typeof value === 'object') {
+      const candidate = value as { name?: unknown; message?: unknown; response?: { status?: unknown } };
+      return {
+        name: typeof candidate.name === 'string' ? candidate.name : 'UnknownError',
+        message:
+          typeof candidate.message === 'string'
+            ? candidate.message
+            : 'Unexpected error',
+        status:
+          typeof candidate.response?.status === 'number'
+            ? candidate.response.status
+            : undefined,
+      };
+    }
+    return { message: 'Unexpected error' };
+  };
+
     } catch (error) {
       const errorName = (error as { name?: string })?.name;
       if (errorName !== 'AbortError') {
-        console.warn('Failed to refresh session after setting password', error);
+        console.warn(
+          'Failed to refresh session after setting password',
+          toSafeErrorLog(error)
+        );
       }
@@
       } catch (updateError) {
         console.warn(
           'Failed to apply fallback auth methods after setting password',
-          updateError
+          toSafeErrorLog(updateError)
         );
         return 'failed';
       }

As per coding guidelines "Keep NextAuth session tokens out of logs and sanitize error payloads".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/platform/src/shared/components/auth/SetPasswordPromptDialog.tsx` around
lines 149 - 165, The current console.warn calls in SetPasswordPromptDialog
(around variables errorName, isMountedRef, update, fallbackAuthMethods) log
whole error objects which may contain sensitive tokens; change them to log a
sanitized error payload containing only safe fields (e.g., name, message,
status/code) instead of the raw error for both the initial refresh error and the
updateError in the catch block; implement this by extracting name/message/status
(or code) into a small object (or a helper sanitizeError) and pass that object
to console.warn along with a concise context string.

Comment thread src/vertex/core/utils/getApiErrorMessage.ts Outdated
@BwanikaRobert
BwanikaRobert force-pushed the Update-Grid-Management-Module-to-use-InfoBanners branch from 816393a to 91d34b6 Compare May 26, 2026 08:41
@BwanikaRobert
BwanikaRobert requested a review from Copilot May 26, 2026 08:46

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated 6 comments.

Comment on lines +18 to +20
return /<\/?[a-z][\s\S]*>/i.test(data)
? 'An unexpected error occurred. Please try again.'
: data;
Comment on lines +25 to +26
await navigator.clipboard.writeText(text);
showBanner({ message: "API URL copied!", severity: "success", scoped: false });

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

execCommand('copy') is deprecated. If someone is on a browser old enough to lack the Clipboard API, the rest of the app almost certainly doesn't work either.

Comment on lines +24 to 32
const handleCopy = async (text: string) => {
if (!text) return;
try {
await navigator.clipboard.writeText(text);
showBanner({ message: "Copied to clipboard", severity: "success", scoped: false });
} catch {
showBanner({ message: "Failed to copy to clipboard", severity: "error", scoped: false });
}
};
Comment on lines +27 to +36
onSuccess: () => {
onClose();
setTimeout(() => {
showBanner({
message: "Grid details updated successfully",
severity: "success",
scoped: false,
});
}, 100);
},

// Hook to update grid details
export const useUpdateGridDetails = (gridId: string) => {
export const useUpdateGridDetails = (gridId: string, options?: UseUpdateGridDetailsOptions) => {
Comment on lines +112 to 119
onSuccess: (data) => {
queryClient.invalidateQueries({ queryKey: ["gridDetails", gridId] });
queryClient.invalidateQueries({ queryKey: ["grids"] });
options?.onSuccess?.(data);
},
onError: (error: AxiosError<ErrorResponse>) => {
ReusableToast({
message: `Failed to update grid: ${getApiErrorMessage(error)}`,
type: "ERROR",
});
options?.onError?.(error);
},

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

user feedback should locally handled by the component

@BwanikaRobert
BwanikaRobert requested a review from Copilot May 26, 2026 09:16

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated 5 comments.

Comment on lines 22 to 30
const handleCopy = async (text: string) => {
if (!text) return;
try {
if (navigator?.clipboard?.writeText) {
await navigator.clipboard.writeText(text);
} else {
const el = document.createElement("textarea");
el.value = text;
el.style.position = "fixed";
el.style.opacity = "0";
document.body.appendChild(el);
el.select();
document.execCommand("copy");
document.body.removeChild(el);
}
ReusableToast({ message: "API URL copied!", type: "SUCCESS" });
await navigator.clipboard.writeText(text);
showBanner({ message: "API URL copied!", severity: "success", scoped: false });
} catch {
ReusableToast({ message: "Failed to copy to clipboard", type: "ERROR" });
showBanner({ message: "Failed to copy to clipboard", severity: "error", scoped: false });
}
};
Comment on lines +24 to 32
const handleCopy = async (text: string) => {
if (!text) return;
try {
await navigator.clipboard.writeText(text);
showBanner({ message: "Copied to clipboard", severity: "success", scoped: false });
} catch {
showBanner({ message: "Failed to copy to clipboard", severity: "error", scoped: false });
}
};
Comment on lines 117 to 119
onError: (error: AxiosError<ErrorResponse>) => {
ReusableToast({
message: `Failed to update grid: ${getApiErrorMessage(error)}`,
type: "ERROR",
});
options?.onError?.(error);
},
Comment on lines +16 to +19
const HTML_PATTERN = /<\/?[a-z][\s\S]*>/i;
const GENERIC_ERROR = 'An unexpected error occurred. Please try again.';

const sanitize = (msg: string): string => (HTML_PATTERN.test(msg) ? GENERIC_ERROR : msg);
Comment thread src/vertex/app/changelog.md Outdated

- **Dialog Error Feedback**: `create-grid.tsx`, `edit-grid-details-dialog.tsx`, and `admin-levels-modal.tsx` now use `scoped: true` so validation and action errors remain visible inside the active dialog without closing it.
- **Post-Dialog Success Feedback**: `create-grid.tsx` and `edit-grid-details-dialog.tsx` now use `scoped: false` with a `setTimeout(100ms)` to allow the dialog to unmount before showing the global success banner.
- **Missing Banner Feedback Added**: `create-admin-level.tsx` now surfaces error feedback that was previously silent.
@BwanikaRobert
BwanikaRobert requested a review from Copilot May 26, 2026 09:29

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated 4 comments.

Comment on lines 22 to 30
const handleCopy = async (text: string) => {
if (!text) return;
try {
if (navigator?.clipboard?.writeText) {
await navigator.clipboard.writeText(text);
} else {
const el = document.createElement("textarea");
el.value = text;
el.style.position = "fixed";
el.style.opacity = "0";
document.body.appendChild(el);
el.select();
document.execCommand("copy");
document.body.removeChild(el);
}
ReusableToast({ message: "API URL copied!", type: "SUCCESS" });
await navigator.clipboard.writeText(text);
showBanner({ message: "API URL copied!", severity: "success", scoped: false });
} catch {
ReusableToast({ message: "Failed to copy to clipboard", type: "ERROR" });
showBanner({ message: "Failed to copy to clipboard", severity: "error", scoped: false });
}
};
Comment on lines +72 to +81
onSuccess: () => {
handleClose();
setTimeout(() => {
showBanner({
message: `New grid added!`,
severity: "success",
scoped: false,
});
}, AFTER_DIALOG_CLOSE_MS);
},
Comment on lines +16 to +20
const HTML_PATTERN = /<\/?[a-z][\s\S]*>/i;
const GENERIC_ERROR = 'An unexpected error occurred. Please try again.';

const sanitize = (msg: string): string => (HTML_PATTERN.test(msg) ? GENERIC_ERROR : msg);

Comment on lines 117 to 119
onError: (error: AxiosError<ErrorResponse>) => {
ReusableToast({
message: `Failed to update grid: ${getApiErrorMessage(error)}`,
type: "ERROR",
});
options?.onError?.(error);
},

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
src/vertex/components/features/grids/grid-details-card.tsx (1)

24-30: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add a best-effort clipboard fallback when navigator.clipboard.writeText isn’t available

  • handleCopy is called from the button click, but navigator.clipboard.writeText is still unavailable/blocked in non-secure or unsupported contexts, causing the current handler to always show “Failed to copy”.
  • Consider feature-detecting navigator.clipboard?.writeText and using a legacy document.execCommand('copy') textarea fallback as a last resort (deprecated/unreliable by design, so treat it as best-effort).
Suggested minimal fallback patch
 const handleCopy = async (text: string) => {
     if (!text) return;
     try {
-        await navigator.clipboard.writeText(text);
+        if (navigator?.clipboard?.writeText) {
+            await navigator.clipboard.writeText(text);
+        } else {
+            const el = document.createElement("textarea");
+            el.value = text;
+            el.style.position = "fixed";
+            el.style.opacity = "0";
+            document.body.appendChild(el);
+            el.select();
+            document.execCommand("copy");
+            document.body.removeChild(el);
+        }
         showBanner({ message: "Copied to clipboard", severity: "success", scoped: false });
     } catch {
         showBanner({ message: "Failed to copy to clipboard", severity: "error", scoped: false });
     }
 };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/vertex/components/features/grids/grid-details-card.tsx` around lines 24 -
30, The handleCopy handler should feature-detect navigator.clipboard?.writeText
and, if unavailable or it throws, fall back to a best-effort legacy approach:
create a hidden textarea, set its value to text, append to document.body, select
its contents, call document.execCommand('copy') inside try/catch, then remove
the textarea and restore focus; call showBanner({message: "Copied to clipboard",
severity: "success", scoped: false}) on success (either clipboard API succeeded
or execCommand returned true) and show the failure banner only if both attempts
fail. Update the async function handleCopy to implement this two-tier approach
and ensure the temporary element is cleaned up in all code paths.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/vertex/app/changelog.md`:
- Line 19: Fix the inconsistent list indentation for the changelog entry about
`create-admin-level.tsx` so it matches the other list items (use the same bullet
indentation level, e.g., no extra leading spaces or consistently two-space
nested indentation). Edit the line containing "- **Admin Level Error Feedback
Migrated**: `create-admin-level.tsx`..." to align with the surrounding list
bullets so markdownlint MD005 passes.

In `@src/vertex/components/features/grids/admin-levels-modal.tsx`:
- Line 75: The inline edit is sending { name } to updateAdminLevel but the
backend expects the field description; change the payload passed to
updateAdminLevel to { description: name } (or rename the local variable to
description and pass { description }) and update the related hook/type for
updateAdminLevel payload to reflect description instead of name (adjust the hook
signature/type used by updateAdminLevel so it expects { levelId, data: {
description } }).

---

Duplicate comments:
In `@src/vertex/components/features/grids/grid-details-card.tsx`:
- Around line 24-30: The handleCopy handler should feature-detect
navigator.clipboard?.writeText and, if unavailable or it throws, fall back to a
best-effort legacy approach: create a hidden textarea, set its value to text,
append to document.body, select its contents, call document.execCommand('copy')
inside try/catch, then remove the textarea and restore focus; call
showBanner({message: "Copied to clipboard", severity: "success", scoped: false})
on success (either clipboard API succeeded or execCommand returned true) and
show the failure banner only if both attempts fail. Update the async function
handleCopy to implement this two-tier approach and ensure the temporary element
is cleaned up in all code paths.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 811bef0e-4753-48b5-9d53-8291ce4a8d62

📥 Commits

Reviewing files that changed from the base of the PR and between 816393a and e64f777.

📒 Files selected for processing (10)
  • src/vertex/app/changelog.md
  • src/vertex/components/features/grids/admin-levels-modal.tsx
  • src/vertex/components/features/grids/create-admin-level.tsx
  • src/vertex/components/features/grids/create-grid.tsx
  • src/vertex/components/features/grids/edit-grid-details-dialog.tsx
  • src/vertex/components/features/grids/grid-details-card.tsx
  • src/vertex/components/features/grids/grid-measurements-api-card.tsx
  • src/vertex/core/constants/ui.ts
  • src/vertex/core/hooks/useGrids.ts
  • src/vertex/core/utils/getApiErrorMessage.ts


- **Dialog Error Feedback**: `create-grid.tsx`, `edit-grid-details-dialog.tsx`, and `admin-levels-modal.tsx` now use `scoped: true` so validation and action errors remain visible inside the active dialog without closing it.
- **Post-Dialog Success Feedback**: `create-grid.tsx` and `edit-grid-details-dialog.tsx` now use `scoped: false` with a `setTimeout(100ms)` to allow the dialog to unmount before showing the global success banner.
- **Admin Level Error Feedback Migrated**: `create-admin-level.tsx` now routes existing error feedback through the centralized banner flow instead of the previous toast-based mechanism.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix list indentation for markdownlint compliance.

At Line 19, list indentation is inconsistent (MD005), which can fail docs lint checks.

Suggested fix
- - **Admin Level Error Feedback Migrated**: `create-admin-level.tsx` now routes existing error feedback through the centralized banner flow instead of the previous toast-based mechanism.
+- **Admin Level Error Feedback Migrated**: `create-admin-level.tsx` now routes existing error feedback through the centralized banner flow instead of the previous toast-based 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.

Suggested change
- **Admin Level Error Feedback Migrated**: `create-admin-level.tsx` now routes existing error feedback through the centralized banner flow instead of the previous toast-based mechanism.
- **Admin Level Error Feedback Migrated**: `create-admin-level.tsx` now routes existing error feedback through the centralized banner flow instead of the previous toast-based mechanism.
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 19-19: Inconsistent indentation for list items at the same level
Expected: 0; Actual: 1

(MD005, list-indent)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/vertex/app/changelog.md` at line 19, Fix the inconsistent list
indentation for the changelog entry about `create-admin-level.tsx` so it matches
the other list items (use the same bullet indentation level, e.g., no extra
leading spaces or consistently two-space nested indentation). Edit the line
containing "- **Admin Level Error Feedback Migrated**:
`create-admin-level.tsx`..." to align with the surrounding list bullets so
markdownlint MD005 passes.

},
}
);
updateAdminLevel({ levelId, data: { name } });

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use the backend-accepted update field to unblock admin-level edits.

At Line 75, the mutation still sends { name }, but the API currently accepts description, so inline edit remains broken.

Suggested fix
-    updateAdminLevel({ levelId, data: { name } });
+    updateAdminLevel({ levelId, data: { description: name } });

And align the hook payload type accordingly:

- useMutation<AdminLevelResponse, AxiosError<ErrorResponse>, { levelId: string; data: { name: string } }>
+ useMutation<AdminLevelResponse, AxiosError<ErrorResponse>, { levelId: string; data: { description: string } }>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/vertex/components/features/grids/admin-levels-modal.tsx` at line 75, The
inline edit is sending { name } to updateAdminLevel but the backend expects the
field description; change the payload passed to updateAdminLevel to {
description: name } (or rename the local variable to description and pass {
description }) and update the related hook/type for updateAdminLevel payload to
reflect description instead of name (adjust the hook signature/type used by
updateAdminLevel so it expects { levelId, data: { description } }).

@BwanikaRobert
BwanikaRobert requested review from Codebmk and Copilot May 26, 2026 09:47

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated 4 comments.

Comment on lines +16 to +19
const HTML_PATTERN = /<\/?[a-z][\s\S]*>/i;
const GENERIC_ERROR = 'An unexpected error occurred. Please try again.';

const sanitize = (msg: string): string => (HTML_PATTERN.test(msg) ? GENERIC_ERROR : msg);
const HTML_PATTERN = /<\/?[a-z][\s\S]*>/i;
const GENERIC_ERROR = 'An unexpected error occurred. Please try again.';

const sanitize = (msg: string): string => (HTML_PATTERN.test(msg) ? GENERIC_ERROR : msg);
Comment on lines +25 to 29
await navigator.clipboard.writeText(text);
showBanner({ message: "API URL copied!", severity: "success", scoped: false });
} catch {
ReusableToast({ message: "Failed to copy to clipboard", type: "ERROR" });
showBanner({ message: "Failed to copy to clipboard", severity: "error", scoped: false });
}
Comment on lines +30 to +36
setTimeout(() => {
showBanner({
message: "Grid details updated successfully",
severity: "success",
scoped: false,
});
}, AFTER_DIALOG_CLOSE_MS);
@BwanikaRobert
BwanikaRobert requested a review from Copilot May 26, 2026 13:45

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated 8 comments.


// Hook to update grid details
export const useUpdateGridDetails = (gridId: string) => {
export const useUpdateGridDetails = (gridId: string, options?: UseUpdateGridDetailsOptions) => {
Comment on lines +112 to 119
onSuccess: (data) => {
queryClient.invalidateQueries({ queryKey: ["gridDetails", gridId] });
queryClient.invalidateQueries({ queryKey: ["grids"] });
options?.onSuccess?.(data);
},
onError: (error: AxiosError<ErrorResponse>) => {
ReusableToast({
message: `Failed to update grid: ${getApiErrorMessage(error)}`,
type: "ERROR",
});
options?.onError?.(error);
},

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Codebmk The Copilot suggestion contradicts the new architecture. Hooks owning their own feedback makes them harder to reuse and override.

Comment on lines +73 to +76
const handleClose = () => {
setOpen(false);
form.reset();
};
Comment on lines +21 to +22
const { updateAdminLevel, isLoading: isUpdating } = useUpdateAdminLevel({
onSuccess: (data) => {
Comment on lines +28 to +30
setEditingId(null);
setEditValue("");
},
Comment on lines 40 to 41
const [editingId, setEditingId] = useState<string | null>(null);
const [editValue, setEditValue] = useState("");
Comment on lines +25 to 29
await navigator.clipboard.writeText(text);
showBanner({ message: "API URL copied!", severity: "success", scoped: false });
} catch {
ReusableToast({ message: "Failed to copy to clipboard", type: "ERROR" });
showBanner({ message: "Failed to copy to clipboard", severity: "error", scoped: false });
}
Comment on lines +26 to +29
try {
await navigator.clipboard.writeText(text);
showBanner({ message: "Copied to clipboard", severity: "success", scoped: false });
} catch {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Codebmk The execCommand suggestion I intentionally removed it. document.execCommand is deprecated and browsers are actively removing it. navigator.clipboard.writeText is supported on all modern browsers and the app requires HTTPS, so the Clipboard API is always available. The catch block handles failure gracefully with an error banner.

@BwanikaRobert
BwanikaRobert requested a review from Copilot May 26, 2026 17:04

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated 5 comments.

Comment on lines +16 to +19
const HTML_PATTERN = /<\/?[a-z][\s\S]*>/i;
const GENERIC_ERROR = 'An unexpected error occurred. Please try again.';

const fallbackIfHtml = (msg: string): string => (HTML_PATTERN.test(msg) ? GENERIC_ERROR : msg);
Comment on lines 117 to 119
onError: (error: AxiosError<ErrorResponse>) => {
ReusableToast({
message: `Failed to update grid: ${getApiErrorMessage(error)}`,
type: "ERROR",
});
options?.onError?.(error);
},
Comment on lines +25 to 29
await navigator.clipboard.writeText(text);
showBanner({ message: "API URL copied!", severity: "success", scoped: false });
} catch {
ReusableToast({ message: "Failed to copy to clipboard", type: "ERROR" });
showBanner({ message: "Failed to copy to clipboard", severity: "error", scoped: false });
}
Comment on lines +65 to +71
const bannerTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);

useEffect(() => {
return () => {
if (bannerTimerRef.current) clearTimeout(bannerTimerRef.current);
};
}, []);
Comment on lines +93 to +99
bannerTimerRef.current = setTimeout(() => {
showBanner({
message: `New grid added!`,
severity: "success",
scoped: false,
});
}, AFTER_DIALOG_CLOSE_MS);
@BwanikaRobert
BwanikaRobert requested a review from Copilot May 26, 2026 17:22

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 12 changed files in this pull request and generated 6 comments.

@@ -1,11 +1,14 @@
import React, { useEffect, useState } from "react";
import React, { useState } from "react";

const canUpdate = usePermission(PERMISSIONS.SITE.UPDATE);

useEffect(() => {
document.body.removeChild(el);
}
ReusableToast({ message: "API URL copied!", type: "SUCCESS" });
await navigator.clipboard.writeText(text);
Comment on lines +24 to +27
const handleCopy = async (text: string) => {
if (!text) return;
try {
await navigator.clipboard.writeText(text);
Comment on lines +15 to +17
const showDeferredBanner = (options: ShowBannerOptions) => {
timerRef.current = setTimeout(() => {
showBanner(options);
Comment on lines +16 to +19
const HTML_PATTERN = /<\/?[a-z][\s\S]*>/i;
const GENERIC_ERROR = 'An unexpected error occurred. Please try again.';

const fallbackIfHtml = (msg: string): string => (HTML_PATTERN.test(msg) ? GENERIC_ERROR : msg);
@BwanikaRobert
BwanikaRobert requested a review from Copilot May 26, 2026 17:33

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 12 changed files in this pull request and generated 7 comments.

@@ -1,11 +1,14 @@
import React, { useEffect, useState } from "react";
import React, { useState } from "react";

const canUpdate = usePermission(PERMISSIONS.SITE.UPDATE);

useEffect(() => {
Comment on lines +25 to +26
await navigator.clipboard.writeText(text);
showBanner({ message: "API URL copied!", severity: "success", scoped: false });
Comment on lines +26 to +27
try {
await navigator.clipboard.writeText(text);
Comment on lines 45 to +46
await navigator.clipboard.writeText(id);
ReusableToast({
showBanner({
"use client";

import { useState, useEffect } from "react";
import { useState,useEffect } from "react";
type: "ERROR",
});
options?.onError?.(error);

@BwanikaRobert
BwanikaRobert requested a review from Copilot May 26, 2026 17:41

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 12 changed files in this pull request and generated 8 comments.

Comment on lines 22 to 30
const handleCopy = async (text: string) => {
if (!text) return;
try {
if (navigator?.clipboard?.writeText) {
await navigator.clipboard.writeText(text);
} else {
const el = document.createElement("textarea");
el.value = text;
el.style.position = "fixed";
el.style.opacity = "0";
document.body.appendChild(el);
el.select();
document.execCommand("copy");
document.body.removeChild(el);
}
ReusableToast({ message: "API URL copied!", type: "SUCCESS" });
await navigator.clipboard.writeText(text);
showBanner({ message: "API URL copied!", severity: "success", scoped: false });
} catch {
ReusableToast({ message: "Failed to copy to clipboard", type: "ERROR" });
showBanner({ message: "Failed to copy to clipboard", severity: "error", scoped: false });
}
};
Comment on lines +26 to 31
try {
await navigator.clipboard.writeText(text);
showBanner({ message: "Copied to clipboard", severity: "success", scoped: false });
} catch {
showBanner({ message: "Failed to copy to clipboard", severity: "error", scoped: false });
}
Comment on lines +97 to +100
interface UseUpdateGridDetailsOptions {
onSuccess?: (data: Grid) => void;
onError?: (error: AxiosError<ErrorResponse>) => void;
}
Comment on lines +125 to +128
interface UseCreateGridOptions {
onSuccess?: (data: Grid) => void;
onError?: (error: AxiosError<ErrorResponse>) => void;
}
Comment on lines +148 to +151
interface UseCreateAdminLevelOptions {
onSuccess?: (data: AdminLevelResponse) => void;
onError?: (error: AxiosError<ErrorResponse>) => void;
}
Comment on lines +184 to +187
interface UseUpdateAdminLevelOptions {
onSuccess?: (data: AdminLevelResponse) => void;
onError?: (error: AxiosError<ErrorResponse>) => void;
}
Comment on lines +16 to +22
const HTML_PATTERN = /<\/?[a-z][\s\S]*>/i;
const GENERIC_ERROR = 'An unexpected error occurred. Please try again.';

const fallbackIfHtml = (msg: string): string => {
const trimmed = msg.trim();
return !trimmed || HTML_PATTERN.test(trimmed) ? GENERIC_ERROR : trimmed;
};
"use client";

import { useState, useEffect } from "react";
import { useState,useEffect } from "react";

@Codebmk Codebmk left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM Much thanks @BwanikaRobert

@Baalmart
Baalmart merged commit a683740 into airqo-platform:staging May 27, 2026
@Baalmart Baalmart mentioned this pull request May 27, 2026
7 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Vertex: Migration] Update Grid Management Module to use InfoBanners

4 participants