PageSourceSearch

https://app.sundaysky.com/client/AdsContainer.Cc8xLuBn.js

js sundaysky.com collected 2026-09-24 08:29:54 UTC 707,088 bytes, 18,299 lines download raw bytes

1
2!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="da6fd120-86a6-583d-8992-c5cd2201c1e6")}catch(e){}}();
3import { o as __toESM, t as __commonJSMin } from "./rolldown-runtime.D1cXj70v.js";
4import { X as require_jsx_runtime, dt as require_react, t as require_material } from "./vendor~index~newNavigation~AccountTierManagementPage~AdminContainer~AISystemPromptManager~C~h0mf93xb.B-8UGg7U.js";
5import { Gn as v4, _r as useParams, fr as Switch$3, gr as useLocation, hr as useHistory, lr as Redirect, ur as Route } from "./vendor~index~newNavigation~AccountTierManagementPage~AdminContainer~AISystemPromptManager~C~hh2h6ti1.CxCdS9_4.js";
6import { A as require_faTriangleExclamation, D as require_faTrashCan, O as require_faPen, b as require_faBullhorn, m as require_faAngleLeft, t as require_dist, x as require_faAngleRight } from "./vendor~index~newNavigation~AccountTierManagementPage~AdminContainer~AISystemPromptManager~S~d59nl763.C6LdGoX3.js";
7import { n as Link$6 } from "./react-router-dom.DXk3bR6C.js";
8import { t as require_dist$1 } from "./dist.DzcutlwI.js";
9import { Ah as TimeUtils, Fi as PendoService, Q as setInfoNotification, Qg as permissions, Z as setErrorNotification, Zg as isPermitted, gf as STAGE_MODE, o as getService, pf as EDITING_MODE, tt as setSuccessNotification, xa as DownloadUtils } from "./logReportUtils.Ca-hg6bk.js";
10import { b as init_networkStatus, y as NetworkStatus } from "./vendor~index~newNavigation~AccountTierManagementPage~AdminContainer~AISystemPromptManager~C~lx5ijrxx.Ds2KGt5R.js";
11import { Cf as useCurrentAccountAdsSpaceCampaignQuery, Df as useCurrentAccountAdsSpaceExperimentsQuery, Ef as useCurrentAccountAdsSpaceEntitiesQuery, Fh as useSaveAdsSpaceServerSettingsMutation, Gr as GqlClientAdsSpaceEntityKind, Hr as GqlClientAdsSpaceCampaignChildTable, Mh as useSaveAdsSpaceEntitiesMutation, Nd as useAdsSpaceAvailableAdsAccountsQuery, Om as useLinkAdsAccountToStudioAccountMutation, Ph as useSaveAdsSpacePixelRulesMutation, Rm as useMoveAdsSpaceCampaignMutation, Uf as useDuplicateAdsSpaceCampaignMutation, Ur as GqlClientAdsSpaceCampaignMode, Wr as GqlClientAdsSpaceDeliveryStatus, Zh as useUpdateAdsSpaceCampaignSectionMutation, bf as useCurrentAccountAdsSpaceAudiencesQuery, df as useCreateAdsSpaceCampaignMutation, jf as useCurrentAccountAdsSpaceServerSettingsQuery, jh as useSaveAdsSpaceAudienceMutation, kf as useCurrentAccountAdsSpacePixelRulesQuery, lf as useCopyAdsSpaceAudienceToAccountMutation, qr as GqlClientAdsSpaceThresholdState, wf as useCurrentAccountAdsSpaceCampaignsQuery } from "./graphqlClient.B8mBnpI6.js";
12import { N as require_faCircleQuestion } from "./vendor~index~newNavigation~AccountTierManagementPage~AdminContainer~AISystemPromptManager~C~e5v1w7z6.UinWi66V.js";
13import { i as require_faXmark, n as require_faCircleInfo } from "./faInfoCircle.D2subuiD.js";
14import { t as require_faAngleDown } from "./faAngleDown.ByAeCpg0.js";
15import { t as FontAwesomeIcon } from "./dist.D6-c0JAy.js";
16import { r as require_faChartMixed, t as require_faEllipsisV } from "./faEllipsisV.BfhkYBLO.js";
17import { n as require_faPlus } from "./faStar.DAIrK_E4.js";
18import { i as require_faPlug, r as require_faPlay } from "./faShieldCheck.CcsGjb2g.js";
19import { n as require_faCopy } from "./faArrowsRotate.DLJqrJfV.js";
20import { n as require_faEarthAmericas, r as require_faDesktop, t as require_faArrowRotateRight } from "./faArrowRotateRight.BILaCnxV.js";
21import { n as require_faDownload, t as require_faWaveformLines } from "./faWaveformLines.DGYekvi_.js";
22import { n as require_faFlask } from "./faClockRotateLeft.BDmEd6Qu.js";
23import { t as require_faLink } from "./faLink.BGvRELs0.js";
24import { t as require_faUserGroup } from "./faUserGroup.CtbEFl4T.js";
25import { t as axios } from "./axios.BvSUZrlt.js";
26import { n as useActiveAccount, o as useUserData, r as useActiveAccountId } from "./model.fbsYc69Q.js";
27import { t as require_faFilm } from "./faFilm.Dzt69dT8.js";
28import { G as useDropzone } from "./DataLibraryUtils.Bu8dkwMa.js";
29import { t as TruffleCircularProgress } from "./TruffleCircularProgress.C8BTwqVq.js";
30import { n as HelpCenterPopover, r as EditHelpCenterPopover } from "./newNavigation.iV4gAi0z.js";
31import { t as NotificationSnackbar } from "./NotificationSnackbar.BiP7swdL.js";
32import { n as sundaysky_default, r as useGlobalDragDetector, t as UserMenu } from "./UserMenu.C2degPWb.js";
33import { w as RowOptionsMenu } from "./UserManagement.IN7bfiVp.js";
34import { a as stickyLeftSx, i as rowHoverSx, n as bodyCellSx, o as stickyRightSx, r as headCellSx, s as DiscardChangesDialog, t as actionsCellSx } from "./userTableSharedStyles.BCrdfiPn.js";
35import { t as require_dist$2 } from "./dist.8h5_VYnW.js";
36import { n as require_faArrowRightArrowLeft, r as require_faLinkSlash, t as require_faFilter } from "./faFilter.BdfO-JKX.js";
37import { c as AnalyticsChart } from "./InfoTooltipIcon.DpGFNd80.js";
38import { $ as STRING_LIST_SEPARATORS, A as THRESHOLD_STATE_MEANINGS, B as formatUtcTime, C as ADS_CAMPAIGN_DASHBOARD_SECTION_ID, Ct as adsTableTotalRowSx, D as STATUS_COLORS, E as QUALITY_LABELS, F as formatCount, G as paceSkewChip, H as inverseStatusColor, I as formatDate, K as paceStatusColor, L as formatMoney, M as describePace, N as ecpmStatus, O as STATUS_LABELS, P as formatCompact, Q as AdsSaveBar, R as formatRate, S as AccountAlertsCard, St as adsSurfaceSx, T as BAND_LABELS, U as marginColor, V as formatWholePercent, W as paceColor, X as DeliveryRefreshButton, Y as AdsAuditLogPage, Z as AdsSaveCommentDialog, _ as DeliveryEmptyState, _t as useAdsSpaceAccount, a as AdsSectionPage, at as describeAdsChangeCount, b as AdsMeter, bt as adsIconTileSx, c as useAdsSpaceAccountDelivery, ct as hasFieldChanged, d as todayChartOption, dt as matches, et as addDays, f as trendChartOption, ft as required, g as AccountDecoupledTable, gt as AdsNonProductionBanner, h as AccountCampaignTable, ht as AdsPageHeader, i as useAdsUnsavedChangesGuard, it as countRowChanges, j as campaignStateChip, k as THRESHOLD_STATE_LABELS, l as useAdsSpaceCampaignDelivery, lt as inRange, m as AdsKpiTile, mt as require_faTrash, n as AdsFlightsPage, nt as asText, o as AdsCopyableText, ot as emailEntryProblem, p as volumeDistributionChartOption, pt as validateFields, q as paceTolerance, r as AdsTableSkeleton, rt as countChanges, s as require_faCalendarDays, st as hasErrors, t as AdsUnreachableNotice, tt as asStringList, u as flightChartOption, ut as listSeparatorFor, v as DeliveryFreshness, vt as AdsEmptyState, w as ADS_CAMPAIGN_FLIGHTS_SECTION_ID, x as AdsDashboardCard, xt as adsPageSx, y as DeliverySkeleton, yt as adsFixedPageSx, z as formatRelativeAge } from "./AdsUnreachableNotice.BWm2MGKy.js";
39//#region client/components/newNav/components/ads/AdsAccountLinkGate.tsx
40var import_dist$1 = require_dist$1();
41var import_faAngleDown = require_faAngleDown();
42var import_dist$2 = require_dist();
43var import_faLink = require_faLink();
44var import_material = require_material();
45var import_react = /* @__PURE__ */ __toESM(require_react());
46var import_jsx_runtime = require_jsx_runtime();
47/**
48* Nothing in the space can be read before this Studio account is mapped to an ads account, so every route
49* renders through here. The drawer stays around it, so a user who lands on an unlinked account is not trapped.
50*/
51var AdsAccountLinkGate = ({ children }) => {
52	const { isLinked, hasLoaded, loading } = useAdsSpaceAccount();
53	if (isLinked) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_jsx_runtime.Fragment, { children });
54	if (loading && !hasLoaded) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Box, {
55		sx: loadingSx$1,
56		children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TruffleCircularProgress, { size: 40 })
57	});
58	return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsAccountLinkScreen, {});
59};
60var AdsAccountLinkScreen = () => {
61	const accountId = useActiveAccountId();
62	const { userRoles } = useUserData();
63	const canLink = isPermitted(userRoles, permissions.adsEditCampaignSettings);
64	const { data, loading: loadingAccounts } = useAdsSpaceAvailableAdsAccountsQuery({ skip: !canLink });
65	const [linkAdsAccount, { loading: linking }] = useLinkAdsAccountToStudioAccountMutation();
66	const [selectedId, setSelectedId] = (0, import_react.useState)(null);
67	const adsAccounts = data?.adsSpaceAvailableAdsAccounts ?? [];
68	const selected = adsAccounts.find((adsAccount) => adsAccount.id === selectedId) ?? null;
69	const link = () => {
70		linkAdsAccount({ variables: {
71			accountId,
72			adsAccountId: selectedId
73		} }).catch(() => setErrorNotification({ message: "Couldn't connect the ads account" }));
74	};
75	return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Box, {
76		sx: adsPageSx,
77		children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsEmptyState, {
78			icon: import_faLink.faLink,
79			heading: "This account is not connected to an ads account yet",
80			subtitle: "The ads space reads campaigns, targeting and settings from an ads account. Pick the one this Studio account belongs to.",
81			children: canLink ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
82				sx: formSx$1,
83				children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Autocomplete, {
84					options: adsAccounts,
85					value: selected,
86					loading: loadingAccounts,
87					getOptionLabel: (option) => option.name,
88					isOptionEqualToValue: (option, value) => option.id === value.id,
89					onChange: (_event, option) => setSelectedId(option?.id ?? null),
90					noOptionsText: loadingAccounts ? "Loading ads accounts" : "No ads accounts are reachable",
91					renderInput: (params) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TextField, {
92						...params,
93						label: "Ads account",
94						placeholder: "Search ads accounts",
95						inputProps: {
96							...params.inputProps,
97							"data-tracking-id": "tracking-id-ads-link-account-input"
98						}
99					})
100				}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Button, {
101					variant: "contained",
102					size: "large",
103					loading: linking,
104					disabled: !selectedId,
105					onClick: link,
106					"data-tracking-id": "tracking-id-ads-link-account-btn",
107					children: "Connect account"
108				})]
109			}) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
110				variant: "body1",
111				color: "text.secondary",
112				sx: readOnlySx,
113				children: "Ask someone who can edit campaign settings on this account to connect it."
114			})
115		})
116	});
117};
118var loadingSx$1 = {
119	display: "flex",
120	alignItems: "center",
121	justifyContent: "center",
122	height: "100%"
123};
124var formSx$1 = {
125	display: "flex",
126	flexDirection: "column",
127	gap: 2,
128	width: 360,
129	textAlign: "left"
130};
131var readOnlySx = { maxWidth: 380 };
132//#endregion
133//#region client/components/newNav/components/ads/AdsAccountLabel.tsx
134/**
135* The scope of everything in the space, at the top of the drawer. The ads account name leads because that is
136* what every write here lands on; the Studio account name and the ads account id are what you quote to someone
137* else. It states where you are and nothing more - switching account lives in the avatar menu at the bottom,
138* the same place it does in every other space.
139*/
140var AdsAccountLabel = () => {
141	const { displayName } = useActiveAccount();
142	const { adsAccountName, adsAccountId } = useAdsSpaceAccount();
143	const headline = adsAccountName ?? displayName;
144	const meta = adsAccountName ? `${displayName} . ads ${adsAccountId}` : "";
145	return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
146		sx: labelSx,
147		children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.TypographyWithTooltipOnOverflow, {
148			noWrap: true,
149			variant: "h4",
150			color: "text.primary",
151			children: headline
152		}
152), meta && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.TypographyWithTooltipOnOverflow, {
153			noWrap: true,
154			variant: "caption",
155			color: "text.secondary",
156			component: "div",
157			children: meta
158		})]
159	});
160};
161var labelSx = {
162	padding: "6px 8px",
163	minWidth: 0,
164	width: "100%"
165};
166//#endregion
167//#region client/components/newNav/components/ads/adsNavModel.ts
168/**
169* Sections rather than a flat list: nineteen rows at once is more than anyone reads, and the first three named
170* zones are the platform's three ways an impression gets served, which is the first thing true about any task
171* here. The dashboard sits above them as a plain row because it is where a session starts rather than a place
172* to configure something.
173*/
174var adsNavZones = [
175	{
176		id: "dashboard",
177		items: [{
178			id: "account-dashboard",
179			label: "Dashboard",
180			to: import_dist$1.newNavAdsDashboardPath,
181			permission: permissions.adsGetCampaignSettings
182		}]
183	},
184	{
185		id: "programmatic",
186		label: "Programmatic",
187		collapsible: true,
188		items: [
189			{
190				id: "campaigns",
191				label: "Campaigns",
192				to: import_dist$1.newNavAdsCampaignsPath,
193				permission: permissions.adsGetCampaignSettings
194			},
195			{
196				id: "targeting",
197				label: "Targeting",
198				to: import_dist$1.newNavAdsTargetingPath,
199				permission: permissions.adsGetCampaignSettings
200			},
201			{
202				id: "flights",
203				label: "Flights",
204				to: import_dist$1.newNavAdsFlightsPath,
205				permission: permissions.adsGetCampaignSettings
206			},
207			{
208				id: "experiments",
209				label: "Experiments",
210				to: import_dist$1.newNavAdsExperimentsPath,
211				permission: permissions.adsGetCampaignSettings
212			}
213		]
214	},
215	{
216		id: "decoupled",
217		label: "Decoupled",
218		collapsible: true,
219		items: [{
220			id: "decoupled-campaigns",
221			label: "Decoupled campaigns",
222			to: import_dist$1.newNavAdsDecoupledCampaignsPath,
223			permission: permissions.adsGetCampaignSettings
224		}, {
225			id: "dsps",
226			label: "Demand-side platforms",
227			to: import_dist$1.newNavAdsDspsPath,
228			permission: permissions.adsSystemManagement
229		}]
230	},
231	{
232		id: "dmp",
233		label: "DMP",
234		collapsible: true,
235		items: [{
236			id: "dmp-providers",
237			label: "Providers (global)",
238			to: import_dist$1.newNavAdsDmpProvidersPath,
239			permission: permissions.adsSystemManagement
240		}, {
241			id: "dmp-account-mappings",
242			label: "Account mappings (this account)",
243			to: import_dist$1.newNavAdsDmpAccountMappingsPath,
244			permission: permissions.adsGetCampaignSettings
245		}]
246	},
247	{
248		id: "accounts",
249		label: "Accounts",
250		collapsible: true,
251		items: [
252			{
253				id: "settings-account",
254				label: "Account",
255				to: ({ accountId }) => (0, import_dist$1.newNavAdsSettingsSectionPath)({
256					accountId,
257					sectionId: "account"
258				}),
259				permission: permissions.adsEditCampaignSettings
260			},
261			{
262				id: "settings-tracking",
263				label: "Sampling & sync",
264				to: ({ accountId }) => (0, import_dist$1.newNavAdsSettingsSectionPath)({
265					accountId,
266					sectionId: "tracking"
267				}),
268				permission: permissions.adsEditCampaignSettings
269			},
270			{
271				id: "settings-site-actions",
272				label: "Site actions",
273				to: ({ accountId }) => (0, import_dist$1.newNavAdsSettingsSectionPath)({
274					accountId,
275					sectionId: "site-actions"
276				}),
277				permission: permissions.adsEditCampaignSettings
278			},
279			{
280				id: "settings-attribution",
281				label: "Attribution reports",
282				to: ({ accountId }) => (0, import_dist$1.newNavAdsSettingsSectionPath)({
283					accountId,
284					sectionId: "attribution"
285				}),
286				permission: permissions.adsEditCampaignSettings
287			},
288			{
289				id: "settings-dsp-data",
290				label: "DSP data",
291				to: ({ accountId }) => (0, import_dist$1.newNavAdsSettingsSectionPath)({
292					accountId,
293					sectionId: "dsp-data"
294				}),
295				permission: permissions.adsEditCampaignSettings
296			}
297		]
298	},
299	{
300		id: "general",
301		label: "General",
302		collapsible: true,
303		items: [
304			{
305				id: "exchanges",
306				label: "Exchanges",
307				to: import_dist$1.newNavAdsExchangesPath,
308				permission: permissions.adsSystemManagement
309			},
310			{
311				id: "prediction-models",
312				label: "Prediction models",
313				to: import_dist$1.newNavAdsPredictionModelsPath,
314				permission: permissions.adsSystemManagement
315			},
316			{
317				id: "video-responses",
318				label: "Video responses",
319				to: import_dist$1.newNavAdsVideoResponsesPath,
320				permission: permissions.adsSystemManagement
321			}
322		]
323	},
324	{
325		id: "audit",
326		belowDivider: true,
327		items: [{
328			id: "audit-log",
329			label: "Audit log",
330			to: import_dist$1.newNavAdsAuditLogPath,
331			permission: permissions.adsGetCampaignSettings
332		}, {
333			id: "system",
334			label: "Server configuration",
335			to: import_dist$1.newNavAdsSystemPath,
336			permission: permissions.adsSystemManagement
337		}]
338	},
339	{
340		id: "utilities",
341		label: "Utilities",
342		collapsible: true,
343		belowDivider: true,
344		items: [{
345			id: "utility-zip-radius",
346			label: "ZIP radius builder",
347			to: ({ accountId }) => (0, import_dist$1.newNavAdsUtilityPath)({
348				accountId,
349				utilityId: "zip-radius"
350			}),
351			permission: permissions.adsGetCampaignSettings
352		}, {
353			id: "utility-pixel-tracking",
354			label: "Pixel tracking builder",
355			to: ({ accountId }) => (0, import_dist$1.newNavAdsUtilityPath)({
356				accountId,
357				utilityId: "pixel-tracking"
358			}),
359			permission: permissions.adsGetCampaignSettings
360		}]
361	}
362];
363/** A zone with no visible item is dropped whole, so a user without the system permission sees no empty heading. */
364var getVisibleAdsNavZones = (userRoles) => adsNavZones.map((zone) => ({
365	...zone,
366	items: zone.items.filter((item) => isPermitted(userRoles, item.permission))
367})).filter((zone) => zone.items.length > 0);
368/**
369* Matched on a path boundary rather than a bare prefix, so a nested route (a campaign, a section) keeps its
370* parent row selected while a sibling whose path merely starts with the same characters does not steal it.
371* The zone comes back too, so arriving on a deep link opens the section that holds the row.
372*/
373var getSelectedAdsNav = (pathname, accountId) => {
374	for (const zone of adsNavZones) {
375		const match = zone.items.find((item) => {
376			const path = item.to({ accountId });
377			return pathname === path || pathname.startsWith(`${path}/`);
378		});
379		if (match) return {
380			zoneId: zone.id,
381			itemId: match.id
382		};
383	}
384	return null;
385};
386//#endregion
387//#region node_modules/@fortawesome/pro-regular-svg-icons/faBullseyePointer.js
388var require_faBullseyePointer = /* @__PURE__ */ __commonJSMin(((exports) => {
389	Object.defineProperty(exports, "__esModule", { value: true });
390	var prefix = "far";
391	var iconName = "bullseye-pointer";
392	var width = 512;
393	var height = 512;
394	var aliases = [];
395	var unicode = "f649";
396	var svgPathData = "M464 256c0 114.9-93.1 208-208 208-4.2 0-8.3-.1-12.4-.4l-6.9 20.6c-3.4 10.3-8.9 18.8-15.6 25.4 11.4 1.6 23 2.4 34.9 2.4 141.4 0 256-114.6 256-256S397.4 0 256 0 0 114.6 0 256c0 11.8 .8 23.5 2.4 34.9 6.6-6.7 15.1-12.1 25.4-15.6l20.6-6.9c-.2-4.1-.4-8.2-.4-12.4 0-114.9 93.1-208 208-208s208 93.1 208 208zm-64 0c0-79.5-64.5-144-144-144-76.5 0-139.1 59.7-143.7 135.1L163.6 230c11.3-40.4 48.4-70 92.4-70 53 0 96 43 96 96 0 44-29.6 81.1-70 92.4l-17.1 51.3C340.3 395.1 400 332.5 400 256zM255.2 277.1c1.9-5.8 .4-12.1-3.9-16.4s-10.6-5.8-16.4-3.9l-192 64C36.4 323 32 329.1 32 336s4.4 13 10.9 15.2l63 21-90.9 90.9c-9.4 9.4-9.4 24.6 0 33.9s24.6 9.4 33.9 0l90.9-90.9 21 63c2.2 6.5 8.3 10.9 15.2 10.9s13-4.4 15.2-10.9l64-192z";
397	exports.definition = {
398		prefix,
399		iconName,
400		icon: [
401			width,
402			height,
403			aliases,
404			unicode,
405			svgPathData
406		]
407	};
408	exports.faBullseyePointer = exports.definition;
409	exports.prefix = prefix;
410	exports.iconName = iconName;
411	exports.width = width;
412	exports.height = height;
413	exports.ligatures = aliases;
414	exports.unicode = unicode;
415	exports.svgPathData = svgPathData;
416	exports.aliases = aliases;
417}));
418//#endregion
419//#region node_modules/@fortawesome/pro-regular-svg-icons/faLocationDot.js
420var require_faLocationDot = /* @__PURE__ */ __commonJSMin(((exports) => {
421	Object.defineProperty(exports, "__esModule", { value: true });
422	var prefix = "far";
423	var iconName = "location-dot";
424	var width = 384;
425	var height = 512;
426	var aliases = ["map-marker-alt"];
427	var unicode = "f3c5";
428	var svgPathData = "M48 188.6C48 111.7 111.7 48 192 48s144 63.7 144 140.6c0 45.6-23.8 101.5-58.9 157.1-28.3 44.8-61 84.8-85.1 112.1-24.1-27.3-56.7-67.2-85.1-112.1-35.1-55.5-58.9-111.5-58.9-157.1zM192 0C86 0 0 84.4 0 188.6 0 307.9 120.2 450.9 170.4 505.4 182.2 518.2 201.8 518.2 213.6 505.4 263.8 450.9 384 307.9 384 188.6 384 84.4 298 0 192 0zM160 192a32 32 0 1 1 64 0 32 32 0 1 1 -64 0zm112 0a80 80 0 1 0 -160 0 80 80 0 1 0 160 0z";
429	exports.definition = {
430		prefix,
431		iconName,
432		icon: [
433			width,
434			height,
435			aliases,
436			unicode,
437			svgPathData
438		]
439	};
440	exports.faLocationDot = exports.definition;
441	exports.prefix = prefix;
442	exports.iconName = iconName;
443	exports.width = width;
444	exports.height = height;
445	exports.ligatures = aliases;
446	exports.unicode = unicode;
447	exports.svgPathData = svgPathData;
448	exports.aliases = aliases;
449}));
450//#endregion
451//#region client/components/newNav/components/ads/adsUtilities.ts
452var import_faBullseyePointer = require_faBullseyePointer();
453var import_faLocationDot = require_faLocationDot();
454var adsUtilities = [{
455	id: "zip-radius",
456	icon: import_faLocationDot.faLocationDot,
457	name: "ZIP radius builder",
458	description: "Expand a list of stores into every ZIP code within a given radius, and the DMAs those ZIP codes fall in."
459}, {
460	id: "pixel-tracking",
461	icon: import_faBullseyePointer.faBullseyePointer,
462	name: "Pixel tracking builder",
463	description: "The advertiser's tracking URLs and the conditions that decide which of them the ad server sends back."
464}];
465var findAdsUtility = (utilityId) => adsUtilities.find((utility) => utility.id === utilityId);
466//#endregion
467//#region common/adsPixelRulesModel.ts
468var import_faAngleRight = require_faAngleRight();
469var import_faAngleLeft = require_faAngleLeft();
470var import_faPlus = require_faPlus();
471var import_faTrash = require_faTrash();
472/**
473* The third-party tracking pixel file: one CSV per campaign, read by the ad server at bid time to decide which
474* of the advertiser's tracking URLs belong in the VAST response. The column list, the operators and the two
475* markers below are the format the Java side defines; nothing here may be widened without changing that side
476* first, because a file we accept and the ad server rejects takes a campaign off air.
477*/
478/** `1002` adds the complete-impression pixels and is what an editor save writes. */
479var PIXEL_PROTOCOL_VERSION_CURRENT = "1002";
480/** A file whose first cell is not this literal is parsed as the pre-2018 format instead. */
481var PIXEL_PROTOCOL_VERSION_HEADER = "Protocol Version";
482var PIXEL_LANE_HEADER = "Lane";
483var pixelOperators = [
484	"equals",
485	"not equals",
486	"contains",
487	"not contains",
488	"contains any of",
489	"contains none of"
490];
491var pixelUrlKindLabels = {
492	impression: "Impression pixels",
493	thirdPartyClick: "Third-party click pixels",
494	clickThrough: "Click-through URL",
495	vastWrapper: "VAST wrapper",
496	completeImpression: "Complete-impression pixels"
497};
498/** The 50 columns the ad server reads, in its order, plus the lane column this client appends. */
499var pixelColumnHeaders = [
500	PIXEL_PROTOCOL_VERSION_HEADER,
501	"Placement ID",
502	"Placement name",
503	"Creative ID",
504	"Creative Name",
505	"Category",
506	...Array.from({ length: 5 }).flatMap((_slot, index) => [
507		`Key ${index + 1}`,
508		`Operator ${index + 1}`,
509		`Value ${index + 1}`
510	]),
511	"Control Group",
512	"Experiment Group",
513	"Duration",
514	"Exchange",
515	"Audience",
516	"Start Date",
517	"End Date",
518	"Platform Type",
519	"Mobile Device Type",
520	"Impression pixel",
521	"Click Through Url",
522	"3rd Party Click Pixel",
523	"Vast Wrapper",
524	...Array.from({ length: 5 }).map((_slot, index) => `Impression pixel ${index + 2}`),
525	...Array.from({ length: 5 }).map((_slot, index) => `3rd Party Click Pixel ${index + 2}`),
526	...Array.from({ length: 6 }).map((_slot, index) => `Complete Impression Pixel ${index + 1}`),
527	PIXEL_LANE_HEADER
528];
529var emptyPixelRule = (id) => ({
530	id,
531	placementId: "",
532	placementName: "",
533	creativeId: "",
534	creativeName: "",
535	category: "",
536	conditions: [],
537	controlGroup: "",
538	experimentGroup: "",
539	duration: "",
540	exchange: "",
541	audience: "",
542	lane: "",
543	startDate: "",
544	endDate: "",
545	platformType: "",
546	mobileDeviceType: "",
547	impressionPixels: [],
548	clickThroughUrl: "",
549	thirdPartyClickPixels: [],
550	vastWrapper: "",
551	completeImpressionPixels: []
552});
553/**
554* Excel rewrites these files, so a date is written ISO and read in every form Excel produces. Anything else is
555* returned as null and reported by the validator rather than silently treated as "no bound".
556*/
557var parsePixelDate = (value) => {
558	const text = value.trim();
559	if (!text) return null;
560	const iso = /^(\d{4})-(\d{1,2})-(\d{1,2})$/.exec(text);
561	if (iso) return `${iso[1]}-${pad(iso[2])}-${pad(iso[3])}`;
562	const american = /^(\d{1,2})[/-](\d{1,2})[/-](\d{2}|\d{4})$/.exec(text);
563	if (american) return `${american[3].length === 2 ? `20${american[3]}` : american[3]}-${pad(american[1])}-${pad(american[2])}`;
564	return null;
565};
566var pad = (value) => value.padStart(2, "0");
567/** `*` and an empty cell both mean "matches anything". */
568var isPixelWildcard = (value) => {
569	const text = value.trim();
570	return text === "" || text === "*";
571};
572var isPixelPartial = (value) => value.trim() === "~";
573/**
574* What the row targets, in one line, from the criteria that are not wildcards. A row that constrains nothing
575* reads as matching everything, which is the thing an operator most needs to be able to see at a glance.
576*/
577var describePixelRuleMatch = (rule) => {
578	const parts = [];
579	if (!isPixelWildcard(rule.duration)) parts.push(isPixelPartial(rule.duration) ? "duration only if nothing else matches" : `${rule.duration}s`);
580	if (!isPixelWildcard(rule.platformType)) parts.push(describeMarker(rule.platformType, "platform"));
581	if (!isPixelWildcard(rule.mobileDeviceType)) parts.push(describeMarker(rule.mobileDeviceType, "device type"));
582	if (!isPixelWildcard(rule.exchange)) parts.push(describeMarker(rule.exchange, "exchange"));
583	if (!isPixelWildcard(rule.category)) parts.push(describeMarker(rule.category, "category"));
584	if (!isPixelWildcard(rule.controlGroup) && rule.controlGroup.trim().toLowerCase() !== "fal
584se") parts.push("control group");
585	if (!isPixelWildcard(rule.experimentGroup)) parts.push(describeMarker(rule.experimentGroup, "experiment group"));
586	if (!isPixelWildcard(rule.audience)) parts.push(describeMarker(rule.audience, "audience"));
587	if (!isPixelWildcard(rule.lane)) parts.push(isPixelPartial(rule.lane) ? "lane only if nothing else matches" : `lane ${rule.lane.trim()}`);
588	rule.conditions.filter((condition) => condition.key.trim()).forEach((condition) => parts.push(`aux ${condition.key} ${condition.operator} ${condition.value}`));
589	if (rule.startDate || rule.endDate) parts.push(`${rule.startDate || "any date"} to ${rule.endDate || "any date"}`);
590	return parts.length > 0 ? parts.join(", ") : "Matches everything";
591};
592var describeMarker = (value, criterion) => isPixelPartial(value) ? `${criterion} only if nothing else matches` : value.trim();
593var countPixelRuleUrls = (rule) => rule.impressionPixels.filter(Boolean).length + rule.thirdPartyClickPixels.filter(Boolean).length + rule.completeImpressionPixels.filter(Boolean).length + (rule.clickThroughUrl ? 1 : 0) + (rule.vastWrapper ? 1 : 0);
594/** Handles the quoted fields the URLs need - a tracking URL with a comma in it is common. */
595var splitCsvLine = (line) => {
596	const cells = [];
597	let cell = "";
598	let isQuoted = false;
599	for (let index = 0; index < line.length; index++) {
600		const character = line[index];
601		if (isQuoted) if (character === "\"") if (line[index + 1] === "\"") {
602			cell += "\"";
603			index++;
604		} else isQuoted = false;
605		else cell += character;
606		else if (character === "\"") isQuoted = true;
607		else if (character === ",") {
608			cells.push(cell);
609			cell = "";
610		} else cell += character;
611	}
612	cells.push(cell);
613	return cells;
614};
615var toCsvCell = (value) => /[",\n]/.test(value) ? `"${value.replace(/"/g, "\"\"")}"` : value;
616var parsePixelRulesCsv = (csvText) => {
617	const lines = csvText.split("\n").map((line) => line.replace(/\r$/, "")).filter((line) => line.trim().length > 0);
618	if (lines.length === 0) return {
619		rules: [],
620		protocolVersion: PIXEL_PROTOCOL_VERSION_CURRENT,
621		isLegacyFormat: false
622	};
623	const isLegacyFormat = splitCsvLine(lines[0])[0]?.trim() !== PIXEL_PROTOCOL_VERSION_HEADER;
624	const bodyLines = isLegacyFormat ? lines : lines.slice(1);
625	const rules = bodyLines.map((line, index) => toRule(splitCsvLine(line), index));
626	return {
627		rules,
628		protocolVersion: (rules[0] ? cellAt(splitCsvLine(bodyLines[0]), 0) : "") || "1002",
629		isLegacyFormat
630	};
631};
632var cellAt = (cells, index) => (cells[index] ?? "").trim();
633var toRule = (cells, index) => {
634	const conditions = [];
635	for (let slot = 0; slot < 5; slot++) {
636		const base = 6 + slot * 3;
637		const key = cellAt(cells, base);
638		if (!key) continue;
639		conditions.push({
640			key,
641			operator: cellAt(cells, base + 1),
642			value: cellAt(cells, base + 2)
643		});
644	}
645	return {
646		id: `row-${index + 1}`,
647		placementId: cellAt(cells, 1),
648		placementName: cellAt(cells, 2),
649		creativeId: cellAt(cells, 3),
650		creativeName: cellAt(cells, 4),
651		category: cellAt(cells, 5),
652		conditions,
653		controlGroup: cellAt(cells, 21),
654		experimentGroup: cellAt(cells, 22),
655		duration: cellAt(cells, 23),
656		exchange: cellAt(cells, 24),
657		audience: cellAt(cells, 25),
658		lane: cellAt(cells, 50),
659		startDate: cellAt(cells, 26),
660		endDate: cellAt(cells, 27),
661		platformType: cellAt(cells, 28),
662		mobileDeviceType: cellAt(cells, 29),
663		impressionPixels: [cellAt(cells, 30), ...range(34, 5).map((at) => cellAt(cells, at))].filter(Boolean),
664		clickThroughUrl: cellAt(cells, 31),
665		thirdPartyClickPixels: [cellAt(cells, 32), ...range(39, 5).map((at) => cellAt(cells, at))].filter(Boolean),
666		vastWrapper: cellAt(cells, 33),
667		completeImpressionPixels: range(44, 6).map((at) => cellAt(cells, at)).filter(Boolean)
668	};
669};
670var range = (start, count) => Array.from({ length: count }).map((_item, index) => start + index);
671var slot = (values, index) => values[index] ?? "";
672/**
673* Excel renders a long numeric id in scientific notation, so `Placement ID` and `Creative ID` are written with
674* a trailing tab to keep it text. The Java reader trims it back off.
675*/
676var asExcelText = (value) => value ? `${value}\t` : "";
677var serializePixelRulesCsv = (rules) => {
678	return [pixelColumnHeaders, ...rules.map((rule) => [
679		PIXEL_PROTOCOL_VERSION_CURRENT,
680		asExcelText(rule.placementId),
681		rule.placementName,
682		asExcelText(rule.creativeId),
683		rule.creativeName,
684		rule.category,
685		...range(0, 5).flatMap((slotIndex) => {
686			const condition = rule.conditions[slotIndex];
687			return [
688				condition?.key ?? "",
689				condition?.operator ?? "",
690				condition?.value ?? ""
691			];
692		}),
693		rule.controlGroup,
694		rule.experimentGroup,
695		rule.duration,
696		rule.exchange,
697		rule.audience,
698		rule.startDate,
699		rule.endDate,
700		rule.platformType,
701		rule.mobileDeviceType,
702		slot(rule.impressionPixels, 0),
703		rule.clickThroughUrl,
704		slot(rule.thirdPartyClickPixels, 0),
705		rule.vastWrapper,
706		...range(1, 5).map((index) => slot(rule.impressionPixels, index)),
707		...range(1, 5).map((index) => slot(rule.thirdPartyClickPixels, index)),
708		...range(0, 6).map((index) => slot(rule.completeImpressionPixels, index)),
709		rule.lane
710	])].map((cells) => cells.map(toCsvCell).join(",")).join("\n");
711};
712//#endregion
713//#region client/components/newNav/components/ads/pixels/pixelRulesMatching.ts
714var emptyPixelOpportunity = (date) => ({
715	date,
716	duration: "",
717	exchange: "",
718	platformType: "",
719	mobileDeviceType: "",
720	hasDeviceId: false,
721	categories: [],
722	controlGroupName: "",
723	experimentGroups: [],
724	audiences: [],
725	lane: "",
726	auxValues: {}
727});
728var worst = (scores) => {
729	if (scores.includes("NONE")) return "NONE";
730	return scores.includes("PARTIAL") ? "PARTIAL" : "FULL";
731};
732/** Every criterion resolves `*`/empty to FULL and `~` to PARTIAL before it is compared at all. */
733var scoreCriterion = (cell, compare) => {
734	if (isPixelWildcard(cell)) return "FULL";
735	if (isPixelPartial(cell)) return "PARTIAL";
736	return compare(cell.trim()) ? "FULL" : "NONE";
737};
738var scoreAuxCondition = (operator, cell, actual) => {
739	const values = cell.split(";").map((value) => value.trim()).filter(Boolean);
740	switch (operator) {
741		case "equals": return actual === cell;
742		case "not equals": return actual !== cell;
743		case "contains": return actual.includes(cell);
744		case "not contains": return !actual.includes(cell);
745		case "contains any of": return values.some((value) => actual.includes(value));
746		case "contains none of": return values.every((value) => !actual.includes(value));
747		default: return false;
748	}
749};
750var scoreMobileDeviceType = (cell, opportunity) => {
751	const text = cell.trim().toLowerCase();
752	const requiresDevice = text.endsWith("_with_device");
753	const forbidsDevice = text.endsWith("_without_device");
754	const base = requiresDevice || forbidsDevice ? text.slice(0, text.lastIndexOf("_with")) : text;
755	if (requiresDevice && !opportunity.hasDeviceId) return "NONE";
756	if (forbidsDevice && opportunity.hasDeviceId) return "NONE";
757	return scoreCriterion(base, (value) => value === opportunity.mobileDeviceType.trim().toLowerCase());
758};
759var scorePlatformType = (cell, opportunity) => {
760	const actual = opportunity.platformType.trim().toLowerCase();
761	if (!actual || actual === "all") return "FULL";
762	if (actual === "unknown") return "PARTIAL";
763	const normalized = actual === "tablet" ? "mobile" : actual;
764	return scoreCriterion(cell, (value) => value.toLowerCase() === normalized);
765};
766var matchPixelRules = (rules, opportunity) => rules.map((rule) => {
767	const start = parsePixelDate(rule.startDate);
768	const end = parsePixelDate(rule.endDate);
769	if (start && opportunity.date < start || end && opportunity.date > end) return {
770		rule,
771		score: "NONE",
772		criteria: [],
773		outOfWindow: true
774	};
775	const criteria = [
776		{
777			criterion: "Duration",
778			score: scoreCriterion(rule.duration, (value) => value === opportunity.duration.trim())
779		},
780		{
781			criterion: "Exchange",
782			score: scoreCriterion(rule.exchange, (value) => opportunity.exchange.startsWith(value))
783		},
784		{
785			criterion: "Category",
786			score: scoreCriterion(rule.category, (value) => opportunity.categories.includes(value))
787		},
788		{
789			criterion: "Control group",
790			score: scoreControlGroup(rule.controlGroup, opportunity)
791		},
792		{
793			criterion: "Experiment group",
794			score: scoreCriterion(rule.experimentGroup, (value) => opportunity.experimentGroups.some((group) => group.toLowerCase() === value.toLowerCase()))
795		},
796		{
797			criterion: "Audience",
798			score: scoreCriterion(rule.audience, (value) => opportunity.audiences.some((audience) => audience.toLowerCase() === value.toLowerCase()))
799		},
800		{
801			criterion: "Lane",
802			score: scoreCriterion(rule.lane, (value) => value.toLowerCase() === opportunity.lane.trim().toLowerCase())
803		},
804		{
805			criterion: "Platform type",
806			score: scorePlatformType(rule.platformType, opportunity)
807		},
808		{
809			criterion: "Mobile device type",
810			score: scoreMobileDeviceType(rule.mobileDeviceType, opportunity)
811		},
812		...rule.conditions.map((condition, index) => ({
813			criterion: `Condition ${index + 1} (aux ${condition.key})`,
814			score: scoreCriterion(condition.value, (value) => scoreAuxCondition(condition.operator, value, opportunity.auxValues[normalizeAuxKey(condition.key)] ?? ""))
815		}))
816	];
817	return {
818		rule,
819		score: worst(criteria.map((result) => result.score)),
820		criteria,
821		outOfWindow: false
822	};
823});
824/**
825* A control-group impression matches when the cell is contained in the request's group name; a normal impression
826* matches only the literal `false`.
827*/
828var scoreControlGroup = (cell, opportunity) => scoreCriterion(cell, (value) => opportunity.controlGroupName ? opportunity.controlGroupName.includes(value) : value.toLowerCase() === "fal
828se");
829var normalizeAuxKey = (key) => key.replace(/^aux\s*/i, "").trim();
830/**
831* The URLs the response would carry. Grouped by the VAST element they become, and within each group a FULL row
832* beats every PARTIAL one - which is the whole point of the `~` marker.
833*/
834var resolvePixelUrls = (matches) => {
835	const surviving = matches.filter((match) => !match.outOfWindow && match.score !== "NONE");
836	const pick = (kind) => {
837		const fromFullRows = surviving.filter((match) => match.score === "FULL").flatMap((match) => ruleUrls(match.rule, kind));
838		return fromFullRows.length > 0 ? fromFullRows : surviving.flatMap((match) => ruleUrls(match.rule, kind));
839	};
840	return {
841		impression: pick("impression"),
842		thirdPartyClick: pick("thirdPartyClick"),
843		completeImpression: pick("completeImpression"),
844		clickThrough: pick("clickThrough"),
845		vastWrapper: pick("vastWrapper")
846	};
847};
848var ruleUrls = (rule, kind) => {
849	switch (kind) {
850		case "impression": return rule.impressionPixels.filter(Boolean);
851		case "thirdPartyClick": return rule.thirdPartyClickPixels.filter(Boolean);
852		case "completeImpression": return rule.completeImpressionPixels.filter(Boolean);
853		case "clickThrough": return rule.clickThroughUrl ? [rule.clickThroughUrl] : [];
854		case "vastWrapper": return rule.vastWrapper ? [rule.vastWrapper] : [];
855	}
856};
857//#endregion
858//#region client/components/newNav/components/ads/pixels/PixelMatchSimulator.tsx
859var scoreDisplay = {
860	FULL: {
861		label: "Full",
862		color: "success"
863	},
864	PARTIAL: {
865		label: "Partial",
866		color: "warning"
867	},
868	NONE: {
869		label: "No match",
870		color: "default"
871	}
872};
873/**
874* Describe one opportunity and see which rows the ad server would pick and which URLs would be emitted. This is
875* the thing a spreadsheet cannot do, and it is the only reason to prefer this page to Excel rather than merely
876* find it nicer.
877*/
878var PixelMatchSimulator = (props) => {
879	const [opportunity, setOpportunity] = (0, import_react.useState)(emptyPixelOpportunity(props.today));
880	const matches = (0, import_react.useMemo)(() => matchPixelRules(props.rules, opportunity), [props.rules, opportunity]);
881	const urls = (0, import_react.useMemo)(() => resolvePixelUrls(matches), [matches]);
882	const set = (key, value) => setOpportunity((current) => ({
883		...current,
884		[key]: value
885	}));
886	return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
887		sx: simulatorSx$1,
888		children: [
889			/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
890				variant: "h5",
891				children: "Match simulator"
892			}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
893				variant: "helper",
894				color: "text.secondary",
895				children: "Describe one impression opportunity - platform, device, categories, control group, experiment groups, audiences, lane, aux values - and this shows which pixel rows the ad server would pick for it and which URLs it would emit. It runs the same scoring the ad server does: a row scores Full only when every criterion is Full, and within each group of URLs a Full row displaces the Partial ones. Nothing is saved. It is the check a spreadsheet cannot do."
896			})] }),
897			/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
898				sx: fieldsGridSx$1,
899				children: [
900					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TextField, {
901						size: "small",
902						type: "date",
903						label: "Date",
904						value: opportunity.date,
905						onChange: (event) => set("date", event.target.value),
906						inputProps: { "data-tracking-id": "tracking-id-ads-pixel-sim-date-input" }
907					}),
908					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TextField, {
909						size: "small",
910						label: "Duration (seconds)",
911						value: opportunity.duration,
912						onChange: (event) => set("duration", event.target.value),
913						inputProps: { "data-tracking-id": "tracking-id-ads-pixel-sim-duration-input" }
914					}),
915					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TextField, {
916						size: "small",
917						label: "Exchange",
918						value: opportunity.exchange,
919						onChange: (event) => set("exchange", event.target.value),
920						inputProps: { "data-tracking-id": "tracking-id-ads-pixel-sim-exchange-input" }
921					}),
922					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TextField, {
923						size: "small",
924						label: "Platform type",
925						placeholder: "desktop, mobile, tablet, unknown",
926						value: opportunity.platformType,
927						onChange: (event) => set("platformType", event.target.value),
928						inputProps: { "data-tracking-id": "tracking-id-ads-pixel-sim-platform-input" }
929					}),
930					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TextField, {
931						size: "small",
932						label: "Mobile device type",
933						placeholder: "app or web",
934						value: opportunity.mobileDeviceType,
935						onChange: (event) => set("mobileDeviceType", event.target.value),
936						inputProps: { "data-tracking-id": "tracking-id-ads-pixel-sim-device-input" }
937					}),
938					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TextField, {
939						size: "small",
940						label: "Categories",
941						placeholder: "Comma separated",
942						value: opportunity.categories.join(","),
943						onChange: (event) => set("categories", splitList(event.target.value)),
944						inputProps: { "data-tracking-id": "tracking-id-ads-pixel-sim-categories-input" }
945					}),
946					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TextField, {
947						size: "small",
948						label: "Control group name",
949						placeholder: "Empty for a normal impression",
950						value: opportunity.controlGroupName,
951						onChange: (event) => set("controlGroupName", event.target.value),
952						inputProps: { "data-tracking-id": "tracking-id-ads-pixel-sim-control-group-input" }
953					}),
954					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TextField, {
955						size: "small",
956						label: "Experiment groups",
957						placeholder: "Comma separated",
958						value: opportunity.experimentGroups.join(","),
959						onChange: (event) => set("experimentGroups", splitList(event.target.value)),
960						inputProps: { "data-tracking-id": "tracking-id-ads-pixel-sim-experiment-input" }
961					}),
962					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TextField, {
963						size: "small",
964						label: "Audiences",
965						placeholder: "Comma separated",
966						value: opportunity.audiences.join(","),
967						onChange: (event) => set("audiences", splitList(event.target.value)),
968						inputProps: { "data-tracking-id": "tracking-id-ads-pixel-sim-audiences-input" }
969					}),
970					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TextField, {
971						size: "small",
972						label: "Lane",
973						placeholder: "The lane the request lands in",
974						value: opportunity.lane,
975						onChange: (event) => set("lane", event.target.value),
976						inputProps: { "data-tracking-id": "tracking-id-ads-pixel-sim-lane-input" }
977					}),
978					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TextField, {
979						size: "small",
980						label: "Aux values",
981						placeholder: "1=SKU123, 3=blue",
982						value: formatAuxValues(opportunity.auxValues),
983						onChange: (event) => set("auxValues", parseAuxValues(event.target.value)),
984						inputProps: { "data-tracking-id": "tracking-id-ads-pixel-sim-aux-input" }
985					}),
986					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.FormControlLabel, {
987						control: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Checkbox, {
988							size: "small",
989							checked: opportunity.hasDeviceId,
990							onChange: (event) => set("hasDeviceId", event.target.checked),
991							"data-tracking-id": "tracking-id-ads-pixel-sim-device-id-checkbox"
992						}),
993						label: "The request carries a device id"
994					})
995				]
996			}),
997			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Paper, {
998				variant: "outlined",
999				children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Table, {
1000					size: "small",
1001					children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableHead, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.TableRow, { children: [
1002						/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, {
1003							sx: headCellSx,
1004							children: "Placement"
1005						}),
1006						/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, {
1007							sx: headCellSx,
1008							children: "Result"
1009						}),
1010						/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, {
1011							sx: headCellSx,
1012							children: "Why"
1013						})
1014					] }) }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableBody, { children: matches.map((match) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.TableRow, { children: [
1015						/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, {
1016							sx: bodyCellSx,
1017							children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
1018								variant: "subtitle2",
1019								children: match.rule.placementName || match.rule.placementId || "Unnamed placement"
1020							})
1021						}),
1022						/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, {
1023							sx: bodyCellSx,
1024							children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.Label, {
1025								size: "small",
1026								variant: match.score === "NONE" ? "outlined" : "standard",
1027								color: scoreDisplay[match.score].color,
1028								label: match.outOfWindow ? "Outside its dates" : scoreDisplay[match.score].label
1029							})
1030						}),
1031						/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, {
1032							sx: bodyCellSx,
1033							children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
1034								variant: "body1",
1035								color: "text.secondary",
1036								children: describeWhy(match.outOfWindow, match.criteria)
1037							})
1038						})
1039					] }, match.rule.id)) })]
1040				})
1041			}),
1042			/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
1043				sx: urlsSx,
1044				children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
1045					variant: "h5",
1046					children: "URLs in the response"
1047				}), Object.keys(pixelUrlKindLabels).map((kind) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
1048					variant: "subtitle2",
1049					children: pixelUrlKindLabels[kind]
1050				}), urls[kind].length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
1051					variant: "body1",
1052					color: "text.secondary",
1053					children: "None"
1054				}) : urls[kind].map((url, index) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
1055					variant: "body1",
1056					sx: urlSx$1,
1057					children: url
1058				}, index))] }, kind))]
1059			})
1060		]
1061	});
1062};
1063var describeWhy = (outOfWindow, criteria) => {
1064	if (outOfWindow) return "The opportunity's date is outside this row's start and end dates";
1065	const failed = criteria.filter((result) => result.score === "NONE").map((result) => result.criterion);
1066	if (failed.length > 0) return `${failed.join(", ")} did not match`;
1067	const partial = criteria.filter((result) => result.score === "PARTIAL").map((result) => result.criterion);
1068	return partial.length > 0 ? `${partial.join(", ")} matched only partially` : "Every criterion matched";
1069};
1070var splitList = (value) => value.split(",").map((item) => item.trim()).filter(Boolean);
1071var formatAuxValues = (values) => Object.entries(values).map(([key, value]) => `${key}=${value}`).join(", ");
1072var parseAuxValues = (text) => {
1073	const values = {};
1074	text.split(",").forEach((pair) => {
1075		const [key, ...rest] = pair.split("=");
1076		if (key.trim() && rest.length > 0) values[key.trim()] = rest.join("=").trim();
1077	});
1078	return values;
1079};
1080var simulatorSx$1 = {
1081	display: "flex",
1082	flexDirection: "column",
1083	gap: 2
1084};
1085var fieldsGridSx$1 = {
1086	display: "grid",
1087	gridTemplateColumns: "repeat(auto-fill, minmax(240px, 1fr))",
1088	gap: 2,
1089	alignItems: "center"
1090};
1091var urlsSx = {
1092	display: "flex",
1093	flexDirection: "column",
1094	gap: 1
1095};
1096var urlSx$1 = { wordBreak: "break-all" };
1097var pixelCriteria = [
1098	{
1099		key: "duration",
1100		label: "Duration",
1101		help: "The video's length in seconds."
1102	},
1103	{
1104		key: "platformType",
1105		label: "Platform type",
1106		options: [
1107			"desktop",
1108			"tablet",
1109			"mobile",
1110			"ctv",
1111			"iptv",
1112			"connected_device"
1113		]
1114	},
1115	{
1116		key: "mobileDeviceType",
1117		label: "Mobile device type",
1118		options: [
1119			"app",
1120			"web",
1121			"app_with_device",
1122			"app_without_device",
1123			"web_with_device",
1124			"web_without_device"
1125		],
1126		help: "The _with_device and _without_device suffixes also require, or forbid, a device id on the request."
1127	},
1128	{
1129		key: "exchange",
1130		label: "Exchange",
1131		help: "Matched as a prefix - the opportunity's exchange has to start with this."
1132	},
1133	{
1134		key: "category",
1135		label: "Category"
1136	},
1137	{
1138		key: "controlGroup",
1139		label: "Control group",
1140		help: "A normal impression matches the literal false; a control-group one matches a name contained in the request's group."
1141	},
1142	{
1143		key: "experimentGroup",
1144		label: "Experiment group"
1145	},
1146	{
1147		key: "audience",
1148		label: "Audience"
1149	},
1150	{
1151		key: "lane",
1152		label: "Lane",
1153		help: "The budget lane the request landed in, from this campaign's flights and budget."
1154	}
1155];
1156/** A criterion the file leaves empty or `*` is not a condition at all, so it has no row. */
1157var isCriterionSet = (rule, key) => !isPixelWildcard(rule[key]);
1158//#endregion
1159//#region client/components/newNav/components/ads/pixels/PixelUrlSlots.tsx
1160/**
1161* The six numbered columns of one URL kind, as an add and remove list. Six numbered inputs would show five empty
1162* boxes for a placement that carries one pixel, and the numbering carries no meaning of its own.
1163*/
1164var PixelUrlSlots = (props) => {
1165	const setUrl = (index, value) => props.onChange(props.urls.map((url, at) => at === index ? value : url));
1166	return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
1167		sx: groupSx$4,
1168		children: [
1169			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
1170				variant: "h5",
1171				children: props.label
1172			}),
1173			props.help && /* @__P
1173URE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
1174				variant: "helper",
1175				color: "text.secondary",
1176				children: props.help
1177			}),
1178			props.urls.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
1179				variant: "body1",
1180				color: "text.secondary",
1181				children: "None on this placement"
1182			}),
1183			props.urls.map((url, index) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
1184				sx: rowSx$3,
1185				children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TextField, {
1186					fullWidth: true,
1187					size: "small",
1188					label: `${props.label} ${index + 1}`,
1189					value: url,
1190					disabled: props.readOnly,
1191					onChange: (event) => setUrl(index, event.target.value),
1192					inputProps: { "data-tracking-id": `${props.trackingIdPrefix}-url-input` }
1193				}), !props.readOnly && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.TruffleIconButton, {
1194					size: "small",
1195					onClick: () => props.onChange(props.urls.filter((_url, at) => at !== index)),
1196					"data-tracking-id": `${props.trackingIdPrefix}-remove-btn`,
1197					children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FontAwesomeIcon, { icon: import_faTrash.faTrash })
1198				})]
1199			}, index)),
1200			!props.readOnly && props.urls.length < 6 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Button, {
1201				variant: "text",
1202				size: "small",
1203				sx: addSx$1,
1204				onClick: () => props.onChange([...props.urls, ""]),
1205				"data-tracking-id": `${props.trackingIdPrefix}-add-btn`,
1206				children: `Add ${props.label.toLowerCase()}`
1207			})
1208		]
1209	});
1210};
1211var groupSx$4 = {
1212	display: "flex",
1213	flexDirection: "column",
1214	gap: 1
1215};
1216var rowSx$3 = {
1217	display: "flex",
1218	alignItems: "center",
1219	gap: 1
1220};
1221var addSx$1 = { alignSelf: "flex-start" };
1222//#endregion
1223//#region client/components/newNav/components/ads/pixels/PixelRuleDetailPanel.tsx
1224var IS_OPERATOR = "is";
1225var FALLBACK_OPERATOR = "only if nothing else matches";
1226/**
1227* The two halves of a placement, kept apart because they are never changed together: which slice of the buy the
1228* row applies to, and which of the advertiser's URLs it carries.
1229*
1230* The match half is a list of the conditions the row actually has, not a field per criterion the file could
1231* carry: nine of the ten cells are empty on a typical row, and an empty cell means "matches anything", which is
1232* the same thing as having no condition at all.
1233*/
1234var PixelRuleDetailPanel = (props) => {
1235	const [tab, setTab] = (0, import_react.useState)("MATCH");
1236	const [addAnchor, setAddAnchor] = (0, import_react.useState)(null);
1237	const [pending, setPending] = (0, import_react.useState)([]);
1238	const { rule } = props;
1239	const set = (key, value) => props.onChange({
1240		...rule,
1241		[key]: value
1242	});
1243	const setCondition = (index, condition) => set("conditions", rule.conditions.map((existing, at) => at === index ? condition : existing));
1244	const optionsFor = (def) => def.key === "lane" ? props.laneNames?.length ? props.laneNames : void 0 : def.options;
1245	const firstValue = (def) => optionsFor(def)?.[0] ?? "";
1246	const rows = pixelCriteria.filter((def) => isCriterionSet(rule, def.key) || pending.includes(def.key));
1247	const unused = pixelCriteria.filter((def) => !rows.includes(def));
1248	const addCriterion = (def) => {
1249		setPending([...pending, def.key]);
1250		set(def.key, firstValue(def));
1251		setAddAnchor(null);
1252	};
1253	const removeCriterion = (def) => {
1254		setPending(pending.filter((key) => key !== def.key));
1255		set(def.key, "");
1256	};
1257	const swapCriterion = (from, to) => {
1258		setPending([...pending.filter((key) => key !== from.key), to.key]);
1259		props.onChange({
1260			...rule,
1261			[from.key]: "",
1262			[to.key]: firstValue(to)
1263		});
1264	};
1265	const addAuxCondition = () => {
1266		set("conditions", [...rule.conditions, {
1267			key: "1",
1268			operator: "equals",
1269			value: ""
1270		}]);
1271		setAddAnchor(null);
1272	};
1273	const isEmpty = rows.length === 0 && rule.conditions.length === 0;
1274	return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
1275		sx: panelSx$1,
1276		children: [
1277			/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
1278				sx: identitySx,
1279				children: [
1280					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TextField, {
1281						fullWidth: true,
1282						size: "small",
1283						label: "Placement name",
1284						value: rule.placementName,
1285						disabled: props.readOnly,
1286						onChange: (event) => set("placementName", event.target.value),
1287						inputProps: { "data-tracking-id": "tracking-id-ads-pixel-placement-name-input" }
1288					}),
1289					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TextField, {
1290						fullWidth: true,
1291						size: "small",
1292						label: "Placement ID",
1293						value: rule.placementId,
1294						disabled: props.readOnly,
1295						onChange: (event) => set("placementId", event.target.value),
1296						inputProps: { "data-tracking-id": "tracking-id-ads-pixel-placement-id-input" }
1297					}),
1298					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TextField, {
1299						fullWidth: true,
1300						size: "small",
1301						label: "Creative name",
1302						value: rule.creativeName,
1303						disabled: props.readOnly,
1304						onChange: (event) => set("creativeName", event.target.value),
1305						inputProps: { "data-tracking-id": "tracking-id-ads-pixel-creative-name-input" }
1306					}),
1307					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TextField, {
1308						fullWidth: true,
1309						size: "small",
1310						label: "Creative ID",
1311						value: rule.creativeId,
1312						disabled: props.readOnly,
1313						onChange: (event) => set("creativeId", event.target.value),
1314						inputProps: { "data-tracking-id": "tracking-id-ads-pixel-creative-id-input" }
1315					})
1316				]
1317			}),
1318			/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_dist$2.TruffleToggleButtonGroup, {
1319				exclusive: true,
1320				size: "small",
1321				value: tab,
1322				onChange: (_event, next) => next && setTab(next),
1323				children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.ToggleButton, {
1324					value: "MATCH",
1325					sx: tabSx,
1326					"data-tracking-id": "tracking-id-ads-pixel-match-tab",
1327					children: "Match"
1328				}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.ToggleButton, {
1329					value: "PIXELS",
1330					sx: tabSx,
1331					"data-tracking-id": "tracking-id-ads-pixel-pixels-tab",
1332					children: "Pixels"
1333				})]
1334			}),
1335			tab === "MATCH" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
1336				sx: groupsSx,
1337				children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
1338					sx: conditionsSx,
1339					children: [
1340						/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
1341							variant: "h5",
1342							children: "Flight window"
1343						}),
1344						/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
1345							variant: "helper",
1346							color: "text.secondary",
1347							children: "An opportunity outside these dates never reaches this row. Leave a date empty for no bound."
1348						}),
1349						/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
1350							sx: datesSx$1,
1351							children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TextField, {
1352								fullWidth: true,
1353								size: "small",
1354								type: "date",
1355								label: "Start date",
1356								value: asDateInputValue(rule.startDate),
1357								disabled: props.readOnly,
1358								onChange: (event) => set("startDate", event.target.value),
1359								inputProps: { "data-tracking-id": "tracking-id-ads-pixel-start-date-input" }
1360							}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TextField, {
1361								fullWidth: true,
1362								size: "small",
1363								type: "date",
1364								label: "End date",
1365								value: asDateInputValue(rule.endDate),
1366								disabled: props.readOnly,
1367								onChange: (event) => set("endDate", event.target.value),
1368								inputProps: { "data-tracking-id": "tracking-id-ads-pixel-end-date-input" }
1369							})]
1370						})
1371					]
1372				}), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
1373					sx: conditionsSx,
1374					children: [
1375						/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
1376							variant: "h5",
1377							children: "Conditions"
1378						}),
1379						/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
1380							variant: "helper",
1381							color: "text.secondary",
1382							children: `Every condition here has to pass. A criterion with no row matches every opportunity. "${FALLBACK_OPERATOR}" is the ~ in the file: the row is used only when no other row matches the opportunity exactly.`
1383						}),
1384						isEmpty && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
1385							variant: "body1",
1386							color: "text.secondary",
1387							children: "No conditions - this placement matches every opportunity in its flight window"
1388						}),
1389						rows.map((def) => {
1390							const value = rule[def.key];
1391							const isFallback = isPixelPartial(value);
1392							const options = optionsFor(def);
1393							const valueOptions = options && !options.includes(value) ? [...options, value] : options;
1394							return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
1395								sx: conditionRowSx,
1396								children: [
1397									/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TextField, {
1398										select: true,
1399										size: "small",
1400										label: "Condition",
1401										sx: criterionSx,
1402										value: def.key,
1403										disabled: props.readOnly,
1404										onChange: (event) => {
1405											const next = pixelCriteria.find((candidate) => candidate.key === event.target.value);
1406											next && swapCriterion(def, next);
1407										},
1408										inputProps: { "data-tracking-id": "tracking-id-ads-pixel-criterion-input" },
1409										children: [def, ...unused].map((candidate) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.MenuItem, {
1410											value: candidate.key,
1411											children: candidate.label
1412										}, candidate.key))
1413									}),
1414									/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TextField, {
1415										select: true,
1416										size: "small",
1417										label: "Operator",
1418										sx: operatorSx,
1419										value: isFallback ? FALLBACK_OPERATOR : IS_OPERATOR,
1420										disabled: props.readOnly,
1421										onChange: (event) => set(def.key, event.target.value === FALLBACK_OPERATOR ? "~" : firstValue(def)),
1422										inputProps: { "data-tracking-id": "tracking-id-ads-pixel-criterion-operator-input" },
1423										children: [IS_OPERATOR, FALLBACK_OPERATOR].map((operator) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.MenuItem, {
1424											value: operator,
1425											children: operator
1426										}, operator))
1427									}),
1428									isFallback ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Box, { sx: valueSpacerSx }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TextField, {
1429										select: Boolean(valueOptions),
1430										fullWidth: true,
1431										size: "small",
1432										label: "Value",
1433										helperText: def.help,
1434										value,
1435										disabled: props.readOnly,
1436										onChange: (event) => set(def.key, event.target.value),
1437										inputProps: { "data-tracking-id": "tracking-id-ads-pixel-criterion-value-input" },
1438										children: valueOptions?.map((option) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.MenuItem, {
1439											value: option,
1440											children: option
1441										}, option))
1442									}),
1443									!props.readOnly && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.TruffleIconButton, {
1444										size: "small",
1445										onClick: () => removeCriterion(def),
1446										"data-tracking-id": "tracking-id-ads-pixel-criterion-remove-btn",
1447										children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FontAwesomeIcon, { icon: import_faTrash.faTrash })
1448									})
1449								]
1450							}, def.key);
1451						}),
1452						rule.conditions.map((condition, index) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
1453							sx: conditionRowSx,
1454							children: [
1455								/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TextField, {
1456									select: true,
1457									size: "small",
1458									label: "Condition",
1459									sx: criterionSx,
1460									value: condition.key,
1461									disabled: props.readOnly,
1462									onChange: (event) => setCondition(index, {
1463										...condition,
1464										key: event.target.value
1465									}),
1466									inputProps: { "data-tracking-id": "tracking-id-ads-pixel-condition-key-input" },
1467									children: Array.from({ length: 10 }).map((_key, at) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.MenuItem, {
1468										value: String(at + 1),
1469										children: `Aux ${at + 1}`
1470									}, at))
1471								}),
1472								/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TextField, {
1473									select: true,
1474									size: "small",
1475									label: "Operator",
1476									sx: operatorSx,
1477									value: condition.operator,
1478									disabled: props.readOnly,
1479									onChange: (event) => setCondition(index, {
1480										...condition,
1481										operator: event.target.value
1482									}),
1483									inputProps: { "data-tracking-id": "tracking-id-ads-pixel-condition-operator-input" },
1484									children: pixelOperators.map((operator) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.MenuItem, {
1485										value: operator,
1486										children: operator
1487									}, operator))
1488								}),
1489								/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TextField, {
1490									fullWidth: true,
1491									size: "small",
1492									label: "Value",
1493									helperText: condition.operator.includes("any of") || condition.operator.includes("none of") ? "Separate values with a semicolon" : `Compared with the request's aux ${condition.key}`,
1494									value: condition.value,
1495									disabled: props.readOnly,
1496									onChange: (event) => setCondition(index, {
1497										...condition,
1498										value: event.target.value
1499									}),
1500									inputProps: { "data-tracking-id": "tracking-id-ads-pixel-condition-value-input" }
1501								}),
1502								!props.readOnly && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.TruffleIconButton, {
1503									size: "small",
1504									onClick: () => set("conditions", rule.conditions.filter((_item, at) => at !== index)),
1505									"data-tracking-id": "tracking-id-ads-pixel-condition-remove-btn",
1506									children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FontAwesomeIcon, { icon: import_faTrash.faTrash })
1507								})
1508							]
1509						}, index)),
1510						!props.readOnly && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Button, {
1511							variant: "text",
1512							size: "small",
1513							sx: addSx,
1514							startIcon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FontAwesomeIcon, { icon: import_faPlus.faPlus }),
1515							disabled: unused.length === 0 && rule.conditions.length >= 5,
1516							onClick: (event) => setAddAnchor(event.currentTarget),
1517							"data-tracking-id": "tracking-id-ads-pixel-condition-add-btn",
1518							children: "Add condition"
1519						}), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Menu, {
1520							open: Boolean(addAnchor),
1521							anchorEl: addAnchor,
1522							onClose: () => setAddAnchor(null),
1523							children: [unused.map((def) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.MenuItem, {
1524								onClick: () => addCriterion(def),
1525								"data-tracking-id": "tracking-id-ads-pixel-condition-add-item",
1526								children: def.label
1527							}
1527, def.key)), rule.conditions.length < 5 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.MenuItem, {
1528								onClick: addAuxCondition,
1529								"data-tracking-id": "tracking-id-ads-pixel-condition-add-aux-item",
1530								children: "An aux key the request carries"
1531							})]
1532						})] })
1533					]
1534				})]
1535			}) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
1536				sx: groupsSx,
1537				children: [
1538					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(PixelUrlSlots, {
1539						label: "Impression pixel",
1540						help: "Fired when the impression is served.",
1541						urls: rule.impressionPixels,
1542						readOnly: props.readOnly,
1543						onChange: (urls) => set("impressionPixels", urls),
1544						trackingIdPrefix: "tracking-id-ads-pixel-impression"
1545					}),
1546					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(PixelUrlSlots, {
1547						label: "Complete impression pixel",
1548						help: "Fired after 100% of the video is viewed.",
1549						urls: rule.completeImpressionPixels,
1550						readOnly: props.readOnly,
1551						onChange: (urls) => set("completeImpressionPixels", urls),
1552						trackingIdPrefix: "tracking-id-ads-pixel-complete"
1553					}),
1554					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(PixelUrlSlots, {
1555						label: "Third-party click pixel",
1556						urls: rule.thirdPartyClickPixels,
1557						readOnly: props.readOnly,
1558						onChange: (urls) => set("thirdPartyClickPixels", urls),
1559						trackingIdPrefix: "tracking-id-ads-pixel-click"
1560					}),
1561					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TextField, {
1562						fullWidth: true,
1563						size: "small",
1564						label: "Click-through URL",
1565						value: rule.clickThroughUrl,
1566						disabled: props.readOnly,
1567						onChange: (event) => set("clickThroughUrl", event.target.value),
1568						inputProps: { "data-tracking-id": "tracking-id-ads-pixel-click-through-input" }
1569					}),
1570					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TextField, {
1571						fullWidth: true,
1572						size: "small",
1573						label: "VAST wrapper",
1574						helperText: "Every wrapper in the file has to come from the same provider, or all be empty.",
1575						value: rule.vastWrapper,
1576						disabled: props.readOnly,
1577						onChange: (event) => set("vastWrapper", event.target.value),
1578						inputProps: { "data-tracking-id": "tracking-id-ads-pixel-vast-wrapper-input" }
1579					})
1580				]
1581			})
1582		]
1583	});
1584};
1585/**
1586* A date input renders nothing at all unless it is given `yyyy-MM-dd`, and Excel writes these files as `7/13/26`,
1587* so the stored date has to be converted or the field looks empty on a row that has one.
1588*/
1589var asDateInputValue = (value) => value ? parsePixelDate(value) ?? "" : "";
1590var panelSx$1 = {
1591	display: "flex",
1592	flexDirection: "column",
1593	gap: 3
1594};
1595var identitySx = {
1596	display: "grid",
1597	gridTemplateColumns: "repeat(auto-fill, minmax(240px, 1fr))",
1598	gap: 2
1599};
1600var tabSx = { flex: 1 };
1601var groupsSx = {
1602	display: "flex",
1603	flexDirection: "column",
1604	gap: 3
1605};
1606var conditionsSx = {
1607	display: "flex",
1608	flexDirection: "column",
1609	gap: 1
1610};
1611var datesSx$1 = {
1612	display: "flex",
1613	gap: 2,
1614	maxWidth: 520
1615};
1616var conditionRowSx = {
1617	display: "flex",
1618	alignItems: "flex-start",
1619	gap: 1
1620};
1621var criterionSx = {
1622	width: 200,
1623	flexShrink: 0
1624};
1625var operatorSx = {
1626	width: 260,
1627	flexShrink: 0
1628};
1629var valueSpacerSx = { flex: 1 };
1630var addSx = { alignSelf: "flex-start" };
1631//#endregion
1632//#region common/adsPixelRulesValidation.ts
1633var mobileDeviceTypes = [
1634	"app",
1635	"web",
1636	"*",
1637	"~"
1638];
1639var mobileDeviceSuffixes = ["_with_device", "_without_device"];
1640/**
1641* Every rule here is one the Java side already enforces - a file it rejects takes the campaign off air.
1642* `laneNames` are the program's budget lanes, where they are known: a lane that is not one of them is a row that
1643* can never match, which nothing in the file itself can reveal.
1644*/
1645var validatePixelRules = (rules, laneNames) => {
1646	const problems = [];
1647	rules.forEach((rule) => {
1648		const at = (message, isWarning) => {
1649			problems.push({
1650				ruleId: rule.id,
1651				message,
1652				isWarning
1653			});
1654		};
1655		if (!rule.placementId.trim()) at("Placement ID is required - it is the row's identity");
1656		rule.conditions.forEach((condition, index) => {
1657			const slot = index + 1;
1658			if (!pixelOperators.includes(condition.operator)) at(`Condition ${slot} has an unknown operator "${condition.operator}"`);
1659			if (!condition.value.trim()) at(`Condition ${slot} has a key but no value`);
1660			const key = condition.key.replace(/^aux\s*/i, "").trim();
1661			if (!/^([1-9]|10)$/.test(key)) at(`Condition ${slot} has key "${condition.key}" - keys are 1 to 10`);
1662			if ((condition.operator === "contains any of" || condition.operator === "contains none of") && !condition.value.includes(";") && condition.value.trim()) at(`Condition ${slot} lists one value; "${condition.operator}" separates values with ";"`, true);
1663		});
1664		if (laneNames?.length && !isPixelWildcard(rule.lane) && rule.lane.trim() !== "~" && !laneNames.includes(rule.lane.trim())) at(`Lane "${rule.lane.trim()}" is not one of this campaign's lanes (${laneNames.join(", ")}), so this row can never match`, true);
1665		if (!isValidMobileDeviceType(rule.mobileDeviceType)) at(`Mobile device type "${rule.mobileDeviceType}" is not app, web, * or ~`);
1666		if (!isPixelWildcard(rule.duration) && rule.duration.trim() !== "~" && !/^\d+$/.test(rule.duration.trim())) at(`Duration "${rule.duration}" is not a whole number of seconds`);
1667		const start = rule.startDate ? parsePixelDate(rule.startDate) : null;
1668		const end = rule.endDate ? parsePixelDate(rule.endDate) : null;
1669		if (rule.startDate && !start) at(`Start date "${rule.startDate}" is not a date`);
1670		if (rule.endDate && !end) at(`End date "${rule.endDate}" is not a date`);
1671		if (start && end && start > end) at("The end date is before the start date");
1672		collectRuleUrls(rule).forEach((url) => {
1673			if (!hasBalancedMacros(url)) at(`"${truncate(url)}" has an unbalanced \${...} macro`);
1674		});
1675	});
1676	duplicates(rules.map((rule) => rule.placementId.trim()).filter(Boolean)).forEach((placementId) => {
1677		problems.push({ message: `Placement ID "${placementId}" is used by more than one row. A duplicate stops this campaign's attribution metadata being generated.` });
1678	});
1679	duplicates(rules.map((rule) => rule.placementName.trim()).filter(Boolean)).forEach((placementName) => {
1680		problems.push({
1681			message: `Placement name "${placementName}" is used by more than one row, so only one of them can be looked up by name.`,
1682			isWarning: true
1683		});
1684	});
1685	const wrapperProviders = rules.filter((rule) => rule.vastWrapper.trim()).map((rule) => ({
1686		placement: rule.placementId || rule.placementName,
1687		provider: wrapperProvider(rule.vastWrapper)
1688	}));
1689	const firstProvider = wrapperProviders[0];
1690	const conflicting = wrapperProviders.find((candidate) => candidate.provider !== firstProvider?.provider);
1691	if (firstProvider && conflicting) problems.push({ message: `Every VAST wrapper in the file must come from one provider, or all be empty. "${firstProvider.placement}" uses ${firstProvider.provider} and "${conflicting.placement}" uses ${conflicting.provider}.` });
1692	return problems;
1693};
1694var isValidMobileDeviceType = (value) => {
1695	const text = value.trim().toLowerCase();
1696	if (!text) return true;
1697	const suffix = mobileDeviceSuffixes.find((candidate) => text.endsWith(candidate));
1698	const base = suffix ? text.slice(0, -suffix.length) : text;
1699	return mobileDeviceTypes.includes(base);
1700};
1701/**
1702* A `$` that is not part of a `${name}` is what the Java check hunts for, and it exists because a legitimate URL
1703* can end in a bare `$` - so the valid macros come out first and only then is a stray one looked for.
1704*/
1705var hasBalancedMacros = (url) => {
1706	const withoutMacros = url.replace(/\$\{[^{}]*\}/g, "");
1707	return !/\$\{|\}|\$[A-Za-z_]/.test(withoutMacros);
1708};
1709var collectRuleUrls = (rule) => [
1710	...rule.impressionPixels,
1711	...rule.thirdPartyClickPixels,
1712	...rule.completeImpressionPixels,
1713	rule.clickThroughUrl,
1714	rule.vastWrapper
1715].filter(Boolean);
1716/** The wrapper's host is what decides the provider;
1716 two wrappers from one host are one provider. */
1717var wrapperProvider = (url) => {
1718	const match = /^[a-z]+:\/\/([^/?]+)/i.exec(url.trim());
1719	return match ? match[1].toLowerCase() : url.trim().toLowerCase();
1720};
1721var duplicates = (values) => {
1722	const seen = /* @__PURE__ */ new Set();
1723	const repeated = /* @__PURE__ */ new Set();
1724	values.forEach((value) => {
1725		if (seen.has(value)) repeated.add(value);
1726		seen.add(value);
1727	});
1728	return Array.from(repeated);
1729};
1730var truncate = (value) => value.length > 60 ? `${value.slice(0, 57)}...` : value;
1731//#endregion
1732//#region client/components/newNav/components/ads/pixels/PixelRulesImportDialog.tsx
1733/**
1734* Paste or upload a file in the ad server's own format and load it in as unsaved rows. The vendor workbook path
1735* stays on the legacy generator page for now - it is an importer of vendor spreadsheets, which is a different job
1736* from editing the file we already own.
1737*/
1738var PixelRulesImportDialog = (props) => {
1739	const [csvText, setCsvText] = (0, import_react.useState)("");
1740	const fileInputRef = (0, import_react.useRef)(null);
1741	(0, import_react.useEffect)(() => {
1742		if (props.open) setCsvText("");
1743	}, [props.open]);
1744	const parsed = csvText.trim() ? parsePixelRulesCsv(csvText) : null;
1745	const blocking = (parsed ? validatePixelRules(parsed.rules) : []).filter((problem) => !problem.isWarning);
1746	const readFile = (file) => {
1747		const reader = new FileReader();
1748		reader.onload = () => setCsvText(String(reader.result ?? ""));
1749		reader.readAsText(file);
1750	};
1751	return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Dialog, {
1752		open: props.open,
1753		onClose: props.onClose,
1754		maxWidth: false,
1755		PaperProps: { sx: paperSx$7 },
1756		children: [
1757			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.TruffleDialogTitle, {
1758				CloseIconButtonProps: {
1759					onClick: props.onClose,
1760					"data-tracking-id": "tracking-id-ads-pixel-import-close-btn"
1761				},
1762				children: "Import a pixel rules CSV"
1763			}),
1764			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.DialogContent, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
1765				sx: contentSx$11,
1766				children: [
1767					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Button, {
1768						variant: "outlined",
1769						size: "medium",
1770						sx: uploadSx,
1771						onClick: () => fileInputRef.current?.click(),
1772						"data-tracking-id": "tracking-id-ads-pixel-import-upload-btn",
1773						children: "Choose a file"
1774					}),
1775					/* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", {
1776						ref: fileInputRef,
1777						type: "file",
1778						accept: ".csv,text/csv",
1779						hidden: true,
1780						onChange: (event) => {
1781							const file = event.target.files?.[0];
1782							if (file) readFile(file);
1783						}
1784					}),
1785					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TextField, {
1786						fullWidth: true,
1787						multiline: true,
1788						size: "small",
1789						minRows: 8,
1790						maxRows: 12,
1791						label: "Or paste the CSV",
1792						value: csvText,
1793						onChange: (event) => setCsvText(event.target.value),
1794						inputProps: {
1795							"data-tracking-id": "tracking-id-ads-pixel-import-paste-input",
1796							style: {
1797								fontFamily: "monospace",
1798								fontSize: "12px"
1799							}
1800						}
1801					}),
1802					parsed && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
1803						variant: "body1",
1804						color: "text.secondary",
1805						children: `${parsed.rules.length} ${parsed.rules.length === 1 ? "placement" : "placements"} read.`
1806					}),
1807					parsed?.isLegacyFormat && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_dist$2.AttentionBox, {
1808						color: "warning",
1809						children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.AttentionBoxTitle, { children: "This is the old file format" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.AttentionBoxContent, { children: "It has no Protocol Version header row, so the ad server reads it as the pre-2018 format. Saving it here writes the current format instead." })]
1810					}),
1811					blocking.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_dist$2.AttentionBox, {
1812						color: "error",
1813						children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.AttentionBoxTitle, { children: `${blocking.length} ${blocking.length === 1 ? "problem" : "problems"} the ad server would reject` }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.AttentionBoxContent, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
1814							sx: problemsSx$1,
1815							children: [blocking.slice(0, MAX_LISTED_PROBLEMS).map((problem, index) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
1816								variant: "body1",
1817								children: problem.message
1818							}, index)), blocking.length >
1818 MAX_LISTED_PROBLEMS && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
1819								variant: "body1",
1820								color: "text.secondary",
1821								children: `and ${blocking.length - MAX_LISTED_PROBLEMS} more.`
1822							})]
1823						}) })]
1824					})
1825				]
1826			}) }),
1827			/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_dist$2.TruffleDialogActions, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Button, {
1828				variant: "text",
1829				size: "large",
1830				onClick: props.onClose,
1831				"data-tracking-id": "tracking-id-ads-pixel-import-cancel-btn",
1832				children: "Cancel"
1833			}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Button, {
1834				variant: "contained",
1835				size: "large",
1836				disabled: !parsed || parsed.rules.length === 0,
1837				onClick: () => {
1838					props.onImport(parsed.rules);
1839					props.onClose();
1840				},
1841				"data-tracking-id": "tracking-id-ads-pixel-import-submit-btn",
1842				children: "Load into the editor"
1843			})] })
1844		]
1845	});
1846};
1847var MAX_LISTED_PROBLEMS = 8;
1848var paperSx$7 = {
1849	width: 720,
1850	maxWidth: "calc(100vw - 64px)"
1851};
1852var contentSx$11 = {
1853	display: "flex",
1854	flexDirection: "column",
1855	gap: 2,
1856	paddingTop: 1
1857};
1858var uploadSx = { alignSelf: "flex-start" };
1859var problemsSx$1 = {
1860	display: "flex",
1861	flexDirection: "column",
1862	gap: .5
1863};
1864//#endregion
1865//#region client/components/newNav/components/ads/pixels/PixelRulesSourceDialog.tsx
1866var import_faCopy = require_faCopy();
1867/** The generated file, as the ad server will read it - so it can be diffed against what the advertiser was sent. */
1868var PixelRulesSourceDialog = (props) => {
1869	const copy = () => {
1870		navigator.clipboard.writeText(props.csvText).then(() => setInfoNotification({ message: "The CSV is on your clipboard" }));
1871	};
1872	return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Dialog, {
1873		open: props.open,
1874		onClose: props.onClose,
1875		maxWidth: false,
1876		PaperProps: { sx: paperSx$6 },
1877		children: [
1878			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.TruffleDialogTitle, {
1879				CloseIconButtonProps: {
1880					onClick: props.onClose,
1881					"data-tracking-id": "tracking-id-ads-pixel-source-close-btn"
1882				},
1883				children: "Generated CSV"
1884			}),
1885			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.DialogContent, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TextField, {
1886				fullWidth: true,
1887				multiline: true,
1888				size: "small",
1889				minRows: 16,
1890				maxRows: 16,
1891				value: props.csvText,
1892				inputProps: {
1893					readOnly: true,
1894					style: {
1895						fontFamily: "monospace",
1896						fontSize: "12px",
1897						whiteSpace: "pre"
1898					}
1899				}
1900			}) }),
1901			/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_dist$2.TruffleDialogActions, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Button, {
1902				variant: "outlined",
1903				size: "large",
1904				startIcon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FontAwesomeIcon, { icon: import_faCopy.faCopy }),
1905				onClick: copy,
1906				"data-tracking-id": "tracking-id-ads-pixel-source-copy-btn",
1907				children: "Copy"
1908			}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Button, {
1909				variant: "contained",
1910				size: "large",
1911				onClick: props.onClose,
1912				"data-tracking-id": "tracking-id-ads-pixel-source-done-btn",
1913				children: "Done"
1914			})] })
1915		]
1916	});
1917};
1918var paperSx$6 = {
1919	width: 900,
1920	maxWidth: "calc(100vw - 64px)"
1921};
1922//#endregion
1923//#region client/components/newNav/components/ads/pixels/PixelRulesEditor.tsx
1924/**
1925* The tracking pixel file as a list of placements rather than a fifty-column grid: a row identifies itself, says
1926* what it targets in one line, and opens for editing in two groups that are never changed together.
1927*/
1928var PixelRulesEditor = (props) => {
1929	const [savedRules, setSavedRules] = (0, import_react.useState)(props.initialRules ?? []);
1930	const [rules, setRules] = (0, import_react.useState)(props.initialRules ?? []);
1931	const [selectedId, setSelectedId] = (0, import_react.useState)(null);
1932	const [isCommentOpen, setIsCommentOpen] = (0, import_react.useState)(false);
1933	const [isImportOpen, setIsImportOpen] = (0, import_react.useState)(false);
1934	const [isSourceOpen, setIsSourceOpen] = (0, import_react.useState)(false);
1935	const [isSimulatorOpen, setIsSimulatorOpen] = (0, import_react.useState)(false);
1936	const problems = (0, import_react.useMemo)(() => validatePixelRules(rules, props.laneNames), [rules, props.laneNames]);
1937	const blocking = (0, import_react.useMemo)(() => problems.filter((problem) => !problem.isWarning), [problems]);
1938	const warnings = (0, import_react.useMemo)(() => problems.filter((problem) => problem.isWarning), [problems]);
1939	const changeCount = countRowChanges(rules, savedRules);
1940	const csvText = (0, import_react.useMemo)(() => serializePixelRulesCsv(rules), [rules]);
1941	const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
1942	const selected = rules.find((rule) => rule.id === selectedId);
1943	const addRule = () => {
1944		const rule = emptyPixelRule(`new-${Date.now()}-${rules.length}`);
1945		setRules([...rules, rule]);
1946		setSelectedId(rule.id);
1947	};
1948	const copyLocation = () => {
1949		navigator.clipboard.writeText(props.location ?? "").then(() => setInfoNotification({ message: "The file's S3 path is on your clipboard" }));
1950	};
1951	const commitSave = async (comment) => {
1952		try {
1953			await props.onSave(csvText, comment);
1954			setSavedRules(rules);
1955			setIsCommentOpen(false);
1956			setSuccessNotification({ message: "Pixel rules saved" });
1957		} catch (error) {
1958			setErrorNotification({ message: error?.message ?? "Saving the pixel rules failed" });
1959		}
1960	};
1961	const saveChrome = props.readOnly || !props.onSave ? null : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsSaveBar, {
1962		changeCount,
1963		saveDisabledReason: blocking.length > 0 ? `Fix ${blocking.length === 1 ? "the problem" : `the ${blocking.length} problems`} above before saving` : void 0,
1964		onCancel: () => {
1965			setRules(savedRules);
1966			setSelectedId(null);
1967		},
1968		onSave: () => setIsCommentOpen(true),
1969		saving: props.saving,
1970		trackingIdPrefix: "tracking-id-ads-pixel"
1971	}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsSaveCommentDialog, {
1972		open: isCommentOpen,
1973		summary: describeAdsChangeCount(changeCount),
1974		warning: props.campaignName ? `The file is what ${props.campaignName}'s ad server reads within a minute of the change landing.` : void 0,
1975		saving: props.saving,
1976		onCancel: () => setIsCommentOpen(false),
1977		onConfirm: (comment) => void commitSave(comment),
1978		trackingIdPrefix: "tracking-id-ads-pixel"
1979	})] });
1980	if (selected) return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
1981		sx: editorSx,
1982		children: [
1983			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Button, {
1984				variant: "text",
1985				size: "small",
1986				sx: backLinkSx$2,
1987				startIcon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FontAwesomeIcon, { icon: import_faAngleLeft.faAngleLeft }),
1988				onClick: () => setSelectedId(null),
1989				"data-tracking-id": "tracking-id-ads-pixel-back-btn",
1990				children: "All placements"
1991			}),
1992			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
1993				variant: "h4",
1994				children: selected.placementName || selected.placementId || "New placement"
1995			}),
1996			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(PixelRuleDetailPanel, {
1997				rule: selected,
1998				laneNames: props.laneNames,
1999				readOnly: props.readOnly,
2000				onChange: (next) => setRules(rules.map((rule) => rule.id === next.id ? next : rule))
2001			}),
2002			saveChrome
2003		]
2004	});
2005	return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
2006		sx: editorSx,
2007		children: [
2008			/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
2009				sx: headerSx$7,
2010				children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
2011					sx: { minWidth: 0 },
2012					children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
2013						variant: "h5",
2014						children: rules.length > 0 ? `Placements (${rules.length})` : "Placements"
2015					}), props.location ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
2016						sx: sourceLineSx,
2017						children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
2018							variant: "helper",
2019							color: "text.secondary",
2020							sx: sourcePrefixSx,
2021							children: "The ad server reads"
2022						}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FrontTrimmedText, {
2023							text: props.location,
2024							tooltip: `Copy ${props.location}`,
2025							variant: "helper",
2026							sx: sourceLinkSx,
2027							onClick: copyLocation,
2028							trackingId: "tracking-id-ads-pixel-location-copy-btn"
2029						})]
2030					}) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
2031						variant: "helper",
2032						color: "text.secondary",
2033						children: describeSource(props)
2034					})]
2035				}), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
2036					sx: headerActionsSx$4,
2037					children: [
2038						/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Button, {
2039							variant: "text",
2040							size: "medium",
2041							onClick: () => setIsSimulatorOpen(!isSimulatorOpen),
2042							"data-tracking-id": "tracking-id-ads-pixel-simulator-btn",
2043							children: isSimulatorOpen ? "Hide simulator" : "Simulate a match"
2044						}),
2045						/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Button, {
2046							variant: "text",
2047							size: "medium",
2048							disabled: rules.length === 0,
2049							onClick: () => setIsSourceOpen(true),
2050							"data-tracking-id": "tracking-id-ads-pixel-source-btn",
2051							children: "View CSV"
2052						}),
2053						!props.readOnly && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Button, {
2054							variant: "outlined",
2055							size: "medium",
2056							onClick: () => setIsImportOpen(true),
2057							"data-tracking-id": "tracking-id-ads-pixel-import-btn",
2058							children: "Import CSV"
2059						}),
2060						!props.readOnly && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Button, {
2061							variant: "contained",
2062							size: "medium",
2063							startIcon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FontAwesomeIcon, { icon: import_faPlus.faPlus }),
2064							onClick: addRule,
2065							"data-tracking-id": "tracking-id-ads-pixel-add-btn",
2066							children: "Add placement"
2067						})
2068					]
2069				})]
2070			}),
2071			blocking.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_dist$2.AttentionBox, {
2072				color: "error",
2073				children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.AttentionBoxTitle, { children: `${blocking.length} ${blocking.length === 1 ? "problem" : "problems"} the ad server would reject` }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.AttentionBoxContent, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Box, {
2074					sx: problemsSx,
2075					children: blocking.map((problem, index) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
2076						variant: "body1",
2077						children: problem.message
2078					}, index))
2079				}) })]
2080			}),
2081			warnings.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.AttentionBox, {
2082				color: "warning",
2083				children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.AttentionBoxContent, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Box, {
2084					sx: problemsSx,
2085					children: warnings.map((problem, index) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
2086						variant: "body1",
2087						children: problem.message
2088					}, index))
2089				}) })
2090			}),
2091			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableContainer, {
2092				component: import_material.Paper,
2093				variant: "outlined",
2094				children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Table, {
2095					size: "small",
2096					stickyHeader: true,
2097					children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableHead, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.TableRow, { children: [
2098						/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, {
2099							sx: (0, import_dist$2.combineSxProps)(headCellSx, placementColumnSx),
2100							children: "Placement"
2101						}),
2102						/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, {
2103							sx: headCellSx,
2104							children: "What it targets"
2105						}),
2106						/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, {
2107							sx: headCellSx,
2108							align: "right",
2109							children: "URLs"
2110						}),
2111						/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, { sx: actionsCellSx }),
2112						/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, { sx: actionsCellSx })
2113					] }) }
2113), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.TableBody, { children: [rules.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableRow, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, {
2114						colSpan: 5,
2115						sx: emptyCellSx$4,
2116						children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
2117							variant: "body1",
2118							color: "text.secondary",
2119							children: "No placements yet. Import the advertiser's CSV, or add one."
2120						})
2121					}) }), rules.map((rule) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.TableRow, {
2122						sx: rowHoverSx,
2123						"data-testid": `testId-ads-pixel-row-${rule.id}`,
2124						children: [
2125							/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.TableCell, {
2126								sx: (0, import_dist$2.combineSxProps)(clickableCellSx$1, placementCellSx),
2127								onClick: () => setSelectedId(rule.id),
2128								children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(FrontTrimmedText, {
2129									text: rule.placementName || "Unnamed placement",
2130									variant: "subtitle2"
2131								}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
2132									variant: "caption",
2133									color: "text.secondary",
2134									children: rule.placementId || "No placement ID"
2135								})]
2136							}),
2137							/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, {
2138								sx: clickableCellSx$1,
2139								onClick: () => setSelectedId(rule.id),
2140								children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
2141									variant: "body1",
2142									color: "text.secondary",
2143									children: describePixelRuleMatch(rule)
2144								})
2145							}),
2146							/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, {
2147								sx: clickableCellSx$1,
2148								align: "right",
2149								onClick: () => setSelectedId(rule.id),
2150								children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
2151									variant: "body1",
2152									children: countPixelRuleUrls(rule)
2153								})
2154							}),
2155							/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, {
2156								sx: actionsCellSx,
2157								children: !props.readOnly && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.TruffleIconButton, {
2158									size: "small",
2159									onClick: () => setRules(rules.filter((candidate) => candidate.id !== rule.id)),
2160									"data-tracking-id": "tracking-id-ads-pixel-remove-btn",
2161									children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FontAwesomeIcon, { icon: import_faTrash.faTrash })
2162								})
2163							}),
2164							/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, {
2165								sx: actionsCellSx,
2166								onClick: () => setSelectedId(rule.id),
2167								children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.SvgIcon, {
2168									sx: chevronSx$2,
2169									children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FontAwesomeIcon, { icon: import_faAngleRight.faAngleRight })
2170								})
2171							})
2172						]
2173					}, rule.id))] })]
2174				})
2175			}),
2176			isSimulatorOpen && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Paper, {
2177				variant: "outlined",
2178				sx: simulatorSx,
2179				children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(PixelMatchSimulator, {
2180					rules,
2181					today
2182				})
2183			}),
2184			saveChrome,
2185			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(PixelRulesImportDialog, {
2186				open: isImportOpen,
2187				onClose: () => setIsImportOpen(false),
2188				onImport: (imported) => {
2189					setRules(imported);
2190					setSelectedId(null);
2191				}
2192			}),
2193			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(PixelRulesSourceDialog, {
2194				open: isSourceOpen,
2195				csvText,
2196				onClose: () => setIsSourceOpen(false)
2197			})
2198		]
2199	});
2200};
2201/**
2202* One line whose end always survives. These names and S3 paths share a long prefix and say what they are at the
2203* end, so it is the front that goes. A right-to-left box is what puts the ellipsis at the left edge; the letters
2204* and digits are strongly left-to-right, so they still read in order inside it. The whole value is in the tooltip.
2205*
2206* `unicode-bidi: plaintext` looks like it belongs here and must not be added: it resolves the direction from the
2207* content, which lays an ASCII value out left-to-right again and moves the ellipsis back to the end.
2208*/
2209var FrontTrimmedText = (props) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Box, {
2210	sx: frontTrimmedRowSx,
2211	children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Tooltip, {
2212		title: props.tooltip ?? props.text,
2213		children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
2214			component: "div",
2215			variant: props.variant,
2216			sx: (0, import_dist$2.combineSxProps)(frontTrimmedSx, props.sx),
2217			onClick: props.onClick,
2218			"data-tracking-id": props.trackingId,
2219			children: props.text
2220		})
2221	})
2222});
2223var describeSource = (props) => {
2224	if (props.campaignName) return "This campaign has no pixel rules file yet - saving creates its first one";
2225	return "Import an advertiser's CSV to read it, or build one from scratch. A campaign's own file is saved from its Tracking pixels panel";
2226};
2227var editorSx = {
2228	display: "flex",
2229	flexDirection: "column",
2230	gap: 2
2231};
2232var backLinkSx$2 = {
2233	alignSelf: "flex-start",
2234	marginBottom: -2
2235};
2236var headerSx$7 = {
2237	display: "flex",
2238	alignItems: "center",
2239	justifyContent: "space-between",
2240	gap: 2
2241};
2242var headerActionsSx$4 = {
2243	display: "flex",
2244	alignItems: "center",
2245	gap: 1,
2246	flexShrink: 0
2247};
2248var problemsSx = {
2249	display: "flex",
2250	flexDirection: "column",
2251	gap: .5
2252};
2253var sourceLineSx = {
2254	display: "flex",
2255	alignItems: "baseline",
2256	gap: .5,
2257	minWidth: 0
2258};
2259var sourcePrefixSx = { flexShrink: 0 };
2260var sourceLinkSx = {
2261	color: "primary.main",
2262	cursor: "pointer"
2263};
2264var frontTrimmedRowSx = {
2265	display: "flex",
2266	minWidth: 0
2267};
2268var frontTrimmedSx = {
2269	direction: "rtl",
2270	minWidth: 0,
2271	maxWidth: "100%",
2272	overflow: "hidden",
2273	textOverflow: "ellipsis",
2274	whiteSpace: "nowrap"
2275};
2276var placementColumnSx = { width: "45%" };
2277var placementCellSx = {
2278	width: "45%",
2279	maxWidth: 0
2280};
2281var clickableCellSx$1 = (0, import_dist$2.combineSxProps)(bodyCellSx, { cursor: "pointer" });
2282var emptyCellSx$4 = {
2283	padding: "32px 16px",
2284	textAlign: "center"
2285};
2286var chevronSx$2 = {
2287	fontSize: "12px !important",
2288	width: "12px !important",
2289	height: "12px !important",
2290	color: "text.disabled",
2291	cursor: "pointer"
2292};
2293var simulatorSx = { padding: 2 };
2294//#endregion
2295//#region node_modules/@fortawesome/pro-regular-svg-icons/faStore.js
2296var require_faStore = /* @__PURE__ */ __commonJSMin(((exports) => {
2297	Object.defineProperty(exports, "__esModule", { value: true });
2298	var prefix = "far";
2299	var iconName = "store";
2300	var width = 512;
2301	var height = 512;
2302	var aliases = [];
2303	var unicode = "f54e";
2304	var svgPathData = "M29.9 81.9C40.4 52 68.6 32 100.3 32l312.2 0c31.7 0 59.9 20 70.4 49.9l18.4 52.6c11 31.5 1 63.9-20.9 84.4l0 189.2c0 39.8-32.2 72-72 72l-304 0c-39.8 0-72-32.2-72-72l0-189.2C10.5 198.4 .5 166 11.5 134.5L29.9 81.9zM80.4 239.8l0 80.2 352 0 0-80.2c-2 .1-3.9 .2-5.9 .2-21.8 0-41.8-8.9-56.2-23.3-14.6 14.4-34.6 23.3-56.9 23.3-22.2 0-42.4-9-57-23.5-14.6 14.5-34.8 23.5-57 23.5-22.3 0-42.3-8.9-56.9-23.3-14.4 14.5-34.3 23.3-56.2 23.3-2 0-4-.1-6-.2zM174.1 80l-7.6 75.7c-1.9 19.4 13.3 36.3 32.8 36.3 18.2 0 33-14.8 33-33l0-79-58.3 0zm106.3 79c0 18.2 14.8 33 33 33 19.5 0 34.8-16.9 32.8-36.3l-7.6-75.7-58.3 0 0 79zm114.9 4.8c1.6 16 15.1 28.2 31.1 28.2 21.6 0 36.7-21.3 29.5-41.6L437.6 97.8C433.9 87.1 423.8 80 412.5 80l-25.6 0 8.4 83.8zM86.4 192c16.1 0 29.5-12.2 31.1-28.2l8.4-83.8-25.6 0C89 80 79 87.1 75.2 97.8L56.8 150.4c-7.1 20.4 8 41.6 29.5 41.6zm-6 216c0 13.3 10.7 24 24 24l304 0c13.3 0 24-10.7 24-24l0-40-352 0 0 40z";
2305	exports.definition = {
2306		prefix,
2307		iconName,
2308		icon: [
2309			width,
2310			height,
2311			aliases,
2312			unicode,
2313			svgPathData
2314		]
2315	};
2316	exports.faStore = exports.definition;
2317	exports.prefix = prefix;
2318	exports.iconName = iconName;
2319	exports.width = width;
2320	exports.height = height;
2321	exports.ligatures = aliases;
2322	exports.unicode = unicode;
2323	exports.svgPathData = svgPathData;
2324	exports.aliases = aliases;
2325}));
2326//#endregion
2327//#region node_modules/@fortawesome/pro-regular-svg-icons/faTowerBroadcast.js
2328var require_faTowerBroadcast = /* @__PURE__ */ __commonJSMin(((exports) => {
2329	Object.defineProperty(exports, "__esModule", { value: true });
2330	var prefix = "far";
2331	var iconName = "tower-broadcast";
2332	var width = 576;
2333	var height = 512;
2334	var aliases = ["broadcast-tower"];
2335	var unicode = "f519";
2336	var svgPathData = "M87.9 11.5c-11.3-6.9-26.1-3.2-33 8.1-24.8 41-39 89.1-39 140.4s14.2 99.4 39 140.4c6.9 11.3 21.6 15 33 8.1s15-21.6 8.1-33C75.7 241.9 64 202.3 64 160S75.7 78.1 96.1 44.4c6.9-11.3 3.2-26.1-8.1-33zm400.1 0c-11.3 6.9-15 21.6-8.1 33 20.4 33.7 32.1 73.3 32.1 115.6s-11.7 81.9-32.1 115.6c-6.9 11.3-3.2 26.1 8.1 33s26.1 3.2 33-8.1c24.8-41 39-89.1 39-140.4S545.8 60.6 521 19.6c-6.9-11.3-21.6-15-33-8.1zM312 210.6c18.9-9 32-28.3 32-50.6 0-30.9-25.1-56-56-56s-56 25.1-
233656 56c0 22.3 13.1 41.6 32 50.6L264 488c0 13.3 10.7 24 24 24s24-10.7 24-24l0-277.4zM180.2 91c7.2-11.2 3.9-26-7.2-33.2s-26-3.9-33.2 7.2c-17.6 27.4-27.8 60-27.8 95s10.2 67.6 27.8 95c7.2 11.2 22 14.4 33.2 7.2s14.4-22 7.2-33.2c-12.8-19.9-20.2-43.6-20.2-69s7.4-49.1 20.2-69zM436.2 65c-7.2-11.2-22-14.4-33.2-7.2s-14.4 22-7.2 33.2c12.8 19.9 20.2 43.6 20.2 69s-7.4 49.1-20.2 69c-7.2 11.2-3.9 26 7.2 33.2s26 3.9 33.2-7.2c17.6-27.4 27.8-60 27.8-95s-10.2-67.6-27.8-95z";
2337	exports.definition = {
2338		prefix,
2339		iconName,
2340		icon: [
2341			width,
2342			height,
2343			aliases,
2344			unicode,
2345			svgPathData
2346		]
2347	};
2348	exports.faTowerBroadcast = exports.definition;
2349	exports.prefix = prefix;
2350	exports.iconName = iconName;
2351	exports.width = width;
2352	exports.height = height;
2353	exports.ligatures = aliases;
2354	exports.unicode = unicode;
2355	exports.svgPathData = svgPathData;
2356	exports.aliases = aliases;
2357}));
2358//#endregion
2359//#region client/components/newNav/components/ads/data/zipcode-geo.meta.json
2360var import_faXmark = require_faXmark();
2361var import_faCircleQuestion = require_faCircleQuestion();
2362var import_faDownload = require_faDownload();
2363var import_faArrowRotateRight = require_faArrowRotateRight();
2364var import_faStore = require_faStore();
2365var import_faTowerBroadcast = require_faTowerBroadcast();
2366var zipcode_geo_meta_default = {
2367	generated_utc: "2026-07-28T11:09:42Z",
2368	generator: "ads-platform:tools/zipdata/build_zipcode_geo.py",
2369	generator_repo: "https://github.com/SundaySky/ads-platform",
2370	rows: 41637,
2371	distinct_dmas: 210,
2372	bytes_raw: 1202184,
2373	bytes_gzip: 375145,
2374	coordinate_decimals: 5,
2375	max_zips_on_one_coordinate: 53,
2376	zips_in_clusters_over_10: 893,
2377	census_rows_far_from_geonames: 10,
2378	sources: {
2379		"centroid_primary": {
2380			"name": "US Census ZIP Code Tabulation Area Gazetteer",
2381			"url": "https://www2.census.gov/geo/docs/maps-data/data/gazetteer/2025_Gazetteer/2025_Gaz_zcta_national.zip",
2382			"license": "public domain (US Government work)",
2383			"rows_used": 33791
2384		},
2385		"centroid_crosswalk": {
2386			"name": "HRSA ZIP Code to ZCTA Crosswalk",
2387			"url": "https://data.hrsa.gov/DataDownload/GeoCareNavigator/ZIP%20Code%20to%20ZCTA%20Crosswalk.xlsx",
2388			"license": "public domain (US Government work)",
2389			"rows_used": 7175,
2390			"rejected_over_100_miles_from_geonames": 4,
2391			"note": "PO-box and point ZIPs inherit the Census centroid of the ZCTA that contains them"
2392		},
2393		"centroid_fallback": {
2394			"name": "GeoNames US postal codes",
2395			"url": "https://download.geonames.org/export/zip/US.zip",
2396			"license": "CC BY 4.0 -- attribution to geonames.org required",
2397			"rows_used": 671,
2398			"low_confidence_rows": 8
2399		},
2400		"dma": {
2401			"name": "ad server zipToDma.csv",
2402			"path": "ads-server/sundaysky-ad-server-service/src/main/resources/zipToDma.csv",
2403			"license": "internal; Nielsen DMA definitions are proprietary",
2404			"zips_without_dma": 929
2405		}
2406	},
2407	attribution_required: "This product includes GeoNames data, licensed CC BY 4.0 (https://www.geonames.org/)."
2408};
2409//#endregion
2410//#region client/components/newNav/components/ads/zipcodeGeoParse.ts
2411var DEGREES_TO_RADIANS$1 = Math.PI / 180;
2412var CARRIAGE_RETURN = 13;
2413var parseZipcodeGeoCsv = (csvText) => {
2414	const lines = csvText.split("\n");
2415	const zips = [];
2416	const dmas = [];
2417	const latDeg = new Float64Array(lines.length);
2418	const lonDeg = new Float64Array(lines.length);
2419	const cosLat = new Float64Array(lines.length);
2420	const indexByZip = /* @__PURE__ */ new Map();
2421	let count = 0;
2422	for (let lineIndex = 1; lineIndex < lines.length; lineIndex++) {
2423		let line = lines[lineIndex];
2424		if (line.charCodeAt(line.length - 1) === CARRIAGE_RETURN) line = line.slice(0, -1);
2425		if (!line) continue;
2426		const firstComma = line.indexOf(",");
2427		const secondComma = line.indexOf(",", firstComma + 1);
2428		const thirdComma = line.indexOf(",", secondComma + 1);
2429		if (thirdComma < 0) throw new Error(`Malformed ZIP code data on line ${lineIndex + 1}`);
2430		const lat = Number(line.slice(firstComma + 1, secondComma));
2431		const lon = Number(line.slice(secondComma + 1, thirdComma));
2432		if (Number.isNaN(lat) || Number.isNaN(lon)) throw new Error(`Malformed ZIP code data on line ${lineIndex + 1}`);
2433		const zip = line.slice(0, firstComma);
2434		zips.push(zip);
2435		dmas.push(line.slice(thirdComma + 1));
2436		latDeg[count] = lat;
2437		lonDeg[count] = lon;
2438		cosLat[count] = Math.cos(lat * DEGREES_TO_RADIANS$1);
2439		indexByZip.set(zip, count);
2440		count++;
2441	}
2442	const latOrder = new Int32Array(count);
2443	for (let index = 0; index < count; index++) latOrder[index] = index;
2444	latOrder.sort((a, b) => latDeg[a] - latDeg[b]);
2445	const latSorted = new Float64Array(count);
2446	for (let position = 0; position < count; position++) latSorted[position] = latDeg[latOrder[position]];
2447	return {
2448		zips,
2449		dmas,
2450		latDeg: latDeg.subarray(0, count),
2451		lonDeg: lonDeg.subarray(0, count),
2452		cosLat: cosLat.subarray(0, count),
2453		latOrder,
2454		latSorted,
2455		indexByZip
2456	};
2457};
2458//#endregion
2459//#region client/components/newNav/components/ads/zipcodeGeoData.ts
2460var zipcodeGeoGeneratedUtc = zipcode_geo_meta_default.generated_utc;
2461var zipcodeGeoRowCount = zipcode_geo_meta_default.rows;
2462zipcode_geo_meta_default.attribution_required;
2463var zipcodeGeoAttributionUrl = "https://www.geonames.org/";
2464var zipcodeGeoGeneratorRepo = zipcode_geo_meta_default.generator_repo;
2465var ZIPCODE_GEO_DATA_URL = "/rest/adsSpace/zipcodeGeoData";
2466var cachedData;
2467/**
2468* Goes through the shared REST client rather than `fetch`: the route is behind the platform's permission
2469* check, which reads the bearer token and account headers that client attaches. The cache is cleared on
2470* failure so calling this again is a real retry, and a dropped connection gets a message that says what to do.
2471*/
2472var loadZipcodeGeoData = () => {
2473	cachedData ??= getService("commonServices").get([EDITING_MODE, STAGE_MODE], ZIPCODE_GEO_DATA_URL).then(parseZipcodeGeoCsv).catch((error) => {
2474		cachedData = void 0;
2475		if (!axios.isAxiosError(error)) throw error;
2476		throw new Error(error.response ? `The ZIP code data could not be loaded (HTTP ${error.response.status}).` : "The ZIP code data could not be loaded. Check your connection and try again.");
2477	});
2478	return cachedData;
2479};
2480//#endregion
2481//#region client/components/newNav/components/ads/ZipDataHelpDialog.tsx
2482var GENERATOR_DIR = "tools/zipdata";
2483var HUB_DATA_DIR = "smartvideo-hub/client/components/newNav/components/ads/data";
2484var S3_KEY = "hub/zipcode-geo.csv";
2485var REGENERATE_COMMANDS = [
2486	"cd tools/zipdata",
2487	"python3 build_zipcode_geo.py --self-check",
2488	"python3 build_zipcode_geo.py"
2489].join("\n");
2490var PUBLISH_COMMANDS = [
2491	"# from the ads-platform checkout, with $HUB pointing at the studio repo",
2492	`aws s3 cp data/zipcode-geo.csv s3://sundaysky-ads-test/${S3_KEY}`,
2493	`aws s3 cp data/zipcode-geo.csv s3://ads.sundaysky.com/${S3_KEY}`,
2494	`cp data/zipcode-geo.meta.json $HUB/${HUB_DATA_DIR}/`
2495].join("\n");
2496var ZipDataHelpDialog = ({ open, onClose }) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Dialog, {
2497	open,
2498	onClose,
2499	maxWidth: "md",
2500	fullWidth: true,
2501	"data-testid": "testId-zip-data-help-dialog",
2502	children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.DialogTitle, {
2503		sx: titleSxProps,
2504		children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
2505			sx: titleTextSxProps,
2506			children: "Where this ZIP code data comes from"
2507		}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.IconButton, {
2508			onClick: onClose,
2509			"aria-label": "Close",
2510			size: "small",
2511			sx: closeButtonSxProps,
2512			"data-testid": "testId-zip-data-help-close",
2513			"data-tracking-id": "tracking-id-zip-data-help-close",
2514			children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.SvgIcon, {
2515				fontSize: "small",
2516				children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FontAwesomeIcon, { icon: import_faXmark.faXmark })
2517			})
2518		})]
2519	}
2519), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.DialogContent, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
2520		sx: bodySxProps,
2521		children: [
2522			/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Typography, {
2523				variant: "body2",
2524				children: [
2525					"The ",
2526					zipcodeGeoRowCount.toLocaleString(),
2527					" ZIP codes this tool searches are a vendored data file, last built on ",
2528					new Date(zipcodeGeoGeneratedUtc).toLocaleDateString(),
2529					". It is not generated in this repo. It is built in the ",
2530					/* @__PURE__ */ (0, import_jsx_runtime.jsx)("b", { children: "ads-platform" }),
2531					" repo, under ",
2532					/* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", { children: GENERATOR_DIR }),
2533					":",
2534					" ",
2535					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Link, {
2536						href: zipcodeGeoGeneratorRepo,
2537						target: "_blank",
2538						rel: "noopener noreferrer",
2539						children: zipcodeGeoGeneratorRepo
2540					})
2541				]
2542			}),
2543			/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Typography, {
2544				variant: "body2",
2545				children: [
2546					"It lives there because the DMA column has to come from the ad server's own",
2547					" ",
2548					/* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", { children: "zipToDma.csv" }),
2549					", which the generator reads as a sibling path inside that monorepo. If this tool built its own copy, the two would drift and the whitelists would stop matching what the ad server enforces at bid time."
2550				]
2551			}),
2552			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
2553				sx: headingSxProps,
2554				children: "1. Rebuild the data"
2555			}),
2556			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
2557				variant: "body2",
2558				children: "In an ads-platform checkout. It downloads the Census gazetteer, the HRSA ZIP-to-ZCTA crosswalk and GeoNames, so it needs network access. No pip install required."
2559			}),
2560			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Box, {
2561				component: "pre",
2562				sx: codeSxProps,
2563				children: REGENERATE_COMMANDS
2564			}),
2565			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
2566				sx: headingSxProps,
2567				children: "2. Publish it"
2568			}),
2569			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
2570				variant: "body2",
2571				children: "The data file goes to the ads S3 bucket, which this app reads it from - it is not committed. Its metadata is committed, and the two travel together: uploaded without it, this dialog and the footer keep showing the old row count and build date. A new upload is picked up when the server next restarts."
2572			}),
2573			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Box, {
2574				component: "pre",
2575				sx: codeSxProps,
2576				children: PUBLISH_COMMANDS
2577			}),
2578			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
2579				sx: headingSxProps,
2580				children: "3. Check it before committing"
2581			}),
2582			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
2583				variant: "body2",
2584				children: "Run the tool's tests in this repo. The generator also prints a per-source row count and any crosswalk joins it rejected as implausible. Read that output: a large swing in the source mix, or a jump in how many ZIPs share a single coordinate, means a source changed shape."
2585			}),
2586			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Box, {
2587				component: "pre",
2588				sx: codeSxProps,
2589				children: "npm run test-client -- test/client/components/newNav/components/ads --no-coverage"
2590			}),
2591			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
2592				variant: "caption",
2593				color: "text.secondary",
2594				children: "Coordinates are ZCTA centroids, so PO-box-only ZIPs sit at the centre of the ZCTA that contains them rather than at their own post office. Expect sub-mile differences against tools that geocode the post office itself, and occasional disagreement right at a radius boundary."
2595			})
2596		]
2597	}) })]
2598});
2599var titleSxProps = {
2600	display: "flex",
2601	alignItems: "flex-start",
2602	justifyContent: "space-between",
2603	gap: "16px"
2604};
2605var titleTextSxProps = {
2606	fontSize: "20px",
2607	fontWeight: 600,
2608	lineHeight: 1.5
2609};
2610var closeButtonSxProps = {
2611	color: (theme) => theme.palette.text.secondary,
2612	marginTop: "-4px"
2613};
2614var bodySxProps = {
2615	display: "flex",
2616	flexDirection: "column",
2617	gap: "12px"
2618};
2619var headingSxProps = {
2620	fontSize: "14px",
2621	fontWeight: 600,
2622	marginTop: "4px"
2623};
2624var codeSxProps = {
2625	backgroundColor: (theme) => theme.palette.action.hover,
2626	borderRadius: "4px",
2627	padding: "12px",
2628	margin: 0,
2629	fontSize: "12px",
2630	fontFamily: "monospace",
2631	whiteSpace: "pre",
2632	overflowX: "auto"
2633};
2634//#endregion
2635//#region client/components/newNav/components/ads/zipRadiusDma.ts
2636var import_dist = require_dist$2();
2637var EARTH_RADIUS_MILES = 3958.7613;
2638var MILES_PER_DEGREE_LAT = Math.PI * EARTH_RADIUS_MILES / 180;
2639var DEGREES_TO_RADIANS = Math.PI / 180;
2640var BOX_MARGIN = 1.001;
2641var TIE_EPSILON_MILES = 1e-9;
2642var MAX_STORE_ROWS = 2e4;
2643var STORE_NUMBER_COLUMN = "storenumber";
2644var ZIP_CODE_COLUMN = "zipcode";
2645/** Trim, drop the ZIP+4 suffix, then left-pad: Excel turns 00501 into the number 501. */
2646var normalizeZip = (rawZip) => {
2647	const withoutPlusFour = rawZip.trim().split("-")[0].trim();
2648	if (!/^\d{1,5}$/.test(withoutPlusFour)) return;
2649	return withoutPlusFour.padStart(5, "0");
2650};
2651/** Read here rather than handing the File to papaparse: its File path pulls in a Node stream shim. */
2652var readFileText = (file) => new Promise((resolve, reject) => {
2653	const reader = new FileReader();
2654	reader.addEventListener("load", () => resolve(String(reader.result)));
2655	reader.addEventListener("error", () => reject(/* @__PURE__ */ new Error("The file could not be read.")));
2656	reader.readAsText(file);
2657});
2658/** The file is read in the browser; nothing is uploaded. */
2659var parseStoreCsv = async (input) => {
2660	const { header, csvData } = await (0, import_dist.csvParse)({
2661		input: typeof input === "string" ? input : await readFileText(input),
2662		skipValueSets: true
2663	});
2664	const columnKey = (name) => name.replace(/^\uFEFF/, "").trim().toLowerCase();
2665	const storeNumberIndex = (header ?? []).findIndex((name) => columnKey(name) === STORE_NUMBER_COLUMN);
2666	const zipCodeIndex = (header ?? []).findIndex((name) => columnKey(name) === ZIP_CODE_COLUMN);
2667	if (storeNumberIndex < 0 || zipCodeIndex < 0) throw new Error("The file must have a StoreNumber column and a ZipCode column.");
2668	const stores = csvData.map((row) => ({
2669		storeNumber: String(row[storeNumberIndex] ?? "").trim(),
2670		zipCode: String(row[zipCodeIndex] ?? "").trim()
2671	})).filter((store) => store.storeNumber || store.zipCode);
2672	if (!stores.length) throw new Error("The file has no store rows.");
2673	if (stores.length > 2e4) throw new Error(`The file has ${stores.length.toLocaleString()} rows; the limit is ${MAX_STORE_ROWS.toLocaleString()}.`);
2674	return stores;
2675};
2676var normalizeLongitude = (longitude) => ((longitude + 180) % 360 + 360) % 360 - 180;
2677/**
2678* The window can straddle the date line - ZIP 96507 sits at +178.88 while Alaska is negative -
2679* in which case the test flips to "or". Computed once per store; the inner loop only compares.
2680*/
2681var longitudeWindow = (centerLongitude, halfWidthDeg) => {
2682	const low = centerLongitude - halfWidthDeg;
2683	const high = centerLongitude + halfWidthDeg;
2684	const normalizedLow = normalizeLongitude(low);
2685	const normalizedHigh = normalizeLongitude(high);
2686	return {
2687		coversAllLongitudes: high - low >= 360,
2688		low: normalizedLow,
2689		high: normalizedHigh,
2690		wrapsAntimeridian: normalizedLow > normalizedHigh
2691	};
2692};
2693/** First position whose value is >= target, in an ascending array. */
2694var lowerBound = (values, target) => {
2695	let low = 0;
2696	let high = values.length;
2697	while (low < high) {
2698		const middle = low + high >>> 1;
2699		if (values[middle] < target) low = middle + 1;
2700		else high = middle;
2701	}
2702	return low;
2703};
2704var computeZipRadiusDma = (geoData, stores, radiusMiles, includeDmas) => {
2705	const { zips, dmas, latDeg, lonDeg, cosLat, latOrder, latSorted, indexByZip } = geoData;
2706	const nearestByZip = /* @__PURE__ */ new Map();
2707	const unresolvedStores = [];
2708	for (const store of stores) {
2709		const normalizedZip = normalizeZip(store.zipCode);
2710		const storeIndex = normalizedZip === void 0 ? void 0 : indexByZip.get(normalizedZip);
2711		if (storeIndex === void 0 || !store.storeNumber) {
2712			unresolvedStores.push(store);
2713			continue;
2714		}
2715		const storeLatDeg = latDeg[storeIndex];
2716		const storeLonDeg = lonDeg[storeIndex];
2717		const storeCosLat = cosLat[storeIndex];
2718		const storeLatRad = storeLatDeg * DEGREES_TO_RADIANS;
2719		const halfHeightDeg = radiusMiles / MILES_PER_DEGREE_LAT * BOX_MARGIN;
2720		const { coversAllLongitudes, low: lonLow, high: lonHigh, wrapsAntimeridian } = longitudeWindow(storeLonDeg, Math.min(180, radiusMiles / (MILES_PER_DEGREE_LAT * Math.max(storeCosLat, 1e-6)) * BOX_MARGIN));
2721		const maxLatDeg = storeLatDeg + halfHeightDeg;
2722		for (let position = lowerBound(latSorted, storeLatDeg - halfHeightDeg); position < latSorted.length && latSorted[position] <= maxLatDeg; position++) {
2723			const index = latOrder[position];
2724			const longitude = lonDeg[index];
2725			if (!(coversAllLongitudes || (wrapsAntimeridian ? longitude >= lonLow || longitude <= lonHigh : longitude >= lonLow && longitude <= lonHigh))) continue;
2726			const halfDeltaLat = (latDeg[index] * DEGREES_TO_RADIANS - storeLatRad) / 2;
2727			const halfDeltaLon = (lonDeg[index] - storeLonDeg) * DEGREES_TO_RADIANS / 2;
2728			const sinHalfDeltaLat = Math.sin(halfDeltaLat);
2729			const sinHalfDeltaLon = Math.sin(halfDeltaLon);
2730			const haversine = sinHalfDeltaLat * sinHalfDeltaLat + storeCosLat * cosLat[index] * sinHalfDeltaLon * sinHalfDeltaLon;
2731			const distanceMiles = 2 * EARTH_RADIUS_MILES * Math.asin(Math.min(1, Math.sqrt(haversine)));
2732			if (distanceMiles > radiusMiles) continue;
2733			const zip = zips[index];
2734			const nearest = nearestByZip.get(zip);
2735			if (!nearest || distanceMiles < nearest.distanceMiles - TIE_EPSILON_MILES || Math.abs(distanceMiles - nearest.distanceMiles) <= TIE_EPSILON_MILES && store.storeNumber < nearest.storeNumber) nearestByZip.set(zip, {
2736				storeNumber: store.storeNumber,
2737				storeZip: normalizedZip,
2738				distanceMiles
2739			});
2740		}
2741	}
2742	const matchedZips = [...nearestByZip.keys()].sort();
2743	const matchedDmas = includeDmas ? [...new Set(matchedZips.map((zip) => dmas[indexByZip.get(zip)]).filter(Boolean))].sort((a, b) => Number(a) - Number(b)) : [];
2744	const zipsWithoutDma = includeDmas ? matchedZips.filter((zip) => !dmas[indexByZip.get(zip)]) : [];
2745	return {
2746		radiusMiles,
2747		storesInFile: stores.length,
2748		unresolvedStores,
2749		matchedZips,
2750		matchedDmas,
2751		zipsWithoutDma,
2752		nearestByZip
2753	};
2754};
2755var buildZipWhitelistCsv = (result) => `mode,field,value\n${result.matchedZips.map((zip) => `whitelist,ZIP_CODE,${zip}\n`).join("")}`;
2756var buildDmaWhitelistCsv = (result) => `mode,field,value\n${result.matchedDmas.map((dma) => `whitelist,DMA,${dma}\n`).join("")}`;
2757/**
2758* ZIP and DMA rows in one file. The ad server combines location fields with AND, so this can only
2759* narrow delivery relative to either list on its own (spec 9.4) - it exists because it was asked
2760* for, not because it is the safe default.
2761*/
2762var buildCombinedWhitelistCsv = (result) => `mode,field,value\n${result.matchedZips.map((zip) => `whitelist,ZIP_CODE,${zip}\n`).join("")}${result.matchedDmas.map((dma) => `whitelist,DMA,${dma}\n`).join("")}`;
2763var buildEligibleCodesTxt = (result) => result.matchedZips.map((zip) => `${zip}\n`).join("");
2764/**
2765* The JS dictionary the old Java tool emitted: a module exporting matched ZIP -> nearest store
2766* number. The leading "ViewerZipcode": "StoreNumber" pair is that file's CSV header carried into
2767* the map; it is kept so this file stays interchangeable with the ones already in use. ZIP codes
2768* only - the DMA option does not affect it.
2769*/
2770var buildDictionaryJs = (result) => {
2771	return `module.exports = {\n${[["ViewerZipcode", "StoreNumber"], ...result.matchedZips.map((zip) => [zip, result.nearestByZip.get(zip).storeNumber])].map(([key, value]) => `"${key}": "${value}"`).join(",\n")}\n}`;
2772};
2773var buildViewerZipcodesCsv = (result) => "ViewerZipcode,StoreZipCode,StoreNumber,distanceToStoreInMiles\n" + result.matchedZips.map((zip) => {
2774	const nearest = result.nearestByZip.get(zip);
2775	return `${zip},${nearest.storeZip},${nearest.storeNumber},${nearest.distanceMiles.toFixed(2)}\n`;
2776}).join("");
2777//#endregion
2778//#region client/components/newNav/components/ads/ZipRadiusDmaTool.tsx
2779var DEFAULT_RADIUS_MILES = "10";
2780var MAX_FILE_SIZE_MB = 5;
2781var MAX_FILE_SIZE_BYTES = MAX_FILE_SIZE_MB * 1024 * 1024;
2782var MAX_LISTED_ITEMS = 20;
2783var DOWNLOADABLE_FILES = [
2784	{
2785		id: "zip-whitelist",
2786		label: "ZIP whitelist",
2787		description: "Ad Server whitelist CSV, listing matching ZIP code only.",
2788		fileNamePart: "geo_whitelist_zip",
2789		contentType: "text/csv",
2790		build: buildZipWhitelistCsv
2791	},
2792	{
2793		id: "dma-whitelist",
2794		label: "DMA whitelist",
2795		description: "Ad Server whitelist CSV, listing matching DMAs only.",
2796		fileNamePart: "geo_whitelist_dma",
2797		contentType: "text/csv",
2798		build: buildDmaWhitelistCsv
2799	},
2800	{
2801		id: "combined-whitelist",
2802		label: "ZIP + DMA whitelist",
2803		description: "Ad Server whitelist CSV with both ZIP code and DMA rows. The Ad Server ANDs location fields, so a viewer must match a ZIP and a DMA - this narrows delivery, it does not broaden it.",
2804		fileNamePart: "geo_whitelist_zip_dma",
2805		contentType: "text/csv",
2806		build: buildCombinedWhitelistCsv
2807	},
2808	{
2809		id: "eligible-codes",
2810		label: "Eligible codes",
2811		description: "Plain list of the matched ZIP codes, one per line, with no header.",
2812		fileNamePart: "eligible_codes",
2813		contentType: "text/plain",
2814		build: buildEligibleCodesTxt
2815	},
2816	{
2817		id: "viewer-zipcodes",
2818		label: "ZIP to nearest store",
2819		description: "CSV of the matched ZIP codes with the distance from each one to its nearest store.",
2820		fileNamePart: "viewer_zipcodes",
2821		contentType: "text/csv",
2822		build: buildViewerZipcodesCsv
2823	},
2824	{
2825		id: "dictionary",
2826		label: "Dictionary (JS)",
2827		description: "JavaScript module mapping of ZIP codes to store (presumably used by Ads-FE).",
2828		fileNamePart: "dictionary",
2829		contentType: "text/plain",
2830		build: buildDictionaryJs
2831	}
2832];
2833var SAMPLE_STORE_CSV = "StoreNumber,ZipCode\n1,10001\n2,60601\n3,90210\n";
2834var downloadSampleStoreCsv = () =>
2834 DownloadUtils.createAndDownloadBlobFile("sample_stores.csv", SAMPLE_STORE_CSV, "text/csv");
2835var formatFileSize = (bytes) => bytes < 1024 ? `${bytes.toLocaleString()} bytes` : `${Math.round(bytes / 1024).toLocaleString()} KB`;
2836var listItems = (items) => items.length > MAX_LISTED_ITEMS ? `${items.slice(0, MAX_LISTED_ITEMS).join(", ")} and ${items.length - MAX_LISTED_ITEMS} more` : items.join(", ");
2837var ZipRadiusDmaFooter = () => {
2838	const [isHelpOpen, setIsHelpOpen] = (0, import_react.useState)(false);
2839	return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
2840		sx: footerSxProps,
2841		children: [
2842			/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Typography, {
2843				variant: "helper",
2844				color: "text.secondary",
2845				"data-testid": "testId-zip-radius-dma-footer",
2846				children: [
2847					zipcodeGeoRowCount.toLocaleString(),
2848					" ZIP codes, updated ",
2849					new Date(zipcodeGeoGeneratedUtc).toLocaleDateString(),
2850					".",
2851					" ",
2852					"Store files stay in your browser; nothing is uploaded.",
2853					" ",
2854					"This product includes GeoNames data, licensed CC BY 4.0 (",
2855					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Link, {
2856						href: zipcodeGeoAttributionUrl,
2857						target: "_blank",
2858						rel: "noopener noreferrer",
2859						children: zipcodeGeoAttributionUrl
2860					}),
2861					")."
2862				]
2863			}),
2864			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.IconButton, {
2865				onClick: () => setIsHelpOpen(true),
2866				"aria-label": "Where this ZIP code data comes from",
2867				size: "small",
2868				sx: helpButtonSxProps,
2869				"data-testid": "testId-zip-radius-dma-help",
2870				"data-tracking-id": "tracking-id-zip-radius-dma-help",
2871				children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.SvgIcon, {
2872					fontSize: "small",
2873					children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FontAwesomeIcon, { icon: import_faCircleQuestion.faCircleQuestion })
2874				})
2875			}),
2876			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(ZipDataHelpDialog, {
2877				open: isHelpOpen,
2878				onClose: () => setIsHelpOpen(false)
2879			})
2880		]
2881	});
2882};
2883var ZipRadiusDmaTool = () => {
2884	const [geoData, setGeoData] = (0, import_react.useState)();
2885	const [loadError, setLoadError] = (0, import_react.useState)();
2886	const [storeFile, setStoreFile] = (0, import_react.useState)();
2887	const [radiusMiles, setRadiusMiles] = (0, import_react.useState)(DEFAULT_RADIUS_MILES);
2888	const [includeDmas, setIncludeDmas] = (0, import_react.useState)(true);
2889	const [isComputing, setIsComputing] = (0, import_react.useState)(false);
2890	const [error, setError] = (0, import_react.useState)();
2891	const [result, setResult] = (0, import_react.useState)();
2892	const isItemDragged = useGlobalDragDetector();
2893	const fileInputRef = (0, import_react.useRef)();
2894	const isMountedRef = (0, import_react.useRef)(true);
2895	const loadGeoData = (0, import_react.useCallback)(() => {
2896		setLoadError(void 0);
2897		loadZipcodeGeoData().then((data) => isMountedRef.current && setGeoData(data)).catch((loadFailure) => isMountedRef.current && setLoadError(loadFailure.message));
2898	}, []);
2899	(0, import_react.useEffect)(() => {
2900		isMountedRef.current = true;
2901		loadGeoData();
2902		return () => {
2903			isMountedRef.current = false;
2904		};
2905	}, [loadGeoData]);
2906	const acceptFile = (file) => {
2907		setResult(void 0);
2908		setStoreFile(void 0);
2909		if (!file) return;
2910		if (!/\.csv$/i.test(file.name)) {
2911			setError("That is not a CSV file.");
2912			return;
2913		}
2914		if (file.size > MAX_FILE_SIZE_BYTES) {
2915			setError(`The file is larger than the ${MAX_FILE_SIZE_MB} MB limit.`);
2916			return;
2917		}
2918		setError(void 0);
2919		setStoreFile(file);
2920	};
2921	const onDrop = (acceptedFiles, rejections) => {
2922		if (rejections.length) {
2923			acceptFile(void 0);
2924			setError("Drop a single CSV file.");
2925			return;
2926		}
2927		acceptFile(acceptedFiles[0]);
2928	};
2929	const { isDragActive, getRootProps } = useDropzone({
2930		multiple: false,
2931		noClick: true,
2932		noKeyboard: true,
2933		onDrop
2934	});
2935	const radius = Number(radiusMiles);
2936	const isRadiusValid = Number.isInteger(radius) && radius >= 1 && radius <= 500;
2937	const canGenerate = Boolean(storeFile) && isRadiusValid && Boolean(geoData) && !isComputing;
2938	const onGenerate = async () => {
2939		setIsComputing(true);
2940		setError(void 0);
2941		setResult(void 0);
2942		try {
2943			const stores = await parseStoreCsv(storeFile);
2944			setResult(computeZipRadiusDma(geoData, stores, radius, includeDmas));
2945		} catch (generateFailure) {
2946			setError(generateFailure.message);
2947		} finally {
2948			setIsComputing(false);
2949		}
2950	};
2951	const onDownload = (file) => {
2952		const baseName = storeFile.name.replace(/\.csv$/i, "");
2953		const extension = file.contentType === "text/plain" ? "txt" : "csv";
2954		const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
2955		DownloadUtils.createAndDownloadBlobFile(`${baseName}_${file.fileNamePart}_${result.radiusMiles}mi_${date}.${extension}`, file.build(result), file.contentType);
2956	};
2957	const hasResults = result && result.matchedZips.length > 0;
2958	const dmaOnlyFileIds = /* @__PURE__ */ new Set(["dma-whitelist", "combined-whitelist"]);
2959	const downloads = DOWNLOADABLE_FILES.filter((file) => includeDmas || !dmaOnlyFileIds.has(file.id));
2960	return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
2961		sx: rootSxProps,
2962		"data-testid": "testId-zip-radius-dma-tool",
2963		children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Paper, {
2964			variant: "outlined",
2965			sx: inputPanelSxProps,
2966			children: [
2967				/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
2968					variant: "h5",
2969					children: "Store list"
2970				}),
2971				/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_dist$2.Dropzone, {
2972					sx: dropzoneSxProps,
2973					hint: isItemDragged,
2974					active: isDragActive,
2975					error: Boolean(error) && !storeFile,
2976					"data-testid": "testId-zip-radius-dma-dropzone",
2977					...getRootProps(),
2978					children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", {
2979						ref: fileInputRef,
2980						type: "file",
2981						accept: ".csv,text/csv",
2982						hidden: true,
2983						onChange: (event) => acceptFile(event.target.files?.[0]),
2984						"data-testid": "testId-file-input"
2985					}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.UploadFile, {
2986						sx: uploadFileSxProps,
2987						primaryText: storeFile ? storeFile.name : "Drop a store CSV here or",
2988						secondaryText: storeFile ? formatFileSize(storeFile.size) : void 0,
2989						actions: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.UploadFileButton, {
2990							onClick: () => fileInputRef.current.click(),
2991							"data-testid": "testId-zip-radius-dma-choose-file",
2992							"data-tracking-id": "tracking-id-zip-radius-dma-choose-file",
2993							children: storeFile ? "Replace store file" : "Choose store file"
2994						})
2995					})]
2996				}),
2997				/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Typography, {
2998					variant: "helper",
2999					color: "text.secondary",
3000					children: [
3001						"Two columns: StoreNumber and ZipCode. Up to ",
3002						MAX_FILE_SIZE_MB,
3003						" MB and ",
3004						MAX_STORE_ROWS.toLocaleString(),
3005						" rows.",
3006						" ",
3007						/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Link, {
3008							component: "button",
3009							type: "button",
3010							variant: "helper",
3011							onClick: downloadSampleStoreCsv,
3012							"data-testid": "testId-zip-radius-dma-sample-file",
3013							"data-tracking-id": "tracking-id-zip-radius-dma-sample-file",
3014							children: "Sample file"
3015						})
3016					]
3017				}),
3018				/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
3019					variant: "h5",
3020					sx: sectionHeadingSxProps,
3021					children: "Catchment"
3022				}),
3023				/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
3024					sx: optionsRowSxProps,
3025					children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TextField, {
3026						label: "Radius (miles)",
3027						type: "number",
3028						size: "small",
3029						value: radiusMiles,
3030						onChange: (event) => setRadiusMiles(event.target.value),
3031						error: Boolean(radiusMiles) && !isRadiusValid,
3032						sx: radiusFieldSxProps,
3033						"data-testid": "testId-zip-radius-dma-radius",
3034						"data-tracking-id": "tracking-id-zip-radius-dma-radius"
3035					}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.FormControlLabel, {
3036						label: "Include DMAs",
3037						sx: includeDmasSxProps,
3038						control: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Checkbox, {
3039							checked: includeDmas,
3040							onChange: (event) => setIncludeDmas(event.target.checked),
3041							"data-testid": "testId-zip-radius-dma-include-dmas",
3042							"data-tracking-id": "tracking-id-zip-radius-dma-include-dmas"
3043						})
3044					})]
3045				}),
3046				/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Typography, {
3047					variant: "helper",
3048					color: isRadiusValid ? "text.secondary" : "error.main",
3049					children: [
3050						"Whole miles, ",
3051						1,
3052						" to ",
3053						500,
3054						". Distance is measured from the centre of each ZIP code to the centre of the nearest store ZIP code."
3055					]
3056				}),
3057				/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
3058					sx: generateRowSxProps,
3059					children: [
3060						/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Button, {
3061							variant: "contained",
3062							disabled: !canGenerate,
3063							onClick: onGenerate,
3064							"data-testid": "testId-zip-radius-dma-generate",
3065							"data-tracking-id": "tracking-id-zip-radius-dma-generate",
3066							children: "Generate"
3067						}),
3068						!geoData && !loadError && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
3069							sx: inlineStatusSxProps,
3070							children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(TruffleCircularProgress, { size: 16 }
3070), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
3071								variant: "body2",
3072								color: "text.secondary",
3073								children: "Loading ZIP code data..."
3074							})]
3075						}),
3076						isComputing && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
3077							sx: inlineStatusSxProps,
3078							children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(TruffleCircularProgress, { size: 16 }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
3079								variant: "body2",
3080								color: "text.secondary",
3081								children: "Computing..."
3082							})]
3083						})
3084					]
3085				}),
3086				loadError && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_dist$2.AttentionBox, {
3087					color: "error",
3088					"data-testid": "testId-zip-radius-dma-load-error",
3089					children: [
3090						/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.AttentionBoxTitle, { children: "ZIP code data unavailable" }),
3091						/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.AttentionBoxContent, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
3092							variant: "body1",
3093							color: "text.secondary",
3094							children: loadError
3095						}) }),
3096						/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.AttentionBoxActions, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Button, {
3097							variant: "outlined",
3098							size: "medium",
3099							startIcon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FontAwesomeIcon, { icon: import_faArrowRotateRight.faArrowRotateRight }),
3100							onClick: loadGeoData,
3101							"data-testid": "testId-zip-radius-dma-retry-load",
3102							"data-tracking-id": "tracking-id-zip-radius-dma-retry-load-btn",
3103							children: "Try again"
3104						}) })
3105					]
3106				}),
3107				error && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.AttentionBox, {
3108					color: "error",
3109					"data-testid": "testId-zip-radius-dma-error",
3110					children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.AttentionBoxContent, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
3111						variant: "body1",
3112						color: "text.secondary",
3113						children: error
3114					}) })
3115				})
3116			]
3117		}), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Paper, {
3118			variant: "outlined",
3119			sx: resultsPanelSxProps,
3120			"data-testid": "testId-zip-radius-dma-results-panel",
3121			children: [
3122				/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
3123					sx: resultsHeaderSxProps,
3124					children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
3125						variant: "h4",
3126						children: "Results"
3127					}), hasResults && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Button, {
3128						variant: "contained",
3129						size: "medium",
3130						startIcon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FontAwesomeIcon, { icon: import_faDownload.faDownload }),
3131						onClick: () => downloads.forEach((file) => onDownload(file)),
3132						"data-testid": "testId-zip-radius-dma-download-all",
3133						"data-tracking-id": "tracking-id-zip-radius-dma-download-all",
3134						children: "Download all"
3135					})]
3136				}),
3137				!result && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
3138					variant: "body1",
3139					color: "text.secondary",
3140					"data-testid": "testId-zip-radius-dma-results-empty",
3141					children: "Choose a store file, set a radius, and click Generate. The whitelists and the ZIP-to-nearest-store table will appear here."
3142				}),
3143				result && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
3144					sx: resultsSxProps,
3145					"data-testid": "testId-zip-radius-dma-results",
3146					children: [
3147						/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
3148							sx: statsStripSxProps,
3149							"data-testid": "testId-zip-radius-dma-summary",
3150							children: [
3151								/* @__PURE__ */ (0, import_jsx_runtime.jsx)(ResultTile, {
3152									icon: import_faStore.faStore,
3153									label: "Stores resolved",
3154									value: (result.storesInFile - result.unresolvedStores.length).toLocaleString(),
3155									caption: `of ${result.storesInFile.toLocaleString()} in the file`
3156								}),
3157								/* @__PURE__ */ (0, import_jsx_runtime.jsx)(ResultTile, {
3158									icon: import_faLocationDot.faLocationDot,
3159									label: "ZIP codes",
3160									value: result.matchedZips.length.toLocaleString(),
3161									caption: `within ${result.radiusMiles} mi of a store`
3162								}),
3163								includeDmas && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ResultTile, {
3164									icon: import_faTowerBroadcast.faTowerBroadcast,
3165									label: "DMAs",
3166									value: result.matchedDmas.length.toLocaleString(),
3167									caption: "TV markets they fall in"
3168								})
3169							]
3170						}),
3171						result.unresolvedStores.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_dist$2.AttentionBox, {
3172							color: "warning",
3173							"data-testid": "testId-zip-radius-dma-unresolved-warning",
3174							children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.AttentionBoxTitle, { children: "Some stores could not be located" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.AttentionBoxContent, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Typography, {
3175								variant: "body1",
3176								color: "text.secondary",
3177								children: [
3178									result.unresolvedStores.length.toLocaleString(),
3179									" of ",
3180									result.storesInFile.toLocaleString(),
3181									" stores could not be located and ",
3182									result.unresolvedStores.length === 1 ? "was" : "were",
3183									" skipped:",
3184									" ",
3185									listItems(result.unresolvedStores.map((store) => `${store.storeNumber || "no store number"} (${store.zipCode || "no ZIP"})`))
3186								]
3187							}) })]
3188						}),
3189						!hasResults && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.AttentionBox, {
3190							color: "warning",
3191							"data-testid": "testId-zip-radius-dma-no-results",
3192							children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.AttentionBoxContent, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Typography, {
3193								variant: "body1",
3194								color: "text.secondary",
3195								children: [
3196									"No ZIP codes fell within ",
3197									result.radiusMiles,
3198									" miles of any store, so there is nothing to download."
3199								]
3200							}) })
3201						}),
3202						result.zipsWithoutDma.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.AttentionBox, {
3203							color: "warning",
3204							"data-testid": "testId-zip-radius-dma-no-dma-warning",
3205							children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.AttentionBoxContent, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Typography, {
3206								variant: "body1",
3207								color: "text.secondary",
3208								children: [result.zipsWithoutDma.length === 1 ? "1 matched ZIP code has no DMA and is missing from the DMA whitelist: " : `${result.zipsWithoutDma.length.toLocaleString()} matched ZIP codes have no DMA and are missing from the DMA whitelist: `, listItems(result.zipsWithoutDma)]
3209							}) })
3210						}),
3211						result.matchedZips.length > 5e3 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.AttentionBox, {
3212							color: "warning",
3213							"data-testid": "testId-zip-radius-dma-large-whitelist-warning",
3214							children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.AttentionBoxContent, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Typography, {
3215								variant: "body1",
3216								color: "text.secondary",
3217								children: [
3218									"This ZIP whitelist has ",
3219									result.matchedZips.length.toLocaleString(),
3220									" rows. The ad server scans the whole list on every bid request, so consider a smaller radius or the DMA whitelist."
3221								]
3222							}) })
3223						}),
3224						hasResults && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableContainer, {
3225							component: import_material.Paper,
3226							variant: "outlined",
3227							children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Table, {
3228								size: "small",
3229								children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableHead, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.TableRow, { children: [
3230									/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, {
3231										sx: headCellSx,
3232										children: "File"
3233									}),
3234									/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, {
3235										sx: headCellSx,
3236										children: "What it contains"
3237									}),
3238									/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, { sx: (0, import_dist$2.combineSxProps)(headCellSx, downloadCellSxProps) })
3239								] }) }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableBody, { children: downloads.map((file) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.TableRow, {
3240									sx: rowHoverSx,
3241									children: [
3242										/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, {
3243											sx: (0, import_dist$2.combineSxProps)(bodyCellSx, fileNameCellSxProps),
3244											children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
3245												variant: "subtitle2",
3246												children: file.label
3247											})
3248										}),
3249										/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, {
3250											sx: bodyCellSx,
3251											children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
3252												variant: "body1",
3253												color: "text.secondary",
3254												children: file.description
3255											})
3256										}),
3257										/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, {
3258											sx: (0, import_dist$2.combineSxProps)(bodyCellSx, downloadCellSxProps),
3259											children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.TruffleIconButton, {
3260												size: "small",
3261												"aria-label": `Download ${file.label}`,
3262												onClick: () => onDownload(file),
3263												"data-testid": `testId-zip-radius-dma-download-${file.id}`,
3264												"data-tracking-id": `tracking-id-zip-radius-dma-download-${file.id}`,
3265												children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FontAwesomeIcon, { icon: import_faDownload.faDownload })
3266											})
3267										})
3268									]
3269								}, file.id)) })]
3270							})
3271						}),
3272						hasResults && includeDmas && result.matchedDmas.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.AttentionBox, {
3273							color: "info",
3274							"data-testid": "testId-zip-radius-dma-expansion-warning",
3275							children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.AttentionBoxContent, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Typography, {
3276								variant: "body1",
3277								color: "text.secondary",
3278								children: [
3279									"A DMA whitelist covers entire television markets, so it is far broader than the ZIP list:",
3280									" ",
3281									result.matchedZips.length.toLocaleString(),
3282									" ZIP codes versus ",
3283									result.matchedDmas.length.toLocaleString(),
3284									" whole DMAs. Attach one file or the other, never both."
3285								]
3286							}) })
3287						})
3288					]
3289				})
3290			]
3291		})]
3292	});
3293};
3294/** Same tile treatment as the campaigns summary strip, so the space reads as one product. */
3295var ResultTile = ({ icon, label, value, caption }) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Paper, {
3296	variant: "outlined",
3297	sx: (0, import_dist$2.combineSxProps)(adsSurfaceSx, tileSxProps),
3298	children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Box, {
3299		sx: adsIconTileSx(32),
3300		children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FontAwesomeIcon, { icon })
3301	}
3301), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
3302		sx: tileBodySxProps,
3303		children: [
3304			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
3305				variant: "caption",
3306				color: "text.secondary",
3307				noWrap: true,
3308				children: label
3309			}),
3310			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
3311				variant: "h2",
3312				noWrap: true,
3313				children: value
3314			}),
3315			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
3316				variant: "caption",
3317				color: "text.secondary",
3318				noWrap: true,
3319				children: caption
3320			})
3321		]
3322	})]
3323});
3324var footerSxProps = {
3325	display: "flex",
3326	alignItems: "center",
3327	gap: "4px"
3328};
3329var helpButtonSxProps = {
3330	color: (theme) => theme.palette.text.secondary,
3331	flexShrink: 0
3332};
3333var rootSxProps = {
3334	display: "flex",
3335	flexDirection: "row",
3336	gap: "24px",
3337	alignItems: "stretch",
3338	flexGrow: 1,
3339	minHeight: 0
3340};
3341var inputPanelSxProps = {
3342	display: "flex",
3343	flexDirection: "column",
3344	gap: "16px",
3345	padding: "24px",
3346	width: "380px",
3347	flexShrink: 0,
3348	overflow: "auto"
3349};
3350var sectionHeadingSxProps = { paddingTop: "8px" };
3351var dropzoneSxProps = {
3352	width: "100%",
3353	height: "180px",
3354	flexShrink: 0,
3355	borderRadius: "4px",
3356	backgroundColor: "other.editorBackground"
3357};
3358var uploadFileSxProps = {
3359	width: "100%",
3360	height: "100%",
3361	backgroundColor: "transparent",
3362	padding: "16px"
3363};
3364var optionsRowSxProps = {
3365	display: "flex",
3366	flexDirection: "row",
3367	alignItems: "flex-end",
3368	justifyContent: "space-between",
3369	gap: "16px"
3370};
3371var includeDmasSxProps = { marginRight: 0 };
3372var radiusFieldSxProps = { width: "160px" };
3373var generateRowSxProps = {
3374	display: "flex",
3375	flexDirection: "row",
3376	alignItems: "center",
3377	gap: "16px"
3378};
3379var inlineStatusSxProps = {
3380	display: "flex",
3381	flexDirection: "row",
3382	alignItems: "center",
3383	gap: "8px"
3384};
3385var resultsPanelSxProps = {
3386	display: "flex",
3387	flexDirection: "column",
3388	gap: "16px",
3389	padding: "24px",
3390	flexGrow: 1,
3391	minWidth: 0,
3392	overflow: "auto"
3393};
3394var resultsHeaderSxProps = {
3395	display: "flex",
3396	alignItems: "center",
3397	justifyContent: "space-between",
3398	gap: "16px"
3399};
3400var resultsSxProps = {
3401	display: "flex",
3402	flexDirection: "column",
3403	alignItems: "stretch",
3404	gap: "16px"
3405};
3406var statsStripSxProps = {
3407	display: "flex",
3408	gap: "16px",
3409	flexWrap: "wrap"
3410};
3411var tileSxProps = {
3412	flex: "1 1 180px",
3413	minWidth: 0
3414};
3415var tileBodySxProps = {
3416	minWidth: 0,
3417	display: "flex",
3418	flexDirection: "column",
3419	gap: "2px"
3420};
3421var fileNameCellSxProps = {
3422	whiteSpace: "nowrap",
3423	width: "180px"
3424};
3425var downloadCellSxProps = {
3426	width: "52px",
3427	textAlign: "center"
3428};
3429//#endregion
3430//#region client/components/newNav/components/ads/AdsUtilityPage.tsx
3431var AdsUtilityPage = () => {
3432	const accountId = useActiveAccountId();
3433	const { userRoles } = useUserData();
3434	const { utilityId } = useParams();
3435	const utility = findAdsUtility(utilityId);
3436	if (!utility) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Redirect, { to: (0, import_dist$1.newNavAdsUtilityPath)({
3437		accountId,
3438		utilityId: "zip-radius"
3439	}) });
3440	return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
3441		sx: adsPageSx,
3442		children: [
3443			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsPageHeader, {
3444				area: "Utilities",
3445				title: utility.name,
3446				subtitle: utility.description
3447			}),
3448			utility.notice && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_dist$2.AttentionBox, {
3449				color: "info",
3450				children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.AttentionBoxTitle, { children: "Not available yet" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.AttentionBoxContent, { children: utility.notice })]
3451			}),
3452			utility.id === "zip-radius" && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(ZipRadiusDmaTool, {}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ZipRadiusDmaFooter, {})] }),
3453			utility.id === "pixel-tracking" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(PixelRulesEditor, { readOnly: !isPermitted(userRoles, permissions.adsEditCampaignSettings) })
3454		]
3455	});
3456};
3457//#endregion
3458//#region client/components/newNav/components/ads/dashboards/AdsAccountDashboardPage.tsx
3459var TREND_HEIGHT = 220;
3460/**
3461* Portfolio triage, and where the space lands. The first question of a session is "is anything wrong in this
3462* account", which nothing else in the space answers.
3463*/
3464var AdsAccountDashboardPage = () => {
3465	const theme = (0, import_material.useTheme)();
3466	const accountId = useActiveAccountId();
3467	const { delivery, accountName, loading, isRefreshing, error, refresh } = useAdsSpaceAccountDelivery();
3468	const trendOption = (0, import_react.useMemo)(() => delivery ? trendChartOption(delivery.trend, delivery.trendSpend, theme) : null, [delivery, theme]);
3469	if (loading) return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
3470		sx: adsPageSx,
3471		children: [
3472			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsPageHeader, {
3473				area: "Dashboard",
3474				title: "Account dashboard"
3475			}),
3476			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(DeliverySkeleton, { height: 120 }),
3477			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(DeliverySkeleton, { height: 280 })
3478		]
3479	});
3480	if (error || !delivery) return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
3481		sx: adsPageSx,
3482		children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsPageHeader, {
3483			area: "Dashboard",
3484			title: "Account dashboard"
3485		}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(DeliveryEmptyState, {
3486			title: "No delivery to show",
3487			message: "This account's delivery could not be read."
3488		})]
3489	});
3490	const { platformStatus } = delivery;
3491	const shortfall = delivery.expectedByNow - delivery.delivered;
3492	const enabledCount = delivery.campaigns.filter((campaign) => campaign.enabled).length;
3493	const inFlightCount = delivery.campaigns.filter((campaign) => campaign.flight).length;
3494	return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
3495		sx: adsPageSx,
3496		children: [
3497			(platformStatus.isPanicMode || platformStatus.trafficReductions.length > 0) && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.AttentionBox, {
3498				color: platformStatus.isPanicMode ? "error" : "warning",
3499				children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.AttentionBoxContent, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
3500					variant: "body1",
3501					color: "text.secondary",
3502					children: platformStatus.isPanicMode ? "Panic mode is active: all bids are suppressed platform-wide." : `Traffic is reduced platform-wide: ${platformStatus.trafficReductions.map((reduction) => `${reduction.scope} at ${reduction.percent}%`).join(", ")}.`
3503				}) })
3504			}),
3505			/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(AdsPageHeader, {
3506				area: "Dashboard",
3507				title: accountName || "Account dashboard",
3508				subtitle: `${delivery.campaigns.length} campaigns, ${enabledCount} enabled, ${inFlightCount} with a flight covering today`,
3509				children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(DeliveryFreshness, {
3510					lastUpdateTime: delivery.freshness.lastUpdateTime,
3511					isKnown: delivery.freshness.isKnown
3512				}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(DeliveryRefreshButton, {
3513					isRefreshing,
3514					onRefresh: refresh
3515				})]
3516			}),
3517			/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
3518				sx: kpiRowSx,
3519				children: [
3520					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsKpiTile, {
3521						dataTestId: "ads-account-kpi-delivered",
3522						label: "Delivered / goal",
3523						value: `${formatCount(delivery.delivered)} / ${formatCompact(delivery.goal)}`,
3524						subLine: `${formatWholePercent(delivery.goal > 0 ? delivery.delivered / delivery.goal : null)} delivered, ${formatWholePercent(delivery.elapsedShare)} of the flights elapsed`,
3525						children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsMeter, {
3526							dataTestId: "ads-account-meter-delivered",
3527							name: "",
3528							fill: delivery.goal > 0 ? delivery.delivered / delivery.goal : 0,
3529							value: "",
3530							tick: delivery.elapsedShare,
3531							color: paceStatusColor(delivery.paceIndex)
3532						})
3533					}),
3534					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsKpiTile, {
3535						dataTestId: "ads-account-kpi-pace",
3536						label: "Pace",
3537						value: formatWholePercent(delivery.paceIndex),
3538						subLine: shortfall > 0 ? `${formatCount(shortfall)} impressions behind the expected line` : `${formatCount(-shortfall)} impressions ahead`,
3539						status: paceSkewChip(delivery.delivered, delivery.expectedByNow, delivery.goal),
3540						infoTooltip: "Delivered across every in-flight campaign, divided by what their plans expected by now."
3541					}),
3542					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsKpiTile, {
3543						dataTestId: "ads-account-kpi-spend",
3544						label: "Spend / revenue",
3545						value: formatMoney(delivery.spend),
3546						subLine: `Revenue ${formatMoney(delivery.revenue)} - margin ${delivery.realisedMarginPercent === null ? "-" : formatRate(delivery.realisedMarginPercent / 100)}`
3547					}),
3548					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsKpiTile, {
3549						dataTestId: "ads-account-kpi-ecpm",
3550						label: "Blended eCPM",
3551						value: formatMoney(delivery.blendedEcpm),
3552						subLine: delivery.targetEcpm === null ? "no weighted target" : `Weighted target ${formatMoney(delivery.targetEcpm)}`,
3553						status: delivery.targetEcpm !== null && delivery.blendedEcpm > delivery.targetEcpm ? {
3554							text: "over target",
3555							color: "warning"
3556						} : void 0
3557					})
3558				]
3559			}),
3560			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(AccountAlertsCard, {
3561				alerts: delivery.alerts,
3562				suppressedAlerts: delivery.suppressedAlerts,
3563				disabledRules: delivery.disabledRules,
3564				accountId
3565			}),
3566			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(AccountCampaignTable, {
3567				campaigns: delivery.campaigns,
3568				accountId
3569			}),
3570			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(AccountDecoupledTable, {
3571				campaigns: delivery.decoupledCampaigns,
3572				accountId
3573			}),
3574			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(AnalyticsChart, {
3575				dataTestId: "ads-account-trend",
3576				title: "Account delivery, last 30 days",
3577				SubTitle: "impressions and spend",
3578				tooltipText: "Impressions as an area on the left axis, spend as a line on the right, across every campaign in this account.",
3579				option: trendOption,
3580				containerStyle: { height: TREND_HEIGHT }
3581			})
3582		]
3583	});
3584};
3585var kpiRowSx = {
3586	display: "grid",
3587	gridTemplateColumns: "repeat(auto-fit, minmax(220px, 1fr))",
3588	gap: 2
3589};
3590//#endregion
3591//#region node_modules/@fortawesome/pro-regular-svg-icons/faBrain.js
3592var require_faBrain = /* @__PURE__ */ __commonJSMin(((exports) => {
3593	Object.defineProperty(exports, "__esModule", { value: true });
3594	var prefix = "far";
3595	var iconName = "brain";
3596	var width = 512;
3597	var height = 512;
3598	var aliases = [129504];
3599	var unicode = "f5dc";
3600	var svgPathData = "M200 0c-36.6 0-67.5 24.6-77 58.1-47.4 9.7-83 51.6-83 101.9 0 11.3 1.8 22.2 5.1 32.3-22.7 19.1-37.1 47.7-37.1 79.7 0 23.7 8 45.6 21.3 63.1-3.5 10.4-5.3 21.4-5.3 32.9 0 54 41.2 98.5 93.9 103.5 15.6 24.3 42.9 40.5 74.1 40.5 25.2 0 48-10.6 64-27.6 16 17 38.8 27.6 64 27.6 31.1 0 58.4-16.2 74.1-40.5 52.7-5.1 93.9-49.5 93.9-103.5 0-11.5-1.9-22.5-5.3-32.9 13.4-17.5 21.3-39.4 21.3-63.1 0-32-14.5-60.6-37.1-79.7 3.3-10.2 5.1-21.1 5.1-32.3 0-50.3-35.6-92.2-83-101.9-9.5-33.5-40.4-58.1-77-58.1-21.8 0-41.6 8.7-56 22.9-14.4-14.1-34.2-22.9-56-22.9zm32 80l0 347.1c-.1 .6-.2 1.2-.3 1.9-2.5 19.7-19.3 35-39.7 35-16.8 0-31.2-10.3-37.1-25.1-3.8-9.6-13.3-15.6-23.6-15-1.1 .1-2.2 .1-3.2 .1-30.9 0-56-25.1-56-56 0-9.5 2.4-18.5 6.5-26.3 5.1-9.6 3.1-21.4-4.8-28.8-11-10.2-17.8-24.8-17.8-40.9 0-21.6 12.2-40.4 30.3-49.8 5.9-3 10.2-8.4 12-14.7s.9-13.2-2.5-18.9c-5-8.4-7.9-18.1-7.9-28.6 0-30.9 25.1-56 56-56 13.3 0 24-10.7 24-24 0-17.7 14.3-32 32-32s32 14.3 32 32zm48 347.1L280 80c0-17.7 14.3-32 32-32s32 14.3 32 32c0 13.3 10.7 24 24 24 30.9 0 56 25.1 56 56 0 10.5-2.9 20.3-7.9 28.6-3.4 5.7-4.3 12.5-2.5 18.9s6.2 11.7 12 14.7c18 9.3 30.3 28.1 30.3 49.8 0 16.1-6.8 30.7-17.8 40.9-7.9 7.4-9.9 19.2-4.8 28.8 4.2 7.8 6.5 16.7 6.5 26.3 0 30.9-25.1 56-56 56-1.1 0-2.2 0-3.2-.1-10.3-.6-19.8 5.5-23.6 15-5.9 14.7-20.4 25.1-37.1 25.1-20.4 0-37.2-15.3-39.7-35-.1-.6-.2-1.3-.3-1.9z";
3601	exports.definition = {
3602		prefix,
3603		iconName,
3604		icon: [
3605			width,
3606			height,
3607			aliases,
3608			unicode,
3609			svgPathData
3610		]
3611	};
3612	exports.faBrain = exports.definition;
3613	exports.prefix = prefix;
3614	exports.iconName = iconName;
3615	exports.width = width;
3616	exports.height = height;
3617	exports.ligatures = aliases;
3618	exports.unicode = unicode;
3619	exports.svgPathData = svgPathData;
3620	exports.aliases = aliases;
3621}));
3622//#endregion
3623//#region node_modules/@fortawesome/pro-regular-svg-icons/faBullseye.js
3624var require_faBullseye = /* @__PURE__ */ __commonJSMin(((exports) => {
3625	Object.defineProperty(exports, "__esModule", { value: true });
3626	var prefix = "far";
3627	var iconName = "bullseye";
3628	var width = 512;
3629	var height = 512;
3630	var aliases = [];
3631	var unicode = "f140";
3632	var svgPathData = "M464 256a208 208 0 1 0 -416 0 208 208 0 1 0 416 0zM0 256a256 256 0 1 1 512 0 256 256 0 1 1 -512 0zm256 96a96 96 0 1 0 0-192 96 96 0 1 0 0 192zm0-240a144 144 0 1 1 0 288 144 144 0 1 1 0-288zM224 256a32 32 0 1 1 64 0 32 32 0 1 1 -64 0z";
3633	exports.definition = {
3634		prefix,
3635		iconName,
3636		icon: [
3637			width,
3638			height,
3639			aliases,
3640			unicode,
3641			svgPathData
3642		]
3643	};
3644	exports.faBullseye = exports.definition;
3645	exports.prefix = prefix;
3646	exports.iconName = iconName;
3647	exports.width = width;
3648	exports.height = height;
3649	exports.ligatures = aliases;
3650	exports.unicode = unicode;
3651	exports.svgPathData = svgPathData;
3652	exports.aliases = aliases;
3653}));
3654//#endregion
3655//#region node_modules/@fortawesome/pro-regular-svg-icons/faSliders.js
3656var require_faSliders = /* @__PURE__ */ __commonJSMin(((exports) => {
3657	Object.defineProperty(exports, "__esModule", { value: true });
3658	var prefix = "far";
3659	var iconName = "sliders";
3660	var width = 512;
3661	var height = 512;
3662	var aliases = ["sliders-h"];
3663	var unicode = "f1de";
3664	var svgPathData = "M24 72C10.7 72 0 82.7 0 96s10.7 24 24 24l91.7 0c10.2 32.5 40.5 56 76.3 56s66.1-23.5 76.3-56L488 120c13.3 0 24-10.7 24-24s-10.7-24-24-24L268.3 72C258.1 39.5 227.8 16 192 16s-66.1 23.5-76.3 56L24 72zm0 160c-13.3 0-24 10.7-24 24s10.7 24 24 24l251.7 0c10.2 32.5 40.5 56 76.3 56s66.1-23.5 76.3-56l59.7 0c13.3 0 24-10.7 24-24s-10.7-24-24-24l-59.7 0c-10.2-32.5-40.5-56-76.3-56s-66.1 23.5-76.3 56L24 232zm0 160c-13.3 0-24 10.7-24 24s10.7 24 24 24l59.7 0c10.2 32.5 40.5 56 76.3 56s66.1-23.5 76.3-56L488 440c13.3 0 24-10.7 24-24s-10.7-24-24-24l-251.7 0c-10.2-32.5-40.5-56-76.3-56s-66.1 23.5-76.3 56L24 392zm136 56a32 32 0 1 1 0-64 32 32 0 1 1 0 64zM352 288a32 32 0 1 1 0-64 32 32 0 1 1 0 64zM160 96a32 32 0 1 1 64 0 32 32 0 1 1 -64 0z";
3665	exports.definition = {
3666		prefix,
3667		iconName,
3668		icon: [
3669			width,
3670			height,
3671			aliases,
3672			unicode,
3673			svgPathData
3674		]
3675	};
3676	exports.faSliders = exports.definition;
3677	exports.prefix = prefix;
3678	exports.iconName = iconName;
3679	exports.width = width;
3680	exports.height = height;
3681	exports.ligatures = aliases;
3682	exports.unicode = unicode;
3683	exports.svgPathData = svgPathData;
3684	exports.aliases = aliases;
3685}));
3686//#endregion
3687//#region node_modules/@fortawesome/pro-regular-svg-icons/faTag.js
3688var require_faTag = /* @__PURE__ */ __commonJSMin(((exports) => {
3689	Object.defineProperty(exports, "__esModule", { value: true });
3690	var prefix = "far";
3691	var iconName = "tag";
3692	var width = 576;
3693	var height = 512;
3694	var aliases = [127991];
3695	var unicode = "f02b";
3696	var svgPathData = "M96.1 88c0-13.3 10.7-24 24-24l156.1 0c6.4 0 12.5 2.5 17 7l208 208c9.4 9.4 9.4 24.6 0 33.9L345.1 469.1c-9.4 9.4-24.6 9.4-33.9 0l-208-208c-4.5-4.5-7-10.6-7-17L96.1 88zm24-72c-39.8 0-72 32.2-72 72l0 156.1c0 19.1 7.6 37.4 21.1 50.9l208 208c28.1 28.1 73.7 28.1 101.8 0L535.1 346.9c28.1-28.1 28.1-73.7 0-101.8l-208-208C313.6 23.6 295.3 16 276.2 16L120.1 16zm56 160a32 32 0 1 0 0-64 32 32 0 1 0 0 64z";
3697	exports.definition = {
3698		prefix,
3699		iconName,
3700		icon: [
3701			width,
3702			height,
3703			aliases,
3704			unicode,
3705			svgPathData
3706		]
3707	};
3708	exports.faTag = exports.definition;
3709	exports.prefix = prefix;
3710	exports.iconName = iconName;
3711	exports.width = width;
3712	exports.height = height;
3713	exports.ligatures = aliases;
3714	exports.unicode = unicode;
3715	exports.svgPathData = svgPathData;
3716	exports.aliases = aliases;
3717}));
3718//#endregion
3719//#region client/components/newNav/components/ads/targeting/geoDefinition.ts
3720var import_faCircleInfo = require_faCircleInfo();
3721var import_faPen = require_faPen();
3722var import_faTrashCan = require_faTrashCan();
3723var import_faChartMixed = require_faChartMixed();
3724var import_faDesktop = require_faDesktop();
3725var import_faEllipsisV = require_faEllipsisV();
3726var import_faUserGroup = require_faUserGroup();
3727var import_faPlug = require_faPlug();
3728var import_faFilm = require_faFilm();
3729var import_faFilter = require_faFilter();
3730var import_faBrain = require_faBrain();
3731var import_faBullseye = require_faBullseye();
3732var import_faCalendarDays = require_faCalendarDays();
3733var import_faSliders = require_faSliders();
3734var import_faTag = require_faTag();
3735/**
3736* The two forms a geography definition can take, and the translation between the second one and something a
3737* person can edit.
3738*
3739* The ad server reads this string one of two ways (`ExclusionLogicFactory.getLocationFilters`): a link to a
3740* `mode,field,value` CSV, or an inline expression like `Country=United States;State!=CA,DC`. Both collapse to the
3741* same thing - one include-or-exclude list per location type, with the types ANDed - so that is what is edited
3742* here, and the string is only how it gets written down.
3743*/
3744/** `ExclusionLogic.LocationField`, spelled the way the parser's `valueOf(toUpperCase())` accepts. */
3745var geoLocationFieldOptions = [
3746	{
3747		value: "Country",
3748		label: "Country"
3749	},
3750	{
3751		value: "State",
3752		label: "State"
3753	},
3754	{
3755		value: "City",
3756		label: "City"
3757	},
3758	{
3759		value: "Zip_Code",
3760		label: "Zip code"
3761	},
3762	{
3763		value: "DMA",
3764		label: "DMA"
3765	},
3766	{
3767		value: "IP",
3768		label: "IP address"
3769	},
3770	{
3771		value: "Latitude",
3772		label: "Latitude"
3773	},
3774	{
3775		value: "Longitude",
3776		label: "Longitude"
3777	}
3778];
3779/** Zip codes, IPs and coordinates match by prefix, so a partial value covers everything under it. */
3780var GEO_PREFIX_MATCHED_FIELDS = [
3781	"Zip_Code",
3782	"IP",
3783	"Latitude",
3784	"Longitude"
3785];
3786/** The column this is stored in. An inline expression longer than this is silently truncated by MySQL. */
3787var GEO_DEFINITION_MAX_LENGTH = 1024;
3788var FILE_PREFIXES = /^(s3|file|https?):/i;
3789var isGeoFileLocation = (text) => FILE_PREFIXES.test(text.trim());
3790var parseRule = (part) => {
3791	const equals = part.indexOf("=");
3792	if (equals < 1 || equals >= part.length - 1) return null;
3793	const include = part.charAt(equals - 1) !== "!";
3794	const field = part.slice(0, include ? equals : equals - 1).trim();
3795	const values = part.slice(equals + 1).split(",").map((value) =>
3795 value.trim()).filter(Boolean);
3796	const known = geoLocationFieldOptions.find((option) => option.value.toUpperCase() === field.toUpperCase());
3797	return known && values.length > 0 ? {
3798		field: known.value,
3799		include,
3800		values
3801	} : null;
3802};
3803var parseGeoDefinition = (definition) => {
3804	const text = (definition ?? "").trim();
3805	if (!text) return {
3806		isFile: true,
3807		fileUrl: "",
3808		rules: [],
3809		unparsed: null
3810	};
3811	if (isGeoFileLocation(text)) return {
3812		isFile: true,
3813		fileUrl: text,
3814		rules: [],
3815		unparsed: null
3816	};
3817	const parts = text.split(";").map((part) => part.trim()).filter(Boolean);
3818	const rules = [];
3819	for (const part of parts) {
3820		const rule = parseRule(part);
3821		if (!rule || rules.some((existing) => existing.field === rule.field)) return {
3822			isFile: false,
3823			fileUrl: "",
3824			rules: [],
3825			unparsed: text
3826		};
3827		rules.push(rule);
3828	}
3829	return {
3830		isFile: false,
3831		fileUrl: "",
3832		rules,
3833		unparsed: null
3834	};
3835};
3836var compileGeoDefinition = (definition) => {
3837	if (definition.unparsed !== null) return definition.unparsed;
3838	if (definition.isFile) return definition.fileUrl.trim();
3839	return definition.rules.filter((rule) => rule.values.length > 0).map((rule) => `${rule.field}${rule.include ? "=" : "!="}${rule.values.join(",")}`).join(";");
3840};
3841var fieldLabel = (field) => geoLocationFieldOptions.find((option) => option.value === field)?.label ?? field;
3842/** A zip list is unreadable at list width and a URL's interesting half is its tail, so neither is shown raw. */
3843var describeGeoDefinition = (definition) => {
3844	const parsed = parseGeoDefinition(definition);
3845	if (parsed.unparsed !== null) return "Custom expression";
3846	if (parsed.isFile) return parsed.fileUrl.split("/").pop() || parsed.fileUrl;
3847	if (parsed.rules.length === 0) return "Not set";
3848	return parsed.rules.map((rule) => `${rule.include ? "" : "not "}${fieldLabel(rule.field).toLowerCase()} (${rule.values.length})`).join(", ");
3849};
3850//#endregion
3851//#region client/components/newNav/components/ads/common/AdsStringListInput.tsx
3852/** Add/remove list, used where the old app had nine numbered inputs side by side. */
3853var AdsStringListInput = ({ label, values, disabled, error, placeholder, entryProblem, onChange, trackingId }) => {
3854	const [draft, setDraft] = (0, import_react.useState)("");
3855	const add = () => {
3856		const added = draft.split(STRING_LIST_SEPARATORS).map((entry) => entry.trim()).filter((entry) => entry && !values.includes(entry));
3857		if (added.length > 0) onChange([...values, ...new Set(added)]);
3858		setDraft("");
3859	};
3860	return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
3861		sx: stringListSx,
3862		children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TextField, {
3863			label,
3864			value: draft,
3865			size: "small",
3866			fullWidth: true,
3867			error,
3868			disabled,
3869			placeholder,
3870			onChange: (event) => setDraft(event.target.value),
3871			onKeyDown: (event) => {
3872				if (event.key === "Enter") {
3873					event.preventDefault();
3874					add();
3875				}
3876			},
3877			InputProps: { endAdornment: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Box, {
3878				component: "button",
3879				type: "button",
3880				sx: addButtonSx$3,
3881				disabled: disabled || !draft.trim(),
3882				onClick: add,
3883				"data-tracking-id": `${trackingId}-add-btn`,
3884				children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FontAwesomeIcon, { icon: import_faPlus.faPlus })
3885			}) },
3886			inputProps: { "data-tracking-id": trackingId }
3887		}), values.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Box, {
3888			sx: chipsSx$2,
3889			children: values.map((entry) => {
3890				const problem = entryProblem?.(entry) ?? null;
3891				return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Tooltip, {
3892					title: problem ?? "",
3893					children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Chip, {
3894						size: "small",
3895						label: entry,
3896						color: problem ? "error" : void 0,
3897						disabled,
3898						onDelete: disabled ? void 0 : () => onChange(values.filter((item) => item !== entry)),
3899						"data-tracking-id": `${trackingId}-remove-btn`
3900					})
3901				}, entry);
3902			})
3903		})]
3904	});
3905};
3906var stringListSx = {
3907	display: "flex",
3908	flexDirection: "column",
3909	gap: 1
3910};
3911var chipsSx$2 = {
3912	display: "flex",
3913	flexWrap: "wrap",
3914	gap: .5
3915};
3916var addButtonSx$3 = {
3917	border: 0,
3918	background: "none",
3919	cursor: "pointer",
3920	color: "primary.main",
3921	padding: 0,
3922	fontSize: "14px",
3923	"&:disabled": {
3924		color: "text.disabled",
3925		cursor: "default"
3926	}
3927};
3928//#endregion
3929//#region client/components/newNav/components/ads/common/AdsGeoDefinitionField.tsx
3930var FILE_HELP = "A CSV with mode, field and value columns - mode is whitelist or blacklist, field is one of the location types, one row per value. s3://, https:// and file: all work.";
3931/**
3932* Where a campaign may serve, as the ad server actually evaluates it: one include-or-exclude list per location
3933* type, all of them ANDed. Typing the expression by hand was the old way and it fails at campaign load rather
3934* than here, so the expression is compiled from these rows instead of being edited directly.
3935*/
3936var AdsGeoDefinitionField = (props) => {
3937	const [definition, setDefinition] = (0, import_react.useState)(() => parseGeoDefinition(props.value));
3938	(0, import_react.useEffect)(() => {
3939		if (compileGeoDefinition(definition) !== props.value) setDefinition(parseGeoDefinition(props.value));
3940	}, [props.value]);
3941	const compiled = compileGeoDefinition(definition);
3942	const update = (next) => {
3943		setDefinition(next);
3944		props.onChange(compileGeoDefinition(next));
3945	};
3946	const updateRule = (index, changes) => update({
3947		...definition,
3948		rules: definition.rules.map((rule, at) => at === index ? {
3949			...rule,
3950			...changes
3951		} : rule)
3952	});
3953	const unusedFields = geoLocationFieldOptions.filter((option) => !definition.rules.some((rule) => rule.field === option.value));
3954	if (definition.unparsed !== null) return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
3955		sx: stackSx$6,
3956		children: [
3957			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
3958				variant: "h6",
3959				children: props.label
3960			}),
3961			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.AttentionBox, {
3962				color: "warning",
3963				children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.AttentionBoxContent, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
3964					variant: "body1",
3965					color: "text.secondary",
3966					children: "This was written by hand and does not fit the editor - it is shown as text so nothing is lost. Clearing it starts over with the editor."
3967				}) })
3968			}),
3969			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TextField, {
3970				value: definition.unparsed,
3971				size: "small",
3972				fullWidth: true,
3973				multiline: true,
3974				minRows: 2,
3975				disabled: props.disabled,
3976				onChange: (event) => props.onChange(event.target.value),
3977				inputProps: { "data-tracking-id": `${props.trackingId}-raw` }
3978			})
3979		]
3980	});
3981	return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
3982		sx: stackSx$6,
3983		children: [
3984			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
3985				variant: "h6",
3986				children: props.label
3987			}),
3988			/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.ToggleButtonGroup, {
3989				exclusive: true,
3990				size: "small",
3991				value: definition.isFile ? "file" : "list",
3992				disabled: props.disabled,
3993				onChange: (_event, next) => next && update({
3994					...definition,
3995					isFile: next === "file"
3996				}),
3997				children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.ToggleButton, {
3998					value: "list",
3999					"data-tracking-id": `${props.trackingId}-list-mode-btn`,
4000					children: "Specify locations"
4001				}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.ToggleButton, {
4002					value: "file",
4003					"data-tracking-id": `${props.trackingId}-file-mode-btn`,
4004					children: "A CSV file"
4005				})]
4006			}),
4007			definition.isFile ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
4008				sx: stackSx$6,
4009				children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TextField, {
4010					label: "CSV location",
4011					value: definition.fileUrl,
4012					size: "small",
4013					fullWidth: true,
4014					disabled: props.disabled,
4015					placeholder: "s3://bucket-name/path/locations.csv",
4016					onChange: (event) => update({
4017						...definition,
4018						fileUrl: event.target.value
4019					}),
4020					inputProps: { "data-tracking-id": `${props.trackingId}-file-input` }
4021				}
4021), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
4022					variant: "helper",
4023					color: "text.secondary",
4024					children: FILE_HELP
4025				})]
4026			}) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
4027				sx: stackSx$6,
4028				children: [
4029					definition.rules.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
4030						variant: "body1",
4031						color: "text.secondary",
4032						children: "No locations yet - this geography constrains nothing."
4033					}),
4034					definition.rules.map((rule, index) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Paper, {
4035						variant: "outlined",
4036						sx: ruleSx,
4037						children: [
4038							/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
4039								sx: ruleHeaderSx,
4040								children: [
4041									/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TextField, {
4042										select: true,
4043										label: "Location type",
4044										size: "small",
4045										value: rule.field,
4046										disabled: props.disabled,
4047										sx: typeSelectSx,
4048										onChange: (event) => updateRule(index, { field: event.target.value }),
4049										inputProps: { "data-tracking-id": `${props.trackingId}-type-input` },
4050										children: [...unusedFields, geoLocationFieldOptions.find((option) => option.value === rule.field)].filter(Boolean).map((option) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.MenuItem, {
4051											value: option.value,
4052											children: option.label
4053										}, option.value))
4054									}),
4055									/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.TextField, {
4056										select: true,
4057										label: "Applies as",
4058										size: "small",
4059										value: rule.include ? "include" : "exclude",
4060										disabled: props.disabled,
4061										sx: modeSelectSx,
4062										onChange: (event) => updateRule(index, { include: event.target.value === "include" }),
4063										inputProps: { "data-tracking-id": `${props.trackingId}-mode-input` },
4064										children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.MenuItem, {
4065											value: "include",
4066											children: "Only these"
4067										}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.MenuItem, {
4068											value: "exclude",
4069											children: "Anywhere but these"
4070										})]
4071									}),
4072									!props.disabled && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.TruffleIconButton, {
4073										size: "small",
4074										onClick: () => update({
4075											...definition,
4076											rules: definition.rules.filter((_rule, at) => at !== index)
4077										}),
4078										"data-tracking-id": `${props.trackingId}-remove-rule-btn`,
4079										children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FontAwesomeIcon, { icon: import_faTrashCan.faTrashCan })
4080									})
4081								]
4082							}),
4083							/* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsStringListInput, {
4084								label: "Values",
4085								values: rule.values,
4086								disabled: props.disabled,
4087								placeholder: rule.field === "Zip_Code" ? "029, 028, 06103" : void 0,
4088								onChange: (values) => updateRule(index, { values }),
4089								trackingId: `${props.trackingId}-values`
4090							}),
4091							/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
4092								variant: "helper",
4093								color: "text.secondary",
4094								children: "Type or paste several at once - commas, spaces and new lines all separate them."
4095							}),
4096							GEO_PREFIX_MATCHED_FIELDS.includes(rule.field) && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
4097								variant: "helper",
4098								color: "text.secondary",
4099								children: "Matched by prefix, so 029 covers every zip starting 029."
4100							})
4101						]
4102					}, rule.field)),
4103					!props.disabled && unusedFields.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Button, {
4104						variant: "text",
4105						size: "small",
4106						sx: addButtonSx$2,
4107						startIcon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FontAwesomeIcon, { icon: import_faPlus.faPlus }),
4108						onClick: () => update({
4109							...definition,
4110							rules: [...definition.rules, {
4111								field: unusedFields[0].value,
4112								include: true,
4113								values: []
4114							}]
4115						}),
4116						"data-tracking-id": `${props.trackingId}-add-rule-btn`,
4117						children: "Add location type"
4118					}),
4119					definition.rules.length > 1 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
4120						variant: "helper",
4121						color: "text.secondary",
4122						children: "A request has to satisfy every location type above, not just one of them."
4123					})
4124				]
4125			}),
4126			compiled.length > 1024 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.AttentionBox, {
4127				color: "error",
4128				children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.AttentionBoxContent, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
4129					variant: "body1",
4130					color: "text.secondary",
4131					children: `This is ${compiled.length} characters and only 1024 are stored. Put a list this long in a CSV file instead.`
4132				}) })
4133			})
4134		]
4135	});
4136};
4137var stackSx$6 = {
4138	display: "flex",
4139	flexDirection: "column",
4140	gap: 1.5
4141};
4142var ruleSx = {
4143	display: "flex",
4144	flexDirection: "column",
4145	gap: 1.5,
4146	padding: 2
4147};
4148var ruleHeaderSx = {
4149	display: "flex",
4150	alignItems: "center",
4151	gap: 1
4152};
4153var typeSelectSx = {
4154	width: 160,
4155	flexShrink: 0
4156};
4157var modeSelectSx = {
4158	flex: 1,
4159	minWidth: 150
4160};
4161var addButtonSx$2 = { alignSelf: "flex-start" };
4162//#endregion
4163//#region client/components/newNav/components/ads/common/AdsFieldControl.tsx
4164/**
4165* One field of an ads configuration form. The help line is rendered as its own Typography rather than through
4166* TextField's helperText, which the theme hides unless the field is focused or in error - and this help is the
4167* only place several of these settings are explained at all.
4168*/
4169var AdsFieldControl = ({ field, value, values, error, readOnly, onChange, trackingIdPrefix }) => {
4170	const trackingId = `${trackingIdPrefix}-${field.key}-input`;
4171	const isInert = Boolean(values && field.disabledWhen?.(values));
4172	const isLocked = readOnly || field.machineWritten || isInert;
4173	const textFieldProps = {
4174		label: field.label,
4175		value: asText(value),
4176		error: Boolean(error),
4177		placeholder: field.placeholder,
4178		disabled: isLocked,
4179		fullWidth: true,
4180		size: "small",
4181		inputProps: { "data-tracking-id": trackingId }
4182	};
4183	const renderControl = () => {
4184		switch (field.type) {
4185			case "bool": return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
4186				variant: "h6",
4187				children: field.label
4188			}
4188), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
4189				sx: switchRowSx,
4190				children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Switch, {
4191					checked: Boolean(value),
4192					disabled: isLocked,
4193					onChange: (event) => onChange(field.key, event.target.checked),
4194					"data-tracking-id": trackingId
4195				}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
4196					variant: "body1",
4197					color: "text.secondary",
4198					children: value ? "On" : "Off"
4199				})]
4200			})] });
4201			case "select": return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TextField, {
4202				...textFieldProps,
4203				select: true,
4204				SelectProps: { displayEmpty: true },
4205				onChange: (event) => onChange(field.key, event.target.value),
4206				children: (field.options ?? []).map((option) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.MenuItem, {
4207					value: option.value,
4208					children: option.label
4209				}, option.value))
4210			});
4211			case "multiSelect": {
4212				const selected = asStringList(value);
4213				return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TextField, {
4214					...textFieldProps,
4215					select: true,
4216					value: selected,
4217					SelectProps: {
4218						multiple: true,
4219						renderValue: (chosen) => chosen.map((item) => field.options?.find((option) => option.value === item)?.label ?? item).join(", ")
4220					},
4221					onChange: (event) => onChange(field.key, event.target.value),
4222					children: (field.options ?? []).map((option) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.MenuItem, {
4223						value: option.value,
4224						children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Checkbox, {
4225							size: "small",
4226							checked: selected.includes(option.value)
4227						}), option.label]
4228					}, option.value))
4229				});
4230			}
4231			case "stringList": return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsStringListInput, {
4232				label: field.label,
4233				values: asStringList(value),
4234				disabled: isLocked,
4235				error: Boolean(error),
4236				placeholder: field.placeholder,
4237				entryProblem: field.entryProblem,
4238				onChange: (next) => onChange(field.key, next),
4239				trackingId
4240			});
4241			case "geoDefinition": return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsGeoDefinitionField, {
4242				label: field.label,
4243				value: asText(value),
4244				disabled: isLocked,
4245				onChange: (next) => onChange(field.key, next),
4246				trackingId
4247			});
4248			case "readOnly": return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
4249				variant: "h6",
4250				children: field.label
4251			}), asText(value) ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ReadOnlyValue, { value: asText(value) }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
4252				variant: "body1",
4253				color: "text.disabled",
4254				children: "Not set"
4255			})] });
4256			case "secret": return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
4257				variant: "h6",
4258				children: field.label
4259			}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
4260				variant: "body1",
4261				color: "text.disabled",
4262				children: "Hidden"
4263			})] });
4264			case "number":
4265			case "decimal": return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TextField, {
4266				...textFieldProps,
4267				type: "number",
4268				inputProps: {
4269					...textFieldProps.inputProps,
4270					min: field.min,
4271					max: field.max,
4272					step: field.type === "decimal" ? "any" : 1
4273				},
4274				onChange: (event) => onChange(field.key, event.target.value)
4275			});
4276			case "date": return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TextField, {
4277				...textFieldProps,
4278				type: "date",
4279				onChange: (event) => onChange(field.key, event.target.value)
4280			});
4281			case "longText": return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TextField, {
4282				...textFieldProps,
4283				multiline: true,
4284				minRows: 2,
4285				onChange: (event) => onChange(field.key, event.target.value)
4286			});
4287			default: return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TextField, {
4288				...textFieldProps,
4289				onChange: (event) =>
4289 onChange(field.key, event.target.value)
4290			});
4291		}
4292	};
4293	const helpLine = error ?? field.help ?? (field.machineWritten ? "Written by the services server - shown here for reference." : void 0);
4294	return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
4295		sx: fieldSx$1(field.wide),
4296		children: [renderControl(), helpLine && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
4297			variant: "helper",
4298			color: error ? "error.main" : "text.secondary",
4299			children: helpLine
4300		})]
4301	});
4302};
4303var ReadOnlyValue = ({ value }) => value.startsWith("http") || value.startsWith("s3://") ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Link, {
4304	href: value,
4305	target: "_blank",
4306	rel: "noopener noreferrer",
4307	variant: "body1",
4308	sx: breakSx,
4309	children: value
4310}) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
4311	variant: "body1",
4312	sx: breakSx,
4313	children: value
4314});
4315var fieldSx$1 = (wide) => ({
4316	display: "flex",
4317	flexDirection: "column",
4318	gap: .5,
4319	gridColumn: wide ? "1 / -1" : "auto"
4320});
4321var switchRowSx = {
4322	display: "flex",
4323	alignItems: "center",
4324	gap: 1,
4325	minHeight: 40
4326};
4327var breakSx = { wordBreak: "break-all" };
4328//#endregion
4329//#region client/components/newNav/components/ads/common/AdsRowDialog.tsx
4330var blankValue = (field) => {
4331	if (field.type === "bool") return false;
4332	return field.type === "multiSelect" || field.type === "stringList" ? [] : "";
4333};
4334var emptyValues = (fields) => Object.fromEntries(fields.map((field) => [field.key, field.defaultValue ?? blankValue(field)]));
4335var AdsRowDialog = (props) => {
4336	const [values, setValues] = (0, import_react.useState)(emptyValues(props.fields));
4337	const [errors, setErrors] = (0, import_react.useState)({});
4338	(0, import_react.useEffect)(() => {
4339		if (props.open) {
4340			setValues(props.row ? { ...props.row } : emptyValues(props.fields));
4341			setErrors({});
4342		}
4343	}, [props.open, props.row]);
4344	const fields = props.fieldsFor?.(values) ?? props.fields;
4345	const validate = (next) => {
4346		const groupErrors = validateFields([{
4347			id: "row",
4348			title: props.title,
4349			fields: props.fieldsFor?.(next) ?? props.fields
4350		}], next);
4351		const rowErrors = props.validateRow?.({
4352			...next,
4353			id: props.row?.id ?? "new"
4354		}, props.otherRows) ?? {};
4355		return {
4356			...groupErrors,
4357			...rowErrors
4358		};
4359	};
4360	const handleChange = (key, value) => {
4361		const next = {
4362			...values,
4363			[key]: value
4364		};
4365		setValues(next);
4366		setErrors(validate(next));
4367	};
4368	const handleSubmit = () => {
4369		const nextErrors = validate(values);
4370		setErrors(nextErrors);
4371		if (!hasErrors(nextErrors)) props.onSubmit({
4372			...values,
4373			id: props.row?.id ?? `row-${Date.now()}`
4374		});
4375	};
4376	const isRoomy = fields.some((field) => field.type === "geoDefinition");
4377	return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Dialog, {
4378		open: props.open,
4379		onClose: props.onClose,
4380		PaperProps: { sx: isRoomy ? roomyPaperSx : paperSx$5 },
4381		children: [
4382			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.TruffleDialogTitle, {
4383				CloseIconButtonProps: {
4384					onClick: props.onClose,
4385					"data-tracking-id": `${props.trackingIdPrefix}-row-dialog-close-btn`
4386				},
4387				children: props.title
4388			}),
4389			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.DialogContent, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Box, {
4390				sx: fieldsSx,
4391				children: fields.map((field) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsFieldControl, {
4392					field,
4393					value: values[field.key] ?? null,
4394					values,
4395					error: errors[field.key],
4396					onChange: handleChange,
4397					trackingIdPrefix: `${props.trackingIdPrefix}-row`
4398				}, field.key))
4399			}) }),
4400			/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_dist$2.TruffleDialogActions, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Button, {
4401				variant: "text",
4402				size: "large",
4403				onClick: props.onClose,
4404				"data-tracking-id": `${props.trackingIdPrefix}-row-dialog-cancel-btn`,
4405				children: "Cancel"
4406			}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Button, {
4407				variant: "contained",
4408				size: "large",
4409				disabled: hasErrors(errors),
4410				onClick: handleSubmit,
4411				"data-tracking-id": `${props.trackingIdPrefix}-row-dialog-submit-btn`,
4412				children: props.row ? "Save" : "Add"
4413			})] })
4414		]
4415	});
4416};
4417var paperSx$5 = {
4418	width: 540,
4419	maxWidth: "calc(100vw - 64px)"
4420};
4421var roomyPaperSx = {
4422	width: 720,
4423	maxWidth: "calc(100vw - 64px)"
4424};
4425var fieldsSx = {
4426	display: "flex",
4427	flexDirection: "column",
4428	gap: 2,
4429	paddingTop: 1
4430};
4431//#endregion
4432//#region client/components/newNav/components/ads/common/AdsChildTable.tsx
4433/**
4434* A child entity of a configuration page - flights, group ranges, tag mappings. The old app nested these as
4435* formsets inside the parent form; a table with an add/edit dialog keeps the parent readable once a campaign
4436* carries a dozen of them.
4437*/
4438var AdsChildTable = (props) => {
4439	const [editing, setEditing] = (0, import_react.useState)(null);
4440	const [isAdding, setIsAdding] = (0, import_react.useState)(false);
4441	const [menuAnchorEl, setMenuAnchorEl] = (0, import_react.useState)(null);
4442	const [menuRow, setMenuRow] = (0, import_react.useState)(null);
4443	const columns = props.columns ?? props.fields.map((field) => ({
4444		key: field.key,
4445		label: field.label
4446	}));
4447	const closeMenu = () => {
4448		setMenuAnchorEl(null);
4449		setMenuRow(null);
4450	};
4451	const defaultCell = (row, key) => {
4452		const field = (props.rowFieldsFor?.(row) ?? props.fields).find((candidate) => candidate.key === key);
4453		const value = row[key];
4454		if (field?.type === "bool") return value ? "Yes" : "No";
4455		if (field?.type === "select") return field.options?.find((option) => option.value === asText(value))?.label ?? asText(value);
4456		if (field?.type === "multiSelect" || field?.type === "stringList") return asStringList(value).join(", ");
4457		return asText(value) || "-";
4458	};
4459	const rowActions = (row) => {
4460		const locked = props.lockedRow?.(row) ?? null;
4461		return [{
4462			label: "Edit",
4463			icon: import_faPen.faPen,
4464			disabled: Boolean(locked?.edit),
4465			disabledReason: locked?.edit,
4466			onClick: () => {
4467				closeMenu();
4468				setEditing(row);
4469			}
4470		}, {
4471			label: "Remove",
4472			icon: import_faTrashCan.faTrashCan,
4473			error: true,
4474			dividerBefore: true,
4475			disabled: Boolean(locked),
4476			disabledReason: locked?.remove,
4477			onClick: () => {
4478				closeMenu();
4479				props.onChange(props.rows.filter((candidate) => candidate.id !== row.id));
4480			}
4481		}];
4482	};
4483	const handleSubmit = (row) => {
4484		props.onChange(props.rows.some((candidate) => candidate.id === row.id) ? props.rows.map((candidate) => candidate.id === row.id ? row : candidate) : [...props.rows, row]);
4485		setEditing(null);
4486		setIsAdding(false);
4487	};
4488	return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
4489		sx: sectionSx$11,
4490		children: [
4491			/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
4492				sx: headerSx$6,
4493				children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
4494					variant: "h5",
4495					children: props.title
4496				}
4496), props.description && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
4497					variant: "body2",
4498					color: "text.secondary",
4499					children: props.description
4500				})] }), !props.readOnly && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
4501					sx: headerActionsSx$3,
4502					children: [props.headerActions, /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Button, {
4503						variant: "outlined",
4504						size: "medium",
4505						sx: addButtonSx$1,
4506						startIcon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FontAwesomeIcon, { icon: import_faPlus.faPlus }),
4507						onClick: () => setIsAdding(true),
4508						"data-tracking-id": `${props.trackingIdPrefix}-add-btn`,
4509						children: props.addLabel
4510					})]
4511				})]
4512			}),
4513			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableContainer, {
4514				component: import_material.Paper,
4515				variant: "outlined",
4516				children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Table, {
4517					size: "small",
4518					children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableHead, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.TableRow, { children: [columns.map((column) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, {
4519						align: column.align ?? "left",
4520						sx: headCellSx,
4521						children: column.label
4522					}, column.key)), !props.readOnly && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, { sx: actionsCellSx })] }) }
4522), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.TableBody, { children: [props.rows.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableRow, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, {
4523						colSpan: columns.length + 1,
4524						sx: emptyCellSx$3,
4525						children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
4526							variant: "body1",
4527							color: "text.secondary",
4528							children: props.emptyText
4529						})
4530					}) }), props.rows.map((row) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.TableRow, {
4531						sx: rowHoverSx,
4532						"data-testid": `ads-child-row-${row.id}`,
4533						children: [columns.map((column) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, {
4534							align: column.align ?? "left",
4535							sx: (0, import_dist$2.combineSxProps)(bodyCellSx, cellSx$1),
4536							children: column.render ? column.render(row) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
4537								variant: "body1",
4538								noWrap: props.fields.find((field) => field.key === column.key)?.type === "date",
4539								children: defaultCell(row, column.key)
4540							})
4541						}, column.key)), !props.readOnly && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, {
4542							sx: actionsCellSx,
4543							children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.TruffleIconButton, {
4544								size: "small",
4545								onClick: (event) => {
4546									setMenuAnchorEl(event.currentTarget);
4547									setMenuRow(row);
4548								},
4549								"data-tracking-id": `${props.trackingIdPrefix}-row-options-btn`,
4550								children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FontAwesomeIcon, { icon: import_faEllipsisV.faEllipsisV })
4551							})
4552						})]
4553					}, row.id))] })]
4554				})
4555			}),
4556			menuRow && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(RowOptionsMenu, {
4557				anchorEl: menuAnchorEl,
4558				open: Boolean(menuAnchorEl),
4559				onClose: closeMenu,
4560				title: props.rowTitle(menuRow),
4561				actions: rowActions(menuRow),
4562				anchorOrigin: {
4563					vertical: "bottom",
4564					horizontal: "right"
4565				},
4566				transformOrigin: {
4567					vertical: "top",
4568					horizontal: "right"
4569				}
4570			}),
4571			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsRowDialog, {
4572				open: props.bulkAdd ? Boolean(editing) : isAdding || Boolean(editing),
4573				title: editing ? `Edit ${props.title.toLowerCase()}` : props.addLabel,
4574				fields: editing && props.rowFields ? props.rowFields(editing) : props.fields,
4575				fieldsFor: props.rowFieldsFor,
4576				row: editing,
4577				otherRows: props.rows.filter((row) => row.id !== editing?.id),
4578				validateRow: props.validateRow,
4579				onClose: () => {
4580					setEditing(null);
4581					setIsAdding(false);
4582				},
4583				onSubmit: handleSubmit,
4584				trackingIdPrefix: props.trackingIdPrefix
4585			}),
4586			props.bulkAdd && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsRowDialog, {
4587				open: isAdding,
4588				title: props.addLabel,
4589				fields: props.bulkAdd.fields,
4590				row: null,
4591				otherRows: [],
4592				onClose: () => setIsAdding(false),
4593				onSubmit: (values) => {
4594					props.onChange([...props.rows, ...props.bulkAdd.toRows(values)]);
4595					setIsAdding(false);
4596				},
4597				trackingIdPrefix: `${props.trackingIdPrefix}-bulk`
4598			})
4599		]
4600	});
4601};
4602var sectionSx$11 = {
4603	display: "flex",
4604	flexDirection: "column",
4605	gap: 2
4606};
4607var headerSx$6 = {
4608	display: "flex",
4609	alignItems: "center",
4610	justifyContent: "space-between",
4611	gap: 2
4612};
4613var headerActionsSx$3 = {
4614	display: "flex",
4615	alignItems: "center",
4616	gap: 1,
4617	flexShrink: 0
4618};
4619var addButtonSx$1 = {
4620	flexShrink: 0,
4621	whiteSpace: "nowrap"
4622};
4623var cellSx$1 = {
4624	maxWidth: 320,
4625	wordBreak: "break-word"
4626};
4627var emptyCellSx$3 = {
4628	padding: "24px 16px",
4629	textAlign: "center"
4630};
4631//#endregion
4632//#region client/components/newNav/components/ads/dashboards/BudgetLanesPanel.tsx
4633/**
4634* A right-aligned percentage in the band colour, with the two counts under it. The 120px meter it replaces had a
4635* floating percentage and no axis, so it carried no information the number did not.
4636*/
4637var PaceCell = ({ paceIndex, delivered, expected, scope, dataTestId }) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
4638	variant: "body2",
4639	align: "right",
4640	color: paceColor(paceIndex, paceTolerance(scope)),
4641	sx: numberSx$1,
4642	"data-testid": dataTestId,
4643	children: formatWholePercent(paceIndex)
4644}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
4645	variant: "helper",
4646	align: "right",
4647	color: "text.secondary",
4648	component: "div",
4649	children: `${formatCompact(delivered)} of ${formatCompact(expected)} expected`
4650})] });
4651/**
4652* Every lane at once, worst pace first, so a laggard is visible without drilling in. The serving side does not
4653* write lane rows yet, so the delivery columns say so rather than showing zeros - a lane at 0 delivered reads as
4654* a broken lane rather than an unshipped feature.
4655*/
4656var BudgetLanesPanel = ({ lanes, hasLaneDelivery, unaccountedImpressions, campaignDelivered, selectedLaneName, onSelectLane, accountId, campaignId }) => {
4657	if (lanes.length === 0) return null;
4658	const ordered = [...lanes].sort((left, right) => (left.pacing?.paceIndex ?? Infinity) - (right.pacing?.paceIndex ?? Infinity));
4659	return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(AdsDashboardCard, {
4660		title: "Budget lanes",
4661		dataTestId: "ads-budget-lanes",
4662		subtitle: hasLaneDelivery ? "worst pace first" : "configuration only",
4663		infoTooltip: hasLaneDelivery ? "Click a row to scope the whole page to that lane." : "The serving side does not write lane rows yet, so these lanes have configuration but no delivery.",
4664		headerActions: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Button, {
4665			size: "small",
4666			variant: "text",
4667			component: Link$6,
4668			to: (0, import_dist$1.newNavAdsCampaignSectionPath)({
4669				accountId,
4670				campaignId,
4671				sectionId: ADS_CAMPAIGN_FLIGHTS_SECTION_ID
4672			}),
4673			"data-testid": "ads-lanes-edit-link",
4674			"data-tracking-id": "tracking-id-ads-dashboard-edit-lanes-link",
4675			children: "Edit lanes"
4676		}),
4677		children: [
4678			!hasLaneDelivery && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
4679				variant: "body2",
4680				color: "text.secondary",
4681				children: "These lanes are configured but the ad servers are not yet writing delivery per lane, so their goals are shown and their delivery is not."
4682			}),
4683			/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Table, {
4684				size: "small",
4685				children: [
4686					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableHead, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.TableRow, { children: [
4687						/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, {
4688							sx: headCellSx,
4689							children: "Lane"
4690						}),
4691						/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, {
4692							align: "right",
4693							sx: headCellSx,
4694							children: "Share"
4695						}),
4696						/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, {
4697							align: "right",
4698							sx: headCellSx,
4699							children: "Goal"
4700						}),
4701						/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, {
4702							align: "right",
4703							sx: headCellSx,
4704							children: "Flight pace"
4705						}),
4706						/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, {
4707							align: "right",
4708							sx: headCellSx,
4709							children: "Today pace"
4710						})
4711					] }) }),
4712					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableBody, { children: ordered.map((lane) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.TableRow, {
4713						selected: lane.name === selectedLaneName,
4714						onClick: () => onSelectLane(lane.name === selectedLaneName ? null : lane.name),
4715						sx: (0, import_dist$2.combineSxProps)(rowHoverSx, rowSx$2),
4716						"data-testid": `ads-lane-row-${lane.name}`,
4717						"data-tracking-id": "tracking-id-ads-dashboard-lane-row",
4718						children: [
4719							/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.TableCell, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
4720								variant: "body2",
4721								sx: laneNameSx$2,
4722								children: lane.name
4723							}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
4724								variant: "helper",
4725								color: "text.secondary",
4726								children: lane.isCatchAll ? "everywhere the earlier lanes did not match" : lane.geoFilterName ? `geo ${lane.geoFilterName}` : "no geography"
4727							})] }),
4728							/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, {
4729								align: "right",
4730								sx: bodyCellSx,
4731								children: `${lane.sharePercent}%`
4732							}),
4733							/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, {
4734								align: "right",
4735								sx: bodyCellSx,
4736								children: formatCompact(lane.goal)
4737							}),
4738							/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, {
4739								align: "right",
4740								sx: paceCellSx,
4741								children: lane.pacing ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(PaceCell, {
4742									paceIndex: lane.pacing.paceIndex,
4743									delivered: lane.pacing.delivered,
4744									expected: lane.pacing.expectedByNow,
4745									scope: "flight",
4746									dataTestId: `ads-lane-flight-pace-${lane.name}`
4747								}) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
4748									variant: "helper",
4749									color: "text.secondary",
4750									children: "not available yet"
4751								})
4752							}),
4753							/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, {
4754								align: "right",
4755								sx: paceCellSx,
4756								children: lane.pacing ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(PaceCell, {
4757									paceIndex: lane.pacing.todayPaceIndex,
4758									delivered: lane.pacing.deliveredToday,
4759									expected: lane.pacing.todayExpected,
4760									scope: "today",
4761									dataTestId: `ads-lane-today-pace-${lane.name}`
4762								}) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
4763									variant: "helper",
4764									color: "text.secondary",
4765									children: "-"
4766								})
4767							})
4768						]
4769					}, lane.name)) }),
4770					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableFooter, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.TableRow, {
4771						sx: adsTableTotalRowSx,
4772						children: [
4773							/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, {
4774								sx: bodyCellSx,
4775								children: "Campaign total"
4776							}),
4777							/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, {
4778								align: "right",
4779								sx: bodyCellSx,
4780								children: `${lanes.reduce((sum, lane) => sum + (lane.enabled ? lane.sharePercent : 0), 0)}%`
4781							}),
4782							/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, {
4783								align: "right",
4784								sx: bodyCellSx,
4785								children: formatCompact(lanes.reduce((sum, lane) => sum + lane.goal, 0))
4786							}),
4787							/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, {
4788								colSpan: 2,
4789								children: `${formatCompact(campaignDelivered)} delivered`
4790							})
4791						]
4792					}) })
4793				]
4794			}),
4795			unaccountedImpressions !== null && unaccountedImpressions !== 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
4796				variant: "body2",
4797				color: "warning.main",
4798				children: `${formatCount(unaccountedImpressions)} impressions are unaccounted for: the lanes do not add up to the campaign, which means a lane row is missing or traffic was served without one.`
4799			})
4800		]
4801	});
4802};
4803var rowSx$2 = { cursor: "pointer" };
4804var laneNameSx$2 = { fontWeight: 500 };
4805var paceCellSx = { minWidth: 120 };
4806var numberSx$1 = { fontVariantNumeric: "tabular-nums" };
4807//#endregion
4808//#region client/components/newNav/components/ads/dashboards/DeliveryByExchange.tsx
4809var DeliveryByExchange = ({ exchanges, totalImpressions, totalSpend }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsDashboardCard, {
4810	dataTestId: "ads-delivery-by-exchange",
4811	title: "Delivery by exchange",
4812	subtitle: "this flight",
4813	children: exchanges.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(DeliveryEmptyState, { message: "No delivery has been attributed to an exchange in this flight." }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Table, {
4814		size: "small",
4815		children: [
4816			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableHead, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.TableRow, { children: [
4817				/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, {
4818					sx: headCellSx,
4819					children: "Exchange"
4820				}),
4821				/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, {
4822					align: "right",
4823					sx: headCellSx,
4824					children: "Impressions"
4825				}),
4826				/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, {
4827					align: "right",
4828					sx: headCellSx,
4829					children: "Share"
4830				}),
4831				/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, {
4832					align: "right",
4833					sx: headCellSx,
4834					children: "Spend"
4835				}),
4836				/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, {
4837					align: "right",
4838					sx: headCellSx,
4839					children: "eCPM"
4840				}),
4841				/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, {
4842					align: "right",
4843					sx: headCellSx,
4844					children: "Daily cap"
4845				}),
4846				/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, {
4847					sx: headCellSx,
4848					children: "Status"
4849				})
4850			] }) }),
4851			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableBody, { children: exchanges.map((exchange) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.TableRow, {
4852				sx: rowHoverSx,
4853				children: [
4854					/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.TableCell, {
4855						sx: bodyCellSx,
4856						children: [exchange.name, /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
4857							variant: "helper",
4858							color: "text.secondary",
4859							component: "div",
4860							children: `id ${exchange.prefix}`
4861						})]
4862					}),
4863					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, {
4864						align: "right",
4865						sx: bodyCellSx,
4866						children: formatCount(exchange.impressions)
4867					}),
4868					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, {
4869						align: "right",
4870						sx: bodyCellSx,
4871						children: formatWholePercent(exchange.share)
4872					}),
4873					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, {
4874						align: "right",
4875						sx: bodyCellSx,
4876						children: formatMoney(exchange.spend)
4877					}),
4878					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, {
4879						align: "right",
4880						sx: bodyCellSx,
4881						children: formatMoney(exchange.ecpm)
4882					}),
4883					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, {
4884						align: "right",
4885						sx: bodyCellSx,
4886						children: exchange.isBlocked ? "blocked" : formatCount(exchange.dailyCap)
4887					}),
4888					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, { children: exchange.isBlocked ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.Label, {
4889						size: "small",
4890						variant: "standard",
4891						color: "error",
4892						label: "blocked by cap"
4893					}) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
4894						variant: "helper",
4895						color: "text.secondary",
4896						children: "-"
4897					}) })
4898				]
4899			}, exchange.prefix)) }),
4900			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableFooter, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.TableRow, {
4901				sx: adsTableTotalRowSx,
4902				children: [
4903					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, {
4904						sx: bodyCellSx,
4905						children: "Total"
4906					}),
4907					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, {
4908						align: "right",
4909						sx: bodyCellSx,
4910						children: formatCount(totalImpressions)
4911					}),
4912					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, {
4913						align: "right",
4914						sx: bodyCellSx,
4915						children: formatWholePercent(1)
4916					}),
4917					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, {
4918						align: "right",
4919						sx: bodyCellSx,
4920						children: formatMoney(totalSpend)
4921					}),
4922					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, {
4923						align: "right",
4924						sx: bodyCellSx,
4925						children: totalImpressions > 0 ? formatMoney(totalSpend * 1e3 / totalImpressions) : "-"
4926					}),
4927					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, { colSpan: 2 })
4928				]
4929			}) })
4930		]
4931	})
4932});
4933//#endregion
4934//#region client/components/newNav/components/ads/dashboards/LaneScopeSelector.tsx
4935/**
4936* The scope switch doubles as a summary: every lane carries its own pace, so a laggard is visible before anyone
4937* clicks into it.
4938*
4939* "Whole campaign" is deliberately outside the toggle group. In one group at one visual level nothing said the
4940* lanes are subsets of it, so the chevron and the words between the two say it instead.
4941*/
4942var LaneScopeSelector = ({ lanes, selected, onSelect, campaignPaceIndex, campaignTolerance }) => {
4943	const options = lanes.map((lane) => ({
4944		value: lane.name,
4945		name: lane.name,
4946		detail: `${lane.sharePercent}% share`,
4947		paceIndex: lane.pacing?.paceIndex ?? null,
4948		hasData: Boolean(lane.pacing)
4949	}));
4950	const selectedLane = lanes.find((lane) => lane.name === selected);
4951	return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
4952		sx: wrapperSx$2,
4953		children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
4954			sx: barSx$2,
4955			children: [
4956				/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
4957					variant: "caption",
4958					color: "text.secondary",
4959					children: "Scope"
4960				}),
4961				/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Button, {
4962					variant: selected === null ? "contained" : "outlined",
4963					sx: campaignButtonSx,
4964					onClick: () => onSelect(null),
4965					"data-testid": "ads-dashboard-scope-campaign",
4966					"data-tracking-id": "tracking-id-ads-dashboard-scope-campaign-btn",
4967					children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
4968						variant: "subtitle1",
4969						color: "inherit",
4970						noWrap: true,
4971						children: "Whole campaign"
4972					}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.Label, {
4973						size: "small",
4974						variant: "standard",
4975						color: paceStatusColor(campaignPaceIndex, campaignTolerance),
4976						label: formatWholePercent(campaignPaceIndex)
4977					})]
4978				}),
4979				options.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
4980					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(FontAwesomeIcon, { icon: import_faAngleRight.faAngleRight }),
4981					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
4982						variant: "caption",
4983						color: "text.secondary",
4984						children: "filtered to"
4985					}),
4986					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.TruffleToggleButtonGroup, {
4987						exclusive: true,
4988						value: selected ?? "",
4989						onChange: (_event, value) => onSelect(value === null || value === "" ? null : value),
4990						"aria-label": "Budget lane scope",
4991						sx: groupSx$3,
4992						children: options.map((option) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.ToggleButton, {
4993							value: option.value,
4994							sx: buttonSx,
4995							"data-testid": `ads-dashboard-scope-${option.value}`,
4996							"data-tracking-id": "tracking-id-ads-dashboard-scope-btn",
4997							children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
4998								variant: "subtitle2",
4999								color: "secondary",
5000								noWrap: true,
5001								children: option.name
5002							}), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
5003								sx: metaSx,
5004								children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
5005									variant: "helper",
5006									color: "text.secondary",
5007									children: option.detail
5008								}), option.hasData ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.Label, {
5009									size: "small",
5010									variant: "standard",
5011									color: paceStatusColor(option.paceIndex, paceTolerance()),
5012									label: formatWholePercent(option.paceIndex)
5013								}) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.Label, {
5014									size: "small",
5015									variant: "dashed",
5016									color: "default",
5017									label: "no data"
5018								})]
5019							})]
5020						}, option.value))
5021					})
5022				] })
5023			]
5024		}), selectedLane && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
5025			variant: "helper",
5026			color: "text.secondary",
5027			"data-testid": "ads-dashboard-scope-note",
5028			children: `Showing the ${selectedLane.name} lane only - ${selectedLane.sharePercent}% of the campaign goal. Every number below is this lane's.`
5029		})]
5030	});
5031};
5032var wrapperSx$2 = {
5033	display: "flex",
5034	flexDirection: "column",
5035	gap: .5,
5036	minWidth: 0
5037};
5038var barSx$2 = {
5039	display: "flex",
5040	alignItems: "center",
5041	gap: 1.5,
5042	flexWrap: "wrap"
5043};
5044var groupSx$3 = {
5045	flexWrap: "wrap",
5046	marginLeft: 1
5047};
5048var campaignButtonSx = {
5049	display: "flex",
5050	alignItems: "center",
5051	gap: 1,
5052	textTransform: "none",
5053	paddingY: 1,
5054	paddingX: 2
5055};
5056var buttonSx = {
5057	display: "flex",
5058	flexDirection: "column",
5059	alignItems: "flex-start",
5060	justifyContent: "center",
5061	gap: .5,
5062	textTransform: "none",
5063	paddingY: 1,
5064	paddingX: 1.5,
5065	minWidth: 140
5066};
5067var metaSx = {
5068	display: "flex",
5069	alignItems: "center",
5070	gap: .75
5071};
5072//#endregion
5073//#region client/components/newNav/components/ads/dashboards/PriceAndQualityCards.tsx
5074var Row = ({ label, value, note, valueColor, status, statusText }) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
5075	sx: rowSx$1,
5076	children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
5077		variant: "body2",
5078		color: "text.secondary",
5079		children: label
5080	}), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
5081		sx: rowValueSx,
5082		children: [
5083			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
5084				variant: "body2",
5085				sx: numberSx,
5086				color: valueColor,
5087				children: value
5088			}),
5089			note && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
5090				variant: "helper",
5091				color: "text.secondary",
5092				children: note
5093			}),
5094			status && status !== "default" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.Label, {
5095				size: "small",
5096				variant: "standard",
5097				color: status,
5098				label: statusText ?? BAND_LABELS[status]
5099			})
5100		]
5101	})]
5102});
5103/**
5104* Where the four non-impression goal types live, as indicators rather than charts. Every metric uses the same
5105* shape - value, target beside it, one bar with the target as a tick - so one visual idiom covers the page.
5106*/
5107var PriceAndQualityCards = ({ price, quality, elapsedShare, tolerance, delivered }) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
5108	sx: twoUpSx$2,
5109	children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsDashboardCard, {
5110		dataTestId: "ads-price-margin",
5111		title: "Price and margin",
5112		subtitle: "campaign total",
5113		children: price ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
5114			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Row, {
5115				label: "Customer CPM",
5116				value: formatMoney(price.customerCpm)
5117			}),
5118			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Row, {
5119				label: "Margin configured",
5120				value: formatRate(price.marginPercent / 100)
5121			}),
5122			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Row, {
5123				label: "Target CPM",
5124				value: formatMoney(price.targetCpm),
5125				note: "(1 - margin) x customer CPM"
5126			}),
5127			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Row, {
5128				label: "Spend",
5129				value: formatMoney(price.spend)
5130			}),
5131			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Row, {
5132				label: "Revenue",
5133				value: formatMoney(price.revenue)
5134			}),
5135			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Row, {
5136				label: "Realised margin",
5137				value: price.realisedMarginPercent === null ? "-" : formatRate(price.realisedMarginPercent / 100),
5138				valueColor: marginColor(price.realisedMarginPercent, price.marginPercent)
5139			}),
5140			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Row, {
5141				label: "Bid range",
5142				value: price.lowBidPrice === null || price.highBidPrice === null ? "-" : `${formatMoney(price.lowBidPrice)} - ${formatMoney(price.highBidPrice)}`
5143			}),
5144			price.spendCap !== null && price.spendCap > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsMeter, {
5145				name: "Spend against the flight cap",
5146				fill: price.spend / price.spendCap,
5147				value: formatWholePercent(price.spend / price.spendCap),
5148				target: `${formatMoney(price.spend)} of ${formatMoney(price.spendCap)}`,
5149				tick: elapsedShare,
5150				tickTitle: "Where the flight should be right now",
5151				color: inverseStatusColor(price.spendCap > 0 ? price.spend / price.spendCap : null, elapsedShare, tolerance),
5152				dataTestId: "ads-price-spend-cap"
5153			}
5153), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
5154				variant: "helper",
5155				color: "text.secondary",
5156				children: "Spend tracking below the tick while the eCPM is under target is the healthy case."
5157			})] })
5158		] }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
5159			variant: "body2",
5160			color: "text.secondary",
5161			children: "No flight covers today, so there is no price to show."
5162		})
5163	}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsDashboardCard, {
5164		dataTestId: "ads-quality",
5165		title: "Delivery quality vs target",
5166		infoTooltip: "Each metric is its running sum divided by impressions.",
5167		children: quality.map((metric) => {
5168			const isUnreported = metric.key === "completionRate" && metric.actual === 0;
5169			return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react.Fragment, { children: [
5170				/* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsMeter, {
5171					name: QUALITY_LABELS[metric.key] ?? metric.key,
5172					fill: isUnreported ? null : metric.actual,
5173					value: metric.actual === null || isUnreported ? "-" : formatRate(metric.actual),
5174					target: isUnreported ? "no completed views in the delivery data" : metric.target === null ? metric.key === "completionRate" ? "no target - informational" : "no target configured" : `target ${formatRate(metric.target)}`,
5175					tick: metric.target,
5176					tickTitle: "The configured target",
5177					color: metric.target === null || metric.actual === null || isUnreported ? "default" : paceStatusColor(metric.actual / metric.target, tolerance),
5178					dataTestId: `ads-quality-${metric.key}`
5179				}),
5180				isUnreported && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
5181					variant: "helper",
5182					color: "text.secondary",
5183					children: "The tracking pipeline has recorded zero completed views for this campaign across the whole flight. Until completion events arrive, this rate cannot be computed."
5184				}),
5185				metric.key === "completionRate" && delivered <= 1e3 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
5186					variant: "helper",
5187					color: "text.secondary",
5188					children: `Fewer than ${formatCount(1e3)} impressions so far, so the CPCV goal is still using the assumed 50%.`
5189				})
5190			] }, metric.key);
5191		})
5192	})]
5193});
5194var twoUpSx$2 = {
5195	display: "grid",
5196	gridTemplateColumns: "repeat(auto-fit, minmax(360px, 1fr))",
5197	gap: 3,
5198	minWidth: 0
5199};
5200var rowSx$1 = {
5201	display: "grid",
5202	gridTemplateColumns: "minmax(0, 1fr) auto",
5203	alignItems: "baseline",
5204	gap: 2,
5205	maxWidth: 480
5206};
5207var rowValueSx = {
5208	display: "flex",
5209	alignItems: "baseline",
5210	gap: 1
5211};
5212var numberSx = { fontVariantNumeric: "tabular-nums" };
5213//#endregion
5214//#region client/components/newNav/components/ads/dashboards/ScoreThresholdPanel.tsx
5215var CHART_HEIGHT$3 = 120;
5216var STATE_COLORS = {
5217	[GqlClientAdsSpaceThresholdState.PINNED_AT_MINIMUM]: "error",
5218	[GqlClientAdsSpaceThresholdState.LOOSENING]: "warning",
5219	[GqlClientAdsSpaceThresholdState.STEADY]: "success",
5220	[GqlClientAdsSpaceThresholdState.TIGHTENING]: "warning",
5221	[GqlClientAdsSpaceThresholdState.PINNED_AT_MAXIMUM]: "error"
5222};
5223var formatThreshold = (value) => value === null || value === void 0 ? "-" : value.toFixed(2);
5224/**
5225* The admission bar: a bid request whose score does not clear it gets no bid. The raw one-minute series is mostly
5226* jitter, so what is shown is a state, a position on the campaign's own range, and a smoothed band.
5227*/
5228var ScoreThresholdPanel = ({ threshold, lanes, laneName }) => {
5229	const theme = (0, import_material.useTheme)();
5230	const option = (0, import_react.useMemo)(() => {
5231		if (!threshold || threshold.history.length === 0) return null;
5232		const labels = threshold.history.map((point) => formatUtcTime(point.at).slice(0, 5));
5233		return {
5234			grid: {
5235				left: 44,
5236				right: 12,
5237				top: 8,
5238				bottom: 20
5239			},
5240			tooltip: {
5241				trigger: "axis",
5242				backgroundColor: theme.palette.background.paper,
5243				borderColor: theme.palette.divider,
5244				textStyle: {
5245					color: theme.palette.text.primary,
5246					fontSize: 12
5247				}
5248			},
5249			xAxis: {
5250				type: "category",
5251				data: labels,
5252				axisLabel: {
5253					color: theme.palette.text.secondary,
5254					fontSize: 10
5255				},
5256				axisLine: { lineStyle: { color: theme.palette.divider } },
5257				axisTick: { show: false }
5258			},
5259			yAxis: {
5260				type: "value",
5261				scale: true,
5262				axisLabel: {
5263					color: theme.palette.text.secondary,
5264					fontSize: 10,
5265					formatter: (value) => value.toFixed(2)
5266				},
5267				splitLine: { lineStyle: {
5268					color: theme.palette.divider,
5269					type: "dashed"
5270				} }
5271			},
5272			series: [
5273				{
5274					name: "min",
5275					type: "line",
5276					stack: "band",
5277					showSymbol: false,
5278					lineStyle: { opacity: 0 },
5279					data: threshold.history.map((point) => point.min)
5280				},
5281				{
5282					name: "spread",
5283					type: "line",
5284					stack: "band",
5285					showSymbol: false,
5286					lineStyle: { opacity: 0 },
5287					areaStyle: {
5288						color: theme.palette.primary.light,
5289						opacity: .35
5290					},
5291					data: threshold.history.map((point) => point.max - point.min)
5292				},
5293				{
5294					name: "median",
5295					type: "line",
5296					showSymbol: false,
5297					smooth: true,
5298					lineStyle: {
5299						color: theme.palette.primary.main,
5300						width: 2
5301					},
5302					itemStyle: { color: theme.palette.primary.main },
5303					data: threshold.history.map((point) => point.median)
5304				}
5305			]
5306		};
5307	}, [threshold, theme]);
5308	if (!laneName && lanes.length > 0) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsDashboardCard, {
5309		dataTestId: "ads-score-threshold",
5310		title: "Score threshold",
5311		subtitle: "one per lane",
5312		children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(LaneThresholdSummary, { lanes })
5313	});
5314	if (!threshold) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsDashboardCard, {
5315		dataTestId: "ads-score-threshold",
5316		title: "Score threshold",
5317		children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(DeliveryEmptyState, { message: "This campaign has no pacing adjustments to show." })
5318	});
5319	if (!threshold.isCurrent) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsDashboardCard, {
5320		dataTestId: "ads-score-threshold",
5321		title: "Score threshold",
5322		children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(DeliveryEmptyState, { message: threshold.writtenAt ? `No recent pacing data. The last value was written ${formatRelativeAge(threshold.writtenAt)}, so it does not describe this campaign now.` : "No pacing data has been written for this campaign." })
5323	});
5324	const railMin = threshold.isRangeDefaulted && threshold.observedMin !== null ? threshold.observedMin : threshold.rangeMin;
5325	const railMax = threshold.isRangeDefaulted && threshold.observedMax !== null ? threshold.observedMax : threshold.rangeMax;
5326	const span = Math.max(railMax - railMin, Number.EPSILON);
5327	const position = Math.min(1, Math.max(0, (threshold.value - railMin) / span));
5328	const bandStart = Math.min(1, Math.max(0, ((threshold.observedMin ?? railMin) - railMin) / span));
5329	const bandWidth = Math.min(1 - bandStart, Math.max(0, ((threshold.observedMax ?? railMax) - (threshold.observedMin ?? railMin)) / span));
5330	return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(AdsDashboardCard, {
5331		dataTestId: "ads-score-threshold",
5332		title: "Score threshold",
5333		subtitle: `updated ${formatUtcTime(threshold.writtenAt)}`,
5334		infoTooltip: `The admission bar: a bid request scoring below it gets no bid.${threshold.host ? ` Last written by ${threshold.host}.` : ""}`,
5335		children: [
5336			/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
5337				sx: stateRowSx,
5338				children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.Label, {
5339					size: "medium",
5340					variant: "standard",
5341					color: STATE_COLORS[threshold.state],
5342					label: THRESHOLD_STATE_LABELS[threshold.state]
5343				}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
5344					variant: "h4",
5345					sx: valueSx,
5346					children: formatThreshold(threshold.value)
5347				})]
5348			}),
5349			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
5350				variant: "body2",
5351				color: "text.secondary",
5352				children: THRESHOLD_STATE_MEANINGS[threshold.state]
5353			}),
5354			threshold.overrideValue !== null && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
5355				variant: "body2",
5356				color: "warning.main",
5357				children: `This campaign is pinned to ${formatThreshold(threshold.overrideValue)} by an override, so it is not adapting at all.`
5358			}),
5359			/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, { children: [
5360				/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
5361					sx: railSx,
5362					children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Box, { sx: observedBandSx(bandStart, bandWidth) }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Box, { sx: markerSx(position) })]
5363				}),
5364				/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
5365					sx: railLabelsSx,
5366					children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
5367						variant: "helper",
5368						color: "text.secondary",
5369						children: `${formatThreshold(railMin)} - bid on everything`
5370					}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
5371						variant: "helper",
5372						color: "text.secondary",
5373						children: `bid on nothing - ${formatThreshold(railMax)}`
5374					})]
5375				}),
5376				threshold.isRangeDefaulted && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
5377					variant: "helper",
5378					color: "text.secondary",
5379					children: "The configured range is not set for this campaign, so the scale is drawn from the value's own observed range."
5380				})
5381			] }),
5382			option && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AnalyticsChart, {
5383				dataTestId: "ads-score-threshold-history",
5384				title: "Last 30 minutes",
5385				option,
5386				containerStyle: { height: CHART_HEIGHT$3 },
5387				removeBorder: true
5388			})
5389		]
5390	});
5391};
5392/** At all-lanes scope: one compact row per lane, since there is no single campaign value to show. */
5393var LaneThresholdSummary = ({ lanes }) => {
5394	const withData = lanes.filter((lane) => lane.scoreThreshold?.isCurrent);
5395	if (withData.length === 0) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(DeliveryEmptyState, { message: "Per-lane pacing data is not available yet, so there is no threshold to show for any lane." });
5396	const pinned = withData.filter((lane) => lane.scoreThreshold.state === GqlClientAdsSpaceThresholdState.PINNED_AT_MINIMUM || lane.scoreThreshold.state === GqlClientAdsSpaceThresholdState.PINNED_AT_MAXIMUM).length;
5397	const values = withData.map((lane) => lane.scoreThreshold.value);
5398	return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Typography, {
5399		variant: "body2",
5400		color: "text.secondary",
5401		children: [`${formatThreshold(Math.min(...values))} to ${formatThreshold(Math.max(...values))} across ${withData.length} lanes`, pinned > 0 ? `, ${pinned} pinned` : ""]
5402	}), withData.map((lane) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
5403		sx: laneRowSx,
5404		children: [
5405			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
5406				variant: "body2",
5407				sx: laneNameSx$1,
5408				children: lane.name
5409			}),
5410			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.Label, {
5411				size: "small",
5412				variant: "standard",
5413				color: STATE_COLORS[lane.scoreThreshold.state],
5414				label: THRESHOLD_STATE_LABELS[lane.scoreThreshold.state]
5415			}),
5416			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
5417				variant: "subtitle2",
5418				sx: valueSx,
5419				children: formatThreshold(lane.scoreThreshold.value)
5420			})
5421		]
5422	}, lane.name))] });
5423};
5424var stateRowSx = {
5425	display: "flex",
5426	alignItems: "center",
5427	justifyContent: "space-between",
5428	gap: 2
5429};
5430var valueSx = { fontVariantNumeric: "tabular-nums" };
5431var railSx = {
5432	position: "relative",
5433	height: 12,
5434	borderRadius: "6px",
5435	backgroundColor: "grey.200",
5436	overflow: "hidden"
5437};
5438var observedBandSx = (start, width) => ({
5439	position: "absolute",
5440	top: 0,
5441	bottom: 0,
5442	left: `${start * 100}%`,
5443	width: `${width * 100}%`,
5444	backgroundColor: "primary.light"
5445});
5446var markerSx = (position) => ({
5447	position: "absolute",
5448	top: 0,
5449	bottom: 0,
5450	left: `${position * 100}%`,
5451	width: "3px",
5452	backgroundColor: "primary.main"
5453});
5454var railLabelsSx = {
5455	display: "flex",
5456	justifyContent: "space-between",
5457	gap: 1,
5458	marginTop: .5
5459};
5460var laneRowSx = {
5461	display: "flex",
5462	alignItems: "center",
5463	gap: 1,
5464	justifyContent: "space-between"
5465};
5466var laneNameSx$1 = {
5467	fontWeight: 500,
5468	minWidth: 0,
5469	overflow: "hidden",
5470	textOverflow: "ellipsis",
5471	whiteSpace: "nowrap"
5472};
5473//#endregion
5474//#region client/components/newNav/components/ads/dashboards/VolumeDistributionCard.tsx
5475var CHART_HEIGHT$2 = 320;
5476/**
5477* The hours-distribution plan behind every "expected by now" number on this page. It sits directly under the
5478* Today chart because the dashed line there is this distribution - the two explain each other.
5479*/
5480var VolumeDistributionCard = ({ distribution }) => {
5481	const theme = (0, import_material.useTheme)();
5482	const [isStacked, setIsStacked] = (0, import_react.useState)(false);
5483	const option = (0, import_react.useMemo)(() => distribution.isCustom ? volumeDistributionChartOption(distribution.byDay, theme, isStacked) : null, [
5484		distribution,
5485		theme,
5486		isStacked
5487	]);
5488	return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsDashboardCard, {
5489		dataTestId: "ads-volume-distribution",
5490		title: "Volume distribution",
5491		infoTooltip: "The share of the week's delivery the plan puts in each hour. This is what every expected figure on the page is derived from.",
5492		headerActions: distribution.isCustom ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_dist$2.TruffleToggleButtonGroup, {
5493			exclusive: true,
5494			size: "small",
5495			value: isStacked ? "stacked" : "byDay",
5496			onChange: (_event, value) => value !== null && setIsStacked(value === "stacked"),
5497			"aria-label": "Volume distribution view",
5498			children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.ToggleButton, {
5499				value: "byDay",
5500				"data-tracking-id": "tracking-id-ads-volume-distribution-by-day-btn",
5501				children: "By day"
5502			}
5502), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.ToggleButton, {
5503				value: "stacked",
5504				"data-tracking-id": "tracking-id-ads-volume-distribution-stacked-btn",
5505				children: "Stacked"
5506			})]
5507		}) : void 0,
5508		children: distribution.isCustom ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
5509			sx: pathSx,
5510			children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
5511				variant: "helper",
5512				color: "text.secondary",
5513				children: "Read from"
5514			}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsCopyableText, {
5515				value: distribution.path,
5516				monospace: true,
5517				maxWidth: 520,
5518				dataTestId: "ads-volume-distribution-path",
5519				trackingId: "tracking-id-ads-volume-distribution-path-copy"
5520			})]
5521		}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AnalyticsChart, {
5522			dataTestId: "ads-volume-distribution-chart",
5523			title: "",
5524			option,
5525			removeBorder: true,
5526			containerStyle: { height: CHART_HEIGHT$2 }
5527		})] }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
5528			variant: "body2",
5529			color: "text.secondary",
5530			children: "No hours-distribution file was readable for this campaign, so delivery is planned flat across the whole flight and every \"expected by now\" on this page is elapsed clock time."
5531		})
5532	});
5533};
5534var pathSx = {
5535	display: "flex",
5536	alignItems: "center",
5537	gap: 1,
5538	minWidth: 0
5539};
5540//#endregion
5541//#region client/components/newNav/components/ads/dashboards/CampaignDashboard.tsx
5542var CHART_HEIGHT$1 = 300;
5543/**
5544* Delivery against plan for one campaign, or for one of its lanes. Everything is read-only; the settings that
5545* explain what is shown here are the sections next door.
5546*/
5547var CampaignDashboard = ({ campaignId }) => {
5548	const theme = (0, import_material.useTheme)();
5549	const accountId = useActiveAccountId();
5550	const [laneName, setLaneName] = (0, import_react.useState)(null);
5551	const { delivery, loading, isRefreshing, error, refresh, campaignPaceIndex } = useAdsSpaceCampaignDelivery(campaignId, laneName);
5552	const flightOption = (0, import_react.useMemo)(() => delivery ? flightChartOption(delivery.flightSeries, delivery.pacing?.goal ?? 0, theme) : null, [delivery, theme]);
5553	const todayOption = (0, import_react.useMemo)(() => delivery ? todayChartOption(delivery.todaySeries, theme) : null, [delivery, theme]);
5554	if (loading) return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
5555		sx: stackSx$5,
5556		children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(DeliverySkeleton, { height: 96 }
5556), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(DeliverySkeleton, { height: CHART_HEIGHT$1 })]
5557	});
5558	if (error || !delivery) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(DeliveryEmptyState, {
5559		title: "No delivery to show",
5560		message: "This campaign's delivery could not be read. The settings sections still apply."
5561	});
5562	const { pacing, flight } = delivery;
5563	const selectedLane = delivery.lanes.find((lane) => lane.name === laneName);
5564	const stateChip = campaignStateChip(delivery.enabled, Boolean(flight), delivery.status, delivery.isPausedOutsideSpace);
5565	return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
5566		sx: stackSx$5,
5567		children: [
5568			/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
5569				sx: headerSx$5,
5570				children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
5571					sx: chipsSx$1,
5572					children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Tooltip, {
5573						title: stateChip.tooltip ?? "",
5574						children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.Label, {
5575							size: "small",
5576							label: stateChip.label,
5577							color: stateChip.color,
5578							variant: "standard"
5579						})
5580					}), flight && pacing && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
5581						variant: "caption",
5582						color: "text.secondary",
5583						children: `Flight ${flight.position} of ${flight.count} - day ${pacing.dayIndex} of ${pacing.dayCount} - ${pacing.daysLeft} days left`
5584					})]
5585				}), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
5586					sx: headerRightSx,
5587					children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
5588						sx: asOfSx,
5589						children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(DeliveryFreshness, {
5590							lastUpdateTime: delivery.freshness.lastUpdateTime,
5591							isKnown: delivery.freshness.isKnown
5592						}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
5593							variant: "helper",
5594							color: "text.secondary",
5595							children: `Config version ${delivery.configVersion ?? "-"}, last changed ${delivery.updatedAt ? formatDate(delivery.updatedAt.slice(0, 10)) : "-"}`
5596						})]
5597					}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(DeliveryRefreshButton, {
5598						isRefreshing,
5599						onRefresh: refresh
5600					})]
5601				})]
5602			}),
5603			delivery.lanes.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Divider, {}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Box, {
5604				sx: stickyScopeSx,
5605				children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(LaneScopeSelector, {
5606					lanes: delivery.lanes,
5607					selected: laneName,
5608					onSelect: setLaneName,
5609					campaignPaceIndex,
5610					campaignTolerance: paceTolerance()
5611				})
5612			})] }),
5613			!flight || !pacing ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(DeliveryEmptyState, {
5614				title: "No flight covers today",
5615				message: "There is no goal to pace against until a flight starts. The settings sections still apply."
5616			}) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
5617				sx: contentSx$10(isRefreshing),
5618				children: [
5619					/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
5620						sx: kpiStripSx$1,
5621						children: [
5622							/* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsKpiTile, {
5623								dataTestId: "ads-kpi-goal",
5624								label: "Goal",
5625								value: formatCount(pacing.goal),
5626								subLine: selectedLane ? `${selectedLane.sharePercent}% of the campaign` : "impressions, this flight"
5627							}),
5628							/* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsKpiTile, {
5629								dataTestId: "ads-kpi-delivered",
5630								label: "Delivered",
5631								value: formatCount(pacing.delivered),
5632								subLine: `${formatWholePercent(pacing.goal > 0 ? pacing.delivered / pacing.goal : null)} of goal`
5633							}),
5634							/* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsKpiTile, {
5635								dataTestId: "ads-kpi-pace",
5636								label: "Pace",
5637								value: describePace(pacing),
5638								subLine: `expected by now ${formatCompact(pacing.expectedByNow)}`,
5639								status: pacing.hasEnoughData ? paceSkewChip(pacing.delivered, pacing.expectedByNow, pacing.goal) : void 0,
5640								infoTooltip: "Delivered divided by what the volume-weighted plan expected by now. 100% is exactly on plan."
5641							}),
5642							/* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsKpiTile, {
5643								dataTestId: "ads-kpi-today",
5644								label: "Today",
5645								value: formatCount(pacing.deliveredToday),
5646								subLine: `of ${formatCompact(pacing.todayGoal)} planned for today`,
5647								status: {
5648									text: formatWholePercent(pacing.todayPaceIndex),
5649									color: paceStatusColor(pacing.todayPaceIndex, paceTolerance("today"))
5650								}
5651							}),
5652							/* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsKpiTile, {
5653								dataTestId: "ads-kpi-ecpm",
5654								label: "eCPM",
5655								value: formatMoney(delivery.price?.actualEcpm),
5656								subLine: delivery.price ? `target ${formatMoney(delivery.price.targetCpm)} - margin ${formatRate(delivery.price.marginPercent / 100)}` : "no price configured",
5657								status: ecpmStatus(delivery.price?.actualEcpm, delivery.price?.targetCpm, paceTolerance()),
5658								infoTooltip: "Spend so far divided by impressions so far, against the target the configured margin implies. Under target is the good direction."
5659							})
5660						]
5661					}),
5662					/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
5663						sx: twoUpSx$1,
5664						children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(AnalyticsChart, {
5665							dataTestId: "ads-flight-delivery",
5666							title: "Flight: delivered vs expected",
5667							SubTitle: `${formatDate(flight.startDate)} - ${formatDate(flight.endDate)}`,
5668							tooltipText: `Cumulative delivered against the volume-weighted plan. Green is delivery that ran ahead of plan, red is the amount it is behind by. ${formatCompact(pacing.remaining)} still to deliver.` + (pacing.isVolumeWeighted ? "" : " No hours-distribution file was readable, so delivery is expected flat and the pace is approximate."),
5669							option: flightOption,
5670							containerStyle: { height: CHART_HEIGHT$1 }
5671						}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AnalyticsChart, {
5672							dataTestId: "ads-today-delivery",
5673							title: "Today: delivered vs expected",
5674							SubTitle: `${formatWholePercent(pacing.todayPaceIndex)} of expected`,
5675							tooltipText: "Each finished hour against what the plan expected of it. The hour in progress shows what it has delivered plus, in dashed gray, what the plan still expects of it before the hour ends. The reference is expected, not goal - the engine also folds in an overclock factor and holidays.",
5676							option: todayOption,
5677							containerStyle: { height: CHART_HEIGHT$1 }
5678						})]
5679					}),
5680					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(VolumeDistributionCard, { distribution: delivery.volumeDistribution }),
5681					/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
5682						sx: twoUpSx$1,
5683						children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(ScoreThresholdPanel, {
5684							threshold: delivery.scoreThreshold,
5685							lanes: delivery.lanes,
5686							laneName
5687						}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(BudgetLanesPanel, {
5688							lanes: delivery.lanes,
5689							hasLaneDelivery: delivery.hasLaneDelivery,
5690							unaccountedImpressions: delivery.unaccountedImpressions,
5691							campaignDelivered: pacing.delivered,
5692							selectedLaneName: laneName,
5693							onSelectLane: setLaneName,
5694							accountId,
5695							campaignId
5696						})]
5697					}),
5698					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(PriceAndQualityCards, {
5699						price: delivery.price,
5700						quality: delivery.quality,
5701						elapsedShare: pacing.elapsedShare,
5702						tolerance: paceTolerance(),
5703						delivered: pacing.delivered
5704					}),
5705					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(DeliveryByExchange, {
5706						exchanges: delivery.exchanges,
5707						totalImpressions: pacing.delivered,
5708						totalSpend: delivery.price?.spend ?? 0
5709					})
5710				]
5711			})
5712		]
5713	});
5714};
5715var stackSx$5 = {
5716	display: "flex",
5717	flexDirection: "column",
5718	gap: 3,
5719	minWidth: 0,
5720	flex: 1,
5721	minHeight: 0,
5722	overflowY: "auto",
5723	paddingBottom: 2,
5724	...import_dist$2.scrollOnHoverSxProps
5725};
5726var contentSx$10 = (isRefreshing) => ({
5727	display: "flex",
5728	flexDirection: "column",
5729	gap: 3,
5730	minWidth: 0,
5731	opacity: isRefreshing ? .6 : 1,
5732	transition: "opacity 150ms ease-in-out"
5733});
5734var stickyScopeSx = {
5735	position: "sticky",
5736	top: 0,
5737	zIndex: 2,
5738	flexShrink: 0,
5739	backgroundColor: "background.paper",
5740	borderBottom: 1,
5741	borderColor: "divider",
5742	paddingY: 1.5
5743};
5744var headerSx$5 = {
5745	display: "flex",
5746	justifyContent: "space-between",
5747	alignItems: "flex-start",
5748	gap: 2,
5749	flexWrap: "wrap"
5750};
5751var chipsSx$1 = {
5752	display: "flex",
5753	gap: 1,
5754	flexWrap: "wrap",
5755	alignItems: "center"
5756};
5757var headerRightSx = {
5758	display: "flex",
5759	alignItems: "flex-start",
5760	gap: 1
5761};
5762var asOfSx = {
5763	display: "flex",
5764	flexDirection: "column",
5765	alignItems: "flex-end",
5766	textAlign: "right"
5767};
5768var kpiStripSx$1 = {
5769	display: "grid",
5770	gridTemplateColumns: {
5771		xs: "repeat(2, 1fr)",
5772		sm: "repeat(3, 1fr)",
5773		md: "repeat(5, 1fr)"
5774	},
5775	gap: 2
5776};
5777var twoUpSx$1 = {
5778	display: "grid",
5779	gridTemplateColumns: "repeat(auto-fit, minmax(360px, 1fr))",
5780	gap: 2,
5781	minWidth: 0
5782};
5783//#endregion
5784//#region client/components/newNav/components/ads/common/adsReferenceData.ts
5785/**
5786* The closed value sets of the ads configuration model. The old app rendered most of these as free text, which
5787* is how `platform_type` and `platformType` both ended up in the data - a key the server does not understand is
5788* ignored without a word.
5789*/
5790/** Exchange prefix to name. Also the value set behind exchange caps and per-exchange score bumps. */
5791var exchangeOptions = [
5792	{
5793		value: "2",
5794		label: "Oath"
5795	},
5796	{
5797		value: "4",
5798		label: "Infolinks"
5799	},
5800	{
5801		value: "6",
5802		label: "RobustApps"
5803	},
5804	{
5805		value: "7",
5806		label: "Teads"
5807	},
5808	{
5809		value: "8",
5810		label: "Connatix"
5811	},
5812	{
5813		value: "9",
5814		label: "Google"
5815	},
5816	{
5817		value: "10",
5818		label: "AppNexus"
5819	},
5820	{
5821		value: "11",
5822		label: "Equativ"
5823	},
5824	{
5825		value: "12",
5826		label: "Pubmatic"
5827	},
5828	{
5829		value: "13",
5830		label: "Vidazoo"
5831	},
5832	{
5833		value: "14",
5834		label: "Tremor"
5835	},
5836	{
5837		value: "15",
5838		label: "ConvertMedia"
5839	},
5840	{
5841		value: "16",
5842		label: "BeachFront"
5843	},
5844	{
5845		value: "17",
5846		label: "OpenX"
5847	},
5848	{
5849		value: "18",
5850		label: "EXCO"
5851	},
5852	{
5853		value: "19",
5854		label: "Rubicon"
5855	},
5856	{
5857		value: "50",
5858		label: "TheTradeDesk"
5859	},
5860	{
5861		value: "51",
5862		label: "Adobe"
5863	},
5864	{
5865		value: "52",
5866		label: "DV360"
5867	},
5868	{
5869		value: "100",
5870		label: "LiveRail"
5871	}
5872];
5873var siteActionMethodOptions = [
5874	{
5875		value: "uui",
5876		label: "uui"
5877	},
5878	{
5879		value: "cart",
5880		label: "cart"
5881	},
5882	{
5883		value: "atc",
5884		label: "atc"
5885	},
5886	{
5887		value: "conv",
5888		label: "conv"
5889	}
5890];
5891var productCatalogDbOptions = [{
5892	value: "0",
5893	label: "default_db"
5894}, {
5895	value: "4",
5896	label: "redis2"
5897}];
5898/** Sync partners an account can be excluded from. */
5899var syncPartnerOptions = [
5900	{
5901		value: "adobe",
5902		label: "adobe"
5903	},
5904	{
5905		value: "beachfront",
5906		label: "beachfront"
5907	},
5908	{
5909		value: "bluekai",
5910		label: "bluekai"
5911	},
5912	{
5913		value: "crosswise",
5914		label: "crosswise"
5915	},
5916	{
5917		value: "liveramp",
5918		label: "liveramp"
5919	},
5920	{
5921		value: "teads",
5922		label: "teads"
5923	},
5924	{
5925		value: "thetradedesk",
5926		label: "thetradedesk"
5927	}
5928];
5929var reportDateFormatOptions = [{
5930	value: "%Y-%m-%d",
5931	label: "yyyy-mm-dd (Default)"
5932}, {
5933	value: "%d-%b-%y",
5934	label: "dd-MM-yy"
5935}];
5936var viewabilityOverrideOptions = [
5937	{
5938		value: "NONE",
5939		label: "Disabled"
5940	},
5941	{
5942		value: "BY_SITE",
5943		label: "By site"
5944	},
5945	{
5946		value: "AUTO_CORRECT",
5947		label: "Auto correct"
5948	}
5949];
5950var videoFormatOptions = [
5951	{
5952		value: "mp4",
5953		label: "mp4"
5954	},
5955	{
5956		value: "webm",
5957		label: "webm"
5958	},
5959	{
5960		value: "hls",
5961		label: "hls"
5962	}
5963];
5964/** The hash space an experiment splits between its groups. */
5965var EXPERIMENT_RANGE_MAX = 1e4;
5966var deviceModeOptions = [{
5967	value: "DESKTOP_MOBILE",
5968	label: "Desktop & Mobile"
5969}, {
5970	value: "TV",
5971	label: "CTV"
5972}];
5973/** The same three values behind each of the four per-platform behaviour fields. */
5974var deviceBehaviorOptions = [
5975	{
5976		value: "0",
5977		label: "Allow cross-device"
5978	},
5979	{
5980		value: "1",
5981		label: "Disallow cross-device"
5982	},
5983	{
5984		value: "2",
5985		label: "Disable Campaign"
5986	}
5987];
5988/** Stored as the raw player-size range strings the ad server matches against. */
5989var desktopPlayerSizeOptions = [
5990	{
5991		value: "0x0-299x4096;0x0-4096x249",
5992		label: "Tiny (Width < 300 or Height < 250)"
5993	},
5994	{
5995		value: "300x250-300x250",
5996		label: "Small (300x250 Exactly)"
5997	},
5998	{
5999		value: "301x250-599x4096;300x251-300x4096",
6000		label: "Medium (300 < Width < 600)"
6001	},
6002	{
6003		value: "600x250-4096x4096",
6004		label: "Large (Width >= 600)"
6005	}
6006];
6007var mobilePlayerSizeOptions = [
6008	{
6009		value: "0x0-4096x47",
6010		label: "Tiny (Height < 48)"
6011	},
6012	{
6013		value: "0x48-4096x199;120x600-120x600",
6014		label: "Small (48 <= Height < 200, or 120x600)"
6015	},
6016	{
6017		value: "0x200-4096x480",
6018		label: "Medium (200 <= Height <= 480)"
6019	},
6020	{
6021		value: "0x481-4096x599;0x600-119x600;121x600-4096x600;0x601-4096x4096",
6022		label: "Large (Height > 480)"
6023	}
6024];
6025var controlGroupModeOptions = [
6026	{
6027		value: "DISABLED",
6028		label: "Disabled (all bids will display a SmartVideo)"
6029	},
6030	{
6031		value: "ENABLED",
6032		label: "Enabled (users who fall inside the group will receive a special video)"
6033	},
6034	{
6035		value: "GHOST",
6036		label: "Ghost (users who fall inside the group will not receive a bid)"
6037	}
6038];
6039var videoQualityProfileOptions = [
6040	{
6041		value: "SD",
6042		label: "SD"
6043	},
6044	{
6045		value: "SD_INTERMEDIATE",
6046		label: "SD_INTERMEDIATE"
6047	},
6048	{
6049		value: "SD_HIGH",
6050		label: "SD_HIGH"
6051	},
6052	{
6053		value: "HD_READY",
6054		label: "HD_READY"
6055	},
6056	{
6057		value: "HD_READY_HIGH",
6058		label: "HD_READY_HIGH"
6059	},
6060	{
6061		value: "HD",
6062		label: "HD"
6063	}
6064];
6065var optimizationControlModeOptions = [
6066	{
6067		value: "FIXED_VALUE",
6068		label: "Fixed value"
6069	},
6070	{
6071		value: "RANDOM_VALUE",
6072		label: "Random from model values"
6073	},
6074	{
6075		value: "BASED_ON_EXPERIMENT_GROUP",
6076		label: "By experiment group"
6077	}
6078];
6079var reportingSourceOptions = [
6080	{
6081		value: "USER_EVENT",
6082		label: "Site Event"
6083	},
6084	{
6085		value: "OPPORTUNITY",
6086		label: "Opportunity"
6087	},
6088	{
6089		value: "RECOMMENDATION_ENGINE",
6090		label: "Recommendation Engine"
6091	},
6092	{
6093		value: "USER_DATA",
6094		label: "User Data"
6095	},
6096	{
6097		value: "LOCATION",
6098		label: "Location"
6099	}
6100];
6101/**
6102* Which Field values a reporting definition accepts depends on its Source. Free text is how `platform_type` and
6103* `platformType` both ended up in the data.
6104*/
6105var reportingFieldOptionsBySource = {
6106	RECOMMENDATION_ENGINE: [
6107		"rule",
6108		"story",
6109		"video_sku",
6110		"sku",
6111		"product_catalog_category",
6112		"user_event_category",
6113		"user_event_method",
6114		"qualification_groups"
6115	].map((value) => ({
6116		value,
6117		label: value
6118	})),
6119	USER_DATA: [
6120		{
6121			value: "experiments",
6122			label: "experiments (experiment groups, comma delimited)"
6123		},
6124		{
6125			value: "dmp_segments",
6126			label: "dmp_segments (user's DMP segments, comma delimited)"
6127		},
6128		{
6129			value: "audience",
6130			label: "audience (first DMP List audience value from the Audience Mapping Logic csv)"
6131		},
6132		{
6133			value: "audiences",
6134			label: "audiences (matching Program audience names, comma delimited)"
6135		}
6136	],
6137	LOCATION: [
6138		{
6139			value: "zip_code",
6140			label: "zip_code"
6141		},
6142		{
6143			value: "city",
6144			label: "city"
6145		},
6146		{
6147			value: "state",
6148			label: "state"
6149		}
6150	]
6151};
6152/** `a1`..`a9` and `x1`..`x9`, the aux slots a reporting definition can land in. */
6153var reportingTargetFieldOptions = [...Array.from({ length: 9 }, (_unused, index) => `a${index + 1}`), ...Array.from({ length: 9 }, (_unused, index) => `x${index + 1}`)].map((value) => ({
6154	value,
6155	label: value
6156}));
6157var goalAdjustmentTypeOptions = [
6158	{
6159		value: "0",
6160		label: "score_threshold"
6161	},
6162	{
6163		value: "1",
6164		label: "spend_saving"
6165	},
6166	{
6167		value: "2",
6168		label: "viewability_adj"
6169	},
6170	{
6171		value: "3",
6172		label: "mobile_adj"
6173	},
6174	{
6175		value: "4",
6176		label: "desktop_adj"
6177	}
6178];
6179/**
6180* The campaign advanced-property keys touched in the last 12 months, plus the rest that carry live rows. Adding
6181* a key the server does not read is silently ignored, which is why this is a picker rather than free text.
6182*/
6183var campaignPropertyKeyOptions = [
6184	"rule.override.class.name",
6185	"enforced.experiment",
6186	"excluded.experiments",
6187	"random.score.context.factor",
6188	"random.score.use.user",
6189	"support.standalone.vast.wrapper",
6190	"vpaid.required",
6191	"isSupportSkippable",
6192	"googleClickThroughUrl",
6193	"googleBuyerCreativeId",
6194	"vpaid.wrapper.provider",
6195	"vpaid.provider",
6196	"support.max.rules",
6197	"log.bids",
6198	"videoQualityProfile",
6199	"catalog.id",
6200	"allow.only.tv",
6201	"tv.type",
6202	"optimization.endpoint",
6203	"optimization.systems",
6204	"disable.user.throttling",
6205	"enable.third.party.id",
6206	"blocked.placements",
6207	"first.price.auction.shadow",
6208	"ghost.bid.factor",
6209	"throttle.ghost.bids",
6210	"static.url"
6211].map((value) => ({
6212	value,
6213	label: value
6214}));
6215//#endregion
6216//#region client/components/newNav/components/ads/decoupled/decoupledFieldGroups.ts
6217/**
6218* The decoupled campaign's own sections. A partner owns the buy, so everything about money and audience is
6219* absent by design - no flights, no bid price, no frequency caps, no device behaviour - and what is left is
6220* about which creative we return when they call us.
6221*/
6222var buildDecoupledIdentityGroups = () => [{
6223	id: "identity",
6224	title: "Identity and status",
6225	description: "The name is also the identifier in the DSP's live tag, so renaming a campaign changes how it is called.",
6226	fields: [
6227		{
6228			key: "name",
6229			label: "Name",
6230			type: "text"
6231		},
6232		{
6233			key: "enabled",
6234			label: "Enabled",
6235			type: "bool"
6236		},
6237		{
6238			key: "clickThroughUrl",
6239			label: "Click-through URL",
6240			type: "url",
6241			wide: true,
6242			placeholder: "https://www.example.com/offer"
6243		},
6244		{
6245			key: "restrictedContentAlcohol",
6246			label: "Restricted content: alcohol",
6247			type: "bool",
6248			help: "Enable this for VAST to include declaration of alcohol in creative"
6249		}
6250	]
6251}];
6252var buildDecoupledFallbackGroups = () => [{
6253	id: "fallback",
6254	title: "Fallback video",
6255	description: "What the ad server serves when it cannot build a personalized video for the request. These are server settings keyed by the campaign name, not campaign fields.",
6256	fields: [
6257		{
6258			key: "fallbackVideoQuality",
6259			label: "Video quality",
6260			type: "select",
6261			options: [{
6262				value: "",
6263				label: "Not set"
6264			}, ...videoQualityProfileOptions]
6265		},
6266		{
6267			key: "fallbackAspectRatio",
6268			label: "Aspect ratio",
6269			type: "text",
6270			placeholder: "16:9"
6271		},
6272		{
6273			key: "fallbackDuration",
6274			label: "Duration",
6275			type: "text",
6276			placeholder: "30"
6277		}
6278	]
6279}];
6280var buildDecoupledDeliveryGroups = () => [{
6281	id: "catalog",
6282	title: "Product catalog",
6283	fields: [{
6284		key: "productCatalogName",
6285		label: "Product catalog name",
6286		type: "text"
6287	}, {
6288		key: "productCatalogSkuTemplate",
6289		label: "SKU template",
6290		type: "text",
6291		wide: true
6292	}]
6293}, {
6294	id: "serving",
6295	title: "Serving",
6296	fields: [
6297		{
6298			key: "pixelRulesLocation",
6299			label: "Pixel rules CSV",
6300			type: "url",
6301			wide: true,
6302			help: "The S3 location for the Pixel Rules CSV"
6303		},
6304		{
6305			key: "videoKbps",
6306			label: "Video bitrate",
6307			type: "number",
6308			min: 0,
6309			help: "Video bit rate in KB"
6310		},
6311		{
6312			key: "useSskyVpaid",
6313			label: "Use SundaySky VPAID",
6314			type: "bool",
6315			help: "Whether to use VPAID response with additional player data"
6316		}
6317	]
6318}];
6319var buildDecoupledControlGroupGroups = () => [{
6320	id: "control-group",
6321	title: "Control group",
6322	advanced: true,
6323	description: "Disabled on every decoupled campaign in production. Kept because turning it on is a real, if rare, choice.",
6324	fields: [{
6325		key: "controlGroupMode",
6326		label: "Control group mode",
6327		type: "select",
6328		options: controlGroupModeOptions,
6329		help: "Control group mode for this program"
6330	}, {
6331		key: "controlGroupPercent",
6332		label: "Control group percent",
6333		type: "number",
6334		min: 0,
6335		max: 100,
6336		help: "Percent of users who belong to the control group (irrelevant if 'Disabled')"
6337	}]
6338}];
6339/** The endpoints a DSP calls. Which one a campaign is sold through is not stored, so all three are listed. */
6340var DECOUPLED_SERVING_ENDPOINTS = [
6341	{
6342		dsp: "The Trade Desk",
6343		endpoint: "/dec_ttd"
6344	},
6345	{
6346		dsp: "Adobe",
6347		endpoint: "/dec_adb"
6348	},
6349	{
6350		dsp: "DV360",
6351		endpoint: "/dec_dv360"
6352	}
6353];
6354//#endregion
6355//#region client/components/newNav/components/ads/decoupled/DecoupledDeliveryPanel.tsx
6356/**
6357* How a DSP actually calls this campaign. Derived, never stored - and incomplete: the ad server's host is not
6358* configured anywhere in the hub, and which of the three DSPs a campaign is sold through is not in the schema,
6359* so all three endpoints are listed and the host is left as the placeholder it is.
6360*/
6361var DecoupledDeliveryPanel = ({ campaignName }) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
6362	sx: sectionSx$10,
6363	children: [
6364		/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
6365			variant: "h5",
6366			children: "DSP delivery"
6367		}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
6368			variant: "body2",
6369			color: "text.secondary",
6370			children: "The tag the partner's DSP calls for a creative. It carries this campaign's name, which is why renaming the campaign changes how it is called."
6371		})] }),
6372		/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Paper, {
6373			variant: "outlined",
6374			sx: paperSx$4,
6375			children: DECOUPLED_SERVING_ENDPOINTS.map(({ dsp, endpoint }) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
6376				sx: rowSx,
6377				children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
6378					variant: "subtitle2",
6379					children: dsp
6380				}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
6381					variant: "body1",
6382					sx: urlSx,
6383					children: `https://<ad server host>${endpoint}?program=${encodeURIComponent(campaignName)}`
6384				})]
6385			}, endpoint))
6386		}),
6387		/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.AttentionBox, {
6388			color: "info",
6389			children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.AttentionBoxContent, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
6390				variant: "body1",
6391				color: "text.secondary",
6392				children: "The ad server's host is not recorded in the hub, so these are paths rather than URLs you can copy. Nothing stores which DSP a campaign is sold through either, so all three are shown."
6393			}) })
6394		})
6395	]
6396});
6397var sectionSx$10 = {
6398	display: "flex",
6399	flexDirection: "column",
6400	gap: 2
6401};
6402var paperSx$4 = {
6403	display: "flex",
6404	flexDirection: "column",
6405	gap: 1.5,
6406	padding: 2
6407};
6408var rowSx = {
6409	display: "flex",
6410	flexDirection: "column"
6411};
6412var urlSx = {
6413	wordBreak: "break-all",
6414	fontFamily: "monospace"
6415};
6416//#endregion
6417//#region client/components/newNav/components/ads/dashboards/DecoupledDashboard.tsx
6418var CHART_HEIGHT = 220;
6419/**
6420* A decoupled campaign skips the auction: the partner's DSP has already bought the impression and calls us for
6421* the video. There is nothing to pace and no goal to miss, so the only way it can fail is by stopping - which is
6422* what this page is for. No goal, pace, projection, expected line or score threshold appears here at all.
6423*/
6424var DecoupledDashboard = ({ campaignId }) => {
6425	const theme = (0, import_material.useTheme)();
6426	const { delivery, loading, error } = useAdsSpaceCampaignDelivery(campaignId, null);
6427	const todayOption = (0, import_react.useMemo)(() => delivery ? todayChartOption(delivery.todaySeries, theme, false) : null, [delivery, theme]);
6428	const trendOption = (0, import_react.useMemo)(() => delivery ? trendChartOption(delivery.last30Days, delivery.last30Days, theme) : null, [delivery, theme]);
6429	if (loading) return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
6430		sx: stackSx$4,
6431		children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(DeliverySkeleton, { height: 96 }
6431), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(DeliverySkeleton, { height: CHART_HEIGHT })]
6432	});
6433	if (error || !delivery) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(DeliveryEmptyState, {
6434		title: "No delivery to show",
6435		message: "This campaign's delivery could not be read."
6436	});
6437	const today = delivery.todaySeries.reduce((sum, point) => sum + (point.delivered ?? 0), 0);
6438	const yesterday = delivery.last30Days[delivery.last30Days.length - 2]?.delivered ?? 0;
6439	const completed = delivery.last30Days.length > 0 ? delivery.last30Days.reduce((sum, point) => sum + (point.delivered ?? 0), 0) : 0;
6440	const completionRate = delivery.quality.find((metric) => metric.key === "completionRate")?.actual ?? null;
6441	return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
6442		sx: stackSx$4,
6443		children: [
6444			/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
6445				sx: headerSx$4,
6446				children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
6447					sx: chipsSx,
6448					children: [
6449						/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.Label, {
6450							size: "small",
6451							label: delivery.enabled ? "enabled" : "disabled",
6452							color: delivery.enabled ? "success" : "default",
6453							variant: "standard"
6454						}),
6455						/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.Label, {
6456							size: "small",
6457							label: "decoupled",
6458							color: "info",
6459							variant: "standard"
6460						}),
6461						/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.Label, {
6462							size: "small",
6463							label: "no flights, no goals",
6464							color: "default",
6465							variant: "standard"
6466						}),
6467						/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.Label, {
6468							size: "small",
6469							label: STATUS_LABELS[delivery.status],
6470							color: STATUS_COLORS[delivery.status],
6471							variant: "standard"
6472						})
6473					]
6474				}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(DeliveryFreshness, {
6475					lastUpdateTime: delivery.freshness.lastUpdateTime,
6476					isKnown: delivery.freshness.isKnown
6477				})]
6478			}),
6479			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsDashboardCard, {
6480				dataTestId: "ads-decoupled-delivering",
6481				title: "Is it still delivering?",
6482				infoTooltip: "A decoupled campaign has no goal and nothing to pace, so stopping is the only way it can fail.",
6483				children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
6484					sx: kpiStripSx,
6485					children: [
6486						/* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsKpiTile, {
6487							dataTestId: "ads-decoupled-kpi-last-impression",
6488							label: "Last impression",
6489							value: delivery.lastImpressionAt ? formatRelativeAge(delivery.lastImpressionAt) : "never",
6490							subLine: delivery.lastImpressionAt ? formatUtcTime(delivery.lastImpressionAt) : "no impressions in the last 30 days",
6491							status: {
6492								text: STATUS_LABELS[delivery.status],
6493								color: delivery.status === GqlClientAdsSpaceDeliveryStatus.DELIVERING ? "success" : "error"
6494							}
6495						}),
6496						/* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsKpiTile, {
6497							dataTestId: "ads-decoupled-kpi-this-hour",
6498							label: "This hour",
6499							value: formatCount(delivery.impressionsThisHour)
6500						}),
6501						/* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsKpiTile, {
6502							dataTestId: "ads-decoupled-kpi-today",
6503							label: "Today",
6504							value: formatCount(today),
6505							subLine: `${formatCount(yesterday)} yesterday`
6506						}),
6507						/* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsKpiTile, {
6508							dataTestId: "ads-decoupled-kpi-completion",
6509							label: "Completion rate",
6510							value: completionRate === null ? "-" : formatRate(completionRate),
6511							subLine: `${formatCount(completed)} impressions in 30 days`
6512						})
6513					]
6514				})
6515			}),
6516			/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
6517				sx: twoUpSx,
6518				children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(AnalyticsChart, {
6519					dataTestId: "ads-decoupled-today",
6520					title: "Today by hour",
6521					SubTitle: formatCount(today),
6522					tooltipText: "No expected line - a decoupled campaign has no plan to compare against.",
6523					option: todayOption,
6524					containerStyle: { height: CHART_HEIGHT }
6525				}
6525), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AnalyticsChart, {
6526					dataTestId: "ads-decoupled-trend",
6527					title: "Last 30 days",
6528					SubTitle: formatCount(completed),
6529					option: trendOption,
6530					containerStyle: { height: CHART_HEIGHT }
6531				})]
6532			}),
6533			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsDashboardCard, {
6534				dataTestId: "ads-decoupled-dsp",
6535				title: "Delivery by DSP",
6536				infoTooltip: "A DSP disappearing from this table is a stall one level down.",
6537				children: delivery.exchanges.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(DeliveryEmptyState, { message: "No impressions have been attributed to a DSP in the last 30 days." }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Table, {
6538					size: "small",
6539					children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableHead, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.TableRow, { children: [
6540						/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, { children: "DSP" }),
6541						/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, {
6542							align: "right",
6543							children: "Impressions"
6544						}),
6545						/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, {
6546							align: "right",
6547							children: "Share"
6548						})
6549					] }) }
6549), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableBody, { children: delivery.exchanges.map((exchange) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.TableRow, { children: [
6550						/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, { children: exchange.name }),
6551						/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, {
6552							align: "right",
6553							children: formatCount(exchange.impressions)
6554						}),
6555						/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, {
6556							align: "right",
6557							children: exchange.share === null ? "-" : formatRate(exchange.share)
6558						})
6559					] }, exchange.prefix)) })]
6560				})
6561			}),
6562			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(DecoupledDeliveryPanel, { campaignName: delivery.campaignName }),
6563			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.AttentionBox, {
6564				color: "warning",
6565				children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.AttentionBoxContent, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
6566					variant: "body1",
6567					color: "text.secondary",
6568					children: "This campaign's name is a public identifier: it is the string the partner's DSP carries in its tag URL, so renaming it breaks live delivery."
6569				}) })
6570			})
6571		]
6572	});
6573};
6574var stackSx$4 = {
6575	display: "flex",
6576	flexDirection: "column",
6577	gap: 3,
6578	minWidth: 0,
6579	flex: 1,
6580	minHeight: 0,
6581	overflowY: "auto",
6582	...import_dist$2.scrollOnHoverSxProps
6583};
6584var headerSx$4 = {
6585	display: "flex",
6586	justifyContent: "space-between",
6587	alignItems: "flex-start",
6588	gap: 2,
6589	flexWrap: "wrap"
6590};
6591var chipsSx = {
6592	display: "flex",
6593	gap: 1,
6594	flexWrap: "wrap",
6595	alignItems: "center"
6596};
6597var kpiStripSx = {
6598	display: "grid",
6599	gridTemplateColumns: "repeat(auto-fit, minmax(160px, 1fr))",
6600	gap: 2
6601};
6602var twoUpSx = {
6603	display: "grid",
6604	gridTemplateColumns: {
6605		xs: "1fr",
6606		lg: "1fr 1fr"
6607	},
6608	gap: 3,
6609	minWidth: 0
6610};
6611//#endregion
6612//#region client/components/newNav/components/ads/common/AdsSettingsForm.tsx
6613/**
6614* The shared shape of every ads configuration form: field groups, an optional collapsed advanced group, and the
6615* space's one save contract - edits stay local, the pinned bar counts them, Save asks for the change comment,
6616* and Cancel or leaving the page confirms first. Version and revision fields are deliberately absent from every
6617* group; saving bumps them, which is what makes the ad servers reload the configuration.
6618*/
6619var AdsSettingsForm = (props) => {
6620	const { form } = props;
6621	const [isCancelConfirmOpen, setIsCancelConfirmOpen] = (0, import_react.useState)(false);
6622	const [isCommentOpen, setIsCommentOpen] = (0, import_react.useState)(false);
6623	const guard = useAdsUnsavedChangesGuard(form.dirty && !form.saving && !props.readOnly, props.navigationScope);
6624	const mainGroups = props.groups.filter((group) => !group.advanced);
6625	const advancedGroups = props.groups.filter((group) => group.advanced);
6626	const invalidLabels = props.invalidLabels ?? props.groups.flatMap((group) => group.fields).filter((field) => form.errors[field.key]).map((field) => field.label);
6627	const continueEditing = () => {
6628		setIsCancelConfirmOpen(false);
6629		guard.cancelNavigation();
6630	};
6631	const discard = () => {
6632		form.onDiscard();
6633		setIsCancelConfirmOpen(false);
6634		guard.confirmNavigation();
6635	};
6636	const confirmSave = async (comment) => {
6637		try {
6638			await form.onSave(comment);
6639		} catch {} finally {
6640			setIsCommentOpen(false);
6641		}
6642	};
6643	const renderFieldGrid = (fields, columns) => fields.length === 0 ? null : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Box, {
6644		sx: columns === 1 ? singleColumnSx : fieldsGridSx,
6645		children: fields.map((field) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsFieldControl, {
6646			field,
6647			value: form.values[field.key] ?? null,
6648			values: form.values,
6649			error: form.errors[field.key],
6650			readOnly: props.readOnly,
6651			onChange: form.onChange,
6652			trackingIdPrefix: props.trackingIdPrefix
6653		}, field.key))
6654	});
6655	/** A group with sections shows its fields under them; one without shows them as a single grid. */
6656	const renderFields = (group) => {
6657		if (!group.sections) return renderFieldGrid(group.fields, group.columns);
6658		return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
6659			sx: sectionsSx,
6660			children: [renderFieldGrid(group.fields.filter((field) => !field.section), group.columns), group.sections.map((section) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
6661				sx: sectionSx$9,
6662				children: [
6663					/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
6664						variant: "h6",
6665						children: section.title
6666					}), section.description && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
6667						variant: "body2",
6668						color: "text.secondary",
6669						children: section.description
6670					})] }),
6671					renderFieldGrid(group.fields.filter((field) => field.section === section.title), section.columns ?? group.columns),
6672					section.content
6673				]
6674			}, section.title))]
6675		});
6676	};
6677	return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
6678		sx: formSx,
6679		children: [
6680			/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
6681				sx: bodySx$3,
6682				children: [
6683					props.headerChildren,
6684					mainGroups.map((group) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
6685						sx: groupSx$2,
6686						id: `ads-group-${group.id}`,
6687						children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
6688							sx: groupTitleSx,
6689							children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
6690								variant: "h5",
6691								children: group.title
6692							}), group.titleAdornment]
6693						}
6693), group.description && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
6694							variant: "body2",
6695							color: "text.secondary",
6696							children: group.description
6697						})] }), renderFields(group)]
6698					}, group.id)),
6699					advancedGroups.map((group) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_dist$2.TruffleAccordion, {
6700						sx: accordionSx,
6701						id: `ads-group-${group.id}`,
6702						children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.AccordionSummary, {
6703							expandIcon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FontAwesomeIcon, { icon: import_faAngleDown.faAngleDown }),
6704							"data-tracking-id": `${props.trackingIdPrefix}-${group.id}-expand-btn`,
6705							children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
6706								sx: groupTitleSx,
6707								children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
6708									variant: "h5",
6709									children: group.title
6710								}), group.titleAdornment]
6711							})
6712						}
6712), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.AccordionDetails, {
6713							sx: accordionDetailsSx,
6714							children: [group.description && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
6715								variant: "body2",
6716								color: "text.secondary",
6717								children: group.description
6718							}), renderFields(group)]
6719						})]
6720					}, group.id)),
6721					props.children
6722				]
6723			}),
6724			!props.readOnly && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsSaveBar, {
6725				changeCount: form.changeCount,
6726				saveDisabledReason: describeInvalidFields(invalidLabels) ?? props.saveBlockedReason,
6727				saving: form.saving,
6728				onCancel: () => setIsCancelConfirmOpen(true),
6729				onSave: () => setIsCommentOpen(true),
6730				trackingIdPrefix: props.trackingIdPrefix
6731			}),
6732			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(DiscardChangesDialog, {
6733				open: isCancelConfirmOpen || guard.pendingNavigation,
6734				onContinueEditing: continueEditing,
6735				onDiscard: discard
6736			}),
6737			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsSaveCommentDialog, {
6738				open: isCommentOpen,
6739				summary: describeAdsChangeCount(form.changeCount),
6740				warning: props.saveWarning,
6741				saving: form.saving,
6742				onCancel: () => setIsCommentOpen(false),
6743				onConfirm: (comment) => void confirmSave(comment),
6744				trackingIdPrefix: props.trackingIdPrefix
6745			})
6746		]
6747	});
6748};
6749var describeInvalidFields = (labels) => {
6750	if (labels.length === 0) return;
6751	return labels.length <= 2 ? `Fix ${labels.join(" and ")} before saving` : `Fix ${labels.length} fields before saving`;
6752};
6753var formSx = {
6754	display: "flex",
6755	flexDirection: "column",
6756	minHeight: 0,
6757	flex: 1
6758};
6759var bodySx$3 = {
6760	display: "flex",
6761	flexDirection: "column",
6762	gap: 4,
6763	flex: 1,
6764	minHeight: 0,
6765	overflow: "auto",
6766	...import_dist$2.scrollOnHoverSxProps
6767};
6768var groupTitleSx = {
6769	display: "flex",
6770	alignItems: "center",
6771	gap: 1
6772};
6773var groupSx$2 = {
6774	display: "flex",
6775	flexDirection: "column",
6776	gap: 2
6777};
6778var sectionsSx = {
6779	display: "flex",
6780	flexDirection: "column",
6781	gap: 3
6782};
6783var sectionSx$9 = {
6784	display: "flex",
6785	flexDirection: "column",
6786	gap: 1.5
6787};
6788var fieldsGridSx = {
6789	display: "grid",
6790	gridTemplateColumns: "repeat(auto-fit, minmax(320px, max-content))",
6791	gap: 3,
6792	alignItems: "start",
6793	maxWidth: 720
6794};
6795var singleColumnSx = {
6796	display: "grid",
6797	gridTemplateColumns: "1fr",
6798	gap: 3,
6799	alignItems: "start",
6800	maxWidth: 360
6801};
6802var accordionSx = {
6803	border: 1,
6804	borderColor: "divider",
6805	borderRadius: "8px",
6806	"&:before": { display: "none" }
6807};
6808var accordionDetailsSx = {
6809	display: "flex",
6810	flexDirection: "column",
6811	gap: 2
6812};
6813//#endregion
6814//#region client/components/newNav/components/ads/targeting/targetingModel.ts
6815/**
6816* Audiences and geographies as account-level entities.
6817*
6818* Both used to be per-campaign columns: `program.locationFilter` for the geography, and an `audience` row owned by
6819* one program for the segments. The same "US minus Florida" rule and the same retargeting audience were therefore
6820* retyped campaign after campaign, with nothing linking the copies. Here they are defined once and campaigns and
6821* budget lanes point at them by id.
6822*/
6823/** How a campaign applies one of its geo filters. Blacklist rows subtract; whitelist rows union. */
6824var GeoFilterMode = /* @__PURE__ */ function(GeoFilterMode) {
6825	GeoFilterMode["WHITELIST"] = "whitelist";
6826	GeoFilterMode["BLACKLIST"] = "blacklist";
6827	return GeoFilterMode;
6828}({});
6829var geoFilterModeOptions = [{
6830	value: "whitelist",
6831	label: "Serve only here"
6832}, {
6833	value: "blacklist",
6834	label: "Never serve here"
6835}];
6836/** Same enums the ads configuration app uses for the audience rule tree. */
6837var ruleMatchTypeOptions = [{
6838	value: "MATCH_ALL_RULES",
6839	label: "Match all rules"
6840}, {
6841	value: "MATCH_ANY_RULE",
6842	label: "Match any rule"
6843}];
6844var audienceRuleOperatorOptions = [
6845	{
6846		value: "EQUALS",
6847		label: "Equals"
6848	},
6849	{
6850		value: "DOES_NOT_EQUAL",
6851		label: "Does not equal"
6852	},
6853	{
6854		value: "EXISTS",
6855		label: "Exists"
6856	},
6857	{
6858		value: "DOES_NOT_EXIST",
6859		label: "Does not exist"
6860	},
6861	{
6862		value: "STRING_CONTAINS",
6863		label: "Contains"
6864	},
6865	{
6866		value: "STRING_DOES_NOT_CONTAIN",
6867		label: "Does not contain"
6868	},
6869	{
6870		value: "ONE_OF",
6871		label: "One of"
6872	},
6873	{
6874		value: "NOT_ONE_OF",
6875		label: "Not one of"
6876	}
6877];
6878/** Turns a row of an account collection into a picker option, so every surface offers the same names. */
6879var toEntityOptions = (rows) => rows.map((row) => ({
6880	value: row.id,
6881	label: asText(row.name)
6882}));
6883var findEntityLabel = (options, id) => options.find((option) => option.value === id)?.label ?? "No longer exists";
6884/**
6885* What the ad server shows a viewer who matched an audience. Values are the strings `product_rule.type` holds,
6886* labels are the ones the ads configuration app uses - note DUMMY_SKU is what it calls "No products".
6887*/
6888var ProductSelectorType = /* @__PURE__ */ function(ProductSelectorType) {
6889	ProductSelectorType["LOCATION_TOP_SELLER"] = "LOCATION_TOP_SELLER";
6890	ProductSelectorType["SITE_TOP_SELLER"] = "SITE_TOP_SELLER";
6891	ProductSelectorType["CATEGORY_TOP_SELLER"] = "CATEGORY_TOP_SELLER";
6892	ProductSelectorType["BEST_MATCH"] = "BEST_MATCH";
6893	ProductSelectorType["SPECIFIC_CATEGORY"] = "SPECIFIC_CATEGORY";
6894	ProductSelectorType["SPECIFIC_PRODUCT"] = "SPECIFIC_PRODUCT";
6895	ProductSelectorType["NO_PRODUCTS"] = "DUMMY_SKU";
6896	return ProductSelectorType;
6897}({});
6898var productSelectorTypeOptions = [
6899	{
6900		value: "LOCATION_TOP_SELLER",
6901		label: "Top seller by location"
6902	},
6903	{
6904		value: "SITE_TOP_SELLER",
6905		label: "Site top seller"
6906	},
6907	{
6908		value: "CATEGORY_TOP_SELLER",
6909		label: "Category top seller"
6910	},
6911	{
6912		value: "BEST_MATCH",
6913		label: "Best match"
6914	},
6915	{
6916		value: "SPECIFIC_CATEGORY",
6917		label: "Specific category"
6918	},
6919	{
6920		value: "SPECIFIC_PRODUCT",
6921		label: "Specific product"
6922	},
6923	{
6924		value: "DUMMY_SKU",
6925		label: "No products"
6926	}
6927];
6928/** The three the ads configuration app requires a value for. The rest may carry one and do not need one. */
6929var PRODUCT_SELECTOR_TYPES_NEEDING_VALUE = [
6930	"SPECIFIC_CATEGORY",
6931	"SPECIFIC_PRODUCT",
6932	"DUMMY_SKU"
6933];
6934var productSelectorTypeLabel = (type) => productSelectorTypeOptions.find((option) => option.value === type)?.label ?? type;
6935var targetingRuntime = (input) => {
6936	const hasLegacyGeo = Boolean(input.locationFilter.trim());
6937	return {
6938		geo: input.accountGeoCount > 0 ? hasLegacyGeo ? "CONFLICT" : "ACCOUNT" : hasLegacyGeo ? "LEGACY" : "NONE",
6939		audience: input.accountAudienceCount > 0 ? input.hasInlineAudiences ? "CONFLICT" : "ACCOUNT" : input.hasInlineAudiences ? "LEGACY" : "NONE"
6940	};
6941};
6942/** The older settings decide something. That is what earns them the top of the tab and a red badge. */
6943var isLegacyTargetingInForce = (runtime) => runtime.geo === "LEGACY" || runtime.audience === "LEGACY";
6944/** Both mechanisms are configured, so something on screen is not being applied. */
6945var hasTargetingConflict = (runtime) => runtime.geo === "CONFLICT" || runtime.audience === "CONFLICT";
6946/** Never red. Leftover settings a campaign is already ignoring are housekeeping, not a fault. */
6947var targetingBoxColor = (runtime) => isLegacyTargetingInForce(runtime) ? "warning" : "info";
6948/**
6949* What actually serves. One line while the account-level settings are what serve - that is the good state, and
6950* a paragraph about the older settings still lying around reads as a fault. Two lines, one per mechanism, only
6951* when the older settings are actually deciding something. No "above" or "below": they render first when they
6952* are in force and last when they are not, so a direction word would be wrong in one of the two layouts.
6953*/
6954var targetingRuntimeLines = (runtime, counts) => {
6955	if (!isLegacyTargetingInForce(runtime)) return [`This campaign serves on ${runtime.geo === "NONE" ? "everywhere" : `the account geographies (${counts.accountGeoCount})`} and bids on ${runtime.audience === "NONE" ? "everyone" : `the account audiences (${counts.accountAudienceCount})`}.`];
6956	const geoLines = {
6957		ACCOUNT: `Where it serves: the attached account geographies (${counts.accountGeoCount}).`,
6958		LEGACY: "Where it serves: the older campaign-level location filter - deprecated, and the only geography in force.",
6959		NONE: "Where it serves: everywhere. No geography is set at either level.",
6960		CONFLICT: `Where it serves: the attached account geographies (${counts.accountGeoCount}). The older location filter is configured but ignored entirely - clear it so the screen matches what serves.`
6961	};
6962	const audienceLines = {
6963		ACCOUNT: `Who it bids on: the attached account audiences (${counts.accountAudienceCount}).`,
6964		LEGACY: "Who it bids on: its older campaign-owned audiences - deprecated, and the only audiences in force.",
6965		NONE: "Who it bids on: everyone. No audience is set at either level.",
6966		CONFLICT: `Who it bids on: the attached account audiences (${counts.accountAudienceCount}). The older campaign-owned audiences are configured but not applied - remove them so the screen matches what serves.`
6967	};
6968	return [geoLines[runtime.geo], audienceLines[runtime.audience]];
6969};
6970var scrollToTargetingElement = (elementId) => document.getElementById(elementId)?.scrollIntoView({
6971	behavior: "smooth",
6972	block: "start"
6973});
6974//#endregion
6975//#region client/components/newNav/components/ads/campaigns/campaignFieldGroups.ts
6976/**
6977* The campaign form as data. The old app splits this across a Campaign page and a Program page that the user has
6978* to edit together to configure one thing; here it is one entity, ordered by how often each field is actually
6979* changed rather than by which table it lands in. Version fields are absent everywhere - saving bumps them.
6980*
6981* Every `help` string is quoted from the ads configuration app. Fields it never documented carry no help rather
6982* than an invented one.
6983*/
6984var ratioSumError = "Sum of mobile and desktop ratios exceed 1. We usually have trouble exceeding 100% of the views";
6985var DELIVERY_SECTIONS = {
6986	reach: "Where it can run",
6987	trafficSplit: "Traffic split"
6988};
6989var PLAYER_SIZE_HELP = "The ad slot sizes this campaign will bid on. An opportunity whose player falls outside every size selected here is not bid on at all. Sizes are the slot's pixel dimensions as the exchange reports them.";
6990/** `deviceMode` is a two-way choice in the ads schema, so a CTV campaign cannot also carry desktop or mobile. */
6991var CTV_DEVICE_MODE = "TV";
6992var isCtvCampaign = (values) => asText(values.deviceMode) === CTV_DEVICE_MODE;
6993var isNotCtvCampaign = (values) => !isCtvCampaign(values);
6994var validateBidPair = (value, values) => {
6995	const floor = asText(values.lowBidPrice);
6996	const ceiling = asText(values.highBidPrice);
6997	if (!floor || !ceiling) return null;
6998	return Number(floor) > Number(ceiling) ? "The bid floor cannot be above the bid ceiling" : null;
6999};
7000var validateRatioPair = (value, values) => {
7001	const rangeError = inRange(0, 1, "Ratio must be between 0 and 1")(value);
7002	if (rangeError) return rangeError;
7003	return Number(asText(values.mobileRatio) || 0) + Number(asText(values.desktopRatio) || 0) > 1 ? ratioSumError : null;
7004};
7005var buildOverviewGroups = () => [{
7006	id: "identity",
7007	title: "Identity and status",
7008	description: "Enabling and disabling a campaign is the single most-used control in the whole system.",
7009	fields: [
7010		{
7011			key: "name",
7012			label: "Name",
7013			type: "text",
7014			validate: required("A campaign needs a name")
7015		},
7016		{
7017			key: "enabled",
7018			label: "Enabled",
7019			type: "bool"
7020		},
7021		{
7022			key: "clickThroughUrl",
7023			label: "Click-through URL",
7024			type: "url",
7025			wide: true,
7026			placeholder: "https://www.example.com/offer",
7027			help: "The advertiser's site, sent as the campaign-level click-through. A pixel rule with its own click-through URL overrides it for the rows it matches."
7028		},
7029		{
7030			key: "buyerCreativeId",
7031			label: "Buyer creative id",
7032			type: "text",
7033			help: "Entered by hand. Refreshing it is the lever for a Google throttle, which is why it is not on a schedule."
7034		}
7035	]
7036}, {
7037	id: "provenance",
7038	title: "From Studio",
7039	description: "How this campaign is linked to Studio. The campaign id is this platform's own key and is what appears in the URL and in the delivery data. The Studio program is the Create-side program it reports into. The Salesforce account is the account in the URL, one level above this campaign.",
7040	fields: [{
7041		key: "studioProgramId",
7042		label: "Studio program id",
7043		type: "readOnly"
7044	}, {
7045		key: "campaignId",
7046		label: "Campaign id",
7047		type: "readOnly"
7048	}]
7049}];
7050var buildDeliveryGroups = () => [{
7051	id: "devices",
7052	title: "Devices and platforms",
7053	description: "A campaign is either Desktop and mobile or CTV, never both. The mobile and desktop ratios do n
7053ot apply to a CTV campaign.",
7054	sections: [{ title: DELIVERY_SECTIONS.reach }, {
7055		title: DELIVERY_SECTIONS.trafficSplit,
7056		description: "How the campaign's impressions are meant to divide between the two. They may not sum above 1.",
7057		columns: 1
7058	}],
7059	fields: [
7060		{
7061			key: "deviceMode",
7062			label: "Device mode",
7063			type: "select",
7064			options: deviceModeOptions,
7065			section: DELIVERY_SECTIONS.reach,
7066			help: "Which one should be targeted"
7067		},
7068		{
7069			key: "behaviorDesktop",
7070			label: "Desktop behaviour",
7071			type: "select",
7072			options: deviceBehaviorOptions,
7073			section: DELIVERY_SECTIONS.reach,
7074			disabledWhen: isCtvCampaign,
7075			help: "Behavior of campaign for desktop users"
7076		},
7077		{
7078			key: "behaviorMobileApp",
7079			label: "Mobile app behaviour",
7080			type: "select",
7081			options: deviceBehaviorOptions,
7082			section: DELIVERY_SECTIONS.reach,
7083			disabledWhen: isCtvCampaign,
7084			help: "Behavior of campaign for mobile/tablet users inside apps"
7085		},
7086		{
7087			key: "behaviorMobileWeb",
7088			label: "Mobile web behaviour",
7089			type: "select",
7090			options: deviceBehaviorOptions,
7091			section: DELIVERY_SECTIONS.reach,
7092			disabledWhen: isCtvCampaign,
7093			help: "Behavior of campaign for mobile/tablet users during web browsing"
7094		},
7095		{
7096			key: "behaviorCtv",
7097			label: "CTV behaviour",
7098			type: "select",
7099			options: deviceBehaviorOptions,
7100			section: DELIVERY_SECTIONS.reach,
7101			disabledWhen: isNotCtvCampaign,
7102			help: "Behavior of campaign for ctv users"
7103		},
7104		{
7105			key: "eligibleDesktopVideoPlayerSizes",
7106			label: "Desktop player sizes",
7107			type: "multiSelect",
7108			options: desktopPlayerSizeOptions,
7109			section: DELIVERY_SECTIONS.reach,
7110			wide: true,
7111			disabledWhen: isCtvCampaign,
7112			help: PLAYER_SIZE_HELP
7113		},
7114		{
7115			key: "eligibleMobileVideoPlayerSizes",
7116			label: "Mobile player sizes",
7117			type: "multiSelect",
7118			options: mobilePlayerSizeOptions,
7119			section: DELIVERY_SECTIONS.reach,
7120			wide: true,
7121			disabledWhen: isCtvCampaign,
7122			help: PLAYER_SIZE_HELP
7123		},
7124		{
7125			key: "mobileRatio",
7126			label: "Mobile ratio",
7127			type: "decimal",
7128			min: 0,
7129			max: 1,
7130			section: DELIVERY_SECTIONS.trafficSplit,
7131			disabledWhen: isCtvCampaign,
7132			help: "Ratio for mobile views (0-1)",
7133			validate: validateRatioPair
7134		},
7135		{
7136			key: "desktopRatio",
7137			label: "Desktop ratio",
7138			type: "decimal",
7139			min: 0,
7140			max: 1,
7141			section: DELIVERY_SECTIONS.trafficSplit,
7142			disabledWhen: isCtvCampaign,
7143			help: "Ratio for desktop views (0-1)",
7144			validate: validateRatioPair
7145		}
7146	]
7147}, {
7148	id: "frequency",
7149	title: "Frequency caps",
7150	description: "How often one user may see this campaign. 0 means no cap.",
7151	columns: 1,
7152	fields: [
7153		{
7154			key: "hourlyFrequencyCap",
7155			label: "Hourly",
7156			type: "number",
7157			min: 0
7158		},
7159		{
7160			key: "dailyFrequencyCap",
7161			label: "Daily",
7162			type: "number",
7163			min: 0
7164		},
7165		{
7166			key: "weeklyFrequencyCap",
7167			label: "Weekly",
7168			type: "number",
7169			min: 0
7170		},
7171		{
7172			key: "monthlyFrequencyCap",
7173			label: "Monthly",
7174			type: "number",
7175			min: 0
7176		}
7177	]
7178}];
7179/**
7180* Bid prices, the VPAID toggle and the hours-distribution path. Platform knobs rather than delivery settings,
7181* which is why this sits under Advanced beside exchange caps and goal adjustments.
7182*/
7183var buildPacingGroups = () => [{
7184	id: "pacing",
7185	title: "Pacing and bidding",
7186	columns: 1,
7187	description: "Every value here belongs to this campaign alone - nothing is inherited. They happen to be identical on every live campaign today, so change one only with a reason. Margin is the one setting in the space that genuinely inherits, and it says so on the budget lanes.",
7188	fields: [
7189		{
7190			key: "lowBidPrice",
7191			label: "Bid floor",
7192			type: "decimal",
7193			min: 0,
7194			help: "Lowest bid price the campaign will offer (USD per CPM)",
7195			validate: validateBidPair
7196		},
7197		{
7198			key: "highBidPrice",
7199			label: "Bid ceiling",
7200			type: "decimal",
7201			min: 0,
7202			help: "Highest bid price the campaign will offer (USD per CPM)",
7203			validate: validateBidPair
7204		},
7205		{
7206			key: "useSskyVpaid",
7207			label: "Use SundaySky VPAID",
7208			type: "bool",
7209			help: "Whether to use VPAID response with additional player data"
7210		},
7211		{
7212			key: "volumeDistributionPath",
7213			label: "Hours distribution CSV",
7214			type: "readOnly",
7215			wide: true,
7216			machineWritten: true,
7217			help: "The s3 location of a hours csv file. For example s3://bucket-name/path/file-name.csv"
7218		}
7219	]
7220}];
7221var LEGACY_TARGETING_SECTIONS = {
7222	location: "Location",
7223	audience: "Audience",
7224	dmp: "DMP segments"
7225};
7226/**
7227* `audiences` is the read-only list of the campaign's own audiences, which has no field of its own to sit under.
7228* `titleAdornment` is the deprecated-but-in-use badge, built by the caller because it is a rendered node.
7229*
7230* The older group is a collapsed accordion only while it is not the mechanism in force. On a campaign serving
7231* entirely on its own location filter, collapsing the one thing that decides where it serves is what made the
7232* setting invisible.
7233*/
7234var buildTargetingGroups = (audiences, isLegacyInForce = false, titleAdornment) => [{
7235	id: "legacy",
7236	title: "Older targeting settings",
7237	advanced: !isLegacyInForce,
7238	titleAdornment,
7239	description: isLegacyInForce ? "These older campaign-level settings are what this campaign actually serves on today. They are deprecated - move them to account-level geographies and audiences so a correction reaches every campaign that shares the same list." : "Superseded by the account's audiences and geographies, and kept because campaigns that have not moved over still serve on them.",
7240	sections: [
7241		{
7242			title: LEGACY_TARGETING_SECTIONS.location,
7243			description: "A campaign with any account geography attached ignores this entirely."
7244		},
7245		{
7246			title: LEGACY_TARGETING_SECTIONS.audience,
7247			description: "Audiences this campaign owns rather than pointing at the account's.",
7248			content: audiences
7249		},
7250		{ title: LEGACY_TARGETING_SECTIONS.dmp }
7251	],
7252	fields: [
7253		{
7254			key: "locationFilter",
7255			label: "Location filter",
7256			type: "geoDefinition",
7257			section: LEGACY_TARGETING_SECTIONS.location,
7258			wide: true
7259		},
7260		{
7261			key: "dmpAccountName",
7262			label: "DMP account name",
7263			type: "text",
7264			section: LEGACY_TARGETING_SECTIONS.dmp,
7265			help: "The DMP account whose segments this campaign may bid on"
7266		},
7267		{
7268			key: "allowThirdPartySegmentsInOptimization",
7269			label: "Allow third-party segments in optimization",
7270			type: "bool",
7271			section: LEGACY_TARGETING_SECTIONS.dmp
7272		}
7273	]
7274}];
7275/** What the campaign refuses to serve against. Nothing here names a place or a person - that is the targeting tab. */
7276var buildFilterGroups = () => [{
7277	id: "filters",
7278	title: "Content filters",
7279	description: "Where the ad may not appear, judged on the page or app the opportunity came from. Each accepts a comma-separated list or a link to a CSV.",
7280	fields: [
7281		{
7282			key: "categoryFilter",
7283			label: "IAB categories filter",
7284			type: "text",
7285			wide: true,
7286			help: "Location of CSV file with list of IAB categories to filter out, or comma separated list of categories"
7287		},
7288		{
7289			key: "urlFilter",
7290			label: "URL filter",
7291			type: "text",
7292			wide: true,
7293			help: "The location of a white list or black list of URLs. The format of this field is 'white:' or 'black:' followed by a location. For example 'white:s3://bucket-name/path/file-name.csv', or comma separated list of urls. Prefix a domain with ! to match the root domain only rather than everything beneath it. The list logic is unchanged from the previous platform.",
7294			validate: matches(/^\S*$/, "The URL filter may not contain spaces")
7295		},
7296		{
7297			key: "keywordsFilter",
7298			label: "Keywords filter",
7299			type: "text",
7300			wide: true,
7301			help: "Location of CSV file with list of keywords to filter out, or comma separated list of keywords"
7302		},
7303		{
7304			key: "appNameFilter",
7305			label: "App name filter",
7306			type: "text",
7307			wide: true,
7308			help: "Location of CSV file with list of apps to filter out, or comma separated list of app names"
7309		}
7310	]
7311}];
7312/** Third-party measurement. Nothing here changes who the campaign serves to. */
7313var buildTrackingGroups = () => [{
7314	id: "pixel-file",
7315	title: "Where the rules live",
7316	description: "The file the ad server reads the pixels from. The editor below writes it; this is the path.",
7317	fields: [{
7318		key: "pixelRulesLocation",
7319		label: "Pixel rules CSV",
7320		type: "url",
7321		wide: true,
7322		help: "The S3 location for the Pixel Rules CSV"
7323	}]
7324}];
7325/** The account geographies a campaign serves in. Several rows union; a "never serve" row subtracts from them. */
7326var programGeoFilterFields = (geoOptions) => [{
7327	key: "geoFilterId",
7328	label: "Geography",
7329	type: "select",
7330	options: geoOptions,
7331	wide: true,
7332	validate: required("Choose one of the account's geographies")
7333}, {
7334	key: "mode",
7335	label: "How it applies",
7336	type: "select",
7337	options: geoFilterModeOptions,
7338	defaultValue: GeoFilterMode.WHITELIST,
7339	help: "Several \"serve only here\" rows are added together. A \"never serve here\" row is subtracted from them, whatever the others say."
7340}];
7341/** The account audiences a campaign bids on. Matching any one of them qualifies the request. */
7342var programAudienceFields = (audienceOptions) => [
7343	{
7344		key: "audienceId",
7345		label: "Audience",
7346		type: "select",
7347		options: audienceOptions,
7348		wide: true,
7349		validate: required("Choose one of the account's audiences")
7350	},
7351	{
7352		key: "audienceIdx",
7353		label: "Priority",
7354		type: "number",
7355		min: 0,
7356		help: "Matching any one audience qualifies the request. When a user matches several, the lowest number here is the audience whose name is sent to third-party pixels and used to pick the story.",
7357		validate: inRange(0, 999, "Use a whole number")
7358	},
7359	{
7360		key: "enabled",
7361		label: "Enabled",
7362		type: "bool",
7363		defaultValue: true
7364	}
7365];
7366/**
7367* A product selector per audience. The ad server looks one up for the audience it matched and refuses to serve
7368* without it, which is why an audience attached above and missing here is called out rather than left silent.
7369*/
7370var productSelectorFields = (audienceOptions) => [
7371	{
7372		key: "audienceId",
7373		label: "Audience",
7374		type: "select",
7375		options: audienceOptions,
7376		wide: true,
7377		validate: required("Choose one of the audiences this campaign targets")
7378	},
7379	{
7380		key: "type",
7381		label: "Show",
7382		type: "select",
7383		options: productSelectorTypeOptions,
7384		wide: true,
7385		defaultValue: ProductSelectorType.LOCATION_TOP_SELLER,
7386		validate: required("Choose what to show")
7387	},
7388	{
7389		key: "extraData",
7390		label: "Value",
7391		type: "text",
7392		wide: true,
7393		help: "A category name for Specific category, a product name for Specific product, and the destination URL for No products. The other types do not need one; a value already stored is kept.",
7394		validate: (value, values) => PRODUCT_SELECTOR_TYPES_NEEDING_VALUE.includes(asText(values.type)) && !asText(value) ? `${productSelectorTypeLabel(asText(values.type))} needs a value` : null
7395	}
7396];
7397var unattached = (options, rows, key) => options.filter((option) => !rows.some((row) => asText(row[key]) === option.value));
7398/** Attaching is picking from a list someone else wrote, so it is one multi-select rather than a dialog per row. */
7399var attachGeoFiltersFields = (geoOptions, attached) => [{
7400	key: "geoFilterIds",
7401	label: "Geographies",
7402	type: "multiSelect",
7403	options: unattached(geoOptions, attached, "geoFilterId"),
7404	wide: true,
7405	validate: required("Pick at least one geography")
7406}, {
7407	key: "mode",
7408	label: "How they apply",
7409	type: "select",
7410	options: geoFilterModeOptions,
7411	defaultValue: GeoFilterMode.WHITELIST,
7412	help: "Applies to every geography picked above. Add a second batch to attach some the other way round."
7413}];
7414var attachAudiencesFields = (audienceOptions, attached) => [{
7415	key: "audienceIds",
7416	label: "Audiences",
7417	type: "multiSelect",
7418	options: unattached(audienceOptions, attached, "audienceId"),
7419	wide: true,
7420	validate: required("Pick at least one audience")
7421}];
7422var isControlGroupOff = (values) => asText(values.controlGroupMode) === "DISABLED";
7423var buildControlGroupGroups = (videoResponseOptions) => [{
7424	id: "control-group",
7425	title: "Control group",
7426	description: "Holds a slice of users back so the campaign's effect can be measured against them.",
7427	fields: [
7428		{
7429			key: "controlGroupMode",
7430			label: "Control group mode",
7431			type: "select",
7432			options: controlGroupModeOptions,
7433			help: "Control group mode for this program"
7434		},
7435		{
7436			key: "controlGroupPercent",
7437			label: "Control group percent",
7438			type: "number",
7439			min: 0,
7440			max: 100,
7441			help: "Percent of users who belong to the control group (irrelevant if 'Disabled')",
7442			disabledWhen: isControlGroupOff,
7443			validate: inRange(0, 100, "Percent runs from 0 to 100")
7444		},
7445		{
7446			key: "controlGroupResponseId",
7447			label: "Control group video",
7448			type: "select",
7449			options: [{
7450				value: "",
7451				label: "None"
7452			}, ...videoResponseOptions],
7453			wide: true,
7454			disabledWhen: isControlGroupOff,
7455			help: "Video to use for control group (only when mode is 'Enabled')"
7456		}
7457	]
7458}];
7459var buildOptimizationGroups = (predictionModelOptions, experimentOptions, experimentGroupOptions) => [{
7460	id: "optimization-experiment",
7461	title: "Test and control",
7462	advanced: true,
7463	description: "The group pickers only offer groups of the experiment selected above.",
7464	fields: [
7465		{
7466			key: "optimizationExperimentEnabled",
7467			label: "Test/control experiment enabled",
7468			type: "bool",
7469			help: "Whether to use test/control experiment in optimization"
7470		},
7471		{
7472			key: "optimizationExperimentId",
7473			label: "Experiment",
7474			type: "select",
7475			options: [{
7476				value: "",
7477				label: "None"
7478			}, ...experimentOptions]
7479		},
7480		{
7481			key: "optimizationTestGroupId",
7482			label: "Test group",
7483			type: "select",
7484			options: [{
7485				value: "",
7486				label: "None"
7487			}, ...experimentGroupOptions]
7488		},
7489		{
7490			key: "optimizationControlGroupId",
7491			label: "Control group",
7492			type: "select",
7493			options: [{
7494				value: "",
7495				label: "None"
7496			}, ...experimentGroupOptions]
7497		},
7498		{
7499			key: "optimizationControlGroupMode",
7500			label: "Control value mode",
7501			type: "select",
7502			options: optimizationControlModeOptions,
7503			wide: true,
7504			help: "Fixed value - control group will use the same value always; Random value - a random value from within the model values will be chosen each time; By experiment group - a separate experiment (set below) will be used, and the matching group name will be the optimization value"
7505		},
7506		{
7507			key: "optimizationControlFixedValue",
7508			label: "Control fixed value",
7509			type: "text",
7510			help: "Fixed optimization value for control users (when the appropriate mode is set)"
7511		},
7512		{
7513			key: "optimizationTestExperimentId",
7514			label: "Control experiment",
7515			type: "select",
7516			options: [{
7517				value: "",
7518				label: "None"
7519			}, ...experimentOptions],
7520			help: "Experiment used to select control users optimization value (when the appropriate mode is set)"
7521		}
7522	]
7523}, {
7524	id: "optimization",
7525	title: "Optimization model",
7526	advanced: true,
7527	description: "Hands the creative choice to a prediction model instead of the campaign's own rules.",
7528	fields: [
7529		{
7530			key: "optimizationEnabled",
7531			label: "Optimization enabled",
7532			type: "bool",
7533			help: "Whether to use optimization"
7534		},
7535		{
7536			key: "optimizationModelId",
7537			label: "Optimization model",
7538			type: "select",
7539			options: [{
7540				value: "",
7541				label: "None"
7542			}, ...predictionModelOptions],
7543			wide: true,
7544			help: "Please ensure that: (1) the model contains all the necessary files - feature, event fields, etc. (2) the model's description contains the SageMaker endpoint (3) there is a file called creativeMapping.json (no model id prefix) in the same S3 folder as the other model files, containing the mapping between creative values and the SageMaker action response"
7545		},
7546		{
7547			key: "optimizationOutputField",
7548			label: "Optimization output field",
7549			type: "text",
7550			help: "Field in Video URL that will contain the optimization result (e.g. creative or experiment)",
7551			validate: matches(/^\S*$/, "The output field may not contain spaces")
7552		}
7553	]
7554}];
7555/** Child table field sets. Each is a row editor, not a section of the parent form. */
7556var flightFields = [
7557	{
7558		key: "startDate",
7559		label: "Start date",
7560		type: "date",
7561		validate: required("A flight needs a start date")
7562	},
7563	{
7564		key: "endDate",
7565		label: "End date",
7566		type: "date",
7567		validate: required("A flight needs an end date")
7568	},
7569	{
7570		key: "totalImpressions",
7571		label: "Total impressions",
7572		type: "number",
7573		min: 0
7574	},
7575	{
7576		key: "totalCompletedViews",
7577		label: "Total completed views",
7578		type: "number",
7579		min: 0,
7580		help: "Use instead of impressions for CPCV deals."
7581	},
7582	{
7583		key: "customerCpm",
7584		label: "Customer CPM",
7585		type: "decimal",
7586		min: 0,
7587		help: "CPM received from the customer for this goal period"
7588	},
7589	{
7590		key: "customerCpcv",
7591		label: "Customer CPCV",
7592		type: "decimal",
7593		min: 0,
7594		help: "CPCV received from the customer for this goal period"
7595	},
7596	{
7597		key: "margin",
7598		label: "Margin",
7599		type: "decimal",
7600		min: 0,
7601		help: "Margin between customer CPM and the provider CPM"
7602	},
7603	{
7604		key: "insertionOrder",
7605		label: "Insertion order",
7606		type: "text",
7607		help: "Optional"
7608	}
7609];
7610/** Flights of one campaign may not overlap, and two may not carry identical dates. */
7611var validateFlightRow = (row, rows) => {
7612	const start = asText(row.startDate);
7613	const end = asText(row.endDate);
7614	if (start && end && start > end) return { endDate: "The end date cannot be before the start date" };
7615	if (rows.find((other) =>
7615 asText(other.startDate) === start && asText(other.endDate) === end)) return { startDate: `Period with same dates already exists: ${start} - ${end}` };
7616	const overlapping = rows.find((other) => start <= asText(other.endDate) && asText(other.startDate) <= end);
7617	if (overlapping) return { startDate: `Period with intersecting dates exists: ${asText(overlapping.startDate)} - ${asText(overlapping.endDate)}` };
7618	return {};
7619};
7620/**
7621* Defaults apply to a new row only, so no existing creative is rewritten. Story one is what nearly every campaign
7622* runs unless an error forces another identifier, and one SKU is the standard experience count.
7623*/
7624var creativeFields = [
7625	{
7626		key: "story",
7627		label: "Story name",
7628		type: "text",
7629		defaultValue: "story1",
7630		help: "Story Name. Remove the row to stop using a story - there is no per-row disable.",
7631		validate: required("A creative needs a story name")
7632	},
7633	{
7634		key: "numSkus",
7635		label: "Number of SKUs",
7636		type: "number",
7637		min: 0,
7638		defaultValue: "1",
7639		help: "Number of SKUs in rule"
7640	},
7641	{
7642		key: "videoParameters",
7643		label: "Video parameters",
7644		type: "text",
7645		wide: true,
7646		placeholder: "creative=retail,rule=${rule},cat=${category}",
7647		help: "Query parameters added to the video URL, comma separated. Macros: ${story}, ${rule}, ${category}, ${location.zip_code}, ${location.city}, ${location.state}, ${a1}..${a9}, and ${<experiment name>} for the matching group's name."
7648	},
7649	{
7650		key: "allowedDurations",
7651		label: "Allowed durations",
7652		type: "text",
7653		help: "supported video durations comma delimited. i.e - 15,30"
7654	},
7655	{
7656		key: "videoQualityProfile",
7657		label: "Video quality profile",
7658		type: "select",
7659		options: videoQualityProfileOptions,
7660		defaultValue: "HD_READY",
7661		help: "video quality profile"
7662	},
7663	{
7664		key: "supportHorizontalAspectRatio",
7665		label: "Horizontal (16:9)",
7666		type: "bool",
7667		defaultValue: true
7668	},
7669	{
7670		key: "supportSquareAspectRatio",
7671		label: "Square (1:1)",
7672		type: "bool"
7673	},
7674	{
7675		key: "supportVerticalAspectRatio",
7676		label: "Vertical (9:16)",
7677		type: "bool"
7678	}
7679];
7680/** Two rows for one story is the case that reads as one replacing the other, so it is the one made impossible. */
7681var validateCreativeRow = (row, rows) => rows.some((other) => asText(other.story).trim() === asText(row.story).trim()) ? { story: "This campaign already has a creative for that story" } : {};
7682var buildReportingFields = (source) => [
7683	{
7684		key: "name",
7685		label: "Name",
7686		type: "text",
7687		help: "Name",
7688		validate: required("A reporting definition needs a name")
7689	},
7690	{
7691		key: "source",
7692		label: "Source",
7693		type: "select",
7694		options: reportingSourceOptions,
7695		help: "Which set of fields the definition may draw from. Audience and audiences are under User data; zip code, city and state are under Location; a site event's own field names are typed by hand."
7696	},
7697	reportingFieldOptionsBySource[source] ? {
7698		key: "field",
7699		label: "Field",
7700		type: "select",
7701		options: reportingFieldOptionsBySource[source]
7702	} : {
7703		key: "field",
7704		label: "Field",
7705		type: "text",
7706		help: "Pick a Source first - the fields available depend on it. A source with no list takes the site event field name."
7707	},
7708	{
7709		key: "tagName",
7710		label: "Tag name",
7711		type: "text",
7712		help: "case sensitive! must be populated if field=product_catalog_tag"
7713	},
7714	{
7715		key: "targetField",
7716		label: "Target field",
7717		type: "select",
7718		options: reportingTargetFieldOptions,
7719		help: "Target Field"
7720	}
7721];
7722var exchangeCapFields = (exchangeOptions) => [{
7723	key: "exchangePrefix",
7724	label: "Exchange",
7725	type: "select",
7726	options: exchangeOptions
7727}, {
7728	key: "dailyImpressions",
7729	label: "Daily impressions",
7730	type: "number",
7731	min: 0,
7732	help: "0 blocks the exchange outright. Remove the row instead to leave it uncapped."
7733}];
7734var campaignPropertyFields = [{
7735	key: "propKey",
7736	label: "Property",
7737	type: "select",
7738	options: campaignPropertyKeyOptions
7739}, {
7740	key: "propValue",
7741	label: "Value",
7742	type: "text",
7743	wide: true
7744}];
7745var modelMappingFields = (predictionModelOptions) => [
7746	{
7747		key: "predictionModelId",
7748		label: "Prediction model",
7749		type: "select",
7750		options: predictionModelOptions
7751	},
7752	{
7753		key: "enabled",
7754		label: "Enabled",
7755		type: "bool"
7756	},
7757	{
7758		key: "enabledFrom",
7759		label: "Enabled from",
7760		type: "date"
7761	},
7762	{
7763		key: "enabledTo",
7764		label: "Enabled to",
7765		type: "date"
7766	}
7767];
7768var goalAdjustmentFields = [
7769	{
7770		key: "type",
7771		label: "Type",
7772		type: "select",
7773		options: goalAdjustmentTypeOptions,
7774		help: "The type of the adjustment we configure",
7775		validate: required("An adjustment needs a type")
7776	},
7777	{
7778		key: "base",
7779		label: "Base",
7780		type: "decimal",
7781		help: "The base value of the adjustment"
7782	},
7783	{
7784		key: "changerate",
7785		label: "Change rate",
7786		type: "decimal",
7787		help: "The rate in which the adjustment value will change in each step"
7788	},
7789	{
7790		key: "minValue",
7791		label: "Min value",
7792		type: "decimal",
7793		help: "The min value of the adjustment"
7794	},
7795	{
7796		key: "maxValue",
7797		label: "Max value",
7798		type: "decimal",
7799		help: "The max value of the adjustment"
7800	},
7801	{
7802		key: "overrideValue",
7803		label: "Override value",
7804		type: "decimal",
7805		help: "The fixed value of the adjustment if we want to make it constant"
7806	}
7807];
7808/** The ad server keys adjustments by type, so a second row of the same type is silently dropped rather than merged. */
7809var validateGoalAdjustmentRow = (row, rows) => rows.some((other) => asText(other.type) === asText(row.type)) ? { type: "This campaign already has an adjustment of that type" } : {};
7810//#endregion
7811//#region client/components/newNav/components/ads/pixels/CampaignPixelRulesPanel.tsx
7812/**
7813* The campaign's own way into the pixel editor, beside the field that until now only showed a path. The editor is
7814* a dialog here rather than a route so the campaign's editing session survives - opening it must not discard
7815* unsaved changes on the tab behind it.
7816*
7817* The file is fetched only once the dialog opens: it is an S3 read over the ads VPC, and the Targeting tab renders
7818* far more often than anyone opens the editor.
7819*/
7820var CampaignPixelRulesPanel = (props) => {
7821	const [isOpen, setIsOpen] = (0, import_react.useState)(false);
7822	const [isRechecking, setIsRechecking] = (0, import_react.useState)(false);
7823	const [checkedAt, setCheckedAt] = (0, import_react.useState)(null);
7824	const [notice, setNotice] = (0, import_react.useState)(null);
7825	const { data, loading, error } = useCurrentAccountAdsSpacePixelRulesQuery({
7826		variables: { campaignId: props.campaignId },
7827		skip: !isOpen
7828	});
7829	const [save, { loading: saving }] = useSaveAdsSpacePixelRulesMutation();
7830	const file = data?.currentAccount?.adsSpace?.pixelRules;
7831	const location = file?.location ?? props.location;
7832	const rules = file?.csvText ? parsePixelRulesCsv(file.csvText).rules : [];
7833	const persist = async (csvText, comment) => {
7834		const saved = (await save({ variables: {
7835			campaignId: props.campaignId,
7836			input: {
7837				campaignId: props.campaignId,
7838				updatedAt: props.updatedAt,
7839				changeComment: comment,
7840				csvText
7841			}
7842		} })).data?.saveAdsSpacePixelRules?.adsSpace?.pixelRules?.location ?? null;
7843		if (saved) props.onSaved(saved);
7844		const rechecked = await recheck();
7845		setNotice(rechecked > 0 ? `Pixel rules saved. ${rechecked} placements are now mapped to this campaign.` : "Pixel rules saved, but no placements are mapped to this campaign. Nothing will be attributed to it.");
7846	};
7847	const recheck = async () => {
7848		setIsRechecking(true);
7849		try {
7850			return await props.onRecheck();
7851		} finally {
7852			setIsRechecking(false);
7853			setCheckedAt((/* @__PURE__ */ new Date()).toLocaleTimeString("en-GB", {
7854				hour: "2-digit",
7855				minute: "2-digit"
7856			}));
7857		}
7858	};
7859	return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Paper, {
7860		variant: "outlined",
7861		sx: adsSurfaceSx,
7862		children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Box, {
7863			sx: adsIconTileSx(40),
7864			children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FontAwesomeIcon, { icon: import_faBullseyePointer.faBullseyePointer })
7865		}), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
7866			sx: bodySx$2,
7867			children: [
7868				/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
7869					variant: "h5",
7870					children: "Tracking pixels"
7871				}),
7872				/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
7873					variant: "body2",
7874					color: "text.secondary",
7875					children: location ? `The advertiser's tracking URLs and their conditions, from ${location}.` : "This campaign has no pixel rules file yet."
7876				}),
7877				/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
7878					variant: "body2",
7879					color: props.mappedPlacementCount === 0 ? "error.main" : "text.primary",
7880					"data-testid": "ads-campaign-placement-count",
7881					children: props.mappedPlacementCount === 0 ? "No placements are mapped to this campaign, so nothing will be attributed to it." : `${props.mappedPlacementCount} placements mapped${checkedAt ? `, last checked ${checkedAt}` : ""}.`
7882				}),
7883				notice && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
7884					variant: "helper",
7885					color: "text.secondary",
7886					children: notice
7887				}),
7888				/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
7889					sx: actionsSx,
7890					children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Button, {
7891						variant: "outlined",
7892						size: "medium",
7893						onClick: () => setIsOpen(true),
7894						"data-tracking-id": "tracking-id-ads-campaign-pixel-rules-btn",
7895						children: props.readOnly ? "View pixel rules" : "Edit pixel rules"
7896					}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Button, {
7897						variant: "text",
7898						size: "medium",
7899						disabled: isRechecking,
7900						onClick: () => void recheck().catch(() => setNotice("Could not re-read the placement mapping.")),
7901						"data-testid": "ads-campaign-placement-recheck-btn",
7902						"data-tracking-id": "tracking-id-ads-campaign-placement-recheck-btn",
7903						children: "Re-check"
7904					})]
7905				})
7906			]
7907		})]
7908	}), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Dialog, {
7909		open: isOpen,
7910		onClose: saving ? void 0 : () => setIsOpen(false),
7911		maxWidth: false,
7912		PaperProps: { sx: dialogPaperSx$3 },
7913		children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.TruffleDialogTitle, {
7914			CloseIconButtonProps: {
7915				onClick: () => setIsOpen(false),
7916				disabled: saving,
7917				"data-tracking-id": "tracking-id-ads-campaign-pixel-rules-close-btn"
7918			},
7919			children: `Tracking pixels - ${props.campaignName}`
7920		}), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.DialogContent, { children: [
7921			loading && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Skeleton, {
7922				variant: "rounded",
7923				height: 320
7924			}),
7925			error && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.AttentionBox, {
7926				color: "error",
7927				children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.AttentionBoxContent, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
7928					variant: "body1",
7929					children: `This campaign's pixel rules file could not be read from ${location || "S3"}.`
7930				}) })
7931			}),
7932			!loading && !error && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(PixelRulesEditor, {
7933				campaignName: props.campaignName,
7934				location,
7935				initialRules: rules,
7936				laneNames: props.laneNames,
7937				readOnly: props.readOnly,
7938				onSave: props.readOnly ? void 0 : persist,
7939				saving
7940			})
7941		] })]
7942	})] });
7943};
7944var bodySx$2 = {
7945	display: "flex",
7946	flexDirection: "column",
7947	gap: 1,
7948	minWidth: 0
7949};
7950var actionsSx = {
7951	display: "flex",
7952	gap: 1,
7953	alignSelf: "flex-start"
7954};
7955var dialogPaperSx$3 = {
7956	width: 1100,
7957	maxWidth: "calc(100vw - 64px)",
7958	height: "calc(100vh - 96px)"
7959};
7960//#endregion
7961//#region client/components/newNav/components/ads/common/useAdsSpaceEntities.ts
7962var toRow = (entity) => {
7963	const values = {};
7964	entity.values.forEach((value) => {
7965		values[value.key] = value.value;
7966	});
7967	return {
7968		...values,
7969		id: entity.id
7970	};
7971};
7972var toWireValues$1 = (row) => Object.entries(row).filter(([key]) => key !== "id").map(([key, value]) => ({
7973	key,
7974	value: Array.isArray(value) ? value.join(",") : value === null ? null : asText(value)
7975}));
7976/**
7977* One configuration table, read and written through the space's generic entity path. Every settings and system
7978* surface uses this rather than a query and a mutation of its own - the server registry is what knows which
7979* table and column each key means.
7980*/
7981var useAdsSpaceEntities = (kind, parentId, options) => {
7982	const { data, loading, error } = useCurrentAccountAdsSpaceEntitiesQuery({
7983		variables: {
7984			kind,
7985			parentId
7986		},
7987		skip: options?.skipRead
7988	});
7989	const [saveEntities, { loading: saving }] = useSaveAdsSpaceEntitiesMutation();
7990	const rows = (0, import_react.useMemo)(() => (data?.currentAccount?.adsSpace?.entities ?? []).map(toRow), [data]);
7991	const runSave = async (changed, comment, deletedIds, intoParentId) => {
7992		const target = intoParentId ?? parentId;
7993		try {
7994			const result = await saveEntities({ variables: {
7995				kind,
7996				parentId: target,
7997				input: {
7998					kind,
7999					parentId: target,
8000					changeComment: comment,
8001					rows: changed.map((row) => ({
8002						id: row.id,
8003						values: toWireValues$1(row)
8004					})),
8005					deletedIds
8006				}
8007			} });
8008			setSuccessNotification({ message: "Saved" });
8009			return (result.data?.saveAdsSpaceEntities?.adsSpace?.entities ?? []).map(toRow);
8010		} catch (saveError) {
8011			setErrorNotification({ message: saveError?.message ?? "Saving failed" });
8012			throw saveError;
8013		}
8014	};
8015	const create = async (row, comment, matchKey = "name") => {
8016		const knownIds = new Set(rows.map((existing) => existing.id));
8017		const added = (await runSave([{
8018			...row,
8019			id: ""
8020		}], comment)).filter((candidate) => !knownIds.has(candidate.id));
8021		const wanted = asText(row[matchKey]);
8022		return added.find((candidate) => asText(candidate[matchKey]) === wanted) ?? added[0] ?? null;
8023	};
8024	return {
8025		rows,
8026		loading,
8027		error: Boolean(error),
8028		saving,
8029		save: async (changed, comment, deletedIds, intoParentId) => {
8030			await runSave(changed, comment, deletedIds, intoParentId);
8031		},
8032		create
8033	};
8034};
8035/**
8036* A child collection edited inside a parent's form - video files on a response, sync conditions on DSP account
8037* data. The rows are held locally until the parent saves, which is the space's contract: nothing reaches the
8038* server without an explicit save and a comment.
8039*/
8040var useAdsSpaceChildRows = (kind, parentId) => {
8041	const { rows: savedRows, saving, loading, save } = useAdsSpaceEntities(kind, parentId);
8042	const [rows, setRows] = (0, import_react.useState)([]);
8043	(0, import_react.useEffect)(() => setRows(savedRows), [savedRows]);
8044	const changeCount = (0, import_react.useMemo)(() => {
8045		const savedById = new Map(savedRows.map((row) => [row.id, row]));
8046		return rows.filter((row) => JSON.stringify(savedById.get(row.id) ?? null) !== JSON.stringify(row)).length + savedRows.filter((saved) => !rows.some((row) => row.id === saved.id)).length;
8047	}, [rows, savedRows]);
8048	const persist = async (comment) => {
8049		if (changeCount === 0) return;
8050		const savedById = new Map(savedRows.map((row) => [row.id, row]));
8051		const changed = rows.filter((row) => JSON.stringify(savedById.get(row.id) ?? null) !== JSON.stringify(row));
8052		const deletedIds = savedRows.filter((saved) => !rows.some((row) => row.id === saved.id)).map((row) => row.id);
8053		await save(changed, comment, deletedIds.length > 0 ? deletedIds : void 0);
8054	};
8055	return {
8056		rows,
8057		setRows,
8058		changeCount,
8059		persist,
8060		saving,
8061		loading,
8062		reset: () => setRows(savedRows)
8063	};
8064};
8065//#endregion
8066//#region client/components/newNav/components/ads/campaigns/AdsChangeCommentField.tsx
8067/** `audit_log_record.comments` is varchar(255), so the limit belongs where the comment is typed. */
8068var COMMENT_MAX_LENGTH$1 = 255;
8069/** Every write in the space carries a change comment; the campaign dialogs collect theirs inline. */
8070var AdsChangeCommentField = ({ value, label, onChange, trackingId }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TextField, {
8071	label: label ?? "What changed and why",
8072	fullWidth: true,
8073	multiline: true,
8074	minRows: 2,
8075	size: "small",
8076	value,
8077	onChange: (event) => onChange(event.target.value),
8078	inputProps: {
8079		"data-tracking-id": trackingId,
8080		maxLength: COMMENT_MAX_LENGTH$1
8081	}
8082});
8083//#endregion
8084//#region client/components/newNav/components/ads/campaigns/MoveCampaignDialog.tsx
8085/**
8086* Repoints a campaign at another Studio program. The list is the programs this account's campaigns already use -
8087* a program with no ads campaign yet cannot be picked here.
8088*/
8089var MoveCampaignDialog = (props) => {
8090	const [studioProgramSfId, setStudioProgramSfId] = (0, import_react.useState)("");
8091	const [comment, setComment] = (0, import_react.useState)("");
8092	(0, import_react.useEffect)(() => {
8093		setStudioProgramSfId("");
8094		setComment("");
8095	}, [props.campaign]);
8096	const targets = props.studioPrograms.filter((program) => program.value !== props.campaign?.salesforceId);
8097	const canSave = Boolean(studioProgramSfId) && comment.trim().length > 0 && !props.saving;
8098	const handleMove = async () => {
8099		if (!canSave) return;
8100		if (await props.onMove({
8101			campaignId: props.campaign.id,
8102			studioProgramSfId,
8103			changeComment: comment.trim()
8104		})) props.onClose();
8105	};
8106	return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Dialog, {
8107		open: Boolean(props.campaign),
8108		onClose: props.saving ? void 0 : props.onClose,
8109		maxWidth: false,
8110		PaperProps: { sx: dialogPaperSx$2 },
8111		children: [
8112			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.TruffleDialogTitle, {
8113				CloseIconButtonProps: {
8114					onClick: props.onClose,
8115					disabled: props.saving,
8116					"data-tracking-id": "tracking-id-ads-move-campaign-close-btn"
8117				},
8118				children: "Move to another program"
8119			}),
8120			/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.DialogContent, {
8121				sx: contentSx$9,
8122				children: [
8123					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TextField, {
8124						select: true,
8125						label: "Studio program",
8126						fullWidth: true,
8127						value: studioProgramSfId,
8128						onChange: (event) => setStudioProgramSfId(event.target.value),
8129						inputProps: { "data-tracking-id": "tracking-id-ads-move-campaign-program-input" },
8130						children: targets.map((program) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.MenuItem, {
8131							value: program.value,
8132							children: program.label
8133						}, program.value))
8134					}),
8135					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsChangeCommentField, {
8136						value: comment,
8137						onChange: setComment,
8138						trackingId: "tracking-id-ads-move-campaign-comment-input"
8139					}),
8140					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.AttentionBox, {
8141						color: "warning",
8142						children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.AttentionBoxContent, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
8143							variant: "body1",
8144							color: "text.secondary",
8145							children: "The program decides which creative logic this campaign resolves against, so moving it changes what it serves. Only a disabled campaign can be moved."
8146						}) })
8147					})
8148				]
8149			}),
8150			/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_dist$2.TruffleDialogActions, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Button, {
8151				variant: "text",
8152				size: "large",
8153				disabled: props.saving,
8154				onClick: props.onClose,
8155				"data-tracking-id": "tracking-id-ads-move-campaign-cancel-btn",
8156				children: "Cancel"
8157			}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Button, {
8158				variant: "contained",
8159				size: "large",
8160				loading: props.saving,
8161				disabled: !canSave,
8162				onClick: () => void handleMove(),
8163				"data-tracking-id": "tracking-id-ads-move-campaign-submit-btn",
8164				children: "Move campaign"
8165			})] })
8166		]
8167	});
8168};
8169var dialogPaperSx$2 = {
8170	width: 540,
8171	maxWidth: "calc(100vw - 64px)"
8172};
8173var contentSx$9 = {
8174	display: "flex",
8175	flexDirection: "column",
8176	gap: 2
8177};
8178//#endregion
8179//#region client/components/newNav/components/ads/campaigns/useAdsSpaceCampaignActions.ts
8180/** Create, duplicate and move. Each reports whether it landed, so a dialog only closes on success. */
8181var useAdsSpaceCampaignActions = () => {
8182	const [createCampaign, createState] = useCreateAdsSpaceCampaignMutation();
8183	const [duplicateCampaign, duplicateState] = useDuplicateAdsSpaceCampaignMutation();
8184	const [moveCampaign, moveState] = useMoveAdsSpaceCampaignMutation();
8185	const run = async (what, action, onFailure) => {
8186		try {
8187			const result = await action();
8188			setSuccessNotification({ message: what });
8189			return result;
8190		} catch (error) {
8191			setErrorNotification({ message: error?.message ?? `${what} failed` });
8192			return onFailure;
8193		}
8194	};
8195	/**
8196	* The mutation returns the account's whole campaign list, so the copy's id is in the response the caller is
8197	* already waiting on. The ads services server assigns it, which is why it has to be read back by name.
8198	*/
8199	const duplicate = async (input) => run(`Campaign copied to "${input.name}"`, async () => {
8200		return ((await duplicateCampaign({ variables: { input } })).data?.duplicateAdsSpaceCampaign?.adsSpace?.campaigns ?? []).find((campaign) => campaign.name === input.name)?.id ?? null;
8201	}, null);
8202	return {
8203		create: (input) => run(`Campaign "${input.name}" created`, async () => {
8204			await createCampaign({ variables: { input } });
8205			return true;
8206		}, false),
8207		duplicate,
8208		move: (input) => run("Campaign moved to another program", async () => {
8209			await moveCampaign({ variables: { input } });
8210			return true;
8211		}, false),
8212		saving: createState.loading || duplicateState.loading || moveState.loading
8213	};
8214};
8215//#endregion
8216//#region client/components/newNav/components/ads/campaigns/campaignsModel.ts
8217var CampaignStatus = /* @__PURE__ */ function(CampaignStatus) {
8218	CampaignStatus["ACTIVE"] = "ACTIVE";
8219	CampaignStatus["PAUSED"] = "PAUSED";
8220	CampaignStatus["SCHEDULED"] = "SCHEDULED";
8221	CampaignStatus["ENDED"] = "ENDED";
8222	CampaignStatus["DRAFT"] = "DRAFT";
8223	return CampaignStatus;
8224}({});
8225var CampaignPricingModel = /* @__PURE__ */ function(CampaignPricingModel) {
8226	CampaignPricingModel["CPM"] = "CPM";
8227	CampaignPricingModel["CPCV"] = "CPCV";
8228	return CampaignPricingModel;
8229}({});
8230var campaignStatusDisplay = {
8231	["ACTIVE"]: {
8232		label: "Active",
8233		color: "success",
8234		variant: "standard"
8235	},
8236	["PAUSED"]: {
8237		label: "Paused",
8238		color: "warning",
8239		variant: "standard"
8240	},
8241	["SCHEDULED"]: {
8242		label: "Scheduled",
8243		color: "info",
8244		variant: "standard"
8245	},
8246	["ENDED"]: {
8247		label: "Ended",
8248		color: "default",
8249		variant: "outlined"
8250	},
8251	["DRAFT"]: {
8252		label: "Draft",
8253		color: "default",
8254		variant: "dashed"
8255	}
8256};
8257/**
8258* The status is not stored: it follows from the switch and the booked flights, so it is derived here, once,
8259* and never held as a column. The order matters - a paused campaign reads as paused whatever its dates say.
8260*/
8261var deriveCampaignStatus = (enabled, flights, today) => {
8262	if (!enabled) return "PAUSED";
8263	if (flights.length === 0) return "DRAFT";
8264	if (flights.every((flight) => flight.startDate > today)) return "SCHEDULED";
8265	if (flights.every((flight) =>
8265 flight.endDate < today)) return "ENDED";
8266	return "ACTIVE";
8267};
8268var getBookedBudget = (campaign) => campaign.flights.reduce((total, flight) => total + flight.budget, 0);
8269var getImpressionsGoal = (campaign) => campaign.flights.reduce((total, flight) => total + flight.impressionsGoal, 0);
8270var getFlightRange = (campaign) => {
8271	if (campaign.flights.length === 0) return null;
8272	const starts = campaign.flights.map((flight) => flight.startDate).sort();
8273	const ends = campaign.flights.map((flight) => flight.endDate).sort();
8274	return {
8275		start: starts[0],
8276		end: ends[ends.length - 1]
8277	};
8278};
8279var currencyFormatter = new Intl.NumberFormat("en-US", {
8280	style: "currency",
8281	currency: "USD",
8282	maximumFractionDigits: 0
8283});
8284var compactFormatter = new Intl.NumberFormat("en-US", {
8285	notation: "compact",
8286	maximumFractionDigits: 1
8287});
8288var dayFormatter = new Intl.DateTimeFormat("en-US", {
8289	month: "short",
8290	day: "numeric"
8291});
8292var formatCurrency = (amount) => currencyFormatter.format(amount);
8293var formatCompactNumber = (value) => compactFormatter.format(value);
8294var formatFlightRange = (range) => range ? `${dayFormatter.format(new Date(range.start))} - ${dayFormatter.format(new Date(range.end))}` : "-";
8295var sortValue = (campaign, key) => {
8296	switch (key) {
8297		case "name": return campaign.name.toLowerCase();
8298		case "status": return campaignStatusDisplay[campaign.status].label;
8299		case "flight": return getFlightRange(campaign)?.start ?? "";
8300		case "budget": return getBookedBudget(campaign);
8301		case "impressions": return getImpressionsGoal(campaign);
8302		case "updatedAt": return campaign.updatedAt ?? "";
8303	}
8304};
8305var sortCampaigns = (campaigns, key, direction) => {
8306	const factor = direction === "asc" ? 1 : -1;
8307	return [...campaigns].sort((a, b) => {
8308		const left = sortValue(a, key);
8309		const right = sortValue(b, key);
8310		if (left === right) return 0;
8311		return (left < right ? -1 : 1) * factor;
8312	});
8313};
8314var filterCampaigns = (campaigns, searchTerm, status) => {
8315	const term = searchTerm.trim().toLowerCase();
8316	return campaigns.filter((campaign) => {
8317		if (status && campaign.status !== status) return false;
8318		if (!term) return true;
8319		return campaign.name.toLowerCase().includes(term) || campaign.insertionOrder.toLowerCase().includes(term) || campaign.salesforceId.toLowerCase().includes(term);
8320	});
8321};
8322var summarizeCampaigns = (campaigns) => ({
8323	activeCount: campaigns.filter((campaign) => campaign.status === "ACTIVE").length,
8324	totalCount: campaigns.length
8325});
8326//#endregion
8327//#region client/components/newNav/components/ads/campaigns/useAdsSpaceCampaigns.ts
8328init_networkStatus();
8329var toFlight = (flight) => {
8330	const impressionsGoal = flight.impressions ?? 0;
8331	const customerCpm = flight.CPM ?? 0;
8332	return {
8333		id: flight.id,
8334		startDate: flight.startDate,
8335		endDate: flight.endDate,
8336		budget: Math.round(impressionsGoal / 1e3 * customerCpm),
8337		impressionsGoal,
8338		totalCompletedViews: flight.completedViews ?? 0,
8339		customerCpm,
8340		customerCpcv: flight.CPCV ?? 0,
8341		margin: flight.margin ?? 0,
8342		insertionOrder: flight.insertionOrder ?? ""
8343	};
8344};
8345var toListItem = (campaign, today) => {
8346	const flights = campaign.flights.map(toFlight);
8347	return {
8348		id: campaign.id,
8349		name: campaign.name,
8350		insertionOrder: campaign.insertionOrder ?? "",
8351		salesforceId: campaign.salesforceId ?? "",
8352		status: deriveCampaignStatus(campaign.enabled, flights, today),
8353		pricingModel: campaign.isCPCV ? CampaignPricingModel.CPCV : CampaignPricingModel.CPM,
8354		flights,
8355		creativeCount: campaign.creativeCount,
8356		updatedAt: campaign.updatedAt ?? ""
8357	};
8358};
8359/** The account's campaigns, straight from the ads-space schema. `error` covers "the ads DB is unreachable". */
8360var useAdsSpaceCampaigns = () => {
8361	const { data, loading, error, refetch, networkStatus } = useCurrentAccountAdsSpaceCampaignsQuery({ notifyOnNetworkStatusChange: true });
8362	const rows = data?.currentAccount?.adsSpace?.campaigns;
8363	const decoupledRows = data?.currentAccount?.adsSpace?.decoupledCampaigns;
8364	const programs = data?.currentAccount?.adsSpace?.studioPrograms;
8365	return {
8366		campaigns: (0, import_react.useMemo)(() => {
8367			const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
8368			return (rows ?? []).map((campaign) => toListItem(campaign, today));
8369		}, [rows]),
8370		decoupledCampaigns: (0, import_react.useMemo)(() => {
8371			const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
8372			return (decoupledRows ?? []).map((campaign) => toListItem(campaign, today));
8373		}, [decoupledRows]),
8374		studioPrograms: (0, import_react.useMemo)(() => (programs ?? []).map((program) => ({
8375			value: program.value,
8376			label: program.label
8377		})), [programs]),
8378		loading,
8379		refetching: networkStatus === NetworkStatus.refetch,
8380		error: Boolean(error),
8381		refetch
8382	};
8383};
8384//#endregion
8385//#region client/components/newNav/components/ads/common/AdsEntityRowsSection.tsx
8386var import_faTriangleExclamation = require_faTriangleExclamation();
8387/**
8388* A collection of configuration rows on real data, with the space's save contract around it: edits stay local,
8389* the pinned bar counts them, Save asks for the change comment, Cancel and leaving confirm. Every settings and
8390* system collection uses this - the only thing that differs between them is the field descriptors.
8391*/
8392var AdsEntityRowsSection = (props) => {
8393	const { rows: savedRows, loading, error, saving, save } = useAdsSpaceEntities(props.kind, props.parentId);
8394	const [rows, setRows] = (0, import_react.useState)([]);
8395	const [isCommentOpen, setIsCommentOpen] = (0, import_react.useState)(false);
8396	const [isDiscardOpen, setIsDiscardOpen] = (0, import_react.useState)(false);
8397	(0, import_react.useEffect)(() => setRows(savedRows), [savedRows]);
8398	const changeCount = (0, import_react.useMemo)(() => countRowChanges(rows, savedRows), [rows, savedRows]);
8399	const guard = useAdsUnsavedChangesGuard(changeCount > 0 && !saving && !props.readOnly);
8400	const persist = async (comment) => {
8401		const savedById = new Map(savedRows.map((row) => [row.id, row]));
8402		const changed = rows.filter((row) => JSON.stringify(savedById.get(row.id) ?? null) !== JSON.stringify(row));
8403		const deletedIds = savedRows.filter((saved) => !rows.some((row) => row.id === saved.id)).map((row) => row.id);
8404		try {
8405			await save(changed, comment, deletedIds.length > 0 ? deletedIds : void 0);
8406		} catch {} finally {
8407			setIsCommentOpen(false);
8408		}
8409	};
8410	return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
8411		sx: sectionSx$8,
8412		children: [
8413			error && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_dist$2.AttentionBox, {
8414				color: "error",
8415				children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.AttentionBoxTitle, { children: "The ads configuration is unreachable" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.AttentionBoxContent, { children: "This list could not be read, so it is not the whole picture." })]
8416			}),
8417			props.children,
8418			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Box, {
8419				sx: bodySx$1,
8420				children: loading && rows.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Skeleton, {
8421					variant: "rounded",
8422					height: 160
8423				}) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsChildTable, {
8424					title: props.title,
8425					description: props.description,
8426					fields: props.fields,
8427					columns: props.columns,
8428					rows,
8429					addLabel: props.addLabel,
8430					emptyText: props.emptyText,
8431					rowTitle: props.rowTitle,
8432					readOnly: props.readOnly,
8433					onChange: setRows,
8434					validateRow: props.validateRow,
8435					trackingIdPrefix: props.trackingIdPrefix
8436				})
8437			}),
8438			!props.readOnly && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsSaveBar, {
8439				changeCount,
8440				saving,
8441				onCancel: () => setIsDiscardOpen(true),
8442				onSave: () => setIsCommentOpen(true),
8443				trackingIdPrefix: props.trackingIdPrefix
8444			}),
8445			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsSaveCommentDialog, {
8446				open: isCommentOpen,
8447				summary: changeCount === 1 ? "1 row changed" : `${changeCount} rows changed`,
8448				saving,
8449				onCancel: () => setIsCommentOpen(false),
8450				onConfirm: (comment) => void persist(comment),
8451				trackingIdPrefix: props.trackingIdPrefix
8452			}),
8453			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(DiscardChangesDialog, {
8454				open: isDiscardOpen || guard.pendingNavigation,
8455				onContinueEditing: () => {
8456					setIsDiscardOpen(false);
8457					guard.cancelNavigation();
8458				},
8459				onDiscard: () => {
8460					setRows(savedRows);
8461					setIsDiscardOpen(false);
8462					guard.confirmNavigation();
8463				}
8464			})
8465		]
8466	});
8467};
8468var sectionSx$8 = {
8469	display: "flex",
8470	flexDirection: "column",
8471	gap: 3,
8472	flex: 1,
8473	minHeight: 0
8474};
8475var bodySx$1 = {
8476	flex: 1,
8477	minHeight: 0,
8478	overflow: "auto"
8479};
8480//#endregion
8481//#region client/components/newNav/components/ads/targeting/GeoListsSection.tsx
8482var geoFilterFields = [
8483	{
8484		key: "name",
8485		label: "Name",
8486		type: "text",
8487		validate: required("A geography needs a name")
8488	},
8489	{
8490		key: "definition",
8491		label: "Locations",
8492		type: "geoDefinition",
8493		wide: true,
8494		validate: (value) => {
8495			const definition = asText(value);
8496			if (!definition.trim()) return "A geography with no locations constrains nothing";
8497			return definition.length > 1024 ? `Only ${GEO_DEFINITION_MAX_LENGTH} characters are stored - use a CSV file for a list this long` : null;
8498		}
8499	},
8500	{
8501		key: "description",
8502		label: "Description",
8503		type: "longText",
8504		wide: true,
8505		help: "Why this geography exists. Whoever edits it in six months is not the person who made it."
8506	}
8507];
8508var validateGeoFilterRow = (row, otherRows) => otherRows.some((other) => asText(other.name).trim().toLowerCase() === asText(row.name).trim().toLowerCase()) ? { name: "This account already has a geography with that name" } : {};
8509/**
8510* The account's saved geographies. A campaign points at these rather than carrying a location filter of its own,
8511* so a zip list lives in exactly one place and correcting it corrects every campaign that uses it at once.
8512*/
8513var GeoListsSection = () => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsEntityRowsSection, {
8514	kind: GqlClientAdsSpaceEntityKind.GEO_FILTER,
8515	title: "Geographies",
8516	description: "Where campaigns may serve. Campaign targeting and budget lanes both pick from this list.",
8517	fields: geoFilterFields,
8518	columns: [
8519		{
8520			key: "name",
8521			label: "Geography"
8522		},
8523		{
8524			key: "definition",
8525			label: "Locations",
8526			render: (row) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
8527				variant: "body1",
8528				children: describeGeoDefinition(asText(row.definition))
8529			})
8530		},
8531		{
8532			key: "definitionRaw",
8533			label: "Reads as",
8534			render: (row) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsCopyableText, {
8535				value: asText(row.definition),
8536				monospace: true,
8537				maxWidth: 280,
8538				trackingId: "tracking-id-ads-targeting-geo-lists-definition-copy"
8539			})
8540		},
8541		{
8542			key: "description",
8543			label: "Description"
8544		}
8545	],
8546	addLabel: "New geography",
8547	emptyText: "No geographies defined yet",
8548	rowTitle: (row) => asText(row.name),
8549	validateRow: validateGeoFilterRow,
8550	trackingIdPrefix: "tracking-id-ads-targeting-geo-lists",
8551	children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.AttentionBox, {
8552		color: "info",
8553		children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.AttentionBoxContent, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Typography, {
8554			variant: "body1",
8555			color: "text.secondary",
8556			children: [
8557				"A request has to satisfy every location type in a geography, so \"state = MA\" and \"city = Boston\" together mean Boston, not either one. Two separate trade areas are two geographies, both attached to the campaign.",
8558				" ",
8559				"Layers work the same way: a DMA minus a handful of zips is one geography with DMA set to \"only these\" and Zip code set to \"anywhere but these\". Serve-only geographies attached to a campaign are unioned; never-serve ones are subtracted from the result."
8560			]
8561		}) })
8562	})
8563});
8564//#endregion
8565//#region client/components/newNav/components/ads/targeting/AdsNewGeographyDialog.tsx
8566/**
8567* Creating an account geography from a screen that asks for one. Geographies save through their own mutation,
8568* so making one mid-edit does not commit the campaign - the pending campaign edits stay pending.
8569*/
8570var AdsNewGeographyDialog = ({ open, initialRow, onClose, onCreated }) => {
8571	const { rows, create } = useAdsSpaceEntities(GqlClientAdsSpaceEntityKind.GEO_FILTER);
8572	const handleSubmit = async (row) => {
8573		const created = await create(row, `Created geography ${asText(row.name)}`);
8574		onClose();
8575		if (created) onCreated(created.id);
8576	};
8577	return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsRowDialog, {
8578		open,
8579		title: "New geography",
8580		fields: geoFilterFields,
8581		row: initialRow ?? null,
8582		otherRows: rows,
8583		onClose,
8584		onSubmit: (row) => void handleSubmit(row).catch(() => void 0),
8585		trackingIdPrefix: "tracking-id-ads-new-geography"
8586	});
8587};
8588//#endregion
8589//#region common/adsBudgetLanes.ts
8590/** Shares are doubles, so a 33.3/33.3/33.4 split has to be allowed to be 100. */
8591var SHARE_EPSILON = .001;
8592var totalSharePercent = (lanes) => lanes.reduce((total, lane) => total + lane.sharePercent, 0);
8593/** Trailing float noise makes a message read as nonsense: "adds up to 99.99999999%". */
8594var describeShare = (share) => String(Number(share.toFixed(2)));
8595/**
8596* The one reason this set of lanes cannot be saved, or null. Both callers show it verbatim, so it names the
8597* fix rather than the rule it broke.
8598*/
8599var budgetLanesProblem = (lanes) => {
8600	if (lanes.length === 0) return null;
8601	if (lanes.filter((lane) => !lane.name.trim()).length >
8601 0) return "Every budget lane needs a name - it becomes part of the pacing key the ad server delivers against.";
8602	const repeated = lanes.map((lane) => lane.name.trim()).find((name, index, names) => names.indexOf(name) !== index);
8603	if (repeated) return `Two lanes are named "${repeated}". Each lane needs its own name - the name is what its delivery is filed under.`;
8604	const catchAll = lanes.filter((lane) => !lane.isTargeted);
8605	if (catchAll.length === 0) return "These lanes have no catch-all lane, so a request matching none of them would be dropped. Exactly one lane must have no targeting of its own.";
8606	if (catchAll.length > 1) return `${catchAll.length} lanes have no targeting of their own. Only one lane can be the catch-all - give the others a geography or remove them.`;
8607	if (lanes.some((lane) => lane.laneIdx > catchAll[0].laneIdx)) return "The catch-all lane has to be last. It matches everything, so any lane ordered after it would never be used.";
8608	const total = totalSharePercent(lanes);
8609	if (Math.abs(total - 100) > SHARE_EPSILON) return total > 100 ? `The budget lanes claim ${describeShare(total)}% of the goal. They have to come to exactly 100%.` : `The budget lanes only account for ${describeShare(total)}% of the goal. They have to come to exactly 100%.`;
8610	return null;
8611};
8612//#endregion
8613//#region client/components/newNav/components/ads/campaigns/budgetLanes.ts
8614/**
8615* Budget lanes split one campaign's booked goal between named slices of its traffic.
8616*
8617* "Give Boston 40% of this campaign" is otherwise a second campaign with its own flight, its own budget worked
8618* out by hand, and two sets of settings to keep in step. A lane keeps the booking whole: it claims a share of the
8619* same goal and names the geography that routes a request into it.
8620*
8621* Exactly one lane is the fallback. It names no geography and its share is not chosen - it is whatever the
8622* targeted lanes leave, which is why a split can never fail to add up to 100%.
8623*/
8624var FALLBACK_LANE_NAME = "fallback";
8625/**
8626* A lane with no predicate at all matches whatever the lanes above it did not. Both halves have to be empty:
8627* the ads platform resolves a lane as geography AND audience over whichever is set, so an audience-only lane
8628* is targeted, not the catch-all.
8629*/
8630var isFallbackLane = (lane) => !asText(lane.geoFilterId) && !asText(lane.audienceId);
8631var getTargetedLanes = (lanes) => lanes.filter((lane) => !isFallbackLane(lane));
8632var getFallbackLane = (lanes) => lanes.find(isFallbackLane);
8633var getLaneShare = (lane) => Number(asText(lane.sharePercent)) || 0;
8634/** A lane's own margin override, or null when it inherits. Empty is the common case, so it is not a zero. */
8635var getLaneMargin = (lane) => {
8636	const text = asText(lane.margin).trim();
8637	if (!text) return null;
8638	const parsed = Number(text);
8639	return Number.isFinite(parsed) ? parsed : null;
8640};
8641/**
8642* What a lane without an override of its own runs at. Only answerable when every flight agrees on one
8643* margin - with none set, or several disagreeing, the lane still inherits but this cannot name the number.
8644*/
8645var inheritedMargin = (flights) => {
8646	const distinct = [...new Set(flights.map((flight) => {
8647		const text = asText(flight.margin).trim();
8648		return text && Number.isFinite(Number(text)) ? Number(text) : null;
8649	}))];
8650	return distinct.length === 1 && distinct[0] !== null ? distinct[0] : null;
8651};
8652/** Two decimal places, which is what the column holds and therefore the limit of any exact figure typed here. */
8653var roundToStoredPrecision = (share) => Math.round(share * 100) / 100;
8654/** What the named lanes claim between them. Above 100 the campaign is over-allocated. */
8655var getTargetedSharePercent = (lanes) => getTargetedLanes(lanes).reduce((total, lane) => total + getLaneShare(lane), 0);
8656/**
8657* Rounded to the two decimal places the column holds. A share entered as impressions or spend is rarely a whole
8658* number, and the raw subtraction leaves floating-point dust that renders as "33.900000000000006%".
8659*/
8660var getFallbackSharePercent = (lanes) => roundToStoredPrecision(Math.max(0, 100 - getTargetedSharePercent(lanes)));
8661var getLaneImpressions = (share, impressionsGoal) => Math.round(impressionsGoal * share / 100);
8662/**
8663* What the split is entered in. The stored column is always a percentage - it is what the ad server reads - so
8664* the other two units are converted on the way in and derived on the way out.
8665*/
8666var BudgetLaneUnit = /* @__PURE__ */ function(BudgetLaneUnit) {
8667	BudgetLaneUnit["SHARE"] = "SHARE";
8668	BudgetLaneUnit["IMPRESSIONS"] = "IMPRESSIONS";
8669	BudgetLaneUnit["SPEND"] = "SPEND";
8670	return BudgetLaneUnit;
8671}({});
8672var budgetLaneUnitLabels = {
8673	["SHARE"]: "Share (%)",
8674	["IMPRESSIONS"]: "Impressions",
8675	["SPEND"]: "Spend (USD)"
8676};
8677var asStoredShare = (share) => roundToStoredPrecision(Math.max(0, Math.min(100, share)));
8678/** The campaign's CPM, weighted by each flight's impressions. Null when no flight prices its impressions. */
8679var weightedCampaignCpm = (flights) => {
8680	let impressions = 0;
8681	let spend = 0;
8682	flights.forEach((flight) => {
8683		const goal = Number(asText(flight.totalImpressions)) || 0;
8684		const cpm = Number(asText(flight.customerCpm)) || 0;
8685		impressions += goal;
8686		spend += goal / 1e3 * cpm;
8687	});
8688	return impressions > 0 && spend > 0 ? spend / impressions * 1e3 : null;
8689};
8690var getLaneSpend = (share, impressionsGoal, cpm) => cpm === null ? null : getLaneImpressions(share, impressionsGoal) / 1e3 * cpm;
8691var shareFromImpressions = (impressions, impressionsGoal) => impressionsGoal > 0 ? asStoredShare(impressions / impressionsGoal * 100) : 0;
8692var shareFromSpend = (spend, impressionsGoal, cpm) => cpm && cpm > 0 ? shareFromImpressions(spend / cpm * 1e3, impressionsGoal) : 0;
8693var describeLaneGeography = (lane, geoOptions) => isFallbackLane(lane) ? "Everyone the lanes above did not match" : findEntityLabel(geoOptions, asText(lane.geoFilterId));
8694/**
8695* Lanes go over the wire as they are ordered here, and the ad server takes the first one a request matches, so
8696* the fallback has to be last. Its share is recomputed on every edit rather than typed, and the row exists so the
8697* remainder is a real allocation the ad server can serve against rather than money quietly left unspent.
8698*/
8699var normalizeLanes = (targeted, existingFallback) => {
8700	const ordered = targeted.map((lane, index) => ({
8701		...lane,
8702		laneIdx: String(index),
8703		enabled: true
8704	}));
8705	if (ordered.length === 0) return [];
8706	const fallback = existingFallback ?? {
8707		id: `lane-fallback-${Date.now()}`,
8708		name: FALLBACK_LANE_NAME
8709	};
8710	return [...ordered, {
8711		...fallback,
8712		name: asText(fallback.name) || FALLBACK_LANE_NAME,
8713		geoFilterId: "",
8714		enabled: true,
8715		laneIdx: String(ordered.length),
8716		sharePercent: String(getFallbackSharePercent(ordered))
8717	}];
8718};
8719var toLaneShapes = (lanes) => lanes.map((lane, index) => ({
8720	name: asText(lane.name),
8721	laneIdx: Number(asText(lane.laneIdx)) || index,
8722	sharePercent: getLaneShare(lane),
8723	isTargeted: !isFallbackLane(lane)
8724}));
8725/**
8726* Judged on the rows as they would be written, not on the remainder the card derives - a campaign whose stored
8727* fallback share was left behind by an earlier edit has to be fixed rather than displayed as though it were fine.
8728*/
8729var getLanesError = (lanes) => budgetLanesProblem(toLaneShapes(lanes));
8730//#endregion
8731//#region client/components/newNav/components/ads/campaigns/BudgetLaneBar.tsx
8732/**
8733* The campaign goal laid out end to end, one block per lane. The fallback block is drawn differently because it
8734* is not a share anyone chose - it is whatever the named lanes did not claim.
8735*/
8736var BudgetLaneBar = ({ lanes, impressionsGoal, geoOptions }) => {
8737	const targetedLanes = getTargetedLanes(lanes);
8738	const targeted = getTargetedSharePercent(lanes);
8739	const fallback = getFallbackLane(lanes);
8740	const fallbackShare = fallback ? getLaneShare(fallback) : 0;
8741	const overAllocated = targeted > 100;
8742	const segments = [...targetedLanes.map((lane) => ({
8743		key: lane.id,
8744		name: asText(lane.name),
8745		share: getLaneShare(lane),
8746		matches: describeLaneGeography(lane, geoOptions),
8747		fallback: false
8748	})), {
8749		key: "fallback",
8750		name: fallback ? asText(fallback.name) : "Catch-all",
8751		share: fallbackShare,
8752		matches: "Everyone the lanes above did not match",
8753		fallback: true
8754	}];
8755	return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
8756		sx: wrapperSx$1,
8757		children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Box, {
8758			sx: barSx$1,
8759			children: segments.filter((segment) => segment.share > 0).map((segment) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Tooltip, {
8760				title: `${segment.name}: ${segment.share}% of the goal (${formatCompactNumber(getLaneImpressions(segment.share, impressionsGoal))} impressions) - ${segment.matches}`,
8761				children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Box, {
8762					sx: laneSegmentSx(segment.fallback, Math.min(segment.share, 100)),
8763					children: segment.share >
8763 10 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
8764						variant: "caption",
8765						sx: laneLabelSx(segment.fallback),
8766						children: `${segment.name} ${segment.share}%`
8767					})
8768				})
8769			}, segment.key))
8770		}), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
8771			sx: legendSx$1,
8772			children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
8773				variant: "helper",
8774				color: "text.secondary",
8775				children: `Targeted lanes: ${targeted}%`
8776			}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
8777				variant: "helper",
8778				color: overAllocated ? "error.main" : "text.secondary",
8779				children: overAllocated ? `Over-allocated by ${targeted - 100}% - the catch-all lane has no share left` : `Catch-all lane: ${fallbackShare}%`
8780			})]
8781		})]
8782	});
8783};
8784var wrapperSx$1 = {
8785	display: "flex",
8786	flexDirection: "column",
8787	gap: .5,
8788	width: "100%"
8789};
8790var barSx$1 = {
8791	display: "flex",
8792	height: 32,
8793	borderRadius: "8px",
8794	overflow: "hidden",
8795	backgroundColor: "grey.200"
8796};
8797var laneSegmentSx = (fallback, share) => ({
8798	width: `${share}%`,
8799	backgroundColor: (theme) => fallback ? (0, import_material.alpha)(theme.palette.primary.main, .3) : theme.palette.primary.main,
8800	borderRight: 1,
8801	borderColor: "background.paper",
8802	display: "flex",
8803	alignItems: "center",
8804	justifyContent: "center",
8805	overflow: "hidden",
8806	minWidth: 0
8807});
8808var laneLabelSx = (fallback) => ({
8809	color: fallback ? "primary.dark" : "primary.contrastText",
8810	whiteSpace: "nowrap",
8811	paddingX: .5
8812});
8813var legendSx$1 = {
8814	display: "flex",
8815	justifyContent: "space-between",
8816	gap: 1
8817};
8818//#endregion
8819//#region client/components/newNav/components/ads/campaigns/BudgetLanesSection.tsx
8820var CATCH_ALL_REMOVE_REASON = "You cannot remove the catch-all lane. Without it, traffic matching none of the lanes above would have no lane at all.";
8821var CATCH_ALL_HELP = "The catch-all lane bids and serves like any other lane. It is not a holdout - traffic landing here still sees a video.";
8822var ROUNDING_NOTE = "Stored as a percentage with two decimal places, so an exact impression count may land one or two impressions out.";
8823/** The virtual keys the impressions and spend units are typed under. Neither is stored - both become a share. */
8824var IMPRESSIONS_KEY = "laneImpressions";
8825var SPEND_KEY = "laneSpend";
8826/**
8827* Splits the campaign's booked impressions between named slices of its traffic. The catch-all lane sits in the
8828* table as a row like any other, because it is the same kind of thing - the only differences are that the table
8829* maintains its share rather than a person typing it, and it cannot be removed.
8830*/
8831var BudgetLanesSection = (props) => {
8832	const [isCreatingGeography, setIsCreatingGeography] = (0, import_react.useState)(false);
8833	const [unit, setUnit] = (0, import_react.useState)(BudgetLaneUnit.SHARE);
8834	const fallback = getFallbackLane(props.lanes);
8835	const lanesError = getLanesError(props.lanes);
8836	const isStarved = Boolean(fallback) && getLaneShare(fallback) === 0;
8837	const marginField = {
8838		key: "margin",
8839		label: "Margin (%)",
8840		type: "number",
8841		min: 0,
8842		max: 100,
8843		help: props.inheritedMargin === null ? "Leave empty to use the flight's margin. Set one only when this slice prices differently." : `Leave empty to use the flight's ${props.inheritedMargin}%. Set one only when this slice prices differently.`,
8844		validate: inRange(0, 100, "A margin runs from 0 to 100")
8845	};
8846	const shareField = unit === BudgetLaneUnit.SHARE ? {
8847		key: "sharePercent",
8848		label: "Share of the goal (%)",
8849		type: "number",
8850		min: 0,
8851		max: 100,
8852		help: `Whatever these lanes leave goes to the catch-all lane. ${ROUNDING_NOTE}`,
8853		validate: inRange(0, 100, "A share runs from 0 to 100")
8854	} : unit === BudgetLaneUnit.IMPRESSIONS ? {
8855		key: IMPRESSIONS_KEY,
8856		label: "Impressions",
8857		type: "number",
8858		min: 0,
8859		max: props.impressionsGoal,
8860		help: `Of the campaign's ${props.impressionsGoal.toLocaleString()} booked. ${ROUNDING_NOTE}`,
8861		validate: inRange(0, props.impressionsGoal, `The campaign has ${props.impressionsGoal.toLocaleString()} impressions to split`)
8862	} : {
8863		key: SPEND_KEY,
8864		label: "Spend (USD)",
8865		type: "number",
8866		min: 0,
8867		help: props.customerCpm === null ? "No flight prices this campaign's impressions yet, so a spend cannot be converted to a share." : `At the campaign's $${props.customerCpm.toFixed(2)} CPM. ${ROUNDING_NOTE}`,
8868		disabledWhen: () => props.customerCpm === null
8869	};
8870	const laneFields = [
8871		{
8872			key: "name",
8873			label: "Lane name",
8874			type: "text",
8875			help: "Letters, digits, hyphen and underscore only - it becomes part of the pacing key.",
8876			validate: (value) => required("A lane needs a name")(value) ?? matches(/^[A-Za-z0-9_-]+$/, "Use letters, digits, hyphens and underscores only")(value)
8877		},
8878		{
8879			key: "geoFilterId",
8880			label: "Geography",
8881			type: "select",
8882			options: props.geoOptions,
8883			wide: true,
8884			help: "One of the account's saved geographies. Lanes are matched top to bottom and the first match wins."
8885		},
8886		shareField,
8887		marginField
8888	];
8889	const validateLaneRow = (row, otherRows) => {
8890		if (fallback && row.id === fallback.id) return {};
8891		const otherTargeted = getTargetedLanes(otherRows);
8892		if (!asText(row.geoFilterId)) return { geoFilterId: "Choose the geography this lane serves" };
8893		if (otherTargeted.some((other) => asText(other.geoFilterId) === asText(row.geoFilterId))) return { geoFilterId: "Another lane already covers that geography. A request only ever lands in one lane." };
8894		const total = otherTargeted.reduce((sum, other) => sum + getLaneShare(other), 0) + getLaneShare(row);
8895		return total > 100 ? { sharePercent: `These lanes would claim ${total}% of the goal, leaving the catch-all lane nothing` } : {};
8896	};
8897	/** A unit with nothing to convert against cannot be picked, rather than being pickable and inert. */
8898	const isUnitUnavailable = (option) => option === BudgetLaneUnit.IMPRESSIONS && props.impressionsGoal <= 0 || option === BudgetLaneUnit.SPEND && props.customerCpm === null;
8899	/** Fills the virtual key the current unit is typed under, so the edit dialog opens on the right number. */
8900	const withUnitValues = (lane) => {
8901		const share = getLaneShare(lane);
8902		const spend = getLaneSpend(share, props.impressionsGoal, props.customerCpm);
8903		return {
8904			...lane,
8905			[IMPRESSIONS_KEY]: String(getLaneImpressions(share, props.impressionsGoal)),
8906			[SPEND_KEY]: spend === null ? "" : String(Math.round(spend))
8907		};
8908	};
8909	/**
8910	* The reverse: whatever unit was typed becomes the percentage the ad server reads. A unit with nothing to
8911	* convert against - no booked impressions, or no CPM - leaves the stored share alone, because deriving it
8912	* from a blank box would silently zero every lane the moment any row is edited.
8913	*/
8914	const toStoredShare = (lane) => {
8915		const { [IMPRESSIONS_KEY]: typedImpressions, [SPEND_KEY]: typedSpend, ...stored } = lane;
8916		if (unit === BudgetLaneUnit.SHARE || isFallbackLane(lane)) return stored;
8917		const isImpressions = unit === BudgetLaneUnit.IMPRESSIONS;
8918		const typed = asText(isImpressions ? typedImpressions : typedSpend);
8919		const canConvert = isImpressions ? props.impressionsGoal > 0 : Boolean(props.customerCpm);
8920		if (!typed || !canConvert) return stored;
8921		const share = isImpressions ? shareFromImpressions(Number(typed) || 0, props.impressionsGoal) : shareFromSpend(Number(typed) || 0, props.impressionsGoal, props.customerCpm);
8922		return {
8923			...stored,
8924			sharePercent: String(share)
8925		};
8926	};
8927	const renderLaneName = (row) => {
8928		if (!isFallbackLane(row)) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
8929			variant: "body1",
8930			children: asText(row.name)
8931		});
8932		return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
8933			sx: laneNameSx,
8934			children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
8935				sx: laneTitleSx,
8936				children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
8937					variant: "body1",
8938					children: asText(row.name)
8939				}
8939), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.Label, {
8940					size: "small",
8941					color: "default",
8942					variant: "outlined",
8943					label: "Catch-all"
8944				})]
8945			}), isStarved && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
8946				sx: warningLineSx,
8947				children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.SvgIcon, {
8948					fontSize: "small",
8949					color: "warning",
8950					sx: warningIconSx,
8951					children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FontAwesomeIcon, { icon: import_faTriangleExclamation.faTriangleExclamation })
8952				}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
8953					variant: "helper",
8954					color: "warning.main",
8955					children: "0% share, so this lane never bids and its traffic gets no ad."
8956				})]
8957			})]
8958		});
8959	};
8960	return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
8961		sx: sectionSx$7,
8962		children: [
8963			props.lanes.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(BudgetLaneBar, {
8964				lanes: props.lanes,
8965				impressionsGoal: props.impressionsGoal,
8966				geoOptions: props.geoOptions
8967			}),
8968			/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
8969				sx: unitRowSx,
8970				children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.TruffleToggleButtonGroup, {
8971					exclusive: true,
8972					size: "small",
8973					value: unit,
8974					onChange: (_event, next) => next && setUnit(next),
8975					children: Object.values(BudgetLaneUnit).map((option) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.ToggleButton, {
8976						value: option,
8977						disabled: isUnitUnavailable(option),
8978						"data-tracking-id": `tracking-id-ads-campaign-lanes-unit-${option.toLowerCase()}-btn`,
8979						children: budgetLaneUnitLabels[option]
8980					}, option))
8981				}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
8982					variant: "helper",
8983					color: "text.secondary",
8984					children: ROUNDING_NOTE
8985				})]
8986			}),
8987			lanesError && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.AttentionBox, {
8988				color: "error",
8989				"data-testid": "ads-campaign-lanes-error",
8990				children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.AttentionBoxContent, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
8991					variant: "body1",
8992					color: "text.secondary",
8993					children: lanesError
8994				}) })
8995			}),
8996			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsChildTable, {
8997				title: "Budget lanes",
8998				description: `Split the booked impressions between geographies instead of running one campaign per region. The catch-all lane takes whatever the others leave. ${CATCH_ALL_HELP}`,
8999				fields: laneFields,
9000				columns: [
9001					{
9002						key: "name",
9003						label: "Lane",
9004						render: renderLaneName
9005					},
9006					{
9007						key: "geoFilterId",
9008						label: "Geography",
9009						render: (row) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
9010							variant: "body1",
9011							children: describeLaneGeography(row, props.geoOptions)
9012						})
9013					},
9014					{
9015						key: "sharePercent",
9016						label: "Share",
9017						align: "right",
9018						render: (row) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
9019							variant: "body1",
9020							color: isFallbackLane(row) && isStarved ? "warning.main" : void 0,
9021							children: `${getLaneShare(row)}%`
9022						})
9023					},
9024					{
9025						key: "impressions",
9026						label: "Impressions",
9027						align: "right",
9028						render: (row) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
9029							variant: "body1",
9030							noWrap: true,
9031							color: isFallbackLane(row) && isStarved ? "warning.main" : void 0,
9032							children: formatCompactNumber(getLaneImpressions(getLaneShare(row), props.impressionsGoal))
9033						})
9034					},
9035					{
9036						key: SPEND_KEY,
9037						label: "Spend",
9038						align: "right",
9039						render: (row) => {
9040							const spend = getLaneSpend(getLaneShare(row), props.impressionsGoal, props.customerCpm);
9041							return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
9042								variant: "body1",
9043								noWrap: true,
9044								color: isFallbackLane(row) && isStarved ? "warning.main" : void 0,
9045								children: spend === null ? "-" : `$${Math.round(spend).toLocaleStr
9045ing()}`
9046							});
9047						}
9048					},
9049					{
9050						key: "margin",
9051						label: "Margin",
9052						align: "right",
9053						render: (row) => {
9054							const own = getLaneMargin(row);
9055							if (own !== null) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
9056								variant: "body1",
9057								noWrap: true,
9058								children: `${own}%`
9059							});
9060							return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
9061								variant: "body1",
9062								noWrap: true,
9063								color: "text.secondary",
9064								children: props.inheritedMargin === null ? "inherited" : `${props.inheritedMargin}% inherited`
9065							});
9066						}
9067					}
9068				],
9069				rows: props.lanes.map(withUnitValues),
9070				addLabel: "Add lane",
9071				emptyText: "No lanes - the whole goal is spent across the campaign's targeting",
9072				rowTitle: (row) => asText(row.name),
9073				readOnly: props.readOnly,
9074				validateRow: validateLaneRow,
9075				lockedRow: (row) => isFallbackLane(row) ? { remove: CATCH_ALL_REMOVE_REASON } : null,
9076				rowFields: (row) => isFallbackLane(row) ? [marginField] : laneFields,
9077				onChange: (rows) => {
9078					const stored = rows.map(toStoredShare);
9079					props.onChange(normalizeLanes(getTargetedLanes(stored), getFallbackLane(stored)));
9080				},
9081				headerActions: props.readOnly ? void 0 : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Button, {
9082					variant: "text",
9083					size: "medium",
9084					onClick: () => setIsCreatingGeography(true),
9085					"data-testid": "ads-lanes-new-geography-btn",
9086					"data-tracking-id": "tracking-id-ads-lanes-new-geography-btn",
9087					children: "New geography"
9088				}),
9089				trackingIdPrefix: "tracking-id-ads-campaign-lanes"
9090			}),
9091			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsNewGeographyDialog, {
9092				open: isCreatingGeography,
9093				onClose: () => setIsCreatingGeography(false),
9094				onCreated: () => setIsCreatingGeography(false)
9095			})
9096		]
9097	});
9098};
9099var sectionSx$7 = {
9100	display: "flex",
9101	flexDirection: "column",
9102	gap: 2
9103};
9104var unitRowSx = {
9105	display: "flex",
9106	alignItems: "center",
9107	gap: 2,
9108	flexWrap: "wrap"
9109};
9110var laneNameSx = {
9111	display: "flex",
9112	flexDirection: "column",
9113	gap: .25,
9114	minWidth: 150
9115};
9116var laneTitleSx = {
9117	display: "flex",
9118	alignItems: "center",
9119	gap: 1
9120};
9121var warningLineSx = {
9122	display: "flex",
9123	alignItems: "flex-start",
9124	gap: .5
9125};
9126var warningIconSx = {
9127	marginTop: "1px",
9128	flexShrink: 0
9129};
9130//#endregion
9131//#region client/components/newNav/components/ads/targeting/audienceModel.ts
9132var AudienceEvidence = /* @__PURE__ */ function(AudienceEvidence) {
9133	AudienceEvidence["DMP"] = "DMP_SEGMENT";
9134	AudienceEvidence["SITE"] = "SITE_VISITOR";
9135	AudienceEvidence["IMPRESSION"] = "IMPRESSION";
9136	return AudienceEvidence;
9137}({});
9138/** A rule's value is split on commas by the ad server whatever the operator, so every value is a list. */
9139var ruleValues = (value) => (value ?? "").split(",").map((entry) => entry.trim()).filter(Boolean);
9140var DMP_SEGMENT_FIELD = "segment";
9141var IMPRESSION_PROGRAM_FIELD = "program";
9142/** Site-event field names carried by live audiences, most used first. `url` is the page address. */
9143var siteVisitorFieldOptions = [
9144	{
9145		value: "url",
9146		label: "Page URL"
9147	},
9148	{
9149		value: "Page Type",
9150		label: "Page Type"
9151	},
9152	{
9153		value: "Existing Subscription",
9154		label: "Existing Subscription"
9155	},
9156	{
9157		value: "User Segment",
9158		label: "User Segment"
9159	},
9160	{
9161		value: "Listing Type",
9162		label: "Listing Type"
9163	},
9164	{
9165		value: "Model",
9166		label: "Model"
9167	},
9168	{
9169		value: "Category Viewed",
9170		label: "Category Viewed"
9171	},
9172	{
9173		value: "Product Viewed",
9174		label: "Product Viewed"
9175	},
9176	{
9177		value: "Business User",
9178		label: "Business User"
9179	},
9180	{
9181		value: "Product Purchased",
9182		label: "Product Purchased"
9183	},
9184	{
9185		value: "Shop Name",
9186		label: "Shop Name"
9187	},
9188	{
9189		value: "Product Carted",
9190		label: "Product Carted"
9191	},
9192	{
9193		value: "Cart Viewed",
9194		label: "Cart Viewed"
9195	},
9196	{
9197		value: "Zip Code",
9198		label: "Zip Code"
9199	},
9200	{
9201		value: "Category Carted",
9202		label: "Category Carted"
9203	}
9204];
9205/** `ConditionOperator`, phrased for a value that is always a list. */
9206var siteVisitorOperatorOptions = [
9207	{
9208		value: "STRING_CONTAINS",
9209		label: "contains any of"
9210	},
9211	{
9212		value: "STRING_DOES_NOT_CONTAIN",
9213		label: "contains none of"
9214	},
9215	{
9216		value: "EQUALS",
9217		label: "is any of"
9218	},
9219	{
9220		value: "DOES_NOT_EQUAL",
9221		label: "is none of"
9222	},
9223	{
9224		value: "EXISTS",
9225		label: "has any value"
9226	},
9227	{
9228		value: "DOES_NOT_EXIST",
9229		label: "is missing"
9230	}
9231];
9232/** The two operators that ask only whether the field is present, so their value box is meaningless. */
9233var OPERATORS_WITHOUT_VALUE = ["EXISTS", "DOES_NOT_EXIST"];
9234var evidenceTitle = {
9235	["DMP_SEGMENT"]: "DMP segments",
9236	["SITE_VISITOR"]: "Site activity",
9237	["IMPRESSION"]: "Not already reached"
9238};
9239var evidenceDescription = {
9240	["DMP_SEGMENT"]: "Segment ids from the data provider. This is what most audiences are.",
9241	["SITE_VISITOR"]: "What the user did on the advertiser's own site, as tracked by the SundaySky tag.",
9242	["IMPRESSION"]: "Leaves out anyone another campaign has already served, so two campaigns do not chase the same person."
9243};
9244var toDmpSegments = (group) => {
9245	const segments = {
9246		include: [],
9247		exclude: []
9248	};
9249	for (const rule of group.rules) {
9250		if (rule.field !== "segment") return null;
9251		if (rule.operator === "EQUALS" || rule.operator === "ONE_OF") segments.include.push(...ruleValues(rule.value));
9252		else if (rule.operator === "DOES_NOT_EQUAL" || rule.operator === "NOT_ONE_OF") segments.exclude.push(...ruleValues(rule.value));
9253		else return null;
9254	}
9255	return segments;
9256};
9257var toExcludedPrograms = (group) => {
9258	const excluded = [];
9259	for (const rule of group.rules) {
9260		if (rule.field !== "program" || rule.operator !== "DOES_NOT_EQUAL") return null;
9261		excluded.push(...ruleValues(rule.value));
9262	}
9263	return excluded;
9264};
9265var isKnownEvidence = (ruleType) => Object.values(AudienceEvidence).some((evidence) => evidence === ruleType);
9266/**
9267* Whether this editor can round-trip the audience. Anything it cannot is left to the four-table view rather
9268* than being rewritten into the nearest shape it does understand - which would quietly change who matches.
9269*/
9270var canEditSimply = (audience) => {
9271	if (!audience.isSimple) return false;
9272	const types = audience.blocks.map((block) => block.ruleType);
9273	if (types.some((type) => !isKnownEvidence(type)) || new Set(types).size !== types.length) return false;
9274	return audience.blocks.every((block) => {
9275		if (block.ruleType === "DMP_SEGMENT") return block.groups.every((group) => toDmpSegments(group) !== null);
9276		if (block.ruleType === "IMPRESSION") return block.groups.length <= 1 && block.groups.every((group) => toExcludedPrograms(group) !== null);
9277		return block.groups.every((group) => group.rules.every((rule) => siteVisitorOperatorOptions.some((option) => option.value === rule.operator)));
9278	});
9279};
9280var hasDays = (days) => days !== null && days !== void 0;
9281var describeWindow = (group) => {
9282	if (hasDays(group.maxAgeDays) && hasDays(group.minAgeDays)) return ` seen ${group.minAgeDays}-${group.maxAgeDays} days ago`;
9283	if (hasDays(group.maxAgeDays)) return ` in the last ${group.maxAgeDays} days`;
9284	if (hasDays(group.minAgeDays)) return ` seen at least ${group.minAgeDays} days ago`;
9285	return "";
9286};
9287var pluralize = (count, noun) => `${count} ${noun}${count === 1 ? "" : "s"}`;
9288var describeGroup = (ruleType, group) => {
9289	if (ruleType === "DMP_SEGMENT") {
9290		const segments = toDmpSegments(group);
9291		if (segments) {
9292			const excluded = segments.exclude.length > 0 ? `, ${segments.exclude.length} excluded` : "";
9293			return `${pluralize(segments.include.length, "DMP segment")}${excluded}`;
9294		}
9295		return "DMP segments";
9296	}
9297	if (ruleType === "IMPRESSION") {
9298		const programs = toExcludedPrograms(group);
9299		return programs ? `not reached by ${programs.join(", ")}` : "impression history";
9300	}
9301	return `${pluralize(group.rules.length, "site condition")}${describeWindow(group)}`;
9302};
9303/** Groups are ORed, so the count leads - the ids themselves are unreadable at list width anyway. */
9304var describeBlock = (block) => {
9305	if (block.groups.length === 0) return "nothing";
9306	if (block.groups.length === 1) return describeGroup(block.ruleType, block.groups[0]);
9307	return `any of ${block.groups.length} groups (${block.groups.map((group) => describeGroup(block.ruleType, group)).join(" / ")})`;
9308};
9309/** One line for the list, so a name is not the only thing distinguishing two audiences. */
9310var describeAudience = (audience) => audience.blocks.length === 0 ? "Matches nobody" : audience.blocks.map(describeBlock).join(" and ");
9311//#endregion
9312//#region client/components/newNav/components/ads/targeting/useCopyAudienceToAccount.ts
9313/**
9314* Copies a campaign's own audience up to the account. The mutation returns the account with its audiences, so
9315* the targeting page picks the new one up without a refetch.
9316*/
9317var useCopyAudienceToAccount = () => {
9318	const [copyAudience, { loading }] = useCopyAdsSpaceAudienceToAccountMutation();
9319	const copy = async (input) => {
9320		try {
9321			await copyAudience({ variables: { input } });
9322			setSuccessNotification({ message: `${input.name} added to the account's audiences` });
9323		} catch (copyError) {
9324			setErrorNotification({ message: copyError?.message ?? "Copying failed" });
9325			throw copyError;
9326		}
9327	};
9328	return {
9329		copying: loading,
9330		copy
9331	};
9332};
9333//#endregion
9334//#region client/components/newNav/components/ads/campaigns/CopyAudienceToAccountDialog.tsx
9335/** `audit_log_record.comments` is varchar(255), so the limit is enforced where the comment is written. */
9336var COMMENT_MAX_LENGTH = 255;
9337/**
9338* Names the account-level copy of a campaign's own audience. The name is asked for rather than inherited because
9339* two account audiences may not share one, and several campaigns carry an audience of the same name.
9340*/
9341var CopyAudienceToAccountDialog = (props) => {
9342	const [name, setName] = (0, import_react.useState)(props.audience.name);
9343	const [comment, setComment] = (0, import_react.useState)("");
9344	const { copying, copy } = useCopyAudienceToAccount();
9345	const confirm = async () => {
9346		try {
9347			await copy({
9348				audienceId: props.audience.id,
9349				name: name.trim(),
9350				changeComment: comment.trim()
9351			});
9352			props.onClose();
9353		} catch {}
9354	};
9355	return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Dialog, {
9356		open: true,
9357		onClose: copying ? void 0 : props.onClose,
9358		PaperProps: { sx: paperSx$3 },
9359		children: [
9360			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.TruffleDialogTitle, {
9361				CloseIconButtonProps: {
9362					onClick: props.onClose,
9363					disabled: copying,
9364					"data-tracking-id": "tracking-id-ads-campaign-copy-audience-close-btn"
9365				},
9366				children: "Copy to account audiences"
9367			}),
9368			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.DialogContent, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
9369				sx: contentSx$8,
9370				children: [
9371					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
9372						variant: "body2",
9373						color: "text.secondary",
9374						children: `Copies ${props.audience.name} and its rules to the account, where any campaign can point at it. This campaign keeps the original and goes on bidding on it, so nothing it serves changes today.`
9375					}),
9376					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
9377						variant: "body2",
9378						color: "text.secondary",
9379						children: describeAudience(props.audience)
9380					}),
9381					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TextField, {
9382						autoFocus: true,
9383						fullWidth: true,
9384						size: "small",
9385						label: "Name on the account",
9386						value: name,
9387						onChange: (event) => setName(event.target.value),
9388						inputProps: { "data-tracking-id": "tracking-id-ads-campaign-copy-audience-name-input" }
9389					}),
9390					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TextField, {
9391						fullWidth: true,
9392						multiline: true,
9393						minRows: 3,
9394						size: "small",
9395						label: "What changed and why",
9396						value: comment,
9397						onChange: (event) => setComment(event.target.value),
9398						inputProps: {
9399							"data-tracking-id": "tracking-id-ads-campaign-copy-audience-comment-input",
9400							maxLength: COMMENT_MAX_LENGTH
9401						}
9402					})
9403				]
9404			}) }),
9405			/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_dist$2.TruffleDialogActions, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Button, {
9406				variant: "text",
9407				size: "large",
9408				disabled: copying,
9409				onClick: props.onClose,
9410				"data-tracking-id": "tracking-id-ads-campaign-copy-audience-cancel-btn",
9411				children: "Cancel"
9412			}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Button, {
9413				variant: "contained",
9414				size: "large",
9415				loading: copying,
9416				disabled: !name.trim() || !comment.trim(),
9417				onClick: () => void confirm(),
9418				"data-tracking-id": "tracking-id-ads-campaign-copy-audience-confirm-btn",
9419				children: "Copy audience"
9420			})] })
9421		]
9422	});
9423};
9424var paperSx$3 = {
9425	width: 540,
9426	maxWidth: "calc(100vw - 64px)"
9427};
9428var contentSx$8 = {
9429	display: "flex",
9430	flexDirection: "column",
9431	gap: 2,
9432	paddingTop: 1
9433};
9434//#endregion
9435//#region client/components/newNav/components/ads/campaigns/InlineAudiencesPanel.tsx
9436var compareRows = (left, right, key, direction) => {
9437	const sign = direction === "asc" ? 1 : -1;
9438	if (key === "enabled") return left.enabled === right.enabled ? left.name.localeCompare(right.name) : sign * (Number(left.enabled) - Number(right.enabled));
9439	return sign * left[key].localeCompare(right[key]);
9440};
9441var SortableHeadCell = ({ label, sortKey, active, direction, onSort }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, {
9442	sx: headCellSx,
9443	sortDirection: active === sortKey ? direction : false,
9444	children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableSortLabel, {
9445		active: active === sortKey,
9446		direction: active === sortKey ? direction : "asc",
9447		onClick: () => onSort(sortKey),
9448		"data-testid": `ads-inline-audiences-sort-${sortKey}`,
9449		"data-tracking-id": "tracking-id-ads-campaign-inline-audiences-sort-btn",
9450		children: label
9451	})
9452});
9453/**
9454* Audiences the campaign owns itself instead of pointing at the account's. Most campaigns on this shape carry
9455* far more of them than they bid on - the ones left over from earlier buys are still on the row, just switched
9456* off - so which are in use is the first thing the table has to answer.
9457*
9458* Read-only: nothing in this space writes an audience belonging to one campaign. Copying one up to the account
9459* is the way out, and it leaves the original alone.
9460*/
9461var InlineAudiencesPanel = (props) => {
9462	const [sortKey, setSortKey] = (0, import_react.useState)("enabled");
9463	const [sortDirection, setSortDirection] = (0, import_react.useState)("desc");
9464	const [menuAnchorEl, setMenuAnchorEl] = (0, import_react.useState)(null);
9465	const [menuAudience, setMenuAudience] = (0, import_react.useState)(null);
9466	const [copying, setCopying] = (0, import_react.useState)(null);
9467	const rows = (0, import_react.useMemo)(() => props.audiences.map((audience) => ({
9468		audience,
9469		name: audience.name || `Audience ${audience.id}`,
9470		matches: describeAudience(audience),
9471		enabled: audience.enabled,
9472		selector: audience.productSelectorType ? productSelectorTypeLabel(audience.productSelectorType) : ""
9473	})), [props.audiences]);
9474	const sorted = (0, import_react.useMemo)(() => [...rows].sort((left, right) => compareRows(left, right, sortKey, sortDirection)), [
9475		rows,
9476		sortKey,
9477		sortDirection
9478	]);
9479	if (props.audiences.length === 0) return null;
9480	const onSort = (key) => {
9481		if (key === sortKey) {
9482			setSortDirection((direction) => direction === "asc" ? "desc" : "asc");
9483			return;
9484		}
9485		setSortKey(key);
9486		setSortDirection(key === "enabled" ? "desc" : "asc");
9487	};
9488	const closeMenu = () => {
9489		setMenuAnchorEl(null);
9490		setMenuAudience(null);
9491	};
9492	const rowActions = (audience) => [{
9493		label: "Copy to account audiences",
9494		icon: import_faCopy.faCopy,
9495		onClick: () => {
9496			closeMenu();
9497			setCopying(audience);
9498		}
9499	}];
9500	const inUse = rows.filter((row) => row.enabled).length;
9501	return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
9502		sx: stackSx$3,
9503		children: [
9504			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.AttentionBox, {
9505				color: "info",
9506				children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.AttentionBoxContent, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
9507					variant: "body1",
9508					color: "text.secondary",
9509					children: `${inUse} of ${rows.length} are in use. Read-only here - nothing in the ads space edits an audience that belongs to one campaign. Copy one to the account to point other campaigns at it; the copy leaves this one running.`
9510				}) })
9511			}),
9512			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableContainer, {
9513				component: import_material.Paper,
9514				variant: "outlined",
9515				children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Table, {
9516					size: "small",
9517					children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableHead, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.TableRow, { children: [
9518						/* @__PURE__ */ (0, import_jsx_runtime.jsx)(SortableHeadCell, {
9519							label: "Audience",
9520							sortKey: "name",
9521							active: sortKey,
9522							direction: sortDirection,
9523							onSort
9524						}),
9525						/* @__PURE__ */ (0, import_jsx_runtime.jsx)(SortableHeadCell, {
9526							label: "What it matches",
9527							sortKey: "matches",
9528							active: sortKey,
9529							direction: sortDirection,
9530							onSort
9531						}),
9532						/* @__PURE__ */ (0, import_jsx_runtime.jsx)(SortableHeadCell, {
9533							label: "Shows",
9534							sortKey: "selector",
9535							active: sortKey,
9536							direction: sortDirection,
9537							onSort
9538						}),
9539						/* @__PURE__ */ (0, import_jsx_runtime.jsx)(SortableHeadCell, {
9540							label: "In use",
9541							sortKey: "enabled",
9542							active: sortKey,
9543							direction: sortDirection,
9544							onSort
9545						}),
9546						/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, { sx: headCellSx })
9547					] }) }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableBody, { children: sorted.map((row) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.TableRow, {
9548						sx: rowHoverSx,
9549						children: [
9550							/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, {
9551								sx: bodyCellSx,
9552								children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
9553									variant: "body1",
9554									children: row.name
9555								})
9556							}),
9557							/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, {
9558								sx: bodyCellSx,
9559								children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
9560									variant: "body2",
9561									color: "text.secondary",
9562									children: row.matches
9563								})
9564							}),
9565							/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, {
9566								sx: bodyCellSx,
9567								children: row.selector ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
9568									variant: "body2",
9569									color: "text.secondary",
9570									children: row.selector
9571								}) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.Label, {
9572									size: "small",
9573									variant: "dashed",
9574									color: row.enabled ? "warning" : "default",
9575									label: "No product selector"
9576								})
9577							}),
9578							/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, {
9579								sx: bodyCellSx,
9580								children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.Label, {
9581									size: "small",
9582									variant: row.enabled ? "standard" : "dashed",
9583									color: row.enabled ? "success" : "default",
9584									label: row.enabled ? "In use" : "Not in use"
9585								})
9586							}),
9587							/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, {
9588								sx: actionsCellSx,
9589								children: !props.readOnly && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.TruffleIconButton, {
9590									size: "small",
9591									onClick: (event) => {
9592										setMenuAnchorEl(event.currentTarget);
9593										setMenuAudience(row.audience);
9594									},
9595									"data-tracking-id": "tracking-id-ads-campaign-inline-audiences-row-menu-btn",
9596									children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FontAwesomeIcon, { icon: import_faEllipsisV.faEllipsisV })
9597								})
9598							})
9599						]
9600					}, row.audience.id)) })]
9601				})
9602			}),
9603			menuAudience && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(RowOptionsMenu, {
9604				anchorEl: menuAnchorEl,
9605				open: Boolean(menuAnchorEl),
9606				onClose: closeMenu,
9607				title: menuAudience.name,
9608				actions: rowActions(menuAudience),
9609				anchorOrigin: {
9610					vertical: "bottom",
9611					horizontal: "right"
9612				},
9613				transformOrigin: {
9614					vertical: "top",
9615					horizontal: "right"
9616				}
9617			}),
9618			copying && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CopyAudienceToAccountDialog, {
9619				audience: copying,
9620				onClose: () => setCopying(null)
9621			})
9622		]
9623	});
9624};
9625var stackSx$3 = {
9626	display: "flex",
9627	flexDirection: "column",
9628	gap: 2
9629};
9630//#endregion
9631//#region node_modules/@fortawesome/pro-regular-svg-icons/faUsersViewfinder.js
9632var require_faUsersViewfinder = /* @__PURE__ */ __commonJSMin(((exports) => {
9633	Object.defineProperty(exports, "__esModule", { value: true });
9634	var prefix = "far";
9635	var iconName = "users-viewfinder";
9636	var width = 576;
9637	var height = 512;
9638	var aliases = [];
9639	var unicode = "e595";
9640	var svgPathData = "M64 0C28.7 0 0 28.7 0 64l0 72c0 13.3 10.7 24 24 24s24-10.7 24-24l0-72c0-8.8 7.2-16 16-16l72 0c13.3 0 24-10.7 24-24S149.3 0 136 0L64 0zM440 0c-13.3 0-24 10.7-24 24s10.7 24 24 24l72 0c8.8 0 16 7.2 16 16l0 72c0 13.3 10.7 24 24 24s24-10.7 24-24l0-72c0-35.3-28.7-64-64-64L440 0zM48 376c0-13.3-10.7-24-24-24S0 362.7 0 376l0 72c0 35.3 28.7 64 64 64l72 0c13.3 0 24-10.7 24-24s-10.7-24-24-24l-72 0c-8.8 0-16-7.2-16-16l0-72zm528 0c0-13.3-10.7-24-24-24s-24 10.7-24 24l0 72c0 8.8-7.2 16-16 16l-72 0c-13.3 0-24 10.7-24 24s10.7 24 24 24l72 0c35.3 0 64-28.7 64-64l0-72zM288 128a32 32 0 1 1 0 64 32 32 0 1 1 0-64zm0 112a80 80 0 1 0 0-160 80 80 0 1 0 0 160zM160 160c0-16.4 3.1-32 8.7-46.4-32.4 7-56.7 35.9-56.7 70.4 0 39.8 32.2 72 72 72 5.9 0 11.6-.7 17.1-2-25.3-23.4-41.1-56.8-41.1-94zm232 96c39.8 0 72-32.2 72-72 0-34.5-24.3-63.3-56.7-70.4 5.6 14.4 8.7 30 8.7 46.4 0 37.1-15.8 70.6-41.1 94 5.5 1.3 11.2 2 17.1 2zM170.5 288.2C120.1 291 80 332.8 80 384l0 8c0 13.3 10.7 24 24 24s24-10.7 24-24l0-8c0-4.6 .6-9 1.8-13.1 4.9-31.8 19.5-60.4 40.7-82.7zm275.7 82.7c1.2 4.2 1.8 8.6 1.8 13.1l0 8c0 13.3 10.7 24 24 24s24-10.7 24-24l0-8c0-51.2-40.1-93-90.5-95.8 21.2 22.3 35.8 50.9 40.7 82.7zM272 288c-53 0-96 43-96 96l0 8c0 13.3 10.7 24 24 24s24-10.7 24-24l0-8c0-26.5 21.5-48 48-48l32 0c26.5 0 48 21.5 48 48l0 8c0 13.3 10.7 24 24 24s24-10.7 24-24l0-8c0-53-43-96-96-96l-32 0z";
9641	exports.definition = {
9642		prefix,
9643		iconName,
9644		icon: [
9645			width,
9646			height,
9647			aliases,
9648			unicode,
9649			svgPathData
9650		]
9651	};
9652	exports.faUsersViewfinder = exports.definition;
9653	exports.prefix = prefix;
9654	exports.iconName = iconName;
9655	exports.width = width;
9656	exports.height = height;
9657	exports.ligatures = aliases;
9658	exports.unicode = unicode;
9659	exports.svgPathData = svgPathData;
9660	exports.aliases = aliases;
9661}));
9662//#endregion
9663//#region client/components/newNav/components/ads/targeting/AudienceEditor.tsx
9664var import_faEarthAmericas = require_faEarthAmericas();
9665var import_faUsersViewfinder = require_faUsersViewfinder();
9666/**
9667* A group's React key. Loading is keyed by position so `toDraft` stays pure - the unsaved-changes check compares
9668* a fresh draft against the edited one - while a group added here takes a counter key that no position can clash
9669* with. A key has to outlive its neighbours being removed: the conditions table under it holds its own dialog
9670* state, and reusing a key would hand that state to a different group.
9671*/
9672var addedGroupCount = 0;
9673var loadedKey = (index) => `group-${index}`;
9674var addedKey = () => `added-group-${addedGroupCount++}`;
9675var emptyDmpGroup = (key) => ({
9676	key,
9677	include: [],
9678	exclude: [],
9679	maxAgeDays: ""
9680});
9681var emptySiteGroup = (key) => ({
9682	key,
9683	minAgeDays: "",
9684	maxAgeDays: "",
9685	rules: []
9686});
9687var blockOf = (audience, evidence) => audience?.blocks.find((block) => block.ruleType === evidence);
9688var dayText = (days) => days === null || days === void 0 ? "" : String(days);
9689var toDmpGroupDraft = (group, index) => {
9690	const segments = toDmpSegments(group);
9691	return {
9692		key: loadedKey(index),
9693		include: segments?.include ?? [],
9694		exclude: segments?.exclude ?? [],
9695		maxAgeDays: dayText(group.maxAgeDays)
9696	};
9697};
9698var toSiteGroupDraft = (group, index) => ({
9699	key: loadedKey(index),
9700	minAgeDays: dayText(group.minAgeDays),
9701	maxAgeDays: dayText(group.maxAgeDays),
9702	rules: group.rules.map((rule, at) => ({
9703		id: `rule-${at}`,
9704		field: rule.field,
9705		operator: rule.operator,
9706		value: ruleValues(rule.value)
9707	}))
9708});
9709/** An empty group is kept in the draft even when the section is off, so switching it on has something to fill in. */
9710var groupDrafts = (block, convert, empty) => block && block.groups.length > 0 ? block.groups.map(convert) : [empty()];
9711var toDraft = (audience) => {
9712	const dmpBlock = blockOf(audience, AudienceEvidence.DMP);
9713	const siteBlock = blockOf(audience, AudienceEvidence.SITE);
9714	const impressionBlock = blockOf(audience, AudienceEvidence.IMPRESSION);
9715	return {
9716		name: audience?.name ?? "",
9717		dmp: {
9718			on: Boolean(dmpBlock),
9719			groups: groupDrafts(dmpBlock, toDmpGroupDraft, () => emptyDmpGroup(loadedKey(0)))
9720		},
9721		site: {
9722			on: Boolean(siteBlock),
9723			groups: groupDrafts(siteBlock, toSiteGroupDraft, () => emptySiteGroup(loadedKey(0)))
9724		},
9725		impression: {
9726			on: Boolean(impressionBlock),
9727			programs: impressionBlock?.groups[0] ? toExcludedPrograms(impressionBlock.groups[0]) ?? [] : []
9728		}
9729	};
9730};
9731var dayCount = (text) => text.trim() ? Number(text) : null;
9732var toBlocks = (draft) => {
9733	const blocks = [];
9734	if (draft.dmp.on) blocks.push({
9735		ruleType: AudienceEvidence.DMP,
9736		groups: draft.dmp.groups.map((group) => ({
9737			minAgeDays: null,
9738			maxAgeDays: dayCount(group.maxAgeDays),
9739			rules: [...group.include.length > 0 ? [{
9740				field: DMP_SEGMENT_FIELD,
9741				operator: "EQUALS",
9742				value: group.include.join(",")
9743			}] : [], ...group.exclude.length > 0 ? [{
9744				field: DMP_SEGMENT_FIELD,
9745				operator: "DOES_NOT_EQUAL",
9746				value: group.exclude.join(",")
9747			}] : []]
9748		}))
9749	});
9750	if (draft.site.on) blocks.push({
9751		ruleType: AudienceEvidence.SITE,
9752		groups: draft.site.groups.map((group) => ({
9753			minAgeDays: dayCount(group.minAgeDays),
9754			maxAgeDays: dayCount(group.maxAgeDays),
9755			rules: group.rules.map((rule) => ({
9756				field: asText(rule.field),
9757				operator: asText(rule.operator),
9758				value: OPERATORS_WITHOUT_VALUE.includes(asText(rule.operator)) ? null : asStringList(rule.value).join(",")
9759			}))
9760		}))
9761	});
9762	if (draft.impression.on) blocks.push({
9763		ruleType: AudienceEvidence.IMPRESSION,
9764		groups: [{
9765			minAgeDays: null,
9766			maxAgeDays: null,
9767			rules: [{
9768				field: IMPRESSION_PROGRAM_FIELD,
9769				operator: "DOES_NOT_EQUAL",
9770				value: draft.impression.programs.join(",")
9771			}]
9772		}]
9773	});
9774	return blocks;
9775};
9776/** Names the group when there is more than one, since the offending box is otherwise anyone's guess. */
9777var groupProblem = (index, total, problem) => total === 1 ? problem : `${problem} in group ${index + 1}`;
9778var describeDraftProblem = (draft) => {
9779	if (!draft.name.trim()) return "Name the audience before saving";
9780	if (!draft.dmp.on && !draft.site.on && !draft.impression.on) return "Turn on at least one of the three - an audie
9780nce with nothing in it matches nobody";
9781	if (draft.dmp.on) {
9782		const empty = draft.dmp.groups.findIndex((group) => group.include.length === 0 && group.exclude.length === 0);
9783		if (empty >= 0) return groupProblem(empty, draft.dmp.groups.length, "Add at least one DMP segment id");
9784	}
9785	if (draft.site.on) {
9786		const empty = draft.site.groups.findIndex((group) => group.rules.length === 0);
9787		if (empty >= 0) return groupProblem(empty, draft.site.groups.length, "Add at least one site activity condition");
9788	}
9789	if (draft.impression.on && draft.impression.programs.length === 0) return "Name at least one campaign to leave out";
9790	return null;
9791};
9792var siteRuleFields = [
9793	{
9794		key: "field",
9795		label: "Where",
9796		type: "select",
9797		options: siteVisitorFieldOptions,
9798		defaultValue: "url"
9799	},
9800	{
9801		key: "operator",
9802		label: "Test",
9803		type: "select",
9804		options: siteVisitorOperatorOptions,
9805		defaultValue: "STRING_CONTAINS"
9806	},
9807	{
9808		key: "value",
9809		label: "Values",
9810		type: "stringList",
9811		wide: true,
9812		placeholder: "internet, get-connected",
9813		help: "Any one of these satisfies the test. Paste a comma-separated list to add them all at once.",
9814		disabledWhen: (values) => OPERATORS_WITHOUT_VALUE.includes(asText(values.operator))
9815	}
9816];
9817var BlockCard = ({ evidence, on, readOnly, onToggle, children }) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Paper, {
9818	variant: "outlined",
9819	sx: cardSx,
9820	children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
9821		sx: cardHeaderSx,
9822		children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Switch, {
9823			checked: on,
9824			disabled: readOnly,
9825			onChange: (event) => onToggle(event.target.checked),
9826			"data-tracking-id": `tracking-id-ads-audience-${evidence.toLowerCase()}-toggle`
9827		}), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
9828			variant: "h5",
9829			children: evidenceTitle[evidence]
9830		}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
9831			variant: "body2",
9832			color: "text.secondary",
9833			children: evidenceDescription[evidence]
9834		})] })]
9835	}
9835), on && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Box, {
9836		sx: cardBodySx,
9837		children
9838	})]
9839});
9840/**
9841* The condition groups of one evidence type. Matching any single group qualifies a request, which is what the ad
9842* server does with a rule set's rule groups - so a segment list that is really four alternative lists stays four
9843* groups here instead of being merged into one.
9844*/
9845var GroupList = ({ groups, keyOf, addLabel, readOnly, trackingId, onChange, empty, renderGroup }) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
9846	sx: groupListSx,
9847	children: [
9848		groups.map((group, index) => {
9849			const update = (changes) => onChange(groups.map((existing, at) => at === index ? {
9850				...existing,
9851				...changes
9852			} : existing));
9853			if (groups.length === 1) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Box, {
9854				sx: groupSx$1,
9855				children: renderGroup(group, update)
9856			}, keyOf(group));
9857			return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Paper, {
9858				variant: "outlined",
9859				sx: groupPaperSx,
9860				children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
9861					sx: groupHeaderSx$1,
9862					children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
9863						variant: "h6",
9864						children: index === 0 ? "Group 1" : `Or group ${index + 1}`
9865					}), !readOnly && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.TruffleIconButton, {
9866						size: "small",
9867						onClick: () => onChange(groups.filter((_group, at) => at !== index)),
9868						"data-tracking-id": `${trackingId}-remove-group-btn`,
9869						children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FontAwesomeIcon, { icon: import_faTrashCan.faTrashCan })
9870					})]
9871				}), renderGroup(group, update)]
9872			}, keyOf(group));
9873		}),
9874		!readOnly && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Button, {
9875			variant: "text",
9876			size: "small",
9877			sx: addButtonSx,
9878			startIcon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FontAwesomeIcon, { icon: import_faPlus.faPlus }),
9879			onClick: () => onChange([...groups, empty()]),
9880			"data-tracking-id": `${trackingId}-add-group-btn`,
9881			children: addLabel
9882		}),
9883		groups.length > 1 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
9884			variant: "helper",
9885			color: "text.secondary",
9886			children: "Matching any one of these groups is enough - they are alternatives, not extra requirements."
9887		})
9888	]
9889});
9890/**
9891* One audience, in the terms the buy is described in rather than in the four tables it is stored as. The rule
9892* tree underneath is assembled on save and never shown.
9893*/
9894var AudienceEditor = ({ draft, readOnly, onChange }) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
9895	sx: stackSx$2,
9896	children: [
9897		/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TextField, {
9898			label: "Name",
9899			value: draft.name,
9900			size: "small",
9901			fullWidth: true,
9902			disabled: readOnly,
9903			onChange: (event) => onChange({
9904				...draft,
9905				name: event.target.value
9906			}),
9907			inputProps: { "data-tracking-id": "tracking-id-ads-audience-name-input" }
9908		}),
9909		/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
9910			variant: "helper",
9911			color: "text.secondary",
9912			children: "This name reaches third-party pixels and story selection, so renaming it changes what partners receive."
9913		}),
9914		/* @__PURE__ */ (0, import_jsx_runtime.jsx)(BlockCard, {
9915			evidence: AudienceEvidence.DMP,
9916			on: draft.dmp.on,
9917			readOnly,
9918			onToggle: (on) => onChange({
9919				...draft,
9920				dmp: {
9921					...draft.dmp,
9922					on
9923				}
9924			}),
9925			children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(GroupList, {
9926				groups: draft.dmp.groups,
9927				keyOf: (group) => group.key,
9928				addLabel: "Add segment group",
9929				readOnly,
9930				trackingId: "tracking-id-ads-audience-dmp",
9931				empty: () => emptyDmpGroup(addedKey()),
9932				onChange: (groups) => onChange({
9933					...draft,
9934					dmp: {
9935						...draft.dmp,
9936						groups
9937					}
9938				}),
9939				renderGroup: (group, update) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
9940					sx: groupBodySx,
9941					children: [
9942						/* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsStringListInput, {
9943							label: "In any of these segments",
9944							values: group.include,
9945							disabled: readOnly,
9946							placeholder: "1008560101, 1011990061",
9947							onChange: (include) => update({ include }),
9948							trackingId: "tracking-id-ads-audience-dmp-include"
9949						}),
9950						/* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsStringListInput, {
9951							label: "But in none of these",
9952							values: group.exclude,
9953							disabled: readOnly,
9954							placeholder: "15153561, 17355442",
9955							onChange: (exclude) => update({ exclude }),
9956							trackingId: "tracking-id-ads-audience-dmp-exclude"
9957						}),
9958						/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TextField, {
9959							label: "Placed in the segment within the last (days)",
9960							type: "number",
9961							size: "small",
9962							sx: windowFieldSx,
9963							value: group.maxAgeDays,
9964							disabled: readOnly,
9965							helperText: "Leave empty to accept the segment however long ago it was assigned.",
9966							onChange: (event) => update({ maxAgeDays: event.target.value }),
9967							inputProps: {
9968								min: 0,
9969								"data-tracking-id": "tracking-id-ads-audience-dmp-max-age-input"
9970							}
9971						})
9972					]
9973				})
9974			})
9975		}),
9976		/* @__PURE__ */ (0, import_jsx_runtime.jsx)(BlockCard, {
9977			evidence: AudienceEvidence.SITE,
9978			on: draft.site.on,
9979			readOnly,
9980			onToggle: (on) => onChange({
9981				...draft,
9982				site: {
9983					...draft.site,
9984					on
9985				}
9986			}),
9987			children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(GroupList, {
9988				groups: draft.site.groups,
9989				keyOf: (group) => group.key,
9990				addLabel: "Add activity group",
9991				readOnly,
9992				trackingId: "tracking-id-ads-audience-site",
9993				empty: () => emptySiteGroup(addedKey()),
9994				onChange: (groups) => onChange({
9995					...draft,
9996					site: {
9997						...draft.site,
9998						groups
9999					}
10000				}),
10001				renderGroup: (group, update) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
10002					sx: groupBodySx,
10003					children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
10004						sx: windowRowSx,
10005						children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TextField, {
10006							label: "Seen in the last (days)",
10007							type: "number",
10008							size: "small",
10009							value: group.maxAgeDays,
10010							disabled: readOnly,
10011							onChange: (event) => update({ maxAgeDays: event.target.value }),
10012							inputProps: {
10013								min: 0,
10014								"data-tracking-id": "tracking-id-ads-audience-site-max-age-input"
10015							}
10016						}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TextField, {
10017							label: "But not more recently than (days)",
10018							type: "number",
10019							size: "small",
10020							value: group.minAgeDays,
10021							disabled: readOnly,
10022							onChange: (event) => update({ minAgeDays: event.target.value }),
10023							inputProps: {
10024								min: 0,
10025								"data-tracking-id": "tracking-id-ads-audience-site-min-age-input"
10026							}
10027						})]
10028					}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsChildTable, {
10029						title: "Conditions",
10030						description: "All of these have to hold for the same user.",
10031						fields: siteRuleFields,
10032						columns: [
10033							{
10034								key: "field",
10035								label: "Where",
10036								render: (row) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
10037									variant: "body1",
10038									children: siteVisitorFieldOptions.find((option) => option.value === asText(row.field))?.label ?? asText(row.field)
10039								})
10040							},
10041							{
10042								key: "operator",
10043								label: "Test",
10044								render: (row) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
10045									variant: "body1",
10046									children: siteVisitorOperatorOptions.find((option) => option.value === asText(row.operator))?.label ?? asText(row.operator)
10047								})
10048							},
10049							{
10050								key: "value",
10051								label: "Values",
10052								render: (row) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
10053									variant: "body1",
10054									children: asStringList(row.value).join(", ") || "-"
10055								})
10056							}
10057						],
10058						rows: group.rules,
10059						addLabel: "Add condition",
10060						emptyText: "No conditions yet",
10061						rowTitle: (row) => asText(row.field),
10062						readOnly,
10063						onChange: (rules) => update({ rules }),
10064						trackingIdPrefix: "tracking-id-ads-audience-site-rules"
10065					})]
10066				})
10067			})
10068		}),
10069		/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(BlockCard, {
10070			evidence: AudienceEvidence.IMPRESSION,
10071			on: draft.impression.on,
10072			readOnly,
10073			onToggle: (on) => onChange({
10074				...draft,
10075				impression: {
10076					...draft.impression,
10077					on
10078				}
10079			}),
10080			children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsStringListInput, {
10081				label: "Leave out anyone already served by",
10082				values: draft.impression.programs,
10083				disabled: readOnly,
10084				placeholder: "martins_retain",
10085				onChange: (programs) => onChange({
10086					...draft,
10087					impression: {
10088						...draft.impression,
10089						programs
10090					}
10091				}),
10092				trackingId: "tracking-id-ads-audience-impression-programs"
10093			}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
10094				variant: "helper",
10095				color: "text.secondary",
10096				children: "Campaign names exactly as they appear in the campaigns list."
10097			})]
10098		}),
10099		[
10100			draft.dmp.on,
10101			draft.site.on,
10102			draft.impression.on
10103		].filter(Boolean).length > 1 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.AttentionBox, {
10104			color: "info",
10105			children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.AttentionBoxContent, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
10106				variant: "body1",
10107				color: "text.secondary",
10108				children: "A user has to satisfy every section switched on above, not just one of them."
10109			}) })
10110		})
10111	]
10112});
10113var stackSx$2 = {
10114	display: "flex",
10115	flexDirection: "column",
10116	gap: 2
10117};
10118var cardSx = {
10119	display: "flex",
10120	flexDirection: "column",
10121	gap: 2,
10122	padding: 2
10123};
10124var cardHeaderSx = {
10125	display: "flex",
10126	alignItems: "flex-start",
10127	gap: 1.5
10128};
10129var cardBodySx = {
10130	display: "flex",
10131	flexDirection: "column",
10132	gap: 2,
10133	paddingLeft: 7
10134};
10135var groupListSx = {
10136	display: "flex",
10137	flexDirection: "column",
10138	gap: 2
10139};
10140var groupSx$1 = {
10141	display: "flex",
10142	flexDirection: "column",
10143	gap: 1.5
10144};
10145var groupPaperSx = {
10146	display: "flex",
10147	flexDirection: "column",
10148	gap: 1.5,
10149	padding: 2
10150};
10151var groupHeaderSx$1 = {
10152	display: "flex",
10153	alignItems: "center",
10154	justifyContent: "space-between",
10155	gap: 1
10156};
10157var groupBodySx = {
10158	display: "flex",
10159	flexDirection: "column",
10160	gap: 2
10161};
10162var windowRowSx = {
10163	display: "flex",
10164	gap: 2,
10165	flexWrap: "wrap"
10166};
10167var windowFieldSx = { maxWidth: 320 };
10168var addButtonSx = { alignSelf: "flex-start" };
10169//#endregion
10170//#region client/components/newNav/components/ads/targeting/useAdsSpaceAudiences.ts
10171/**
10172* The account's audiences with their rule trees. One query rather than a walk down four collections, because
10173* the list has to say what each audience matches and the editor holds the whole tree at once.
10174*/
10175var useAdsSpaceAudiences = () => {
10176	const { data, loading, error, refetch } = useCurrentAccountAdsSpaceAudiencesQuery();
10177	const [saveAudience, { loading: saving }] = useSaveAdsSpaceAudienceMutation();
10178	const audiences = (0, import_react.useMemo)(() => data?.currentAccount?.adsSpace?.audiences ?? [], [data]);
10179	const save = async (input) => {
10180		try {
10181			await saveAudience({ variables: { input } });
10182			setSuccessNotification({ message: "Saved" });
10183		} catch (saveError) {
10184			setErrorNotification({ message: saveError?.message ?? "Saving failed" });
10185			throw saveError;
10186		}
10187	};
10188	const reload = async () => {
10189		return (await refetch()).data?.currentAccount?.adsSpace?.audiences ?? [];
10190	};
10191	return {
10192		audiences,
10193		loading,
10194		error: Boolean(error),
10195		saving,
10196		save,
10197		refetch: reload
10198	};
10199};
10200//#endregion
10201//#region client/components/newNav/components/ads/targeting/AdsNewAudienceDialog.tsx
10202/**
10203* Creating an account audience from a screen that asks for one. Audiences save through their own mutation, so
10204* making one mid-edit does not commit the campaign - the pending campaign edits stay pending.
10205*/
10206var AdsNewAudienceDialog = ({ open, onClose, onCreated }) => {
10207	const { save, saving, refetch } = useAdsSpaceAudiences();
10208	const [draft, setDraft] = (0, import_react.useState)(() => toDraft(null));
10209	const [comment, setComment] = (0, import_react.useState)("");
10210	(0, import_react.useEffect)(() => {
10211		if (open) {
10212			setDraft(toDraft(null));
10213			setComment("");
10214		}
10215	}, [open]);
10216	const problem = describeDraftProblem(draft);
10217	const canSave = problem === null && comment.trim().length > 0 && !saving;
10218	const handleSave = async () => {
10219		await save({
10220			name: draft.name,
10221			blocks: toBlocks(draft),
10222			changeComment: comment
10223		});
10224		const reloaded = await refetch();
10225		onClose();
10226		const created = reloaded.find((audience) => audience.name === draft.name);
10227		if (created) onCreated(created.id);
10228	};
10229	return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Dialog, {
10230		open,
10231		onClose: saving ? void 0 : onClose,
10232		maxWidth: "md",
10233		fullWidth: true,
10234		"data-testid": "ads-new-audience-dialog",
10235		children: [
10236			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.TruffleDialogTitle, {
10237				CloseIconButtonProps: {
10238					onClick: onClose,
10239					disabled: saving,
10240					"data-tracking-id": "tracking-id-ads-new-audience-close-btn"
10241				},
10242				children: "New audience"
10243			}),
10244			/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.DialogContent, { children: [
10245				/* @__PURE__ */ (0, import_jsx_runtime.jsx)(AudienceEditor, {
10246					draft,
10247					onChange: setDraft
10248				}),
10249				/* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsChangeCommentField, {
10250					value: comment,
10251					onChange: setComment,
10252					trackingId: "tracking-id-ads-new-audience-comment-input"
10253				}),
10254				problem && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
10255					variant: "helper",
10256					color: "error.main",
10257					children: problem
10258				})
10259			] }),
10260			/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_dist$2.TruffleDialogActions, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Button, {
10261				variant: "text",
10262				size: "large",
10263				disabled: saving,
10264				onClick: onClose,
10265				"data-tracking-id": "tracking-id-ads-new-audience-cancel-btn",
10266				children: "Cancel"
10267			}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Button, {
10268				variant: "contained",
10269				size: "large",
10270				disabled: !canSave,
10271				onClick: () => void handleSave().catch(() => void 0),
10272				"data-tracking-id": "tracking-id-ads-new-audience-save-btn",
10273				children: "Create"
10274			})] })
10275		]
10276	});
10277};
10278//#endregion
10279//#region client/components/newNav/components/ads/campaigns/CampaignEntityTargeting.tsx
10280var duplicateError = (key, message) => (row, otherRows) => otherRows.some((other) => asText(other[key]) === asText(row[key])) ? { [key]: message } : {};
10281var validateGeoRow = duplicateError("geoFilterId", "This campaign already targets that geography");
10282var validateAudienceRow = duplicateError("audienceId", "This campaign already targets that audience");
10283var validateSelectorRow = duplicateError("audienceId", "That audience already has a product selector");
10284/**
10285* The older settings' title row when they are deciding something. Red rather than a caption: the complaint this
10286* answers is that a deprecated setting looked like an inactive one.
10287*/
10288var LegacyTargetingBadge = ({ runtime }) => {
10289	if (isLegacyTargetingInForce(runtime)) return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
10290		sx: badgeSx,
10291		children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.SvgIcon, {
10292			fontSize: "small",
10293			color: "error",
10294			children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FontAwesomeIcon, { icon: import_faTriangleExclamation.faTriangleExclamation })
10295		}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.Label, {
10296			size: "small",
10297			color: "error",
10298			variant: "outlined",
10299			label: "In use - deprecated"
10300		})]
10301	});
10302	return hasTargetingConflict(runtime) ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.Label, {
10303		size: "small",
10304		color: "warning",
10305		variant: "outlined",
10306		label: "Configured but ignored"
10307	}) : null;
10308};
10309/**
10310* The two account-level collections a campaign points at. Both are plain link rows - the geography and the
10311* audience themselves live on the account, so what is edited here is only which of them apply and how.
10312*/
10313var CampaignEntityTargeting = (props) => {
10314	const accountId = useActiveAccountId();
10315	const geoRows = props.form.childRows(GqlClientAdsSpaceCampaignChildTable.PROGRAM_GEO_FILTERS);
10316	const audienceRows = props.form.childRows(GqlClientAdsSpaceCampaignChildTable.PROGRAM_AUDIENCES);
10317	const selectorRows = props.form.childRows(GqlClientAdsSpaceCampaignChildTable.PRODUCT_SELECTORS);
10318	const noEntitiesDefined = props.geoOptions.length === 0 && props.audienceOptions.length === 0;
10319	const runtime = targetingRuntime({
10320		locationFilter: asText(props.form.values.locationFilter),
10321		hasInlineAudiences: props.hasInlineAudiences,
10322		accountGeoCount: geoRows.length,
10323		accountAudienceCount: audienceRows.length
10324	});
10325	const part = props.part ?? "all";
10326	const isLegacyInForce = isLegacyTargetingInForce(runtime);
10327	const attachedAudienceOptions = props.audienceOptions.filter((option) => audienceRows.some((row) => asText(row.audienceId) === option.value));
10328	const [newEntity, setNewEntity] = (0, import_react.useState)(null);
10329	const [isConverting, setIsConverting] = (0, import_react.useState)(false);
10330	const locationFilter = asText(props.form.values.locationFilter);
10331	const isConvertible = Boolean(locationFilter.trim()) && parseGeoDefinition(locationFilter).unparsed === null;
10332	const convertedGeoRow = {
10333		id: "new",
10334		name: `${props.campaignName}_geo`,
10335		definition: locationFilter,
10336		description: `Converted from the older location filter on ${props.campaignName}.`
10337	};
10338	const attachGeography = (geoFilterId) => props.form.onChildRowsChange(GqlClientAdsSpaceCampaignChildTable.PROGRAM_GEO_FILTERS, [...geoRows, {
10339		id: `geo-${geoFilterId}`,
10340		geoFilterId,
10341		mode: GeoFilterMode.WHITELIST,
10342		positionIdx: String(geoRows.length)
10343	}]);
10344	const attachAudience = (audienceId) => props.form.onChildRowsChange(GqlClientAdsSpaceCampaignChildTable.PROGRAM_AUDIENCES, [...audienceRows, {
10345		id: `audience-${audienceId}`,
10346		audienceId,
10347		enabled: true,
10348		audienceIdx: String(audienceRows.length)
10349	}]);
10350	const uncovered = audienceRows.filter((row) => row.enabled !== false).filter((row) => !selectorRows.some((selector) => asText(selector.audienceId) === asText(row.audienceId))).map((row) => findEntityLabel(props.audienceOptions, asText(row.audienceId)));
10351	return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
10352		sx: stackSx$1,
10353		children: [
10354			noEntitiesDefined && part !== "box" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.AttentionBox, {
10355				color: "info",
10356				children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_dist$2.AttentionBoxContent, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
10357					variant: "body1",
10358					color: "text.secondary",
10359					children: "This account has no saved audiences or geographies yet. Define them once on the targeting page and every campaign can point at them from here."
10360				}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Button, {
10361					component: Link$6,
10362					to: (0, import_dist$1.newNavAdsTargetingSectionPath)({
10363						accountId,
10364						sectionId: "geo-lists"
10365					}),
10366					variant: "text",
10367					size: "small",
10368					"data-tracking-id": "tracking-id-ads-campaign-targeting-define-entities-btn",
10369					children: "Go to targeting"
10370				})] })
10371			}),
10372			part !== "tables" && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_dist$2.AttentionBox, {
10373				color: targetingBoxColor(runtime),
10374				"data-testid": "testId-ads-campaign-targeting-runtime",
10375				children: [isLegacyInForce && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.AttentionBoxTitle, { children: "What this campaign serves on right now" }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_dist$2.AttentionBoxContent, {
10376					sx: isLegacyInForce ? void 0 : compactBoxSx,
10377					children: [targetingRuntimeLines(runtime, {
10378						accountGeoCount: geoRows.length,
10379						accountAudienceCount: audienceRows.length
10380					}).map((line) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
10381						variant: "body1",
10382						color: "text.secondary",
10383						children: line
10384					}, line)), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
10385						sx: boxActionsSx,
10386						children: [
10387							isLegacyInForce && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Button, {
10388								variant: "text",
10389								size: "small",
10390								onClick: () => scrollToTargetingElement("ads-targeting-account-geo"),
10391								"data-tracking-id": "tracking-id-ads-campaign-jump-account-targeting-btn",
10392								children: "See the account-level targeting"
10393							}),
10394							hasTargetingConflict(runtime) && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Button, {
10395								variant: "text",
10396								size: "small",
10397								onClick: () => scrollToTargetingElement("ads-group-legacy"),
10398								"data-tracking-id": "tracking-id-ads-campaign-jump-legacy-btn",
10399								children: "See the older settings that are being ignored"
10400							}),
10401							runtime.geo === "LEGACY" && !props.readOnly && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Tooltip, {
10402								title: isConvertible ? "" : "This filter was written by hand and cannot be converted automatically. Create the account geography and clear this field by hand.",
10403								children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Button, {
10404									variant: "text",
10405									size: "small",
10406									disabled: !isConvertible,
10407									onClick: () => {
10408										setIsConverting(true);
10409										setNewEntity("geography");
10410									},
10411									"data-testid": "ads-campaign-move-to-account-geo-btn",
10412									"data-tracking-id": "tracking-id-ads-campaign-move-to-account-geo-btn",
10413									children: "Move to account geographies"
10414								}) })
10415							})
10416						]
10417					})]
10418				})]
10419			}),
10420			part !== "box" && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
10421				sx: groupSx,
10422				id: "ads-targeting-account-geo",
10423				children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
10424					sx: groupHeaderSx,
10425					children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.SvgIcon, {
10426						fontSize: "small",
10427						color: "action",
10428						children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FontAwesomeIcon, { icon: import_faEarthAmericas.faEarthAmericas })
10429					}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
10430						variant: "h5",
10431						children: "Where it serves"
10432					})]
10433				}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsChildTable, {
10434					title: "Geographies",
10435					description: "Serve-only rows add up; a never-serve row is then subtracted, so a place covered by both is excluded. None attached means unrestricted.",
10436					fields: programGeoFilterFields(props.geoOptions),
10437					columns: [{
10438						key: "geoFilterId",
10439						label: "Geography",
10440						render: (row) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
10441							variant: "body1",
10442							children: findEntityLabel(props.geoOptions, asText(row.geoFilterId))
10443						})
10444					}, {
10445						key: "mode",
10446						label: "How it applies",
10447						render: (row) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.Label, {
10448							size: "small",
10449							variant: "standard",
10450							color: asText(row.mode) === GeoFilterMode.BLACKLIST ? "warning" : "success",
10451							label: asText(row.mode) === GeoFilterMode.BLACKLIST ? "Never serve here" : "Serve only here"
10452						})
10453					}],
10454					rows: geoRows,
10455					addLabel: "Add geographies",
10456					emptyText: runtime.geo === "LEGACY" ? "No account geographies attached - this campaign serves on its older location filter, in the deprecated settings section" : "No geographies attached - this campaign may serve anywhere",
10457					rowTitle: (row) => findEntityLabel(props.geoOptions, asText(row.geoFilterId)),
10458					readOnly: props.readOnly,
10459					validateRow: validateGeoRow,
10460					bulkAdd: {
10461						fields: attachGeoFiltersFields(props.geoOptions, geoRows),
10462						toRows: (values) => asStringList(values.geoFilterIds).map((geoFilterId) => ({
10463							id: `geo-${geoFilterId}`,
10464							geoFilterId,
10465							mode: asText(values.mode) || GeoFilterMode.WHITELIST
10466						}))
10467					},
10468					onChange: (rows) => props.form.onChildRowsChange(GqlClientAdsSpaceCampaignChildTable.PROGRAM_GEO_FILTERS, rows.map((row, index) => ({
10469						...row,
10470						mode: asText(row.mode) || GeoFilterMode.WHITELIST,
10471						positionIdx: String(index)
10472					}))),
10473					headerActions: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Button, {
10474						variant: "text",
10475						size: "medium",
10476						onClick: () => {
10477							setIsConverting(false);
10478							setNewEntity("geography");
10479						},
10480						"data-testid": "ads-campaign-new-geography-btn",
10481						"data-tracking-id": "tracking-id-ads-campaign-new-geography-btn",
10482						children: "New geography"
10483					}),
10484					trackingIdPrefix: "tracking-id-ads-campaign-geo-filters"
10485				})]
10486			}),
10487			part !== "box" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Divider, {}),
10488			part !== "box" && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
10489				sx: groupSx,
10490				id: "ads-targeting-account-audiences",
10491				children: [
10492					/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
10493						sx: groupHeaderSx,
10494						children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.SvgIcon, {
10495							fontSize: "small",
10496							color: "action",
10497							children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FontAwesomeIcon, { icon: import_faUsersViewfinder.faUsersViewfinder })
10498						}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
10499							variant: "h5",
10500							children: "Who it bids on"
10501						})]
10502					}),
10503					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsChildTable, {
10504						title: "Audiences",
10505						description: "Matching any one of these qualifies a request; priority only decides which name is reported. None attached means everyone the campaign may serve to.",
10506						fields: programAudienceFields(props.audienceOptions),
10507						columns: [
10508							{
10509								key: "audienceId",
10510								label: "Audience",
10511								render: (row) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
10512									variant: "body1",
10513									children: findEntityLabel(props.audienceOptions, asText(row.audienceId))
10514								})
10515							},
10516							{
10517								key: "audienceIdx",
10518								label: "Reporting priority",
10519								align: "right"
10520							},
10521							{
10522								key: "enabled",
10523								label: "Status",
10524								render: (row) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.Label, {
10525									size: "small",
10526									variant: row.enabled ? "standard" : "dashed",
10527									color: row.enabled ? "success" : "default",
10528									label: row.enabled ? "Enabled" : "Paused"
10529								})
10530							}
10531						],
10532						rows: audienceRows,
10533						addLabel: "Add audiences",
10534						emptyText: props.hasInlineAudiences ? "No account audiences attached - this campaign targets its own older audiences, in the deprecated settings section" : "No audiences attached - this campaign bids on everyone",
10535						rowTitle: (row) => findEntityLabel(props.audienceOptions, asText(row.audienceId)),
10536						readOnly: props.readOnly,
10537						validateRow: validateAudienceRow,
10538						bulkAdd: {
10539							fields: attachAudiencesFields(props.audienceOptions, audienceRows),
10540							toRows: (values) => asStringList(values.audienceIds).map((audienceId) => ({
10541								id: `audience-${audienceId}`,
10542								audienceId,
10543								enabled: true
10544							}))
10545						},
10546						onChange: (rows) => props.form.onChildRowsChange(GqlClientAdsSpaceCampaignChildTable.PROGRAM_AUDIENCES, rows.map((row, index) => ({
10547							...row,
10548							audienceIdx: asText(row.audienceIdx) || String(index),
10549							enabled: row.enabled ?? true
10550						}))),
10551						headerActions: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Button, {
10552							variant: "text",
10553							size: "medium",
10554							onClick: () => setNewEntity("audience"),
10555							"data-testid": "ads-campaign-new-audience-btn",
10556							"data-tracking-id": "tracking-id-ads-campaign-new-audience-btn",
10557							children: "New audience"
10558						}),
10559						trackingIdPrefix: "tracking-id-ads-campaign-audiences"
10560					}),
10561					uncovered.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.AttentionBox, {
10562						color: "warning",
10563						children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.AttentionBoxContent, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Typography, {
10564							variant: "body1",
10565							color: "text.secondary",
10566							children: [
10567								`${uncovered.length === 1 ? "One audience has" : `${uncovered.length} audiences have`}`,
10568								" no product selector: ",
10569								uncovered.join(", "),
10570								". The ad server looks one up for the audience it matched and will not serve",
10571								" without it."
10572							]
10573						}) })
10574					}),
10575					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsChildTable, {
10576						title: "Product selectors",
10577						description: "What a viewer who matched an audience is shown. One per attached audience. Campaign-owned audiences keep theirs in the deprecated settings section and are not editable here.",
10578						fields: productSelectorFields(attachedAudienceOptions),
10579						columns: [
10580							{
10581								key: "audienceId",
10582								label: "Audience",
10583								render: (row) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
10584									variant: "body1",
10585									children: findEntityLabel(props.audienceOptions, asText(row.audienceId))
10586								})
10587							},
10588							{
10589								key: "type",
10590								label: "Show",
10591								render: (row) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
10592									variant: "body1",
10593									children: productSelectorTypeLabel(asText(row.type))
10594								})
10595							},
10596							{
10597								key: "extraData",
10598								label: "Value"
10599							}
10600						],
10601						rows: selectorRows,
10602						addLabel: "Add product selector",
10603						emptyText: "No product selectors - a matched audience has no product to show",
10604						rowTitle: (row) => findEntityLabel(props.audienceOptions, asText(row.audienceId)),
10605						readOnly: props.readOnly,
10606						validateRow: validateSelectorRow,
10607						onChange: (rows) => props.form.onChildRowsChange(GqlClientAdsSpaceCampaignChildTable.PRODUCT_SELECTORS, rows.map((row) => ({
10608							...row,
10609							type: asText(row.type) || ProductSelectorType.LOCATION_TOP_SELLER
10610						}))),
10611						trackingIdPrefix: "tracking-id-ads-campaign-product-selectors"
10612					})
10613				]
10614			}),
10615			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsNewGeographyDialog, {
10616				open: newEntity === "geography",
10617				initialRow: isConverting ? convertedGeoRow : null,
10618				onClose: () => setNewEntity(null),
10619				onCreated: (geoFilterId) => {
10620					attachGeography(geoFilterId);
10621					if (isConverting) props.form.onChange("locationFilter", "");
10622				}
10623			}),
10624			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsNewAudienceDialog, {
10625				open: newEntity === "audience",
10626				onClose: () => setNewEntity(null),
10627				onCreated: attachAudience
10628			})
10629		]
10630	});
10631};
10632var badgeSx = {
10633	display: "flex",
10634	alignItems: "center",
10635	gap: 1
10636};
10637var compactBoxSx = {
10638	display: "flex",
10639	alignItems: "center",
10640	flexWrap: "wrap",
10641	gap: 1
10642};
10643var boxActionsSx = {
10644	display: "flex",
10645	alignItems: "center",
10646	flexWrap: "wrap",
10647	gap: 1
10648};
10649var stackSx$1 = {
10650	display: "flex",
10651	flexDirection: "column",
10652	gap: 4
10653};
10654var groupSx = {
10655	display: "flex",
10656	flexDirection: "column",
10657	gap: 2
10658};
10659var groupHeaderSx = {
10660	display: "flex",
10661	alignItems: "center",
10662	gap: 1
10663};
10664//#endregion
10665//#region client/components/newNav/components/ads/campaigns/adsCampaignValues.ts
10666/**
10667* The server stores every campaign setting as a string and the field descriptors say what it means, so the map
10668* below is the one place that knows a value is a switch or a multi-select. Options and rendered content are
10669* irrelevant to the type, which is why the builders are called with empty ones here.
10670*/
10671var fieldTypeByKey = Object.fromEntries([
10672	...[
10673		...buildOverviewGroups(),
10674		...buildDeliveryGroups(),
10675		...buildPacingGroups(),
10676		...buildTargetingGroups(null),
10677		...buildControlGroupGroups([]),
10678		...buildOptimizationGroups([], [], [])
10679	].flatMap((group) => group.fields),
10680	...flightFields,
10681	...creativeFields,
10682	...buildReportingFields(""),
10683	...exchangeCapFields([]),
10684	...campaignPropertyFields,
10685	...modelMappingFields([]),
10686	...goalAdjustmentFields,
10687	...programGeoFilterFields([]),
10688	...programAudienceFields([])
10689].map((field) => [field.key, field.type]));
10690var fromWireValue = (key, value) => {
10691	const type = fieldTypeByKey[key];
10692	if (type === "bool") return value === "true";
10693	if (type === "multiSelect" || type === "stringList") return value ? value.split(listSeparatorFor(type)).filter(Boolean) : [];
10694	return value ?? "";
10695};
10696var toWireValue = (key, value) => {
10697	const type = fieldTypeByKey[key];
10698	if (type === "bool") return value ? "true" : "false";
10699	if (type === "multiSelect" || type === "stringList") return asStringList(value).join(",");
10700	return asText(value);
10701};
10702var fromWireValues = (values) => Object.fromEntries(values.map((entry) => [entry.key, fromWireValue(entry.key, entry.value)]));
10703var toWireValues = (values, keys) => keys.map((key) => ({
10704	key,
10705	value: toWireValue(key, values[key] ?? null)
10706}));
10707/** A child row is the same bag with an id, so it converts the same way in both directions. */
10708var toWireChildRow = (row, keys) => ({
10709	id: row.id,
10710	values: toWireValues(row, keys)
10711}
10711);
10712//#endregion
10713//#region client/components/newNav/components/ads/campaigns/campaignChildTables.ts
10714var keysOf = (fields) => fields.map((field) => field.key);
10715/** Ordering columns nobody types - the editor assigns them from the row's position before the save. */
10716var ORDER_KEYS = {
10717	geoFilter: "positionIdx",
10718	lane: "laneIdx"
10719};
10720/** Which value keys travel with a row of each child table. The server rejects any key not in its own registry. */
10721var campaignChildFieldKeys = {
10722	[GqlClientAdsSpaceCampaignChildTable.FLIGHTS]: keysOf(flightFields),
10723	[GqlClientAdsSpaceCampaignChildTable.BUDGET_LANES]: [
10724		"name",
10725		"geoFilterId",
10726		"audienceId",
10727		"sharePercent",
10728		"margin",
10729		"enabled",
10730		ORDER_KEYS.lane
10731	],
10732	[GqlClientAdsSpaceCampaignChildTable.PROGRAM_GEO_FILTERS]: [...keysOf(programGeoFilterFields([])), ORDER_KEYS.geoFilter],
10733	[GqlClientAdsSpaceCampaignChildTable.PROGRAM_AUDIENCES]: keysOf(programAudienceFields([])),
10734	[GqlClientAdsSpaceCampaignChildTable.PRODUCT_SELECTORS]: keysOf(productSelectorFields([])),
10735	[GqlClientAdsSpaceCampaignChildTable.CREATIVES]: keysOf(creativeFields),
10736	[GqlClientAdsSpaceCampaignChildTable.REPORTING_DEFINITIONS]: keysOf(buildReportingFields("")),
10737	[GqlClientAdsSpaceCampaignChildTable.EXCHANGE_CAPS]: keysOf(exchangeCapFields([])),
10738	[GqlClientAdsSpaceCampaignChildTable.CAMPAIGN_PROPERTIES]: keysOf(campaignPropertyFields),
10739	[GqlClientAdsSpaceCampaignChildTable.MODEL_MAPPINGS]: keysOf(modelMappingFields([])),
10740	[GqlClientAdsSpaceCampaignChildTable.GOAL_ADJUSTMENTS]: keysOf(goalAdjustmentFields)
10741};
10742//#endregion
10743//#region client/components/newNav/components/ads/campaigns/useAdsCampaignForm.ts
10744/**
10745* One editing session for a whole campaign. Every tab writes into this one state, so switching tabs keeps the
10746* edits; a single Save sends every tab's fields and child tables in one mutation with one change comment and one
10747* optimistic-lock check. The saved baseline is the campaign the page read, so the mutation's returned campaign
10748* clears the dirty state and re-seeds `updatedAt` for the next save.
10749*/
10750var useAdsCampaignForm = (campaign, sections) => {
10751	const [values, setValues] = (0, import_react.useState)(campaign.values);
10752	const [rowsByTable, setRowsByTable] = (0, import_react.useState)({});
10753	const [errors, setErrors] = (0, import_react.useState)({});
10754	const [save, { loading: saving }] = useUpdateAdsSpaceCampaignSectionMutation();
10755	const allGroups = (0, import_react.useMemo)(() => sections.flatMap((section) => section.groups), [sections]);
10756	const allTables = (0, import_react.useMemo)(() => Array.from(new Set(sections.flatMap((section) => section.childTables))), [sections]);
10757	const childRows = (table) => rowsByTable[table] ?? campaign.childRows[table];
10758	const changeCount = (0, import_react.useMemo)(() => countChanges(values, campaign.values) + allTables.reduce((total, table) => total + countRowChanges(rowsByTable[table] ?? campaign.childRows[table], campaign.childRows[table]), 0), [
10759		values,
10760		rowsByTable,
10761		campaign,
10762		allTables
10763	]);
10764	const invalidLabels = (0, import_react.useMemo)(() => sections.flatMap((section) => section.groups.flatMap((group) => group.fields).filter((field) => errors[field.key]).map((field) => `${field.label} on ${section.label}`)), [sections, errors]);
10765	const onChange = (key, value) => {
10766		const next = {
10767			...values,
10768			[key]: value
10769		};
10770		setValues(next);
10771		setErrors(validateFields(allGroups, next));
10772	};
10773	const persist = async (comment) => {
10774		const sectionInputs = sections.map((section) => ({
10775			section: section.label,
10776			editableKeys: section.groups.flatMap((group) => group.fields).filter((field) => field.type !== "readOnly" && !field.machineWritten).map((field) => field.key)
10777		})).filter((section) => section.editableKeys.some((key) => hasFieldChanged(values, campaign.values, key))).map((section) => ({
10778			section: section.section,
10779			values: toWireValues(values, section.editableKeys)
10780		}));
10781		const changedTables = allTables.filter((table) => countRowChanges(childRows(table), campaign.childRows[table]) > 0);
10782		try {
10783			await save({ variables: {
10784				campaignId: campaign.id,
10785				input: {
10786					campaignId: campaign.id,
10787					updatedAt: campaign.updatedAt,
10788					changeComment: comment,
10789					sections: sectionInputs,
10790					childTables: changedTables.map((table) => ({
10791						table,
10792						rows: childRows(table).map((row) => toWireChildRow(row, campaignChildFieldKeys[table]))
10793					}))
10794				}
10795			} });
10796			setRowsByTable({});
10797			setSuccessNotification({ message: "Campaign saved" });
10798		} catch (error) {
10799			setErrorNotification({ message: error?.message ?? "Saving this campaign failed" });
10800		}
10801	};
10802	const onDiscard = () => {
10803		setValues(campaign.values);
10804		setRowsByTable({});
10805		setErrors({});
10806	};
10807	return {
10808		values,
10809		errors,
10810		dirty: changeCount > 0,
10811		changeCount,
10812		invalidLabels,
10813		onChange,
10814		onSave: persist,
10815		onDiscard,
10816		childRows,
10817		onChildRowsChange: (table, rows) => setRowsByTable((current) => ({
10818			...current,
10819			[table]: rows
10820		})),
10821		saving
10822	};
10823};
10824//#endregion
10825//#region client/components/newNav/components/ads/campaigns/useAdsSpaceCampaign.ts
10826var emptyChildRows = () => Object.values(GqlClientAdsSpaceCampaignChildTable).reduce((rows, table) => ({
10827	...rows,
10828	[table]: []
10829}), {});
10830var groupChildRows = (rows) => {
10831	const grouped = emptyChildRows();
10832	rows.forEach((row) => {
10833		grouped[row.table].push({
10834			...fromWireValues(row.values),
10835			id: row.id
10836		});
10837	});
10838	return grouped;
10839};
10840var toDetail = (campaign) => ({
10841	id: campaign.id,
10842	name: campaign.name,
10843	enabled: campaign.enabled,
10844	isCPCV: campaign.isCPCV,
10845	mode: campaign.mode,
10846	salesforceId: campaign.salesforceId ?? "",
10847	insertionOrder: campaign.insertionOrder ?? "",
10848	updatedAt: campaign.updatedAt ?? null,
10849	values: {
10850		...fromWireValues(campaign.values),
10851		salesforceId: campaign.salesforceId ?? "",
10852		campaignId: campaign.id
10853	},
10854	childRows: groupChildRows(campaign.childRows),
10855	experiments: campaign.experiments.map((experiment) => ({
10856		id: experiment.id,
10857		name: experiment.name,
10858		groups: experiment.groups.map((group) => ({
10859			id: group.id,
10860			name: group.name,
10861			min: group.min,
10862			max: group.max
10863		}))
10864	})),
10865	predictionModelOptions: campaign.predictionModels.map((model) => ({
10866		value: model.value,
10867		label: model.label
10868	})),
10869	inlineAudiences: campaign.inlineAudiences,
10870	mappedPlacementCount: campaign.mappedPlacementCount
10871});
10872/** One campaign as the settings page reads it. `campaign` is null once loaded when the id is not this account's. */
10873var useAdsSpaceCampaign = (campaignId) => {
10874	const { data, loading, error, refetch } = useCurrentAccountAdsSpaceCampaignQuery({ variables: { campaignId } });
10875	const row = data?.currentAccount?.adsSpace?.campaign;
10876	const campaign = (0, import_react.useMemo)(() => row ? toDetail(row) : null, [row]);
10877	const recheckPlacements = async () => {
10878		return (await refetch()).data?.currentAccount?.adsSpace?.campaign?.mappedPlacementCount ?? 0;
10879	};
10880	return {
10881		campaign,
10882		loading,
10883		error: Boolean(error),
10884		recheckPlacements
10885	};
10886};
10887//#endregion
10888//#region client/components/newNav/components/ads/campaigns/CampaignDetailPage.tsx
10889/** Impression goals run to eight digits; unseparated they are unreadable. */
10890var countCell = (key) => (row) => {
10891	const value = Number(asText(row[key]));
10892	return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
10893		variant: "body1",
10894		children: value > 0 ? value.toLocaleString() : "-"
10895	});
10896};
10897/** The aspect ratios a creative supports, readable from the list without opening a row. */
10898var ASPECT_RATIOS = [
10899	["supportHorizontalAspectRatio", "16:9"],
10900	["supportSquareAspectRatio", "1:1"],
10901	["supportVerticalAspectRatio", "9:16"]
10902];
10903var aspectRatioCell = (row) => {
10904	const on = ASPECT_RATIOS.filter(([key]) => row[key]).map(([, label]) => label);
10905	return on.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
10906		variant: "body1",
10907		color: "text.secondary",
10908		children: "none"
10909	}) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Box, {
10910		sx: chipRowSx,
10911		children: on.map((label) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.Label, {
10912			size: "small",
10913			variant: "standard",
10914			color: "info",
10915			label
10916		}, label))
10917	});
10918};
10919/**
10920* Enabled first, then the window that runs latest, then the newest model. Sorting purely by model id puts a
10921* disabled model at the top of the list, which is the trap.
10922*/
10923var sortModelMappings = (rows) => [...rows].sort((left, right) => {
10924	if (Boolean(left.enabled) !== Boolean(right.enabled)) return left.enabled ? -1 : 1;
10925	const byWindow = asText(right.enabledTo).localeCompare(asText(left.enabledTo));
10926	return byWindow !== 0 ? byWindow : Number(right.predictionModelId) - Number(left.predictionModelId);
10927});
10928var sumRows = (rows, key) => rows.reduce((total, row) => total + (Number(asText(row[key])) || 0), 0);
10929var CampaignDetailPage = () => {
10930	const { campaignId } = useParams();
10931	const { campaign, loading, recheckPlacements } = useAdsSpaceCampaign(campaignId);
10932	if (loading && !campaign) return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
10933		sx: adsPageSx,
10934		children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsPageHeader, {
10935			area: "Programmatic / Campaigns",
10936			title: "Campaign"
10937		}
10937), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Skeleton, {
10938			variant: "rounded",
10939			height: 320
10940		})]
10941	});
10942	if (!campaign) return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
10943		sx: adsPageSx,
10944		children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsPageHeader, {
10945			area: "Programmatic / Campaigns",
10946			title: "Campaign"
10947		}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsEmptyState, {
10948			icon: import_faCircleInfo.faCircleInfo,
10949			heading: "This campaign no longer exists",
10950			subtitle: "It may have been removed since this page was opened."
10951		})]
10952	});
10953	return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CampaignEditor, {
10954		campaign,
10955		onRecheckPlacements: recheckPlacements
10956	});
10957};
10958/**
10959* The whole campaign is one editing session: a single form backs every tab, so switching tabs keeps the edits
10960* and does not confirm; one Save persists every tab together with one change comment. The discard confirmation
10961* fires only when the user leaves this campaign - `navigationScope` lets same-campaign tab routes pass through.
10962*/
10963var CampaignEditor = ({ campaign, onRecheckPlacements }) => {
10964	const accountId = useActiveAccountId();
10965	const { userRoles } = useUserData();
10966	const canEdit = isPermitted(userRoles, permissions.adsEditCampaignSettings);
10967	const isDecoupled = campaign.mode === GqlClientAdsSpaceCampaignMode.DECOUPLED;
10968	const flightRows = campaign.childRows[GqlClientAdsSpaceCampaignChildTable.FLIGHTS];
10969	const impressionsGoal = sumRows(flightRows, "totalImpressions");
10970	const geoOptions = toEntityOptions(useAdsSpaceEntities(GqlClientAdsSpaceEntityKind.GEO_FILTER).rows);
10971	const audienceOptions = toEntityOptions(useAdsSpaceEntities(GqlClientAdsSpaceEntityKind.AUDIENCE).rows);
10972	const videoResponseOptions = toEntityOptions(useAdsSpaceEntities(GqlClientAdsSpaceEntityKind.VIDEO_RESPONSE).rows);
10973	const experimentOptions = campaign.experiments.map((experiment) => ({
10974		value: experiment.id,
10975		label: experiment.name
10976	}));
10977	const experimentGroupOptions = (campaign.experiments.find((experiment) => experiment.id === asText(campaign.values.optimizationExperimentId))?.groups ?? []).map((group) => ({
10978		value: group.id,
10979		label: `${group.name} (${group.min}-${group.max})`
10980	}));
10981	const isFreshCopy = Boolean(useLocation().state?.isFreshCopy);
10982	const savedTargetingRuntime = targetingRuntime({
10983		locationFilter: asText(campaign.values.locationFilter),
10984		hasInlineAudiences: campaign.inlineAudiences.length > 0,
10985		accountGeoCount: campaign.childRows[GqlClientAdsSpaceCampaignChildTable.PROGRAM_GEO_FILTERS].length,
10986		accountAudienceCount: campaign.childRows[GqlClientAdsSpaceCampaignChildTable.PROGRAM_AUDIENCES].length
10987	});
10988	const dashboardSectionDef = {
10989		id: ADS_CAMPAIGN_DASHBOARD_SECTION_ID,
10990		label: "Dashboard",
10991		icon: import_faChartMixed.faChartMixed,
10992		groups: [],
10993		childTables: [],
10994		renderInstead: () => isDecoupled ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(DecoupledDashboard, { campaignId: campaign.id }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CampaignDashboard, { campaignId: campaign.id })
10995	};
10996	const programmaticSectionDefs = [
10997		dashboardSectionDef,
10998		{
10999			id: "overview",
11000			label: "Overview",
11001			icon: import_faCircleInfo.faCircleInfo,
11002			groups: buildOverviewGroups(),
11003			childTables: [],
11004			renderHeader: () => isFreshCopy ? freshCopyNotice : null
11005		},
11006		{
11007			id: "targeting",
11008			label: "Targeting",
11009			icon: import_faBullseye.faBullseye,
11010			groups: buildTargetingGroups(/* @__PURE__ */ (0, import_jsx_runtime.jsx)(InlineAudiencesPanel, {
11011				audiences: campaign.inlineAudiences,
11012				readOnly: !canEdit
11013			}), isLegacyTargetingInForce(savedTargetingRuntime), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(LegacyTargetingBadge, { runtime: savedTargetingRuntime })),
11014			childTables: [
11015				GqlClientAdsSpaceCampaignChildTable.PROGRAM_GEO_FILTERS,
11016				GqlClientAdsSpaceCampaignChildTable.PROGRAM_AUDIENCES,
11017				GqlClientAdsSpaceCampaignChildTable.PRODUCT_SELECTORS
11018			],
11019			renderHeader: (form) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CampaignEntityTargeting, {
11020				form,
11021				geoOptions,
11022				audienceOptions,
11023				hasInlineAudiences: campaign.inlineAudiences.length > 0,
11024				campaignName: campaign.name,
11025				part: isLegacyTargetingInForce(savedTargetingRuntime) ? "box" : "all",
11026				readOnly: !canEdit
11027			}),
11028			renderChildren: (form) =>
11028 isLegacyTargetingInForce(savedTargetingRuntime) ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CampaignEntityTargeting, {
11029				form,
11030				geoOptions,
11031				audienceOptions,
11032				hasInlineAudiences: campaign.inlineAudiences.length > 0,
11033				campaignName: campaign.name,
11034				part: "tables",
11035				readOnly: !canEdit
11036			}) : null
11037		},
11038		{
11039			id: "flights",
11040			label: "Flights and budget",
11041			icon: import_faCalendarDays.faCalendarDays,
11042			groups: [],
11043			childTables: [GqlClientAdsSpaceCampaignChildTable.FLIGHTS, GqlClientAdsSpaceCampaignChildTable.BUDGET_LANES],
11044			renderChildren: (form) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
11045				sx: stackSx,
11046				children: [
11047					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsChildTable, {
11048						title: "Flights",
11049						description: "Booked delivery windows. Flights of one campaign may not overlap.",
11050						fields: flightFields,
11051						columns: [
11052							{
11053								key: "startDate",
11054								label: "Start"
11055							},
11056							{
11057								key: "endDate",
11058								label: "End"
11059							},
11060							{
11061								key: "totalImpressions",
11062								label: "Impressions",
11063								align: "right",
11064								render: countCell("totalImpressions")
11065							},
11066							{
11067								key: "totalCompletedViews",
11068								label: "Completed views",
11069								align: "right",
11070								render: countCell("totalCompletedViews")
11071							},
11072							{
11073								key: "customerCpm",
11074								label: "Customer CPM",
11075								align: "right"
11076							},
11077							{
11078								key: "margin",
11079								label: "Margin",
11080								align: "right"
11081							},
11082							{
11083								key: "insertionOrder",
11084								label: "Insertion order"
11085							}
11086						],
11087						rows: form.childRows(GqlClientAdsSpaceCampaignChildTable.FLIGHTS),
11088						addLabel: "Add flight",
11089						emptyText: "No flights booked yet",
11090						rowTitle: (row) => `${asText(row.startDate)} - ${asText(row.endDate)}`,
11091						readOnly: !canEdit,
11092						validateRow: validateFlightRow,
11093						onChange: (rows) => form.onChildRowsChange(GqlClientAdsSpaceCampaignChildTable.FLIGHTS, rows),
11094						trackingIdPrefix: "tracking-id-ads-campaign-flights"
11095					}),
11096					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Divider, {}),
11097					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(BudgetLanesSection, {
11098						lanes: form.childRows(GqlClientAdsSpaceCampaignChildTable.BUDGET_LANES),
11099						impressionsGoal: sumRows(form.childRows(GqlClientAdsSpaceCampaignChildTable.FLIGHTS), "totalImpressions"),
11100						customerCpm: weightedCampaignCpm(form.childRows(GqlClientAdsSpaceCampaignChildTable.FLIGHTS)),
11101						inheritedMargin: inheritedMargin(form.childRows(GqlClientAdsSpaceCampaignChildTable.FLIGHTS)),
11102						geoOptions,
11103						readOnly: !canEdit,
11104						onChange: (lanes) => form.onChildRowsChange(GqlClientAdsSpaceCampaignChildTable.BUDGET_LANES, lanes)
11105					})
11106				]
11107			})
11108		},
11109		{
11110			id: "creatives",
11111			label: "Creatives",
11112			icon: import_faFilm.faFilm,
11113			groups: [],
11114			childTables: [GqlClientAdsSpaceCampaignChildTable.CREATIVES],
11115			renderChildren: (form) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsChildTable, {
11116				title: "Creative definitions",
11117				description: "One row per story. A campaign with several stories has several rows and they all apply; the ad server picks by rule.",
11118				fields: creativeFields,
11119				columns: [
11120					{
11121						key: "story",
11122						label: "Story"
11123					},
11124					{
11125						key: "numSkus",
11126						label: "SKUs",
11127						align: "right"
11128					},
11129					{
11130						key: "allowedDurations",
11131						label: "Durations"
11132					},
11133					{
11134						key: "videoQualityProfile",
11135						label: "Quality"
11136					},
11137					{
11138						key: "videoParameters",
11139						label: "Video parameters"
11140					},
11141					{
11142						key: "aspectRatios",
11143						label: "Aspect ratios",
11144						render: aspectRatioCell
11145					}
11146				],
11147				rows: form.childRows(GqlClientAdsSpaceCampaignChildTable.CREATIVES),
11148				addLabel: "Add creative",
11149				emptyText: "No creatives defined yet",
11150				rowTitle: (row) => asText(row.story),
11151				readOnly: !canEdit,
11152				validateRow: validateCreativeRow,
11153				onChange: (rows) => form.onChildRowsChange(GqlClientAdsSpaceCampaignChildTable.CREATIVES, rows),
11154				trackingIdPrefix: "tracking-id-ads-campaign-creatives"
11155			})
11156		},
11157		{
11158			id: "delivery",
11159			label: "Delivery",
11160			icon: import_faDesktop.faDesktop,
11161			groups: buildDeliveryGroups(),
11162			childTables: []
11163		},
11164		{
11165			id: "filters",
11166			label: "Filters",
11167			icon: import_faFilter.faFilter,
11168			groups: buildFilterGroups(),
11169			childTables: []
11170		},
11171		{
11172			id: "tracking",
11173			label: "Tracking and reporting",
11174			icon: import_faTag.faTag,
11175			groups: buildTrackingGroups(),
11176			childTables: [GqlClientAdsSpaceCampaignChildTable.REPORTING_DEFINITIONS],
11177			renderHeader: (form) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsChildTable, {
11178				title: "Reporting definitions",
11179				description: "Extra fields carried into reporting. Which Field values are offered depends on the Source.",
11180				fields: buildReportingFields(""),
11181				rowFieldsFor: (values) => buildReportingFields(asText(values.source)),
11182				rows: form.childRows(GqlClientAdsSpaceCampaignChildTable.REPORTING_DEFINITIONS),
11183				addLabel: "Add definition",
11184				emptyText: "No reporting definitions yet",
11185				rowTitle: (row) => asText(row.name),
11186				readOnly: !canEdit,
11187				onChange: (rows) => form.onChildRowsChange(GqlClientAdsSpaceCampaignChildTable.REPORTING_DEFINITIONS, rows),
11188				trackingIdPrefix: "tracking-id-ads-campaign-reporting"
11189			}),
11190			renderChildren: (form) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CampaignPixelRulesPanel, {
11191				campaignId: campaign.id,
11192				campaignName: campaign.name,
11193				location: asText(form.values.pixelRulesLocation),
11194				updatedAt: campaign.updatedAt,
11195				laneNames: form.childRows(GqlClientAdsSpaceCampaignChildTable.BUDGET_LANES).map((lane) 
11195=> asText(lane.name)).filter(Boolean),
11196				mappedPlacementCount: campaign.mappedPlacementCount,
11197				onRecheck: onRecheckPlacements,
11198				onSaved: (location) => form.onChange("pixelRulesLocation", location),
11199				readOnly: !canEdit
11200			})
11201		},
11202		{
11203			id: "optimization",
11204			label: "Optimization",
11205			icon: import_faBrain.faBrain,
11206			groups: [...buildOptimizationGroups(campaign.predictionModelOptions, experimentOptions, experimentGroupOptions), ...buildControlGroupGroups(videoResponseOptions)],
11207			childTables: [GqlClientAdsSpaceCampaignChildTable.MODEL_MAPPINGS],
11208			renderHeader: (form) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsChildTable, {
11209				title: "Prediction model mappings",
11210				description: "Which model runs for this campaign, and when. Retiring one model and opening a window for the next is the usual edit.",
11211				fields: modelMappingFields(campaign.predictionModelOptions),
11212				rows: sortModelMappings(form.childRows(GqlClientAdsSpaceCampaignChildTable.MODEL_MAPPINGS)),
11213				addLabel: "Add mapping",
11214				emptyText: "No models mapped to this campaign",
11215				rowTitle: (row) => `Model ${asText(row.predictionModelId)}`,
11216				readOnly: !canEdit,
11217				onChange: (rows) => form.onChildRowsChange(GqlClientAdsSpaceCampaignChildTable.MODEL_MAPPINGS, rows),
11218				trackingIdPrefix: "tracking-id-ads-campaign-model-mappings"
11219			})
11220		},
11221		{
11222			id: "advanced",
11223			label: "Advanced",
11224			icon: import_faSliders.faSliders,
11225			groups: buildPacingGroups(),
11226			childTables: [
11227				GqlClientAdsSpaceCampaignChildTable.CAMPAIGN_PROPERTIES,
11228				GqlClientAdsSpaceCampaignChildTable.EXCHANGE_CAPS,
11229				GqlClientAdsSpaceCampaignChildTable.GOAL_ADJUSTMENTS
11230			],
11231			renderChildren: (form) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
11232				sx: stackSx,
11233				children: [
11234					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsChildTable, {
11235						title: "Advanced properties",
11236						description: "A closed key set. A key the ad server does not read is ignored without a word, which is why this is a picker.",
11237						fields: campaignPropertyFields,
11238						rows: form.childRows(GqlClientAdsSpaceCampaignChildTable.CAMPAIGN_PROPERTIES),
11239						addLabel: "Add property",
11240						emptyText: "No advanced properties set",
11241						rowTitle: (row) => asText(row.propKey),
11242						readOnly: !canEdit,
11243						onChange: (rows) => form.onChildRowsChange(GqlClientAdsSpaceCampaignChildTable.CAMPAIGN_PROPERTIES, rows),
11244						trackingIdPrefix: "tracking-id-ads-campaign-properties"
11245					}),
11246					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsChildTable, {
11247						title: "Exchange caps",
11248						description: "No row means the exchange is uncapped. A cap of 0 blocks it outright.",
11249						fields: exchangeCapFields(exchangeOptions),
11250						rows: form.childRows(GqlClientAdsSpaceCampaignChildTable.EXCHANGE_CAPS),
11251						addLabel: "Add cap",
11252						emptyText: "Every exchange is uncapped",
11253						rowTitle: (row) => exchangeOptions.find((option) => option.value === asText(row.exchangePrefix))?.label ?? "Exchange",
11254						readOnly: !canEdit,
11255						onChange: (rows) => form.onChildRowsChange(GqlClientAdsSpaceCampaignChildTable.EXCHANGE_CAPS, rows),
11256						trackingIdPrefix: "tracking-id-ads-campaign-exchange-caps"
11257					}),
11258					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsChildTable, {
11259						title: "Goal adjustments",
11260						description: "Pacing tuning, one row per adjustment type. A handful of campaigns use this.",
11261						fields: goalAdjustmentFields,
11262						columns: [
11263							{
11264								key: "type",
11265								label: "Type",
11266								render: (row) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
11267									variant: "body1",
11268									children: goalAdjustmentTypeOptions.find((option) => option.value === asText(row.type))?.label ?? "-"
11269								})
11270							},
11271							{
11272								key: "base",
11273								label: "Base",
11274								align: "right"
11275							},
11276							{
11277								key: "changerate",
11278								label: "Change rate",
11279								align: "right"
11280							},
11281							{
11282								key: "minValue",
11283								label: "Min",
11284								align: "right"
11285							},
11286							{
11287								key: "maxValue",
11288								label: "Max",
11289								align: "right"
11290							},
11291							{
11292								key: "overrideValue",
11293								label: "Override",
11294								align: "right"
11295							}
11296						],
11297						rows: form.childRows(GqlClientAdsSpaceCampaignChildTable.GOAL_ADJUSTMENTS),
11298						addLabel: "Add adjustment",
11299						emptyText: "No goal adjustments on this campaign",
11300						rowTitle: (row) => goalAdjustmentTypeOptions.find((option) => option.value === asText(row.type))?.label ?? "Adjustment",
11301						readOnly: !canEdit,
11302						validateRow: validateGoalAdjustmentRow,
11303						onChange: (rows) => form.onChildRowsChange(GqlClientAdsSpaceCampaignChildTable.GOAL_ADJUSTMENTS, rows),
11304						trackingIdPrefix: "tracking-id-ads-campaign-goal-adjustments"
11305					})
11306				]
11307			})
11308		}
11309	];
11310	const decoupledSectionDefs = [
11311		dashboardSectionDef,
11312		{
11313			id: "overview",
11314			label: "Identity",
11315			icon: import_faCircleInfo.faCircleInfo,
11316			groups: buildDecoupledIdentityGroups(),
11317			childTables: []
11318		},
11319		{
11320			id: "dsp-delivery",
11321			label: "DSP delivery",
11322			icon: import_faPlug.faPlug,
11323			groups: [],
11324			childTables: [],
11325			renderChildren: () => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(DecoupledDeliveryPanel, { campaignName: campaign.name })
11326		},
11327		{
11328			id: "fallback",
11329			label: "Fallback video",
11330			icon: import_faFilm.faFilm,
11331			groups: buildDecoupledFallbackGroups(),
11332			childTables: []
11333		},
11334		{
11335			id: "creatives",
11336			label: "Creative",
11337			icon: import_faFilm.faFilm,
11338			groups: [],
11339			childTables: [GqlClientAdsSpaceCampaignChildTable.CREATIVES],
11340			renderChildren: (form) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsChildTable, {
11341				title: "Creative definitions",
11342				description: "What maps this campaign to its videos. One row per story.",
11343				fields: creativeFields,
11344				columns: [
11345					{
11346						key: "story",
11347						label: "Story"
11348					},
11349					{
11350						key: "numSkus",
11351						label: "SKUs",
11352						align: "right"
11353					},
11354					{
11355						key: "allowedDurations",
11356						label: "Durations"
11357					},
11358					{
11359						key: "videoQualityProfile",
11360						label: "Quality"
11361					},
11362					{
11363						key: "videoParameters",
11364						label: "Video parameters"
11365					}
11366				],
11367				rows: form.childRows(GqlClientAdsSpaceCampaignChildTable.CREATIVES),
11368				addLabel: "Add creative",
11369				emptyText: "No creatives defined yet",
11370				rowTitle: (row) => asText(row.story),
11371				readOnly: !canEdit,
11372				onChange: (rows) => form.onChildRowsChange(GqlClientAdsSpaceCampaignChildTable.CREATIVES, rows),
11373				trackingIdPrefix: "tracking-id-ads-decoupled-creatives"
11374			})
11375		},
11376		{
11377			id: "reporting",
11378			label: "Reporting definitions",
11379			icon: import_faChartMixed.faChartMixed,
11380			groups: [],
11381			childTables: [GqlClientAdsSpaceCampaignChildTable.REPORTING_DEFINITIONS],
11382			renderChildren: (form) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsChildTable, {
11383				title: "Reporting definitions",
11384				description: "Extra fields carried into reporting. Which Field values are offered depends on the Source.",
11385				fields: buildReportingFields(""),
11386				rowFieldsFor: (values) => buildReportingFields(asText(values.source)),
11387				rows: form.childRows(GqlClientAdsSpaceCampaignChildTable.REPORTING_DEFINITIONS),
11388				addLabel: "Add definition",
11389				emptyText: "No reporting definitions yet",
11390				rowTitle: (row) => asText(row.name),
11391				readOnly: !canEdit,
11392				onChange: (rows) => form.onChildRowsChange(GqlClientAdsSpaceCampaignChildTable.REPORTING_DEFINITIONS, rows),
11393				trackingIdPrefix: "tracking-id-ads-decoupled-reporting"
11394			})
11395		},
11396		{
11397			id: "delivery",
11398			label: "Delivery settings",
11399			icon: import_faDesktop.faDesktop,
11400			groups: buildDecoupledDeliveryGroups(),
11401			childTables: []
11402		},
11403		{
11404			id: "control-group",
11405			label: "Control group",
11406			icon: import_faUserGroup.faUserGroup,
11407			groups: buildDecoupledControlGroupGroups(),
11408			childTables: []
11409		},
11410		{
11411			id: "advanced",
11412			label: "Advanced properties",
11413			icon: import_faSliders.faSliders,
11414			groups: [],
11415			childTables: [GqlClientAdsSpaceCampaignChildTable.CAMPAIGN_PROPERTIES],
11416			renderChildren: (form) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsChildTable, {
11417				title: "Advanced properties",
11418				description: "A closed key set. A key the ad server does not read is ignored without a word, which is why this is a picker.",
11419				fields: campaignPropertyFields,
11420				rows: form.childRows(GqlClientAdsSpaceCampaignChildTable.CAMPAIGN_PROPERTIES),
11421				addLabel: "Add property",
11422				emptyText: "No advanced properties set",
11423				rowTitle: (row) => asText(row.propKey),
11424				readOnly: !canEdit,
11425				onChange: (rows) => form.onChildRowsChange(GqlClientAdsSpaceCampaignChildTable.CAMPAIGN_PROPERTIES, rows),
11426				trackingIdPrefix: "tracking-id-ads-decoupled-properties"
11427			})
11428		}
11429	];
11430	const sectionDefs = isDecoupled ? decoupledSectionDefs : programmaticSectionDefs;
11431	const form = useAdsCampaignForm(campaign, sectionDefs);
11432	const willDisable = campaign.enabled && form.values.enabled === false;
11433	const saveWarning = form.values.name !== void 0 && asText(form.values.name) !== campaign.name && isDecoupled ? "The DSP's live tag carries this name. Renaming changes the tag they must call, and the campaign's fallback-video settings are re-keyed to the new name in the same save." : willDisable ? "This campaign stops serving as soon as the ad servers pick the change up." : void 0;
11434	const lanesProblem = isDecoupled ? null : getLanesError(form.childRows(GqlClientAdsSpaceCampaignChildTable.BUDGET_LANES));
11435	const isWithinCampaign = (location) => {
11436		const base = isDecoupled ? (0, import_dist$1.newNavAdsDecoupledCampaignPath)({
11437			accountId,
11438			campaignId: campaign.id
11439		}) : (0, import_dist$1.newNavAdsCampaignPath)({
11440			accountId,
11441			campaignId: campaign.id
11442		});
11443		return location.pathname === base || location.pathname.startsWith(`${base}/`);
11444	};
11445	const sections = sectionDefs.map((def) => ({
11446		id: def.id,
11447		label: def.label,
11448		icon: def.icon,
11449		render: () => def.renderInstead?.() ?? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsSettingsForm, {
11450			groups: def.groups,
11451			form,
11452			readOnly: !canEdit,
11453			navigationScope: isWithinCampaign,
11454			invalidLabels: form.invalidLabels,
11455			saveBlockedReason: lanesProblem ?? void 0,
11456			saveWarning,
11457			headerChildren: def.renderHeader?.(form),
11458			trackingIdPrefix: "tracking-id-ads-campaign",
11459			children: def.renderChildren?.(form)
11460		})
11461	}));
11462	const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
11463	const { studioPrograms } = useAdsSpaceCampaigns();
11464	const { move, saving } = useAdsSpaceCampaignActions();
11465	const [isMoveOpen, setIsMoveOpen] = (0, import_react.useState)(false);
11466	const status = campaignStatusDisplay[deriveCampaignStatus(campaign.enabled, flightRows.map((row) => ({
11467		startDate: asText(row.startDate),
11468		endDate: asText(row.endDate)
11469	})), today)];
11470	return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsSectionPage, {
11471		area: isDecoupled ? "Decoupled / Decoupled campaigns" : "Programmatic / Campaigns",
11472		title: campaign.name,
11473		subtitle: isDecoupled ? "A partner owns this buy and calls us for the creative" : `${campaign.insertionOrder || "No insertion order"} - ${formatCompactNumber(impressionsGoal)} impressions booked`,
11474		sections,
11475		sectionPath: (sectionId) => isDecoupled ? (0, import_dist$1.newNavAdsDecoupledCampaignSectionPath)({
11476			accountId,
11477			campaignId: campaign.id,
11478			sectionId
11479		}) : (0, import_dist$1.newNavAdsCampaignSectionPath)({
11480			accountId,
11481			campaignId: campaign.id,
11482			sectionId
11483		}),
11484		backLink: isDecoupled ? {
11485			label: "Back to decoupled campaigns",
11486			to: (0, import_dist$1.newNavAdsDecoupledCampaignsPath)({ accountId })
11487		} : {
11488			label: "Back to campaigns",
11489			to: (0, import_dist$1.newNavAdsCampaignsPath)({ accountId })
11490		},
11491		headerActions: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
11492			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.Label, {
11493				size: "medium",
11494				label: status.label,
11495				color: status.color,
11496				variant: status.variant
11497			}),
11498			!isDecoupled && canEdit && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Tooltip, {
11499				title: campaign.enabled ? "Disable the campaign before moving it - the move rewrites which program its delivery reports into." : "",
11500				children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Button, {
11501					variant: "outlined",
11502					size: "small",
11503					disabled: campaign.enabled,
11504					onClick: () => setIsMoveOpen(true),
11505					"data-testid": "ads-campaign-move-btn",
11506					"data-tracking-id": "tracking-id-ads-campaign-move-btn",
11507					children: "Move to another Studio program"
11508				}) })
11509			}),
11510			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(MoveCampaignDialog, {
11511				campaign: isMoveOpen ? {
11512					id: campaign.id,
11513					salesforceId: campaign.salesforceId
11514				} : null,
11515				studioPrograms,
11516				saving,
11517				onClose: () => setIsMoveOpen(false),
11518				onMove: async (input) => {
11519					const done = await move(input);
11520					if (done) setIsMoveOpen(false);
11521					return done;
11522				}
11523			})
11524		] }),
11525		historyCampaignName: campaign.name,
11526		trackingIdPrefix: "tracking-id-ads-campaign"
11527	});
11528};
11529var freshCopyNotice = /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_dist$2.AttentionBox, {
11530	color: "info",
11531	"data-testid": "testId-ads-campaign-fresh-copy",
11532	children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.AttentionBoxTitle, { children: "This is a copy, and it is not serving yet" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.AttentionBoxContent, { children: "It is disabled, and it has no pixel rules and no buyer creative id - both have to be unique per campaign. Set them, then enable it." })]
11533});
11534var stackSx = {
11535	display: "flex",
11536	flexDirection: "column",
11537	gap: 4
11538};
11539var chipRowSx = {
11540	display: "flex",
11541	gap: .5,
11542	flexWrap: "wrap"
11543};
11544//#endregion
11545//#region client/components/newNav/components/ads/campaigns/CampaignsSummaryStrip.tsx
11546var import_faBullhorn = require_faBullhorn();
11547/**
11548* Summarizes the whole account, not the filtered table below it - filtering the table should not silently
11549* change the account totals someone is reading this for. One tile only, per D-10: everything else that used
11550* to be here needed a delivery feed the hub does not have.
11551*/
11552var CampaignsSummaryStrip = ({ campaigns }) => {
11553	const summary = summarizeCampaigns(campaigns);
11554	return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Box, {
11555		sx: stripSx,
11556		children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Paper, {
11557			variant: "outlined",
11558			sx: (0, import_dist$2.combineSxProps)(adsSurfaceSx, tileSx),
11559			children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Box, {
11560				sx: adsIconTileSx(32),
11561				children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FontAwesomeIcon, { icon: import_faBullseye.faBullseye })
11562			}), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
11563				sx: tileBodySx,
11564				children: [
11565					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
11566						variant: "caption",
11567						color: "text.secondary",
11568						noWrap: true,
11569						children: "Active campaigns"
11570					}),
11571					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
11572						variant: "h2",
11573						noWrap: true,
11574						children: `${summary.activeCount}`
11575					}),
11576					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
11577						variant: "caption",
11578						color: "text.secondary",
11579						noWrap: true,
11580						children: `of ${summary.totalCount} in this account`
11581					})
11582				]
11583			})]
11584		})
11585	});
11586};
11587var stripSx = { display: "flex" };
11588var tileSx = { minWidth: 220 };
11589var tileBodySx = {
11590	minWidth: 0,
11591	display: "flex",
11592	flexDirection: "column",
11593	gap: "2px"
11594};
11595//#endregion
11596//#region node_modules/@fortawesome/pro-regular-svg-icons/faPause.js
11597var require_faPause = /* @__PURE__ */ __commonJSMin(((exports) => {
11598	Object.defineProperty(exports, "__esModule", { value: true });
11599	var prefix = "far";
11600	var iconName = "pause";
11601	var width = 384;
11602	var height = 512;
11603	var aliases = [9208];
11604	var unicode = "f04c";
11605	var svgPathData = "M48 80l0 352 64 0 0-352-64 0zM0 80C0 53.5 21.5 32 48 32l64 0c26.5 0 48 21.5 48 48l0 352c0 26.5-21.5 48-48 48l-64 0c-26.5 0-48-21.5-48-48L0 80zm272 0l0 352 64 0 0-352-64 0zm-48 0c0-26.5 21.5-48 48-48l64 0c26.5 0 48 21.5 48 48l0 352c0 26.5-21.5 48-48 48l-64 0c-26.5 0-48-21.5-48-48l0-352z";
11606	exports.definition = {
11607		prefix,
11608		iconName,
11609		icon: [
11610			width,
11611			height,
11612			aliases,
11613			unicode,
11614			svgPathData
11615		]
11616	};
11617	exports.faPause = exports.definition;
11618	exports.prefix = prefix;
11619	exports.iconName = iconName;
11620	exports.width = width;
11621	exports.height = height;
11622	exports.ligatures = aliases;
11623	exports.unicode = unicode;
11624	exports.svgPathData = svgPathData;
11625	exports.aliases = aliases;
11626}));
11627//#endregion
11628//#region client/components/newNav/components/ads/campaigns/CampaignsTable.tsx
11629var import_faArrowRightArrowLeft = require_faArrowRightArrowLeft();
11630var import_faPause = require_faPause();
11631var import_faPlay = require_faPlay();
11632var columns = [
11633	{
11634		key: "name",
11635		label: "Campaign"
11636	},
11637	{
11638		key: "status",
11639		label: "Status"
11640	},
11641	{
11642		key: "flight",
11643		label: "Flight"
11644	},
11645	{
11646		key: "budget",
11647		label: "Budget",
11648		numeric: true
11649	},
11650	{
11651		key: "impressions",
11652		label: "Impressions",
11653		numeric: true
11654	},
11655	{
11656		key: "updatedAt",
11657		label: "Updated"
11658	}
11659];
11660var CampaignsTable = ({ campaigns, accountId, canEdit, onDuplicate, onMove }) => {
11661	const history = useHistory();
11662	const [sortKey, setSortKey] = (0, import_react.useState)("updatedAt");
11663	const [sortDirection, setSortDirection] = (0, import_react.useState)("desc");
11664	const [menuAnchorEl, setMenuAnchorEl] = (0, import_react.useState)(null);
11665	const [menuCampaign, setMenuCampaign] = (0, import_react.useState)(null);
11666	const sortedCampaigns = (0, import_react.useMemo)(() => sortCampaigns(campaigns, sortKey, sortDirection), [
11667		campaigns,
11668		sortKey,
11669		sortDirection
11670	]);
11671	const handleSort = (key) => {
11672		if (key === sortKey) {
11673			setSortDirection(sortDirection === "asc" ? "desc" : "asc");
11674			return;
11675		}
11676		setSortKey(key);
11677		setSortDirection(key === "name" ? "asc" : "desc");
11678	};
11679	const closeMenu = () => {
11680		setMenuAnchorEl(null);
11681		setMenuCampaign(null);
11682	};
11683	const notYetAvailable = (action) => () => {
11684		closeMenu();
11685		setInfoNotification({ message: `${action} is not available yet` });
11686	};
11687	const openSection = (campaign, sectionId) => () => {
11688		closeMenu();
11689		history.push((0, import_dist$1.newNavAdsCampaignSectionPath)({
11690			accountId,
11691			campaignId: campaign.id,
11692			sectionId
11693		}));
11694	};
11695	const openDialog = (campaign, open) => () => {
11696		closeMenu();
11697		open(campaign);
11698	};
11699	const menuActions = (campaign) => [
11700		{
11701			label: "Edit campaign settings",
11702			icon: import_faPen.faPen,
11703			onClick: openSection(campaign, "overview")
11704		},
11705		{
11706			label: "Duplicate campaign",
11707			icon: import_faCopy.faCopy,
11708			onClick: openDialog(campaign, onDuplicate)
11709		},
11710		{
11711			label: "Move to another program",
11712			icon: import_faArrowRightArrowLeft.faArrowRightArrowLeft,
11713			onClick: openDialog(campaign, onMove)
11714		},
11715		campaign.status === CampaignStatus.PAUSED ? {
11716			label: "Resume campaign",
11717			icon: import_faPlay.faPlay,
11718			onClick: openSection(campaign, "overview"),
11719			dividerBefore: true
11720		} : {
11721			label: "Pause campaign",
11722			icon: import_faPause.faPause,
11723			onClick: openSection(campaign, "overview"),
11724			dividerBefore: true
11725		},
11726		{
11727			label: "Delete campaign",
11728			icon: import_faTrashCan.faTrashCan,
11729			onClick: notYetAvailable("Deleting a campaign"),
11730			error: true,
11731			dividerBefore: true
11732		}
11733	];
11734	return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.TableContainer, {
11735		component: import_material.Paper,
11736		variant: "outlined",
11737		sx: containerSx$1,
11738		children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Table, {
11739			size: "small",
11740			stickyHeader: true,
11741			children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableHead, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.TableRow, { children: [columns.map((column, index) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, {
11742				align: column.numeric ? "right" : "left",
11743				sortDirection: sortKey === column.key ? sortDirection : false,
11744				sx: index === 0 ? (0, import_dist$2.combineSxProps)(headCellSx, stickyLeftSx("default", true)) : headCellSx,
11745				children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableSortLabel, {
11746					active: sortKey === column.key,
11747					direction: sortKey === column.key ? sortDirection : "asc",
11748					onClick: () => handleSort(column.key),
11749					"data-tracking-id": `tracking-id-ads-campaigns-sort-${column.key}-btn`,
11750					children: column.label
11751				})
11752			}, column.key)), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, { sx: (0, import_dist$2.combineSxProps)(actionsCellSx, stickyRightSx("default", true)) })] }) }
11752), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.TableBody, { children: [sortedCampaigns.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableRow, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, {
11753				colSpan: columns.length + 1,
11754				sx: noResultsCellSx$1,
11755				children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
11756					variant: "body1",
11757					color: "text.secondary",
11758					children: "No campaigns match your filters"
11759				})
11760			}) }), sortedCampaigns.map((campaign) => {
11761				const status = campaignStatusDisplay[campaign.status];
11762				const impressionsGoal = getImpressionsGoal(campaign);
11763				return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.TableRow, {
11764					sx: rowHoverSx,
11765					"data-testid": `testId-ads-campaign-row-${campaign.id}`,
11766					children: [
11767						/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, {
11768							sx: (0, import_dist$2.combineSxProps)(bodyCellSx, stickyLeftSx("default")),
11769							children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
11770								sx: nameCellSx$2,
11771								children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Link$6, {
11772									to: (0, import_dist$1.newNavAdsCampaignSectionPath)({
11773										accountId,
11774										campaignId: campaign.id,
11775										sectionId: ADS_CAMPAIGN_DASHBOARD_SECTION_ID
11776									}),
11777									style: {
11778										textDecoration: "none",
11779										color: "inherit"
11780									},
11781									"data-tracking-id": "tracking-id-ads-campaigns-name-link",
11782									children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.TypographyWithTooltipOnOverflow, {
11783										variant: "subtitle2",
11784										multiline: true,
11785										children: campaign.name
11786									})
11787								}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
11788									variant: "caption",
11789									color: "text.secondary",
11790									noWrap: true,
11791									children: campaign.insertionOrder
11792								})]
11793							})
11794						}),
11795						/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, {
11796							sx: bodyCellSx,
11797							children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.Label, {
11798								size: "small",
11799								label: status.label,
11800								color: status.color,
11801								variant: status.variant
11802							})
11803						}),
11804						/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, {
11805							sx: bodyCellSx,
11806							children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
11807								variant: "body1",
11808								noWrap: true,
11809								children: formatFlightRange(getFlightRange(campaign))
11810							})
11811						}),
11812						/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.TableCell, {
11813							align: "right",
11814							sx: bodyCellSx,
11815							children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
11816								variant: "body1",
11817								noWrap: true,
11818								children: formatCurrency(getBookedBudget(campaign))
11819							}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
11820								variant: "caption",
11821								color: "text.secondary",
11822								children: campaign.pricingModel
11823							})]
11824						}),
11825						/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, {
11826							align: "right",
11827							sx: bodyCellSx,
11828							children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
11829								variant: "body1",
11830								noWrap: true,
11831								children: impressionsGoal > 0 ? formatCompactNumber(impressionsGoal) : "-"
11832							})
11833						}),
11834						/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, {
11835							sx: bodyCellSx,
11836							children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
11837								variant: "body1",
11838								noWrap: true,
11839								children: campaign.updatedAt ? TimeUtils.getDesignSystemReadableTimeDate(campaign.updatedAt, true) : "-"
11840							})
11841						}),
11842						/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, {
11843							sx: (0, import_dist$2.combineSxProps)(actionsCellSx, stickyRightSx("default")),
11844							children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.TruffleIconButton, {
11845								size: "small",
11846								disabled: !canEdit,
11847								onClick: (event) => {
11848									setMenuAnchorEl(event.currentTarget);
11849									setMenuCampaign(campaign);
11850								},
11851								"data-tracking-id": "tracking-id-ads-campaigns-row-options-btn",
11852								children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FontAwesomeIcon, { icon: import_faEllipsisV.faEllipsisV })
11853							})
11854						})
11855					]
11856				}, campaign.id);
11857			})] })]
11858		}
11858), menuCampaign && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(RowOptionsMenu, {
11859			anchorEl: menuAnchorEl,
11860			open: Boolean(menuAnchorEl),
11861			onClose: closeMenu,
11862			title: menuCampaign.name,
11863			captions: [menuCampaign.insertionOrder ? `Insertion order: ${menuCampaign.insertionOrder}` : "No insertion order", menuCampaign.salesforceId ? `Salesforce id: ${menuCampaign.salesforceId}` : "Not linked to Salesforce"],
11864			actions: menuActions(menuCampaign),
11865			anchorOrigin: {
11866				vertical: "bottom",
11867				horizontal: "right"
11868			},
11869			transformOrigin: {
11870				vertical: "top",
11871				horizontal: "right"
11872			}
11873		})]
11874	});
11875};
11876var containerSx$1 = {
11877	flex: 1,
11878	minHeight: 0
11879};
11880var nameCellSx$2 = {
11881	display: "flex",
11882	flexDirection: "column",
11883	maxWidth: 280
11884};
11885var noResultsCellSx$1 = {
11886	padding: "32px 16px",
11887	textAlign: "center"
11888};
11889//#endregion
11890//#region client/components/newNav/components/ads/campaigns/DuplicateCampaignDialog.tsx
11891var DUPLICATE_NAME_SUFFIX = "_copy";
11892var DUPLICATE_NAME_ERROR$1 = "A campaign with this name already exists.";
11893/** The copy keeps the flights, creatives, reporting and targeting, and comes up disabled. */
11894var DuplicateCampaignDialog = (props) => {
11895	const [name, setName] = (0, import_react.useState)("");
11896	const [comment, setComment] = (0, import_react.useState)("");
11897	(0, import_react.useEffect)(() => {
11898		setName(props.campaign ? `${props.campaign.name}${DUPLICATE_NAME_SUFFIX}` : "");
11899		setComment("");
11900	}, [props.campaign]);
11901	const trimmedName = name.trim();
11902	const isNameTaken = trimmedName.length > 0 && props.existingCampaignNames.some((existing) => existing.trim().toLowerCase() === trimmedName.toLowerCase());
11903	const canSave = trimmedName.length > 0 && !isNameTaken && comment.trim().length > 0 && !props.saving;
11904	const handleDuplicate = async () => {
11905		if (!canSave) return;
11906		if (await props.onDuplicate({
11907			campaignId: props.campaign.id,
11908			name: trimmedName,
11909			changeComment: comment.trim()
11910		})) props.onClose();
11911	};
11912	return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Dialog, {
11913		open: Boolean(props.campaign),
11914		onClose: props.saving ? void 0 : props.onClose,
11915		maxWidth: false,
11916		PaperProps: { sx: dialogPaperSx$1 },
11917		children: [
11918			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.TruffleDialogTitle, {
11919				CloseIconButtonProps: {
11920					onClick: props.onClose,
11921					disabled: props.saving,
11922					"data-tracking-id": "tracking-id-ads-duplicate-campaign-close-btn"
11923				},
11924				children: "Duplicate campaign"
11925			}),
11926			/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.DialogContent, {
11927				sx: contentSx$7,
11928				children: [
11929					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TextField, {
11930						label: "Name of the copy",
11931						fullWidth: true,
11932						value: name,
11933						onChange: (event) => setName(event.target.value),
11934						error: isNameTaken,
11935						helperText: isNameTaken ? DUPLICATE_NAME_ERROR$1 : void 0,
11936						inputProps: { "data-tracking-id": "tracking-id-ads-duplicate-campaign-name-input" }
11937					}),
11938					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsChangeCommentField, {
11939						value: comment,
11940						onChange: setComment,
11941						trackingId: "tracking-id-ads-duplicate-campaign-comment-input"
11942					}),
11943					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.AttentionBox, {
11944						color: "info",
11945						children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.AttentionBoxContent, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
11946							variant: "body1",
11947							color: "text.secondary",
11948							children: "The copy carries this campaign's flights, creatives, reporting definitions, exch
11948ange caps and targeting. It comes up disabled, without pixel rules and without a buyer creative id - both have to be unique per campaign."
11949						}) })
11950					})
11951				]
11952			}),
11953			/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_dist$2.TruffleDialogActions, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Button, {
11954				variant: "text",
11955				size: "large",
11956				disabled: props.saving,
11957				onClick: props.onClose,
11958				"data-tracking-id": "tracking-id-ads-duplicate-campaign-cancel-btn",
11959				children: "Cancel"
11960			}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Button, {
11961				variant: "contained",
11962				size: "large",
11963				loading: props.saving,
11964				disabled: !canSave,
11965				onClick: () => void handleDuplicate(),
11966				"data-tracking-id": "tracking-id-ads-duplicate-campaign-submit-btn",
11967				children: "Duplicate"
11968			})] })
11969		]
11970	});
11971};
11972var dialogPaperSx$1 = {
11973	width: 540,
11974	maxWidth: "calc(100vw - 64px)"
11975};
11976var contentSx$7 = {
11977	display: "flex",
11978	flexDirection: "column",
11979	gap: 2
11980};
11981//#endregion
11982//#region client/components/newNav/components/ads/campaigns/NewCampaignDialog.tsx
11983var DUPLICATE_NAME_ERROR = "A campaign with this name already exists.";
11984/**
11985* Everything a campaign needs before its settings page is worth opening. The Salesforce id is deliberately not
11986* asked for - it comes from the Studio program, and the ads app's own help text calls its editable copy
11987* deprecated.
11988*/
11989var NewCampaignDialog = (props) => {
11990	const [name, setName] = (0, import_react.useState)("");
11991	const [pricingModel, setPricingModel] = (0, import_react.useState)(CampaignPricingModel.CPM);
11992	const [deviceMode, setDeviceMode] = (0, import_react.useState)(deviceModeOptions[0].value);
11993	const [comment, setComment] = (0, import_react.useState)("");
11994	const [nameError, setNameError] = (0, import_react.useState)(null);
11995	const isNameTaken = (candidate) => {
11996		const trimmed = candidate.trim();
11997		return trimmed.length > 0 && props.existingCampaignNames.some((existing) => existing.trim().toLowerCase() === trimmed.toLowerCase());
11998	};
11999	const trimmedName = name.trim();
12000	const canSave = trimmedName.length > 0 && !nameError && comment.trim().length > 0 && !props.saving;
12001	const handleClose = () => {
12002		setName("");
12003		setPricingModel(CampaignPricingModel.CPM);
12004		setDeviceMode(deviceModeOptions[0].value);
12005		setComment("");
12006		setNameError(null);
12007		props.onClose();
12008	};
12009	const handleCreate = async () => {
12010		if (!canSave) return;
12011		if (await props.onCreate({
12012			name: trimmedName,
12013			isCPCV: pricingModel === CampaignPricingModel.CPCV,
12014			deviceMode,
12015			mode: props.mode,
12016			changeComment: comment.trim()
12017		})) handleClose();
12018	};
12019	return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Dialog, {
12020		open: props.open,
12021		onClose: props.saving ? void 0 : handleClose,
12022		maxWidth: false,
12023		PaperProps: { sx: dialogPaperSx },
12024		children: [
12025			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.TruffleDialogTitle, {
12026				CloseIconButtonProps: {
12027					onClick: handleClose,
12028					disabled: props.saving,
12029					"data-tracking-id": "tracking-id-ads-new-campaign-close-btn"
12030				},
12031				children: "New campaign"
12032			}),
12033			/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.DialogContent, {
12034				sx: contentSx$6,
12035				children: [
12036					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TextField, {
12037						label: "Campaign name",
12038						fullWidth: true,
12039						value: name,
12040						onChange: (event) => {
12041							const nextName = event.target.value;
12042							setName(nextName);
12043							setNameError(isNameTaken(nextName) ? DUPLICATE_NAME_ERROR : null);
12044						},
12045						error: !!nameError,
12046						helperText: nameError ?? void 0,
12047						"data-testid": "testId-new-campaign-name-input",
12048						inputProps: { "data-tracking-id": "tracking-id-ads-new-campaign-name-input" }
12049					}),
12050					/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.FormControl, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.FormLabel, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
12051						variant: "h5",
12052						children: "Pricing model"
12053					}) }
12053), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.RadioGroup, {
12054						row: true,
12055						value: pricingModel,
12056						onChange: (event) => setPricingModel(event.target.value),
12057						children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.FormControlLabel, {
12058							value: CampaignPricingModel.CPM,
12059							control: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Radio, { "data-tracking-id": "tracking-id-ads-new-campaign-cpm-radio" }),
12060							label: "CPM - cost per thousand impressions"
12061						}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.FormControlLabel, {
12062							value: CampaignPricingModel.CPCV,
12063							control: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Radio, { "data-tracking-id": "tracking-id-ads-new-campaign-cpcv-radio" }),
12064							label: "CPCV - cost per completed view"
12065						})]
12066					})] }),
12067					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TextField, {
12068						select: true,
12069						label: "Device mode",
12070						fullWidth: true,
12071						value: deviceMode,
12072						onChange: (event) => setDeviceMode(event.target.value),
12073						inputProps: { "data-tracking-id": "tracking-id-ads-new-campaign-device-mode-input" },
12074						children: deviceModeOptions.map((option) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.MenuItem, {
12075							value: option.value,
12076							children: option.label
12077						}, option.value))
12078					}),
12079					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsChangeCommentField, {
12080						value: comment,
12081						onChange: setComment,
12082						trackingId: "tracking-id-ads-new-campaign-comment-input"
12083					}),
12084					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.AttentionBox, {
12085						color: "info",
12086						children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.AttentionBoxContent, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
12087							variant: "body1",
12088							color: "text.secondary",
12089							children: "The campaign is created disabled. Book its flights and add its creatives on the settings page before enabling it. Its Salesforce id arrives from the Studio program."
12090						}) })
12091					})
12092				]
12093			}),
12094			/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_dist$2.TruffleDialogActions, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Button, {
12095				variant: "text",
12096				size: "large",
12097				disabled: props.saving,
12098				onClick: handleClose,
12099				"data-tracking-id": "tracking-id-ads-new-campaign-cancel-btn",
12100				children: "Cancel"
12101			}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Button, {
12102				variant: "contained",
12103				size: "large",
12104				loading: props.saving,
12105				disabled: !canSave,
12106				onClick: () => void handleCreate(),
12107				"data-tracking-id": "tracking-id-ads-new-campaign-submit-btn",
12108				"data-testid": "testId-new-campaign-submit",
12109				children: "Create campaign"
12110			})] })
12111		]
12112	});
12113};
12114var dialogPaperSx = {
12115	width: 540,
12116	maxWidth: "calc(100vw - 64px)"
12117};
12118var contentSx$6 = {
12119	display: "flex",
12120	flexDirection: "column",
12121	gap: 2
12122};
12123//#endregion
12124//#region client/components/newNav/components/ads/campaigns/CampaignsPage.tsx
12125var CampaignsPage = () => {
12126	const accountId = useActiveAccountId();
12127	const history = useHistory();
12128	const { userRoles } = useUserData();
12129	const canEdit = isPermitted(userRoles, permissions.adsEditCampaignSettings);
12130	const { campaigns, studioPrograms, loading, refetching, error, refetch } = useAdsSpaceCampaigns();
12131	const actions = useAdsSpaceCampaignActions();
12132	const [searchTerm, setSearchTerm] = (0, import_react.useState)("");
12133	const [statusFilter, setStatusFilter] = (0, import_react.useState)(CampaignStatus.ACTIVE);
12134	const [filterAnchorEl, setFilterAnchorEl] = (0, import_react.useState)(null);
12135	const [isNewCampaignDialogOpen, setIsNewCampaignDialogOpen] = (0, import_react.useState)(false);
12136	const [campaignToDuplicate, setCampaignToDuplicate] = (0, import_react.useState)(null);
12137	const [campaignToMove, setCampaignToMove] = (0, import_react.useState)(null);
12138	const visibleCampaigns = (0, import_react.useMemo)(() => filterCampaigns(campaigns, searchTerm, statusFilter), [
12139		campaigns,
12140		searchTerm,
12141		statusFilter
12142	]);
12143	const handleDuplicate = async (input) => {
12144		const newCampaignId = await actions.duplicate(input);
12145		if (!newCampaignId) return false;
12146		history.push({
12147			pathname: (0, import_dist$1.newNavAdsCampaignSectionPath)({
12148				accountId,
12149				campaignId: newCampaignId,
12150				sectionId: "overview"
12151			}),
12152			state: { isFreshCopy: true }
12153		});
12154		return true;
12155	};
12156	const newCampaignButton = /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Button, {
12157		variant: "contained",
12158		size: "medium",
12159		startIcon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FontAwesomeIcon, { icon: import_faPlus.faPlus }),
12160		onClick: () => setIsNewCampaignDialogOpen(true),
12161		"data-tracking-id": "tracking-id-ads-campaigns-new-campaign-btn",
12162		children: "New campaign"
12163	});
12164	const hasCampaigns = campaigns.length > 0;
12165	return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
12166		sx: adsPageSx,
12167		children: [
12168			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsPageHeader, {
12169				area: "Programmatic / Campaigns",
12170				title: "Campaigns",
12171				count: campaigns.length,
12172				children: hasCampaigns && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
12173					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.Filter, {
12174						placeholderValue: campaignStatusDisplay[CampaignStatus.ACTIVE].label,
12175						selectedValue: statusFilter === CampaignStatus.ACTIVE ? null : statusFilter ? campaignStatusDisplay[statusFilter].label : "All statuses",
12176						onClear: () => setStatusFilter(CampaignStatus.ACTIVE),
12177						popoverTitle: "Filter by status",
12178						anchorEl: filterAnchorEl,
12179						setAnchorEl: setFilterAnchorEl,
12180						"data-tracking-id": "tracking-id-ads-campaigns-status-filter-btn",
12181						children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.List, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.ListItemButton, {
12182							dense: true,
12183							selected: statusFilter === null,
12184							onClick: () => {
12185								setStatusFilter(null);
12186								setFilterAnchorEl(null);
12187							},
12188							children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.ListItemText, { primary: "All statuses" })
12189						}), Object.values(CampaignStatus).map((status) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.ListItemButton, {
12190							dense: true,
12191							selected: statusFilter === status,
12192							onClick: () => {
12193								setStatusFilter(status);
12194								setFilterAnchorEl(null);
12195							},
12196							children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.ListItemText, { primary: campaignStatusDisplay[status].label })
12197						}, status))] })
12198					}),
12199					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Box, {
12200						sx: searchWrapperSx$3,
12201						children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.Search, {
12202							placeholder: "Search campaigns",
12203							value: searchTerm,
12204							onChange: (event) => setSearchTerm(event.target.value),
12205							onClear: () => setSearchTerm(""),
12206							numberOfResults: visibleCampaigns.length,
12207							inputProps: { "data-tracking-id": "tracking-id-ads-campaigns-search-input" }
12208						})
12209					}),
12210					canEdit && newCampaignButton
12211				] })
12212			}),
12213			error && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_dist$2.AttentionBox, {
12214				color: "error",
12215				"data-testid": "testId-ads-campaigns-error",
12216				children: [
12217					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.AttentionBoxTitle, { children: "The ads configuration is unreachable" }),
12218					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.AttentionBoxContent, { children: "This account's campaigns could not be read, so this list is not the whole picture." }),
12219					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.AttentionBoxActions, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Button, {
12220						variant: "text",
12221						size: "small",
12222						loading: refetching,
12223						onClick: () => void refetch(),
12224						"data-tracking-id": "tracking-id-ads-campaigns-retry-btn",
12225						children: "Try again"
12226					}) })
12227				]
12228			}),
12229			loading && !hasCampaigns && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsTableSkeleton, {}),
12230			!loading && !error && !hasCampaigns && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsEmptyState, {
12231				icon: import_faBullhorn.faBullhorn,
12232				heading: "No campaigns yet",
12233				subtitle: "Campaigns hold the flights, budgets and targeting that decide where this account's ads run.",
12234				children: canEdit && newCampaignButton
12235			}),
12236			hasCampaigns && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(CampaignsSummaryStrip, { campaigns }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CampaignsTable, {
12237				campaigns: visibleCampaigns,
12238				accountId,
12239				canEdit,
12240				onDuplicate: setCampaignToDuplicate,
12241				onMove: setCampaignToMove
12242			})] }),
12243			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(NewCampaignDialog, {
12244				open: isNewCampaignDialogOpen,
12245				mode: GqlClientAdsSpaceCampaignMode.PROGRAMMATIC,
12246				saving: actions.saving,
12247				onClose: () => setIsNewCampaignDialogOpen(false),
12248				existingCampaignNames: campaigns.map((campaign) => campaign.name),
12249				onCreate: actions.create
12250			}),
12251			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(DuplicateCampaignDialog, {
12252				campaign: campaignToDuplicate,
12253				saving: actions.saving,
12254				onClose: () => setCampaignToDuplicate(null),
12255				existingCampaignNames: campaigns.map((campaign) => campaign.name),
12256				onDuplicate: handleDuplicate
12257			}),
12258			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(MoveCampaignDialog, {
12259				campaign: campaignToMove,
12260				studioPrograms,
12261				saving: actions.saving,
12262				onClose: () => setCampaignToMove(null),
12263				onMove: actions.move
12264			})
12265		]
12266	});
12267};
12268var searchWrapperSx$3 = { width: 280 };
12269//#endregion
12270//#region client/components/newNav/components/ads/decoupled/DecoupledCampaignsTable.tsx
12271var import_faLinkSlash = require_faLinkSlash();
12272/**
12273* Which partner buys a decoupled campaign, and which of the three serving endpoints they call, is not recorded
12274* anywhere in the schema - so those two columns are absent rather than guessed.
12275*/
12276var DecoupledCampaignsTable = ({ campaigns, accountId }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableContainer, {
12277	component: import_material.Paper,
12278	variant: "outlined",
12279	sx: containerSx,
12280	children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Table, {
12281		size: "small",
12282		stickyHeader: true,
12283		children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableHead, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.TableRow, { children: [
12284			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, {
12285				sx: headCellSx,
12286				children: "Campaign"
12287			}),
12288			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, {
12289				sx: headCellSx,
12290				children: "Status"
12291			}),
12292			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, {
12293				align: "right",
12294				sx: headCellSx,
12295				children: "Creatives"
12296			}),
12297			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, {
12298				sx: headCellSx,
12299				children: "Updated"
12300			})
12301		] }) }
12301), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableBody, { children: campaigns.map((campaign) => {
12302			const status = campaignStatusDisplay[campaign.status];
12303			return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.TableRow, {
12304				sx: rowHoverSx,
12305				"data-testid": `testId-ads-decoupled-row-${campaign.id}`,
12306				children: [
12307					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, {
12308						sx: bodyCellSx,
12309						children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Box, {
12310							sx: nameCellSx$1,
12311							children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Link, {
12312								component: Link$6,
12313								variant: "subtitle2",
12314								underline: "hover",
12315								to: (0, import_dist$1.newNavAdsDecoupledCampaignSectionPath)({
12316									accountId,
12317									campaignId: campaign.id,
12318									sectionId: "overview"
12319								}),
12320								"data-tracking-id": "tracking-id-ads-decoupled-campaign-link",
12321								children: campaign.name
12322							})
12323						})
12324					}),
12325					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, {
12326						sx: bodyCellSx,
12327						children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.Label, {
12328							size: "small",
12329							label: status.label,
12330							color: status.color,
12331							variant: status.variant
12332						})
12333					}),
12334					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, {
12335						align: "right",
12336						sx: bodyCellSx,
12337						children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
12338							variant: "body1",
12339							children: campaign.creativeCount
12340						})
12341					}),
12342					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, {
12343						sx: bodyCellSx,
12344						children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
12345							variant: "body1",
12346							noWrap: true,
12347							children: campaign.updatedAt ? TimeUtils.getDesignSystemReadableTimeDate(campaign.updatedAt, true) : "-"
12348						})
12349					})
12350				]
12351			}, campaign.id);
12352		}) })]
12353	})
12354});
12355var containerSx = {
12356	flex: 1,
12357	minHeight: 0
12358};
12359var nameCellSx$1 = {
12360	display: "flex",
12361	flexDirection: "column",
12362	maxWidth: 320
12363};
12364//#endregion
12365//#region client/components/newNav/components/ads/decoupled/DecoupledCampaignsPage.tsx
12366/**
12367* Decoupled campaigns: a partner owns the buy and calls us for the creative. Eight of them exist in production,
12368* all disabled since 2023, so this page's first real job is creating number nine.
12369*/
12370var DecoupledCampaignsPage = () => {
12371	const accountId = useActiveAccountId();
12372	const { userRoles } = useUserData();
12373	const canEdit = isPermitted(userRoles, permissions.adsEditCampaignSettings);
12374	const { decoupledCampaigns, campaigns, loading, error, refetch } = useAdsSpaceCampaigns();
12375	const actions = useAdsSpaceCampaignActions();
12376	const [isNewCampaignDialogOpen, setIsNewCampaignDialogOpen] = (0, import_react.useState)(false);
12377	const newCampaignButton = /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Button, {
12378		variant: "contained",
12379		size: "medium",
12380		startIcon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FontAwesomeIcon, { icon: import_faPlus.faPlus }),
12381		onClick: () => setIsNewCampaignDialogOpen(true),
12382		"data-tracking-id": "tracking-id-ads-decoupled-new-campaign-btn",
12383		children: "New decoupled campaign"
12384	});
12385	const hasCampaigns = decoupledCampaigns.length > 0;
12386	return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
12387		sx: adsPageSx,
12388		children: [
12389			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsPageHeader, {
12390				area: "Decoupled / Decoupled campaigns",
12391				title: "Decoupled campaigns",
12392				count: decoupledCampaigns.length,
12393				children: hasCampaigns && canEdit && newCampaignButton
12394			}),
12395			/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_dist$2.AttentionBox, {
12396				color: "info",
12397				children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.AttentionBoxTitle, { children: "SundaySky is not the DSP here" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.AttentionBoxContent, { children: "A partner has already won the impression and calls us for the creative, so there are no flights, no budget and n
12397o bidding on these campaigns - only what decides which video we return." })]
12398			}),
12399			error && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_dist$2.AttentionBox, {
12400				color: "error",
12401				"data-testid": "testId-ads-decoupled-error",
12402				children: [
12403					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.AttentionBoxTitle, { children: "The ads configuration is unreachable" }),
12404					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.AttentionBoxContent, { children: "This account's decoupled campaigns could not be read, so this list is not the whole picture." }),
12405					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.AttentionBoxActions, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Button, {
12406						variant: "text",
12407						size: "small",
12408						onClick: () => void refetch(),
12409						"data-tracking-id": "tracking-id-ads-decoupled-retry-btn",
12410						children: "Try again"
12411					}) })
12412				]
12413			}),
12414			loading && !hasCampaigns && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsTableSkeleton, { rows: 3 }),
12415			!loading && !error && !hasCampaigns && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsEmptyState, {
12416				icon: import_faLinkSlash.faLinkSlash,
12417				heading: "No decoupled campaigns yet",
12418				subtitle: "A decoupled campaign is what a partner's DSP calls when it wants a video from us. Create one and give them its name.",
12419				children: canEdit && newCampaignButton
12420			}),
12421			hasCampaigns && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(DecoupledCampaignsTable, {
12422				campaigns: decoupledCampaigns,
12423				accountId
12424			}),
12425			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(NewCampaignDialog, {
12426				open: isNewCampaignDialogOpen,
12427				mode: GqlClientAdsSpaceCampaignMode.DECOUPLED,
12428				saving: actions.saving,
12429				onClose: () => setIsNewCampaignDialogOpen(false),
12430				existingCampaignNames: [...campaigns, ...decoupledCampaigns].map((campaign) => campaign.name),
12431				onCreate: actions.create
12432			})
12433		]
12434	});
12435};
12436//#endregion
12437//#region client/components/newNav/components/ads/common/AdsDrillDownSection.tsx
12438/**
12439* A collection of configuration entities inside a settings section. The detail replaces the list in the same
12440* pane rather than opening a third column - the space already spends 475px on navigation before the content
12441* starts.
12442*/
12443var AdsDrillDownSection = (props) => {
12444	const [selectedId, setSelectedId] = (0, import_react.useState)(null);
12445	const [searchTerm, setSearchTerm] = (0, import_react.useState)("");
12446	const visibleRows = (0, import_react.useMemo)(() => {
12447		const term = searchTerm.trim().toLowerCase();
12448		if (!term) return props.rows;
12449		return props.rows.filter((row) => props.searchKeys.some((key) => asText(row[key]).toLowerCase().includes(term)));
12450	}, [
12451		props.rows,
12452		props.searchKeys,
12453		searchTerm
12454	]);
12455	const canDrillDown = Boolean(props.renderDetail);
12456	const selected = canDrillDown ? props.rows.find((row) => row.id === selectedId) : void 0;
12457	if (selected && props.renderDetail) return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
12458		sx: detailSx,
12459		children: [
12460			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Button, {
12461				variant: "text",
12462				size: "small",
12463				sx: backLinkSx$1,
12464				startIcon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FontAwesomeIcon, { icon: import_faAngleLeft.faAngleLeft }),
12465				onClick: () => setSelectedId(null),
12466				"data-tracking-id": `${props.trackingIdPrefix}-back-btn`,
12467				children: props.title
12468			}),
12469			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
12470				variant: "h4",
12471				children: props.getTitle(selected)
12472			}),
12473			props.renderDetail(selected)
12474		]
12475	});
12476	return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
12477		sx: listSx,
12478		children: [
12479			/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
12480				sx: headerSx$3,
12481				children: [!props.titleInPageHeader && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
12482					variant: "h5",
12483					sx: { minWidth: 0 },
12484					children: props.title
12485				}), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
12486					sx: headerActionsSx$2,
12487					children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Box, {
12488						sx: searchWrapperSx$2,
12489						children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.Search, {
12490							placeholder: props.searchPlaceholder,
12491							value: searchTerm,
12492							onChange: (event) => setSearchTerm(event.target.value),
12493							onClear: () => setSearchTerm(""),
12494							numberOfResults: visibleRows.length,
12495							inputProps: { "data-tracking-id": `${props.trackingIdPrefix}-search-input` }
12496						})
12497					}), props.headerActions]
12498				})]
12499			}),
12500			props.description && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
12501				variant: "body2",
12502				color: "text.secondary",
12503				sx: descriptionSx,
12504				children: props.description
12505			}),
12506			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableContainer, {
12507				component: import_material.Paper,
12508				variant: "outlined",
12509				sx: tableContainerSx,
12510				children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Table, {
12511					size: "small",
12512					stickyHeader: true,
12513					sx: tableSx$1,
12514					children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableHead, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.TableRow, { children: [props.columns.map((column) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, {
12515						align: column.align ?? "left",
12516						sx: headCellSx,
12517						children: column.label
12518					}, column.key)), canDrillDown && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, { sx: actionsCellSx })] }) }
12518), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.TableBody, { children: [
12519						props.loading && props.rows.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableRow, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, {
12520							colSpan: props.columns.length + 1,
12521							sx: emptyCellSx$2,
12522							children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsTableSkeleton, {
12523								rows: 5,
12524								height: 32
12525							})
12526						}) }),
12527						!props.loading && visibleRows.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableRow, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, {
12528							colSpan: props.columns.length + 1,
12529							sx: emptyCellSx$2,
12530							children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
12531								variant: "body1",
12532								color: "text.secondary",
12533								children: searchTerm ? `No results for "${searchTerm}"` : props.emptyText ?? "Nothing here yet"
12534							})
12535						}) }),
12536						visibleRows.map((row) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.TableRow, {
12537							sx: rowHoverSx,
12538							onClick: canDrillDown ? () => setSelectedId(row.id) : void 0,
12539							"data-testid": `ads-drilldown-row-${row.id}`,
12540							"data-tracking-id": canDrillDown ? `${props.trackingIdPrefix}-row-btn` : void 0,
12541							children: [props.columns.map((column) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, {
12542								align: column.align ?? "left",
12543								sx: canDrillDown ? clickableCellSx : bodyCellSx,
12544								children: column.render ? column.render(row) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
12545									variant: "body1",
12546									children: asText(row[column.key]) || "-"
12547								})
12548							}
12548, column.key)), canDrillDown && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, {
12549								sx: actionsCellSx,
12550								children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.SvgIcon, {
12551									sx: chevronSx$1,
12552									children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FontAwesomeIcon, { icon: import_faAngleRight.faAngleRight })
12553								})
12554							})]
12555						}, row.id))
12556					] })]
12557				})
12558			})
12559		]
12560	});
12561};
12562var listSx = {
12563	display: "flex",
12564	flexDirection: "column",
12565	gap: 2,
12566	flex: 1,
12567	minHeight: 0
12568};
12569var detailSx = {
12570	display: "flex",
12571	flexDirection: "column",
12572	gap: 3,
12573	flex: 1,
12574	minHeight: 0,
12575	overflowY: "auto",
12576	paddingBottom: 2,
12577	...import_dist$2.scrollOnHoverSxProps
12578};
12579var tableContainerSx = {
12580	flex: 1,
12581	minHeight: 0,
12582	...import_dist$2.scrollOnHoverSxProps
12583};
12584var tableSx$1 = {
12585	tableLayout: "fixed",
12586	"& .MuiTableCell-root": { overflowWrap: "anywhere" }
12587};
12588var backLinkSx$1 = {
12589	alignSelf: "flex-start",
12590	marginBottom: -2
12591};
12592var headerSx$3 = {
12593	display: "flex",
12594	alignItems: "center",
12595	justifyContent: "space-between",
12596	gap: 2
12597};
12598var descriptionSx = { marginTop: -1.5 };
12599var headerActionsSx$2 = {
12600	display: "flex",
12601	alignItems: "center",
12602	gap: 1,
12603	flexShrink: 0
12604};
12605var searchWrapperSx$2 = { width: 280 };
12606var clickableCellSx = (0, import_dist$2.combineSxProps)(bodyCellSx, { cursor: "pointer" });
12607var emptyCellSx$2 = {
12608	padding: "32px 16px",
12609	textAlign: "center"
12610};
12611var chevronSx$1 = {
12612	fontSize: "12px !important",
12613	width: "12px !important",
12614	height: "12px !important",
12615	color: "text.disabled"
12616};
12617//#endregion
12618//#region client/components/newNav/components/ads/common/useAdsSettingsForm.ts
12619/**
12620* Form state for one ads configuration entity. Nothing here reaches the server: an edit updates local state and
12621* only an explicit save commits it, which is the contract every surface in the space keeps. Errors are
12622* recomputed on every change rather than derived at render, so a field never flashes an error before it has
12623* been touched.
12624*
12625* A surface with no `onPersist` has no write path yet, and its save says so rather than silently doing nothing.
12626*/
12627var useAdsSettingsForm = (groups, initialValues, entityLabel, onPersist, saving, childCollections = []) => {
12628	const [savedValues, setSavedValues] = (0, import_react.useState)(initialValues);
12629	const [values, setValues] = (0, import_react.useState)(initialValues);
12630	const [errors, setErrors] = (0, import_react.useState)({});
12631	const childChangeCount = childCollections.reduce((total, collection) => total + collection.changeCount, 0);
12632	const changeCount = (0, import_react.useMemo)(() => countChanges(values, savedValues) + childChangeCount, [
12633		values,
12634		savedValues,
12635		childChangeCount
12636	]);
12637	const onChange = (key, value) => {
12638		const next = {
12639			...values,
12640			[key]: value
12641		};
12642		setValues(next);
12643		setErrors(validateFields(groups, next));
12644	};
12645	const onSave = async (comment) => {
12646		if (!onPersist) {
12647			setSavedValues(values);
12648			setInfoNotification({ message: `${entityLabel} is not connected to the ads configuration service yet, so this change stays on this page` });
12649			return;
12650		}
12651		await onPersist(values, comment);
12652		setSavedValues(values);
12653	};
12654	const onDiscard = () => {
12655		setValues(savedValues);
12656		setErrors({});
12657		childCollections.forEach((collection) => collection.reset());
12658	};
12659	return {
12660		values,
12661		errors,
12662		dirty: changeCount > 0,
12663		changeCount,
12664		saving,
12665		onChange,
12666		onSave,
12667		onDiscard
12668	};
12669};
12670//#endregion
12671//#region client/components/newNav/components/ads/dsp/nexxenIntegration.ts
12672/**
12673* The Nexxen integration's fixed values, from the platform's own reference (`ads-platform`,
12674* `docs/NEXXEN-INTEGRATION.md`). None of them varies by account: Nexxen tells our accounts apart from the
12675* `acc=` value inside the data pixel, not from the ids, so there is nothing to request from them when a new
12676* account is onboarded. What is genuinely per-account is only the experiment that splits its shoppers.
12677*/
12678var NEXXEN_DSP_NAME = "nexxen";
12679/** Every site event. Stored as a plain URL - the route A generator wraps it, route B redirects to it. */
12680var NEXXEN_DATA_PIXEL_TEMPLATE = "https://d.turn.com/r/dd/id/${NEXXEN_ID}/kv/cat=${CATEGORY},cb=${CB},ordid=${EVENT_LABEL},method=${METHOD},prod=${PRODUCT},acc=${ACCOUNT},seg=${udt.seg},sskyid=${SSKY_USER_ID},sp=${SOURCE_PAGE},fam=${udt.family},value=${EVENT_VALUE}";
12681/** Conversions only. Carries the shopper itself, because route B drops the data pixel on a conversion. */
12682var NEXXEN_BEACON_TEMPLATE = "https://r.turn.com/r/beacon?b2=${NEXXEN_BEACON_ID}&cid=${EVENT_LABEL}&bprice=${EVENT_VALUE}&fpcid=${SSKY_USER_ID}";
12683/** Both Nexxen ids, pre-encoded as Nexxen supplies them. The same string for every account. */
12684var NEXXEN_LINK_DATA = "id=L21rdC8yMzQ0L2NpZC8xNzUwODExMTc2L3QvMg;b2=1Iu7eiTMLvRqedXJrhojE-JrZ2skq51XXiS63prY2DjNJkug-ESTYTT70Sg7a3bIcPpo9gvl9eRS4Wwqj-KKw";
12685/** Half the shoppers go to Nexxen. The tracking server rejects any other split, so this is not a preference. */
12686var NEXXEN_SPLIT_POINT = 5e3;
12687var NEXXEN_RANGE_MAX = 1e4;
12688/**
12689* The names the tracking server rebuilds from the account name and compares character for character. All
12690* lowercase; a capital anywhere fails validation and Nexxen stays off for the account.
12691*/
12692var nexxenNames = (adsAccountName) => {
12693	const account = (adsAccountName ?? "").trim().toLowerCase();
12694	return {
12695		account,
12696		experiment: `${account}_nexxen_ab`,
12697		sundayskyGroup: `${account}_sundaysky`,
12698		nexxenGroup: `${account}_nexxen`
12699	};
12700};
12701/** What each macro in the two templates resolves to, for someone reading a template rather than writing one. */
12702var nexxenMacros = [
12703	{
12704		macro: "${NEXXEN_ID}",
12705		meaning: "The data pixel id, from this account's link data key id"
12706	},
12707	{
12708		macro: "${NEXXEN_BEACON_ID}",
12709		meaning: "The beacon token, from this account's link data key b2"
12710	},
12711	{
12712		macro: "${SSKY_USER_ID}",
12713		meaning: "The SundaySky shopper id - without it Nexxen cannot attribute anything"
12714	},
12715	{
12716		macro: "${ACCOUNT}",
12717		meaning: "The account the event came from. This is how Nexxen tells our accounts apart"
12718	},
12719	{
12720		macro: "${METHOD}",
12721		meaning: "The site-action method: uui, cat, atc, conv"
12722	},
12723	{
12724		macro: "${EVENT_LABEL}",
12725		meaning: "The order id on a conversion"
12726	},
12727	{
12728		macro: "${EVENT_VALUE}",
12729		meaning: "The order value on a conversion"
12730	},
12731	{
12732		macro: "${CATEGORY}, ${PRODUCT}",
12733		meaning: "The category and product the shopper viewed"
12734	},
12735	{
12736		macro: "${SOURCE_PAGE}",
12737		meaning: "The page the event came from"
12738	},
12739	{
12740		macro: "${udt.seg}, ${udt.family}",
12741		meaning: "Segment and family, from the site tag's udt fields"
12742	},
12743	{
12744		macro: "${CB}",
12745		meaning: "A fresh cache buster per link"
12746	}
12747];
12748//#endregion
12749//#region client/components/newNav/components/ads/dsp/dspModel.ts
12750/**
12751* The DSP surfaces. A DSP is the demand side of a decoupled campaign: a partner buys the impression and calls
12752* the ad server for the video, and these rows are how the tracking server builds the retargeting and sync links
12753* it hands them.
12754*/
12755/** Substituted at runtime by the tracking server. Documented on the page because nothing else documents them. */
12756var dspTemplatePlaceholders = [
12757	{
12758		placeholder: "${SID}",
12759		meaning: "The segment id for the event type, taken from the account's DSP data"
12760	},
12761	{
12762		placeholder: "${REDIRECT}",
12763		meaning: "The URL to redirect to after the DSP pixel fires"
12764	},
12765	{
12766		placeholder: "${TM_USER_ID}",
12767		meaning: "The DSP's user id"
12768	},
12769	{
12770		placeholder: "${SSKY_USER_ID}",
12771		meaning: "The SundaySky user id"
12772	}
12773];
12774var dspTemplateExamples = [{
12775	label: "Retargeting",
12776	value: "SSKY.fire('https://rtd-tm.everesttech.net/upi/?sid=${SID}&cs=1&redir=${REDIRECT}');"
12777}, {
12778	label: "Exchange sync",
12779	value: "https://vop.sundaysky.com/exchange_sync?exchange=51&exchangeUserId=${TM_USER_ID}&sskyUserId=${SSKY_USER_ID}"
12780}];
12781/** The nine operators the sync-condition evaluator understands. */
12782var dspConditionOperators = [
12783	{
12784		value: "EQUALS",
12785		label: "Equals"
12786	},
12787	{
12788		value: "DOES_NOT_EQUAL",
12789		label: "Does not equal"
12790	},
12791	{
12792		value: "EXISTS",
12793		label: "Exists"
12794	},
12795	{
12796		value: "DOES_NOT_EXIST",
12797		label: "Does not exist"
12798	},
12799	{
12800		value: "STRING_CONTAINS",
12801		label: "Contains"
12802	},
12803	{
12804		value: "STRING_DOES_NOT_CONTAIN",
12805		label: "Does not contain"
12806	},
12807	{
12808		value: "ONE_OF",
12809		label: "One of"
12810	},
12811	{
12812		value: "NOT_ONE_OF",
12813		label: "Not one of"
12814	},
12815	{
12816		value: "GREATER_THAN",
12817		label: "Greater than"
12818	}
12819];
12820/**
12821* The link-data key set depends on the DSP, and the keys are the same event-type vocabulary as the site-action
12822* `method` values on the account tags page. A typo in one silently stops that event type being reported, which
12823* is why the value is a dropdown rather than a line of text.
12824*/
12825var dspLinkDataKeys = {
12826	adobe: [
12827		{
12828			value: "cat",
12829			label: "cat - category view"
12830		},
12831		{
12832			value: "uui",
12833			label: "uui - user identification"
12834		},
12835		{
12836			value: "atc",
12837			label: "atc - add to cart"
12838		},
12839		{
12840			value: "conv",
12841			label: "conv - conversion"
12842		},
12843		{
12844			value: "cart",
12845			label: "cart - cart view"
12846		}
12847	],
12848	thetradedesk: [{
12849		value: "adv",
12850		label: "adv - advertiser"
12851	}, {
12852		value: "ct",
12853		label: "ct - conversion tracker"
12854	}],
12855	[NEXXEN_DSP_NAME]: [{
12856		value: "id",
12857		label: "id - the data pixel id"
12858	}, {
12859		value: "b2",
12860		label: "b2 - the conversion beacon token"
12861	}]
12862};
12863/** Empty for a DSP whose vocabulary is not known here, which is what makes the key a free-text field instead. */
12864var dspLinkDataKeyOptions = (dspName) => dspLinkDataKeys[(dspName ?? "").toLowerCase()] ?? [];
12865/**
12866* Stored as `key=value` entries; edited as rows so a mistyped key is visible. Separated by a semicolon or a
12867* newline and split on the first `=`, matching the tracking server's own parser - a row written by hand as
12868* `id=...;b2=...` has to come back as two rows, and a base64 value's trailing `=` padding has to survive.
12869*/
12870var parseDspLinkData = (linkData) => asText(linkData).split(/[;\r\n]+/).map((line) => line.trim()).filter(Boolean).map((line, index) => {
12871	const separator = line.indexOf("=");
12872	return separator === -1 ? {
12873		id: `link-${index}`,
12874		linkKey: line,
12875		linkValue: ""
12876	} : {
12877		id: `link-${index}`,
12878		linkKey: line.slice(0, separator).trim(),
12879		linkValue: line.slice(separator + 1).trim()
12880	};
12881});
12882var serializeDspLinkData = (rows) => rows.filter((row) => asText(row.linkKey)).map((row) => `${asText(row.linkKey)}=${asText(row.linkValue)}`).join("\n");
12883/** A filter that does not compile reports nobody; one that is empty or `.*` reports everybody. */
12884var validateUserFilterRegex = (value) => {
12885	const text = asText(value).trim();
12886	if (!text) return null;
12887	try {
12888		new RegExp(text);
12889		return null;
12890	} catch {
12891		return "This is not a valid regular expression";
12892	}
12893};
12894var describeUserFilter = (value) => {
12895	const text = asText(value).trim();
12896	return !text || text === ".*" ? "All users will be reported to this DSP." : null;
12897};
12898//#endregion
12899//#region client/components/newNav/components/ads/dsp/DspAccountDataSection.tsx
12900var accountDataGroups = (dspOptions) => [{
12901	id: "identity",
12902	title: "Platform",
12903	fields: [{
12904		key: "dspId",
12905		label: "Demand-side platform",
12906		type: "select",
12907		options: dspOptions,
12908		validate: required("Pick the platform this data belongs to")
12909	}, {
12910		key: "userFilterRegex",
12911		label: "User filter",
12912		type: "text",
12913		wide: true,
12914		validate: validateUserFilterRegex,
12915		help: "If set, only users matching this regex will be reported to the DSP"
12916	}]
12917}];
12918/**
12919* A known DSP gets its own vocabulary as a dropdown, because a typo there silently stops that entry being
12920* reported. One this page has no vocabulary for gets a text box, so a new integration is not blocked on an
12921* edit here.
12922*/
12923var linkDataFields = (dspName) => {
12924	const options = dspLinkDataKeyOptions(dspName);
12925	return [options.length > 0 ? {
12926		key: "linkKey",
12927		label: "Key",
12928		type: "select",
12929		options
12930	} : {
12931		key: "linkKey",
12932		label: "Key",
12933		type: "text"
12934	}, {
12935		key: "linkValue",
12936		label: "Value",
12937		type: "text"
12938	}];
12939};
12940var newAccountDataFields = (dspOptions) => [
12941	{
12942		key: "dspId",
12943		label: "Demand-side platform",
12944		type: "select",
12945		options: dspOptions,
12946		validate: required("Pick the platform this account will report to")
12947	},
12948	{
12949		key: "linkData",
12950		label: "Link data",
12951		type: "longText",
12952		wide: true,
12953		help: "One key=value per line. Nexxen takes the shared id and b2 pair; leave it empty to fill it in afterwards"
12954	},
12955	{
12956		key: "userFilterRegex",
12957		label: "User filter",
12958		type: "text",
12959		wide: true,
12960		validate: validateUserFilterRegex,
12961		help: "If set, only users matching this regex will be reported to the DSP"
12962	}
12963];
12964var linkDataDescription = (dspName) => dspName.toLowerCase() === "nexxen" ? "The two Nexxen pixel ids, substituted into the platform's templates. The same pair for every account - copy it from the Nexxen integration guide rather than asking Nexxen for one." : "What segment id to report for each event type. The keys are the same vocabulary as the site-action methods on the account tags page.";
12965var conditionFields = [
12966	{
12967		key: "eventField",
12968		label: "Event field",
12969		type: "text",
12970		help: "Name of event field to test"
12971	},
12972	{
12973		key: "type",
12974		label: "Operator",
12975		type: "select",
12976		options: dspConditionOperators,
12977		help: "Condition operator"
12978	},
12979	{
12980		key: "eventValues",
12981		label: "Values",
12982		type: "text",
12983		help: "Values of events (comma-separated)"
12984	}
12985];
12986var AccountDataDetail = (props) => {
12987	const groups = accountDataGroups(props.dspOptions);
12988	const [linkRows, setLinkRows] = (0, import_react.useState)(parseDspLinkData(props.accountData.linkData));
12989	const conditions = useAdsSpaceEntities(GqlClientAdsSpaceEntityKind.DSP_SYNC_CONDITION, props.accountData.id);
12990	const [conditionRows, setConditionRows] = (0, import_react.useState)(null);
12991	const rows = conditionRows ?? conditions.rows;
12992	const form = useAdsSettingsForm(groups, {
12993		...props.accountData,
12994		linkData: serializeDspLinkData(linkRows)
12995	}, "The DSP account data", async (values, comment) => {
12996		await props.save([{
12997			...values,
12998			linkData: serializeDspLinkData(linkRows),
12999			id: props.accountData.id
13000		}], comment);
13001		const removedIds = conditions.rows.filter((saved) => !rows.some((row) => row.id === saved.id)).map((row) => row.id);
13002		if (conditionRows) {
13003			await conditions.save(rows, comment, removedIds);
13004			setConditionRows(null);
13005		}
13006	}, props.saving || conditions.saving);
13007	const filterWarning = describeUserFilter(form.values.userFilterRegex);
13008	const dspName = props.dspNames[asText(form.values.dspId)] ?? "";
13009	return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(AdsSettingsForm, {
13010		groups,
13011		form,
13012		trackingIdPrefix: "tracking-id-ads-dsp-account-data",
13013		children: [
13014			filterWarning && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.AttentionBox, {
13015				color: "warning",
13016				children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.AttentionBoxContent, { children: filterWarning })
13017			}),
13018			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsChildTable, {
13019				title: "Link data",
13020				description: linkDataDescription(dspName),
13021				fields: linkDataFields(dspName),
13022				rows: linkRows,
13023				addLabel: "Add entry",
13024				emptyText: "No link data - this platform has nothing to substitute into 
13024its templates",
13025				rowTitle: (row) => asText(row.linkKey) || "Entry",
13026				onChange: (next) => {
13027					setLinkRows(next);
13028					form.onChange("linkData", serializeDspLinkData(next));
13029				},
13030				trackingIdPrefix: "tracking-id-ads-dsp-link-data"
13031			}),
13032			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsChildTable, {
13033				title: "Sync conditions",
13034				description: "All conditions must match for an event to produce a retargeting link. An event failing any of them gets no link, and no conditions means no filtering.",
13035				fields: conditionFields,
13036				rows,
13037				addLabel: "Add condition",
13038				emptyText: "No conditions - every event produces a link",
13039				rowTitle: (row) => asText(row.eventField) || "Condition",
13040				onChange: setConditionRows,
13041				trackingIdPrefix: "tracking-id-ads-dsp-sync-conditions"
13042			})
13043		]
13044	});
13045};
13046/**
13047* This account's data on each DSP: who gets reported, and what segment id each event type carries. Reachable
13048* from account settings and from the decoupled area, because someone setting up decoupled delivery should not
13049* have to know it is filed under account settings.
13050*/
13051var DspAccountDataSection = () => {
13052	const platforms = useAdsSpaceEntities(GqlClientAdsSpaceEntityKind.DSP);
13053	const accountData = useAdsSpaceEntities(GqlClientAdsSpaceEntityKind.DSP_ACCOUNT_DATA);
13054	const [isAdding, setIsAdding] = (0, import_react.useState)(false);
13055	const dspOptions = platforms.rows.map((row) => ({
13056		value: row.id,
13057		label: asText(row.name)
13058	}));
13059	const dspNames = Object.fromEntries(platforms.rows.map((row) => [row.id, asText(row.name)]));
13060	const createAccountData = async (row) => {
13061		await accountData.create(row, `Connected this account to ${dspNames[asText(row.dspId)] ?? "a demand-side platform"}`, "dspId");
13062		setIsAdding(false);
13063	};
13064	return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
13065		sx: sectionSx$6,
13066		children: [
13067			platforms.rows.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_dist$2.AttentionBox, {
13068				color: "info",
13069				children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.AttentionBoxTitle, { children: "No demand-side platforms are configured" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.AttentionBoxContent, { children: "A platform has to exist before an account can carry data for it." })]
13070			}),
13071			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsDrillDownSection, {
13072				title: "DSP account data",
13073				titleInPageHeader: true,
13074				searchPlaceholder: "Search platforms",
13075				rows: accountData.rows,
13076				loading: accountData.loading,
13077				searchKeys: ["dspId", "userFilterRegex"],
13078				columns: [
13079					{
13080						key: "dspId",
13081						label: "Platform",
13082						render: (row) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
13083							variant: "subtitle2",
13084							children: dspNames[asText(row.dspId)] ?? asText(row.dspId)
13085						})
13086					},
13087					{
13088						key: "userFilterRegex",
13089						label: "User filter"
13090					},
13091					{
13092						key: "linkData",
13093						label: "Event types",
13094						render: (row) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: parseDspLinkData(row.linkData).length })
13095					}
13096				],
13097				getTitle: (row) => dspNames[asText(row.dspId)] ?? "DSP account data",
13098				renderDetail: (row) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AccountDataDetail, {
13099					accountData: row,
13100					dspOptions,
13101					dspNames,
13102					save: accountData.save,
13103					saving: accountData.saving
13104				}, row.id),
13105				emptyText: "This account reports to no demand-side platform yet.",
13106				headerActions: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Button, {
13107					variant: "outlined",
13108					size: "medium",
13109					disabled: platforms.rows.length === 0,
13110					onClick: () => setIsAdding(true),
13111					"data-testid": "ads-dsp-account-data-new-btn",
13112					"data-tracking-id": "tracking-id-ads-dsp-account-data-new-btn",
13113					children: "Connect a platform"
13114				}),
13115				trackingIdPrefix: "tracking-id-ads-dsp-account-data"
13116			}),
13117			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsRowDialog, {
13118				open: isAdding,
13119				title: "Connect a demand-side platform",
13120				fields: newAccountDataFields(dspOptions),
13121				row: null,
13122				otherRows: accountData.rows,
13123				validateRow: (row, otherRows) => otherRows.some((other) => asText(other.dspId) === asText(row.dspId)) ? { dspId: "This account already reports to that platform" } : {},
13124				onClose: () => setIsAdding(false),
13125				onSubmit: (row) => void createAccountData(row).catch(() => setIsAdding(false)),
13126				trackingIdPrefix: "tracking-id-ads-dsp-account-data-new"
13127			})
13128		]
13129	});
13130};
13131var sectionSx$6 = {
13132	display: "flex",
13133	flexDirection: "column",
13134	gap: 2,
13135	flex: 1,
13136	minHeight: 0
13137};
13138//#endregion
13139//#region client/components/newNav/components/ads/dsp/DspPlatformsSection.tsx
13140var placeholderHelp = dspTemplatePlaceholders.map((entry) => `${entry.placeholder} - ${entry.meaning}`).join("; ");
13141var dspGroups = [{
13142	id: "identity",
13143	title: "Identity",
13144	fields: [{
13145		key: "name",
13146		label: "Name",
13147		type: "text",
13148		validate: required("A DSP needs a name"),
13149		help: "Identifies the DSP; used to pick the right link generator in the tracking server"
13150	}]
13151}, {
13152	id: "templates",
13153	title: "Link templates",
13154	description: `Placeholders substituted at runtime: ${placeholderHelp}`,
13155	fields: [{
13156		key: "retargetingTemplate",
13157		label: "Retargeting template",
13158		type: "longText",
13159		wide: true,
13160		help: "General template for retargeting links for this DSP"
13161	}, {
13162		key: "exchangeSyncTemplate",
13163		label: "Exchange sync template",
13164		type: "longText",
13165		wide: true,
13166		help: "General template for sync redirect (to VOP) links for this DSP"
13167	}]
13168}];
13169var DspDetail = ({ dsp, save, saving }) => {
13170	const form = useAdsSettingsForm(dspGroups, dsp, "The demand-side platform", (values, comment) => save([{
13171		...values,
13172		id: dsp.id
13173	}], comment), saving);
13174	return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsSettingsForm, {
13175		groups: dspGroups,
13176		form,
13177		saveWarning: "The retargeting template runs as JavaScript on customer pages. A malformed one stops all retargeting for every account on this DSP.",
13178		trackingIdPrefix: "tracking-id-ads-dsp",
13179		children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
13180			sx: examplesSx,
13181			children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
13182				variant: "h5",
13183				children: "Adobe, as a worked example"
13184			}), dspTemplateExamples.map((example) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
13185				variant: "helper",
13186				color: "text.secondary",
13187				sx: codeSx,
13188				children: `${example.label}: ${example.value}`
13189			}, example.label))]
13190		})
13191	});
13192};
13193/**
13194* The demand-side platforms the tracking server knows how to build links for. One row is live today, and both of
13195* its templates are code that runs against real traffic - so this page carries the warning and the worked
13196* example rather than presenting them as ordinary text fields.
13197*/
13198var DspPlatformsSection = () => {
13199	const { rows, save, saving, loading, create } = useAdsSpaceEntities(GqlClientAdsSpaceEntityKind.DSP);
13200	const [isAdding, setIsAdding] = (0, import_react.useState)(false);
13201	const createPlatform = async (row) => {
13202		await create(row, `Added the demand-side platform ${asText(row.name)}`);
13203		setIsAdding(false);
13204	};
13205	return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
13206		sx: sectionSx$5,
13207		children: [
13208			/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_dist$2.AttentionBox, {
13209				color: "warning",
13210				children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.AttentionBoxTitle, { children: "These templates run on customer pages" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.AttentionBoxContent, { children: "The retargeting template is executed as JavaScript by the tracking server's site tag. Adding a DSP row alone serves nothing - the ad server also needs a matching /dec_ endpoint, which is code." })]
13211			}),
13212			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsDrillDownSection, {
13213				title: "Demand-side platforms",
13214				titleInPageHeader: true,
13215				searchPlaceholder: "Search platforms",
13216				rows,
13217				loading,
13218				searchKeys: ["name"],
13219				columns: [{
13220					key: "name",
13221					label: "Name",
13222					render: (row) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
13223						variant: "subtitle2",
13224						children: asText(row.name)
13225					})
13226				}, {
13227					key: "retargetingTemplate",
13228					label: "Retargeting template"
13229				}],
13230				getTitle: (row) => asText(row.name),
13231				renderDetail: (row) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(DspDetail, {
13232					dsp: row,
13233					save,
13234					saving
13235				}, row.id),
13236				emptyText: "No demand-side platforms are configured",
13237				headerActions: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Button, {
13238					variant: "outlined",
13239					size: "medium",
13240					onClick: () => setIsAdding(true),
13241					"data-testid": "ads-dsp-new-platform-btn",
13242					"data-tracking-id": "tracking-id-ads-dsp-new-platform-btn",
13243					children: "New platform"
13244				}),
13245				trackingIdPrefix: "tracking-id-ads-dsp-platforms"
13246			}),
13247			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsRowDialog, {
13248				open: isAdding,
13249				title: "New demand-side platform",
13250				fields: dspGroups.flatMap((group) => group.fields),
13251				row: null,
13252				otherRows: rows,
13253				validateRow: (row, otherRows) => otherRows.some((other) => asText(other.name).toLowerCase() === asText(row.name).trim().toLowerCase()) ? { name: "A platform with that name already exists" } : {},
13254				onClose: () => setIsAdding(false),
13255				onSubmit: (row) => void createPlatform(row).catch(() => setIsAdding(false)),
13256				trackingIdPrefix: "tracking-id-ads-dsp-new-platform"
13257			})
13258		]
13259	});
13260};
13261var sectionSx$5 = {
13262	display: "flex",
13263	flexDirection: "column",
13264	gap: 2,
13265	flex: 1,
13266	minHeight: 0
13267};
13268var examplesSx = {
13269	display: "flex",
13270	flexDirection: "column",
13271	gap: 1
13272};
13273var codeSx = {
13274	fontFamily: "monospace",
13275	wordBreak: "break-all"
13276};
13277//#endregion
13278//#region client/components/newNav/components/ads/dsp/NexxenIntegrationDialog.tsx
13279var Step = ({ number, title, children }) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
13280	sx: stepSx,
13281	children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
13282		sx: stepHeaderSx,
13283		children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Box, {
13284			sx: stepNumberSx,
13285			children: number
13286		}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
13287			variant: "h5",
13288			children: title
13289		})]
13290	}
13290), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Box, {
13291		sx: stepBodySx,
13292		children
13293	})]
13294});
13295var Field = ({ label, children }) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
13296	sx: fieldSx,
13297	children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
13298		variant: "subtitle2",
13299		sx: fieldLabelSx,
13300		children: label
13301	}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Box, {
13302		sx: fieldValueSx,
13303		children
13304	})]
13305});
13306/**
13307* How to put one account onto Nexxen, on the page where two of the three steps are done. Everything Nexxen
13308* needs is the same for every account, so this is a copy-and-paste job rather than a conversation with them -
13309* the only thing that has to be typed out per account is the experiment that splits its shoppers in half.
13310*/
13311var NexxenIntegrationDialog = ({ open, onClose }) => {
13312	const accountId = useActiveAccountId();
13313	const { adsAccountName } = useAdsSpaceAccount();
13314	const names = nexxenNames(adsAccountName);
13315	const hasAccountName = Boolean(names.account);
13316	return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Dialog, {
13317		open,
13318		onClose,
13319		PaperProps: { sx: paperSx$2 },
13320		children: [
13321			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.TruffleDialogTitle, {
13322				CloseIconButtonProps: {
13323					onClick: onClose,
13324					"data-tracking-id": "tracking-id-ads-nexxen-guide-close-btn"
13325				},
13326				children: "Nexxen integration"
13327			}),
13328			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.DialogContent, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
13329				sx: contentSx$5,
13330				"data-testid": "ads-nexxen-guide",
13331				children: [
13332					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
13333						variant: "body1",
13334						color: "text.secondary",
13335						children: "Nexxen bids on shoppers we have seen. Once an account is switched on, every site event it sends us is also reported to Nexxen - but only for half its shoppers, so the two halves can be compared. Nexxen supplies the same two pixels and the same two ids to every account and tells our accounts apart from the account name inside the pixel, so there is nothing to request from them when you add one."
13336					}),
13337					/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_dist$2.AttentionBox, {
13338						color: "warning",
13339						children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.AttentionBoxTitle, { children: "Check the account can actually receive it first" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.AttentionBoxContent, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Typography, {
13340							variant: "body1",
13341							color: "text.secondary",
13342							children: [
13343								"An ineligible account fails silently: the counters look healthy and nothing ever reaches Nexxen. The account has to be enabled in the ads platform - a disabled one can never load its experiment - and its site events have to arrive in a shape where a pixel reaches the shopper's browser. Events sent to",
13344								" ",
13345								/* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", { children: "/t/v1/img" }),
13346								" ",
13347								"always do. Events sent to",
13348								" ",
13349								/* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", { children: "/t/v1/j" }),
13350								" ",
13351								"only do when the page runs the response as a script; when our own SDK pulls it as an image the browser throws every pixel away. Confirm which one the page does before switching the account on."
13352							]
13353						}) })]
13354					}),
13355					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Divider, {}),
13356					/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Step, {
13357						number: 1,
13358						title: "The Nexxen platform - once for the whole system",
13359						children: [
13360							/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Typography, {
13361								variant: "body1",
13362								color: "text.secondary",
13363								children: [
13364									"Skip this if a platform named",
13365									" ",
13366									/* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", { children: NEXXEN_DSP_NAME }),
13367									" ",
13368									"is already in the list behind this dialog. It is shared by every account, and it is inert until an account points at it. Both templates are plain URLs - never JavaScript, because one pair has to serve both integration shapes."
13369								]
13370							}),
13371							/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Field, {
13372								label: "Name",
13373								children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsCopyableText, {
13374									value: NEXXEN_DSP_NAME,
13375									monospace: true,
13376									maxWidth: 520,
13377									trackingId: "tracking-id-ads-nexxen-guide-name-copy"
13378								})
13379							}),
13380							/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Field, {
13381								label: "Retargeting template - the data pixel, sent on every site event",
13382								children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsCopyableText, {
13383									value: NEXXEN_DATA_PIXEL_TEMPLATE,
13384									monospace: true,
13385									maxWidth: 520,
13386									trackingId: "tracking-id-ads-nexxen-guide-data-pixel-copy"
13387								})
13388							}),
13389							/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Field, {
13390								label: "Exchange sync template - the beacon, sent on conversions only",
13391								children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsCopyableText, {
13392									value: NEXXEN_BEACON_TEMPLATE,
13393									monospace: true,
13394									maxWidth: 520,
13395									trackingId: "tracking-id-ads-nexxen-guide-beacon-copy"
13396								})
13397							}),
13398							/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
13399								variant: "helper",
13400								color: "text.secondary",
13401								children: "Nexxen writes their own macros as [ORDER_ID], [REVENUE] and [SSKYID]. Those are their notation for \"put this here\", not something we substitute - the templates above are already translated. Paste their spec verbatim and the URL we serve carries the literal brackets."
13402							}),
13403							/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Box, {
13404								sx: macrosSx,
13405								children: nexxenMacros.map((entry) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Typography, {
13406									variant: "helper",
13407									color: "text.secondary",
13408									children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", { children: entry.macro }), ` - ${entry.meaning}`]
13409								}, entry.macro))
13410							}),
13411							/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Box, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Button, {
13412								variant: "text",
13413								size: "small",
13414								component: Link$6,
13415								to: (0, import_dist$1.newNavAdsDspSectionPath)({
13416									accountId,
13417									sectionId: "platforms"
13418								}),
13419								onClick: onClose,
13420								"data-tracking-id": "tracking-id-ads-nexxen-guide-platforms-link",
13421								children: "Go to Platforms"
13422							}) })
13423						]
13424					}),
13425					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Divider, {}),
13426					/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Step, {
13427						number: 2,
13428						title: "The 50/50 split - once for this account",
13429						children: [
13430							/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Typography, {
13431								variant: "body1",
13432								color: "text.secondary",
13433								children: [
13434									"This is the only genuinely per-account piece. The tracking server rebuilds these three names from the account name and compares them character for character, so they are not free text: all lowercase, exactly as below. The two groups have to cover 0 to",
13435									` ${NEXXEN_RANGE_MAX} `,
13436									"with no gap and no overlap and",
13437									` ${NEXXEN_SPLIT_POINT} `,
13438									"each - anything else fails validation and Nexxen stays off for the account."
13439								]
13440							}),
13441							hasAccountName ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
13442								/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Field, {
13443									label: "Experiment name and salt",
13444									children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsCopyableText, {
13445										value: names.experiment,
13446										monospace: true,
13447										maxWidth: 520,
13448										trackingId: "tracking-id-ads-nexxen-guide-experiment-copy"
13449									})
13450								}),
13451								/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Field, {
13452									label: `First group - the half that stays ours, 0 to ${NEXXEN_SPLIT_POINT}`,
13453									children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsCopyableText, {
13454										value: names.sundayskyGroup,
13455										monospace: true,
13456										maxWidth: 520,
13457										trackingId: "tracking-id-ads-nexxen-guide-sundaysky-group-copy"
13458									})
13459								}),
13460								/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Field, {
13461									label: `Second group - the half reported to Nexxen, ${NEXXEN_SPLIT_POINT} to ${NEXXEN_RANGE_MAX}`,
13462									children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsCopyableText, {
13463										value: names.nexxenGroup,
13464										monospace: true,
13465										maxWidth: 520,
13466										trackingId: "tracking-id-ads-nexxen-guide-nexxen-group-copy"
13467									})
13468								})
13469							] }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.AttentionBox, {
13470								color: "info",
13471								children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.AttentionBoxContent, { children: "This Studio account is not connected to an ads account yet, so the names cannot be worked out. They are the ads account name, lowercase, followed by _nexxen_ab for the experiment and _sundaysky and _nexxen for its two groups." })
13472							}),
13473							/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
13474								variant: "helper",
13475								color: "text.secondary",
13476								children: "Leave the end date empty. A start date of today is fine - the split takes effect as soon as the experiment is running. New experiments open with a control and a test group already covering the right halves, so only the three names have to be changed."
13477							}),
13478							/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Box, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Button, {
13479								variant: "text",
13480								size: "small",
13481								component: Link$6,
13482								to: (0, import_dist$1.newNavAdsExperimentsPath)({ accountId }),
13483								onClick: onClose,
13484								"data-tracking-id": "tracking-id-ads-nexxen-guide-experiments-link",
13485								children: "Go to Experiments"
13486							}) })
13487						]
13488					}),
13489					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Divider, {}),
13490					/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Step, {
13491						number: 3,
13492						title: "Switch this account on",
13493						children: [
13494							/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Typography, {
13495								variant: "body1",
13496								color: "text.secondary",
13497								children: [
13498									"Do this last. Connecting the account before its experiment exists reports nobody and logs a fatal for the whole time in between. On the",
13499									" ",
13500									/* @__PURE__ */ (0, import_jsx_runtime.jsx)("b", { children: "This account" }),
13501									" ",
13502									"tab, connect the",
13503									" ",
13504									/* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", { children: NEXXEN_DSP_NAME }),
13505									" ",
13506									"platform and paste the link data below. Leave the user filter empty: the experiment is what decides who is reported."
13507								]
13508							}),
13509							/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Field, {
13510								label: "Link data - the same pair for every account",
13511								children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsCopyableText, {
13512									value: NEXXEN_LINK_DATA,
13513									monospace: true,
13514									maxWidth: 520,
13515									trackingId: "tracking-id-ads-nexxen-guide-link-data-copy"
13516								})
13517							}),
13518							/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Typography, {
13519								variant: "helper",
13520								color: "text.secondary",
13521								children: [
13522									"These are ids, never URLs. Putting a template in here substitutes a whole URL into the placeholder and the pixel we serve carries a literal $",
13523									"{",
13524									"NEXXEN_ID",
13525									"}",
13526									". The two are different values and are not interchangeable: id feeds the data pixel, b2 feeds the beacon, and swapping them produces URLs that still look perfectly well formed."
13527								]
13528							}),
13529							/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Box, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Button, {
13530								variant: "text",
13531								size: "small",
13532								component: Link$6,
13533								to: (0, import_dist$1.newNavAdsDspSectionPath)({
13534									accountId,
13535									sectionId: "account-data"
13536								}),
13537								onClick: onClose,
13538								"data-tracking-id": "tracking-id-ads-nexxen-guide-account-data-link",
13539								children: "Go to This account"
13540							}) })
13541						]
13542					}),
13543					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Divider, {}),
13544					/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
13545						sx: afterSx,
13546						children: [
13547							/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Box, {
13548								sx: afterHeaderSx,
13549								children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.Label, {
13550									size: "small",
13551									variant: "standard",
13552									color: "success",
13553									label: "After you save"
13554								})
13555							}),
13556							/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
13557								variant: "body1",
13558								color: "text.secondary",
13559								children: "Saving here already nudges the running servers - the ads services pick the account up on their next scan, about a minute later, with nothing else to run. Then watch the two per-account counters in the tracking server's summaries: reported and skipped_sundaysky_group should be roughly equal. Both report even at zero, so seeing them at all is the confirmation the account is live. Silence means it is not."
13560							}),
13561							/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
13562								variant: "body1",
13563								color: "text.secondary",
13564								children: "To pause an account without unpicking anything, give its experiment an end date. The configuration survives, reporting stops within a minute, and clearing the end date starts it again."
13565							})
13566						]
13567					})
13568				]
13569			}) }),
13570			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.TruffleDialogActions, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Button, {
13571				variant: "contained",
13572				size: "large",
13573				onClick: onClose,
13574				"data-tracking-id": "tracking-id-ads-nexxen-guide-done-btn",
13575				children: "Done"
13576			}) })
13577		]
13578	});
13579};
13580var paperSx$2 = {
13581	width: 720,
13582	maxWidth: "calc(100vw - 64px)"
13583};
13584var contentSx$5 = {
13585	display: "flex",
13586	flexDirection: "column",
13587	gap: 2,
13588	paddingTop: 1
13589};
13590var stepSx = {
13591	display: "flex",
13592	flexDirection: "column",
13593	gap: 1
13594};
13595var stepHeaderSx = {
13596	display: "flex",
13597	alignItems: "center",
13598	gap: 1.5
13599};
13600var stepNumberSx = {
13601	width: 24,
13602	height: 24,
13603	borderRadius: "50%",
13604	backgroundColor: "primary.light",
13605	color: "primary.main",
13606	display: "flex",
13607	alignItems: "center",
13608	justifyContent: "center",
13609	fontSize: "0.75rem",
13610	fontWeight: 600,
13611	flexShrink: 0
13612};
13613var stepBodySx = {
13614	display: "flex",
13615	flexDirection: "column",
13616	gap: 1,
13617	paddingLeft: "36px",
13618	minWidth: 0
13619};
13620var fieldSx = {
13621	display: "flex",
13622	flexDirection: "column",
13623	gap: .25,
13624	minWidth: 0
13625};
13626var fieldLabelSx = { color: "text.secondary" };
13627var fieldValueSx = { minWidth: 0 };
13628var macrosSx = {
13629	display: "flex",
13630	flexDirection: "column",
13631	gap: .25,
13632	paddingTop: .5
13633};
13634var afterSx = {
13635	display: "flex",
13636	flexDirection: "column",
13637	gap: 1
13638};
13639var afterHeaderSx = { display: "flex" };
13640//#endregion
13641//#region client/components/newNav/components/ads/dsp/AdsDspPage.tsx
13642var sections$2 = [{
13643	id: "platforms",
13644	label: "Platforms",
13645	icon: import_faTowerBroadcast.faTowerBroadcast,
13646	entityKinds: [GqlClientAdsSpaceEntityKind.DSP],
13647	render: () => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(DspPlatformsSection, {})
13648}, {
13649	id: "account-data",
13650	label: "This account",
13651	icon: import_faSliders.faSliders,
13652	entityKinds: [GqlClientAdsSpaceEntityKind.DSP_ACCOUNT_DATA, GqlClientAdsSpaceEntityKind.DSP_SYNC_CONDITION],
13653	render: () => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(DspAccountDataSection, {})
13654}];
13655/** The two levels of DSP configuration in one place: the platform itself, and what this account sends it. */
13656var AdsDspPage = () => {
13657	const accountId = useActiveAccountId();
13658	const [isNexxenGuideOpen, setIsNexxenGuideOpen] = (0, import_react.useState)(false);
13659	return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsSectionPage, {
13660		area: "Decoupled / Demand-side platforms",
13661		title: "Demand-side platforms",
13662		subtitle: "The partners that buy decoupled impressions and call the ad server for the video.",
13663		sections: sections$2,
13664		sectionPath: (sectionId) => (0, import_dist$1.newNavAdsDspSectionPath)({
13665			accountId,
13666			sectionId
13667		}),
13668		headerActions: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Button, {
13669			variant: "outlined",
13670			size: "medium",
13671			startIcon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FontAwesomeIcon, { icon: import_faCircleQuestion.faCircleQuestion }),
13672			onClick: () => setIsNexxenGuideOpen(true),
13673			"data-testid": "ads-dsp-nexxen-guide-btn",
13674			"data-tracking-id": "tracking-id-ads-dsp-nexxen-guide-btn",
13675			children: "Nexxen integration"
13676		}),
13677		trackingIdPrefix: "tracking-id-ads-dsp"
13678	}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(NexxenIntegrationDialog, {
13679		open: isNexxenGuideOpen,
13680		onClose: () => setIsNexxenGuideOpen(false)
13681	})] });
13682};
13683//#endregion
13684//#region client/components/newNav/components/ads/experiments/experimentsModel.ts
13685var import_faCalendarPlus = (/* @__PURE__ */ __commonJSMin(((exports) => {
13686	Object.defineProperty(exports, "__esModule", { value: true });
13687	var prefix = "far";
13688	var iconName = "calendar-plus";
13689	var width = 448;
13690	var height = 512;
13691	var aliases = [];
13692	var unicode = "f271";
13693	var svgPathData = "M120 0c13.3 0 24 10.7 24 24l0 40 160 0 0-40c0-13.3 10.7-24 24-24s24 10.7 24 24l0 40 32 0c35.3 0 64 28.7 64 64l0 288c0 35.3-28.7 64-64 64L64 480c-35.3 0-64-28.7-64-64L0 128C0 92.7 28.7 64 64 64l32 0 0-40c0-13.3 10.7-24 24-24zm0 112l-56 0c-8.8 0-16 7.2-16 16l0 288c0 8.8 7.2 16 16 16l320 0c8.8 0 16-7.2 16-16l0-288c0-8.8-7.2-16-16-16l-264 0zm104 64c13.3 0 24 10.7 24 24l0 48 48 0c13.3 0 24 10.7 24 24s-10.7 24-24 24l-48 0 0 48c0 13.3-10.7 24-24 24s-24-10.7-24-24l0-48-48 0c-13.3 0-24-10.7-24-24s10.7-24 24-24l48 0 0-48c0-13.3 10.7-24 24-24z";
13694	exports.definition = {
13695		prefix,
13696		iconName,
13697		icon: [
13698			width,
13699			height,
13700			aliases,
13701			unicode,
13702			svgPathData
13703		]
13704	};
13705	exports.faCalendarPlus = exports.definition;
13706	exports.prefix = prefix;
13707	exports.iconName = iconName;
13708	exports.width = width;
13709	exports.height = height;
13710	exports.ligatures = aliases;
13711	exports.unicode = unicode;
13712	exports.svgPathData = svgPathData;
13713	exports.aliases = aliases;
13714})))();
13715var ExperimentStatus = /* @__PURE__ */ function(ExperimentStatus) {
13716	ExperimentStatus["SCHEDULED"] = "SCHEDULED";
13717	ExperimentStatus["RUNNING"] = "RUNNING";
13718	ExperimentStatus["ENDED"] = "ENDED";
13719	return ExperimentStatus;
13720}({});
13721var experimentStatusDisplay = {
13722	["RUNNING"]: {
13723		label: "Running",
13724		color: "success",
13725		variant: "standard"
13726	},
13727	["SCHEDULED"]: {
13728		label: "Scheduled",
13729		color: "info",
13730		variant: "standard"
13731	},
13732	["ENDED"]: {
13733		label: "Ended",
13734		color: "default",
13735		variant: "outlined"
13736	}
13737};
13738/** An experiment without an end date has started or not; it never ends on its own. */
13739var getExperimentStatus = (experiment, today) => {
13740	if (experiment.start > today) return "SCHEDULED";
13741	if (!experiment.end) return "RUNNING";
13742	return experiment.end < today ? "ENDED" : "RUNNING";
13743};
13744/** Experiment names reach the video URL, so the platform only accepts letters, digits, dot, dash and underscore. */
13745var EXPERIMENT_NAME_PATTERN = /^[A-Za-z0-9_.-]*$/;
13746/**
13747* The 0-10000 hash space laid out end to end, gaps included. Splitting ranges by hand across a dozen groups is
13748* the actual work on this page, and the old app validated none of it.
13749*/
13750var getExperimentCoverage = (groups) => {
13751	const sorted = [...groups].sort((left, right) => left.min - right.min);
13752	const segments = [];
13753	let cursor = 0;
13754	sorted.forEach((group) => {
13755		if (group.min > cursor) segments.push({
13756			group: null,
13757			min: cursor,
13758			max: group.min
13759		});
13760		segments.push({
13761			group,
13762			min: group.min,
13763			max: group.max
13764		});
13765		cursor = Math.max(cursor, group.max);
13766	});
13767	if (cursor < 1e4) segments.push({
13768		group: null,
13769		min: cursor,
13770		max: EXPERIMENT_RANGE_MAX
13771	});
13772	return segments;
13773};
13774var getCoveredShare = (groups) => {
13775	return getExperimentCoverage(groups).filter((segment) => segment.group).reduce((total, segment) => total + (segment.max - segment.min), 0) / EXPERIMENT_RANGE_MAX;
13776};
13777/**
13778* Groups have to tile the whole hash space end to end. A gap silently drops those users out of the experiment
13779* and an overlap makes the group a user lands in depend on iteration order, so neither can be saved.
13780* Ranges are half-open, matching `findOverlappingGroup`.
13781*/
13782var validateExperimentCoverage = (groups) => {
13783	if (groups.length === 0) return `Add groups covering 0 to ${EXPERIMENT_RANGE_MAX}`;
13784	const sorted = [...groups].sort((left, right) => left.min - right.min);
13785	let cursor = 0;
13786	for (const group of sorted) {
13787		if (group.min > cursor) return `Nothing covers ${cursor} to ${group.min}`;
13788		if (group.min < cursor) return `"${group.name}" overlaps the group before it`;
13789		cursor = group.max;
13790	}
13791	return cursor === 1e4 ? null : `Nothing covers ${cursor} to ${EXPERIMENT_RANGE_MAX}`;
13792};
13793/**
13794* What a new experiment starts with. Nearly every experiment is a control and a test at even odds, so the
13795* common case is created without typing a range at all.
13796*/
13797var defaultExperimentGroups = () => [{
13798	id: "seed-control",
13799	name: "control",
13800	min: 0,
13801	max: EXPERIMENT_RANGE_MAX / 2
13802}, {
13803	id: "seed-test",
13804	name: "test",
13805	min: EXPERIMENT_RANGE_MAX / 2,
13806	max: EXPERIMENT_RANGE_MAX
13807}];
13808var findOverlappingGroup = (candidate, groups) => groups.find((group) => group.id !== candidate.id && candidate.min < group.max && group.min < candidate.max) ?? null;
13809var filterExperiments = (experiments, searchTerm, status, today) => {
13810	const term = searchTerm.trim().toLowerCase();
13811	return experiments.filter((experiment) => {
13812		if (status && getExperimentStatus(experiment, today) !== status) return false;
13813		if (!term) return true;
13814		return experiment.name.toLowerCase().includes(term) || experiment.accountName.toLowerCase().includes(term) || experiment.groups.some((group) => group.name.toLowerCase().includes(term));
13815	});
13816};
13817//#endregion
13818//#region client/components/newNav/components/ads/experiments/ExperimentRangeBar.tsx
13819/**
13820* The hash space an experiment splits between its groups, laid out end to end. Uncovered space is a real state,
13821* not an error: users who fall in it get no group at all.
13822*/
13823/** Neighbouring groups have to be told apart at a glance; these are theme palette keys, so they stay on-brand. */
13824var groupPaletteKeys = [
13825	"primary",
13826	"success",
13827	"warning",
13828	"secondary",
13829	"info"
13830];
13831var ExperimentRangeBar = ({ groups, dense }) => {
13832	const segments = getExperimentCoverage(groups);
13833	const covered = getCoveredShare(groups);
13834	const paletteKeyByGroupId = new Map([...groups].sort((left, right) => left.min - right.min).map((group, index) => [group.id, groupPaletteKeys[index % groupPaletteKeys.length]]));
13835	return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
13836		sx: wrapperSx(dense),
13837		children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Box, {
13838			sx: barSx(dense),
13839			children: segments.map((segment) => {
13840				const width = (segment.max - segment.min) / EXPERIMENT_RANGE_MAX * 100;
13841				const title = segment.group ? `${segment.group.name}: ${segment.min} to ${segment.max}` : `Unassigned: ${segment.min} to ${segment.max}`;
13842				const paletteKey = segment.group ? paletteKeyByGroupId.get(segment.group.id) ?? "primary" : null;
13843				return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Tooltip, {
13844					title,
13845					children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Box, {
13846						sx: segmentSx(paletteKey, width),
13847						children: !dense && width >
13847 8 && segment.group && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
13848							variant: "caption",
13849							sx: segmentLabelSx(paletteKey),
13850							children: segment.group.name
13851						})
13852					})
13853				}, `${segment.min}-${segment.max}`);
13854			})
13855		}), !dense && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
13856			sx: legendSx,
13857			children: [
13858				/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
13859					variant: "helper",
13860					color: "text.secondary",
13861					children: "0"
13862				}),
13863				/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
13864					variant: "helper",
13865					color: covered < 1 ? "warning.main" : "text.secondary",
13866					children: covered < 1 ? `${Math.round((1 - covered) * 100)}% of users fall outside every group` : "Every user falls into a group"
13867				}),
13868				/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
13869					variant: "helper",
13870					color: "text.secondary",
13871					children: 1e4
13872				})
13873			]
13874		})]
13875	});
13876};
13877var wrapperSx = (dense) => ({
13878	display: "flex",
13879	flexDirection: "column",
13880	gap: .5,
13881	width: dense ? 120 : "100%"
13882});
13883var barSx = (dense) => ({
13884	display: "flex",
13885	height: dense ? 6 : 32,
13886	borderRadius: dense ? "3px" : "8px",
13887	overflow: "hidden",
13888	backgroundColor: "grey.200"
13889});
13890var segmentSx = (paletteKey, width) => ({
13891	width: `${width}%`,
13892	backgroundColor: paletteKey ? `${paletteKey}.main` : "grey.200",
13893	borderRight: paletteKey ? 1 : 0,
13894	borderColor: "background.paper",
13895	display: "flex",
13896	alignItems: "center",
13897	justifyContent: "center",
13898	overflow: "hidden",
13899	minWidth: 0
13900});
13901var segmentLabelSx = (paletteKey) => ({
13902	color: `${paletteKey ?? "primary"}.contrastText`,
13903	whiteSpace: "nowrap",
13904	paddingX: .5
13905});
13906var legendSx = {
13907	display: "flex",
13908	justifyContent: "space-between",
13909	gap: 1
13910};
13911//#endregion
13912//#region client/components/newNav/components/ads/experiments/ExperimentValuesMatrix.tsx
13913var cellKey = (groupId, propertyId) => `${groupId}:${propertyId}`;
13914var propertyField = [{
13915	key: "propertyKey",
13916	label: "Property key",
13917	type: "text",
13918	help: "The name the video reads this value under. One column of the matrix.",
13919	validate: required("Enter the property key")
13920}];
13921/**
13922* One group's row. The values of a group are a child collection of that group, so each row owns its own read -
13923* there is no experiment-wide read of them in the ads schema.
13924*/
13925var ValuesRow = ({ group, properties, drafts, readOnly, onLoaded, onCellChange }) => {
13926	const values = useAdsSpaceEntities(GqlClientAdsSpaceEntityKind.EXPERIMENT_PROPERTY_VALUE, group.id);
13927	(0, import_react.useEffect)(() => onLoaded(group.id, values.rows), [
13928		group.id,
13929		values.rows,
13930		onLoaded
13931	]);
13932	const storedValue = (propertyId) => asText(values.rows.find((row) => asText(row.propertyKeyId) === propertyId)?.value);
13933	return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.TableRow, {
13934		"data-testid": `ads-experiment-values-row-${group.id}`,
13935		children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.TableCell, {
13936			sx: (0, import_dist$2.combineSxProps)(bodyCellSx, groupCellSx),
13937			children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
13938				variant: "subtitle2",
13939				children: group.name
13940			}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
13941				variant: "caption",
13942				color: "text.secondary",
13943				children: `${group.min} - ${group.max}`
13944			})]
13945		}), properties.map((property) => {
13946			const key = cellKey(group.id, property.id);
13947			return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, {
13948				sx: bodyCellSx,
13949				children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TextField, {
13950					size: "small",
13951					fullWidth: true,
13952					disabled: readOnly || values.loading,
13953					value: drafts[key] ?? storedValue(property.id),
13954					onChange: (event) => onCellChange(group.id, property.id, event.target.value),
13955					inputProps: { "data-tracking-id": "tracking-id-ads-experiment-value-input" }
13956				})
13957			}, property.id);
13958		})]
13959	});
13960};
13961/**
13962* Property keys down the columns and groups down the rows, because that is the shape of the data: one
13963* `experiment_property_value` row per group per key, and a blank cell is a group with no value for that key.
13964*/
13965var ExperimentValuesMatrix = ({ experimentId, groups, readOnly }) => {
13966	const properties = useAdsSpaceEntities(GqlClientAdsSpaceEntityKind.EXPERIMENT_PROPERTY, experimentId);
13967	const values = useAdsSpaceEntities(GqlClientAdsSpaceEntityKind.EXPERIMENT_PROPERTY_VALUE, void 0, { skipRead: true });
13968	const [drafts, setDrafts] = (0, import_react.useState)({});
13969	const [storedByGroup, setStoredByGroup] = (0, import_react.useState)({});
13970	const [isAddingProperty, setIsAddingProperty] = (0, import_react.useState)(false);
13971	const [propertyToDelete, setPropertyToDelete] = (0, import_react.useState)(null);
13972	const [isSaveOpen, setIsSaveOpen] = (0, import_react.useState)(false);
13973	const onLoaded = import_react.useCallback((groupId, rows) => setStoredByGroup((current) => current[groupId] === rows ? current : {
13974		...current,
13975		[groupId]: rows
13976	}), []);
13977	const storedCell = (groupId, propertyId) => (storedByGroup[groupId] ?? []).find((row) => asText(row.propertyKeyId) === propertyId);
13978	const changedKeys = Object.keys(drafts).filter((key) => {
13979		const [groupId, propertyId] = key.split(":");
13980		return drafts[key] !== asText(storedCell(groupId, propertyId)?.value);
13981	});
13982	const validateProperty = (row, otherRows) => {
13983		const wanted = asText(row.propertyKey).trim().toLowerCase();
13984		return otherRows.some((other) => asText(other.propertyKey).trim().toLowerCase() === wanted) ? { propertyKey: "This experiment already has a property with that key" } : {};
13985	};
13986	const saveValues = async (comment) => {
13987		const byGroup = {};
13988		changedKeys.forEach((key) => {
13989			const groupId = key.split(":")[0];
13990			byGroup[groupId] = [...byGroup[groupId] ?? [], key];
13991		});
13992		for (const [groupId, keys] of Object.entries(byGroup)) {
13993			const changed = [];
13994			const deletedIds = [];
13995			keys.forEach((key) => {
13996				const propertyId = key.split(":")[1];
13997				const stored = storedCell(groupId, propertyId);
13998				if (!drafts[key].trim()) {
13999					if (stored) deletedIds.push(stored.id);
14000					return;
14001				}
14002				changed.push({
14003					id: stored?.id ?? "",
14004					value: drafts[key].trim(),
14005					propertyKeyId: Number(propertyId)
14006				});
14007			});
14008			if (changed.length === 0 && deletedIds.length === 0) continue;
14009			await values.save(changed, comment, deletedIds.length > 0 ? deletedIds : void 0, groupId);
14010		}
14011		setDrafts({});
14012		setIsSaveOpen(false);
14013	};
14014	return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
14015		sx: matrixSx,
14016		children: [
14017			/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
14018				sx: headerSx$2,
14019				children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
14020					variant: "h5",
14021					children: "Experiment values"
14022				}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
14023					variant: "body1",
14024					color: "text.secondary",
14025					children: "One value per group per property key. The video reads the value of the group its user landed in; a blank cell means that group has no value for that key."
14026				})] }
14026), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
14027					sx: headerActionsSx$1,
14028					children: [changedKeys.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Button, {
14029						variant: "contained",
14030						size: "small",
14031						onClick: () => setIsSaveOpen(true),
14032						"data-tracking-id": "tracking-id-ads-experiment-values-save-btn",
14033						children: `Save ${changedKeys.length} ${changedKeys.length === 1 ? "value" : "values"}`
14034					}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Button, {
14035						variant: "outlined",
14036						size: "small",
14037						disabled: readOnly,
14038						startIcon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FontAwesomeIcon, { icon: import_faPlus.faPlus }),
14039						onClick: () => setIsAddingProperty(true),
14040						"data-tracking-id": "tracking-id-ads-experiment-values-add-property-btn",
14041						children: "Add property"
14042					})]
14043				})]
14044			}),
14045			properties.rows.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
14046				variant: "body1",
14047				color: "text.secondary",
14048				children: "This experiment has no property keys, so its groups carry no values. Add one to start."
14049			}) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableContainer, {
14050				component: import_material.Paper,
14051				variant: "outlined",
14052				children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Table, {
14053					size: "small",
14054					children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableHead, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.TableRow, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, {
14055						sx: headCellSx,
14056						children: "Group"
14057					}), properties.rows.map((property) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, {
14058						sx: headCellSx,
14059						children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
14060							sx: columnHeadSx,
14061							children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: asText(property.propertyKey) }
14061), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.TruffleIconButton, {
14062								size: "small",
14063								disabled: readOnly,
14064								onClick: () => setPropertyToDelete(property),
14065								"data-tracking-id": "tracking-id-ads-experiment-values-remove-property-btn",
14066								children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FontAwesomeIcon, { icon: import_faTrashCan.faTrashCan })
14067							})]
14068						})
14069					}, property.id))] }) }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableBody, { children: groups.map((group) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ValuesRow, {
14070						group,
14071						properties: properties.rows,
14072						drafts,
14073						readOnly,
14074						onLoaded,
14075						onCellChange: (groupId, propertyId, value) => setDrafts((current) => ({
14076							...current,
14077							[cellKey(groupId, propertyId)]: value
14078						}))
14079					}, group.id)) })]
14080				})
14081			}),
14082			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsRowDialog, {
14083				open: isAddingProperty,
14084				title: "Add property",
14085				fields: propertyField,
14086				row: null,
14087				otherRows: properties.rows,
14088				validateRow: validateProperty,
14089				onClose: () => setIsAddingProperty(false),
14090				onSubmit: (row) => {
14091					setIsAddingProperty(false);
14092					properties.save([{
14093						...row,
14094						id: ""
14095					}], `Added experiment property ${asText(row.propertyKey)}`).catch(() => void 0);
14096				},
14097				trackingIdPrefix: "tracking-id-ads-experiment-property"
14098			}),
14099			/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Dialog, {
14100				open: Boolean(propertyToDelete),
14101				onClose: () => setPropertyToDelete(null),
14102				PaperProps: { sx: confirmPaperSx },
14103				children: [
14104					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.TruffleDialogTitle, {
14105						CloseIconButtonProps: {
14106							onClick: () => setPropertyToDelete(null),
14107							"data-tracking-id": "tracking-id-ads-experiment-property-delete-close-btn"
14108						},
14109						children: "Delete this property"
14110					}),
14111					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.DialogContent, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
14112						variant: "body1",
14113						color: "text.secondary",
14114						children: `Deleting "${asText(propertyToDelete?.propertyKey)}" also deletes every group's value for it. The values have to go first, so this cannot leave one column half-removed.`
14115					}) }),
14116					/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_dist$2.TruffleDialogActions, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Button, {
14117						variant: "text",
14118						size: "large",
14119						onClick: () => setPropertyToDelete(null),
14120						"data-tracking-id": "tracking-id-ads-experiment-property-delete-cancel-btn",
14121						children: "Cancel"
14122					}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Button, {
14123						variant: "contained",
14124						color: "error",
14125						size: "large",
14126						onClick: () => {
14127							const removed = propertyToDelete;
14128							setPropertyToDelete(null);
14129							properties.save([], `Removed experiment property ${asText(removed.propertyKey)}`, [removed.id]).catch(() => void 0);
14130						},
14131						"data-tracking-id": "tracking-id-ads-experiment-property-delete-confirm-btn",
14132						children: "Delete"
14133					})] })
14134				]
14135			}),
14136			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsSaveCommentDialog, {
14137				open: isSaveOpen,
14138				summary: `${changedKeys.length} ${changedKeys.length === 1 ? "experiment value" : "experiment values"}`,
14139				saving: values.saving,
14140				onCancel: () => setIsSaveOpen(false),
14141				onConfirm: (comment) => void saveValues(comment).catch(() => void 0),
14142				trackingIdPrefix: "tracking-id-ads-experiment-values"
14143			})
14144		]
14145	});
14146};
14147var matrixSx = {
14148	display: "flex",
14149	flexDirection: "column",
14150	gap: 2
14151};
14152var headerSx$2 = {
14153	display: "flex",
14154	justifyContent: "space-between",
14155	alignItems: "flex-start",
14156	gap: 2
14157};
14158var headerActionsSx$1 = {
14159	display: "flex",
14160	gap: 1,
14161	flexShrink: 0
14162};
14163var columnHeadSx = {
14164	display: "flex",
14165	alignItems: "center",
14166	gap: .5
14167};
14168var groupCellSx = {
14169	display: "flex",
14170	flexDirection: "column"
14171};
14172var confirmPaperSx = {
14173	width: 480,
14174	maxWidth: "calc(100vw - 64px)"
14175};
14176//#endregion
14177//#region client/components/newNav/components/ads/experiments/experimentsStore.ts
14178var toExperiment = (experiment) => ({
14179	id: experiment.id,
14180	name: experiment.name,
14181	accountName: experiment.accountName,
14182	salt: experiment.salt ?? "",
14183	start: experiment.startDate ?? "",
14184	end: experiment.endDate ?? "",
14185	groups: experiment.groups.map((group) => ({
14186		id: group.id,
14187		name: group.name,
14188		min: group.min,
14189		max: group.max
14190	}))
14191});
14192/**
14193* The account's experiments, from the ads schema. The campaign optimization pickers read the same rows.
14194* `loading` matters to every caller: an empty list before the read lands is indistinguishable from an account
14195* with no experiments, and a detail page that treats it as "not found" redirects away from a valid URL.
14196*/
14197var useExperiments = () => {
14198	const { data, loading } = useCurrentAccountAdsSpaceExperimentsQuery();
14199	const rows = data?.currentAccount?.adsSpace?.experiments;
14200	return {
14201		experiments: (0, import_react.useMemo)(() => (rows ?? []).map(toExperiment), [rows]),
14202		loading
14203	};
14204};
14205/**
14206* Writes go through the space's generic entity path, so an experiment save is audited like every other one.
14207* Groups are a child collection keyed by the experiment's id, which the ads services server assigns - so a
14208* create reads the stored experiment back and then attaches its groups to that id.
14209*/
14210var useExperimentActions = () => {
14211	const experiments = useAdsSpaceEntities(GqlClientAdsSpaceEntityKind.EXPERIMENT);
14212	const groups = useAdsSpaceEntities(GqlClientAdsSpaceEntityKind.EXPERIMENT_GROUP, void 0, { skipRead: true });
14213	const toValues = (input) => ({
14214		id: "new",
14215		name: input.name,
14216		salt: input.salt,
14217		startDate: input.start,
14218		endDate: input.end
14219	});
14220	const create = async (input, newGroups, comment) => {
14221		const stored = await experiments.create(toValues(input), comment);
14222		if (!stored || newGroups.length === 0) return;
14223		await groups.save(newGroups.map((group) => ({
14224			id: "",
14225			name: group.name,
14226			min: group.min,
14227			max: group.max,
14228			active: true
14229		})), comment, void 0, stored.id);
14230	};
14231	return {
14232		saving: experiments.saving || groups.saving,
14233		create,
14234		update: (experimentId, input, comment) => experiments.save([{
14235			...toValues(input),
14236			id: experimentId
14237		}], comment)
14238	};
14239};
14240//#endregion
14241//#region client/components/newNav/components/ads/experiments/ExtendEndDateDialog.tsx
14242var extensionOptions = [
14243	7,
14244	14,
14245	30,
14246	90
14247];
14248/**
14249* Pushing an experiment's end date out is the single most common action on this page - 89 of the 228 edits in a
14250* year. It gets one click and a preview instead of a datetime picker inside a form.
14251*/
14252var ExtendEndDateDialog = ({ experiment, onClose, onExtend }) => {
14253	const [days, setDays] = (0, import_react.useState)(extensionOptions[0]);
14254	const [comment, setComment] = (0, import_react.useState)("");
14255	(0, import_react.useEffect)(() => {
14256		if (experiment) {
14257			setDays(extensionOptions[0]);
14258			setComment("");
14259		}
14260	}, [experiment]);
14261	const newEnd = experiment ? addDays(experiment.end, days) : "";
14262	return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Dialog, {
14263		open: Boolean(experiment),
14264		onClose,
14265		maxWidth: "sm",
14266		fullWidth: true,
14267		children: [
14268			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.TruffleDialogTitle, {
14269				CloseIconButtonProps: {
14270					onClick: onClose,
14271					"data-tracking-id": "tracking-id-ads-experiments-extend-close-btn"
14272				},
14273				children: "Extend the experiment"
14274			}),
14275			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.DialogContent, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
14276				sx: contentSx$4,
14277				children: [
14278					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
14279						variant: "body1",
14280						color: "text.secondary",
14281						children: experiment ? `${experiment.name} currently ends on ${TimeUtils.getDesignSystemReadableTimeDate(experiment.end, true)}.` : ""
14282					}),
14283					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.TruffleToggleButtonGroup, {
14284						exclusive: true,
14285						size: "small",
14286						value: days,
14287						onChange: (_event, next) => next && setDays(next),
14288						children: extensionOptions.map((option) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.ToggleButton, {
14289							value: option,
14290							sx: { flex: 1 },
14291							"data-tracking-id": `tracking-id-ads-experiments-extend-${option}-days-btn`,
14292							children: `${option} days`
14293						}, option))
14294					}),
14295					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
14296						variant: "body1",
14297						children: newEnd ? `New end date: ${TimeUtils.getDesignSystemReadableTimeDate(newEnd, true)}` : ""
14298					}),
14299					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsChangeCommentField, {
14300						value: comment,
14301						onChange: setComment,
14302						trackingId: "tracking-id-ads-experiments-extend-comment-input"
14303					})
14304				]
14305			}) }),
14306			/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_dist$2.TruffleDialogActions, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Button, {
14307				variant: "text",
14308				size: "large",
14309				onClick: onClose,
14310				"data-tracking-id": "tracking-id-ads-experiments-extend-cancel-btn",
14311				children: "Cancel"
14312			}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Button, {
14313				variant: "contained",
14314				size: "large",
14315				disabled: !comment.trim(),
14316				onClick: () => {
14317					if (experiment) onExtend(experiment.id, newEnd, comment.trim());
14318				},
14319				"data-tracking-id": "tracking-id-ads-experiments-extend-confirm-btn",
14320				children: "Extend"
14321			})] })
14322		]
14323	});
14324};
14325var contentSx$4 = {
14326	display: "flex",
14327	flexDirection: "column",
14328	gap: 2,
14329	paddingTop: 1
14330};
14331//#endregion
14332//#region client/components/newNav/components/ads/experiments/ExperimentDetailPage.tsx
14333var experimentGroups = [{
14334	id: "identity",
14335	title: "Experiment",
14336	fields: [
14337		{
14338			key: "name",
14339			label: "Name",
14340			type: "text",
14341			help: "Experiment name",
14342			validate: matches(EXPERIMENT_NAME_PATTERN, "Use letters, digits, dots, dashes and underscores only")
14343		},
14344		{
14345			key: "accountName",
14346			label: "Account name",
14347			type: "readOnly",
14348			help: "The ads account this experiment belongs to. Set when it is created."
14349		},
14350		{
14351			key: "salt",
14352			label: "Salt",
14353			type: "text",
14354			wide: true,
14355			help: "A salt for the one-way-function from the user id to the user's group"
14356		}
14357	]
14358}, {
14359	id: "schedule",
14360	title: "Schedule",
14361	description: "Dates are UTC and run from midnight to midnight.",
14362	fields: [{
14363		key: "start",
14364		label: "Start",
14365		type: "date",
14366		help: "Start date of the experiment",
14367		validate: required("Pick a start date")
14368	}, {
14369		key: "end",
14370		label: "End (optional)",
14371		type: "date",
14372		help: "Leave it empty to run until someone stops it. An experiment with no end date never reads as ended."
14373	}]
14374}];
14375var groupFields = [
14376	{
14377		key: "name",
14378		label: "Name",
14379		type: "text",
14380		validate: required("Enter the group name")
14381	},
14382	{
14383		key: "min",
14384		label: "Min",
14385		type: "number",
14386		min: 0,
14387		max: EXPERIMENT_RANGE_MAX,
14388		help: "Start of the group range (total range is 0-10000)"
14389	},
14390	{
14391		key: "max",
14392		label: "Max",
14393		type: "number",
14394		min: 0,
14395		max: EXPERIMENT_RANGE_MAX,
14396		help: "End of the group range (total range is 0-10000)"
14397	}
14398];
14399var toGroups = (rows) => rows.map((row) => ({
14400	id: row.id,
14401	name: asText(row.name),
14402	min: Number(asText(row.min)) || 0,
14403	max: Number(asText(row.max)) || 0
14404}));
14405/** The old app validated none of this, and hand-split ranges across a dozen groups is the job on this page. */
14406var validateGroupRow = (row, otherRows) => {
14407	const min = Number(asText(row.min));
14408	const max = Number(asText(row.max));
14409	if (Number.isNaN(min) || min < 0 || min > 1e4) return { min: `The range runs from 0 to ${EXPERIMENT_RANGE_MAX}` };
14410	if (Number.isNaN(max) || max < 0 || max > 1e4) return { max: `The range runs from 0 to ${EXPERIMENT_RANGE_MAX}` };
14411	if (max <= min) return { max: "Max has to be greater than min" };
14412	const clash = findOverlappingGroup({
14413		id: row.id,
14414		min,
14415		max
14416	}, toGroups(otherRows));
14417	return clash ? { min: `This range overlaps "${clash.name}" (${clash.min} to ${clash.max})` } : {};
14418};
14419var ExperimentDetailPage = () => {
14420	const accountId = useActiveAccountId();
14421	const { experimentId } = useParams();
14422	const { experiments, loading } = useExperiments();
14423	const experiment = experiments.find((candidate) => candidate.id === experimentId);
14424	if (loading && !experiment) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Skeleton, {
14425		variant: "rounded",
14426		height: 400,
14427		sx: loadingSx
14428	});
14429	if (!experiment) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Redirect, { to: (0, import_dist$1.newNavAdsExperimentsPath)({ accountId }) });
14430	return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ExperimentDetail, { experiment }, experiment.id);
14431};
14432var ExperimentDetail = ({ experiment }) => {
14433	const accountId = useActiveAccountId();
14434	const { userRoles } = useUserData();
14435	const canEdit = isPermitted(userRoles, permissions.adsEditCampaignSettings);
14436	const actions = useExperimentActions();
14437	const groupRows = useAdsSpaceChildRows(GqlClientAdsSpaceEntityKind.EXPERIMENT_GROUP, experiment.id);
14438	const today = (0, import_react.useMemo)(() => (/* @__PURE__ */ new Date()).toISOString().slice(0, 10), []);
14439	const [isExtending, setIsExtending] = (0, import_react.useState)(false);
14440	const form = useAdsSettingsForm(experimentGroups, {
14441		name: experiment.name,
14442		accountName: experiment.accountName,
14443		salt: experiment.salt,
14444		start: experiment.start,
14445		end: experiment.end
14446	}, "The experiment", async (values, comment) => {
14447		await actions.update(experiment.id, {
14448			name: asText(values.name),
14449			salt: asText(values.salt),
14450			start: asText(values.start),
14451			end: asText(values.end)
14452		}, comment);
14453		await groupRows.persist(comment);
14454	}, actions.saving || groupRows.saving, [groupRows]);
14455	const status = experimentStatusDisplay[getExperimentStatus(experiment, today)];
14456	const coverageProblem = groupRows.changeCount > 0 ? validateExperimentCoverage(toGroups(groupRows.rows)) : null;
14457	return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
14458		sx: adsFixedPageSx,
14459		children: [
14460			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Button, {
14461				component: Link$6,
14462				to: (0, import_dist$1.newNavAdsExperimentsPath)({ accountId }),
14463				variant: "text",
14464				size: "small",
14465				sx: backLinkSx,
14466				startIcon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FontAwesomeIcon, { icon: import_faAngleLeft.faAngleLeft }),
14467				"data-tracking-id": "tracking-id-ads-experiment-back-btn",
14468				children: "Experiments"
14469			}),
14470			/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(AdsPageHeader, {
14471				area: "Programmatic / Experiments",
14472				title: experiment.name,
14473				children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.Label, {
14474					size: "small",
14475					label: status.label,
14476					color: status.color,
14477					variant: status.variant
14478				}), canEdit && experiment.end && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Button, {
14479					variant: "outlined",
14480					size: "medium",
14481					startIcon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FontAwesomeIcon, { icon: import_faCalendarPlus.faCalendarPlus }),
14482					onClick: () => setIsExtending(true),
14483					"data-tracking-id": "tracking-id-ads-experiment-extend-btn",
14484					children: "Extend end date"
14485				})]
14486			}),
14487			/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Paper, {
14488				variant: "outlined",
14489				sx: panelSx,
14490				children: [
14491					/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
14492						sx: coverageSx,
14493						children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
14494							variant: "h5",
14495							children: "Group coverage"
14496						}), groupRows.loading && groupRows.rows.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Skeleton, {
14497							variant: "rounded",
14498							animation: "wave",
14499							height: 32
14500						}) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ExperimentRangeBar, { groups: toGroups(groupRows.rows) })]
14501					}),
14502					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsSettingsForm, {
14503						groups: experimentGroups,
14504						form,
14505						readOnly: !canEdit,
14506						saveBlockedReason: coverageProblem ?? void 0,
14507						trackingIdPrefix: "tracking-id-ads-experiment",
14508						children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsChildTable, {
14509							title: "Groups",
14510							description: "Each group claims a slice of the 0 to 10000 hash space. The group name is what the video receives.",
14511							fields: groupFields,
14512							rows: groupRows.rows,
14513							addLabel: "Add group",
14514							emptyText: "This experiment has no groups, so no user falls into it",
14515							rowTitle: (row) => asText(row.name),
14516							readOnly: !canEdit,
14517							onChange: groupRows.setRows,
14518							validateRow: validateGroupRow,
14519							trackingIdPrefix: "tracking-id-ads-experiment-group"
14520						})
14521					}),
14522					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(ExperimentValuesMatrix, {
14523						experimentId: experiment.id,
14524						groups: experiment.groups,
14525						readOnly: !canEdit
14526					})
14527				]
14528			}),
14529			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(ExtendEndDateDialog, {
14530				experiment: isExtending ? experiment : null,
14531				onClose: () => setIsExtending(false),
14532				onExtend: (id, end, comment) => {
14533					actions.update(id, { end }, comment).catch(() => void 0);
14534					form.onChange("end", end);
14535					setIsExtending(false);
14536				}
14537			})
14538		]
14539	});
14540};
14541var loadingSx = { margin: "32px" };
14542var backLinkSx = {
14543	alignSelf: "flex-start",
14544	marginBottom: -2
14545};
14546var panelSx = {
14547	padding: 3,
14548	display: "flex",
14549	flexDirection: "column",
14550	gap: 4,
14551	flex: 1,
14552	minHeight: 0
14553};
14554var coverageSx = {
14555	display: "flex",
14556	flexDirection: "column",
14557	gap: 2
14558};
14559//#endregion
14560//#region client/components/newNav/components/ads/experiments/NewExperimentDialog.tsx
14561var import_faFlask = require_faFlask();
14562/**
14563* The one page in the ads configuration model with real creation traffic - 75 new experiments in a year - so
14564* unlike the campaign pages it gets a create flow rather than edit-only. The groups are defined here too: an
14565* experiment with no groups catches nobody, so creating one and then leaving is never the finished job.
14566*/
14567var NewExperimentDialog = ({ open, existingNames, onClose, onCreate }) => {
14568	const [name, setName] = (0, import_react.useState)("");
14569	const [salt, setSalt] = (0, import_react.useState)("");
14570	const [isSaltOwnValue, setIsSaltOwnValue] = (0, import_react.useState)(false);
14571	const [start, setStart] = (0, import_react.useState)("");
14572	const [end, setEnd] = (0, import_react.useState)("");
14573	const [groups, setGroups] = (0, import_react.useState)(defaultExperimentGroups());
14574	const [nameError, setNameError] = (0, import_react.useState)(null);
14575	const [dateError, setDateError] = (0, import_react.useState)(null);
14576	const [comment, setComment] = (0, import_react.useState)("");
14577	(0, import_react.useEffect)(() => {
14578		if (open) {
14579			setName("");
14580			setSalt("");
14581			setIsSaltOwnValue(false);
14582			setStart("");
14583			setEnd("");
14584			setGroups(defaultExperimentGroups());
14585			setNameError(null);
14586			setDateError(null);
14587			setComment("");
14588		}
14589	}, [open]);
14590	const handleNameChange = (value) => {
14591		setName(value);
14592		if (!isSaltOwnValue) setSalt(value);
14593		if (!EXPERIMENT_NAME_PATTERN.test(value)) {
14594			setNameError("Use letters, digits, dots, dashes and underscores only");
14595			return;
14596		}
14597		setNameError(existingNames.includes(value) ? "An experiment with this name already exists" : null);
14598	};
14599	const handleDatesChange = (nextStart, nextEnd) => {
14600		setStart(nextStart);
14601		setEnd(nextEnd);
14602		setDateError(nextStart && nextEnd && nextEnd <= nextStart ? "The end date has to come after the start date" : null);
14603	};
14604	const updateGroup = (index, change) => setGroups(groups.map((group, position) => position === index ? {
14605		...group,
14606		...change
14607	} : group));
14608	const addGroup = () => {
14609		const highest = groups.reduce((edge, group) => Math.max(edge, group.max), 0);
14610		setGroups([...groups, {
14611			id: v4(),
14612			name: "",
14613			min: highest,
14614			max: EXPERIMENT_RANGE_MAX
14615		}]);
14616	};
14617	const coverageProblem = validateExperimentCoverage(groups);
14618	const unnamedGroup = groups.some((group) => !group.name.trim());
14619	const canCreate = Boolean(name && start) && !nameError && !dateError && !coverageProblem && !unnamedGroup;
14620	return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Dialog, {
14621		open,
14622		onClose,
14623		PaperProps: { sx: paperSx$1 },
14624		children: [
14625			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.TruffleDialogTitle, {
14626				CloseIconButtonProps: {
14627					onClick: onClose,
14628					"data-tracking-id": "tracking-id-ads-experiments-new-close-btn"
14629				},
14630				children: "New experiment"
14631			}),
14632			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.DialogContent, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
14633				sx: contentSx$3,
14634				children: [
14635					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TextField, {
14636						label: "Name",
14637						value: name,
14638						error: Boolean(nameError),
14639						size: "small",
14640						fullWidth: true,
14641						onChange: (event) => handleNameChange(event.target.value),
14642						inputProps: { "data-tracking-id": "tracking-id-ads-experiments-new-name-input" }
14643					}),
14644					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
14645						variant: "helper",
14646						color: nameError ? "error.main" : "text.secondary",
14647						children: nameError ?? "The name reaches the video URL, so it takes letters, digits, dots, dashes and underscores."
14648					}),
14649					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TextField, {
14650						label: "Salt",
14651						value: salt,
14652						size: "small",
14653						fullWidth: true,
14654						onChange: (event) => {
14655							setIsSaltOwnValue(true);
14656							setSalt(event.target.value);
14657						},
14658						inputProps: { "data-tracking-id": "tracking-id-ads-experiments-new-salt-input" }
14659					}),
14660					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
14661						variant: "helper",
14662						color: "text.secondary",
14663						children: "Defaults to the name. Change it only to re-randomise which users land in which group."
14664					}),
14665					/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
14666						sx: datesSx,
14667						children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TextField, {
14668							label: "Start",
14669							type: "date",
14670							value: start,
14671							error: Boolean(dateError),
14672							size: "small",
14673							fullWidth: true,
14674							onChange: (event) => handleDatesChange(event.target.value, end),
14675							inputProps: { "data-tracking-id": "tracking-id-ads-experiments-new-start-input" }
14676						}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TextField, {
14677							label: "End (optional)",
14678							type: "date",
14679							value: end,
14680							error: Boolean(dateError),
14681							size: "small",
14682							fullWidth: true,
14683							onChange: (event) => handleDatesChange(start, event.target.value),
14684							inputProps: { "data-tracking-id": "tracking-id-ads-experiments-new-end-input" }
14685						})]
14686					}),
14687					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
14688						variant: "helper",
14689						color: dateError ? "error.main" : "text.secondary",
14690						children: dateError ?? "Dates are UTC and run from midnight to midnight. Leave the end empty to run until it is stopped."
14691					}),
14692					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
14693						variant: "h5",
14694						sx: groupsHeadingSx,
14695						children: "Groups"
14696					}),
14697					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(ExperimentRangeBar, { groups }),
14698					groups.map((group, index) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
14699						sx: groupRowSx,
14700						children: [
14701							/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TextField, {
14702								label: "Name",
14703								value: group.name,
14704								size: "small",
14705								fullWidth: true,
14706								onChange: (event) => updateGroup(index, { name: event.target.value }),
14707								inputProps: { "data-tracking-id": "tracking-id-ads-experiments-new-group-name-input" }
14708							}),
14709							/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TextField, {
14710								label: "Min",
14711								type: "number",
14712								value: group.min,
14713								size: "small",
14714								onChange: (event) => updateGroup(index, { min: Number(event.target.value) }),
14715								inputProps: {
14716									min: 0,
14717									max: EXPERIMENT_RANGE_MAX,
14718									"data-tracking-id": "tracking-id-ads-experiments-new-group-min-input"
14719								}
14720							}),
14721							/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TextField, {
14722								label: "Max",
14723								type: "number",
14724								value: group.max,
14725								size: "small",
14726								onChange: (event) => updateGroup(index, { max: Number(event.target.value) }),
14727								inputProps: {
14728									min: 0,
14729									max: EXPERIMENT_RANGE_MAX,
14730									"data-tracking-id": "tracking-id-ads-experiments-new-group-max-input"
14731								}
14732							}),
14733							/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.TruffleIconButton, {
14734								size: "small",
14735								disabled: groups.length === 1,
14736								onClick: () => setGroups(groups.filter((_, position) => position !== index)),
14737								"data-tracking-id": "tracking-id-ads-experiments-new-group-remove-btn",
14738								children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FontAwesomeIcon, { icon: import_faTrashCan.faTrashCan })
14739							})
14740						]
14741					}, group.id)),
14742					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Box, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Button, {
14743						variant: "text",
14744						size: "small",
14745						startIcon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FontAwesomeIcon, { icon: import_faPlus.faPlus }),
14746						onClick: addGroup,
14747						"data-tracking-id": "tracking-id-ads-experiments-new-group-add-btn",
14748						children: "Add group"
14749					}) }),
14750					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
14751						variant: "helper",
14752						color: coverageProblem || unnamedGroup ? "error.main" : "text.secondary",
14753						children: coverageProblem ?? (unnamedGroup ? "Every group needs a name - it is what the video receives." : `The groups tile 0 to 10000 with no gap and no overlap.`)
14754					}),
14755					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Divider, { sx: reasonDividerSx }),
14756					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsChangeCommentField, {
14757						label: "Reason (optional)",
14758						value: comment,
14759						onChange: setComment,
14760						trackingId: "tracking-id-ads-experiments-new-comment-input"
14761					})
14762				]
14763			}) }),
14764			/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_dist$2.TruffleDialogActions, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Button, {
14765				variant: "text",
14766				size: "large",
14767				onClick: onClose,
14768				"data-tracking-id": "tracking-id-ads-experiments-new-cancel-btn",
14769				children: "Cancel"
14770			}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Button, {
14771				variant: "contained",
14772				size: "large",
14773				disabled: !canCreate,
14774				onClick: () => onCreate({
14775					name,
14776					salt,
14777					start,
14778					end
14779				}, groups, comment.trim()),
14780				"data-tracking-id": "tracking-id-ads-experiments-new-create-btn",
14781				children: "Create experiment"
14782			})] })
14783		]
14784	});
14785};
14786var paperSx$1 = {
14787	width: 640,
14788	maxWidth: "calc(100vw - 64px)"
14789};
14790var contentSx$3 = {
14791	display: "flex",
14792	flexDirection: "column",
14793	gap: 1,
14794	paddingTop: 1
14795};
14796var datesSx = {
14797	display: "flex",
14798	gap: 2
14799};
14800var groupsHeadingSx = { marginTop: 2 };
14801var groupRowSx = {
14802	display: "flex",
14803	gap: 1,
14804	alignItems: "center",
14805	"& .MuiTextField-root:not(:first-of-type)": {
14806		width: 96,
14807		flexShrink: 0
14808	}
14809}
14809;
14810var reasonDividerSx = { marginTop: 2 };
14811//#endregion
14812//#region client/components/newNav/components/ads/experiments/ExperimentsPage.tsx
14813var ExperimentsPage = () => {
14814	const history = useHistory();
14815	const accountId = useActiveAccountId();
14816	const { userRoles } = useUserData();
14817	const canEdit = isPermitted(userRoles, permissions.adsEditCampaignSettings);
14818	const { experiments, loading } = useExperiments();
14819	const actions = useExperimentActions();
14820	const today = (0, import_react.useMemo)(() => (/* @__PURE__ */ new Date()).toISOString().slice(0, 10), []);
14821	const [searchTerm, setSearchTerm] = (0, import_react.useState)("");
14822	const [statusFilter, setStatusFilter] = (0, import_react.useState)(null);
14823	const [filterAnchorEl, setFilterAnchorEl] = (0, import_react.useState)(null);
14824	const [menuAnchorEl, setMenuAnchorEl] = (0, import_react.useState)(null);
14825	const [menuExperiment, setMenuExperiment] = (0, import_react.useState)(null);
14826	const [extending, setExtending] = (0, import_react.useState)(null);
14827	const [isNewDialogOpen, setIsNewDialogOpen] = (0, import_react.useState)(false);
14828	const visibleExperiments = (0, import_react.useMemo)(() => filterExperiments(experiments, searchTerm, statusFilter, today), [
14829		experiments,
14830		searchTerm,
14831		statusFilter,
14832		today
14833	]);
14834	const closeMenu = () => {
14835		setMenuAnchorEl(null);
14836		setMenuExperiment(null);
14837	};
14838	const menuActions = (experiment) => [{
14839		label: "Extend end date",
14840		icon: import_faCalendarPlus.faCalendarPlus,
14841		disabled: !experiment.end,
14842		disabledReason: "This experiment has no end date, so there is nothing to push out.",
14843		onClick: () => {
14844			closeMenu();
14845			setExtending(experiment);
14846		}
14847	}, {
14848		label: "Edit experiment",
14849		icon: import_faPen.faPen,
14850		onClick: () => {
14851			closeMenu();
14852			history.push((0, import_dist$1.newNavAdsExperimentPath)({
14853				accountId,
14854				experimentId: experiment.id
14855			}));
14856		}
14857	}];
14858	const newExperimentButton = /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Button, {
14859		variant: "contained",
14860		size: "medium",
14861		startIcon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FontAwesomeIcon, { icon: import_faPlus.faPlus }),
14862		onClick: () => setIsNewDialogOpen(true),
14863		"data-tracking-id": "tracking-id-ads-experiments-new-btn",
14864		children: "New experiment"
14865	});
14866	return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
14867		sx: adsPageSx,
14868		children: [
14869			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsPageHeader, {
14870				area: "Programmatic / Experiments",
14871				title: "Experiments",
14872				count: experiments.length,
14873				subtitle: "Splits users into groups by a hash of their id. The matching group name is handed to the video.",
14874				children: experiments.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
14875					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.Filter, {
14876						placeholderValue: "All statuses",
14877						selectedValue: statusFilter ? experimentStatusDisplay[statusFilter].label : null,
14878						onClear: () => setStatusFilter(null),
14879						popoverTitle: "Filter by status",
14880						anchorEl: filterAnchorEl,
14881						setAnchorEl: setFilterAnchorEl,
14882						"data-tracking-id": "tracking-id-ads-experiments-status-filter-btn",
14883						children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.List, { children: Object.values(ExperimentStatus).map((status) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.ListItemButton, {
14884							dense: true,
14885							selected: statusFilter === status,
14886							onClick: () => {
14887								setStatusFilter(status);
14888								setFilterAnchorEl(null);
14889							},
14890							children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.ListItemText, { primary: experimentStatusDisplay[status].label })
14891						}, status)) })
14892					}),
14893					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Box, {
14894						sx: searchWrapperSx$1,
14895						children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.Search, {
14896							placeholder: "Search experiments and groups",
14897							value: searchTerm,
14898							onChange: (event) => setSearchTerm(event.target.value),
14899							onClear: () => setSearchTerm(""),
14900							numberOfResults: visibleExperiments.length,
14901							inputProps: { "data-tracking-id": "tracking-id-ads-experiments-search-input" }
14902						})
14903					}),
14904					canEdit && newExperimentButton
14905				] })
14906			}),
14907			loading && experiments.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsTableSkeleton, {}),
14908			!loading && experiments.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsEmptyState, {
14909				icon: import_faFlask.faFlask,
14910				heading: "No experiments yet",
14911				subtitle: "An experiment splits users into groups so a campaign can serve each group a different video.",
14912				children: canEdit && newExperimentButton
14913			}),
14914			experiments.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableContainer, {
14915				component: import_material.Paper,
14916				variant: "outlined",
14917				children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Table, {
14918					size: "small",
14919					stickyHeader: true,
14920					children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableHead, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.TableRow, { children: [
14921						/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, {
14922							sx: (0, import_dist$2.combineSxProps)(headCellSx, stickyLeftSx("default", true)),
14923							children: "Experiment"
14924						}),
14925						/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, {
14926							sx: headCellSx,
14927							children: "Status"
14928						}),
14929						/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, {
14930							sx: headCellSx,
14931							children: "Runs"
14932						}),
14933						/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, {
14934							sx: headCellSx,
14935							children: "Groups"
14936						}),
14937						/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, {
14938							sx: headCellSx,
14939							children: "Coverage"
14940						}),
14941						/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, { sx: (0, import_dist$2.combineSxProps)(actionsCellSx, stickyRightSx("default", true)) })
14942					] }) }
14942), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.TableBody, { children: [visibleExperiments.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableRow, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, {
14943						colSpan: 6,
14944						sx: noResultsCellSx,
14945						children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
14946							variant: "body1",
14947							color: "text.secondary",
14948							children: "No experiments match your filters"
14949						})
14950					}) }), visibleExperiments.map((experiment) => {
14951						const status = experimentStatusDisplay[getExperimentStatus(experiment, today)];
14952						return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.TableRow, {
14953							sx: rowHoverSx,
14954							"data-testid": `ads-experiment-row-${experiment.id}`,
14955							children: [
14956								/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, {
14957									sx: (0, import_dist$2.combineSxProps)(bodyCellSx, stickyLeftSx("default")),
14958									children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
14959										sx: nameCellSx,
14960										children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
14961											variant: "subtitle2",
14962											children: experiment.name
14963										}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
14964											variant: "caption",
14965											color: "text.secondary",
14966											children: experiment.accountName
14967										})]
14968									})
14969								}),
14970								/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, {
14971									sx: bodyCellSx,
14972									children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.Label, {
14973										size: "small",
14974										label: status.label,
14975										color: status.color,
14976										variant: status.variant
14977									})
14978								}),
14979								/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, {
14980									sx: bodyCellSx,
14981									children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
14982										variant: "body1",
14983										noWrap: true,
14984										children: `${TimeUtils.getDesignSystemReadableTimeDate(experiment.start, true)} - ${experiment.end ? TimeUtils.getDesignSystemReadableTimeDate(experiment.end, true) : "No end date"}`
14985									})
14986								}),
14987								/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, {
14988									sx: bodyCellSx,
14989									children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
14990										variant: "body1",
14991										children: experiment.groups.length
14992									})
14993								}),
14994								/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, {
14995									sx: bodyCellSx,
14996									children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ExperimentRangeBar, {
14997										groups: experiment.groups,
14998										dense: true
14999									})
15000								}),
15001								/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, {
15002									sx: (0, import_dist$2.combineSxProps)(actionsCellSx, stickyRightSx("default")),
15003									children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.TruffleIconButton, {
15004										size: "small",
15005										disabled: !canEdit,
15006										onClick: (event) => {
15007											setMenuAnchorEl(event.currentTarget);
15008											setMenuExperiment(experiment);
15009										},
15010										"data-tracking-id": "tracking-id-ads-experiments-row-options-btn",
15011										children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FontAwesomeIcon, { icon: import_faEllipsisV.faEllipsisV })
15012									})
15013								})
15014							]
15015						}, experiment.id);
15016					})] })]
15017				})
15018			}),
15019			menuExperiment && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(RowOptionsMenu, {
15020				anchorEl: menuAnchorEl,
15021				open: Boolean(menuAnchorEl),
15022				onClose: closeMenu,
15023				title: menuExperiment.name,
15024				captions: [`Account: ${menuExperiment.accountName}`, `Groups: ${menuExperiment.groups.length}`],
15025				actions: menuActions(menuExperiment),
15026				anchorOrigin: {
15027					vertical: "bottom",
15028					horizontal: "right"
15029				},
15030				transformOrigin: {
15031					vertical: "top",
15032					horizontal: "right"
15033				}
15034			}),
15035			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(ExtendEndDateDialog, {
15036				experiment: extending,
15037				onClose: () => setExtending(null),
15038				onExtend: (experimentId, end, comment) => {
15039					actions.update(experimentId, { end }, comment).catch(() => void 0);
15040					setExtending(null);
15041				}
15042			}),
15043			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(NewExperimentDialog, {
15044				open: isNewDialogOpen,
15045				existingNames: experiments.map((experiment) => experiment.name),
15046				onClose: () => setIsNewDialogOpen(false),
15047				onCreate: (input, groups, comment) => {
15048					actions.create(input, groups, comment).catch(() => void 0);
15049					setIsNewDialogOpen(false);
15050				}
15051			})
15052		]
15053	});
15054};
15055var searchWrapperSx$1 = { width: 280 };
15056var nameCellSx = {
15057	display: "flex",
15058	flexDirection: "column",
15059	maxWidth: 280
15060};
15061var noResultsCellSx = {
15062	padding: "32px 16px",
15063	textAlign: "center"
15064};
15065//#endregion
15066//#region node_modules/@fortawesome/pro-regular-svg-icons/faBuilding.js
15067var require_faBuilding = /* @__PURE__ */ __commonJSMin(((exports) => {
15068	Object.defineProperty(exports, "__esModule", { value: true });
15069	var prefix = "far";
15070	var iconName = "building";
15071	var width = 384;
15072	var height = 512;
15073	var aliases = [127970, 61687];
15074	var unicode = "f1ad";
15075	var svgPathData = "M64 48c-8.8 0-16 7.2-16 16l0 384c0 8.8 7.2 16 16 16l80 0 0-80c0-17.7 14.3-32 32-32l32 0c17.7 0 32 14.3 32 32l0 80 80 0c8.8 0 16-7.2 16-16l0-384c0-8.8-7.2-16-16-16L64 48zM0 64C0 28.7 28.7 0 64 0L320 0c35.3 0 64 28.7 64 64l0 384c0 35.3-28.7 64-64 64L64 512c-35.3 0-64-28.7-64-64L0 64zm96 48c0-8.8 7.2-16 16-16l32 0c8.8 0 16 7.2 16 16l0 32c0 8.8-7.2 16-16 16l-32 0c-8.8 0-16-7.2-16-16l0-32zM240 96l32 0c8.8 0 16 7.2 16 16l0 32c0 8.8-7.2 16-16 16l-32 0c-8.8 0-16-7.2-16-16l0-32c0-8.8 7.2-16 16-16zM96 240c0-8.8 7.2-16 16-16l32 0c8.8 0 16 7.2 16 16l0 32c0 8.8-7.2 16-16 16l-32 0c-8.8 0-16-7.2-16-16l0-32zm144-16l32 0c8.8 0 16 7.2 16 16l0 32c0 8.8-7.2 16-16 16l-32 0c-8.8 0-16-7.2-16-16l0-32c0-8.8 7.2-16 16-16z";
15076	exports.definition = {
15077		prefix,
15078		iconName,
15079		icon: [
15080			width,
15081			height,
15082			aliases,
15083			unicode,
15084			svgPathData
15085		]
15086	};
15087	exports.faBuilding = exports.definition;
15088	exports.prefix = prefix;
15089	exports.iconName = iconName;
15090	exports.width = width;
15091	exports.height = height;
15092	exports.ligatures = aliases;
15093	exports.unicode = unicode;
15094	exports.svgPathData = svgPathData;
15095	exports.aliases = aliases;
15096}));
15097//#endregion
15098//#region client/components/newNav/components/ads/common/useAdsSpaceEntityForm.ts
15099var import_faWaveformLines = require_faWaveformLines();
15100var import_faBuilding = require_faBuilding();
15101var listKeys = (groups) => groups.flatMap((group) => group.fields).filter((field) => field.type === "multiSelect" || field.type === "stringList").map((field) => ({
15102	key: field.key,
15103	separator: listSeparatorFor(field.type)
15104}));
15105var splitLists = (values, keys) => {
15106	const next = { ...values };
15107	keys.forEach(({ key, separator }) => {
15108		const text = asText(next[key]);
15109		next[key] = text ? text.split(separator).map((part) => part.trim()).filter(Boolean) : [];
15110	});
15111	return next;
15112};
15113var joinLists = (values, keys) => {
15114	const next = { ...values };
15115	keys.forEach(({ key }) => {
15116		next[key] = asStringList(next[key]).join(",");
15117	});
15118	return next;
15119};
15120/** A read-only field may be shown from somewhere other than this row, so sending it back would fail on an unknown column. */
15121var withoutReadOnly = (values, groups) => {
15122	const next = { ...values };
15123	groups.flatMap((group) => group.fields).filter((field) => field.type === "readOnly").forEach((field) => delete next[field.key]);
15124	return next;
15125};
15126/**
15127* One row of a configuration table as a form: the account, or the single row of a one-row table. The row's id
15128* travels with the save, so the write path updates it rather than adding a second one.
15129*/
15130var useAdsSpaceEntityForm = (kind, groups) => {
15131	const { rows, loading, error, saving, save } = useAdsSpaceEntities(kind);
15132	const keys = listKeys(groups);
15133	const row = rows[0];
15134	return {
15135		values: row ? splitLists(row, keys) : null,
15136		loading,
15137		error,
15138		saving,
15139		persist: (values, comment) => save([{
15140			...withoutReadOnly(joinLists(values, keys), groups),
15141			id: row?.id ?? "new"
15142		}], comment)
15143	};
15144};
15145/**
15146* A key/value table rendered as a typed form - `tracking_property` is nine keys across the whole platform, and a
15147* bag of strings is how a misspelled key gets saved and then silently ignored.
15148*/
15149var useAdsSpaceKeyValueForm = (kind, groups) => {
15150	const { rows, loading, error, saving, save } = useAdsSpaceEntities(kind);
15151	const keys = listKeys(groups);
15152	const values = {};
15153	rows.forEach((row) => {
15154		values[asText(row.propKey)] = row.propValue;
15155	});
15156	const persist = async (next, comment) => {
15157		const joined = joinLists(next, keys);
15158		const changed = Object.entries(joined).filter(([key, value]) => {
15159			return asText(rows.find((row) => asText(row.propKey) === key)?.propValue) !== asText(value);
15160		}).map(([key, value]) => {
15161			return {
15162				id: rows.find((row) => asText(row.propKey) === key)?.id ?? `new-${key}`,
15163				propKey: key,
15164				propValue: asText(value)
15165			};
15166		});
15167		if (changed.length > 0) await save(changed, comment);
15168	};
15169	return {
15170		values: loading ? null : splitLists(values, keys),
15171		loading,
15172		error,
15173		saving,
15174		persist
15175	};
15176};
15177//#endregion
15178//#region client/components/newNav/components/ads/settings/AccountSection.tsx
15179var validateAccountName = (value) => {
15180	const name = asText(value);
15181	if (name.length > 15) return "The account name should have 15 letters top";
15182	if (name && !/^[a-z]+$/.test(name)) return "The account name should contain only lower case letters";
15183	return null;
15184};
15185var groups$1 = [
15186	{
15187		id: "identity",
15188		title: "Identity",
15189		fields: [
15190			{
15191				key: "name",
15192				label: "Name",
15193				type: "text",
15194				validate: validateAccountName
15195			},
15196			{
15197				key: "enabled",
15198				label: "Enabled",
15199				type: "bool"
15200			},
15201			{
15202				key: "medusaName",
15203				label: "Medusa name",
15204				type: "text"
15205			},
15206			{
15207				key: "repositoryPath",
15208				label: "Repository path",
15209				type: "text"
15210			},
15211			{
15212				key: "productCatalogDb",
15213				label: "Product catalog DB",
15214				type: "select",
15215				options: productCatalogDbOptions,
15216				help: "The location of the product catalog for this account"
15217			}
15218		]
15219	},
15220	{
15221		id: "studio",
15222		title: "Studio mapping",
15223		description: "The link between this ads account and the Studio product. It is the studio_account row, changed by linking the account rather than by typing here.",
15224		fields: [{
15225			key: "adsAccountId",
15226			label: "Ads account id",
15227			type: "readOnly"
15228		}, {
15229			key: "adsAccountName",
15230			label: "Linked ads account",
15231			type: "readOnly"
15232		}]
15233	},
15234	{
15235		id: "notifications",
15236		title: "Notifications and metadata",
15237		fields: [
15238			{
15239				key: "notificationEmailAddresses",
15240				label: "Notification emails",
15241				type: "stringList",
15242				wide: true,
15243				placeholder: "[email protected]",
15244				entryProblem: emailEntryProblem,
15245				help: "List (separated by comma) of mail addresses to notify of changes in the account"
15246			},
15247			{
15248				key: "advertiserDomains",
15249				label: "Advertiser domains",
15250				type: "stringList",
15251				wide: true,
15252				help: "Comma separated list of domains relevant to this account. Google advertisers should use the numerical value, prefixed by 'g.'. For example: g.10234"
15253			},
15254			{
15255				key: "categories",
15256				label: "Categories",
15257				type: "stringList",
15258				wide: true,
15259				help: "Uses the same 'g.' prefix convention as advertiser domains."
15260			},
15261			{
15262				key: "tagsDefinitionLocation",
15263				label: "Tags definition location",
15264				type: "url",
15265				wide: true,
15266				help: "Location of the Excel file with the tags definitions for this account"
15267			}
15268		]
15269	}
15270];
15271var AccountForm = ({ source }) => {
15272	const form = useAdsSettingsForm(groups$1, source.values, "The account page", source.persist, source.saving);
15273	return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsSettingsForm, {
15274		groups: groups$1,
15275		form,
15276		trackingIdPrefix: "tracking-id-ads-settings-account"
15277	});
15278};
15279var AccountSection = () => {
15280	const source = useAdsSpaceEntityForm(GqlClientAdsSpaceEntityKind.ADS_ACCOUNT, groups$1);
15281	const { adsAccountId, adsAccountName } = useAdsSpaceAccount();
15282	if (!source.values) return source.loading ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Skeleton, {
15283		variant: "rounded",
15284		height: 320
15285	}) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsUnreachableNotice, { what: "This ads account" });
15286	const values = {
15287		...source.values,
15288		adsAccountId,
15289		adsAccountName
15290	};
15291	return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AccountForm, { source: {
15292		...source,
15293		values
15294	} }, JSON.stringify(values));
15295};
15296//#endregion
15297//#region client/components/newNav/components/ads/settings/AttributionReportsSection.tsx
15298/** Each of these holds a column header exactly as it appears in the customer's own report file. */
15299var columnMappingField = (key, label, extraHelp) => ({
15300	key,
15301	label,
15302	type: "text",
15303	help: extraHelp ? `Report '${label}' field. ${extraHelp}` : `Report '${label}' field`
15304});
15305var internalReportGroups = [
15306	{
15307		id: "identity",
15308		title: "Identity",
15309		fields: [{
15310			key: "fileName",
15311			label: "Report file name",
15312			type: "text",
15313			validate: required("Enter the report file name")
15314		}, {
15315			key: "subject",
15316			label: "Email subject",
15317			type: "text",
15318			help: "(Optional) Email Subject"
15319		}]
15320	},
15321	{
15322		id: "parsing",
15323		title: "Parsing",
15324		fields: [{
15325			key: "updateWindowDays",
15326			label: "Update window (days)",
15327			type: "number",
15328			min: 0,
15329			help: "number of days to look back at reports data; set to 0 in order to process all the data in the report"
15330		}, {
15331			key: "expectedDateFormat",
15332			label: "Expected date format",
15333			type: "select",
15334			options: reportDateFormatOptions
15335		}]
15336	},
15337	{
15338		id: "columns",
15339		title: "Column mapping",
15340		description: "The header name each value carries in the customer's file.",
15341		fields: [
15342			columnMappingField("date", "Date"),
15343			columnMappingField("impressions", "Impressions"),
15344			columnMappingField("clicks", "Clicks"),
15345			columnMappingField("clickThroughConversions", "Click-through Conversions"),
15346			columnMappingField("clickThroughRevenue", "Click-through Revenue"),
15347			columnMappingField("viewThroughConversions", "View-through Conversions"),
15348			columnMappingField("viewThroughRevenue", "View-through Revenue"),
15349			columnMappingField("totalConversions", "Total Conversions"),
15350			columnMappingField("totalRevenue", "Total Revenue"),
15351			columnMappingField("placement", "Placement", "Can be calculated from multiple fields separated by ,"),
15352			columnMappingField("placementId", "Placement Id")
15353		]
15354	},
15355	{
15356		id: "custom",
15357		title: "Custom fields",
15358		advanced: true,
15359		description: "Custom field that will be populated in redshift",
15360		fields: [
15361			1,
15362			2,
15363			3,
15364			4,
15365			5,
15366			6,
15367			7,
15368			8,
15369			9
15370		].map((index) => ({
15371			key: `customField${index}`,
15372			label: `Custom field ${index}`,
15373			type: "text"
15374		}))
15375	}
15376];
15377var validateReportHeaders = (value) => asText(value).split(",").map((part) => part.trim()).filter(Boolean).length >= 3 ? null : "Insert at least 3 headers that will show in the report for sure";
15378var thirdPartyGroups = [
15379	{
15380		id: "identity",
15381		title: "Identity",
15382		fields: [
15383			{
15384				key: "fileName",
15385				label: "Report file name",
15386				type: "text",
15387				validate: required("Enter the report file name")
15388			},
15389			{
15390				key: "subject",
15391				label: "Email subject",
15392				type: "text",
15393				help: "(Optional) Email Subject"
15394			},
15395			{
15396				key: "redshiftTableName",
15397				label: "Redshift table name",
15398				type: "readOnly",
15399				wide: true,
15400				help: "Redshift table name. When multiple tabs, a table is opened for each tab with tab name as its suffix"
15401			}
15402		]
15403	},
15404	{
15405		id: "parsing",
15406		title: "Parsing",
15407		fields: [{
15408			key: "reportHeaders",
15409			label: "Report header",
15410			type: "text",
15411			wide: true,
15412			help: "Insert at least 3 (comma delimited) headers that will show in the report for sure! i.e. date,media partner,placement",
15413			validate: validateReportHeaders
15414		}]
15415	},
15416	{
15417		id: "tabs",
15418		title: "Tabs",
15419		advanced: true,
15420		description: "tab name to process in case of multiple tabs",
15421		fields: [
15422			1,
15423			2,
15424			3,
15425			4,
15426			5,
15427			6,
15428			7,
15429			8,
15430			9
15431		].map((index) => ({
15432			key: `tabName${index}`,
15433			label: `Tab ${index}`,
15434			type: "text"
15435		}))
15436	}
15437];
15438var ReportDetail = ({ report, groups, trackingIdPrefix, save, saving }) => {
15439	return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsSettingsForm, {
15440		groups,
15441		form: useAdsSettingsForm(groups, report, "Attribution report configuration", (values, comment) => save([{
15442			...values,
15443			id: report.id
15444		}], comment), saving),
15445		trackingIdPrefix
15446	});
15447};
15448var AttributionReportsSection = () => {
15449	const [kind, setKind] = (0, import_react.useState)("internal");
15450	const internal = useAdsSpaceEntities(GqlClientAdsSpaceEntityKind.ATTRIBUTION_REPORT);
15451	const thirdParty = useAdsSpaceEntities(GqlClientAdsSpaceEntityKind.ATTRIBUTION_REPORT_THIRD_PARTY);
15452	const placements = useAdsSpaceEntities(GqlClientAdsSpaceEntityKind.ATTRIBUTION_PLACEMENT_MAPPING);
15453	return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
15454		sx: sectionSx$4,
15455		children: [
15456			/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_dist$2.TruffleToggleButtonGroup, {
15457				exclusive: true,
15458				size: "small",
15459				value: kind,
15460				onChange: (_event, next) => next && setKind(next),
15461				children: [
15462					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.ToggleButton, {
15463						value: "internal",
15464						"data-tracking-id": "tracking-id-ads-settings-attribution-internal-btn",
15465						children: "Internal"
15466					}),
15467					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.ToggleButton, {
15468						value: "thirdParty",
15469						"data-tracking-id": "tracking-id-ads-settings-attribution-third-party-btn",
15470						children: "Third party"
15471					}),
15472					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.ToggleButton, {
15473						value: "placements",
15474						"data-tracking-id": "tracking-id-ads-settings-attribution-placements-btn",
15475						children: "Placement mapping"
15476					})
15477				]
15478			}),
15479			kind === "internal" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsDrillDownSection, {
15480				title: "Internal reports",
15481				description: "Maps the columns of a customer's report file onto the fields SundaySky attributes on.",
15482				searchPlaceholder: "Search report files",
15483				rows: internal.rows,
15484				loading: internal.loading,
15485				searchKeys: ["fileName", "subject"],
15486				columns: [
15487					{
15488						key: "fileName",
15489						label: "Report file"
15490					},
15491					{
15492						key: "subject",
15493						label: "Email subject"
15494					},
15495					{
15496						key: "updateWindowDays",
15497						label: "Update window (days)",
15498						align: "right"
15499					}
15500				],
15501				getTitle: (row) => asText(row.fileName),
15502				renderDetail: (row) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ReportDetail, {
15503					report: row,
15504					groups: internalReportGroups,
15505					trackingIdPrefix: "tracking-id-ads-settings-attribution-internal",
15506					save: internal.save,
15507					saving: internal.saving
15508				}, row.id),
15509				emptyText: "No internal report configurations for this account",
15510				trackingIdPrefix: "tracking-id-ads-settings-attribution-internal"
15511			}),
15512			kind === "thirdParty" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsDrillDownSection, {
15513				title: "Third party reports",
15514				description: "Reports that arrive from a media partner rather than from the customer.",
15515				searchPlaceholder: "Search report files",
15516				rows: thirdParty.rows,
15517				loading: thirdParty.loading,
15518				searchKeys: ["fileName", "redshiftTableName"],
15519				columns: [{
15520					key: "fileName",
15521					label: "Report file"
15522				}, {
15523					key: "redshiftTableName",
15524					label: "Redshift table"
15525				}],
15526				getTitle: (row) => asText(row.fileName),
15527				renderDetail: (row) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ReportDetail, {
15528					report: row,
15529					groups: thirdPartyGroups,
15530					trackingIdPrefix: "tracking-id-ads-settings-attribution-third-party",
15531					save: thirdParty.save,
15532					saving: thirdParty.saving
15533				}, row.id),
15534				emptyText: "No third party report configurations for this account",
15535				trackingIdPrefix: "tracking-id-ads-settings-attribution-third-party"
15536			}),
15537			kind === "placements" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsDrillDownSection, {
15538				title: "Placement mapping",
15539				description: "Which placement in a report belongs to which campaign. Written by the report ingestion, read-only here.",
15540				searchPlaceholder: "Search placements",
15541				rows: placements.rows,
15542				loading: placements.loading,
15543				searchKeys: ["placementId", "identifier"],
15544				columns: [
15545					{
15546						key: "placementId",
15547						label: "Placement id"
15548					},
15549					{
15550						key: "identifier",
15551						label: "Identifier"
15552					},
15553					{
15554						key: "programId",
15555						label: "Campaign id"
15556					}
15557				],
15558				getTitle: (row) => asText(row.placementId),
15559				emptyText: "No placements are mapped for this account",
15560				trackingIdPrefix: "tracking-id-ads-settings-attribution-placements"
15561			})
15562		]
15563	});
15564};
15565var sectionSx$4 = {
15566	display: "flex",
15567	flexDirection: "column",
15568	gap: 3,
15569	alignItems: "stretch"
15570};
15571//#endregion
15572//#region client/components/newNav/components/ads/settings/SiteActionTagsSection.tsx
15573/** A tag may only map to one site action, which is the clash the old app let you save twice. */
15574var validateRow = (row, otherRows) => otherRows.some((other) => asText(other.tag) === asText(row.tag) && asText(other.actionId) === asText(row.actionId)) ? { tag: "This tag is already mapped to that site action" } : {};
15575var SiteActionTagsSection = () => {
15576	const { rows: siteActions } = useAdsSpaceEntities(GqlClientAdsSpaceEntityKind.SITE_ACTION);
15577	const actionOptions = (0, import_react.useMemo)(() => siteActions.map((action) => ({
15578		value: action.id,
15579		label: asText(action.name)
15580	})), [siteActions]);
15581	const fields = [
15582		{
15583			key: "actionId",
15584			label: "Site action",
15585			type: "select",
15586			options: actionOptions,
15587			validate: required("Pick the site action this tag maps to")
15588		},
15589		{
15590			key: "tag",
15591			label: "Tag",
15592			type: "text",
15593			help: "The event name as it arrives from the account's own site.",
15594			validate: required("Enter the tag as the site sends it")
15595		},
15596		{
15597			key: "method",
15598			label: "Method",
15599			type: "select",
15600			options: siteActionMethodOptions,
15601			help: "optional, use when tags are not unique identifiers"
15602		}
15603	];
15604	return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsEntityRowsSection, {
15605		kind: GqlClientAdsSpaceEntityKind.ACCOUNT_ACTION_TAG,
15606		title: "Site action tags",
15607		fields,
15608		columns: [
15609			{
15610				key: "actionId",
15611				label: "Site action",
15612				render: (row) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
15613					variant: "subtitle2",
15614					children: actionOptions.find((option) => option.value === asText(row.actionId))?.label ?? asText(row.actionId)
15615				})
15616			},
15617			{
15618				key: "tag",
15619				label: "Tag"
15620			},
15621			{
15622				key: "method",
15623				label: "Method",
15624				render: (row) =>
15624 asText(row.method) ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.Label, {
15625					size: "small",
15626					label: asText(row.method),
15627					color: "default",
15628					variant: "outlined"
15629				}) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
15630					variant: "body1",
15631					color: "text.disabled",
15632					children: "-"
15633				})
15634			}
15635		],
15636		addLabel: "Add tag mapping",
15637		emptyText: "No tags are mapped for this account yet",
15638		rowTitle: (row) => asText(row.tag),
15639		validateRow,
15640		trackingIdPrefix: "tracking-id-ads-settings-site-actions",
15641		children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.AttentionBox, {
15642			color: "info",
15643			children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.AttentionBoxContent, { children: "Site actions are the canonical list SundaySky reports on. This page maps them to the event names this account's own site sends." })
15644		})
15645	});
15646};
15647//#endregion
15648//#region client/components/newNav/components/ads/settings/TrackingPropertiesSection.tsx
15649/**
15650* Stored as a key/value bag, but the key set is closed - nine keys across the whole platform. Typed inputs
15651* instead of a bag mean a misspelled key can no longer be saved and then silently ignored.
15652*/
15653var groups = [
15654	{
15655		id: "sampling",
15656		title: "Sampling",
15657		fields: [{
15658			key: "samplingRate",
15659			label: "Sampling rate",
15660			type: "decimal",
15661			min: 0,
15662			max: 1,
15663			help: "Tracking sampling rate for the account, between 0 and 1.",
15664			validate: inRange(0, 1, "Sampling rate must be between 0 and 1")
15665		}, {
15666			key: "redShiftUuiSamplingRate",
15667			label: "Redshift UUI sampling rate",
15668			type: "decimal",
15669			min: 0,
15670			max: 1,
15671			help: "UUI logging sampling rate into Redshift.",
15672			validate: inRange(0, 1, "Sampling rate must be between 0 and 1")
15673		}]
15674	},
15675	{
15676		id: "syncing",
15677		title: "User syncing",
15678		fields: [
15679			{
15680				key: "maxSyncsPerPage",
15681				label: "Max syncs per page",
15682				type: "number",
15683				min: 0,
15684				help: "Max user syncs injected per page."
15685			},
15686			{
15687				key: "syncBatchSize",
15688				label: "Sync batch size",
15689				type: "number",
15690				min: 0
15691			},
15692			{
15693				key: "sync_blacklist",
15694				label: "Excluded sync partners",
15695				type: "multiSelect",
15696				options: syncPartnerOptions,
15697				wide: true,
15698				help: "Partners excluded from syncing for this account."
15699			}
15700		]
15701	},
15702	{
15703		id: "advanced",
15704		title: "Advanced",
15705		advanced: true,
15706		description: "Single-account settings. Each of these is currently in use by exactly one account.",
15707		fields: [
15708			{
15709				key: "MinimumUserAgeSecondsForSync",
15710				label: "Minimum user age for sync (seconds)",
15711				type: "number",
15712				min: 0
15713			},
15714			{
15715				key: "AddScriptSuffixTypes",
15716				label: "Add script suffix types",
15717				type: "text"
15718			},
15719			{
15720				key: "BrightRollSegId",
15721				label: "BrightRoll segment id",
15722				type: "text"
15723			},
15724			{
15725				key: "UdtFieldsToTrackingFields",
15726				label: "UDT fields to tracking fields",
15727				type: "text",
15728				wide: true,
15729				placeholder: "vmake=make,vmdl=model"
15730			}
15731		]
15732	}
15733];
15734var TrackingPropertiesForm = ({ source }) => {
15735	const form = useAdsSettingsForm(groups, source.values, "Sampling & sync", source.persist, source.saving);
15736	return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsSettingsForm, {
15737		groups,
15738		form,
15739		trackingIdPrefix: "tracking-id-ads-settings-tracking"
15740	});
15741};
15742var TrackingPropertiesSection = () => {
15743	const source = useAdsSpaceKeyValueForm(GqlClientAdsSpaceEntityKind.TRACKING_PROPERTY, groups);
15744	if (!source.values) return source.loading ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Skeleton, {
15745		variant: "rounded",
15746		height: 320
15747	}) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsUnreachableNotice, { what: "These tracking properties" });
15748	return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TrackingPropertiesForm, { source }, JSON.stringify(source.values));
15749};
15750//#endregion
15751//#region client/components/newNav/components/ads/settings/AdsSettingsPage.tsx
15752var sections$1 = [
15753	{
15754		id: "account",
15755		label: "Account",
15756		icon: import_faBuilding.faBuilding,
15757		entityKinds: [GqlClientAdsSpaceEntityKind.ADS_ACCOUNT],
15758		render: () => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AccountSection, {})
15759	},
15760	{
15761		id: "tracking",
15762		label: "Sampling & sync",
15763		icon: import_faWaveformLines.faWaveformLines,
15764		entityKinds: [GqlClientAdsSpaceEntityKind.TRACKING_PROPERTY],
15765		render: () => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TrackingPropertiesSection, {})
15766	},
15767	{
15768		id: "site-actions",
15769		label: "Site actions",
15770		icon: import_faTag.faTag,
15771		entityKinds: [GqlClientAdsSpaceEntityKind.ACCOUNT_ACTION_TAG, GqlClientAdsSpaceEntityKind.SITE_ACTION],
15772		render: () => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SiteActionTagsSection, {})
15773	},
15774	{
15775		id: "attribution",
15776		label: "Attribution reports",
15777		icon: import_faChartMixed.faChartMixed,
15778		entityKinds: [
15779			GqlClientAdsSpaceEntityKind.ATTRIBUTION_REPORT,
15780			GqlClientAdsSpaceEntityKind.ATTRIBUTION_REPORT_THIRD_PARTY,
15781			GqlClientAdsSpaceEntityKind.ATTRIBUTION_PLACEMENT_MAPPING
15782		],
15783		render: () => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AttributionReportsSection, {})
15784	},
15785	{
15786		id: "dsp-data",
15787		label: "DSP data",
15788		icon: import_faTowerBroadcast.faTowerBroadcast,
15789		entityKinds: [GqlClientAdsSpaceEntityKind.DSP_ACCOUNT_DATA, GqlClientAdsSpaceEntityKind.DSP_SYNC_CONDITION],
15790		render: () => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(DspAccountDataSection, {})
15791	}
15792];
15793/**
15794* Settings of the ads account itself. Each section is its own row in the left panel and its own page here, so
15795* reaching one is a single click rather than a page plus an inner menu.
15796*/
15797var AdsSettingsPage = () => {
15798	const accountId = useActiveAccountId();
15799	const { sectionId } = useParams();
15800	const selected = sections$1.find((section) => section.id === sectionId);
15801	if (!selected) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Redirect, { to: (0, import_dist$1.newNavAdsSettingsSectionPath)({
15802		accountId,
15803		sectionId: "account"
15804	}) });
15805	return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsSectionPage, {
15806		area: "Accounts",
15807		title: selected.label,
15808		sections: [selected],
15809		trackingIdPrefix: "tracking-id-ads-settings"
15810	});
15811};
15812//#endregion
15813//#region client/components/newNav/components/ads/system/AdsDmpAccountMappingsPage.tsx
15814/**
15815* Which of each provider's own account names feed this ads account. Provider-wide configuration lives on the
15816* Providers page behind the system permission; this is the account-scoped half of the same data, which the
15817* server gives us by matching the mapping's SundaySky account name.
15818*/
15819var DmpAccountMappingsSection = () => {
15820	const { rows, loading } = useAdsSpaceEntities(GqlClientAdsSpaceEntityKind.DMP_MAPPING);
15821	const providers = useAdsSpaceEntities(GqlClientAdsSpaceEntityKind.DMP_PROVIDER);
15822	const providerName = (providerId) => asText(providers.rows.find((provider) => provider.id === providerId)?.name) || providerId;
15823	return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsDrillDownSection, {
15824		title: "Account mappings",
15825		titleInPageHeader: true,
15826		searchPlaceholder: "Search mappings",
15827		rows,
15828		loading,
15829		searchKeys: ["dmpAccountName", "sskyAccountName"],
15830		columns: [{
15831			key: "dmpProviderId",
15832			label: "Provider",
15833			render: (row) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
15834				variant: "subtitle2",
15835				children: providerName(asText(row.dmpProviderId))
15836			})
15837		}, {
15838			key: "dmpAccountName",
15839			label: "DMP account name"
15840		}],
15841		getTitle: (row) => asText(row.dmpAccountName),
15842		emptyText: "No DMP account is mapped to this ads account",
15843		trackingIdPrefix: "tracking-id-ads-dmp-account-mappings"
15844	});
15845};
15846var AdsDmpAccountMappingsPage = () => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsSectionPage, {
15847	area: "DMP / Account mappings",
15848	title: "Account mappings",
15849	subtitle: "The provider account names whose offline files are ingested for this ads account.",
15850	sections: [{
15851		id: "dmp-account-mappings",
15852		label: "Account mappings",
15853		entityKinds: [GqlClientAdsSpaceEntityKind.DMP_MAPPING],
15854		render: () => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(DmpAccountMappingsSection, {})
15855	}],
15856	trackingIdPrefix: "tracking-id-ads-dmp-account-mappings"
15857});
15858//#endregion
15859//#region client/components/newNav/components/ads/system/DmpProvidersSection.tsx
15860var providerGroups = [{
15861	id: "identity",
15862	title: "Provider",
15863	fields: [{
15864		key: "name",
15865		label: "Name",
15866		type: "text",
15867		help: "The DMP Provider Name",
15868		validate: required("Enter the provider name")
15869	}]
15870}];
15871var sourceFields = [
15872	{
15873		key: "bucket",
15874		label: "Source bucket",
15875		type: "text",
15876		help: "The DMP's source bucket",
15877		validate: required("Enter the source bucket")
15878	},
15879	{
15880		key: "path",
15881		label: "Source path",
15882		type: "text",
15883		help: "The DMP's source path"
15884	},
15885	{
15886		key: "regex",
15887		label: "Source file regex",
15888		type: "text",
15889		help: "The DMP's source file regex",
15890		validate: (value) => {
15891			const pattern = asText(value);
15892			if (!pattern) return null;
15893			try {
15894				RegExp(pattern);
15895				return null;
15896			} catch {
15897				return "This is not a valid regular expression";
15898			}
15899		}
15900	}
15901];
15902var mappingFields = [{
15903	key: "dmpAccountName",
15904	label: "DMP account name",
15905	type: "text",
15906	validate: required("Enter the provider's account name")
15907}, {
15908	key: "sskyAccountName",
15909	label: "SundaySky account name",
15910	type: "text",
15911	validate: required("Enter the SundaySky account name")
15912}];
15913var ProviderDetail = ({ provider, save, saving }) => {
15914	const sources = useAdsSpaceChildRows(GqlClientAdsSpaceEntityKind.DMP_SOURCE, provider.id);
15915	const mappings = useAdsSpaceChildRows(GqlClientAdsSpaceEntityKind.DMP_MAPPING, provider.id);
15916	const form = useAdsSettingsForm(providerGroups, provider, "The DMP provider", async (values, comment) => {
15917		await save([{
15918			...values,
15919			id: provider.id
15920		}], comment);
15921		await Promise.all([sources.persist(comment), mappings.persist(comment)]);
15922	}, saving || sources.saving || mappings.saving, [sources, mappings]);
15923	return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsSettingsForm, {
15924		groups: providerGroups,
15925		form,
15926		trackingIdPrefix: "tracking-id-ads-system-dmp-provider",
15927		children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
15928			sx: childTablesSx$1,
15929			children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsChildTable, {
15930				title: "Sources",
15931				description: "Where the provider's files land and which of them to ingest.",
15932				fields: sourceFields,
15933				rows: sources.rows,
15934				addLabel: "Add source",
15935				emptyText: "No sources are configured for this provider",
15936				rowTitle: (row) => asText(row.bucket),
15937				onChange: sources.setRows,
15938				trackingIdPrefix: "tracking-id-ads-system-dmp-source"
15939			}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsChildTable, {
15940				title: "Account mappings",
15941				description: "The provider's own account name for each SundaySky account it sends files for.",
15942				fields: mappingFields,
15943				rows: mappings.rows,
15944				addLabel: "Add mapping",
15945				emptyText: "No accounts are mapped to this provider",
15946				rowTitle: (row) => asText(row.dmpAccountName),
15947				onChange: mappings.setRows,
15948				trackingIdPrefix: "tracking-id-ads-system-dmp-mapping"
15949			})]
15950		})
15951	});
15952};
15953var DmpProvidersSection = () => {
15954	const { rows, save, saving, loading } = useAdsSpaceEntities(GqlClientAdsSpaceEntityKind.DMP_PROVIDER);
15955	return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsDrillDownSection, {
15956		title: "Providers",
15957		titleInPageHeader: true,
15958		searchPlaceholder: "Search providers",
15959		rows,
15960		loading,
15961		searchKeys: ["name"],
15962		columns: [{
15963			key: "name",
15964			label: "Provider"
15965		}, {
15966			key: "account",
15967			label: "Provider account"
15968		}],
15969		getTitle: (row) => asText(row.name),
15970		renderDetail: (row) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ProviderDetail, {
15971			provider: row,
15972			save,
15973			saving
15974		}, row.id),
15975		emptyText: "No DMP providers are configured",
15976		trackingIdPrefix: "tracking-id-ads-system-dmp"
15977	});
15978};
15979var childTablesSx$1 = {
15980	display: "flex",
15981	flexDirection: "column",
15982	gap: 4
15983};
15984//#endregion
15985//#region client/components/newNav/components/ads/system/AdsDmpProvidersPage.tsx
15986var AdsDmpProvidersPage = () => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsSectionPage, {
15987	area: "DMP / Providers",
15988	title: "Providers",
15989	subtitle: "Where audience data comes from, and where each provider's files land.",
15990	sections: [{
15991		id: "dmp-providers",
15992		label: "Providers",
15993		entityKinds: [GqlClientAdsSpaceEntityKind.DMP_PROVIDER, GqlClientAdsSpaceEntityKind.DMP_SOURCE],
15994		render: () => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(DmpProvidersSection, {})
15995	}],
15996	trackingIdPrefix: "tracking-id-ads-dmp-providers"
15997});
15998//#endregion
15999//#region client/components/newNav/components/ads/system/ExchangesSection.tsx
16000var rate = (key, label, help) => ({
16001	key,
16002	label,
16003	type: "decimal",
16004	min: 0,
16005	max: 1,
16006	help,
16007	validate: inRange(0, 1, "Sampling rates are between 0 and 1")
16008});
16009var exchangeGroups = [
16010	{
16011		id: "identity",
16012		title: "Identity",
16013		fields: [
16014			{
16015				key: "prefix",
16016				label: "Prefix",
16017				type: "readOnly"
16018			},
16019			{
16020				key: "name",
16021				label: "Name",
16022				type: "text",
16023				help: "Human readable name of exchange"
16024			},
16025			{
16026				key: "adsTxtName",
16027				label: "ads.txt name",
16028				type: "text",
16029				help: "The name by which the exchange is recognized in ads.txt"
16030			}
16031		]
16032	},
16033	{
16034		id: "bidding",
16035		title: "Bidding",
16036		fields: [
16037			{
16038				key: "maxBidTime",
16039				label: "Max bid time (ms)",
16040				type: "number",
16041				min: 0,
16042				help: "The number of milliseconds after which the ad server will abort handling a bid request."
16043			},
16044			{
16045				key: "lowBallRate",
16046				label: "Low ball rate",
16047				type: "decimal",
16048				min: 0,
16049				max: 1,
16050				help: "The Low Ball rate [0 = never,1 = always], turning failed bids into 1 cent bids",
16051				validate: inRange(0, 1, "The low ball rate is between 0 and 1")
16052			},
16053			{
16054				key: "lowBallDummyProgram",
16055				label: "Low ball dummy campaign",
16056				type: "text",
16057				help: "The program whose control group video is used for Low Ball response generation"
16058			},
16059			{
16060				key: "minSecondsBetweenSameOpportunity",
16061				label: "Min seconds between opportunities",
16062				type: "number",
16063				min: 0,
16064				help: "The minimum number of seconds between two opportunities for the same user (subsequent opportunities will be throttled)"
16065			}
16066		]
16067	},
16068	{
16069		id: "sampling",
16070		title: "Sampling",
16071		description: "What the ad server keeps from this exchange's traffic. Lowering these loses training data for future models.",
16072		fields: [
16073			rate("opportunityCreationRate", "Opportunity creation rate", "Sampling rate for opportunity creation. If a request is not sampled then the user profile is not fetched and prospecting model scores cannot be calculated."),
16074			rate("opportunityCreationNonSSkyUserRate", "Non-SundaySky user creation rate", "Sampling rate for non ssky opportunity creation."),
16075			rate("opportunityLogRate", "Opportunity log rate", "Sampling rate for opportunity logging. Sampling rate only for requests which passed the opportunity creation filters. If a request is not sampled it is not sent to the processor and cannot be used for training future models."),
16076			rate("fullOpportunityLogRate", "Full opportunity log rate", "Sampling rate for full opportunity logging."),
16077			rate("reqRespLogRate", "Request and response log rate", "Sampling rate for logging requests and responses handled by the ad server.")
16078		]
16079	},
16080	{
16081		id: "viewability",
16082		title: "Viewability",
16083		fields: [
16084			{
16085				key: "viewabilityDefault",
16086				label: "Default viewability",
16087				type: "number",
16088				help: "The default viewability for this exchange - to use when no value is reported"
16089			},
16090			{
16091				key: "viewabilityFactor",
16092				label: "Viewability factor",
16093				type: "decimal",
16094				help: "The factor to apply to the viewability when reported by the exchange."
16095			},
16096			{
16097				key: "viewabilityOverrideType",
16098				label: "Correction mode",
16099				type: "select",
16100				options: viewabilityOverrideOptions,
16101				help: "Viewability Correction Mode"
16102			},
16103			{
16104				key: "viewabilityOverrideMapPath",
16105				label: "Correction data path",
16106				type: "url",
16107				help: "S3 Path to the CSV of viewability correction data"
16108			},
16109			{
16110				key: "viewabilityOverrideRatio",
16111				label: "Correction ratio",
16112				type: "decimal",
16113				min: 0,
16114				max: 1,
16115				help: "Ratio of correction from the final viewability score. 1.0 = use correction value completely; 0.0 = use original value completely; 0.25 = 0.75*correction + 0.25*original",
16116				validate: inRange(0, 1, "The correction ratio is between 0 and 1")
16117			}
16118		]
16119	},
16120	{
16121		id: "users",
16122		title: "Users",
16123		fields: [{
16124			key: "useExchangeUserResolving",
16125			label: "Use exchange user resolving",
16126			type: "bool",
16127			help: "enable/disable exchange user resolving with third party id"
16128		}]
16129	},
16130	{
16131		id: "reports",
16132		title: "Reports",
16133		advanced: true,
16134		description: "How the exchange's own delivery report is recognised and parsed.",
16135		fields: [
16136			{
16137				key: "reportSender",
16138				label: "Report sender",
16139				type: "text",
16140				help: "Report sender domain (after the @)"
16141			},
16142			{
16143				key: "reportDate",
16144				label: "Date column",
16145				type: "text",
16146				help: "Date column in exchange report"
16147			},
16148			{
16149				key: "reportImpressions",
16150				label: "Impressions column",
16151				type: "text",
16152				help: "Impressions column in exchange report"
16153			},
16154			{
16155				key: "reportCost",
16156				label: "Cost column",
16157				type: "text",
16158				help: "Cost column in exchange report"
16159			},
16160			{
16161				key: "updateWindowDays",
16162				label: "Update window (days)",
16163				type: "number",
16164				min: 0,
16165				help: "number of days to look back at reports data; set to 0 in order to process all the data in the report"
16166			}
16167		]
16168	}
16169];
16170var ExchangeDetail = ({ exchange, save, saving }) => {
16171	const form = useAdsSettingsForm(exchangeGroups, exchange, "The exchange", (values, comment) => save([{
16172		...values,
16173		id: exchange.id
16174	}], comment), saving);
16175	return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsSettingsForm, {
16176		groups: exchangeGroups,
16177		form,
16178		saveWarning: "These settings apply to every campaign bidding into this exchange, on every account.",
16179		trackingIdPrefix: "tracking-id-ads-system-exchange"
16180	});
16181};
16182/**
16183* Twenty rows, changed four times in a year - and every one of them applies to all traffic from that exchange.
16184* Low traffic, high blast radius, which is why the sampling rates carry their full help rather than a label.
16185*/
16186var ExchangesSection = () => {
16187	const { rows, save, saving, loading } = useAdsSpaceEntities(GqlClientAdsSpaceEntityKind.EXCHANGE);
16188	return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsDrillDownSection, {
16189		title: "Exchanges",
16190		titleInPageHeader: true,
16191		searchPlaceholder: "Search exchanges",
16192		rows,
16193		loading,
16194		searchKeys: [
16195			"name",
16196			"prefix",
16197			"adsTxtName"
16198		],
16199		columns: [
16200			{
16201				key: "name",
16202				label: "Exchange",
16203				render: (row) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
16204					variant: "subtitle2",
16205					children: asText(row.name)
16206				})
16207			},
16208			{
16209				key: "prefix",
16210				label: "Prefix",
16211				align: "right"
16212			},
16213			{
16214				key: "maxBidTime",
16215				label: "Max bid time (ms)",
16216				align: "right"
16217			},
16218			{
16219				key: "viewabilityOverrideType",
16220				label: "Viewability correction",
16221				render: (row) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.Label, {
16222					size: "small",
16223					variant: asText(row.viewabilityOverrideType) === "NONE" ? "dashed" : "standard",
16224					color: asText(row.viewabilityOverrideType) === "NONE" ? "default" : "info",
16225					label: viewabilityOverrideOptions.find((option) => option.value === asText(row.viewabilityOverrideType))?.label ?? "Disabled"
16226				})
16227			}
16228		],
16229		getTitle: (row) => asText(row.name),
16230		renderDetail: (row) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ExchangeDetail, {
16231			exchange: row,
16232			save,
16233			saving
16234		}, row.id),
16235		emptyText: "No exchanges are configured",
16236		trackingIdPrefix: "tracking-id-ads-system-exchanges"
16237	});
16238};
16239//#endregion
16240//#region client/components/newNav/components/ads/system/AdsExchangesPage.tsx
16241var AdsExchangesPage = () => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsSectionPage, {
16242	area: "Exchanges",
16243	title: "Exchanges",
16244	subtitle: "Per-exchange timeouts, sampling and viewability. Each setting applies to every campaign bidding into that exchange.",
16245	sections: [{
16246		id: "exchanges",
16247		label: "Exchanges",
16248		entityKinds: [GqlClientAdsSpaceEntityKind.EXCHANGE],
16249		render: () => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ExchangesSection, {})
16250	}],
16251	trackingIdPrefix: "tracking-id-ads-exchanges"
16252});
16253//#endregion
16254//#region client/components/newNav/components/ads/system/PredictionModelsSection.tsx
16255/**
16256* Written by the training pipeline, not by people - the last human edit was in 2024. Everything is read-only
16257* except the description, which is where the SageMaker endpoint is recorded and which the campaign optimization
16258* help text tells people to fill in.
16259*/
16260var modelGroups = [{
16261	id: "identity",
16262	title: "Model",
16263	fields: [
16264		{
16265			key: "id",
16266			label: "Model id",
16267			type: "readOnly"
16268		},
16269		{
16270			key: "modelType",
16271			label: "Model type",
16272			type: "readOnly"
16273		},
16274		{
16275			key: "targetType",
16276			label: "Target",
16277			type: "readOnly"
16278		},
16279		{
16280			key: "useCase",
16281			label: "Use case",
16282			type: "readOnly"
16283		},
16284		{
16285			key: "trainedOnAccount",
16286			label: "Trained on account",
16287			type: "readOnly"
16288		},
16289		{
16290			key: "trainedOnCampaign",
16291			label: "Trained on campaign",
16292			type: "readOnly"
16293		},
16294		{
16295			key: "trainedAt",
16296			label: "Trained at",
16297			type: "readOnly"
16298		}
16299	]
16300}, {
16301	id: "description",
16302	title: "Description",
16303	description: "The one editable field. Campaign optimization expects the SageMaker endpoint to be recorded here.",
16304	fields: [{
16305		key: "description",
16306		label: "Description",
16307		type: "longText",
16308		wide: true
16309	}]
16310}];
16311var ModelDetail = ({ model, save, saving }) => {
16312	const form = useAdsSettingsForm(modelGroups, model, "The prediction model description", (values, comment) => save([{
16313		...values,
16314		id: model.id
16315	}], comment), saving);
16316	return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsSettingsForm, {
16317		groups: modelGroups,
16318		form,
16319		trackingIdPrefix: "tracking-id-ads-system-prediction-model"
16320	});
16321};
16322var PredictionModelsSection = () => {
16323	const { rows, save, saving, loading } = useAdsSpaceEntities(GqlClientAdsSpaceEntityKind.PREDICTION_MODEL);
16324	return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
16325		sx: sectionSx$3,
16326		children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.AttentionBox, {
16327			color: "info",
16328			children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.AttentionBoxContent, { children: "Models are produced by the training pipeline. Search here by account, campaign or target - a campaign's optimization picker reads the same list." })
16329		}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsDrillDownSection, {
16330			title: "Prediction models",
16331			titleInPageHeader: true,
16332			searchPlaceholder: "Search by account, campaign or target",
16333			rows,
16334			loading,
16335			searchKeys: [
16336				"id",
16337				"trainedOnAccount",
16338				"trainedOnCampaign",
16339				"targetType",
16340				"modelType",
16341				"description"
16342			],
16343			columns: [
16344				{
16345					key: "trainedOnAccount",
16346					label: "Trained on",
16347					render: (row) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
16348						variant: "subtitle2",
16349						children: asText(row.trainedOnAccount)
16350					}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
16351						variant: "caption",
16352						color: "text.secondary",
16353						children: asText(row.trainedOnCampaign)
16354					})] })
16355				},
16356				{
16357					key: "targetType",
16358					label: "Target",
16359					render: (row) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.Label, {
16360						size: "small",
16361						color: "default",
16362						variant: "outlined",
16363						label: asText(row.targetType)
16364					})
16365				},
16366				{
16367					key: "modelType",
16368					label: "Model type"
16369				},
16370				{
16371					key: "trainedAt",
16372					label: "Trained",
16373					render: (row) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
16374						variant: "body1",
16375						noWrap: true,
16376						children: TimeUtils.getDesignSystemReadableTimeDate(asText(row.trainedAt), true)
16377					})
16378				},
16379				{
16380					key: "id",
16381					label: "Id",
16382					align: "right"
16383				}
16384			],
16385			getTitle: (row) => `Model ${asText(row.id)}`,
16386			renderDetail: (row) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ModelDetail, {
16387				model: row,
16388				save,
16389				saving
16390			}, row.id),
16391			emptyText: "No prediction models",
16392			trackingIdPrefix: "tracking-id-ads-system-prediction-models"
16393		})]
16394	});
16395};
16396var sectionSx$3 = {
16397	display: "flex",
16398	flexDirection: "column",
16399	gap: 3
16400};
16401//#endregion
16402//#region client/components/newNav/components/ads/system/AdsPredictionModelsPage.tsx
16403var AdsPredictionModelsPage = () => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsSectionPage, {
16404	area: "Programmatic / Prediction models",
16405	title: "Prediction models",
16406	sections: [{
16407		id: "prediction-models",
16408		label: "Prediction models",
16409		entityKinds: [GqlClientAdsSpaceEntityKind.PREDICTION_MODEL],
16410		render: () => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(PredictionModelsSection, {})
16411	}],
16412	trackingIdPrefix: "tracking-id-ads-prediction-models"
16413});
16414//#endregion
16415//#region client/components/newNav/components/ads/system/KillSwitchConfirmDialog.tsx
16416/**
16417* Some settings stop traffic across the platform. The old app let one flip with the same two clicks as a log
16418* sampling rate, so the destructive treatment starts here.
16419*/
16420var KillSwitchConfirmDialog = (props) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Dialog, {
16421	open: props.open,
16422	onClose: props.onCancel,
16423	maxWidth: "sm",
16424	fullWidth: true,
16425	children: [
16426		/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.TruffleDialogTitle, {
16427			CloseIconButtonProps: {
16428				onClick: props.onCancel,
16429				"data-tracking-id": "tracking-id-ads-system-kill-switch-close-btn"
16430			},
16431			children: "Change a kill switch?"
16432		}),
16433		/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.DialogContent, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
16434			sx: contentSx$2,
16435			children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
16436				variant: "body1",
16437				color: "text.secondary",
16438				children: `${props.settingKey} will be set to ${props.nextValue} on every server of this type, immediately after the next configuration reload.`
16439			}
16439), props.help && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
16440				variant: "body1",
16441				color: "text.secondary",
16442				children: props.help
16443			})]
16444		}) }),
16445		/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_dist$2.TruffleDialogActions, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Button, {
16446			variant: "text",
16447			size: "large",
16448			onClick: props.onCancel,
16449			"data-tracking-id": "tracking-id-ads-system-kill-switch-cancel-btn",
16450			children: "Cancel"
16451		}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Button, {
16452			variant: "contained",
16453			color: "error",
16454			size: "large",
16455			onClick: props.onConfirm,
16456			"data-tracking-id": "tracking-id-ads-system-kill-switch-confirm-btn",
16457			children: "Change it"
16458		})] })
16459	]
16460});
16461var contentSx$2 = {
16462	display: "flex",
16463	flexDirection: "column",
16464	gap: 2
16465};
16466//#endregion
16467//#region client/components/newNav/components/ads/system/serverSettingsModel.ts
16468var serverTypes = [
16469	"ADS",
16470	"TRACKING",
16471	"SERVICES",
16472	"DMP",
16473	"WALLED_GARDEN"
16474];
16475var serverTypeLabels = {
16476	ADS: "Ads Server",
16477	TRACKING: "Tracking Server",
16478	SERVICES: "Services Server",
16479	DMP: "DMP",
16480	WALLED_GARDEN: "Walled Garden Engine"
16481};
16482/** The walled garden track is not being rebuilt, so its configuration is readable but not editable. */
16483var readOnlyServerTypes = ["WALLED_GARDEN"];
16484/** What a definition may say a value is. `SECRET` is deliberately absent - that is a flag, not a type. */
16485var serverSettingValueTypes = [
16486	"BOOLEAN",
16487	"INT",
16488	"FLOAT",
16489	"RATIO",
16490	"LIST",
16491	"ENUM",
16492	"STRING"
16493];
16494var ServerSettingFilter = /* @__PURE__ */ function(ServerSettingFilter) {
16495	ServerSettingFilter["ALL"] = "ALL";
16496	ServerSettingFilter["KILL_SWITCH"] = "KILL_SWITCH";
16497	ServerSettingFilter["UNDOCUMENTED"] = "UNDOCUMENTED";
16498	ServerSettingFilter["NOT_SET"] = "NOT_SET";
16499	ServerSettingFilter["DEPRECATED"] = "DEPRECATED";
16500	return ServerSettingFilter;
16501}({});
16502var serverSettingFilterLabels = {
16503	["ALL"]: "All settings",
16504	["KILL_SWITCH"]: "Kill switches",
16505	["UNDOCUMENTED"]: "Missing help text",
16506	["NOT_SET"]: "Not set",
16507	["DEPRECATED"]: "Deprecated"
16508};
16509var matchesServerSettingFilter = (row, filter) => {
16510	switch (filter) {
16511		case "KILL_SWITCH": return row.isKillSwitch;
16512		case "UNDOCUMENTED": return !row.description;
16513		case "NOT_SET": return !row.hasValue;
16514		case "DEPRECATED": return row.isDeprecated;
16515		default: return true;
16516	}
16517};
16518var filterServerSettings = (rows, searchTerm, filter) => {
16519	const term = searchTerm.trim().toLowerCase();
16520	return rows.filter((row) => matchesServerSettingFilter(row, filter) && (!term || row.key.toLowerCase().includes(term) || (row.description ?? "").toLowerCase().includes(term)));
16521};
16522var countUndocumented = (rows) => rows.filter((row) => !row.description).length;
16523/** Documented keys nobody has given a value - the add-setting picker's whole list. */
16524var undocumentedValueKeys = (rows) => rows.filter((row) => !row.hasValue);
16525//#endregion
16526//#region client/components/newNav/components/ads/system/NewServerSettingDialog.tsx
16527/** Documents a key and optionally gives it a value. A key the server does not read is ignored without a word. */
16528var NewServerSettingDialog = (props) => {
16529	const [propertyName, setPropertyName] = (0, import_react.useState)("");
16530	const [valueType, setValueType] = (0, import_react.useState)(serverSettingValueTypes[serverSettingValueTypes.length - 1]);
16531	const [description, setDescription] = (0, import_react.useState)("");
16532	const [value, setValue] = (0, import_react.useState)("");
16533	const [comment, setComment] = (0, import_react.useState)("");
16534	const trimmedName = propertyName.trim();
16535	const isTaken = props.existingKeys.includes(trimmedName);
16536	const canCreate = Boolean(trimmedName) && !isTaken && Boolean(comment.trim()) && !props.saving;
16537	const close = () => {
16538		setPropertyName("");
16539		setDescription("");
16540		setValue("");
16541		setComment("");
16542		props.onClose();
16543	};
16544	return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Dialog, {
16545		open: props.open,
16546		onClose: props.saving ? void 0 : close,
16547		PaperProps: { sx: paperSx },
16548		children: [
16549			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.TruffleDialogTitle, {
16550				CloseIconButtonProps: {
16551					onClick: close,
16552					"data-tracking-id": "tracking-id-ads-system-new-setting-close-btn"
16553				},
16554				children: `New ${props.serverType} setting`
16555			}),
16556			/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.DialogContent, {
16557				sx: contentSx$1,
16558				children: [
16559					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TextField, {
16560						label: "Key",
16561						fullWidth: true,
16562						value: propertyName,
16563						error: isTaken,
16564						helperText: isTaken ? "This key is already documented" : void 0,
16565						onChange: (event) => setPropertyName(event.target.value),
16566						inputProps: { "data-tracking-id": "tracking-id-ads-system-new-setting-key-input" }
16567					}),
16568					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TextField, {
16569						select: true,
16570						label: "Value type",
16571						fullWidth: true,
16572						value: valueType,
16573						onChange: (event) => setValueType(event.target.value),
16574						inputProps: { "data-tracking-id": "tracking-id-ads-system-new-setting-type-input" },
16575						children: serverSettingValueTypes.map((type) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.MenuItem, {
16576							value: type,
16577							children: type
16578						}, type))
16579					}),
16580					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TextField, {
16581						label: "What it means",
16582						fullWidth: true,
16583						multiline: true,
16584						minRows: 2,
16585						value: description,
16586						onChange: (event) => setDescription(event.target.value),
16587						inputProps: { "data-tracking-id": "tracking-id-ads-system-new-setting-description-input" }
16588					}),
16589					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TextField, {
16590						label: "Value (optional)",
16591						fullWidth: true,
16592						value,
16593						onChange: (event) => setValue(event.target.value),
16594						inputProps: { "data-tracking-id": "tracking-id-ads-system-new-setting-value-input" }
16595					}),
16596					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsChangeCommentField, {
16597						value: comment,
16598						onChange: setComment,
16599						trackingId: "tracking-id-ads-system-new-setting-comment-input"
16600					}),
16601					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.AttentionBox, {
16602						color: "info",
16603						children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.AttentionBoxContent, { children: "Documenting a key does not make the server read it. Only keys the server already looks for have any effect; the rest are ignored silently." })
16604					})
16605				]
16606			}),
16607			/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_dist$2.TruffleDialogActions, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Button, {
16608				variant: "text",
16609				size: "large",
16610				disabled: props.saving,
16611				onClick: close,
16612				"data-tracking-id": "tracking-id-ads-system-new-setting-cancel-btn",
16613				children: "Cancel"
16614			}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Button, {
16615				variant: "contained",
16616				size: "large",
16617				loading: props.saving,
16618				disabled: !canCreate,
16619				onClick: () => {
16620					props.onCreate({
16621						propertyName: trimmedName,
16622						valueType,
16623						description,
16624						isKillSwitch: false,
16625						value
16626					}, comment.trim());
16627					close();
16628				},
16629				"data-tracking-id": "tracking-id-ads-system-new-setting-create-btn",
16630				children: "Add setting"
16631			})] })
16632		]
16633	});
16634};
16635var paperSx = {
16636	width: 540,
16637	maxWidth: "calc(100vw - 64px)"
16638};
16639var contentSx$1 = {
16640	display: "flex",
16641	flexDirection: "column",
16642	gap: 2
16643};
16644//#endregion
16645//#region client/components/newNav/components/ads/system/useAdsSpaceServerSettings.ts
16646/** One ads server's runtime configuration: the hub-owned definitions merged with the live values. */
16647var useAdsSpaceServerSettings = (serverType) => {
16648	const { data, loading, error } = useCurrentAccountAdsSpaceServerSettingsQuery({ variables: { serverType } });
16649	const [saveSettings, { loading: saving }] = useSaveAdsSpaceServerSettingsMutation();
16650	const rows = (0, import_react.useMemo)(() => data?.currentAccount?.adsSpace?.serverSettings ?? [], [data]);
16651	const save = async (values, comment) => {
16652		try {
16653			await saveSettings({ variables: {
16654				serverType,
16655				input: {
16656					serverType,
16657					changeComment: comment,
16658					values
16659				}
16660			} });
16661			setSuccessNotification({ message: "Configuration saved - the servers reload it on their next poll" });
16662		} catch (saveError) {
16663			setErrorNotification({ message: saveError?.message ?? "Saving this configuration failed" });
16664		}
16665	};
16666	return {
16667		rows,
16668		loading,
16669		error: Boolean(error),
16670		saving,
16671		save
16672	};
16673};
16674//#endregion
16675//#region client/components/newNav/components/ads/system/ServerConfigurationSection.tsx
16676/**
16677* The platform's runtime configuration console, and the page ops opens during an incident. What the old app did
16678* not do: the documented meaning of every key lives beside it and can be edited, a kill switch says so before it
16679* is flipped, and a value that would not fit the column is refused rather than silently truncated.
16680*/
16681var ServerConfigurationSection = () => {
16682	const [serverType, setServerType] = (0, import_react.useState)("ADS");
16683	const [searchTerm, setSearchTerm] = (0, import_react.useState)("");
16684	const [filter, setFilter] = (0, import_react.useState)(ServerSettingFilter.ALL);
16685	const [filterAnchorEl, setFilterAnchorEl] = (0, import_react.useState)(null);
16686	const [addAnchorEl, setAddAnchorEl] = (0, import_react.useState)(null);
16687	const [valueEdits, setValueEdits] = (0, import_react.useState)({});
16688	const [descriptionEdits, setDescriptionEdits] = (0, import_react.useState)({});
16689	const [pendingKillSwitch, setPendingKillSwitch] = (0, import_react.useState)(null);
16690	const [isCommentOpen, setIsCommentOpen] = (0, import_react.useState)(false);
16691	const [isDiscardOpen, setIsDiscardOpen] = (0, import_react.useState)(false);
16692	const [isNewOpen, setIsNewOpen] = (0, import_react.useState)(false);
16693	const { rows, loading, saving, save } = useAdsSpaceServerSettings(serverType);
16694	const definitions = useAdsSpaceEntities(GqlClientAdsSpaceEntityKind.SERVER_SETTING_DEFINITION);
16695	const visibleRows = (0, import_react.useMemo)(() => filterServerSettings(rows, searchTerm, filter), [
16696		rows,
16697		searchTerm,
16698		filter
16699	]);
16700	const isReadOnly = readOnlyServerTypes.includes(serverType);
16701	const changeCount = Object.keys(valueEdits).length + Object.keys(descriptionEdits).length;
16702	const guard = useAdsUnsavedChangesGuard(changeCount > 0 && !saving && !isReadOnly);
16703	const valueOf = (row) => valueEdits[row.key] ?? row.value ?? "";
16704	const descriptionOf = (row) => descriptionEdits[row.key] ?? row.description ?? "";
16705	const tooLong = Object.entries(valueEdits).filter(([, value]) => value.length > 255).map(([key]) => key);
16706	const editValue = (row, value) => {
16707		if (row.isKillSwitch && !valueEdits[row.key]) {
16708			setPendingKillSwitch({
16709				row,
16710				value
16711			});
16712			return;
16713		}
16714		setValueEdits({
16715			...valueEdits,
16716			[row.key]: value
16717		});
16718	};
16719	const discard = () => {
16720		setValueEdits({});
16721		setDescriptionEdits({});
16722		setIsDiscardOpen(false);
16723		guard.confirmNavigation();
16724	};
16725	const persist = async (comment) => {
16726		const values = Object.entries(valueEdits).map(([key, value]) => ({
16727			key,
16728			value
16729		}));
16730		const changedDefinitions = Object.entries(descriptionEdits).map(([key, description]) => {
16731			const row = rows.find((candidate) => candidate.key === key);
16732			return row?.definitionId ? {
16733				id: row.definitionId,
16734				description
16735			} : {
16736				id: `new-${key}`,
16737				serverType,
16738				propertyName: key,
16739				valueType: "STRING",
16740				description
16741			};
16742		});
16743		try {
16744			if (values.length > 0) await save(values, comment);
16745			if (changedDefinitions.length > 0) await definitions.save(changedDefinitions, comment);
16746			setValueEdits({});
16747			setDescriptionEdits({});
16748		} catch {} finally {
16749			setIsCommentOpen(false);
16750		}
16751	};
16752	const createSetting = async (input, comment) => {
16753		try {
16754			await definitions.save([{
16755				id: `new-${input.propertyName}`,
16756				serverType,
16757				propertyName: input.propertyName,
16758				valueType: input.valueType,
16759				description: input.description,
16760				isKillSwitch: String(input.isKillSwitch),
16761				isSecret: "false",
16762				isDeprecated: "false"
16763			}], comment);
16764			if (input.value) await save([{
16765				key: input.propertyName,
16766				value: input.value
16767			}], comment);
16768		} catch {}
16769	};
16770	return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
16771		sx: sectionSx$2,
16772		children: [
16773			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.TruffleToggleButtonGroup, {
16774				exclusive: true,
16775				size: "small",
16776				value: serverType,
16777				onChange: (_event, next) => next && setServerType(next),
16778				children: serverTypes.map((type) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.ToggleButton, {
16779					value: type,
16780					"data-tracking-id": `tracking-id-ads-system-server-type-${type.toLowerCase()}-btn`,
16781					children: serverTypeLabels[type]
16782				}, type))
16783			}),
16784			isReadOnly ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.AttentionBox, {
16785				color: "warning",
16786				children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.AttentionBoxContent, { children: "The walled garden track is not being rebuilt. Its settings are here so nothing is lost, but they cannot be changed from this page." })
16787			}) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.AttentionBox, {
16788				color: "info",
16789				children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.AttentionBoxContent, { children: "Saving bumps the configuration version, which is what makes the servers reload. Values apply to every account." })
16790			}),
16791			/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
16792				sx: headerSx$1,
16793				children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
16794					sx: { minWidth: 0 },
16795					children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
16796						variant: "h5",
16797						children: `${serverTypeLabels[serverType]} (${rows.length})`
16798					}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
16799						variant: "body2",
16800						color: "text.secondary",
16801						children: `${countUndocumented(rows)} of these have no documented meaning yet.`
16802					})]
16803				}), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
16804					sx: headerActionsSx,
16805					children: [
16806						/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.Filter, {
16807							placeholderValue: serverSettingFilterLabels[ServerSettingFilter.ALL],
16808							selectedValue: filter === ServerSettingFilter.ALL ? null : serverSettingFilterLabels[filter],
16809							onClear: () => setFilter(ServerSettingFilter.ALL),
16810							popoverTitle: "Filter settings",
16811							anchorEl: filterAnchorEl,
16812							setAnchorEl: setFilterAnchorEl,
16813							"data-tracking-id": "tracking-id-ads-system-server-config-filter-btn",
16814							children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.List, { children: Object.values(ServerSettingFilter).map((option) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.ListItemButton, {
16815								dense: true,
16816								selected: filter === option,
16817								onClick: () => {
16818									setFilter(option);
16819									setFilterAnchorEl(null);
16820								},
16821								children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.ListItemText, { primary: serverSettingFilterLabels[option] })
16822							}, option)) })
16823						}),
16824						/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Box, {
16825							sx: searchWrapperSx,
16826							children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.Search, {
16827								placeholder: "Search keys and help",
16828								value: searchTerm,
16829								onChange: (event) => setSearchTerm(event.target.value),
16830								onClear: () => setSearchTerm(""),
16831								numberOfResults: visibleRows.length,
16832								inputProps: { "data-tracking-id": "tracking-id-ads-system-server-config-search-input" }
16833							})
16834						}),
16835						!isReadOnly && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.Filter, {
16836							placeholderValue: "Add setting",
16837							selectedValue: null,
16838							onClear: () => setAddAnchorEl(null),
16839							popoverTitle: "Documented keys with no value",
16840							anchorEl: addAnchorEl,
16841							setAnchorEl: setAddAnchorEl,
16842							"data-tracking-id": "tracking-id-ads-system-server-config-add-btn",
16843							children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.List, { children: undocumentedValueKeys(rows).map((row) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.ListItemButton, {
16844								dense: true,
16845								onClick: () => {
16846									setValueEdits({
16847										...valueEdits,
16848										[row.key]: ""
16849									});
16850									setSearchTerm(row.key);
16851									setAddAnchorEl(null);
16852								},
16853								children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.ListItemText, { primary: row.key })
16854							}, row.key)) })
16855						}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Button, {
16856							variant: "outlined",
16857							size: "medium",
16858							startIcon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FontAwesomeIcon, { icon: import_faPlus.faPlus }),
16859							onClick: () => setIsNewOpen(true),
16860							"data-tracking-id": "tracking-id-ads-system-server-config-new-key-btn",
16861							children: "New key"
16862						})] })
16863					]
16864				})]
16865			}),
16866			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableContainer, {
16867				component: import_material.Paper,
16868				variant: "outlined",
16869				sx: tableSx,
16870				children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Table, {
16871					size: "small",
16872					stickyHeader: true,
16873					children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableHead, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.TableRow, { children: [
16874						/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, {
16875							sx: headCellSx,
16876							children: "Setting"
16877						}),
16878						/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, {
16879							sx: headCellSx,
16880							children: "Type"
16881						}),
16882						/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, {
16883							sx: valueHeadCellSx,
16884							children: "Value"
16885						})
16886					] }) }
16886), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.TableBody, { children: [
16887						loading && rows.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableRow, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, {
16888							colSpan: 3,
16889							sx: emptyCellSx$1,
16890							children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsTableSkeleton, {
16891								rows: 6,
16892								height: 32
16893							})
16894						}) }),
16895						!loading && visibleRows.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableRow, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, {
16896							colSpan: 3,
16897							sx: emptyCellSx$1,
16898							children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
16899								variant: "body1",
16900								color: "text.secondary",
16901								children: "No settings match this filter"
16902							})
16903						}) }),
16904						visibleRows.map((row) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.TableRow, {
16905							sx: rowHoverSx,
16906							"data-testid": `ads-server-setting-${row.key}`,
16907							children: [
16908								/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, {
16909									sx: bodyCellSx,
16910									children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
16911										sx: keyCellSx,
16912										children: [
16913											/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
16914												sx: keyLineSx,
16915												children: [
16916													/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
16917														variant: "subtitle2",
16918														sx: keyTextSx,
16919														children: row.key
16920													}),
16921													row.isKillSwitch && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.Label, {
16922														size: "small",
16923														color: "error",
16924														variant: "standard",
16925														label: "Kill switch"
16926													}),
16927													row.isDeprecated && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.Label, {
16928														size: "small",
16929														color: "warning",
16930														variant: "outlined",
16931														label: "Deprecated"
16932													}),
16933													!row.hasValue && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.Label, {
16934														size: "small",
16935														color: "default",
16936														variant: "dashed",
16937														label: "Not set"
16938													})
16939												]
16940											}),
16941											isReadOnly ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
16942												variant: "helper",
16943												color: row.description ? "text.secondary" : "text.disabled",
16944												children: row.description || "No documented meaning yet"
16945											}) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TextField, {
16946												variant: "standard",
16947												size: "small",
16948												fullWidth: true,
16949												multiline: true,
16950												placeholder: "No documented meaning yet - write one",
16951												value: descriptionOf(row),
16952												onChange: (event) => setDescriptionEdits({
16953													...descriptionEdits,
16954													[row.key]: event.target.value
16955												}),
16956												inputProps: { "data-tracking-id": "tracking-id-ads-system-server-config-description-input" }
16957											}),
16958											row.overrides.map((override) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
16959												variant: "helper",
16960												color: "text.secondary",
16961												children: `Override for hosts containing "${override.host}": ${override.value}`
16962											}, override.host))
16963										]
16964									})
16965								}),
16966								/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, {
16967									sx: bodyCellSx,
16968									children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
16969										variant: "caption",
16970										color: "text.secondary",
16971										children: row.valueType
16972									})
16973								}),
16974								/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, {
16975									sx: valueCellSx,
16976									children: row.isSecret ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
16977										variant: "body1",
16978										color: "text.disabled",
16979										children: "Hidden"
16980									}) : row.valueType === "BOOLEAN" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Switch, {
16981										checked: valueOf(row) === "true",
16982										disabled: isReadOnly,
16983										onChange: (event) => editValue(row, String(event.target.checked)),
16984										"data-tracking-id": "tracking-id-ads-system-server-config-value-toggle"
16985									}) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TextField, {
16986										size: "small",
16987										fullWidth: true,
16988										disabled: isReadOnly,
16989										value: valueOf(row),
16990										error: tooLong.includes(row.key),
16991										placeholder: row.hasValue ? void 0 : "Not set",
16992										onChange: (event) => editValue(row, event.target.value),
16993										inputProps: { "data-tracking-id": "tracking-id-ads-system-server-config-value-input" }
16994									})
16995								})
16996							]
16997						}, row.key))
16998					] })]
16999				})
17000			}),
17001			!isReadOnly && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsSaveBar, {
17002				changeCount,
17003				saveDisabledReason: tooLong.length > 0 ? `A value is longer than 255 characters: ${tooLong[0]}` : void 0,
17004				saving: saving || definitions.saving,
17005				onCancel: () => setIsDiscardOpen(true),
17006				onSave: () => setIsCommentOpen(true),
17007				trackingIdPrefix: "tracking-id-ads-system-server-config"
17008			}),
17009			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(KillSwitchConfirmDialog, {
17010				open: Boolean(pendingKillSwitch),
17011				settingKey: pendingKillSwitch?.row.key ?? "",
17012				help: pendingKillSwitch?.row.description ?? void 0,
17013				nextValue: pendingKillSwitch?.value ?? "",
17014				onCancel: () => setPendingKillSwitch(null),
17015				onConfirm: () => {
17016					if (pendingKillSwitch) setValueEdits({
17017						...valueEdits,
17018						[pendingKillSwitch.row.key]: pendingKillSwitch.value
17019					});
17020					setPendingKillSwitch(null);
17021				}
17022			}),
17023			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(NewServerSettingDialog, {
17024				open: isNewOpen,
17025				serverType,
17026				existingKeys: rows.map((row) => row.key),
17027				saving: definitions.saving,
17028				onClose: () => setIsNewOpen(false),
17029				onCreate: (input, comment) => void createSetting(input, comment)
17030			}),
17031			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsSaveCommentDialog, {
17032				open: isCommentOpen,
17033				summary: changeCount === 1 ? "1 change" : `${changeCount} changes`,
17034				warning: "These values apply to every account, on every server of this type.",
17035				saving: saving || definitions.saving,
17036				onCancel: () => setIsCommentOpen(false),
17037				onConfirm: (comment) => void persist(comment),
17038				trackingIdPrefix: "tracking-id-ads-system-server-config"
17039			}),
17040			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(DiscardChangesDialog, {
17041				open: isDiscardOpen || guard.pendingNavigation,
17042				onContinueEditing: () => {
17043					setIsDiscardOpen(false);
17044					guard.cancelNavigation();
17045				},
17046				onDiscard: discard
17047			})
17048		]
17049	});
17050};
17051var sectionSx$2 = {
17052	display: "flex",
17053	flexDirection: "column",
17054	gap: 2,
17055	alignItems: "stretch",
17056	flex: 1,
17057	minHeight: 0
17058};
17059var headerSx$1 = {
17060	display: "flex",
17061	alignItems: "center",
17062	justifyContent: "space-between",
17063	gap: 2,
17064	flexWrap: "wrap"
17065};
17066var headerActionsSx = {
17067	display: "flex",
17068	alignItems: "center",
17069	gap: 1,
17070	flexShrink: 0
17071};
17072var searchWrapperSx = { width: 280 };
17073var tableSx = {
17074	flex: 1,
17075	minHeight: 0
17076};
17077var keyCellSx = {
17078	display: "flex",
17079	flexDirection: "column",
17080	gap: .5,
17081	maxWidth: 420
17082};
17083var keyLineSx = {
17084	display: "flex",
17085	alignItems: "center",
17086	gap: 1,
17087	flexWrap: "wrap"
17088};
17089var keyTextSx = {
17090	fontFamily: "monospace",
17091	wordBreak: "break-all"
17092};
17093var valueHeadCellSx = {
17094	padding: "10px 16px",
17095	whiteSpace: "nowrap",
17096	width: 220
17097};
17098var valueCellSx = {
17099	padding: "10px 16px",
17100	width: 220
17101};
17102var emptyCellSx$1 = {
17103	padding: "32px 16px",
17104	textAlign: "center"
17105};
17106//#endregion
17107//#region client/components/newNav/components/ads/system/AdsSystemPage.tsx
17108/** Sections that became their own nav rows in the drawer. Their old links keep working. */
17109var movedSections = {
17110	exchanges: import_dist$1.newNavAdsExchangesPath,
17111	"video-responses": import_dist$1.newNavAdsVideoResponsesPath,
17112	"prediction-models": import_dist$1.newNavAdsPredictionModelsPath,
17113	"dmp-providers": import_dist$1.newNavAdsDmpProvidersPath
17114};
17115/**
17116* The platform's runtime configuration, behind the user-scope adsSystemManagement permission - an account admin
17117* must not be able to reconfigure the ad servers for every account.
17118*/
17119var AdsSystemPage = () => {
17120	const accountId = useActiveAccountId();
17121	const { sectionId } = useParams();
17122	const moved = movedSections[sectionId];
17123	if (moved) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Redirect, { to: moved({ accountId }) });
17124	return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsSectionPage, {
17125		area: "Server configuration",
17126		title: "Server configuration",
17127		subtitle: "Platform-wide ads configuration. Changes here affect every account.",
17128		sections: [{
17129			id: "server-configuration",
17130			label: "Server configuration",
17131			render: () => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ServerConfigurationSection, {})
17132		}],
17133		historyChangeTypeContains: "Server Configuration",
17134		trackingIdPrefix: "tracking-id-ads-system"
17135	});
17136};
17137//#endregion
17138//#region client/components/newNav/components/ads/system/VideoResponsesSection.tsx
17139var videoResponseGroups = [
17140	{
17141		id: "identity",
17142		title: "Identity",
17143		fields: [
17144			{
17145				key: "title",
17146				label: "Title",
17147				type: "text",
17148				validate: required("Enter a title")
17149			},
17150			{
17151				key: "description",
17152				label: "Description",
17153				type: "text"
17154			},
17155			{
17156				key: "sku",
17157				label: "SKU",
17158				type: "text"
17159			},
17160			{
17161				key: "productURL",
17162				label: "Product URL",
17163				type: "url",
17164				wide: true
17165			}
17166		]
17167	},
17168	{
17169		id: "format",
17170		title: "Format",
17171		fields: [
17172			{
17173				key: "width",
17174				label: "Width",
17175				type: "number",
17176				min: 0
17177			},
17178			{
17179				key: "height",
17180				label: "Height",
17181				type: "number",
17182				min: 0
17183			},
17184			{
17185				key: "duration",
17186				label: "Duration (seconds)",
17187				type: "number",
17188				min: 0
17189			},
17190			{
17191				key: "bitrateKbs",
17192				label: "Bitrate (kbps)",
17193				type: "number",
17194				min: 0
17195			}
17196		]
17197	},
17198	{
17199		id: "aux",
17200		title: "Auxiliary values",
17201		advanced: true,
17202		description: "Carried into the video URL as ${a1} to ${a4}.",
17203		fields: [
17204			{
17205				key: "aux1",
17206				label: "Aux 1",
17207				type: "text"
17208			},
17209			{
17210				key: "aux2",
17211				label: "Aux 2",
17212				type: "text"
17213			},
17214			{
17215				key: "aux3",
17216				label: "Aux 3",
17217				type: "text"
17218			},
17219			{
17220				key: "aux4",
17221				label: "Aux 4",
17222				type: "text"
17223			}
17224		]
17225	}
17226];
17227var videoFileFields = [
17228	{
17229		key: "format",
17230		label: "Format",
17231		type: "select",
17232		options: videoFormatOptions,
17233		validate: required("Pick the file format")
17234	},
17235	{
17236		key: "url",
17237		label: "URL",
17238		type: "url",
17239		validate: required("Enter the file URL")
17240	},
17241	{
17242		key: "videoid",
17243		label: "Video id",
17244		type: "text"
17245	}
17246];
17247var impressionPixelFields = [{
17248	key: "impressionPixel",
17249	label: "Impression pixel",
17250	type: "url",
17251	validate: required("Enter the pixel URL")
17252}];
17253var companionBannerFields = [
17254	{
17255		key: "creativeType",
17256		label: "Creative type",
17257		type: "text",
17258		help: "The type of the image (jpg, gif, etc.)"
17259	},
17260	{
17261		key: "companionUri",
17262		label: "Companion URI",
17263		type: "url",
17264		help: "The URI of the companion banner"
17265	},
17266	{
17267		key: "width",
17268		label: "Width",
17269		type: "number",
17270		min: 0
17271	},
17272	{
17273		key: "height",
17274		label: "Height",
17275		type: "number",
17276		min: 0
17277	}
17278];
17279var VideoResponseDetail = ({ response, save, saving }) => {
17280	const videos = useAdsSpaceChildRows(GqlClientAdsSpaceEntityKind.VIDEO_RESPONSE_FILE, response.id);
17281	const pixels = useAdsSpaceChildRows(GqlClientAdsSpaceEntityKind.VIDEO_RESPONSE_PIXEL, response.id);
17282	const banners = useAdsSpaceChildRows(GqlClientAdsSpaceEntityKind.VIDEO_RESPONSE_BANNER, response.id);
17283	const form = useAdsSettingsForm(videoResponseGroups, response, "The video response", async (values, comment) => {
17284		await save([{
17285			...values,
17286			id: response.id
17287		}], comment);
17288		await Promise.all([
17289			videos.persist(comment),
17290			pixels.persist(comment),
17291			banners.persist(comment)
17292		]);
17293	}, saving || videos.saving || pixels.saving || banners.saving, [
17294		videos,
17295		pixels,
17296		banners
17297	]);
17298	return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsSettingsForm, {
17299		groups: videoResponseGroups,
17300		form,
17301		trackingIdPrefix: "tracking-id-ads-system-video-response",
17302		children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
17303			sx: childTablesSx,
17304			children: [
17305				/* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsChildTable, {
17306					title: "Video files",
17307					description: "One row per format the player can be handed.",
17308					fields: videoFileFields,
17309					rows: videos.rows,
17310					addLabel: "Add file",
17311					emptyText: "No files on this response yet",
17312					rowTitle: (row) => asText(row.url),
17313					onChange: videos.setRows,
17314					trackingIdPrefix: "tracking-id-ads-system-video-file"
17315				}),
17316				/* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsChildTable, {
17317					title: "Impression pixels",
17318					fields: impressionPixelFields,
17319					rows: pixels.rows,
17320					addLabel: "Add pixel",
17321					emptyText: "No impression pixels on this response",
17322					rowTitle: (row) => asText(row.impressionPixel),
17323					onChange: pixels.setRows,
17324					trackingIdPrefix: "tracking-id-ads-system-video-pixel"
17325				}),
17326				/* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsChildTable, {
17327					title: "Companion banners",
17328					fields: companionBannerFields,
17329					rows: banners.rows,
17330					addLabel: "Add banner",
17331					emptyText: "No companion banners on this response",
17332					rowTitle: (row) => asText(row.companionUri),
17333					onChange: banners.setRows,
17334					trackingIdPrefix: "tracking-id-ads-system-video-banner"
17335				})
17336			]
17337		})
17338	});
17339};
17340/**
17341* The concrete assets a campaign's control group picker chooses from, so finding one is the whole job - and
17342* most rows carry no title, description or SKU at all. What identifies them in practice is the id a campaign
17343* points at and the product URL, so those lead and the title is shown when there is one.
17344*/
17345var VideoResponsesSection = () => {
17346	const { rows, save, saving, loading } = useAdsSpaceEntities(GqlClientAdsSpaceEntityKind.VIDEO_RESPONSE);
17347	const describe = (row) => asText(row.title) || asText(row.productURL) || `Response ${row.id}`;
17348	return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsDrillDownSection, {
17349		title: "Video responses",
17350		titleInPageHeader: true,
17351		searchPlaceholder: "Search by id, product URL or title",
17352		rows,
17353		loading,
17354		searchKeys: [
17355			"id",
17356			"title",
17357			"sku",
17358			"description",
17359			"productURL"
17360		],
17361		columns: [
17362			{
17363				key: "id",
17364				label: "Id",
17365				align: "right"
17366			},
17367			{
17368				key: "title",
17369				label: "Video response",
17370				render: (row) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
17371					variant: "subtitle2",
17372					children: describe(row)
17373				}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
17374					variant: "caption",
17375					color: "text.secondary",
17376					children: asText(row.description) || `${asText(row.width)}x${asText(row.height)}`
17377				})] })
17378			},
17379			{
17380				key: "sku",
17381				label: "SKU"
17382			},
17383			{
17384				key: "duration",
17385				label: "Duration",
17386				align: "right",
17387				render: (row) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
17388					variant: "body1",
17389					children: `${asText(row.duration)}s`
17390				})
17391			}
17392		],
17393		getTitle: describe,
17394		renderDetail: (row) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(VideoResponseDetail, {
17395			response: row,
17396			save,
17397			saving
17398		}, row.id),
17399		emptyText: "No video responses are configured",
17400		trackingIdPrefix: "tracking-id-ads-system-video-responses"
17401	});
17402};
17403var childTablesSx = {
17404	display: "flex",
17405	flexDirection: "column",
17406	gap: 4
17407};
17408//#endregion
17409//#region client/components/newNav/components/ads/system/AdsVideoResponsesPage.tsx
17410var AdsVideoResponsesPage = () => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsSectionPage, {
17411	area: "Video responses",
17412	title: "Video responses",
17413	subtitle: "Ready-made videos the ad server can serve directly, including the control group videos campaigns pick by name.",
17414	sections: [{
17415		id: "video-responses",
17416		label: "Video responses",
17417		entityKinds: [
17418			GqlClientAdsSpaceEntityKind.VIDEO_RESPONSE,
17419			GqlClientAdsSpaceEntityKind.VIDEO_RESPONSE_FILE,
17420			GqlClientAdsSpaceEntityKind.VIDEO_RESPONSE_PIXEL,
17421			GqlClientAdsSpaceEntityKind.VIDEO_RESPONSE_BANNER
17422		],
17423		render: () => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(VideoResponsesSection, {})
17424	}],
17425	trackingIdPrefix: "tracking-id-ads-video-responses"
17426});
17427//#endregion
17428//#region client/components/newNav/components/ads/targeting/AudienceTreeEditor.tsx
17429var optionLabel = (options, value) => options.find((option) => option.value === value)?.label ?? value;
17430var describeRecency = (row) => {
17431	const min = asText(row.minAgeDays);
17432	const max = asText(row.maxAgeDays);
17433	if (!min && !max) return "Any time";
17434	return `${min || "0"} to ${max || "any"} days ago`;
17435};
17436var levels = [
17437	{
17438		kind: GqlClientAdsSpaceEntityKind.AUDIENCE_RULE_SUPER_GROUP,
17439		title: "Rule sets",
17440		description: "Each set covers one source of evidence. All the sets of an audience must match.",
17441		addLabel: "Add rule set",
17442		emptyText: "No rule sets - this audience matches nobody",
17443		fields: [{
17444			key: "ruleType",
17445			label: "Evidence",
17446			type: "text",
17447			defaultValue: "SITE_VISITOR"
17448		}, {
17449			key: "matchType",
17450			label: "Rule groups must",
17451			type: "select",
17452			options: ruleMatchTypeOptions,
17453			defaultValue: "MATCH_ANY_RULE"
17454		}],
17455		columns: [{
17456			key: "ruleType",
17457			label: "Evidence"
17458		}, {
17459			key: "matchType",
17460			label: "Rule groups must",
17461			render: (row) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
17462				variant: "body1",
17463				children: optionLabel(ruleMatchTypeOptions, asText(row.matchType))
17464			})
17465		}],
17466		rowTitle: (row) => asText(row.ruleType),
17467		drillLabel: "Rule groups"
17468	},
17469	{
17470		kind: GqlClientAdsSpaceEntityKind.AUDIENCE_RULE_GROUP,
17471		title: "Rule groups",
17472		description: "A group narrows its rules to a window of time - how long ago the evidence was seen.",
17473		addLabel: "Add rule group",
17474		emptyText: "No rule groups in this set",
17475		fields: [
17476			{
17477				key: "matchType",
17478				label: "Rules must",
17479				type: "select",
17480				options: ruleMatchTypeOptions,
17481				defaultValue: "MATCH_ALL_RULES"
17482			},
17483			{
17484				key: "minAgeDays",
17485				label: "Seen no sooner than (days ago)",
17486				type: "number",
17487				min: 0,
17488				validate: inRange(0, 3650, "Use a whole number of days")
17489			},
17490			{
17491				key: "maxAgeDays",
17492				label: "Seen no longer ago than (days)",
17493				type: "number",
17494				min: 0,
17495				validate: inRange(0, 3650, "Use a whole number of days")
17496			}
17497		],
17498		columns: [{
17499			key: "matchType",
17500			label: "Rules must",
17501			render: (row) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
17502				variant: "body1",
17503				children: optionLabel(ruleMatchTypeOptions, asText(row.matchType))
17504			})
17505		}, {
17506			key: "recency",
17507			label: "Seen",
17508			render: (row) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
17509				variant: "body1",
17510				children: describeRecency(row)
17511			})
17512		}],
17513		rowTitle: (row) => describeRecency(row),
17514		drillLabel: "Rules"
17515	},
17516	{
17517		kind: GqlClientAdsSpaceEntityKind.AUDIENCE_RULE,
17518		title: "Rules",
17519		description: "One condition on the evidence - a segment id, a page name, a product.",
17520		addLabel: "Add rule",
17521		emptyText: "No rules in this group",
17522		fields: [
17523			{
17524				key: "name",
17525				label: "Field",
17526				type: "text",
17527				validate: required("A rule needs a field")
17528			},
17529			{
17530				key: "operator",
17531				label: "Operator",
17532				type: "select",
17533				options: audienceRuleOperatorOptions
17534			},
17535			{
17536				key: "value",
17537				label: "Value",
17538				type: "text",
17539				wide: true,
17540				help: "The ad server splits this on commas, so several values are a comma-separated list."
17541			}
17542		],
17543		columns: [
17544			{
17545				key: "name",
17546				label: "Field"
17547			},
17548			{
17549				key: "operator",
17550				label: "Operator",
17551				render: (row) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
17552					variant: "body1",
17553					children: optionLabel(audienceRuleOperatorOptions, asText(row.operator))
17554				})
17555			},
17556			{
17557				key: "value",
17558				label: "Value"
17559			}
17560		],
17561		rowTitle: (row) => asText(row.name)
17562	}
17563];
17564/**
17565* The audience rule tree exactly as the schema holds it, a level at a time. This is the escape hatch: the
17566* everyday editor covers the three shapes audiences actually take, and anything outside them - a rule set with
17567* several rule groups - is edited here rather than being flattened into the nearest shape that editor can hold.
17568*/
17569var AudienceTreeEditor = ({ audienceId }) => {
17570	const [path, setPath] = (0, import_react.useState)([]);
17571	const level = levels[path.length];
17572	const parentId = path.length === 0 ? audienceId : path[path.length - 1].id;
17573	const drillColumn = {
17574		key: "drill",
17575		label: "",
17576		align: "right",
17577		render: (row) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Button, {
17578			variant: "text",
17579			size: "small",
17580			endIcon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FontAwesomeIcon, { icon: import_faAngleRight.faAngleRight }),
17581			onClick: () => setPath([...path, {
17582				id: row.id,
17583				label: level.rowTitle(row)
17584			}]),
17585			"data-tracking-id": "tracking-id-ads-targeting-audience-drill-btn",
17586			children: level.drillLabel
17587		})
17588	};
17589	return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
17590		sx: sectionSx$1,
17591		children: [path.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Breadcrumbs, {
17592			separator: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FontAwesomeIcon, { icon: import_faAngleRight.faAngleRight }),
17593			children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Link, {
17594				component: "button",
17595				variant: "body1",
17596				underline: "hover",
17597				onClick: () => setPath([]),
17598				"data-tracking-id": "tracking-id-ads-targeting-audience-breadcrumb-btn",
17599				children: "Rule sets"
17600			}), path.map((crumb, index) => index === path.length - 1 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
17601				variant: "body1",
17602				color: "text.primary",
17603				children: crumb.label
17604			}, crumb.id) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Link, {
17605				component: "button",
17606				variant: "body1",
17607				underline: "hover",
17608				onClick: () => setPath(path.slice(0, index + 1)),
17609				"data-tracking-id": "tracking-id-ads-targeting-audience-breadcrumb-btn",
17610				children: crumb.label
17611			}, crumb.id))]
17612		}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsEntityRowsSection, {
17613			kind: level.kind,
17614			parentId,
17615			title: level.title,
17616			description: level.description,
17617			fields: level.fields,
17618			columns: level.drillLabel ? [...level.columns, drillColumn] : level.columns,
17619			addLabel: level.addLabel,
17620			emptyText: level.emptyText,
17621			rowTitle: level.rowTitle,
17622			trackingIdPrefix: "tracking-id-ads-targeting-audience-tree"
17623		}, `${level.kind}-${parentId}`)]
17624	});
17625};
17626var sectionSx$1 = {
17627	display: "flex",
17628	flexDirection: "column",
17629	gap: 2,
17630	flex: 1,
17631	minHeight: 0
17632};
17633//#endregion
17634//#region client/components/newNav/components/ads/targeting/AudiencesSection.tsx
17635/**
17636* The account's audiences. An audience is four tables deep in the ads schema and the configuration app makes
17637* people walk all four to define one; here the everyday shapes - a segment list, site activity, an exclusion of
17638* whoever another campaign already reached - are entered directly and the tree is assembled on save.
17639*/
17640var AudiencesSection = () => {
17641	const { audiences, loading, error, saving, save, refetch } = useAdsSpaceAudiences();
17642	const { save: saveEntities, saving: deleting } = useAdsSpaceEntities(GqlClientAdsSpaceEntityKind.AUDIENCE);
17643	const [editing, setEditing] = (0, import_react.useState)(null);
17644	const [advancedId, setAdvancedId] = (0, import_react.useState)(null);
17645	const [isCommentOpen, setIsCommentOpen] = (0, import_react.useState)(false);
17646	const [isDiscardOpen, setIsDiscardOpen] = (0, import_react.useState)(false);
17647	const [menuAnchorEl, setMenuAnchorEl] = (0, import_react.useState)(null);
17648	const [menuAudience, setMenuAudience] = (0, import_react.useState)(null);
17649	const isDirty = Boolean(editing) && JSON.stringify(editing.draft) !== JSON.stringify(toDraft(editing.audience));
17650	const guard = useAdsUnsavedChangesGuard(isDirty && !saving);
17651	const closeMenu = () => {
17652		setMenuAnchorEl(null);
17653		setMenuAudience(null);
17654	};
17655	const remove = async (audience) => {
17656		closeMenu();
17657		try {
17658			await saveEntities([], `Removed audience ${audience.name}`, [audience.id]);
17659			await refetch();
17660		} catch {}
17661	};
17662	const persist = async (comment) => {
17663		try {
17664			await save({
17665				id: editing.audience?.id,
17666				name: editing.draft.name.trim(),
17667				blocks: toBlocks(editing.draft),
17668				changeComment: comment
17669			});
17670			setEditing(null);
17671		} catch {} finally {
17672			setIsCommentOpen(false);
17673		}
17674	};
17675	const rowActions = (audience) => [{
17676		label: canEditSimply(audience) ? "Edit" : "Edit rule tree",
17677		icon: import_faPen.faPen,
17678		onClick: () => {
17679			closeMenu();
17680			if (canEditSimply(audience)) setEditing({
17681				audience,
17682				draft: toDraft(audience)
17683			});
17684			else setAdvancedId(audience.id);
17685		}
17686	}, {
17687		label: "Remove",
17688		icon: import_faTrashCan.faTrashCan,
17689		error: true,
17690		dividerBefore: true,
17691		onClick: () => void remove(audience)
17692	}];
17693	if (advancedId) {
17694		const audience = audiences.find((candidate) => candidate.id === advancedId);
17695		return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
17696			sx: sectionSx,
17697			children: [
17698				/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Breadcrumbs, {
17699					separator: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FontAwesomeIcon, { icon: import_faAngleRight.faAngleRight }),
17700					children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Link, {
17701						component: "button",
17702						variant: "body1",
17703						underline: "hover",
17704						onClick: () => setAdvancedId(null),
17705						"data-tracking-id": "tracking-id-ads-targeting-audiences-breadcrumb-btn",
17706						children: "Audiences"
17707					}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
17708						variant: "body1",
17709						color: "text.primary",
17710						children: audience?.name ?? "Audience"
17711					})]
17712				}),
17713				/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_dist$2.AttentionBox, {
17714					color: "warning",
17715					children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.AttentionBoxTitle, { children: "This audience uses a shape the simple editor cannot hold" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.AttentionBoxContent, { children: "One of its rule sets has more than one rule group, which is an either/or the simple editor would flatten. It is edited here as the four tables it is stored in." })]
17716				}),
17717				/* @__PURE__ */ (0, import_jsx_runtime.jsx)(AudienceTreeEditor, { audienceId: advancedId })
17718			]
17719		});
17720	}
17721	if (editing) {
17722		const problem = describeDraftProblem(editing.draft);
17723		return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
17724			sx: sectionSx,
17725			children: [
17726				/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Breadcrumbs, {
17727					separator: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FontAwesomeIcon, { icon: import_faAngleRight.faAngleRight }),
17728					children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Link, {
17729						component: "button",
17730						variant: "body1",
17731						underline: "hover",
17732						onClick: () => isDirty ? setIsDiscardOpen(true) : setEditing(null),
17733						"data-tracking-id": "tracking-id-ads-targeting-audiences-breadcrumb-btn",
17734						children: "Audiences"
17735					}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
17736						variant: "body1",
17737						color: "text.primary",
17738						children: editing.audience?.name || "New audience"
17739					})]
17740				}),
17741				/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Box, {
17742					sx: bodySx,
17743					children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AudienceEditor, {
17744						draft: editing.draft,
17745						onChange: (draft) => setEditing({
17746							...editing,
17747							draft
17748						})
17749					})
17750				}),
17751				/* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsSaveBar, {
17752					changeCount: isDirty ? 1 : 0,
17753					saveDisabledReason: problem ?? void 0,
17754					saving,
17755					onCancel: () => isDirty ? setIsDiscardOpen(true) : setEditing(null),
17756					onSave: () => setIsCommentOpen(true),
17757					trackingIdPrefix: "tracking-id-ads-targeting-audiences"
17758				}),
17759				/* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsSaveCommentDialog, {
17760					open: isCommentOpen,
17761					summary: editing.audience ? `Changes to ${editing.audience.name}` : "A new audience",
17762					warning: editing.audience ? "Every campaign pointing at this audience serves on the new rules." : void 0,
17763					saving,
17764					onCancel: () => setIsCommentOpen(false),
17765					onConfirm: (comment) => void persist(comment),
17766					trackingIdPrefix: "tracking-id-ads-targeting-audiences"
17767				}),
17768				/* @__PURE__ */ (0, import_jsx_runtime.jsx)(DiscardChangesDialog, {
17769					open: isDiscardOpen || guard.pendingNavigation,
17770					onContinueEditing: () => {
17771						setIsDiscardOpen(false);
17772						guard.cancelNavigation();
17773					},
17774					onDiscard: () => {
17775						setEditing(null);
17776						setIsDiscardOpen(false);
17777						guard.confirmNavigation();
17778					}
17779				})
17780			]
17781		});
17782	}
17783	return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
17784		sx: sectionSx,
17785		children: [
17786			error && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_dist$2.AttentionBox, {
17787				color: "error",
17788				children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.AttentionBoxTitle, { children: "The ads configuration is unreachable" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.AttentionBoxContent, { children: "This list could not be read, so it is not the whole picture." })]
17789			}),
17790			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.AttentionBox, {
17791				color: "info",
17792				children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.AttentionBoxContent, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
17793					variant: "body1",
17794					color: "text.secondary",
17795					children: "An audience belongs to the account, not to one campaign. Changing it changes every campaign pointing at it, and its name is what third-party pixels receive."
17796				}) })
17797			}),
17798			/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
17799				sx: headerSx,
17800				children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
17801					variant: "h5",
17802					children: "Audiences"
17803				}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
17804					variant: "body2",
17805					color: "text.secondary",
17806					children: "Who campaigns may bid on. Define one here and point any number of campaigns at it."
17807				})] }
17807), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Button, {
17808					variant: "outlined",
17809					size: "medium",
17810					startIcon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FontAwesomeIcon, { icon: import_faPlus.faPlus }),
17811					onClick: () => setEditing({
17812						audience: null,
17813						draft: toDraft(null)
17814					}),
17815					"data-tracking-id": "tracking-id-ads-targeting-audiences-add-btn",
17816					children: "New audience"
17817				})]
17818			}),
17819			/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Box, {
17820				sx: bodySx,
17821				children: loading && audiences.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Skeleton, {
17822					variant: "rounded",
17823					height: 160
17824				}) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableContainer, {
17825					component: import_material.Paper,
17826					variant: "outlined",
17827					children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Table, {
17828						size: "small",
17829						children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableHead, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.TableRow, { children: [
17830							/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, {
17831								sx: headCellSx,
17832								children: "Audience"
17833							}),
17834							/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, {
17835								sx: headCellSx,
17836								children: "Matches"
17837							}),
17838							/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, { sx: actionsCellSx })
17839						] }) }
17839), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.TableBody, { children: [audiences.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableRow, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, {
17840							colSpan: 3,
17841							sx: emptyCellSx,
17842							children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
17843								variant: "body1",
17844								color: "text.secondary",
17845								children: "No audiences defined yet"
17846							})
17847						}) }), audiences.map((audience) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.TableRow, {
17848							sx: rowHoverSx,
17849							"data-testid": `ads-audience-row-${audience.id}`,
17850							children: [
17851								/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, {
17852									sx: (0, import_dist$2.combineSxProps)(bodyCellSx, cellSx),
17853									children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
17854										variant: "body1",
17855										children: audience.name
17856									})
17857								}),
17858								/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, {
17859									sx: (0, import_dist$2.combineSxProps)(bodyCellSx, cellSx),
17860									children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
17861										sx: matchesCellSx,
17862										children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Typography, {
17863											variant: "body1",
17864											children: describeAudience(audience)
17865										}
17865), !canEditSimply(audience) && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.Label, {
17866											size: "small",
17867											variant: "dashed",
17868											color: "default",
17869											label: "Rule tree"
17870										})]
17871									})
17872								}),
17873								/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.TableCell, {
17874									sx: actionsCellSx,
17875									children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dist$2.TruffleIconButton, {
17876										size: "small",
17877										disabled: deleting,
17878										onClick: (event) => {
17879											setMenuAnchorEl(event.currentTarget);
17880											setMenuAudience(audience);
17881										},
17882										"data-tracking-id": "tracking-id-ads-targeting-audiences-row-options-btn",
17883										children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FontAwesomeIcon, { icon: import_faEllipsisV.faEllipsisV })
17884									})
17885								})
17886							]
17887						}, audience.id))] })]
17888					})
17889				})
17890			}),
17891			menuAudience && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(RowOptionsMenu, {
17892				anchorEl: menuAnchorEl,
17893				open: Boolean(menuAnchorEl),
17894				onClose: closeMenu,
17895				title: menuAudience.name,
17896				actions: rowActions(menuAudience),
17897				anchorOrigin: {
17898					vertical: "bottom",
17899					horizontal: "right"
17900				},
17901				transformOrigin: {
17902					vertical: "top",
17903					horizontal: "right"
17904				}
17905			})
17906		]
17907	});
17908};
17909var sectionSx = {
17910	display: "flex",
17911	flexDirection: "column",
17912	gap: 3,
17913	flex: 1,
17914	minHeight: 0
17915};
17916var headerSx = {
17917	display: "flex",
17918	alignItems: "center",
17919	justifyContent: "space-between",
17920	gap: 2
17921};
17922var bodySx = {
17923	flex: 1,
17924	minHeight: 0,
17925	overflow: "auto"
17926};
17927var cellSx = {
17928	maxWidth: 420,
17929	wordBreak: "break-word"
17930};
17931var matchesCellSx = {
17932	display: "flex",
17933	alignItems: "center",
17934	gap: 1
17935};
17936var emptyCellSx = {
17937	padding: "24px 16px",
17938	textAlign: "center"
17939};
17940//#endregion
17941//#region client/components/newNav/components/ads/targeting/AdsTargetingPage.tsx
17942var sections = [{
17943	id: "audiences",
17944	label: "Audiences",
17945	icon: import_faUsersViewfinder.faUsersViewfinder,
17946	render: () => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AudiencesSection, {})
17947}, {
17948	id: "geo-lists",
17949	label: "Geographies",
17950	icon: import_faEarthAmericas.faEarthAmericas,
17951	render: () => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(GeoListsSection, {})
17952}];
17953/**
17954* The account's reusable targeting objects. Both were per-campaign columns in the old app, which is why the same
17955* audience and the same state exclusion list exist in a dozen slightly different copies.
17956*/
17957var AdsTargetingPage = () => {
17958	const accountId = useActiveAccountId();
17959	return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsSectionPage, {
17960		area: "Programmatic / Targeting",
17961		title: "Targeting",
17962		subtitle: "Audiences and geographies defined once for the account. Campaigns and budget lanes point at them.",
17963		sections,
17964		sectionPath: (sectionId) => (0, import_dist$1.newNavAdsTargetingSectionPath)({
17965			accountId,
17966			sectionId
17967		}),
17968		trackingIdPrefix: "tracking-id-ads-targeting"
17969	});
17970};
17971//#endregion
17972//#region client/components/newNav/components/ads/AdsSpaceShell.tsx
17973var AdsSpaceShell = ({ logout }) => {
17974	const accountId = useActiveAccountId();
17975	const { userRoles } = useUserData();
17976	const location = useLocation();
17977	const navZones = (0, import_react.useMemo)(() => getVisibleAdsNavZones(userRoles), [userRoles]);
17978	const selectedNav = (0, import_react.useMemo)(() => getSelectedAdsNav(location.pathname, accountId), [location.pathname, accountId]);
17979	const selectedNavId = selectedNav?.itemId ?? null;
17980	const [opened, setOpened] = (0, import_react.useState)(null);
17981	const expandedZoneId = [
17982		opened?.pathname === location.pathname ? opened.zoneId : null,
17983		selectedNav?.zoneId,
17984		"programmatic"
17985	].find((candidate) => navZones.some((zone) => zone.id === candidate && zone.collapsible)) ?? navZones.find((zone) => zone.collapsible)?.id;
17986	const canEditSettings = isPermitted(userRoles, permissions.adsEditCampaignSettings);
17987	const canManageSystem = isPermitted(userRoles, permissions.adsSystemManagement);
17988	return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
17989		sx: spaceSx,
17990		children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Drawer, {
17991			sx: drawerSx,
17992			variant: "permanent",
17993			anchor: "left",
17994			children: [
17995				/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
17996					sx: lockupSx,
17997					children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Link$6, {
17998						to: (0, import_dist$1.newNavAdsDashboardPath)({ accountId }),
17999						"data-tracking-id": "tracking-id-ads-space-left-panel-ssky-logo-btn",
18000						children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("img", {
18001							src: sundaysky_default,
18002							alt: "sundaysky-logo",
18003							width: "100%",
18004							style: { padding: "0 32px" }
18005						})
18006					}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsAccountLabel, {})]
18007				}),
18008				/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Divider, { sx: lockupDividerSx }),
18009				/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.List, {
18010					sx: navListSx,
18011					component: "nav",
18012					children: navZones.map((zone, zoneIndex) => {
18013						const isExpanded = !zone.collapsible || expandedZoneId === zone.id;
18014						const isFirstBelowDivider = Boolean(zone.belowDivider) && !navZones[zoneIndex - 1]?.belowDivider;
18015						const rows = zone.items.map((item) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.ListItemButton, {
18016							dense: true,
18017							component: Link$6,
18018							to: item.to({ accountId }),
18019							selected: selectedNavId === item.id,
18020							sx: zone.collapsible ? nestedItemSx : void 0,
18021							"data-tracking-id": `tracking-id-ads-space-left-panel-${item.id}-tab`,
18022							children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.ListItemText, {
18023								primary: item.label,
18024								slotProps: { primary: selectedNavId === item.id ? {
18025									variant: "subtitle2",
18026									color: "primary.main",
18027									noWrap: true
18028								} : { noWrap: true } }
18029							})
18030						}, item.id));
18031						return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react.Fragment, { children: [
18032							isFirstBelowDivider && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Divider, { sx: navDividerSx }),
18033							zone.collapsible && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.ListItemButton, {
18034								dense: true,
18035								onClick: () => setOpened({
18036									zoneId: zone.id,
18037									pathname: location.pathname
18038								}),
18039								"data-tracking-id": `tracking-id-ads-space-left-panel-${zone.id}-section-btn`,
18040								children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.ListItemText, {
18041									primary: zone.label,
18042									slotProps: { primary: {
18043										variant: "subtitle2",
18044										color: "text.secondary",
18045										noWrap: true
18046									} }
18047								}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.SvgIcon, {
18048									fontSize: "small",
18049									color: "action",
18050									sx: chevronSx(isExpanded),
18051									children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FontAwesomeIcon, { icon: import_faAngleDown.faAngleDown })
18052								})]
18053							}),
18054							zone.collapsible ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Collapse, {
18055								in: isExpanded,
18056								unmountOnExit: true,
18057								children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.List, {
18058									component: "div",
18059									disablePadding: true,
18060									children: rows
18061								})
18062							}) : rows
18063						] }, zone.id);
18064					})
18065				}),
18066				canManageSystem && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Button, {
18067					variant: "outlined",
18068					size: "small",
18069					fullWidth: true,
18070					component: Link$6,
18071					to: (0, import_dist$1.newNavAdsManagementPath)(),
18072					sx: allAccountsButtonSx,
18073					"data-tracking-id": "tracking-id-ads-space-left-panel-ads-management-btn",
18074					children: "All-accounts management"
18075				}),
18076				/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
18077					sx: userMenuSx,
18078					children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Divider, { sx: { width: "100%" } }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(UserMenu, {
18079						trackingId: "tracking-id-avatar-3dots-menu-ads-space",
18080						maxWidth: `203px`,
18081						logout
18082					})]
18083				})
18084			]
18085		}), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_material.Box, {
18086			sx: contentSx,
18087			children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsNonProductionBanner, {}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_material.Box, {
18088				sx: scrollingContentSx,
18089				children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsAccountLinkGate, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Switch$3, { children: [
18090					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Route, {
18091						exact: true,
18092						path: import_dist$1.newNavAdsDashboardPath.pattern,
18093						children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsAccountDashboardPage, {})
18094					}),
18095					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Route, {
18096						exact: true,
18097						path: import_dist$1.newNavAdsCampaignsPath.pattern,
18098						children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CampaignsPage, {})
18099					}),
18100					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Route, {
18101						exact: true,
18102						path: [import_dist$1.newNavAdsCampaignSectionPath.pattern, import_dist$1.newNavAdsCampaignPath.pattern],
18103						children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CampaignDetailPage, {})
18104					}),
18105					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Route, {
18106						exact: true,
18107						path: import_dist$1.newNavAdsDecoupledCampaignsPath.pattern,
18108						children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(DecoupledCampaignsPage, {})
18109					}),
18110					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Route, {
18111						exact: true,
18112						path: [import_dist$1.newNavAdsDecoupledCampaignSectionPath.pattern, import_dist$1.newNavAdsDecoupledCampaignPath.pattern],
18113						children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CampaignDetailPage, {})
18114					}),
18115					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Route, {
18116						exact: true,
18117						path: [import_dist$1.newNavAdsTargetingSectionPath.pattern, import_dist$1.newNavAdsTargetingPath.pattern],
18118						children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsTargetingPage, {})
18119					}),
18120					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Route, {
18121						exact: true,
18122						path: import_dist$1.newNavAdsFlightsPath.pattern,
18123						children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsFlightsPage, {})
18124					}),
18125					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Route, {
18126						exact: true,
18127						path: import_dist$1.newNavAdsExperimentsPath.pattern,
18128						children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ExperimentsPage, {})
18129					}),
18130					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Route, {
18131						exact: true,
18132						path: import_dist$1.newNavAdsExperimentPath.pattern,
18133						children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ExperimentDetailPage, {})
18134					}),
18135					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Route, {
18136						exact: true,
18137						path: import_dist$1.newNavAdsAuditLogPath.pattern,
18138						children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsAuditLogPage, {})
18139					}),
18140					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Route, {
18141						exact: true,
18142						path: import_dist$1.newNavAdsUtilitiesPath.pattern,
18143						children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Redirect, { to: (0, import_dist$1.newNavAdsUtilityPath)({
18144							accountId,
18145							utilityId: "zip-radius"
18146						}) })
18147					}),
18148					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Route, {
18149						exact: true,
18150						path: import_dist$1.newNavAdsUtilityPath.pattern,
18151						children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsUtilityPage, {})
18152					}),
18153					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Route, {
18154						exact: true,
18155						path: import_dist$1.newNavAdsDmpAccountMappingsPath.pattern,
18156						children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsDmpAccountMappingsPage, {})
18157					}),
18158					canManageSystem && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Route, {
18159						exact: true,
18160						path: import_dist$1.newNavAdsExchangesPath.pattern,
18161						children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsExchangesPage, {})
18162					}),
18163					canManageSystem && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Route, {
18164						exact: true,
18165						path: import_dist$1.newNavAdsVideoResponsesPath.pattern,
18166						children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsVideoResponsesPage, {})
18167					}),
18168					canManageSystem && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Route, {
18169						exact: true,
18170						path: import_dist$1.newNavAdsPredictionModelsPath.pattern,
18171						children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsPredictionModelsPage, {})
18172					}),
18173					canManageSystem && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Route, {
18174						exact: true,
18175						path: import_dist$1.newNavAdsDmpProvidersPath.pattern,
18176						children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsDmpProvidersPage, {})
18177					}),
18178					canManageSystem && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Route, {
18179						exact: true,
18180						path: [import_dist$1.newNavAdsDspSectionPath.pattern, import_dist$1.newNavAdsDspsPath.pattern],
18181						children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsDspPage, {})
18182					}),
18183					canEditSettings && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Route, {
18184						exact: true,
18185						path: [import_dist$1.newNavAdsSettingsSectionPath.pattern, import_dist$1.newNavAdsSettingsPath.pattern],
18186						children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsSettingsPage, {})
18187					}),
18188					canManageSystem && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Route, {
18189						exact: true,
18190						path: [import_dist$1.newNavAdsSystemSectionPath.pattern, import_dist$1.newNavAdsSystemPath.pattern],
18191						children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsSystemPage, {})
18192					}),
18193					/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Route, {
18194						path: "*",
18195						children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Redirect, { to: (0, import_dist$1.newNavAdsDashboardPath)({ accountId }) })
18196					})
18197				] }) })
18198			})]
18199		})]
18200	});
18201};
18202var spaceSx = {
18203	display: "grid",
18204	gridTemplateAreas: `
18205        "drawer content"
18206    `,
18207	gridTemplateRows: "1fr",
18208	gridTemplateColumns: `235px 1fr`,
18209	minHeight: "100%",
18210	backgroundColor: (theme) => theme.palette.other.editorBackground
18211};
18212var drawerSx = {
18213	width: `235px`,
18214	gridArea: "drawer",
18215	"& .MuiDrawer-paper": {
18216		width: `235px`,
18217		boxSizing: "border-box",
18218		padding: "24px 16px 16px 16px",
18219		display: "flex",
18220		flexDirection: "column"
18221	}
18222};
18223var lockupSx = {
18224	display: "flex",
18225	flexDirection: "column",
18226	gap: "2px",
18227	width: "100%",
18228	flexShrink: 0
18229};
18230var lockupDividerSx = {
18231	margin: "16px 0 8px",
18232	flexShrink: 0
18233};
18234var navListSx = {
18235	padding: 0,
18236	width: "100%",
18237	flex: 1,
18238	minHeight: 0,
18239	overflowY: "auto",
18240	...import_dist$2.scrollOnHoverSxProps
18241};
18242var nestedItemSx = { paddingLeft: 3 };
18243var chevronSx = (isExpanded) => ({
18244	transform: isExpanded ? "rotate(0deg)" : "rotate(-90deg)",
18245	transition: (theme) => theme.transitions.create("transform", { duration: theme.transitions.duration.shortest })
18246});
18247var navDividerSx = { margin: "8px 0" };
18248var allAccountsButtonSx = {
18249	marginTop: "auto",
18250	flexShrink: 0
18251};
18252var userMenuSx = {
18253	marginTop: 2,
18254	width: "100%",
18255	display: "flex",
18256	flexDirection: "column",
18257	alignItems: "flex-start",
18258	gap: "10px"
18259};
18260var contentSx = {
18261	gridArea: "content",
18262	height: "100vh",
18263	minWidth: 0,
18264	display: "flex",
18265	flexDirection: "column",
18266	overflow: "hidden"
18267};
18268var scrollingContentSx = {
18269	flex: 1,
18270	minHeight: 0,
18271	overflow: "auto",
18272	...import_dist$2.scrollOnHoverSxProps
18273};
18274//#endregion
18275//#region client/components/newNav/components/ads/AdsContainer.tsx
18276var AdsContainer = (props) => {
18277	const { name, accountId: activeAccountId } = useActiveAccount();
18278	const { userName, userRoles } = useUserData();
18279	const pendo = PendoService.getInstance();
18280	const pendoAccountInfo = {
18281		id: activeAccountId,
18282		name
18283	};
18284	(0, import_react.useEffect)(() => {
18285		pendo.initUserInfoIfRequired(userName, userRoles?.join(", "), pendoAccountInfo);
18286	}, []);
18287	pendo.updateAccountInfoIfChanged(pendoAccountInfo);
18288	return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
18289		/* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdsSpaceShell, { logout: props.logout }),
18290		/* @__PURE__ */ (0, import_jsx_runtime.jsx)(EditHelpCenterPopover, {}),
18291		/* @__PURE__ */ (0, import_jsx_runtime.jsx)(HelpCenterPopover, {}),
18292		/* @__PURE__ */ (0, import_jsx_runtime.jsx)(NotificationSnackbar, {})
18293	] });
18294};
18295//#endregion
18296export { AdsContainer };
18297
18298//# sourceMappingURL=AdsContainer.Cc8xLuBn.js.map
18299//# debugId=da6fd120-86a6-583d-8992-c5cd2201c1e6

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.