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 +104 -43 3.0.11 → 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 {
@@ -324,50 +326,26 @@
324 326 );
325 327 // eslint-disable-next-line react-hooks/exhaustive-deps
326 328 }, [currentSubpage, currentTab, currentAction, urlKey]);
327 329
328 - // 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.
329 337 const [expandedMenus, setExpandedMenus] = useState<string[]>(() => {
330 338 const params = new URLSearchParams(window.location.search);
331 339 const subpage = params.get("subpage") || "dashboard";
332 - const menus: string[] = [];
333 340
334 - if (subpage === "trips") {
335 - menus.push("trips");
336 - }
337 -
338 - if (subpage === "itinerary") {
339 - menus.push("itinerary");
340 - }
341 -
342 - return menus;
341 + return DEFAULT_MENU_ITEMS.some(
342 + (item) => item.slug === subpage && (item.submenu?.length ?? 0) > 0,
343 + )
344 + ? [subpage]
345 + : [];
343 346 });
344 347
345 - // Auto-expand menu when on submenu pages
346 - useEffect(() => {
347 - const menusToExpand: string[] = [];
348 -
349 - if (currentSubpage === "trips") {
350 - menusToExpand.push("trips");
351 - }
352 -
353 - if (currentSubpage === "itinerary") {
354 - menusToExpand.push("itinerary");
355 - }
356 -
357 - setExpandedMenus((prev) => {
358 - // Only update if the menus to expand are different
359 - const newMenus = [...new Set([...prev, ...menusToExpand])];
360 - if (
361 - newMenus.length !== prev.length ||
362 - !newMenus.every((m) => prev.includes(m))
363 - ) {
364 - return newMenus;
365 - }
366 - return prev;
367 - });
368 - }, [currentSubpage, urlKey]);
369 -
370 348 // Get base admin URL
371 349 const baseUrl = useMemo(() => {
372 350 return window.yatraAdmin?.siteUrl
373 351 ? `${window.yatraAdmin.siteUrl}/wp-admin/admin.php?page=yatra`
@@ -488,8 +466,36 @@
488 466 subpage: "payments",
489 467 label: __("Payments", "yatra"),
490 468 icon: CreditCard,
491 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,
492 498 },
493 499 {
494 500 subpage: "bookings",
495 501 label: __("Bookings", "yatra"),
@@ -815,8 +821,13 @@
815 821 const topLevel = orderGroup("", groups.get("") ?? []);
816 822 return topLevel.map((atom) => {
817 823 const childAtoms = orderGroup(atom.key, groups.get(atom.key) ?? []);
818 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,
819 830 subpage: atom.subpage,
820 831 label: atom.label,
821 832 icon: atom.icon,
822 833 iconOverride: atom.iconOverride,
@@ -822,8 +833,11 @@
822 833 iconOverride: atom.iconOverride,
823 834 isPremium: atom.isPremium,
824 835 submenu: childAtoms.length
825 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,
826 840 // Submenu rendering supports both legacy intra-parent items
827 841 // (matching parent's subpage) and promoted/demoted items
828 842 // that point at their own subpage.
829 843 tab: child.defaultParent === atom.subpage ? child.tab : undefined,
@@ -842,8 +856,28 @@
842 856 };
843 857 });
844 858 }, [menuItems, baseUrl]);
845 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 +
846 880 /** UI chrome visibility flags (version, Back to WP, Join Community). */
847 881 const uiChrome = useMemo(() => readUiChrome(), []);
848 882
849 883 const isActive = (subpage: string, tab?: string) => {
@@ -948,9 +982,18 @@
948 982 const isExpanded = hasSubmenu && isMenuExpanded(item.subpage);
949 983 const active = isActive(item.subpage);
950 984
951 985 return (
952 - <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 + >
953 996 {hasSubmenu ? (
954 997 <>
955 998 <button
956 999 onClick={() => toggleMenu(item.subpage)}
@@ -967,13 +1010,28 @@
967 1010 className="w-5 h-5"
968 1011 />
969 1012 <span>{item.label}</span>
970 1013 </div>
971 - {isExpanded ? (
972 - <ChevronDown className="w-4 h-4" />
973 - ) : (
974 - <ChevronRight className="w-4 h-4" />
975 - )}
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>
976 1034 </button>
977 1035 {isExpanded && item.submenu && (
978 1036 <div className="ml-4 mt-1 space-y-1">
979 1037 {item.submenu.map((subItem: any) => {
@@ -988,9 +1046,12 @@
988 1046 : isActive(navSubpage);
989 1047 const SubIcon = subItem.icon;
990 1048 return (
991 1049 <a
992 - key={`${navSubpage}.${navTab ?? ""}`}
1050 + key={
1051 + subItem.menuKey ??
1052 + `${navSubpage}.${navTab ?? ""}`
1053 + }
993 1054 href={getUrl(navSubpage, navTab)}
994 1055 onClick={(e) =>
995 1056 handleMenuNavClick(e, navSubpage, navTab)
996 1057 }