PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.17
Yatra – Travel Booking & Tour Operator Software v3.0.17
3.0.17 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 All 85 releases
← All changes | resources/js/pages/EmailTemplateForm.tsx +497 -7 3.0.10 → 3.0.17 View file →
@@ -1,7 +1,22 @@
1 1 import React, { useState, useEffect, useMemo } from "react";
2 2 import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
3 -import { __ } from "../lib/i18n";
3 +import { __, sprintf } from "../lib/i18n";
4 +import { MultiSelect } from "../components/ui/multi-select";
5 +import { SearchableSelect } from "../components/ui/searchable-select";
6 +import {
7 + decodeTargets,
8 + describeTargets,
9 + encodeTargets,
10 + hasTargets,
11 + useTripTargets,
12 + type TripTargets,
13 +} from "../hooks/useTripTargets";
14 +import { EmailOverrideCreateModal } from "../components/email/EmailOverrideCreateModal";
15 +import {
16 + fetchEmailTemplates,
17 + resolveEmailTemplatesForTrip,
18 +} from "../api/email-automation-api";
4 19 import { useToast } from "../components/ui/toast";
5 20 import {
6 21 Card,
7 22 CardContent,
@@ -76,8 +91,18 @@
76 91 };
77 92 }, []);
78 93
79 94 const isCoreSettingsEdit = isCoreTemplateSlug(coreTemplateSlug);
95 + // Per-template BCC/CC settings keys are derived from the template's subject
96 + // key (email_tpl_booking_subject -> _bcc / _cc), exactly as the PHP side does,
97 + // so the catalog needs no extra entries and a new template type is covered
98 + // automatically.
99 + const coreAddressKey = (
100 + subjectKey: string | undefined,
101 + suffix: "bcc" | "cc",
102 + ): string | null =>
103 + subjectKey ? subjectKey.replace(/_subject$/, "") + "_" + suffix : null;
104 +
80 105 const coreDef = isCoreSettingsEdit
81 106 ? getCoreTemplateDefinition(coreTemplateSlug)
82 107 : undefined;
83 108
@@ -121,13 +146,21 @@
121 146 from_name: "",
122 147 from_email: "",
123 148 to_email: "",
124 149 reply_to: "",
150 + bcc: "",
151 + cc: "",
152 + category: "booking",
125 153 subject: "",
126 154 body: "",
127 155 event_key: "",
128 156 is_active: true,
129 157 });
158 + // Trip-specific overrides (Pro Email Automation). `targets` is only sent
159 + // for override rows; `previewTrip` feeds preview / test with a real trip.
160 + const [targets, setTargets] = useState<TripTargets>({});
161 + const [previewTrip, setPreviewTrip] = useState<number | null>(null);
162 + const [overrideModalOpen, setOverrideModalOpen] = useState(false);
130 163 const [showPreview, setShowPreview] = useState(false);
131 164 const [previewData, setPreviewData] = useState<PreviewData | null>(null);
132 165 const [testEmail, setTestEmail] = useState("");
133 166 const [copiedVar, setCopiedVar] = useState<string | null>(null);
@@ -162,8 +195,67 @@
162 195 queryFn: () => fetchEmailTemplate(id as string),
163 196 enabled: Boolean(id) && !isCoreSettingsEdit,
164 197 });
165 198
199 + // System templates own their trigger event; everything else may choose one.
200 + const isSystemTemplate = Boolean((templateData as any)?.is_system);
201 + const overridesSupported =
202 + !isCoreSettingsEdit &&
203 + !!(window as any).yatraAdmin?.emailTemplateOverridesEnabled;
204 + const overrideOf = String((templateData as any)?.overrides ?? "");
205 + const isOverride = overridesSupported && overrideOf !== "";
206 + const isOverridableGlobal =
207 + overridesSupported &&
208 + !isOverride &&
209 + Boolean((templateData as any)?.overridable);
210 + // Global list (for the override's parent name, the global's override strip
211 + // and the picker labels). Cached with the Templates tab.
212 + const { data: allTemplatesData } = useQuery({
213 + queryKey: ["email-templates"],
214 + queryFn: () => fetchEmailTemplates(),
215 + enabled: overridesSupported && !isCreateMode,
216 + });
217 + const allTemplates = useMemo(
218 + () => (Array.isArray(allTemplatesData) ? (allTemplatesData as any[]) : []),
219 + [allTemplatesData],
220 + );
221 + const parentTemplate = useMemo(
222 + () => allTemplates.find((t) => t.template_key === overrideOf) || null,
223 + [allTemplates, overrideOf],
224 + );
225 + const myOverrides = useMemo(
226 + () =>
227 + allTemplates
228 + .filter(
229 + (t) =>
230 + t.overrides && t.overrides === (templateData as any)?.template_key,
231 + )
232 + .sort((a, b) => (a.priority || 0) - (b.priority || 0)),
233 + [allTemplates, templateData],
234 + );
235 + const { options: tripTargetOptions } = useTripTargets(
236 + overridesSupported && (isOverride || isOverridableGlobal),
237 + );
238 + const tripOptions = useMemo(
239 + () =>
240 + tripTargetOptions
241 + .filter((o) => String(o.value).startsWith("trip:"))
242 + .map((o) => ({
243 + id: Number(String(o.value).slice(5)),
244 + label: o.label.replace(/^[^:]+: /, ""),
245 + })),
246 + [tripTargetOptions],
247 + );
248 + // "Check a trip": which template the chosen trip would actually get.
249 + const { data: tripResolution } = useQuery({
250 + queryKey: ["email-templates-resolve", previewTrip],
251 + queryFn: () => resolveEmailTemplatesForTrip(previewTrip as number),
252 + enabled:
253 + overridesSupported &&
254 + !!previewTrip &&
255 + (isOverride || isOverridableGlobal),
256 + });
257 +
166 258 // Event-scoped variables: the sidebar re-fetches whenever the
167 259 // operator switches the trigger event so the "Available
168 260 // Variables" panel reflects only tags that will actually
169 261 // resolve at send-time for the chosen event. Empty event_key
@@ -223,13 +315,23 @@
223 315 from_name: String(t.from_name ?? ""),
224 316 from_email: String(t.from_email ?? ""),
225 317 to_email: String(t.to_email ?? ""),
226 318 reply_to: String(t.reply_to ?? ""),
319 + bcc: String(t.bcc ?? ""),
320 + cc: String(t.cc ?? ""),
321 + category: String(t.category ?? "booking"),
227 322 subject: String(t.subject ?? ""),
228 323 body: String(t.body ?? ""),
229 - event_key: String(t.event_key ?? ""),
324 + // An override's own event_key is stored empty; show (and fetch
325 + // variables for) the global template's event instead.
326 + event_key: String(t.effective_event_key || t.event_key || ""),
230 327 is_active: Boolean(t.is_active ?? true),
231 328 });
329 + setTargets(
330 + t.overrides && t.targets && typeof t.targets === "object"
331 + ? (t.targets as TripTargets)
332 + : {},
333 + );
232 334 }, [templateData]);
233 335
234 336 useEffect(() => {
235 337 if (!isCoreSettingsEdit || !settings || !coreDef) return;
@@ -244,8 +346,11 @@
244 346 from_name: "",
245 347 from_email: "",
246 348 to_email: coreDef.to_email || "",
247 349 reply_to: "",
350 + bcc: String(s[coreAddressKey(subj, "bcc") ?? ""] ?? ""),
351 + cc: String(s[coreAddressKey(subj, "cc") ?? ""] ?? ""),
352 + category: String(coreDef.category ?? "booking"),
248 353 subject: String(s[subj] ?? ""),
249 354 body: String(s[bodyKey] ?? ""),
250 355 event_key: coreDef.event_key,
251 356 is_active: Boolean(s[flag]),
@@ -320,8 +425,13 @@
320 425 }
321 426 base[coreDef.settingsSubject] = formData.subject;
322 427 base[coreDef.settingsBody] = formData.body;
323 428 base[coreDef.settingsFlag] = formData.is_active;
429 +
430 + const bccKey = coreAddressKey(coreDef.settingsSubject, "bcc");
431 + const ccKey = coreAddressKey(coreDef.settingsSubject, "cc");
432 + if (bccKey) base[bccKey] = formData.bcc;
433 + if (ccKey) base[ccKey] = formData.cc;
324 434 await saveSettings(base);
325 435 return base;
326 436 },
327 437 onSuccess: (saved) => {
@@ -336,9 +446,9 @@
336 446 });
337 447
338 448 const previewMutation = useMutation({
339 449 mutationFn: async () => {
340 - return previewEmailTemplate(id as string);
450 + return previewEmailTemplate(id as string, previewTrip);
341 451 },
342 452 onSuccess: (data) => {
343 453 setPreviewData(data);
344 454 setShowPreview(true);
@@ -371,9 +481,9 @@
371 481 });
372 482
373 483 const testMutation = useMutation({
374 484 mutationFn: async (email: string) => {
375 - return sendEmailTemplateTest(id as string, email);
485 + return sendEmailTemplateTest(id as string, email, previewTrip);
376 486 },
377 487 onSuccess: () => {
378 488 showToast(__("Test email sent successfully"), "success");
379 489 setTestEmail("");
@@ -401,10 +511,29 @@
401 511 if (!formData.subject.trim()) {
402 512 showToast(__("Subject line is required"), "error");
403 513 return;
404 514 }
515 + if (isOverride && !hasTargets(targets)) {
516 + showToast(
517 + __(
518 + "An override needs at least one trip, category or trip type — or delete it to use the global template.",
519 + "yatra",
520 + ),
521 + "error",
522 + );
523 + return;
524 + }
405 525
406 - saveMutation.mutate(formData);
526 + saveMutation.mutate(
527 + isOverride
528 + ? // event_key is not sent: an override always fires on its global's event.
529 + ({
530 + ...formData,
531 + event_key: undefined,
532 + targets,
533 + } as unknown as typeof formData)
534 + : formData,
535 + );
407 536 };
408 537
409 538 const copyVariable = (variable: string) => {
410 539 const text = `{{${variable}}}`;
@@ -603,8 +732,33 @@
603 732 "Core customer email (saved with site settings). From address is set under Email → Delivery.",
604 733 "yatra",
605 734 )
606 735 : String(templateData?.name ?? "")}
736 + {isOverride && (
737 + <span className="ml-2 inline-flex items-center gap-1 text-xs align-middle">
738 + <span className="px-1.5 py-0.5 rounded font-medium bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-300">
739 + {__("Override", "yatra")}
740 + </span>
741 + <span className="text-gray-400">
742 + {__("of global template", "yatra")}
743 + </span>
744 + {parentTemplate ? (
745 + <a
746 + href={`admin.php?page=yatra&subpage=email-automation&tab=templates&action=edit&id=${parentTemplate.id}`}
747 + className="text-blue-600 dark:text-blue-400 hover:underline font-medium"
748 + >
749 + {parentTemplate.name}
750 + </a>
751 + ) : (
752 + <span>{overrideOf}</span>
753 + )}
754 + </span>
755 + )}
756 + {isOverridableGlobal && (
757 + <span className="ml-2 px-1.5 py-0.5 rounded text-xs font-medium align-middle bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-300">
758 + {__("Global", "yatra")}
759 + </span>
760 + )}
607 761 </p>
608 762 </div>
609 763 </div>
610 764 <div className="flex items-center gap-3">
@@ -666,8 +820,118 @@
666 820
667 821 <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
668 822 {/* Main Form */}
669 823 <div className="lg:col-span-2 space-y-6">
824 + {isOverride && (
825 + <div
826 + className="rounded-lg border border-purple-300 dark:border-purple-800 overflow-hidden"
827 + data-testid="override-applies-to"
828 + >
829 + <div className="px-4 py-2.5 bg-purple-50 dark:bg-purple-900/20 text-sm font-semibold text-purple-800 dark:text-purple-200">
830 + {__("Applies to", "yatra")}{" "}
831 + <span className="font-normal text-purple-600 dark:text-purple-300">
832 + {__(
833 + "— bookings on these trips get this override instead of the global template",
834 + "yatra",
835 + )}
836 + </span>
837 + </div>
838 + <div className="p-4 space-y-3">
839 + <MultiSelect
840 + value={encodeTargets(targets)}
841 + onChange={(vals) => setTargets(decodeTargets(vals))}
842 + options={tripTargetOptions}
843 + placeholder={__(
844 + "Search trips, categories, trip types…",
845 + "yatra",
846 + )}
847 + />
848 + {!hasTargets(targets) && (
849 + <p className="text-xs text-red-600">
850 + {__(
851 + "Pick at least one trip, category or trip type.",
852 + "yatra",
853 + )}
854 + </p>
855 + )}
856 + <ul className="text-xs text-gray-500 dark:text-gray-400 list-disc pl-5 space-y-0.5">
857 + <li>
858 + {__(
859 + "Any match applies. A trip in a sub-category also matches its parent category.",
860 + "yatra",
861 + )}
862 + </li>
863 + <li>
864 + {__(
865 + "When several overrides match one trip, the most specific wins — trip › category › trip type — then the priority order in the Override templates list.",
866 + "yatra",
867 + )}
868 + </li>
869 + <li>
870 + {__(
871 + "Trips that match no override keep the global template. Switching this override off does the same for its trips.",
872 + "yatra",
873 + )}
874 + </li>
875 + </ul>
876 + </div>
877 + </div>
878 + )}
879 + {isOverridableGlobal && (
880 + <div
881 + className="flex flex-wrap items-start gap-3 rounded-lg border border-purple-300 dark:border-purple-800 bg-purple-50 dark:bg-purple-900/20 px-4 py-3 text-sm text-purple-900 dark:text-purple-200"
882 + data-testid="global-overrides-strip"
883 + >
884 + <div className="flex-1">
885 + {myOverrides.length === 0
886 + ? __(
887 + "No overrides yet. Every trip gets this template. Add an override to give some trips different wording.",
888 + "yatra",
889 + )
890 + : sprintf(
891 + __(
892 + "This global template has %d override(s) with their own wording. Changes you make here don't change them; every other trip gets this template.",
893 + "yatra",
894 + ),
895 + myOverrides.length,
896 + )}
897 + {myOverrides.length > 0 && (
898 + <ul className="mt-1 space-y-0.5">
899 + {myOverrides.map((o) => (
900 + <li
901 + key={o.id}
902 + className="flex flex-wrap items-center gap-2"
903 + >
904 + <a
905 + href={`admin.php?page=yatra&subpage=email-automation&tab=templates&action=edit&id=${o.id}`}
906 + className="font-medium text-blue-700 dark:text-blue-300 hover:underline"
907 + >
908 + {o.name}
909 + </a>
910 + <span className="text-xs text-purple-700 dark:text-purple-300">
911 + {describeTargets(o.targets, tripTargetOptions)}
912 + </span>
913 + {!o.is_active && (
914 + <span className="text-xs text-amber-700 dark:text-amber-300">
915 + {__("(off)", "yatra")}
916 + </span>
917 + )}
918 + </li>
919 + ))}
920 + </ul>
921 + )}
922 + </div>
923 + <Button
924 + type="button"
925 + variant="outline"
926 + size="sm"
927 + onClick={() => setOverrideModalOpen(true)}
928 + data-testid="add-override-from-global"
929 + >
930 + {__("+ Add override", "yatra")}
931 + </Button>
932 + </div>
933 + )}
670 934 <Card>
671 935 <CardHeader>
672 936 <CardTitle className="flex items-center justify-between">
673 937 <span>{__("Template Details")}</span>
@@ -728,9 +992,11 @@
728 992 <div>
729 993 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
730 994 {__("Trigger Event")}
731 995 </label>
732 - {Boolean(templateData?.is_system) || isCoreSettingsEdit ? (
996 + {Boolean(templateData?.is_system) ||
997 + isCoreSettingsEdit ||
998 + isOverride ? (
733 999 <div className="space-y-2">
734 1000 <div className="flex items-center gap-2">
735 1001 <span className="inline-flex items-center gap-1.5 px-3 py-2 rounded-lg text-sm font-medium bg-indigo-100 text-indigo-800 dark:bg-indigo-900/30 dark:text-indigo-400 border border-indigo-200 dark:border-indigo-800">
736 1002 <Zap className="w-4 h-4" />
@@ -736,9 +1002,14 @@
736 1002 <Zap className="w-4 h-4" />
737 1003 {formData.event_key || "-"}
738 1004 </span>
739 1005 <span className="text-xs text-gray-500 dark:text-gray-400">
740 - {__("System templates cannot change events")}
1006 + {isOverride
1007 + ? __(
1008 + "Same as the global template — an override can't change when it is sent, only what it says",
1009 + "yatra",
1010 + )
1011 + : __("System templates cannot change events")}
741 1012 </span>
742 1013 </div>
743 1014 {formData.event_key && (
744 1015 <div className="p-3 bg-indigo-50 dark:bg-indigo-900/20 rounded-lg border border-indigo-200 dark:border-indigo-800">
@@ -953,8 +1224,42 @@
953 1224 </p>
954 1225 </div>
955 1226 </div>
956 1227
1228 + {/* Category — lets the operator classify the template (e.g.
1229 + Payment) instead of it always defaulting to Booking. The
1230 + recipient (Customer/Admin) is derived server-side from the
1231 + To address ({{admin_email}} => admin). Locked for core/
1232 + system templates whose category is fixed. */}
1233 + {!isCoreSettingsEdit && (
1234 + <div className="mb-4">
1235 + <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
1236 + {__("Category")}
1237 + </label>
1238 + <select
1239 + aria-label={__("Category")}
1240 + value={formData.category || "booking"}
1241 + onChange={(e) =>
1242 + setFormData({ ...formData, category: e.target.value })
1243 + }
1244 + disabled={isCoreViewMode || isSystemTemplate}
1245 + className="w-full px-3 py-2 text-sm rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 dark:text-white disabled:opacity-60"
1246 + >
1247 + <option value="booking">{__("Booking")}</option>
1248 + <option value="payment">{__("Payment")}</option>
1249 + <option value="reminder">{__("Reminder")}</option>
1250 + <option value="enquiry">{__("Enquiry")}</option>
1251 + <option value="account">{__("Account")}</option>
1252 + <option value="marketing">{__("Marketing")}</option>
1253 + </select>
1254 + <p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
1255 + {__(
1256 + "How this template is grouped in the list. Recipient (Customer/Admin) follows the To address.",
1257 + )}
1258 + </p>
1259 + </div>
1260 + )}
1261 +
957 1262 <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
958 1263 <div>
959 1264 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
960 1265 {__("To Email")}
@@ -1003,8 +1308,51 @@
1003 1308 )}
1004 1309 </p>
1005 1310 )}
1006 1311
1312 + {/* Copy addresses — available for every template, core or Pro.
1313 + Both are opt-in: left empty, no copy is sent. */}
1314 + <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
1315 + <div>
1316 + <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
1317 + {__("BCC", "yatra")}
1318 + </label>
1319 + <Input
1320 + value={formData.bcc}
1321 + onChange={(e) =>
1322 + setFormData({ ...formData, bcc: e.target.value })
1323 + }
1324 + placeholder={__("e.g., [email protected]", "yatra")}
1325 + disabled={isCoreViewMode}
1326 + />
1327 + <p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
1328 + {__(
1329 + "Blind copy, hidden from the recipient. Separate several addresses with commas. Leave empty to send no copy.",
1330 + "yatra",
1331 + )}
1332 + </p>
1333 + </div>
1334 + <div>
1335 + <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
1336 + {__("CC", "yatra")}
1337 + </label>
1338 + <Input
1339 + value={formData.cc}
1340 + onChange={(e) =>
1341 + setFormData({ ...formData, cc: e.target.value })
1342 + }
1343 + placeholder={__("e.g., [email protected]", "yatra")}
1344 + disabled={isCoreViewMode}
1345 + />
1346 + <p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
1347 + {__(
1348 + "Visible copy — the recipient can see this address. Separate several addresses with commas.",
1349 + "yatra",
1350 + )}
1351 + </p>
1352 + </div>
1353 + </div>
1354 +
1007 1355 <div>
1008 1356 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
1009 1357 {__("Subject Line")}
1010 1358 </label>
@@ -1064,8 +1412,117 @@
1064 1412 </div>
1065 1413 </CardContent>
1066 1414 </Card>
1067 1415
1416 + {/* One trip dropdown for the editor: preview, test AND "which
1417 + template does this trip get". It is NEVER saved — the saved
1418 + targeting is the Applies-to block. */}
1419 + {!isCreateMode &&
1420 + !isCoreSettingsEdit &&
1421 + overridesSupported &&
1422 + (isOverride || isOverridableGlobal) && (
1423 + <Card>
1424 + <CardHeader>
1425 + <CardTitle className="text-base">
1426 + {__("Try it with a real trip", "yatra")}
1427 + </CardTitle>
1428 + </CardHeader>
1429 + <CardContent className="space-y-2">
1430 + <div data-testid="preview-trip">
1431 + <SearchableSelect
1432 + value={previewTrip ? String(previewTrip) : ""}
1433 + onChange={(value) =>
1434 + setPreviewTrip(value ? Number(value) : null)
1435 + }
1436 + options={tripOptions.map((t) => ({
1437 + value: String(t.id),
1438 + label: t.label,
1439 + }))}
1440 + placeholder={__("Sample data (no trip)", "yatra")}
1441 + searchPlaceholder={__("Search trips…", "yatra")}
1442 + />
1443 + </div>
1444 + <p className="text-xs text-gray-500">
1445 + {__(
1446 + "Not saved. Preview and Send Test Email use this trip's real name, link and sample booking data. Which trips actually get this template is set in “Applies to”.",
1447 + "yatra",
1448 + )}
1449 + </p>
1450 + {previewTrip && tripResolution && (
1451 + <div
1452 + className="text-sm rounded-md border border-dashed border-gray-300 dark:border-gray-600 px-3 py-2"
1453 + data-testid="check-trip-result"
1454 + >
1455 + <span className="text-gray-500">
1456 + {__("This trip's customers get:", "yatra")}
1457 + </span>{" "}
1458 + {(() => {
1459 + const parentKey = isOverride
1460 + ? overrideOf
1461 + : String((templateData as any)?.template_key || "");
1462 + const hit = tripResolution.resolved[parentKey];
1463 + if (isOverride) {
1464 + if (hit?.id === Number(id)) {
1465 + return (
1466 + <span className="px-1.5 py-0.5 rounded text-xs font-medium bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-300">
1467 + {__("this override", "yatra")}
1468 + {hit?.reason ? ` · ${hit.reason}` : ""}
1469 + </span>
1470 + );
1471 + }
1472 + const noBodyYet = !String(
1473 + (templateData as any)?.body || "",
1474 + ).trim()
1475 + ? " — " +
1476 + __(
1477 + "this override has no body yet, so it is skipped",
1478 + "yatra",
1479 + )
1480 + : "";
1481 + if (hit) {
1482 + return (
1483 + <span className="text-amber-700 dark:text-amber-300">
1484 + {sprintf(
1485 + __("“%1$s” (%2$s)", "yatra"),
1486 + hit.name,
1487 + hit.reason,
1488 + )}
1489 + {noBodyYet}
1490 + </span>
1491 + );
1492 + }
1493 + return (
1494 + <span className="text-gray-700 dark:text-gray-300">
1495 + {sprintf(
1496 + __("the global template (%s)", "yatra"),
1497 + parentTemplate?.name || overrideOf,
1498 + )}
1499 + {noBodyYet}
1500 + </span>
1501 + );
1502 + }
1503 + return hit ? (
1504 + <span className="text-amber-700 dark:text-amber-300">
1505 + {sprintf(
1506 + __(
1507 + "the override “%1$s” (%2$s), not this template",
1508 + "yatra",
1509 + ),
1510 + hit.name,
1511 + hit.reason,
1512 + )}
1513 + </span>
1514 + ) : (
1515 + <span className="px-1.5 py-0.5 rounded text-xs font-medium bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-300">
1516 + {__("this template", "yatra")}
1517 + </span>
1518 + );
1519 + })()}
1520 + </div>
1521 + )}
1522 + </CardContent>
1523 + </Card>
1524 + )}
1068 1525 {/* Test Email - Only show in edit mode */}
1069 1526 {!isCreateMode && !isCoreSettingsEdit && (
1070 1527 <Card>
1071 1528 <CardHeader>
@@ -1250,8 +1707,33 @@
1250 1707 <Zap className="w-3 h-3" />
1251 1708 {formData.event_key || "-"}
1252 1709 </span>
1253 1710 </div>
1711 + {/* The badge above only reports the choice. This panel sits right
1712 + under "Select an event to trigger this email automatically",
1713 + so give it a control that actually selects one — a plain
1714 + <select>, which cannot be clipped by an ancestor and needs no
1715 + open/close state, unlike the styled dropdown in the main
1716 + column. System and core templates keep their fixed event. */}
1717 + {!isSystemTemplate && !isCoreSettingsEdit && (
1718 + <select
1719 + aria-label={__("Trigger Event")}
1720 + value={formData.event_key || ""}
1721 + onChange={(e) =>
1722 + setFormData({ ...formData, event_key: e.target.value })
1723 + }
1724 + className="w-full mt-1 px-2 py-1.5 text-xs rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 dark:text-white"
1725 + >
1726 + <option value="">
1727 + {__("No event (use in a sequence)")}
1728 + </option>
1729 + {events.map((ev: any) => (
1730 + <option key={ev.key} value={ev.key}>
1731 + {ev.name || ev.key}
1732 + </option>
1733 + ))}
1734 + </select>
1735 + )}
1254 1736 {formData.event_key && (
1255 1737 <p className="text-xs text-gray-500 dark:text-gray-400 italic">
1256 1738 {events.find((e: any) => e.key === formData.event_key)
1257 1739 ?.description || ""}
@@ -1396,8 +1878,16 @@
1396 1878 onAccept={({ subject, body }) =>
1397 1879 setFormData({ ...formData, subject, body })
1398 1880 }
1399 1881 />
1882 + {isOverridableGlobal && templateData && (
1883 + <EmailOverrideCreateModal
1884 + isOpen={overrideModalOpen}
1885 + onClose={() => setOverrideModalOpen(false)}
1886 + globals={[templateData as any]}
1887 + parent={templateData as any}
1888 + />
1889 + )}
1400 1890 </div>
1401 1891 );
1402 1892 };
1403 1893