PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.16
Yatra – Travel Booking & Tour Operator Software v3.0.16
3.0.16 3.0.15 3.0.14 3.0.14.1 3.0.14.2 3.0.12 3.0.13 3.0.11 3.0.10 3.0.9 3.0.8 3.0.7 3.0.6 3.0.5 3.0.5.1 3.0.4 3.0.3 3.0.2.9 3.0.2.7 3.0.2.8 3.0.2.6 trunk 1.0.0 2.0.0 2.0.1 All 84 releases
← All changes | resources/js/components/Layout.tsx +139 -47 3.0.6 → 3.0.16 View file →
@@ -9,8 +9,9 @@
9 9 import {
10 10 LayoutDashboard,
11 11 MapPin,
12 12 Calendar,
13 + CalendarClock,
13 14 CalendarDays,
14 15 Star,
15 16 BarChart3,
16 17 Settings,
@@ -81,8 +82,9 @@
81 82 import {
82 83 readMenuOverrides,
83 84 readMenuOrder,
84 85 readUiChrome,
86 + DEFAULT_MENU_ITEMS,
85 87 type MenuOverrides,
86 88 } from "../lib/sidebar-menu-defaults";
87 89 import { MenuIcon } from "../lib/menu-icon";
88 90 import {
@@ -100,8 +102,12 @@
100 102 } from "../hooks/useModules";
101 103 import { isProPluginActive, isModuleActive } from "../lib/plugin-utils";
102 104 import { navigateMenu } from "../hooks/useNavigate";
103 105 import { canCap } from "../hooks/useCapabilities";
106 +import {
107 + useNotificationCounts,
108 + SUBPAGE_TO_SECTION,
109 +} from "../hooks/useNotificationCounts";
104 110 import { InlineNotices } from "./notices/InlineNotices";
105 111
106 112 interface LayoutProps {
107 113 children: React.ReactNode;
@@ -285,21 +291,31 @@
285 291 // Get current subpage and tab from URL
286 292 const currentSubpage = useMemo(() => {
287 293 const params = new URLSearchParams(window.location.search);
288 294 return params.get("subpage") || "dashboard";
289 - // eslint-disable-next-line react-hooks/exhaustive-deps
295 + // eslint-disable-next-line react-hooks/exhaustive-deps
290 296 }, [urlKey]);
291 297
298 + // "New since last seen" counts for the sidebar badges (Bookings / Payments /
299 + // Abandoned Recovery). Read-only — if the request fails, no badge is shown.
300 + const { data: newCounts } = useNotificationCounts();
301 + const getNewCount = (subpage: string): number => {
302 + const section = SUBPAGE_TO_SECTION[subpage];
303 + if (!section) return 0;
304 + const value = newCounts?.[section];
305 + return typeof value === "number" && value > 0 ? value : 0;
306 + };
307 +
292 308 const currentTab = useMemo(() => {
293 309 const params = new URLSearchParams(window.location.search);
294 310 return params.get("tab") || "all";
295 - // eslint-disable-next-line react-hooks/exhaustive-deps
311 + // eslint-disable-next-line react-hooks/exhaustive-deps
296 312 }, [urlKey]);
297 313
298 314 const currentAction = useMemo(() => {
299 315 const params = new URLSearchParams(window.location.search);
300 316 return params.get("action");
301 - // eslint-disable-next-line react-hooks/exhaustive-deps
317 + // eslint-disable-next-line react-hooks/exhaustive-deps
302 318 }, [urlKey]);
303 319
304 320 // Check if we're on the trip form page
305 321 const isTripFormPage = useMemo(() => {
@@ -307,53 +323,29 @@
307 323 currentSubpage === "trips" &&
308 324 (currentTab === "all" || !currentTab) &&
309 325 (currentAction === "create" || currentAction === "edit")
310 326 );
311 - // eslint-disable-next-line react-hooks/exhaustive-deps
327 + // eslint-disable-next-line react-hooks/exhaustive-deps
312 328 }, [currentSubpage, currentTab, currentAction, urlKey]);
313 329
314 - // Track expanded submenus - initialize based on current subpage
330 + // Track expanded submenus — start with the parent of the page being loaded
331 + // open, so a deep link or a refresh lands with the right submenu already
332 + // expanded (no flash of a collapsed menu).
333 + //
334 + // Derived from the menu definition rather than a hardcoded list of parents:
335 + // it used to name `trips` and `itinerary` explicitly, which meant every other
336 + // parent (Payments, and anything added later) stayed collapsed on refresh.
315 337 const [expandedMenus, setExpandedMenus] = useState<string[]>(() => {
316 338 const params = new URLSearchParams(window.location.search);
317 339 const subpage = params.get("subpage") || "dashboard";
318 - const menus: string[] = [];
319 340
320 - if (subpage === "trips") {
321 - menus.push("trips");
322 - }
323 -
324 - if (subpage === "itinerary") {
325 - menus.push("itinerary");
326 - }
327 -
328 - return menus;
341 + return DEFAULT_MENU_ITEMS.some(
342 + (item) => item.slug === subpage && (item.submenu?.length ?? 0) > 0,
343 + )
344 + ? [subpage]
345 + : [];
329 346 });
330 347
331 - // Auto-expand menu when on submenu pages
332 - useEffect(() => {
333 - const menusToExpand: string[] = [];
334 -
335 - if (currentSubpage === "trips") {
336 - menusToExpand.push("trips");
337 - }
338 -
339 - if (currentSubpage === "itinerary") {
340 - menusToExpand.push("itinerary");
341 - }
342 -
343 - setExpandedMenus((prev) => {
344 - // Only update if the menus to expand are different
345 - const newMenus = [...new Set([...prev, ...menusToExpand])];
346 - if (
347 - newMenus.length !== prev.length ||
348 - !newMenus.every((m) => prev.includes(m))
349 - ) {
350 - return newMenus;
351 - }
352 - return prev;
353 - });
354 - }, [currentSubpage, urlKey]);
355 -
356 348 // Get base admin URL
357 349 const baseUrl = useMemo(() => {
358 350 return window.yatraAdmin?.siteUrl
359 351 ? `${window.yatraAdmin.siteUrl}/wp-admin/admin.php?page=yatra`
@@ -474,8 +466,36 @@
474 466 subpage: "payments",
475 467 label: __("Payments", "yatra"),
476 468 icon: CreditCard,
477 469 cap: "yatra_view_financial_reports",
470 + // Payments only becomes a parent when the Pro "Scheduled Payments"
471 + // module is enabled — otherwise there is nothing to nest and it stays
472 + // exactly as it was: a single item that navigates straight to the
473 + // payments list. Children are only rendered for a visible parent, so
474 + // the pair inherits the Payments capability gate.
475 + //
476 + // `undefined` rather than an empty array: the rest of this component
477 + // treats "has a submenu" as truthiness, and an empty array would read
478 + // as a parent with no matching child — which would stop the Payments
479 + // item highlighting on its own page.
480 + submenu: (window as any).yatraAdmin?.scheduledPaymentsEnabled
481 + ? [
482 + {
483 + tab: "all",
484 + label: __("All Payments", "yatra"),
485 + icon: CreditCard,
486 + },
487 + {
488 + // "Scheduled" alone — the parent already says Payments. This
489 + // label is also the page's heading (the top bar reads it from
490 + // the active submenu item).
491 + tab: "scheduled",
492 + label: __("Scheduled", "yatra"),
493 + icon: CalendarClock,
494 + isPremium: true,
495 + },
496 + ]
497 + : undefined,
478 498 },
479 499 {
480 500 subpage: "bookings",
481 501 label: __("Bookings", "yatra"),
@@ -801,8 +821,13 @@
801 821 const topLevel = orderGroup("", groups.get("") ?? []);
802 822 return topLevel.map((atom) => {
803 823 const childAtoms = orderGroup(atom.key, groups.get(atom.key) ?? []);
804 824 return {
825 + // React key. NOT `subpage`: a child promoted to top-level keeps its
826 + // parent's subpage (`payments.scheduled` → subpage `payments`), so two
827 + // siblings could share it. Duplicate keys make React reconcile the
828 + // wrong nodes and throw "removeChild ... not a child of this node".
829 + menuKey: atom.key,
805 830 subpage: atom.subpage,
806 831 label: atom.label,
807 832 icon: atom.icon,
808 833 iconOverride: atom.iconOverride,
@@ -808,8 +833,11 @@
808 833 iconOverride: atom.iconOverride,
809 834 isPremium: atom.isPremium,
810 835 submenu: childAtoms.length
811 836 ? childAtoms.map((child) => ({
837 + // Unique per child, for the same reason as `menuKey` above:
838 + // subpage + tab can repeat once items are moved between parents.
839 + menuKey: child.key,
812 840 // Submenu rendering supports both legacy intra-parent items
813 841 // (matching parent's subpage) and promoted/demoted items
814 842 // that point at their own subpage.
815 843 tab: child.defaultParent === atom.subpage ? child.tab : undefined,
@@ -828,12 +856,37 @@
828 856 };
829 857 });
830 858 }, [menuItems, baseUrl]);
831 859
860 + // Keep the active page's parent expanded as the URL changes. Lives here
861 + // (after `brandedMenuItems`) so it reads the menu actually being rendered —
862 + // that includes parents the menu customizer created by demoting an item into
863 + // one, which the static defaults above don't know about.
864 + useEffect(() => {
865 + const parentOfCurrentPage = brandedMenuItems.find(
866 + (item: any) =>
867 + item.subpage === currentSubpage && (item.submenu?.length ?? 0) > 0,
868 + );
869 + if (!parentOfCurrentPage) {
870 + return;
871 + }
872 +
873 + setExpandedMenus((prev) =>
874 + prev.includes(parentOfCurrentPage.subpage)
875 + ? prev
876 + : [...prev, parentOfCurrentPage.subpage],
877 + );
878 + }, [currentSubpage, urlKey, brandedMenuItems]);
879 +
832 880 /** UI chrome visibility flags (version, Back to WP, Join Community). */
833 881 const uiChrome = useMemo(() => readUiChrome(), []);
834 882
835 883 const isActive = (subpage: string, tab?: string) => {
884 + // The Google Calendar dashboard is reached from Settings → Integration, so
885 + // keep the Settings menu item highlighted while that page is open.
886 + if (subpage === "settings" && currentSubpage === "google-calendar") {
887 + return true;
888 + }
836 889 if (tab) {
837 890 return currentSubpage === subpage && currentTab === tab;
838 891 }
839 892 // For parent menu items, check if current subpage matches
@@ -929,9 +982,18 @@
929 982 const isExpanded = hasSubmenu && isMenuExpanded(item.subpage);
930 983 const active = isActive(item.subpage);
931 984
932 985 return (
933 - <div key={item.subpage}>
986 + // Keyed by the unique menu key, and by the item's shape: when a
987 + // module toggle turns a plain item into a parent (Payments →
988 + // All Payments / Scheduled) the branch below swaps an <a> for a
989 + // <button> + children, and a shape-aware key makes React remount
990 + // that subtree cleanly instead of reusing mismatched nodes.
991 + <div
992 + key={`${(item as any).menuKey ?? item.subpage}:${
993 + hasSubmenu ? "parent" : "leaf"
994 + }`}
995 + >
934 996 {hasSubmenu ? (
935 997 <>
936 998 <button
937 999 onClick={() => toggleMenu(item.subpage)}
@@ -948,13 +1010,28 @@
948 1010 className="w-5 h-5"
949 1011 />
950 1012 <span>{item.label}</span>
951 1013 </div>
952 - {isExpanded ? (
953 - <ChevronDown className="w-4 h-4" />
954 - ) : (
955 - <ChevronRight className="w-4 h-4" />
956 - )}
1014 + <div className="flex items-center gap-2">
1015 + {/* New-since-last-seen badge. Also rendered here, not
1016 + only on flat items: a section that gains children
1017 + (Payments) would otherwise lose its badge. */}
1018 + {!active && getNewCount(item.subpage) > 0 && (
1019 + <span
1020 + className="inline-flex items-center justify-center min-w-[18px] h-[18px] px-1.5 rounded-full bg-red-500 text-white text-[10px] font-semibold leading-none"
1021 + aria-label={`${getNewCount(item.subpage)} ${__("new", "yatra")}`}
1022 + >
1023 + {getNewCount(item.subpage) > 99
1024 + ? "99+"
1025 + : getNewCount(item.subpage)}
1026 + </span>
1027 + )}
1028 + {isExpanded ? (
1029 + <ChevronDown className="w-4 h-4" />
1030 + ) : (
1031 + <ChevronRight className="w-4 h-4" />
1032 + )}
1033 + </div>
957 1034 </button>
958 1035 {isExpanded && item.submenu && (
959 1036 <div className="ml-4 mt-1 space-y-1">
960 1037 {item.submenu.map((subItem: any) => {
@@ -969,9 +1046,12 @@
969 1046 : isActive(navSubpage);
970 1047 const SubIcon = subItem.icon;
971 1048 return (
972 1049 <a
973 - key={`${navSubpage}.${navTab ?? ""}`}
1050 + key={
1051 + subItem.menuKey ??
1052 + `${navSubpage}.${navTab ?? ""}`
1053 + }
974 1054 href={getUrl(navSubpage, navTab)}
975 1055 onClick={(e) =>
976 1056 handleMenuNavClick(e, navSubpage, navTab)
977 1057 }
@@ -1025,8 +1105,20 @@
1025 1105 className="w-5 h-5"
1026 1106 />
1027 1107 <span>{item.label}</span>
1028 1108 </div>
1109 + {/* New-since-last-seen badge (Bookings / Payments /
1110 + Abandoned Recovery). Hidden while viewing that page. */}
1111 + {!active && getNewCount(item.subpage) > 0 && (
1112 + <span
1113 + className="ml-auto inline-flex items-center justify-center min-w-[18px] h-[18px] px-1.5 rounded-full bg-red-500 text-white text-[10px] font-semibold leading-none"
1114 + aria-label={`${getNewCount(item.subpage)} ${__("new", "yatra")}`}
1115 + >
1116 + {getNewCount(item.subpage) > 99
1117 + ? "99+"
1118 + : getNewCount(item.subpage)}
1119 + </span>
1120 + )}
1029 1121 {item.isPremium && !isProPluginActive() && (
1030 1122 <div className="absolute inset-y-0 right-2 flex items-center justify-center">
1031 1123 <div className="w-4 h-4 rounded-full bg-gradient-to-r from-amber-500 to-orange-500 text-white flex items-center justify-center">
1032 1124 <Crown className="w-2.5 h-2.5" />