feat(grids): Replace all toast-based feedback in grid management components#3534
Conversation
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughMigrates 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. ChangesGrid Management Banner Migration
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested labels
Suggested reviewers
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🧪 Generate unit tests (beta)
Warning Review ran into problems🔥 ProblemsErrors were encountered while retrieving linked issues. Errors (1)
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. Comment |
There was a problem hiding this comment.
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 winGive 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
📒 Files selected for processing (7)
src/vertex/components/features/grids/admin-levels-modal.tsxsrc/vertex/components/features/grids/create-admin-level.tsxsrc/vertex/components/features/grids/create-grid.tsxsrc/vertex/components/features/grids/edit-grid-details-dialog.tsxsrc/vertex/components/features/grids/grid-details-card.tsxsrc/vertex/components/features/grids/grid-measurements-api-card.tsxsrc/vertex/core/hooks/useGrids.ts
| onError: (error) => { | ||
| showBanner({ | ||
| message: `Failed to update admin level: ${getApiErrorMessage(error)}`, | ||
| severity: "error", | ||
| scoped: true, | ||
| }); |
There was a problem hiding this comment.
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.
| 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.
There was a problem hiding this comment.
please apply coderabbit feedback @BwanikaRobert , thanks
There was a problem hiding this comment.
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!
| 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 }); | ||
| } | ||
| }; |
There was a problem hiding this comment.
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.
| 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.
There was a problem hiding this comment.
Outside the scope of the PR
There was a problem hiding this comment.
@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?
(´• ω •`) 🐇
There was a problem hiding this comment.
nice actionable feedback from coderabbit, please address this within current pr @BwanikaRobert
There was a problem hiding this comment.
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!
|
@Codebmk This is ready for review when you have a moment. |
| 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 }); | ||
| } | ||
| }; |
There was a problem hiding this comment.
nice actionable feedback from coderabbit, please address this within current pr @BwanikaRobert
| onError: (error) => { | ||
| showBanner({ | ||
| message: `Failed to update admin level: ${getApiErrorMessage(error)}`, | ||
| severity: "error", | ||
| scoped: true, | ||
| }); |
There was a problem hiding this comment.
please apply coderabbit feedback @BwanikaRobert , thanks
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
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 winAdd
<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 whenBannerSlotis 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
📒 Files selected for processing (1)
src/vertex/components/features/grids/edit-grid-details-dialog.tsx
There was a problem hiding this comment.
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
useGridsto accept optionalonSuccess/onErrorcallbacks instead of firing toasts directly. - Updated grid/admin-level dialogs and cards to use
showBannerwith appropriatescopedbehavior (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.
| </details> | ||
|
|
||
| <details> | ||
| <summary><strong>Files Updated (6)</strong></summary> |
…onents with the useBanner hook.
There was a problem hiding this comment.
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 winPersist the backend-issued OAuth token here.
This branch still stores
oauthTokeneven thoughfetchEnhancedUserProfile()now returnsprofile.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
📒 Files selected for processing (30)
k8s/platform/k8s-aks/airqo-platform/values-stage.yamlsrc/platform/middleware.tssrc/platform/src/app/(dashboard)/request-organization/layout.tsxsrc/platform/src/app/(dashboard)/system/feedback/page.tsxsrc/platform/src/modules/api-client/ApiClientPage.tsxsrc/platform/src/modules/billing/BillingPage.tsxsrc/platform/src/modules/billing/components/CheckoutDialog.tsxsrc/platform/src/modules/billing/components/SubscriptionSection.tsxsrc/platform/src/modules/feedback/components/FeedbackLauncher.tsxsrc/platform/src/next-auth.d.tssrc/platform/src/shared/components/auth/SetPasswordPromptDialog.tsxsrc/platform/src/shared/components/auth/SocialAuthSection.tsxsrc/platform/src/shared/components/header/index.tsxsrc/platform/src/shared/components/header/types/index.tssrc/platform/src/shared/hooks/index.tssrc/platform/src/shared/hooks/useUser.tssrc/platform/src/shared/layouts/MainLayout.tsxsrc/platform/src/shared/lib/auth.tssrc/platform/src/shared/lib/oauth-session.tssrc/platform/src/shared/lib/validators.tssrc/platform/src/shared/providers/auth-provider.tsxsrc/platform/src/shared/services/apiClient.tssrc/platform/src/shared/services/sessionAuthToken.tssrc/platform/src/shared/services/subscriptionService.tssrc/platform/src/shared/services/userService.tssrc/platform/src/shared/types/api.tssrc/platform/src/shared/types/global.d.tssrc/vertex/app/changelog.mdsrc/vertex/components/features/grids/grid-measurements-api-card.tsxsrc/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
| const previousTier = | ||
| payload.data?.previousTier || getPaidTier(currentTier); | ||
| const effectiveFrom = | ||
| payload.data?.effectiveFrom || | ||
| (currentPlan && selectedPlan.price > currentPlan.price | ||
| ? 'immediately' | ||
| : 'next_billing_period'); | ||
|
|
There was a problem hiding this comment.
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.
| 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.
| 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 | ||
| ); |
There was a problem hiding this comment.
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.
816393a to
91d34b6
Compare
| return /<\/?[a-z][\s\S]*>/i.test(data) | ||
| ? 'An unexpected error occurred. Please try again.' | ||
| : data; |
| await navigator.clipboard.writeText(text); | ||
| showBanner({ message: "API URL copied!", severity: "success", scoped: false }); |
There was a problem hiding this comment.
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.
| 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 }); | ||
| } | ||
| }; |
| 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) => { |
| 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); | ||
| }, |
There was a problem hiding this comment.
user feedback should locally handled by the component
| 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 }); | ||
| } | ||
| }; |
| 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 }); | ||
| } | ||
| }; |
| onError: (error: AxiosError<ErrorResponse>) => { | ||
| ReusableToast({ | ||
| message: `Failed to update grid: ${getApiErrorMessage(error)}`, | ||
| type: "ERROR", | ||
| }); | ||
| options?.onError?.(error); | ||
| }, |
| 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); |
|
|
||
| - **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. |
| 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 }); | ||
| } | ||
| }; |
| onSuccess: () => { | ||
| handleClose(); | ||
| setTimeout(() => { | ||
| showBanner({ | ||
| message: `New grid added!`, | ||
| severity: "success", | ||
| scoped: false, | ||
| }); | ||
| }, AFTER_DIALOG_CLOSE_MS); | ||
| }, |
| 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); | ||
|
|
| onError: (error: AxiosError<ErrorResponse>) => { | ||
| ReusableToast({ | ||
| message: `Failed to update grid: ${getApiErrorMessage(error)}`, | ||
| type: "ERROR", | ||
| }); | ||
| options?.onError?.(error); | ||
| }, |
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (1)
src/vertex/components/features/grids/grid-details-card.tsx (1)
24-30:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winAdd a best-effort clipboard fallback when
navigator.clipboard.writeTextisn’t available
handleCopyis called from the button click, butnavigator.clipboard.writeTextis still unavailable/blocked in non-secure or unsupported contexts, causing the current handler to always show “Failed to copy”.- Consider feature-detecting
navigator.clipboard?.writeTextand using a legacydocument.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
📒 Files selected for processing (10)
src/vertex/app/changelog.mdsrc/vertex/components/features/grids/admin-levels-modal.tsxsrc/vertex/components/features/grids/create-admin-level.tsxsrc/vertex/components/features/grids/create-grid.tsxsrc/vertex/components/features/grids/edit-grid-details-dialog.tsxsrc/vertex/components/features/grids/grid-details-card.tsxsrc/vertex/components/features/grids/grid-measurements-api-card.tsxsrc/vertex/core/constants/ui.tssrc/vertex/core/hooks/useGrids.tssrc/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. |
There was a problem hiding this comment.
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.
| - **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 } }); |
There was a problem hiding this comment.
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 } }).
| 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); |
| 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 }); | ||
| } |
| setTimeout(() => { | ||
| showBanner({ | ||
| message: "Grid details updated successfully", | ||
| severity: "success", | ||
| scoped: false, | ||
| }); | ||
| }, AFTER_DIALOG_CLOSE_MS); |
|
|
||
| // Hook to update grid details | ||
| export const useUpdateGridDetails = (gridId: string) => { | ||
| export const useUpdateGridDetails = (gridId: string, options?: UseUpdateGridDetailsOptions) => { |
| 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); | ||
| }, |
There was a problem hiding this comment.
@Codebmk The Copilot suggestion contradicts the new architecture. Hooks owning their own feedback makes them harder to reuse and override.
| const handleClose = () => { | ||
| setOpen(false); | ||
| form.reset(); | ||
| }; |
| const { updateAdminLevel, isLoading: isUpdating } = useUpdateAdminLevel({ | ||
| onSuccess: (data) => { |
| setEditingId(null); | ||
| setEditValue(""); | ||
| }, |
| const [editingId, setEditingId] = useState<string | null>(null); | ||
| const [editValue, setEditValue] = useState(""); |
| 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 }); | ||
| } |
| try { | ||
| await navigator.clipboard.writeText(text); | ||
| showBanner({ message: "Copied to clipboard", severity: "success", scoped: false }); | ||
| } catch { |
There was a problem hiding this comment.
@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.
| 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); |
| onError: (error: AxiosError<ErrorResponse>) => { | ||
| ReusableToast({ | ||
| message: `Failed to update grid: ${getApiErrorMessage(error)}`, | ||
| type: "ERROR", | ||
| }); | ||
| options?.onError?.(error); | ||
| }, |
| 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 }); | ||
| } |
| const bannerTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null); | ||
|
|
||
| useEffect(() => { | ||
| return () => { | ||
| if (bannerTimerRef.current) clearTimeout(bannerTimerRef.current); | ||
| }; | ||
| }, []); |
| bannerTimerRef.current = setTimeout(() => { | ||
| showBanner({ | ||
| message: `New grid added!`, | ||
| severity: "success", | ||
| scoped: false, | ||
| }); | ||
| }, AFTER_DIALOG_CLOSE_MS); |
…etTimeout banner patter
…etTimeout banner patter
| @@ -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); |
| const handleCopy = async (text: string) => { | ||
| if (!text) return; | ||
| try { | ||
| await navigator.clipboard.writeText(text); |
| const showDeferredBanner = (options: ShowBannerOptions) => { | ||
| timerRef.current = setTimeout(() => { | ||
| showBanner(options); |
| 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); |
… via GENERIC_ERROR constant
| @@ -1,11 +1,14 @@ | |||
| import React, { useEffect, useState } from "react"; | |||
| import React, { useState } from "react"; | |||
|
|
||
| const canUpdate = usePermission(PERMISSIONS.SITE.UPDATE); | ||
|
|
||
| useEffect(() => { |
| await navigator.clipboard.writeText(text); | ||
| showBanner({ message: "API URL copied!", severity: "success", scoped: false }); |
| try { | ||
| await navigator.clipboard.writeText(text); |
| await navigator.clipboard.writeText(id); | ||
| ReusableToast({ | ||
| showBanner({ |
| "use client"; | ||
|
|
||
| import { useState, useEffect } from "react"; | ||
| import { useState,useEffect } from "react"; |
| type: "ERROR", | ||
| }); | ||
| options?.onError?.(error); | ||
|
|
| 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 }); | ||
| } | ||
| }; |
| 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 }); | ||
| } |
| interface UseUpdateGridDetailsOptions { | ||
| onSuccess?: (data: Grid) => void; | ||
| onError?: (error: AxiosError<ErrorResponse>) => void; | ||
| } |
| interface UseCreateGridOptions { | ||
| onSuccess?: (data: Grid) => void; | ||
| onError?: (error: AxiosError<ErrorResponse>) => void; | ||
| } |
| interface UseCreateAdminLevelOptions { | ||
| onSuccess?: (data: AdminLevelResponse) => void; | ||
| onError?: (error: AxiosError<ErrorResponse>) => void; | ||
| } |
| interface UseUpdateAdminLevelOptions { | ||
| onSuccess?: (data: AdminLevelResponse) => void; | ||
| onError?: (error: AxiosError<ErrorResponse>) => void; | ||
| } |
| 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
left a comment
There was a problem hiding this comment.
LGTM Much thanks @BwanikaRobert
Summary of Changes (What does this PR do?)
Migrates all toast-based notifications in the Grid Management module to the
centralized InfoBanner system (
useBannerhook). Since most grid managementactions occur inside dialogs, banner scoping has been carefully applied:
scoped: true— error banners rendered inside the active dialog'sbuilt-in
BannerSlot(dialog stays open on error).scoped: false+setTimeout(100ms)— success banners shown after adialog closes, giving the dialog time to unmount before the global banner
renders.
Files changed:
src/vertex/core/hooks/useGrids.ts— Confirmed hooks already used thecallback options pattern with no toast calls. Minor cleanup only.
src/vertex/components/features/grids/create-grid.tsx— Replaced toastwith
useBanner; successscoped: false, errorscoped: 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— Replacedtoast with
useBanner; all actionsscoped: true(dialog stays open).src/vertex/components/features/grids/create-admin-level.tsx— Addedmissing banner feedback (previously silent on error).
src/vertex/components/features/grids/grid-details-card.tsx— Copy actionuses
scoped: false; hardened withasync/awaitand error fallback.src/vertex/components/features/grids/grid-measurements-api-card.tsx—Copy actions use
scoped: false.Known limitations:
(
MiniMap) does not render in the current development environment.triggered internally by
ReusableInputField'sshowCopyButtonprop, whichis outside the scope of this PR.
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?
name or visibility and save. Confirm a success banner appears at the top
of the page after the dialog closes.
error banner appears inside the dialog.
Confirm an error banner appears inside the dialog.
Confirm a global success banner appears.
returns a server-side parameter error — see Known Limitations above.)
global success banner appears.
Confirm a global success banner appears.
What are the relevant tickets?
Screenshots (optional)
Summary by CodeRabbit