Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions src/vertex/app/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,38 @@

---

## Version 1.23.49
**Released:** May 26, 2026

### Grid Management Banner Migration

Migrated all toast-based notifications in the Grid Management module to the centralized `InfoBanner` system powered by `useBanner`. Since most grid actions occur inside dialogs, banner scoping was applied to keep errors inline in the active dialog while deferring success banners until the dialog has closed.

<details>
<summary><strong>Grid Management Banner Migration (4)</strong></summary>

- **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.

- **Copy Feedback Hardened**: `grid-details-card.tsx` and `grid-measurements-api-card.tsx` now use `scoped: false` and handle async copy failures with a fallback error message.

</details>

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

- `src/vertex/components/features/grids/create-grid.tsx` [MODIFIED]
- `src/vertex/components/features/grids/edit-grid-details-dialog.tsx` [MODIFIED]
- `src/vertex/components/features/grids/admin-levels-modal.tsx` [MODIFIED]
- `src/vertex/components/features/grids/create-admin-level.tsx` [MODIFIED]
- `src/vertex/components/features/grids/grid-details-card.tsx` [MODIFIED]
- `src/vertex/components/features/grids/grid-measurements-api-card.tsx` [MODIFIED]
- `src/vertex/core/hooks/useGrids.ts` [MODIFIED]

</details>

---

## Version 1.23.48
**Released:** May 23, 2026

Expand Down
45 changes: 30 additions & 15 deletions src/vertex/components/features/grids/admin-levels-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import ReusableDialog from "@/components/shared/dialog/ReusableDialog";
import ReusableButton from "@/components/shared/button/ReusableButton";
import ReusableInputField from "@/components/shared/inputfield/ReusableInputField";
import { Copy, Edit2, Check, X } from "lucide-react";
import ReusableToast from "@/components/shared/toast/ReusableToast";
import { useBanner } from "@/context/banner-context";
import { getApiErrorMessage } from "@/core/utils/getApiErrorMessage";

interface AdminLevelsModalProps {
isOpen: boolean;
Expand All @@ -15,21 +16,43 @@ interface AdminLevelsModalProps {

export function AdminLevelsModal({ isOpen, onClose }: AdminLevelsModalProps) {
const { adminLevels, isLoading: isLoadingLevels } = useAdminLevels();
const { updateAdminLevel, isLoading: isUpdating } = useUpdateAdminLevel();
const { showBanner } = useBanner();

const [editingId, setEditingId] = useState<string | null>(null);
const [editValue, setEditValue] = useState("");

const { updateAdminLevel, isLoading: isUpdating } = useUpdateAdminLevel({
onSuccess: (data) => {
Comment on lines +24 to +25
showBanner({
message: `Admin level updated to '${data.admin_levels.name}' successfully`,
severity: "success",
scoped: true,
});
setEditingId(null);
setEditValue("");
},
Comment on lines +31 to +33
onError: (error) => {
showBanner({
message: `Failed to update admin level: ${getApiErrorMessage(error)}`,
severity: "error",
scoped: true,
});
Comment on lines +34 to +39

@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!

}
});

const handleCopyId = async (id: string) => {
try {
await navigator.clipboard.writeText(id);
ReusableToast({
showBanner({
Comment on lines 45 to +46
message: "ID copied to clipboard",
type: "SUCCESS",
severity: "success",
scoped: true,
});
} catch {
ReusableToast({
showBanner({
message: "Failed to copy ID",
type: "ERROR",
severity: "error",
scoped: true,
});
}
};
Expand All @@ -49,15 +72,7 @@ export function AdminLevelsModal({ isOpen, onClose }: AdminLevelsModalProps) {
if (!name) {
return;
}
updateAdminLevel(
{ levelId, data: { name } },
{
onSuccess: () => {
setEditingId(null);
setEditValue("");
},
}
);
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 } }).

};

return (
Expand Down
26 changes: 20 additions & 6 deletions src/vertex/components/features/grids/create-admin-level.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ import {
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { AdminLevelsModal } from "./admin-levels-modal";
import { useBanner } from "@/context/banner-context";
import { getApiErrorMessage } from "@/core/utils/getApiErrorMessage";
import { useDeferredBanner } from "@/core/hooks/useDeferredBanner";

const adminLevelFormSchema = z.object({
name: z.string().min(2, {
Expand All @@ -29,7 +32,8 @@ type AdminLevelFormValues = z.infer<typeof adminLevelFormSchema>;
export function CreateAdminLevel() {
const [open, setOpen] = useState(false);
const [viewModalOpen, setViewModalOpen] = useState(false);
const { createAdminLevel, isLoading } = useCreateAdminLevel();
const { showBanner } = useBanner();
const { showDeferredBanner } = useDeferredBanner();

const form = useForm<AdminLevelFormValues>({
resolver: zodResolver(adminLevelFormSchema),
Expand All @@ -43,12 +47,22 @@ export function CreateAdminLevel() {
form.reset();
};

const { createAdminLevel, isLoading } = useCreateAdminLevel({
onSuccess: () => {
handleClose();
showDeferredBanner({ message: "Admin level created successfully", severity: "success", scoped: false });
},
onError: (error) => {
showBanner({
message: `Failed to create admin level: ${getApiErrorMessage(error)}`,
severity: "error",
scoped: true,
});
},
});

const onSubmit = (data: AdminLevelFormValues) => {
createAdminLevel(data, {
onSuccess: () => {
handleClose();
},
});
createAdminLevel(data);
};

return (
Expand Down
39 changes: 27 additions & 12 deletions src/vertex/components/features/grids/create-grid.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import { useState, useEffect } from "react";
import { useState,useEffect } from "react";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import * as z from "zod";
Expand All @@ -15,6 +15,9 @@ import ReusableButton from "@/components/shared/button/ReusableButton";
import ReusableInputField from "@/components/shared/inputfield/ReusableInputField";
import { useNetworks } from "@/core/hooks/useNetworks";
import ReusableSelectInput from "@/components/shared/select/ReusableSelectInput";
import { useBanner } from "@/context/banner-context";
import { getApiErrorMessage } from "@/core/utils/getApiErrorMessage";
import { useDeferredBanner } from "@/core/hooks/useDeferredBanner";

// Lazy load MiniMap to reduce initial bundle size
const MiniMap = dynamic(() => import("@/components/features/mini-map/mini-map"), {
Expand Down Expand Up @@ -58,7 +61,9 @@ type GridFormValues = z.infer<typeof gridFormSchema>;
export function CreateGridForm() {
const [open, setOpen] = useState(false);
const polygon = useAppSelector((state) => state.grids.polygon);
const { createGrid, isLoading } = useCreateGrid();
const { showBanner } = useBanner();
const { showDeferredBanner } = useDeferredBanner();

const { networks, isLoading: isLoadingNetworks } = useNetworks();

const form = useForm<GridFormValues>({
Expand All @@ -71,6 +76,25 @@ export function CreateGridForm() {
},
});

const handleClose = () => {
setOpen(false);
form.reset();
};
Comment on lines +79 to +82

const { createGrid, isLoading } = useCreateGrid({
onSuccess: () => {
handleClose();
showDeferredBanner({ message: `New grid added!`, severity: "success", scoped: false });
},
Comment on lines +85 to +88
onError: (error) => {
showBanner({
message: `Failed to create grid: ${getApiErrorMessage(error)}`,
severity: "error",
scoped: true,
});
}
});

useEffect(() => {
if (polygon) {
form.setValue("shapefile", JSON.stringify(polygon), { shouldValidate: true });
Expand All @@ -79,11 +103,6 @@ export function CreateGridForm() {
}
}, [polygon, form]);

const handleClose = () => {
setOpen(false);
form.reset();
};

const onSubmit = (data: GridFormValues) => {
const gridData = {
name: data.name,
Expand All @@ -92,11 +111,7 @@ export function CreateGridForm() {
network: data.network,
};

createGrid(gridData, {
onSuccess: () => {
handleClose();
},
});
createGrid(gridData);
};

return (
Expand Down
29 changes: 26 additions & 3 deletions src/vertex/components/features/grids/edit-grid-details-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ 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";
import { useDeferredBanner } from "@/core/hooks/useDeferredBanner";

interface EditGridDetailsDialogProps {
open: boolean;
Expand All @@ -19,7 +22,23 @@ const EditGridDetailsDialog: React.FC<EditGridDetailsDialogProps> = ({
onClose,
}) => {
const [form, setForm] = useState({ name: "", visibility: false, admin_level: "" });
const { updateGridDetails, isLoading } = useUpdateGridDetails(grid._id);
const { showBanner } = useBanner();
const { showDeferredBanner } = useDeferredBanner();

const { updateGridDetails, isLoading } = useUpdateGridDetails(grid._id, {
onSuccess: () => {
onClose();
showDeferredBanner({ message: "Grid details updated successfully", severity: "success", scoped: false });
},
Comment on lines +29 to +32
onError: (error) => {
showBanner({
message: `Failed to update grid: ${getApiErrorMessage(error)}`,
severity: "error",
scoped: true,
});
}
});

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

useEffect(() => {
Expand All @@ -39,6 +58,11 @@ const EditGridDetailsDialog: React.FC<EditGridDetailsDialogProps> = ({
const trimmedAdminLevel = form.admin_level.trim();

if (trimmedName.length === 0 || trimmedAdminLevel.length === 0) {
showBanner({
severity: "error",
message: "Grid Name and Admin Level cannot be empty.",
scoped: true,
});
return;
}

Expand All @@ -56,9 +80,8 @@ const EditGridDetailsDialog: React.FC<EditGridDetailsDialogProps> = ({

try {
await updateGridDetails(updates);
onClose();
} catch {
return
return;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
};

Expand Down
15 changes: 10 additions & 5 deletions src/vertex/components/features/grids/grid-details-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import { Badge } from "@/components/ui/badge";
import { Loader2 } from "lucide-react";
import ReusableButton from "@/components/shared/button/ReusableButton";
import { AqCopy01, AqEdit01 } from "@airqo/icons-react";
import ReusableToast from "@/components/shared/toast/ReusableToast";
import { Grid } from "@/app/types/grids";
import { useBanner } from "@/context/banner-context";

interface GridDetailsCardProps {
grid: Grid;
Expand All @@ -15,14 +15,19 @@ interface GridDetailsCardProps {
}

const GridDetailsCard: React.FC<GridDetailsCardProps> = ({ grid, onEdit, loading }) => {
const { showBanner } = useBanner();

if (loading) {
return <Card className="w-full rounded-lg flex flex-col justify-between items-center p-8"><Loader2 className="w-6 h-6 animate-spin" /></Card>;
}

const handleCopy = (text: string) => {
if (text) {
navigator.clipboard.writeText(text);
ReusableToast({ message: "Copied to clipboard", type: "SUCCESS" });
const handleCopy = async (text: string) => {
if (!text) return;
try {
await navigator.clipboard.writeText(text);
Comment on lines +24 to +27
Comment on lines +26 to +27
showBanner({ message: "Copied to clipboard", severity: "success", scoped: false });
} catch {
Comment on lines +26 to +29

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.

showBanner({ message: "Failed to copy to clipboard", severity: "error", scoped: false });
}
Comment on lines +26 to 31
};
Comment on lines +24 to 32

@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!

Comment on lines +24 to 32
Comment on lines +24 to 32

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,37 +4,28 @@ import { Card } from "@/components/ui/card";
import { Loader2 } from "lucide-react";
import ReusableButton from "@/components/shared/button/ReusableButton";
import { AqCopy01 } from "@airqo/icons-react";
import ReusableToast from "@/components/shared/toast/ReusableToast";
import { Grid } from "@/app/types/grids";
import { useBanner } from "@/context/banner-context";

interface GridMeasurementsApiCardProps {
grid: Grid;
loading: boolean;
}

const GridMeasurementsApiCard: React.FC<GridMeasurementsApiCardProps> = ({ grid, loading }) => {
const { showBanner } = useBanner();

if (loading) {
return <Card className="w-full rounded-lg flex flex-col justify-between items-center p-8"><Loader2 className="w-6 h-6 animate-spin" /></Card>;
}

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 });
Comment on lines +25 to +26

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 +25 to +26
} catch {
ReusableToast({ message: "Failed to copy to clipboard", type: "ERROR" });
showBanner({ message: "Failed to copy to clipboard", severity: "error", scoped: false });
}
Comment on lines +25 to 29
Comment on lines +25 to 29
Comment on lines +25 to 29
};
Comment on lines 22 to 30
Comment on lines 22 to 30
Comment on lines 22 to 30

Expand Down
2 changes: 1 addition & 1 deletion src/vertex/context/banner-context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ interface BannerState {
isScoped: boolean;
}

type ShowBannerOptions = BannerDisplayProps & { scoped?: boolean };
export type ShowBannerOptions = BannerDisplayProps & { scoped?: boolean };

interface BannerContextValue {
state: BannerState;
Expand Down
5 changes: 5 additions & 0 deletions src/vertex/core/constants/ui.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/**
* Delay (ms) before showing a global banner after a dialog closes.
* Allows the dialog's unmount cleanup (which clears scoped banners) to complete first.
*/
export const AFTER_DIALOG_CLOSE_MS = 100;
Loading
Loading