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
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import { NetworkCreationRequest } from "@/core/apis/networks";
import ReusableDialog from "@/components/shared/dialog/ReusableDialog";
import ReusableInputField from "@/components/shared/inputfield/ReusableInputField";
import ReusableButton from "@/components/shared/button/ReusableButton";
import ReusableToast from "@/components/shared/toast/ReusableToast";
import { getApiErrorMessage } from "@/core/utils/getApiErrorMessage";
import { useBanner } from "@/context/banner-context";

type TabStatus = "all" | "pending" | "under_review" | "approved" | "denied";

Expand All @@ -24,7 +24,7 @@ export default function NetworkRequestsClient({ initialRequests }: NetworkReques
const router = useRouter();
const [activeTab, setActiveTab] = useState<TabStatus>("pending");
const [isUpdating, setIsUpdating] = useState(false);

const { showBanner } = useBanner();
// Action management
const [selectedRequest, setSelectedRequest] = useState<NetworkCreationRequest | null>(null);
const [actionType, setActionType] = useState<'approve' | 'deny' | 'review' | null>(null);
Expand Down Expand Up @@ -67,18 +67,20 @@ export default function NetworkRequestsClient({ initialRequests }: NetworkReques
reviewer_notes: notes
});

ReusableToast({
showBanner({
message: response.data.message || 'Status updated successfully',
type: 'SUCCESS'
severity: 'success',
scoped: false
});

setSelectedRequest(null);
setActionType(null);
router.refresh(); // Invalidate server data
} catch (error: unknown) {
ReusableToast({
showBanner({
message: `Action failed: ${getApiErrorMessage(error)}`,
type: 'ERROR'
severity: 'error',
scoped: false
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Comment on lines +83 to 84
} finally {
setIsUpdating(false);
Expand Down
28 changes: 28 additions & 0 deletions src/vertex/app/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,34 @@
> **Note**: This changelog consolidates all recent improvements, features, and fixes to the AirQo Vertex frontend.

---
## Version 1.23.56
**Released:** May 29, 2026
Comment on lines +6 to +7

### Banner Notifications for Network Management

Migrated user feedback in the network management feature from toast notifications to the banner system. This standardizes notifications across the networks admin area for a consistent feedback experience.

<details>
<summary><strong>Changes (5)</strong></summary>

- **Network Requests Actions**: Replaced toast notifications with page-level banners (`scoped: false`) for approve, deny, and review status updates in `NetworkRequestsClient`.
- **Copy ID Feedback**: Updated the "copy Sensor Manufacturer ID" action in the networks table to show success/error banners (`scoped: false`) instead of toasts.
- **Create Network Form**: Success feedback now uses `showBannerWithDelay` so the banner appears after the dialog closes (`scoped: false`); error feedback uses an in-dialog banner (`scoped: true`).
- **Network Request Dialog**: Submission success and errors both use `scoped: true` to render feedback inline inside the dialog via `BannerSlot`; success additionally uses `showBannerWithDelay` to time the display correctly.
- **Banner Text Alignment Fix**: Added `text-left` to the `Banner` component's root div so banner text is always left-aligned regardless of any ancestor container that sets `text-center` (e.g. shadcn `DialogHeader`).

</details>

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

- `src/vertex/app/(authenticated)/admin/networks/requests/NetworkRequestsClient.tsx` [MODIFIED]
- `src/vertex/components/features/networks/client-paginated-networks-table.tsx` [MODIFIED]
- `src/vertex/components/features/networks/create-network-form.tsx` [MODIFIED]
- `src/vertex/components/features/networks/network-request-dialog.tsx` [MODIFIED]
- `src/vertex/components/ui/banner.tsx` [MODIFIED]

</details>

## Version 1.23.55
**Released:** May 28, 2026
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import ReusableTable, {
import { useRouter } from "next/navigation";
import { Network } from "@/core/apis/networks";
import { AqCopy01, AqLinkExternal01 } from "@airqo/icons-react";
import ReusableToast from "@/components/shared/toast/ReusableToast";
import { useBanner } from "@/context/banner-context";

interface NetworksTableProps {
networks: Network[];
Expand All @@ -26,6 +26,7 @@ export default function ClientPaginatedNetworksTable({
onNetworkClick,
}: NetworksTableProps) {
const router = useRouter();
const { showBanner } = useBanner();

const handleNetworkClick = (item: unknown) => {
const network = item as Network;
Expand All @@ -38,9 +39,9 @@ export default function ClientPaginatedNetworksTable({
if (text) {
try {
await navigator.clipboard.writeText(text);
ReusableToast({ message: "Sensor Manufacturer ID copied", type: "SUCCESS" });
showBanner({ message: "Sensor Manufacturer ID copied", severity: "success", scoped: false });
} catch {
ReusableToast({ message: "Failed to copy ID", type: "ERROR" });
showBanner({ message: "Failed to copy ID", severity: "error", scoped: false });
}
Comment on lines 43 to 45
}
};
Expand Down
18 changes: 9 additions & 9 deletions src/vertex/components/features/networks/create-network-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,19 @@ import { Form, FormField } from "@/components/ui/form";
import ReusableDialog from "@/components/shared/dialog/ReusableDialog";
import ReusableInputField from "@/components/shared/inputfield/ReusableInputField";
import ReusableButton from "@/components/shared/button/ReusableButton";
import ReusableToast from "@/components/shared/toast/ReusableToast";
import { AqPlus } from "@airqo/icons-react";
import { getApiErrorMessage } from "@/core/utils/getApiErrorMessage";
import ReusableSelectInput from "@/components/shared/select/ReusableSelectInput";

import { useBanner } from "@/context/banner-context";
import { useBannerWithDelay } from "@/core/hooks/useBannerWithDelay";
import { networkFormSchema, NetworkFormValues } from "./schema";

export function CreateNetworkForm() {
const [open, setOpen] = useState(false);
const [isPending, setIsPending] = useState(false);
const queryClient = useQueryClient();

const { showBanner } = useBanner();
const { showBannerWithDelay } = useBannerWithDelay();
const form = useForm<NetworkFormValues>({
resolver: zodResolver(networkFormSchema),
defaultValues: {
Expand Down Expand Up @@ -48,19 +49,18 @@ export function CreateNetworkForm() {
try {
await axios.post('/api/network', values, {
headers: { 'Content-Type': 'application/json' },
});

ReusableToast({ message: 'Sensor Manufacturer created successfully!', type: 'SUCCESS' });
});
queryClient.invalidateQueries({ queryKey: ["networks"] });
handleClose();
showBannerWithDelay({ severity: 'success', message: 'Sensor Manufacturer created successfully!', scoped: false });
} catch (error: unknown) {
const errorMessage = getApiErrorMessage(error);
ReusableToast({ message: errorMessage, type: 'ERROR' });
const errorMessage = getApiErrorMessage(error);
showBanner({ severity: 'error', message: errorMessage, scoped: true });
Comment on lines +57 to +58

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.

The Bannerslot is form ReusableDialog and the dialog only closes after success.

Comment on lines +55 to +58

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.

@copilot ReusableDialog already includes internally no changes needed.

} finally {
setIsPending(false);
}
},
[handleClose, queryClient]
[handleClose, queryClient, showBanner, showBannerWithDelay]
);

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ import ReusableDialog from "@/components/shared/dialog/ReusableDialog";
import ReusableInputField from "@/components/shared/inputfield/ReusableInputField";
import { networkRequestSchema, NetworkRequestValues } from "./schema";
import { useAppSelector } from "@/core/redux/hooks";
import ReusableToast from "@/components/shared/toast/ReusableToast";
import { getApiErrorMessage } from "@/core/utils/getApiErrorMessage";
import { useBanner } from "@/context/banner-context";
import { useBannerWithDelay } from "@/core/hooks/useBannerWithDelay";

interface NetworkRequestDialogProps {
open: boolean;
Expand All @@ -20,6 +21,8 @@ interface NetworkRequestDialogProps {
export function NetworkRequestDialog({ open, onOpenChange }: NetworkRequestDialogProps) {
const userDetails = useAppSelector((state) => state.user.userDetails);
const queryClient = useQueryClient();
const { showBanner } = useBanner();
const { showBannerWithDelay } = useBannerWithDelay();

const { mutate: submitRequest, isPending } = useMutation({
mutationFn: async (data: NetworkRequestValues) => {
Expand All @@ -37,15 +40,12 @@ export function NetworkRequestDialog({ open, onOpenChange }: NetworkRequestDialo
}
return response.json();
},
onSuccess: (resp) => {
ReusableToast({
message: resp.message || "Your request for a new Sensor Manufacturer has been submitted successfully!",
type: "SUCCESS",
});
onSuccess: (resp) => {
queryClient.invalidateQueries({ queryKey: ["network-requests"] });
showBannerWithDelay({ severity: 'success', message: resp.message || "Your request for a new Sensor Manufacturer has been submitted successfully!", scoped: true });

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.

This was already explained

},
onError: (error) => {
ReusableToast({ message: getApiErrorMessage(error), type: "ERROR" });
onError: (error) => {
showBanner({ severity: 'error', message: getApiErrorMessage(error), scoped: true });
},
Comment on lines +43 to 49

@coderabbitai coderabbitai Bot May 29, 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

Both banners are currently unreachable as wired.

Two separate problems here, mirroring (and partly contradicting) create-network-form.tsx:

  1. Delayed success (line 45) uses scoped: true. The success banner fires after the dialog closes (handleClose on line 90), at which point any in-dialog BannerSlot is unmounted — and GlobalBannerContainer ignores scoped banners. So it never shows. This should be scoped: false (the after-close global path), matching create-network-form.tsx.

  2. Error (line 48) uses scoped: true but there's no <BannerSlot /> mounted in this dialog, so submission errors are invisible. Either mount a BannerSlot below the header for the error case, or fall back to scoped: false.

🛠️ Suggested fix
-    const {showBanner}=useBanner();
+    const { showBanner, BannerSlot } = useBanner();
@@
-        onSuccess: (resp) => {            
-            queryClient.invalidateQueries({ queryKey: ["network-requests"] });
-            showBannerWithDelay({severity:'success',message:resp.message || "Your request for a new Sensor Manufacturer has been submitted successfully!",scoped:true});
-        },
-        onError: (error) => {            
-            showBanner({severity:'error',message:getApiErrorMessage(error),scoped:true})
-        },
+        onSuccess: (resp) => {
+            queryClient.invalidateQueries({ queryKey: ["network-requests"] });
+            showBannerWithDelay({ severity: 'success', message: resp.message || "Your request for a new Sensor Manufacturer has been submitted successfully!", scoped: false });
+        },
+        onError: (error) => {
+            showBanner({ severity: 'error', message: getApiErrorMessage(error), scoped: true });
+        },

And mount the slot inside the form for the scoped error:

             <Form {...form}>
+                <BannerSlot />
                 <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
📝 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
onSuccess: (resp) => {
queryClient.invalidateQueries({ queryKey: ["network-requests"] });
showBannerWithDelay({severity:'success',message:resp.message || "Your request for a new Sensor Manufacturer has been submitted successfully!",scoped:true});
},
onError: (error) => {
ReusableToast({ message: getApiErrorMessage(error), type: "ERROR" });
onError: (error) => {
showBanner({severity:'error',message:getApiErrorMessage(error),scoped:true})
},
onSuccess: (resp) => {
queryClient.invalidateQueries({ queryKey: ["network-requests"] });
showBannerWithDelay({ severity: 'success', message: resp.message || "Your request for a new Sensor Manufacturer has been submitted successfully!", scoped: false });
},
onError: (error) => {
showBanner({ severity: 'error', message: getApiErrorMessage(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/networks/network-request-dialog.tsx` around
lines 43 - 49, The success banner called via showBannerWithDelay in the
onSuccess handler should use scoped:false (not scoped:true) because it fires
after handleClose and must go through the global path; update the
showBannerWithDelay call accordingly, and for the onError handler either mount a
<BannerSlot /> inside this dialog (e.g., under the header in
network-request-dialog) if you want scoped errors, or change showBanner({...,
scoped: true}) to scoped:false so errors are visible via
GlobalBannerContainer—pick one approach and make both handlers consistent with
that decision (reference showBannerWithDelay, showBanner, BannerSlot,
handleClose, and GlobalBannerContainer).

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.

The success banner has to be scoped true because here we have two modals open the banner displays in the second open modal setting it to false the banner will display in the global scope yet we still have an open modal

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.

The PR description images show three scenarios: (1) net1 — "Create A New Sensor Manufacturer" modal with a scoped error banner rendered inside it; (2) net2 — "Request New Sensor Manufacturer" modal with a scoped error banner ("Network request timed out") visible inside the modal; (3) net3 — "Import External Device" modal displaying a green success banner ("sensor manufacturer creation request submitted successfully") after the Request dialog has already closed.


@BwanikaRobert, that context clears it up — net3 confirms it perfectly. The NetworkRequestDialog is opened from within the Import External Device modal, which has its own BannerSlot still mounted. After the child dialog closes, showBannerWithDelay with scoped: true correctly targets that parent modal's slot, so the success message surfaces inside the still-open workflow rather than leaking to the global scope. That's actually a cleaner UX than scoped: false would produce here.

Similarly, the error banner in net2 appearing inside the dialog makes sense for the same reason — the parent modal's BannerSlot is the active scoped receiver.

My original concern was based on a single-modal assumption; the stacked-modal context makes scoped: true the right call. Thanks for walking through it — withdrawing that suggestion.

(◕‿◕)🐇


✏️ Learnings added
Learnt from: BwanikaRobert
Repo: airqo-platform/AirQo-frontend PR: 3558
File: src/vertex/components/features/networks/network-request-dialog.tsx:43-49
Timestamp: 2026-05-29T16:31:51.091Z
Learning: In `src/vertex/components/features/networks/network-request-dialog.tsx`, `NetworkRequestDialog` is opened from within a parent modal (e.g., "Import External Device"). Because the parent modal has its own `BannerSlot` that stays mounted after the child dialog closes, both success and error banners are intentionally set to `scoped: true`. The success banner uses `showBannerWithDelay({ scoped: true })` so it appears in the parent modal's BannerSlot after the child closes; the error banner uses `showBanner({ scoped: true })` to render inside the active dialog context. Using `scoped: false` would send banners to the global container, which is incorrect when a parent modal is still visible.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: Codebmk
Repo: airqo-platform/AirQo-frontend PR: 3092
File: src/vertex/components/features/networks/network-detail-card.tsx:34-36
Timestamp: 2025-12-12T05:22:05.515Z
Learning: In the networks feature, when deriving display names for network entities, do not append ' Network' if the name already includes 'Network'. This prevents duplication like 'XYZ Network Network'. Apply this check in network-detail-card.tsx and similar components under this folder; add a conditional formatting guard and ensure tests cover both names containing and not containing 'Network'.

Learnt from: Codebmk
Repo: airqo-platform/AirQo-frontend PR: 3092
File: src/vertex/components/features/networks/network-detail-card.tsx:34-36
Timestamp: 2025-12-12T05:22:05.515Z
Learning: In the Vertex project, ensure network names shown in NetworkDetailsCard are styled with the Tailwind 'uppercase' class to render uppercase text. Apply the 'uppercase' class to the element that displays the network name in src/vertex/components/features/networks/network-detail-card.tsx, ensuring the change covers all similar render paths in that component. Avoid duplicating styles, preserve accessibility attributes, and verify no regressions in theming by testing in both light/dark modes.

Comment on lines +47 to 49
});

Expand Down
2 changes: 1 addition & 1 deletion src/vertex/components/ui/banner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ export const Banner: React.FC<BannerProps> = ({
return (
<div
className={cn(
'flex items-start gap-3 rounded-md border shadow-sm',
'flex items-start gap-3 rounded-md border shadow-sm text-left',
paddingClass,
config.bgColor,
config.borderColor,
Expand Down
Loading