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
yatra / resources / js / components / shared / BulkActionToolbar.tsx

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

346 lines 12.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import React, { useState } from "react";
2 import { Button } from "../ui/button";
3 import { Select } from "../ui/select";
4 import { Columns } from "lucide-react";
5 import { __ } from "../../lib/i18n";
6 import { ConfirmationDialog } from "../ui/confirmation-dialog";
7
8 interface BulkActionOption {
9 value: string;
10 label: string;
11 }
12
13 interface StatusFilterOption {
14 key: string;
15 label: string;
16 count: number;
17 }
18
19 interface ColumnVisibilityOption {
20 key: string;
21 label: string;
22 visible: boolean;
23 }
24
25 interface BulkActionToolbarProps {
26 selectedIds: (string | number)[];
27 bulkAction: string;
28 setBulkAction: (action: string) => void;
29 onApply: () => void;
30 onClearSelection: () => void;
31 statusFilter: string;
32 setStatusFilter: (filter: string) => void;
33 statusOptions: StatusFilterOption[];
34 showColumnsDropdown: boolean;
35 setShowColumnsDropdown: (show: boolean) => void;
36 columnOptions: ColumnVisibilityOption[];
37 onToggleColumn: (columnKey: string) => void;
38 bulkMutationPending: boolean;
39 totalItems: number;
40 bulkActionOptions: BulkActionOption[];
41 }
42
43 export const BulkActionToolbar: React.FC<BulkActionToolbarProps> = ({
44 selectedIds,
45 bulkAction,
46 setBulkAction,
47 onApply,
48 onClearSelection,
49 statusFilter,
50 setStatusFilter,
51 statusOptions,
52 showColumnsDropdown,
53 setShowColumnsDropdown,
54 columnOptions,
55 onToggleColumn,
56 bulkMutationPending,
57 totalItems,
58 bulkActionOptions,
59 }) => {
60 const [confirmDialog, setConfirmDialog] = useState<{
61 isOpen: boolean;
62 action: string;
63 count: number;
64 }>({
65 isOpen: false,
66 action: "",
67 count: 0,
68 });
69
70 const handleApplyClick = () => {
71 if (!bulkAction || selectedIds.length === 0) {
72 onApply(); // Let parent handle validation messages
73 return;
74 }
75
76 // Show confirmation dialog
77 setConfirmDialog({
78 isOpen: true,
79 action: bulkAction,
80 count: selectedIds.length,
81 });
82 };
83
84 const handleConfirm = () => {
85 setConfirmDialog({ isOpen: false, action: "", count: 0 });
86 onApply(); // Execute the actual bulk action
87 setBulkAction(""); // Reset bulk action selection
88 };
89
90 const handleCancel = () => {
91 setConfirmDialog({ isOpen: false, action: "", count: 0 });
92 };
93
94 // Close dropdown when clicking outside
95 React.useEffect(() => {
96 const handleClickOutside = (event: MouseEvent) => {
97 const target = event.target as HTMLElement;
98 if (
99 target.closest("[data-columns-trigger]") ||
100 target.closest("[data-columns-content]")
101 ) {
102 return;
103 }
104 setShowColumnsDropdown(false);
105 };
106
107 if (showColumnsDropdown) {
108 document.addEventListener("click", handleClickOutside);
109 return () => document.removeEventListener("click", handleClickOutside);
110 }
111 }, [showColumnsDropdown, setShowColumnsDropdown]);
112
113 return (
114 <>
115 {/* Bulk actions toolbar - Always above the table */}
116 <div className="py-3 space-y-3">
117 {/* Top row: Bulk actions and status tabs */}
118 <div className="flex items-center justify-between gap-4">
119 {/* Left: Bulk actions - all in one line */}
120 <div className="flex items-center gap-3 flex-nowrap">
121 <Select
122 value={bulkAction}
123 onChange={(e) => setBulkAction(e.target.value)}
124 className="min-w-[140px]"
125 disabled={totalItems === 0}
126 >
127 <option value="">{__("Bulk actions", "yatra")}</option>
128 {bulkActionOptions.map((option) => (
129 <option key={option.value} value={option.value}>
130 {option.label}
131 </option>
132 ))}
133 </Select>
134 <Button
135 variant="outline"
136 onClick={handleApplyClick}
137 disabled={bulkMutationPending || selectedIds.length === 0}
138 className="h-11 px-4 flex-shrink-0"
139 >
140 {__("Apply", "yatra")}
141 </Button>
142
143 {/* Selection info right after Apply button */}
144 {selectedIds.length > 0 && (
145 <div className="flex items-center gap-2 text-sm text-gray-600 dark:text-gray-400 bg-blue-50 dark:bg-blue-900/20 px-2 py-1 rounded border border-blue-200 dark:border-blue-800 whitespace-nowrap">
146 <span className="font-medium text-xs">{`${__("Selected:", "yatra")} ${selectedIds.length}`}</span>
147 <button
148 type="button"
149 className="text-xs text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300 font-medium"
150 onClick={onClearSelection}
151 >
152 {__("Clear", "yatra")}
153 </button>
154 </div>
155 )}
156 </div>
157
158 {/* Right: Status filter tabs */}
159 <div className="flex items-center gap-1 flex-shrink-0">
160 {statusOptions.map((filter) => {
161 const isActive = statusFilter === filter.key;
162 return (
163 <button
164 key={filter.key}
165 type="button"
166 className={`px-3 py-2.5 text-sm font-medium rounded border transition-all duration-200 ${
167 isActive
168 ? "bg-gray-900 text-white border-gray-900 shadow-sm"
169 : "bg-white dark:bg-gray-800 text-gray-600 dark:text-gray-300 border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700 hover:border-gray-400 dark:border-gray-500"
170 }`}
171 onClick={() => {
172 setStatusFilter(filter.key);
173 onClearSelection();
174 setBulkAction("");
175 }}
176 >
177 <span className="flex items-center gap-2">
178 {filter.label}
179 <span
180 className={`px-1.5 py-0.5 text-xs rounded-full font-medium ${
181 isActive
182 ? "bg-white text-gray-900"
183 : "bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-400"
184 }`}
185 >
186 {filter.count ?? 0}
187 </span>
188 </span>
189 </button>
190 );
191 })}
192
193 {/* Columns visibility toggle - only when there are column options */}
194 {columnOptions.length > 0 && (
195 <div className="relative ml-2">
196 <Button
197 variant="outline"
198 size="sm"
199 onClick={(e) => {
200 e.preventDefault();
201 e.stopPropagation();
202 setShowColumnsDropdown(!showColumnsDropdown);
203 }}
204 className="h-10 px-3 flex items-center gap-2"
205 data-columns-trigger
206 >
207 <Columns className="w-4 h-4" />
208 {__("Columns", "yatra")}
209 </Button>
210
211 {/* Columns dropdown */}
212 {showColumnsDropdown && (
213 <div
214 className="absolute right-0 top-11 z-[9999] bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-md shadow-lg py-2"
215 style={{ width: "240px", minWidth: "240px" }}
216 data-columns-content
217 onClick={(e) => e.stopPropagation()}
218 >
219 <div className="px-4 py-2.5 text-sm font-medium text-gray-900 dark:text-white border-b border-gray-200 dark:border-gray-700">
220 {__("Show Columns", "yatra")}
221 </div>
222
223 <div className="py-1">
224 {columnOptions.map((option) => (
225 <label
226 key={option.key}
227 className="flex items-center px-4 py-3 hover:bg-gray-50 dark:hover:bg-gray-700 cursor-pointer whitespace-nowrap"
228 >
229 <input
230 type="checkbox"
231 checked={option.visible}
232 onChange={() => onToggleColumn(option.key)}
233 className="rounded border-gray-300 dark:border-gray-600 h-4 w-4 mr-3 text-blue-600 focus:ring-blue-500"
234 />
235 <span className="text-sm text-gray-700 dark:text-gray-300 ml-2">
236 {option.label}
237 </span>
238 </label>
239 ))}
240 </div>
241 </div>
242 )}
243 </div>
244 )}
245 </div>
246 </div>
247 </div>
248
249 {/* Bulk Action Confirmation Dialog */}
250 <ConfirmationDialog
251 isOpen={confirmDialog.isOpen}
252 onClose={handleCancel}
253 onConfirm={handleConfirm}
254 title={(() => {
255 switch (confirmDialog.action) {
256 case "delete":
257 return __("Delete Permanently", "yatra");
258 case "mark_trash":
259 case "trash":
260 return __("Move to Trash", "yatra");
261 case "mark_publish":
262 case "publish":
263 return __("Publish Items", "yatra");
264 case "mark_draft":
265 case "draft":
266 return __("Make Draft", "yatra");
267 case "mark_archived":
268 return __("Archive Items", "yatra");
269 case "restore":
270 return __("Restore Items", "yatra");
271 default:
272 return __("Confirm Bulk Action", "yatra");
273 }
274 })()}
275 message={(() => {
276 const count = confirmDialog.count;
277 switch (confirmDialog.action) {
278 case "delete":
279 return __(
280 "Are you sure you want to permanently delete {count} item(s)? This action cannot be undone.",
281 "yatra",
282 ).replace("{count}", count.toString());
283 case "mark_trash":
284 case "trash":
285 return __(
286 "Are you sure you want to move {count} item(s) to trash?",
287 "yatra",
288 ).replace("{count}", count.toString());
289 case "mark_publish":
290 case "publish":
291 return __(
292 "Are you sure you want to publish {count} item(s)?",
293 "yatra",
294 ).replace("{count}", count.toString());
295 case "mark_draft":
296 case "draft":
297 return __(
298 "Are you sure you want to make {count} item(s) draft?",
299 "yatra",
300 ).replace("{count}", count.toString());
301 case "mark_archived":
302 return __(
303 "Are you sure you want to archive {count} item(s)?",
304 "yatra",
305 ).replace("{count}", count.toString());
306 case "restore":
307 return __(
308 "Are you sure you want to restore {count} item(s)?",
309 "yatra",
310 ).replace("{count}", count.toString());
311 default:
312 return __(
313 "Are you sure you want to perform this action on {count} item(s)?",
314 "yatra",
315 ).replace("{count}", count.toString());
316 }
317 })()}
318 confirmText={(() => {
319 switch (confirmDialog.action) {
320 case "delete":
321 return __("Delete Permanently", "yatra");
322 case "mark_trash":
323 case "trash":
324 return __("Move to Trash", "yatra");
325 case "mark_publish":
326 case "publish":
327 return __("Publish", "yatra");
328 case "mark_draft":
329 case "draft":
330 return __("Make Draft", "yatra");
331 case "mark_archived":
332 return __("Archive", "yatra");
333 case "restore":
334 return __("Restore", "yatra");
335 default:
336 return __("Confirm", "yatra");
337 }
338 })()}
339 cancelText={__("Cancel", "yatra")}
340 variant={confirmDialog.action === "delete" ? "danger" : "warning"}
341 isLoading={bulkMutationPending}
342 />
343 </>
344 );
345 };
346