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/shared/Table.tsx +75 -137 3.0.2.6 → 3.0.16 View file →
@@ -1,5 +1,6 @@
1 1 import React, { useMemo, useState } from "react";
2 +import { createPortal } from "react-dom";
2 3 import {
3 4 Table as UITable,
4 5 TableBody,
5 6 TableCell,
@@ -146,8 +147,78 @@
146 147 return () => document.removeEventListener("click", handleClickOutside);
147 148 }
148 149 }, [openDropdownId]);
149 150
151 + const getVisibleActions = (item: any) =>
152 + actions.filter((a) => !a.condition || a.condition(item));
153 +
154 + const openActionsMenu = (
155 + e: React.MouseEvent<HTMLButtonElement>,
156 + item: any,
157 + ) => {
158 + e.preventDefault();
159 + e.stopPropagation();
160 + const itemId = getItemId(item);
161 + const rect = e.currentTarget.getBoundingClientRect();
162 + const visibleActions = getVisibleActions(item);
163 + const dropdownHeight = visibleActions.length * 40 + 16;
164 + const spaceBelow = window.innerHeight - rect.bottom;
165 + const spaceAbove = rect.top;
166 + const shouldPositionAbove =
167 + spaceBelow < dropdownHeight && spaceAbove > dropdownHeight;
168 +
169 + // position:fixed uses viewport coordinates; getBoundingClientRect is viewport-relative
170 + setDropdownPosition({
171 + top: shouldPositionAbove
172 + ? Math.max(8, rect.top - dropdownHeight)
173 + : rect.bottom,
174 + right: window.innerWidth - rect.right,
175 + });
176 + setOpenDropdownId(openDropdownId === itemId ? null : itemId);
177 + };
178 +
179 + const renderActionsDropdown = (item: any) => {
180 + const itemId = getItemId(item);
181 + if (openDropdownId !== itemId || !dropdownPosition) return null;
182 +
183 + return createPortal(
184 + <div
185 + className="fixed min-w-[180px] w-max bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-md shadow-md py-1"
186 + style={{
187 + top: `${dropdownPosition.top}px`,
188 + right: `${dropdownPosition.right}px`,
189 + zIndex: 999999,
190 + }}
191 + data-dropdown-content
192 + onClick={(e) => e.stopPropagation()}
193 + >
194 + {getVisibleActions(item).map((action) => (
195 + <button
196 + key={action.key}
197 + type="button"
198 + onClick={(e) => {
199 + e.preventDefault();
200 + e.stopPropagation();
201 + action.onClick(item);
202 + setOpenDropdownId(null);
203 + }}
204 + className={`w-full px-4 py-2 text-left text-sm hover:bg-gray-100 dark:hover:bg-gray-700 flex items-center gap-3 transition-colors cursor-pointer whitespace-nowrap ${
205 + action.variant === "destructive"
206 + ? "text-red-600 dark:text-red-400"
207 + : action.variant === "outline"
208 + ? "text-blue-600 dark:text-blue-400"
209 + : "text-gray-700 dark:text-gray-300"
210 + }`}
211 + >
212 + {action.icon}
213 + {action.label}
214 + </button>
215 + ))}
216 + </div>,
217 + document.body,
218 + );
219 + };
220 +
150 221 // Render skeleton loading state
151 222 const renderSkeleton = () => (
152 223 <UITable>
153 224 <TableHeader>
@@ -498,38 +569,9 @@
498 569 <div className="relative inline-block">
499 570 <Button
500 571 variant="ghost"
501 572 size="icon"
502 - onClick={(e) => {
503 - e.preventDefault();
504 - e.stopPropagation();
505 - const button = e.currentTarget;
506 - const rect = button.getBoundingClientRect();
507 -
508 - // Calculate dropdown height (estimate based on number of actions)
509 - const dropdownHeight =
510 - actions.filter((a) => !a.condition || a.condition(item))
511 - .length *
512 - 40 +
513 - 16;
514 - const spaceBelow = window.innerHeight - rect.bottom;
515 - const spaceAbove = rect.top;
516 -
517 - // Position dropdown above if not enough space below
518 - const shouldPositionAbove =
519 - spaceBelow < dropdownHeight &&
520 - spaceAbove > dropdownHeight;
521 -
522 - setDropdownPosition({
523 - top: shouldPositionAbove
524 - ? rect.top + window.scrollY - dropdownHeight
525 - : rect.bottom + window.scrollY,
526 - right: window.innerWidth - rect.right + window.scrollX,
527 - });
528 - setOpenDropdownId(
529 - openDropdownId === itemId ? null : itemId,
530 - );
531 - }}
573 + onClick={(e) => openActionsMenu(e, item)}
532 574 className="h-8 w-8 hover:bg-gray-100 dark:hover:bg-gray-700"
533 575 aria-label={__("More actions", "yatra")}
534 576 data-dropdown-trigger
535 577 >
@@ -534,48 +576,9 @@
534 576 data-dropdown-trigger
535 577 >
536 578 <MoreVertical className="w-4 h-4" />
537 579 </Button>
538 -
539 - {openDropdownId === itemId && dropdownPosition && (
540 - <div
541 - className="fixed min-w-[180px] w-max bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-md shadow-2xl py-1"
542 - style={{
543 - top: `${dropdownPosition.top}px`,
544 - right: `${dropdownPosition.right}px`,
545 - zIndex: 999999,
546 - }}
547 - data-dropdown-content
548 - onClick={(e) => e.stopPropagation()}
549 - >
550 - {actions
551 - .filter(
552 - (action) => !action.condition || action.condition(item),
553 - )
554 - .map((action) => (
555 - <button
556 - key={action.key}
557 - type="button"
558 - onClick={(e) => {
559 - e.preventDefault();
560 - e.stopPropagation();
561 - action.onClick(item);
562 - setOpenDropdownId(null);
563 - }}
564 - className={`w-full px-4 py-2 text-left text-sm hover:bg-gray-100 dark:hover:bg-gray-700 flex items-center gap-3 transition-colors cursor-pointer whitespace-nowrap ${
565 - action.variant === "destructive"
566 - ? "text-red-600 dark:text-red-400"
567 - : action.variant === "outline"
568 - ? "text-blue-600 dark:text-blue-400"
569 - : "text-gray-700 dark:text-gray-300"
570 - }`}
571 - >
572 - {action.icon}
573 - {action.label}
574 - </button>
575 - ))}
576 - </div>
577 - )}
580 + {renderActionsDropdown(item)}
578 581 </div>
579 582 </TableCell>
580 583 )}
581 584 </TableRow>
@@ -631,35 +634,9 @@
631 634 <div className="relative inline-block">
632 635 <Button
633 636 variant="ghost"
634 637 size="icon"
635 - onClick={(e) => {
636 - e.preventDefault();
637 - e.stopPropagation();
638 - const button = e.currentTarget;
639 - const rect = button.getBoundingClientRect();
640 -
641 - // Calculate dropdown height (estimate based on number of actions)
642 - const dropdownHeight =
643 - actions.filter((a) => !a.condition || a.condition(item))
644 - .length *
645 - 40 +
646 - 16;
647 - const spaceBelow = window.innerHeight - rect.bottom;
648 - const spaceAbove = rect.top;
649 -
650 - // Position dropdown above if not enough space below
651 - const shouldPositionAbove =
652 - spaceBelow < dropdownHeight && spaceAbove > dropdownHeight;
653 -
654 - setDropdownPosition({
655 - top: shouldPositionAbove
656 - ? rect.top + window.scrollY - dropdownHeight
657 - : rect.bottom + window.scrollY,
658 - right: window.innerWidth - rect.right + window.scrollX,
659 - });
660 - setOpenDropdownId(openDropdownId === itemId ? null : itemId);
661 - }}
638 + onClick={(e) => openActionsMenu(e, item)}
662 639 className="h-8 w-8 hover:bg-gray-100 dark:hover:bg-gray-700"
663 640 aria-label={__("More actions", "yatra")}
664 641 data-dropdown-trigger
665 642 >
@@ -664,48 +641,9 @@
664 641 data-dropdown-trigger
665 642 >
666 643 <MoreVertical className="w-4 h-4" />
667 644 </Button>
668 -
669 - {openDropdownId === itemId && dropdownPosition && (
670 - <div
671 - className="fixed min-w-[180px] w-max bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-md shadow-2xl py-1"
672 - style={{
673 - top: `${dropdownPosition.top}px`,
674 - right: `${dropdownPosition.right}px`,
675 - zIndex: 999999,
676 - }}
677 - data-dropdown-content
678 - onClick={(e) => e.stopPropagation()}
679 - >
680 - {actions
681 - .filter(
682 - (action) => !action.condition || action.condition(item),
683 - )
684 - .map((action) => (
685 - <button
686 - key={action.key}
687 - type="button"
688 - onClick={(e) => {
689 - e.preventDefault();
690 - e.stopPropagation();
691 - action.onClick(item);
692 - setOpenDropdownId(null);
693 - }}
694 - className={`w-full px-4 py-2 text-left text-sm hover:bg-gray-100 dark:hover:bg-gray-700 flex items-center gap-3 transition-colors cursor-pointer whitespace-nowrap ${
695 - action.variant === "destructive"
696 - ? "text-red-600 dark:text-red-400"
697 - : action.variant === "outline"
698 - ? "text-blue-600 dark:text-blue-400"
699 - : "text-gray-700 dark:text-gray-300"
700 - }`}
701 - >
702 - {action.icon}
703 - {action.label}
704 - </button>
705 - ))}
706 - </div>
707 - )}
645 + {renderActionsDropdown(item)}
708 646 </div>
709 647 </TableCell>
710 648 )}
711 649 </TableRow>