PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.10
Yatra – Travel Booking & Tour Operator Software v3.0.10
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 2.0.10 All 83 releases
yatra / resources / js / components / shared / Table.tsx

Table.tsx in Yatra – Travel Booking & Tour Operator Software 3.0.10, at resources/js/components/shared/Table.tsx

717 lines 24.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import React, { useMemo, useState } from "react";
2 import { createPortal } from "react-dom";
3 import {
4 Table as UITable,
5 TableBody,
6 TableCell,
7 TableHead,
8 TableHeader,
9 TableRow,
10 } from "../ui/table";
11 import { Button } from "../ui/button";
12 import {
13 MoreVertical,
14 ChevronRight,
15 ChevronDown,
16 Copy,
17 Check,
18 } from "lucide-react";
19 import { __ } from "../../lib/i18n";
20 import { ConditionalRender } from "../ui/conditional-render";
21 import { ErrorRequestInfo } from "../../lib/errors";
22
23 interface TableColumn {
24 key: string;
25 label: string;
26 sortable?: boolean;
27 visible?: boolean;
28 width?: string;
29 render?: (item: any, index: number) => React.ReactNode;
30 }
31
32 interface TableAction {
33 key: string;
34 label: string;
35 icon: React.ReactNode;
36 onClick: (item: any) => void;
37 condition?: (item: any) => boolean;
38 variant?: "default" | "destructive" | "outline";
39 }
40
41 interface TableProps {
42 data: any[];
43 columns: TableColumn[];
44 actions?: TableAction[];
45 isLoading?: boolean;
46 isError?: boolean;
47 errorText?: string;
48 emptyText?: string;
49 emptyDescription?: string;
50 onCreateClick?: () => void;
51 onSort?: (field: string) => void;
52 getSortIcon?: (field: string) => React.ReactNode;
53 selectedItemIds?: (string | number)[];
54 onSelectItem?: (id: string | number, checked: boolean) => void;
55 onSelectAll?: (checked: boolean) => void;
56 isAllSelected?: boolean;
57 getItemId?: (item: any) => string | number;
58 getItemStatus?: (item: any) => string;
59 statusFilter?: string;
60 capability?: string;
61 skeletonRows?: number;
62 errorDescription?: string;
63 onRetry?: () => void;
64 errorDetails?: string;
65 errorRequestInfo?: ErrorRequestInfo;
66 // Hierarchical support
67 isHierarchical?: boolean;
68 expandedIds?: Set<string | number>;
69 onToggleExpand?: (id: string | number) => void;
70 getChildren?: (item: any) => any[];
71 renderRowContent?: (
72 item: any,
73 index: number,
74 isChild?: boolean,
75 ) => React.ReactNode[];
76 }
77
78 export const Table: React.FC<TableProps> = ({
79 data,
80 columns,
81 actions = [],
82 isLoading = false,
83 isError = false,
84 errorText = __("Error loading data", "yatra"),
85 emptyText = __("No items found", "yatra"),
86 emptyDescription = __("Get started by creating your first item.", "yatra"),
87 onCreateClick,
88 onSort,
89 getSortIcon,
90 selectedItemIds = [],
91 onSelectItem,
92 onSelectAll,
93 isAllSelected = false,
94 getItemId = (item) => item.id,
95 getItemStatus = (item) => item.status,
96 statusFilter = "",
97 capability = "yatra_view_trips",
98 skeletonRows = 5,
99 errorDescription = __(
100 "Something went wrong while loading this data. Please try again in a moment.",
101 "yatra",
102 ),
103 onRetry,
104 errorDetails,
105 errorRequestInfo,
106 // Hierarchical props
107 isHierarchical = false,
108 expandedIds = new Set(),
109 onToggleExpand,
110 getChildren,
111 renderRowContent,
112 }) => {
113 const [openDropdownId, setOpenDropdownId] = useState<string | number | null>(
114 null,
115 );
116 const [dropdownPosition, setDropdownPosition] = useState<{
117 top: number;
118 right: number;
119 } | null>(null);
120 const [copied, setCopied] = useState(false);
121
122 const handleRetry = () => {
123 if (onRetry) {
124 onRetry();
125 return;
126 }
127 if (typeof window !== "undefined") {
128 window.location.reload();
129 }
130 };
131
132 // Close dropdown when clicking outside
133 React.useEffect(() => {
134 const handleClickOutside = (event: MouseEvent) => {
135 const target = event.target as HTMLElement;
136 if (
137 target.closest("[data-dropdown-trigger]") ||
138 target.closest("[data-dropdown-content]")
139 ) {
140 return;
141 }
142 setOpenDropdownId(null);
143 };
144
145 if (openDropdownId !== null) {
146 document.addEventListener("click", handleClickOutside);
147 return () => document.removeEventListener("click", handleClickOutside);
148 }
149 }, [openDropdownId]);
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
221 // Render skeleton loading state
222 const renderSkeleton = () => (
223 <UITable>
224 <TableHeader>
225 <TableRow>
226 {isHierarchical && <TableHead className="w-12"></TableHead>}
227 {onSelectItem && onSelectAll && (
228 <TableHead className="w-12"></TableHead>
229 )}
230 {columns
231 .filter((col) => col.visible !== false)
232 .map((column) => (
233 <TableHead key={column.key} className={column.width}>
234 {column.label}
235 </TableHead>
236 ))}
237 {actions.length > 0 && (
238 <TableHead className="text-right w-[100px]">
239 {__("Actions", "yatra")}
240 </TableHead>
241 )}
242 </TableRow>
243 </TableHeader>
244 <TableBody>
245 {[...Array(skeletonRows)].map((_, index) => (
246 <TableRow key={`skeleton-${index}`}>
247 {isHierarchical && (
248 <TableCell>
249 <div className="w-4 h-4 bg-gray-100 dark:bg-gray-800 rounded animate-pulse" />
250 </TableCell>
251 )}
252 {onSelectItem && onSelectAll && (
253 <TableCell>
254 <div className="w-4 h-4 bg-gray-100 dark:bg-gray-800 rounded animate-pulse" />
255 </TableCell>
256 )}
257 {columns
258 .filter((col) => col.visible !== false)
259 .map((column) => (
260 <TableCell key={`${column.key}-${index}`}>
261 <div className="h-4 bg-gray-100 dark:bg-gray-800 rounded animate-pulse" />
262 </TableCell>
263 ))}
264 {actions.length > 0 && (
265 <TableCell className="text-right">
266 <div className="flex items-center justify-end gap-2">
267 <div className="h-4 w-4 bg-gray-100 dark:bg-gray-800 rounded animate-pulse" />
268 </div>
269 </TableCell>
270 )}
271 </TableRow>
272 ))}
273 </TableBody>
274 </UITable>
275 );
276
277 // Render error state
278 const composedErrorText = useMemo(() => {
279 const parts: string[] = [];
280 if (errorRequestInfo) {
281 if (errorRequestInfo.method) {
282 parts.push(`Request Method: ${errorRequestInfo.method}`);
283 }
284 if (errorRequestInfo.url) {
285 parts.push(`Request URL: ${errorRequestInfo.url}`);
286 }
287 if (errorRequestInfo.payload) {
288 parts.push(`Request Payload:\n${errorRequestInfo.payload}`);
289 }
290 }
291 if (errorDetails) {
292 parts.push(`Response:\n${errorDetails}`);
293 }
294 return parts.join("\n\n").trim();
295 }, [errorRequestInfo, errorDetails]);
296
297 const renderError = () => (
298 <div className="relative flex flex-col items-center justify-center text-center py-14 px-6 my-4 min-h-[360px]">
299 <div className="absolute inset-4 rounded-2xl border-2 border-dashed border-red-200/60 dark:border-red-900/50 bg-gradient-to-br from-red-50 via-orange-50/60 to-white dark:from-red-900/40 dark:via-orange-900/10 dark:to-gray-900 shadow-sm" />
300 <div className="relative z-10 max-w-lg mx-auto space-y-6">
301 <div className="inline-flex items-center justify-center w-24 h-24 rounded-2xl bg-gradient-to-br from-red-100 via-rose-50 to-orange-100 dark:from-red-900/50 dark:via-rose-800/30 dark:to-orange-900/40 ring-8 ring-red-100/70 dark:ring-red-900/30 shadow-lg">
302 <svg
303 className="w-12 h-12 text-red-500 dark:text-red-300"
304 viewBox="0 0 24 24"
305 fill="none"
306 stroke="currentColor"
307 strokeWidth="1.5"
308 >
309 <path
310 strokeLinecap="round"
311 strokeLinejoin="round"
312 d="M12 9v4m0 4h.01M10.29 3.86L2.82 17a2 2 0 001.71 3h14.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z"
313 />
314 </svg>
315 </div>
316
317 <div className="space-y-3">
318 <h3 className="text-2xl font-bold text-gray-900 dark:text-white">
319 {errorText}
320 </h3>
321 <p className="text-base text-gray-600 dark:text-gray-400 leading-relaxed">
322 {errorDescription}
323 </p>
324 </div>
325
326 <div className="flex flex-col sm:flex-row items-center justify-center gap-3">
327 <Button onClick={handleRetry} className="w-full sm:w-auto px-6">
328 {__("Try again", "yatra")}
329 </Button>
330 <Button
331 variant="outline"
332 onClick={() => {
333 if (typeof window !== "undefined") {
334 window.open(
335 "https://wordpress.org/support/plugin/yatra",
336 "_blank",
337 );
338 }
339 }}
340 className="w-full sm:w-auto px-6 text-gray-700 dark:text-gray-200 border-gray-200 dark:border-gray-700"
341 >
342 {__("Visit support center", "yatra")}
343 </Button>
344 </div>
345
346 {(errorDetails || errorRequestInfo) && (
347 <div className="relative w-full text-left rounded-2xl border border-red-100/80 dark:border-red-900/40 bg-white/90 dark:bg-gray-900/70 shadow-inner space-y-0">
348 <div className="flex items-center justify-between px-4 py-3 border-b border-red-50 dark:border-red-900/30">
349 <p className="text-sm font-medium text-gray-700 dark:text-gray-300">
350 {__("Technical details", "yatra")}
351 </p>
352 <Button
353 type="button"
354 size="sm"
355 variant="ghost"
356 className="flex items-center gap-2 text-gray-600 dark:text-gray-300 hover:text-gray-900"
357 onClick={() => {
358 if (!navigator?.clipboard || !composedErrorText) {
359 return;
360 }
361 navigator.clipboard
362 .writeText(composedErrorText)
363 .then(() => {
364 setCopied(true);
365 setTimeout(() => setCopied(false), 2000);
366 })
367 .catch(() => {});
368 }}
369 >
370 {copied ? (
371 <>
372 <Check className="w-4 h-4" />
373 {__("Copied", "yatra")}
374 </>
375 ) : (
376 <>
377 <Copy className="w-4 h-4" />
378 {__("Copy details", "yatra")}
379 </>
380 )}
381 </Button>
382 </div>
383 {errorRequestInfo && (
384 <div className="px-4 py-3 border-b border-red-50 dark:border-red-900/20 space-y-2 text-sm text-left text-gray-700 dark:text-gray-200">
385 {errorRequestInfo.method && (
386 <div>
387 <span className="font-medium">
388 {__("Method:", "yatra")}
389 </span>{" "}
390 <span className="font-mono">{errorRequestInfo.method}</span>
391 </div>
392 )}
393 {errorRequestInfo.url && (
394 <div className="break-all">
395 <span className="font-medium">{__("URL:", "yatra")}</span>{" "}
396 <span className="font-mono">{errorRequestInfo.url}</span>
397 </div>
398 )}
399 {errorRequestInfo.payload && (
400 <div>
401 <span className="font-medium block mb-1">
402 {__("Payload:", "yatra")}
403 </span>
404 <pre className="max-h-40 overflow-auto px-3 py-2 rounded bg-red-50/60 dark:bg-red-900/30 text-xs font-mono text-gray-800 dark:text-gray-100 whitespace-pre-wrap">
405 {errorRequestInfo.payload}
406 </pre>
407 </div>
408 )}
409 </div>
410 )}
411 {errorDetails && (
412 <pre className="max-h-56 overflow-auto px-4 py-3 text-xs leading-relaxed font-mono text-gray-700 dark:text-gray-200 whitespace-pre-wrap">
413 {errorDetails}
414 </pre>
415 )}
416 </div>
417 )}
418 </div>
419 </div>
420 );
421
422 // Render empty state
423 const renderEmpty = () => (
424 <div className="relative flex flex-col items-center justify-center text-center py-12 px-6 my-4 min-h-[350px]">
425 {/* Background decoration */}
426 <div className="absolute inset-4 bg-gradient-to-br from-gray-50/80 via-blue-50/30 to-white dark:from-gray-900/80 dark:via-blue-900/20 dark:to-gray-800/80 rounded-2xl border-2 border-dashed border-gray-300/50 dark:border-gray-600/50 shadow-sm"></div>
427
428 {/* Content */}
429 <div className="relative z-10 max-w-lg mx-auto space-y-8">
430 {/* Icon */}
431 <div className="inline-flex items-center justify-center w-24 h-24 rounded-2xl bg-gradient-to-br from-blue-100 via-blue-50 to-indigo-100 dark:from-blue-900/40 dark:via-blue-800/20 dark:to-indigo-900/40 ring-8 ring-blue-100/60 dark:ring-blue-900/30 shadow-lg">
432 <svg
433 className="w-12 h-12 text-blue-600 dark:text-blue-400"
434 fill="none"
435 stroke="currentColor"
436 viewBox="0 0 24 24"
437 strokeWidth="1.5"
438 >
439 <path
440 strokeLinecap="round"
441 strokeLinejoin="round"
442 d="M7 7h10v10H7zM5 3h14a2 2 0 012 2v14a2 2 0 01-2 2H5a2 2 0 01-2-2V5a2 2 0 012-2z"
443 />
444 <path
445 strokeLinecap="round"
446 strokeLinejoin="round"
447 d="M9 9h6M9 12h6M9 15h4"
448 />
449 </svg>
450 </div>
451
452 {/* Text content */}
453 <div className="space-y-4">
454 <h3 className="text-2xl font-bold text-gray-900 dark:text-white">
455 {emptyText}
456 </h3>
457 <p className="text-base text-gray-600 dark:text-gray-400 leading-relaxed max-w-md mx-auto">
458 {emptyDescription}
459 </p>
460 </div>
461
462 {/* Action button */}
463 {onCreateClick && (
464 <div className="pt-4">
465 <Button
466 onClick={onCreateClick}
467 className="inline-flex items-center gap-3 px-8 py-3 bg-blue-600 hover:bg-blue-700 text-white text-base font-semibold rounded-xl shadow-lg hover:shadow-xl transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-gray-900 transform hover:scale-[1.02]"
468 >
469 <svg
470 className="w-4 h-4"
471 fill="none"
472 stroke="currentColor"
473 viewBox="0 0 24 24"
474 strokeWidth="2"
475 >
476 <path
477 strokeLinecap="round"
478 strokeLinejoin="round"
479 d="M12 4v16m8-8H4"
480 />
481 </svg>
482 {__("Create New", "yatra")}
483 </Button>
484 </div>
485 )}
486 </div>
487 </div>
488 );
489
490 // Render hierarchical row with children
491 const renderHierarchicalRow = (
492 item: any,
493 index: number,
494 isChild = false,
495 ): React.ReactNode => {
496 const itemId = getItemId(item);
497 const itemStatus = getItemStatus(item);
498 const isTrash = itemStatus === "trash" || statusFilter === "trash";
499 const children = getChildren ? getChildren(item) : [];
500 const hasChildren = children.length > 0;
501 const isExpanded = expandedIds.has(itemId);
502
503 return (
504 <React.Fragment key={itemId}>
505 <TableRow
506 className={`${isTrash ? "bg-red-50/30 dark:bg-red-900/10 opacity-75 hover:bg-red-50/50 dark:hover:bg-red-900/20" : ""} ${isChild ? "bg-gray-50 dark:bg-gray-900/50" : ""}`}
507 >
508 {/* Expand/collapse column */}
509 <TableCell className="w-12">
510 {hasChildren && onToggleExpand && (
511 <button
512 onClick={() => onToggleExpand(itemId)}
513 className="p-1 hover:bg-gray-200 dark:hover:bg-gray-700 rounded"
514 >
515 {isExpanded ? (
516 <ChevronDown className="w-4 h-4" />
517 ) : (
518 <ChevronRight className="w-4 h-4" />
519 )}
520 </button>
521 )}
522 </TableCell>
523
524 {/* Selection checkbox */}
525 {onSelectItem && (
526 <TableCell>
527 <input
528 type="checkbox"
529 className="rounded border-gray-300 dark:border-gray-600"
530 checked={selectedItemIds.includes(itemId)}
531 onChange={(e) => onSelectItem(itemId, e.target.checked)}
532 aria-label={__("Select item", "yatra")}
533 />
534 </TableCell>
535 )}
536
537 {/* Custom row content or default columns */}
538 {renderRowContent
539 ? renderRowContent(item, index, isChild).map(
540 (cellContent, cellIndex) => (
541 <TableCell
542 key={`cell-${itemId}-${cellIndex}`}
543 className={
544 isTrash ? "text-gray-400 dark:text-gray-600" : ""
545 }
546 >
547 {cellContent}
548 </TableCell>
549 ),
550 )
551 : columns
552 .filter((col) => col.visible !== false)
553 .map((column) => (
554 <TableCell
555 key={`${column.key}-${itemId}`}
556 className={
557 isTrash ? "text-gray-400 dark:text-gray-600" : ""
558 }
559 >
560 {column.render
561 ? column.render(item, index)
562 : item[column.key]}
563 </TableCell>
564 ))}
565
566 {/* Actions */}
567 {actions.length > 0 && (
568 <TableCell className="text-right">
569 <div className="relative inline-block">
570 <Button
571 variant="ghost"
572 size="icon"
573 onClick={(e) => openActionsMenu(e, item)}
574 className="h-8 w-8 hover:bg-gray-100 dark:hover:bg-gray-700"
575 aria-label={__("More actions", "yatra")}
576 data-dropdown-trigger
577 >
578 <MoreVertical className="w-4 h-4" />
579 </Button>
580 {renderActionsDropdown(item)}
581 </div>
582 </TableCell>
583 )}
584 </TableRow>
585
586 {/* Render children if expanded */}
587 {hasChildren &&
588 isExpanded &&
589 children.map((child, childIndex) =>
590 renderHierarchicalRow(child, childIndex, true),
591 )}
592 </React.Fragment>
593 );
594 };
595
596 // Render flat row (original behavior)
597 const renderFlatRow = (item: any, index: number): React.ReactNode => {
598 const itemId = getItemId(item);
599 const itemStatus = getItemStatus(item);
600 const isTrash = itemStatus === "trash" || statusFilter === "trash";
601
602 return (
603 <TableRow
604 key={itemId}
605 className={
606 isTrash
607 ? "bg-red-50/30 dark:bg-red-900/10 opacity-75 hover:bg-red-50/50 dark:hover:bg-red-900/20"
608 : ""
609 }
610 >
611 {onSelectItem && (
612 <TableCell>
613 <input
614 type="checkbox"
615 className="rounded border-gray-300 dark:border-gray-600"
616 checked={selectedItemIds.includes(itemId)}
617 onChange={(e) => onSelectItem(itemId, e.target.checked)}
618 aria-label={__("Select item", "yatra")}
619 />
620 </TableCell>
621 )}
622 {columns
623 .filter((col) => col.visible !== false)
624 .map((column) => (
625 <TableCell
626 key={`${column.key}-${itemId}`}
627 className={isTrash ? "text-gray-400 dark:text-gray-600" : ""}
628 >
629 {column.render ? column.render(item, index) : item[column.key]}
630 </TableCell>
631 ))}
632 {actions.length > 0 && (
633 <TableCell className="text-right">
634 <div className="relative inline-block">
635 <Button
636 variant="ghost"
637 size="icon"
638 onClick={(e) => openActionsMenu(e, item)}
639 className="h-8 w-8 hover:bg-gray-100 dark:hover:bg-gray-700"
640 aria-label={__("More actions", "yatra")}
641 data-dropdown-trigger
642 >
643 <MoreVertical className="w-4 h-4" />
644 </Button>
645 {renderActionsDropdown(item)}
646 </div>
647 </TableCell>
648 )}
649 </TableRow>
650 );
651 };
652
653 // Render table data
654 const renderTable = () => {
655 if (isLoading) return renderSkeleton();
656 if (isError) return renderError();
657 if (data.length === 0) return renderEmpty();
658
659 return (
660 <UITable>
661 <TableHeader>
662 <TableRow>
663 {/* Expand/collapse column for hierarchical tables */}
664 {isHierarchical && <TableHead className="w-12"></TableHead>}
665 {onSelectItem && onSelectAll && (
666 <TableHead className="w-12">
667 <input
668 type="checkbox"
669 className="rounded border-gray-300 dark:border-gray-600"
670 checked={isAllSelected}
671 onChange={(e) => onSelectAll(e.target.checked)}
672 aria-label={__("Select all items", "yatra")}
673 />
674 </TableHead>
675 )}
676 {columns
677 .filter((col) => col.visible !== false)
678 .map((column) => (
679 <TableHead key={column.key} className={column.width}>
680 {column.sortable && onSort ? (
681 <button
682 onClick={() => onSort(column.key)}
683 className="flex items-center hover:text-gray-900 dark:hover:text-white transition-colors"
684 >
685 {column.label}
686 {getSortIcon && getSortIcon(column.key)}
687 </button>
688 ) : (
689 column.label
690 )}
691 </TableHead>
692 ))}
693 {actions.length > 0 && (
694 <TableHead className="text-right w-[100px]">
695 {__("Actions", "yatra")}
696 </TableHead>
697 )}
698 </TableRow>
699 </TableHeader>
700 <TableBody>
701 {data.map((item, index) =>
702 isHierarchical
703 ? renderHierarchicalRow(item, index)
704 : renderFlatRow(item, index),
705 )}
706 </TableBody>
707 </UITable>
708 );
709 };
710
711 return (
712 <ConditionalRender capability={capability}>
713 {renderTable()}
714 </ConditionalRender>
715 );
716 };
717