@aglyn/plugins-marketing 1.0.0-beta.173 → 1.0.0-beta.175

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/README.md +2 -1
  2. package/package.json +15 -15
  3. package/src/lib/components/campaign-detail-card.d.ts +12 -1
  4. package/src/lib/components/campaign-detail-card.js +146 -35
  5. package/src/lib/components/campaign-detail-card.js.map +1 -1
  6. package/src/lib/components/campaign-edit-drawer.d.ts +13 -0
  7. package/src/lib/components/campaign-edit-drawer.js +56 -4
  8. package/src/lib/components/campaign-edit-drawer.js.map +1 -1
  9. package/src/lib/components/campaign-glance-card.component.js +10 -2
  10. package/src/lib/components/campaign-glance-card.component.js.map +1 -1
  11. package/src/lib/components/campaign-members-section.d.ts +6 -0
  12. package/src/lib/components/campaign-members-section.js +2 -2
  13. package/src/lib/components/campaign-members-section.js.map +1 -1
  14. package/src/lib/components/campaign-queries.d.ts +58 -0
  15. package/src/lib/components/campaign-queries.js +72 -0
  16. package/src/lib/components/campaign-queries.js.map +1 -0
  17. package/src/lib/components/campaign-reach-sections.d.ts +21 -7
  18. package/src/lib/components/campaign-reach-sections.js +20 -17
  19. package/src/lib/components/campaign-reach-sections.js.map +1 -1
  20. package/src/lib/components/campaign-report-card.d.ts +8 -1
  21. package/src/lib/components/campaign-report-card.js +33 -16
  22. package/src/lib/components/campaign-report-card.js.map +1 -1
  23. package/src/lib/components/campaigns-card.d.ts +10 -1
  24. package/src/lib/components/campaigns-card.js +102 -19
  25. package/src/lib/components/campaigns-card.js.map +1 -1
  26. package/src/lib/components/email-compose-card.d.ts +3 -2
  27. package/src/lib/components/email-compose-card.js +18 -7
  28. package/src/lib/components/email-compose-card.js.map +1 -1
  29. package/src/lib/components/email-detail.d.ts +11 -2
  30. package/src/lib/components/email-detail.js +50 -23
  31. package/src/lib/components/email-detail.js.map +1 -1
  32. package/src/lib/components/emails-list-card.d.ts +10 -1
  33. package/src/lib/components/emails-list-card.js +129 -23
  34. package/src/lib/components/emails-list-card.js.map +1 -1
  35. package/src/lib/components/host-marketing-summary-card.component.js +9 -1
  36. package/src/lib/components/host-marketing-summary-card.component.js.map +1 -1
  37. package/src/lib/components/marketing-console-page.js +81 -4
  38. package/src/lib/components/marketing-console-page.js.map +1 -1
  39. package/src/lib/components/marketing-console-sections.d.ts +16 -1
  40. package/src/lib/components/marketing-console-sections.js +25 -2
  41. package/src/lib/components/marketing-console-sections.js.map +1 -1
  42. package/src/lib/components/marketing-org-mount.d.ts +71 -0
  43. package/src/lib/components/marketing-org-mount.js +84 -0
  44. package/src/lib/components/marketing-org-mount.js.map +1 -0
  45. package/src/lib/components/use-campaign-send-api.d.ts +2 -2
  46. package/src/lib/components/use-campaign-send-api.js +22 -6
  47. package/src/lib/components/use-campaign-send-api.js.map +1 -1
  48. package/src/lib/components/use-emails-hub-path.d.ts +13 -3
  49. package/src/lib/components/use-emails-hub-path.js +25 -4
  50. package/src/lib/components/use-emails-hub-path.js.map +1 -1
  51. package/src/lib/components/use-marketing-hub-path.d.ts +19 -16
  52. package/src/lib/components/use-marketing-hub-path.js +6 -32
  53. package/src/lib/components/use-marketing-hub-path.js.map +1 -1
  54. package/src/lib/model/marketing-record-routes.js +11 -4
  55. package/src/lib/model/marketing-record-routes.js.map +1 -1
  56. package/src/lib/plugin.js +31 -1
  57. package/src/lib/plugin.js.map +1 -1
  58. package/src/lib/server/campaign-manage.d.ts +31 -2
  59. package/src/lib/server/campaign-manage.js +365 -137
  60. package/src/lib/server/campaign-manage.js.map +1 -1
  61. package/src/lib/server/campaign-org-refs.d.ts +45 -0
  62. package/src/lib/server/campaign-org-refs.js +83 -0
  63. package/src/lib/server/campaign-org-refs.js.map +1 -0
  64. package/src/lib/server/campaign-process-scheduled.d.ts +32 -2
  65. package/src/lib/server/campaign-process-scheduled.js +79 -9
  66. package/src/lib/server/campaign-process-scheduled.js.map +1 -1
  67. package/src/lib/server/campaign-recipients.js +18 -4
  68. package/src/lib/server/campaign-recipients.js.map +1 -1
  69. package/src/lib/server/campaign-send.js +139 -55
  70. package/src/lib/server/campaign-send.js.map +1 -1
  71. package/src/lib/server/email-campaign-reach.d.ts +6 -5
  72. package/src/lib/server/email-campaign-reach.js +14 -14
  73. package/src/lib/server/email-campaign-reach.js.map +1 -1
  74. package/src/lib/server/email-events.js +48 -17
  75. package/src/lib/server/email-events.js.map +1 -1
  76. package/src/lib/server/marketing-figures.js +12 -1
  77. package/src/lib/server/marketing-figures.js.map +1 -1
  78. package/src/lib/server.js +7 -3
  79. package/src/lib/server.js.map +1 -1
@@ -17,6 +17,7 @@
17
17
  import { jsx as _jsx } from "react/jsx-runtime";
18
18
  import { GridItems } from "@aglyn/shared-ui-jsx";
19
19
  import { HubSections } from "@aglyn/shared-ui-next";
20
+ import { useMemo } from "react";
20
21
  import AnnouncementBarCard from "./announcement-bar-card.component.js";
21
22
  import CampaignDetailCard from "./campaign-detail-card.js";
22
23
  import CampaignsCard from "./campaigns-card.js";
@@ -25,6 +26,10 @@ import HostExperimentsCard from "./host-experiments-card.component.js";
25
26
  import HostMarketingSummaryCard from "./host-marketing-summary-card.component.js";
26
27
  import HostOverlaysCard from "./host-overlays-card.component.js";
27
28
  import PopupCard from "./popup-card.component.js";
29
+ import EmailComposeCard from "./email-compose-card.js";
30
+ import EmailDetail from "./email-detail.js";
31
+ import EmailsListCard from "./emails-list-card.js";
32
+ import { MarketingOrgMountProvider } from "./marketing-org-mount.js";
28
33
  /**
29
34
  * The body of one marketing section, built only when that section is the one
30
35
  * being read (AGL-2501).
@@ -124,6 +129,57 @@ import PopupCard from "./popup-card.component.js";
124
129
  return null;
125
130
  }
126
131
  }
132
+ /**
133
+ * The body of one ORGANIZATION-level section: the campaigns and the emails,
134
+ * over every site.
135
+ *
136
+ * The same cards as the site hub, handed no site. Each reads the org's
137
+ * collections unfiltered and takes what stays a site fact — the sender, the
138
+ * designs, the conversions — from the site a send is sent as, or asks which
139
+ * site when it is about to create one.
140
+ */ function orgSectionBody(section, detail, basePath) {
141
+ switch(section){
142
+ case 'campaigns':
143
+ return detail[0] ? /*#__PURE__*/ _jsx(CampaignDetailCard, {
144
+ hostId: null,
145
+ campaignId: detail[0],
146
+ basePath: basePath
147
+ }) : /*#__PURE__*/ _jsx(CampaignsCard, {
148
+ hostId: null,
149
+ basePath: basePath
150
+ });
151
+ case 'emails':
152
+ /*
153
+ * `…/emails` lists them, `…/emails/{id}` reports on one and
154
+ * `…/emails/{id}/edit` writes it — the Emails console's Messages
155
+ * grammar, under the org hub because there is no site to host it.
156
+ */ return detail[0] ? detail[1] === 'edit' ? /*#__PURE__*/ _jsx(EmailComposeCard, {
157
+ hostId: null,
158
+ emailId: detail[0],
159
+ basePath: basePath
160
+ }) : /*#__PURE__*/ _jsx(EmailDetail, {
161
+ hostId: null,
162
+ emailId: detail[0],
163
+ basePath: basePath
164
+ }) : /*#__PURE__*/ _jsx(EmailsListCard, {
165
+ hostId: null,
166
+ basePath: basePath
167
+ });
168
+ default:
169
+ return null;
170
+ }
171
+ }
172
+ /** The shell's org mount, as the Marketing cards read it through context. */ function marketingOrgMount(orgMount, basePath) {
173
+ if (!orgMount || !basePath) return null;
174
+ return {
175
+ orgId: orgMount.orgId,
176
+ orgSlug: orgMount.orgSlug,
177
+ hosts: orgMount.hosts,
178
+ hostsReady: orgMount.hostsReady,
179
+ hostsPath: orgMount.hostsPath,
180
+ basePath
181
+ };
182
+ }
127
183
  /**
128
184
  * Marketing page (AGL-251 → AGL-395): the at-a-glance rollup, the email
129
185
  * campaigns, the overlay managers (multi-overlay + announcement bar + popup),
@@ -146,18 +202,39 @@ import PopupCard from "./popup-card.component.js";
146
202
  * linkable, the back button walks sections, the breadcrumb says where you are,
147
203
  * and "mount only what is open" is structural rather than a `lazy` flag.
148
204
  */ export function MarketingConsolePage(props) {
149
- const { hostId, org, section, sections, basePath, segments } = props;
205
+ const { hostId, orgMount, org, section, sections, basePath, segments } = props;
206
+ /*
207
+ * Mounted with no site: the organization's hub. Memoised because every card
208
+ * below reads it through context, and a fresh object each render would
209
+ * re-render all of them for nothing.
210
+ */ const mount = useMemo(()=>hostId == null ? marketingOrgMount(orgMount, basePath) : null, [
211
+ hostId,
212
+ orgMount,
213
+ basePath
214
+ ]);
150
215
  /*
151
216
  * Nothing until the URL names a section. The shell redirects a bare hub URL
152
217
  * to the landing section and holds a spinner while it does, so this state is
153
218
  * transient — and rendering a default section here instead would pay for its
154
219
  * listens on a URL that is already being replaced.
155
220
  */ if (!section || !(sections == null ? void 0 : sections.length) || !basePath) return null;
221
+ // `segments[0]` IS the section — the shell resolved it into `section`
222
+ // already — so what a section owns is everything after it.
223
+ const detail = (segments != null ? segments : []).slice(1);
224
+ if (hostId == null) {
225
+ // No site and no org to stand in for it: nothing this page can scope.
226
+ if (!mount) return null;
227
+ return /*#__PURE__*/ _jsx(MarketingOrgMountProvider, {
228
+ value: mount,
229
+ children: /*#__PURE__*/ _jsx(HubSections, {
230
+ sections: sections,
231
+ children: orgSectionBody(section, detail, basePath)
232
+ })
233
+ });
234
+ }
156
235
  return /*#__PURE__*/ _jsx(HubSections, {
157
236
  sections: sections,
158
- children: sectionBody(section, hostId, org, (// `segments[0]` IS the section — the shell resolved it into `section`
159
- // already — so what a section owns is everything after it.
160
- segments != null ? segments : []).slice(1), basePath)
237
+ children: sectionBody(section, hostId, org, detail, basePath)
161
238
  });
162
239
  }
163
240
  MarketingConsolePage.displayName = 'MarketingConsolePage';
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/marketing/src/lib/components/marketing-console-page.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport type { AglynOrgBilling, ConsolePluginPageProps } from '@aglyn/aglyn'\nimport { GridItems } from '@aglyn/shared-ui-jsx'\nimport { HubSections } from '@aglyn/shared-ui-next'\nimport type { ReactNode } from 'react'\nimport AnnouncementBarCard from './announcement-bar-card.component'\nimport CampaignDetailCard from './campaign-detail-card'\nimport CampaignsCard from './campaigns-card'\nimport CampaignConversionsCard from './campaign-conversions-card'\nimport HostExperimentsCard from './host-experiments-card.component'\nimport HostMarketingSummaryCard from './host-marketing-summary-card.component'\nimport HostOverlaysCard from './host-overlays-card.component'\nimport PopupCard from './popup-card.component'\nimport type { MarketingConsoleSectionId } from './marketing-console-sections'\n\n/**\n * The body of one marketing section, built only when that section is the one\n * being read (AGL-2501).\n *\n * A function rather than a map of nodes on purpose: a `Record<id, ReactNode>`\n * would CONSTRUCT every section every render, and each card opens its\n * Firestore listens on mount — which is the entire cost this page exists to\n * stop paying. Only the returned branch is ever built.\n */\nfunction sectionBody(\n section: MarketingConsoleSectionId,\n hostId: string,\n org: Partial<AglynOrgBilling> | undefined,\n /**\n * The section's OWN segments — `segments[1]` onward, already sliced by the\n * caller. A section that owns deeper routes reads them here; one that does\n * not simply ignores them.\n */\n detail: readonly string[],\n basePath: string,\n): ReactNode {\n switch (section) {\n case 'overview':\n return <HostMarketingSummaryCard hostId={hostId} />\n case 'campaigns':\n /*\n * `/marketing/campaigns/{id}` is one campaign, and it is a ROUTE rather\n * than an expanded row for two reasons: it is linkable, which is what a\n * merchant wants to paste into a message about last week's send; and\n * the composer plus the campaign list are the surface's expensive\n * listens, so a reader who came for one campaign's numbers must not pay\n * for them.\n *\n * The id may name a CAMPAIGN or, for every URL minted before campaigns\n * grouped their emails, a single SEND. `CampaignDetailCard` answers that\n * by reading, and falls through to the send's own report — so no pasted\n * link stops resolving.\n *\n * No registry entry is needed — the shell hands a section every segment\n * beneath it, so a section owns its own subtree. The gate is the\n * section's, which is the same gate the composer is behind.\n */\n return detail[0] ? (\n <CampaignDetailCard\n hostId={hostId}\n campaignId={detail[0]}\n basePath={basePath}\n />\n ) : (\n <CampaignsCard hostId={hostId} basePath={basePath} />\n )\n case 'conversions':\n /*\n * `/marketing/conversions/{campaignId}` narrows the list to one\n * campaign's credited conversions — the drill-down from that campaign's\n * report. A segment rather than a query string, so the section owns its\n * own subtree exactly as `campaigns` does and the URL is linkable.\n */\n return (\n <CampaignConversionsCard\n hostId={hostId}\n basePath={basePath}\n campaignId={detail[0]}\n />\n )\n case 'overlays':\n return (\n <GridItems\n masonry\n spacing={3}\n items={[\n {\n size: { xs: 12 },\n children: <HostOverlaysCard hostId={hostId} org={org} />,\n },\n {\n size: { xs: 12, md: 6 },\n children: <AnnouncementBarCard hostId={hostId} org={org} />,\n },\n {\n size: { xs: 12, md: 6 },\n children: <PopupCard hostId={hostId} org={org} />,\n },\n ]}\n />\n )\n case 'experiments':\n return <HostExperimentsCard hostId={hostId} org={org} />\n default:\n return null\n }\n}\n\n/**\n * Marketing page (AGL-251 → AGL-395): the at-a-glance rollup, the email\n * campaigns, the overlay managers (multi-overlay + announcement bar + popup),\n * and A/B testing — owned by the marketing plugin and rendered by the shell's\n * generic plugin route. Each gated card runs its own entitlement check\n * (overlays vs A/B are distinct plan flags) off the shell's resolved `org`;\n * the popup image picker uses the shell's media browser via `useMediaPicker`.\n *\n * A CAMPAIGN belongs here and not on the Emails console because it is a\n * container with a window of dates, a set of lists, a topic and revenue\n * attribution — a marketing object that reaches people by email. Its server\n * half has always lived in this plugin. The individual MESSAGES, the\n * templates they are built from and the identities they leave on stay on\n * Emails, and each email's page links here for its campaign.\n *\n * Sections are ROUTES (AGL-2501). `HubTabs lazy` already mounted one panel, so\n * this is not a read saving — `marketing-console-read-cost.spec.tsx` was\n * written BEFORE the conversion precisely to hold that line, and reports the\n * same counts after. What routing adds is that the URL names the section: it is\n * linkable, the back button walks sections, the breadcrumb says where you are,\n * and \"mount only what is open\" is structural rather than a `lazy` flag.\n */\nexport function MarketingConsolePage(props: ConsolePluginPageProps) {\n const { hostId, org, section, sections, basePath, segments } = props\n\n /*\n * Nothing until the URL names a section. The shell redirects a bare hub URL\n * to the landing section and holds a spinner while it does, so this state is\n * transient — and rendering a default section here instead would pay for its\n * listens on a URL that is already being replaced.\n */\n if (!section || !sections?.length || !basePath) return null\n\n return (\n <HubSections sections={sections}>\n {sectionBody(\n section as MarketingConsoleSectionId,\n hostId,\n org,\n // `segments[0]` IS the section — the shell resolved it into `section`\n // already — so what a section owns is everything after it.\n (segments ?? []).slice(1),\n basePath,\n )}\n </HubSections>\n )\n}\nMarketingConsolePage.displayName = 'MarketingConsolePage'\n\nexport default MarketingConsolePage\n"],"names":["GridItems","HubSections","AnnouncementBarCard","CampaignDetailCard","CampaignsCard","CampaignConversionsCard","HostExperimentsCard","HostMarketingSummaryCard","HostOverlaysCard","PopupCard","sectionBody","section","hostId","org","detail","basePath","campaignId","masonry","spacing","items","size","xs","children","md","MarketingConsolePage","props","sections","segments","length","slice","displayName"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;AAGA,SAASA,SAAS,QAAQ,uBAAsB;AAChD,SAASC,WAAW,QAAQ,wBAAuB;AAEnD,OAAOC,yBAAyB,uCAAmC;AACnE,OAAOC,wBAAwB,4BAAwB;AACvD,OAAOC,mBAAmB,sBAAkB;AAC5C,OAAOC,6BAA6B,iCAA6B;AACjE,OAAOC,yBAAyB,uCAAmC;AACnE,OAAOC,8BAA8B,6CAAyC;AAC9E,OAAOC,sBAAsB,oCAAgC;AAC7D,OAAOC,eAAe,4BAAwB;AAG9C;;;;;;;;CAQC,GACD,SAASC,YACPC,OAAkC,EAClCC,MAAc,EACdC,GAAyC,EACzC;;;;GAIC,GACDC,MAAyB,EACzBC,QAAgB;IAEhB,OAAQJ;QACN,KAAK;YACH,qBAAO,KAACJ;gBAAyBK,QAAQA;;QAC3C,KAAK;YACH;;;;;;;;;;;;;;;;OAgBC,GACD,OAAOE,MAAM,CAAC,EAAE,iBACd,KAACX;gBACCS,QAAQA;gBACRI,YAAYF,MAAM,CAAC,EAAE;gBACrBC,UAAUA;+BAGZ,KAACX;gBAAcQ,QAAQA;gBAAQG,UAAUA;;QAE7C,KAAK;YACH;;;;;OAKC,GACD,qBACE,KAACV;gBACCO,QAAQA;gBACRG,UAAUA;gBACVC,YAAYF,MAAM,CAAC,EAAE;;QAG3B,KAAK;YACH,qBACE,KAACd;gBACCiB,OAAO;gBACPC,SAAS;gBACTC,OAAO;oBACL;wBACEC,MAAM;4BAAEC,IAAI;wBAAG;wBACfC,wBAAU,KAACd;4BAAiBI,QAAQA;4BAAQC,KAAKA;;oBACnD;oBACA;wBACEO,MAAM;4BAAEC,IAAI;4BAAIE,IAAI;wBAAE;wBACtBD,wBAAU,KAACpB;4BAAoBU,QAAQA;4BAAQC,KAAKA;;oBACtD;oBACA;wBACEO,MAAM;4BAAEC,IAAI;4BAAIE,IAAI;wBAAE;wBACtBD,wBAAU,KAACb;4BAAUG,QAAQA;4BAAQC,KAAKA;;oBAC5C;iBACD;;QAGP,KAAK;YACH,qBAAO,KAACP;gBAAoBM,QAAQA;gBAAQC,KAAKA;;QACnD;YACE,OAAO;IACX;AACF;AAEA;;;;;;;;;;;;;;;;;;;;;CAqBC,GACD,OAAO,SAASW,qBAAqBC,KAA6B;IAChE,MAAM,EAAEb,MAAM,EAAEC,GAAG,EAAEF,OAAO,EAAEe,QAAQ,EAAEX,QAAQ,EAAEY,QAAQ,EAAE,GAAGF;IAE/D;;;;;GAKC,GACD,IAAI,CAACd,WAAW,EAACe,4BAAAA,SAAUE,MAAM,KAAI,CAACb,UAAU,OAAO;IAEvD,qBACE,KAACd;QAAYyB,UAAUA;kBACpBhB,YACCC,SACAC,QACAC,KAGA,CAFA,sEAAsE;QACtE,2DAA2D;QAC1Dc,mBAAAA,WAAY,EAAE,EAAEE,KAAK,CAAC,IACvBd;;AAIR;AACAS,qBAAqBM,WAAW,GAAG;AAEnC,eAAeN,qBAAoB"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/marketing/src/lib/components/marketing-console-page.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport type {\n AglynOrgBilling,\n ConsolePluginOrgMount,\n ConsolePluginPageProps,\n} from '@aglyn/aglyn'\nimport { GridItems } from '@aglyn/shared-ui-jsx'\nimport { HubSections } from '@aglyn/shared-ui-next'\nimport { useMemo, type ReactNode } from 'react'\nimport AnnouncementBarCard from './announcement-bar-card.component'\nimport CampaignDetailCard from './campaign-detail-card'\nimport CampaignsCard from './campaigns-card'\nimport CampaignConversionsCard from './campaign-conversions-card'\nimport HostExperimentsCard from './host-experiments-card.component'\nimport HostMarketingSummaryCard from './host-marketing-summary-card.component'\nimport HostOverlaysCard from './host-overlays-card.component'\nimport PopupCard from './popup-card.component'\nimport EmailComposeCard from './email-compose-card'\nimport EmailDetail from './email-detail'\nimport EmailsListCard from './emails-list-card'\nimport type { MarketingConsoleSectionId } from './marketing-console-sections'\nimport {\n MarketingOrgMountProvider,\n type MarketingOrgMount,\n} from './marketing-org-mount'\n\n/**\n * The body of one marketing section, built only when that section is the one\n * being read (AGL-2501).\n *\n * A function rather than a map of nodes on purpose: a `Record<id, ReactNode>`\n * would CONSTRUCT every section every render, and each card opens its\n * Firestore listens on mount — which is the entire cost this page exists to\n * stop paying. Only the returned branch is ever built.\n */\nfunction sectionBody(\n section: MarketingConsoleSectionId,\n hostId: string,\n org: Partial<AglynOrgBilling> | undefined,\n /**\n * The section's OWN segments — `segments[1]` onward, already sliced by the\n * caller. A section that owns deeper routes reads them here; one that does\n * not simply ignores them.\n */\n detail: readonly string[],\n basePath: string,\n): ReactNode {\n switch (section) {\n case 'overview':\n return <HostMarketingSummaryCard hostId={hostId} />\n case 'campaigns':\n /*\n * `/marketing/campaigns/{id}` is one campaign, and it is a ROUTE rather\n * than an expanded row for two reasons: it is linkable, which is what a\n * merchant wants to paste into a message about last week's send; and\n * the composer plus the campaign list are the surface's expensive\n * listens, so a reader who came for one campaign's numbers must not pay\n * for them.\n *\n * The id may name a CAMPAIGN or, for every URL minted before campaigns\n * grouped their emails, a single SEND. `CampaignDetailCard` answers that\n * by reading, and falls through to the send's own report — so no pasted\n * link stops resolving.\n *\n * No registry entry is needed — the shell hands a section every segment\n * beneath it, so a section owns its own subtree. The gate is the\n * section's, which is the same gate the composer is behind.\n */\n return detail[0] ? (\n <CampaignDetailCard\n hostId={hostId}\n campaignId={detail[0]}\n basePath={basePath}\n />\n ) : (\n <CampaignsCard hostId={hostId} basePath={basePath} />\n )\n case 'conversions':\n /*\n * `/marketing/conversions/{campaignId}` narrows the list to one\n * campaign's credited conversions — the drill-down from that campaign's\n * report. A segment rather than a query string, so the section owns its\n * own subtree exactly as `campaigns` does and the URL is linkable.\n */\n return (\n <CampaignConversionsCard\n hostId={hostId}\n basePath={basePath}\n campaignId={detail[0]}\n />\n )\n case 'overlays':\n return (\n <GridItems\n masonry\n spacing={3}\n items={[\n {\n size: { xs: 12 },\n children: <HostOverlaysCard hostId={hostId} org={org} />,\n },\n {\n size: { xs: 12, md: 6 },\n children: <AnnouncementBarCard hostId={hostId} org={org} />,\n },\n {\n size: { xs: 12, md: 6 },\n children: <PopupCard hostId={hostId} org={org} />,\n },\n ]}\n />\n )\n case 'experiments':\n return <HostExperimentsCard hostId={hostId} org={org} />\n default:\n return null\n }\n}\n\n/**\n * The body of one ORGANIZATION-level section: the campaigns and the emails,\n * over every site.\n *\n * The same cards as the site hub, handed no site. Each reads the org's\n * collections unfiltered and takes what stays a site fact — the sender, the\n * designs, the conversions — from the site a send is sent as, or asks which\n * site when it is about to create one.\n */\nfunction orgSectionBody(\n section: MarketingConsoleSectionId,\n detail: readonly string[],\n basePath: string,\n): ReactNode {\n switch (section) {\n case 'campaigns':\n return detail[0] ? (\n <CampaignDetailCard\n hostId={null}\n campaignId={detail[0]}\n basePath={basePath}\n />\n ) : (\n <CampaignsCard hostId={null} basePath={basePath} />\n )\n case 'emails':\n /*\n * `…/emails` lists them, `…/emails/{id}` reports on one and\n * `…/emails/{id}/edit` writes it — the Emails console's Messages\n * grammar, under the org hub because there is no site to host it.\n */\n return detail[0] ? (\n detail[1] === 'edit' ? (\n <EmailComposeCard\n hostId={null}\n emailId={detail[0]}\n basePath={basePath}\n />\n ) : (\n <EmailDetail hostId={null} emailId={detail[0]} basePath={basePath} />\n )\n ) : (\n <EmailsListCard hostId={null} basePath={basePath} />\n )\n default:\n return null\n }\n}\n\n/** The shell's org mount, as the Marketing cards read it through context. */\nfunction marketingOrgMount(\n orgMount: ConsolePluginOrgMount | undefined,\n basePath: string | undefined,\n): MarketingOrgMount | null {\n if (!orgMount || !basePath) return null\n return {\n orgId: orgMount.orgId,\n orgSlug: orgMount.orgSlug,\n hosts: orgMount.hosts,\n hostsReady: orgMount.hostsReady,\n hostsPath: orgMount.hostsPath,\n basePath,\n }\n}\n\n/**\n * Marketing page (AGL-251 → AGL-395): the at-a-glance rollup, the email\n * campaigns, the overlay managers (multi-overlay + announcement bar + popup),\n * and A/B testing — owned by the marketing plugin and rendered by the shell's\n * generic plugin route. Each gated card runs its own entitlement check\n * (overlays vs A/B are distinct plan flags) off the shell's resolved `org`;\n * the popup image picker uses the shell's media browser via `useMediaPicker`.\n *\n * A CAMPAIGN belongs here and not on the Emails console because it is a\n * container with a window of dates, a set of lists, a topic and revenue\n * attribution — a marketing object that reaches people by email. Its server\n * half has always lived in this plugin. The individual MESSAGES, the\n * templates they are built from and the identities they leave on stay on\n * Emails, and each email's page links here for its campaign.\n *\n * Sections are ROUTES (AGL-2501). `HubTabs lazy` already mounted one panel, so\n * this is not a read saving — `marketing-console-read-cost.spec.tsx` was\n * written BEFORE the conversion precisely to hold that line, and reports the\n * same counts after. What routing adds is that the URL names the section: it is\n * linkable, the back button walks sections, the breadcrumb says where you are,\n * and \"mount only what is open\" is structural rather than a `lazy` flag.\n */\nexport function MarketingConsolePage(props: ConsolePluginPageProps) {\n const { hostId, orgMount, org, section, sections, basePath, segments } = props\n /*\n * Mounted with no site: the organization's hub. Memoised because every card\n * below reads it through context, and a fresh object each render would\n * re-render all of them for nothing.\n */\n const mount = useMemo(\n () => (hostId == null ? marketingOrgMount(orgMount, basePath) : null),\n [hostId, orgMount, basePath],\n )\n\n /*\n * Nothing until the URL names a section. The shell redirects a bare hub URL\n * to the landing section and holds a spinner while it does, so this state is\n * transient — and rendering a default section here instead would pay for its\n * listens on a URL that is already being replaced.\n */\n if (!section || !sections?.length || !basePath) return null\n // `segments[0]` IS the section — the shell resolved it into `section`\n // already — so what a section owns is everything after it.\n const detail = (segments ?? []).slice(1)\n\n if (hostId == null) {\n // No site and no org to stand in for it: nothing this page can scope.\n if (!mount) return null\n return (\n <MarketingOrgMountProvider value={mount}>\n <HubSections sections={sections}>\n {orgSectionBody(\n section as MarketingConsoleSectionId,\n detail,\n basePath,\n )}\n </HubSections>\n </MarketingOrgMountProvider>\n )\n }\n\n return (\n <HubSections sections={sections}>\n {sectionBody(\n section as MarketingConsoleSectionId,\n hostId,\n org,\n detail,\n basePath,\n )}\n </HubSections>\n )\n}\nMarketingConsolePage.displayName = 'MarketingConsolePage'\n\nexport default MarketingConsolePage\n"],"names":["GridItems","HubSections","useMemo","AnnouncementBarCard","CampaignDetailCard","CampaignsCard","CampaignConversionsCard","HostExperimentsCard","HostMarketingSummaryCard","HostOverlaysCard","PopupCard","EmailComposeCard","EmailDetail","EmailsListCard","MarketingOrgMountProvider","sectionBody","section","hostId","org","detail","basePath","campaignId","masonry","spacing","items","size","xs","children","md","orgSectionBody","emailId","marketingOrgMount","orgMount","orgId","orgSlug","hosts","hostsReady","hostsPath","MarketingConsolePage","props","sections","segments","mount","length","slice","value","displayName"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;AAOA,SAASA,SAAS,QAAQ,uBAAsB;AAChD,SAASC,WAAW,QAAQ,wBAAuB;AACnD,SAASC,OAAO,QAAwB,QAAO;AAC/C,OAAOC,yBAAyB,uCAAmC;AACnE,OAAOC,wBAAwB,4BAAwB;AACvD,OAAOC,mBAAmB,sBAAkB;AAC5C,OAAOC,6BAA6B,iCAA6B;AACjE,OAAOC,yBAAyB,uCAAmC;AACnE,OAAOC,8BAA8B,6CAAyC;AAC9E,OAAOC,sBAAsB,oCAAgC;AAC7D,OAAOC,eAAe,4BAAwB;AAC9C,OAAOC,sBAAsB,0BAAsB;AACnD,OAAOC,iBAAiB,oBAAgB;AACxC,OAAOC,oBAAoB,wBAAoB;AAE/C,SACEC,yBAAyB,QAEpB,2BAAuB;AAE9B;;;;;;;;CAQC,GACD,SAASC,YACPC,OAAkC,EAClCC,MAAc,EACdC,GAAyC,EACzC;;;;GAIC,GACDC,MAAyB,EACzBC,QAAgB;IAEhB,OAAQJ;QACN,KAAK;YACH,qBAAO,KAACR;gBAAyBS,QAAQA;;QAC3C,KAAK;YACH;;;;;;;;;;;;;;;;OAgBC,GACD,OAAOE,MAAM,CAAC,EAAE,iBACd,KAACf;gBACCa,QAAQA;gBACRI,YAAYF,MAAM,CAAC,EAAE;gBACrBC,UAAUA;+BAGZ,KAACf;gBAAcY,QAAQA;gBAAQG,UAAUA;;QAE7C,KAAK;YACH;;;;;OAKC,GACD,qBACE,KAACd;gBACCW,QAAQA;gBACRG,UAAUA;gBACVC,YAAYF,MAAM,CAAC,EAAE;;QAG3B,KAAK;YACH,qBACE,KAACnB;gBACCsB,OAAO;gBACPC,SAAS;gBACTC,OAAO;oBACL;wBACEC,MAAM;4BAAEC,IAAI;wBAAG;wBACfC,wBAAU,KAAClB;4BAAiBQ,QAAQA;4BAAQC,KAAKA;;oBACnD;oBACA;wBACEO,MAAM;4BAAEC,IAAI;4BAAIE,IAAI;wBAAE;wBACtBD,wBAAU,KAACxB;4BAAoBc,QAAQA;4BAAQC,KAAKA;;oBACtD;oBACA;wBACEO,MAAM;4BAAEC,IAAI;4BAAIE,IAAI;wBAAE;wBACtBD,wBAAU,KAACjB;4BAAUO,QAAQA;4BAAQC,KAAKA;;oBAC5C;iBACD;;QAGP,KAAK;YACH,qBAAO,KAACX;gBAAoBU,QAAQA;gBAAQC,KAAKA;;QACnD;YACE,OAAO;IACX;AACF;AAEA;;;;;;;;CAQC,GACD,SAASW,eACPb,OAAkC,EAClCG,MAAyB,EACzBC,QAAgB;IAEhB,OAAQJ;QACN,KAAK;YACH,OAAOG,MAAM,CAAC,EAAE,iBACd,KAACf;gBACCa,QAAQ;gBACRI,YAAYF,MAAM,CAAC,EAAE;gBACrBC,UAAUA;+BAGZ,KAACf;gBAAcY,QAAQ;gBAAMG,UAAUA;;QAE3C,KAAK;YACH;;;;OAIC,GACD,OAAOD,MAAM,CAAC,EAAE,GACdA,MAAM,CAAC,EAAE,KAAK,uBACZ,KAACR;gBACCM,QAAQ;gBACRa,SAASX,MAAM,CAAC,EAAE;gBAClBC,UAAUA;+BAGZ,KAACR;gBAAYK,QAAQ;gBAAMa,SAASX,MAAM,CAAC,EAAE;gBAAEC,UAAUA;+BAG3D,KAACP;gBAAeI,QAAQ;gBAAMG,UAAUA;;QAE5C;YACE,OAAO;IACX;AACF;AAEA,2EAA2E,GAC3E,SAASW,kBACPC,QAA2C,EAC3CZ,QAA4B;IAE5B,IAAI,CAACY,YAAY,CAACZ,UAAU,OAAO;IACnC,OAAO;QACLa,OAAOD,SAASC,KAAK;QACrBC,SAASF,SAASE,OAAO;QACzBC,OAAOH,SAASG,KAAK;QACrBC,YAAYJ,SAASI,UAAU;QAC/BC,WAAWL,SAASK,SAAS;QAC7BjB;IACF;AACF;AAEA;;;;;;;;;;;;;;;;;;;;;CAqBC,GACD,OAAO,SAASkB,qBAAqBC,KAA6B;IAChE,MAAM,EAAEtB,MAAM,EAAEe,QAAQ,EAAEd,GAAG,EAAEF,OAAO,EAAEwB,QAAQ,EAAEpB,QAAQ,EAAEqB,QAAQ,EAAE,GAAGF;IACzE;;;;GAIC,GACD,MAAMG,QAAQxC,QACZ,IAAOe,UAAU,OAAOc,kBAAkBC,UAAUZ,YAAY,MAChE;QAACH;QAAQe;QAAUZ;KAAS;IAG9B;;;;;GAKC,GACD,IAAI,CAACJ,WAAW,EAACwB,4BAAAA,SAAUG,MAAM,KAAI,CAACvB,UAAU,OAAO;IACvD,sEAAsE;IACtE,2DAA2D;IAC3D,MAAMD,SAAS,CAACsB,mBAAAA,WAAY,EAAE,EAAEG,KAAK,CAAC;IAEtC,IAAI3B,UAAU,MAAM;QAClB,sEAAsE;QACtE,IAAI,CAACyB,OAAO,OAAO;QACnB,qBACE,KAAC5B;YAA0B+B,OAAOH;sBAChC,cAAA,KAACzC;gBAAYuC,UAAUA;0BACpBX,eACCb,SACAG,QACAC;;;IAKV;IAEA,qBACE,KAACnB;QAAYuC,UAAUA;kBACpBzB,YACCC,SACAC,QACAC,KACAC,QACAC;;AAIR;AACAkB,qBAAqBQ,WAAW,GAAG;AAEnC,eAAeR,qBAAoB"}
@@ -15,7 +15,7 @@
15
15
  * limitations under the License.
16
16
  */
17
17
  import type { ConsoleNavSection } from '@aglyn/aglyn';
18
- export type MarketingConsoleSectionId = 'overview' | 'campaigns' | 'conversions' | 'overlays' | 'experiments';
18
+ export type MarketingConsoleSectionId = 'overview' | 'campaigns' | 'conversions' | 'overlays' | 'experiments' | 'emails';
19
19
  /**
20
20
  * The marketing console's sections, in rail order (AGL-2501).
21
21
  *
@@ -32,3 +32,18 @@ export type MarketingConsoleSectionId = 'overview' | 'campaigns' | 'conversions'
32
32
  * overlays and A/B are distinct plan flags, and a plan is not a release flag.
33
33
  */
34
34
  export declare const MARKETING_CONSOLE_SECTIONS: readonly ConsoleNavSection[];
35
+ /**
36
+ * The ORGANIZATION-level Marketing hub's sections, at `/[orgSlug]/marketing`.
37
+ *
38
+ * Campaigns and their emails belong to the organization, so the org hub lists
39
+ * every one of them over every site. The rest of the site rail stays on the
40
+ * site: overlays and A/B tests are drawn on one site's pages, and the
41
+ * conversions list is one site's visitors — each campaign's page links to
42
+ * the conversions of the site it asks about.
43
+ *
44
+ * `emails` is here and not on the site rail because a site's messages
45
+ * already have a page — the Emails console's Messages section, which this
46
+ * plugin fills through a zone. With no site there is no Emails console, so
47
+ * the org hub carries the same list itself.
48
+ */
49
+ export declare const MARKETING_ORG_CONSOLE_SECTIONS: readonly ConsoleNavSection[];
@@ -67,11 +67,34 @@
67
67
  id: 'experiments',
68
68
  label: 'A/B testing'
69
69
  }
70
- ] /*
70
+ ];
71
+ /*
71
72
  * Rail ORDER decides where `/marketing` lands: the shell redirects a bare hub
72
73
  * URL to the first section in this list the reader may open (AGL-2501). There
73
74
  * is deliberately no separate default constant — a second place to say which
74
75
  * section is first is a second place for it to disagree with the rail.
75
- */ ;
76
+ */ /**
77
+ * The ORGANIZATION-level Marketing hub's sections, at `/[orgSlug]/marketing`.
78
+ *
79
+ * Campaigns and their emails belong to the organization, so the org hub lists
80
+ * every one of them over every site. The rest of the site rail stays on the
81
+ * site: overlays and A/B tests are drawn on one site's pages, and the
82
+ * conversions list is one site's visitors — each campaign's page links to
83
+ * the conversions of the site it asks about.
84
+ *
85
+ * `emails` is here and not on the site rail because a site's messages
86
+ * already have a page — the Emails console's Messages section, which this
87
+ * plugin fills through a zone. With no site there is no Emails console, so
88
+ * the org hub carries the same list itself.
89
+ */ export const MARKETING_ORG_CONSOLE_SECTIONS = [
90
+ {
91
+ id: 'campaigns',
92
+ label: 'Campaigns'
93
+ },
94
+ {
95
+ id: 'emails',
96
+ label: 'Emails'
97
+ }
98
+ ];
76
99
 
77
100
  //# sourceMappingURL=marketing-console-sections.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/marketing/src/lib/components/marketing-console-sections.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { ConsoleNavSection } from '@aglyn/aglyn'\n\nexport type MarketingConsoleSectionId =\n | 'overview'\n | 'campaigns'\n | 'conversions'\n | 'overlays'\n | 'experiments'\n\n/**\n * The marketing console's sections, in rail order (AGL-2501).\n *\n * One list, read twice and never copied: `plugin.ts` registers it on the nav\n * item so the shell can route and gate each section, and the page switches its\n * body on the id the shell resolves back.\n *\n * Ids are the `?tab=` ids this page already deep-linked by, kept deliberately\n * so a bookmark that named a tab names the same section as a route. `experiments`\n * therefore keeps its id while its LABEL reads \"A/B testing\".\n *\n * No `navTabId` on any of them: they inherit the Marketing nav item's gate.\n * The per-card ENTITLEMENT checks are unchanged and still live in the cards —\n * overlays and A/B are distinct plan flags, and a plan is not a release flag.\n */\nexport const MARKETING_CONSOLE_SECTIONS: readonly ConsoleNavSection[] = [\n { id: 'overview', label: 'Overview' },\n /*\n * A campaign is a CONTAINER with a window of dates, a set of lists, a topic\n * and revenue attribution — a marketing object that reaches people by\n * email, rather than an email object. It sits beside A/B testing because\n * that is what campaign results are attributed against, and beside the\n * overlays because both are ways of putting the same offer in front of the\n * same audience.\n *\n * The individual MESSAGES stay on the Emails console, along with the\n * templates they are built from and the sending identities they leave on.\n * Each email's page links here for the campaign it belongs to.\n */\n { id: 'campaigns', label: 'Campaigns' },\n /*\n * What the campaigns CAUSED, from the conversions' end. A campaign's own\n * report answers this for that campaign; a section is what answers it for\n * the site, and it is the only place two things can be said that a campaign\n * report structurally cannot: the conversions credited to a tagged web link\n * rather than to a campaign document, and the ones credited to nothing at\n * all. A surface listing only the credited ones renders \"we credited nine\n * of these\" as \"nine of these happened\".\n */\n { id: 'conversions', label: 'Conversions' },\n { id: 'overlays', label: 'Overlays' },\n { id: 'experiments', label: 'A/B testing' },\n]\n\n/*\n * Rail ORDER decides where `/marketing` lands: the shell redirects a bare hub\n * URL to the first section in this list the reader may open (AGL-2501). There\n * is deliberately no separate default constant — a second place to say which\n * section is first is a second place for it to disagree with the rail.\n */\n"],"names":["MARKETING_CONSOLE_SECTIONS","id","label"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAWD;;;;;;;;;;;;;;CAcC,GACD,OAAO,MAAMA,6BAA2D;IACtE;QAAEC,IAAI;QAAYC,OAAO;IAAW;IACpC;;;;;;;;;;;GAWC,GACD;QAAED,IAAI;QAAaC,OAAO;IAAY;IACtC;;;;;;;;GAQC,GACD;QAAED,IAAI;QAAeC,OAAO;IAAc;IAC1C;QAAED,IAAI;QAAYC,OAAO;IAAW;IACpC;QAAED,IAAI;QAAeC,OAAO;IAAc;CAC3C,CAED;;;;;CAKC,IAPA"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/marketing/src/lib/components/marketing-console-sections.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { ConsoleNavSection } from '@aglyn/aglyn'\n\nexport type MarketingConsoleSectionId =\n | 'overview'\n | 'campaigns'\n | 'conversions'\n | 'overlays'\n | 'experiments'\n | 'emails'\n\n/**\n * The marketing console's sections, in rail order (AGL-2501).\n *\n * One list, read twice and never copied: `plugin.ts` registers it on the nav\n * item so the shell can route and gate each section, and the page switches its\n * body on the id the shell resolves back.\n *\n * Ids are the `?tab=` ids this page already deep-linked by, kept deliberately\n * so a bookmark that named a tab names the same section as a route. `experiments`\n * therefore keeps its id while its LABEL reads \"A/B testing\".\n *\n * No `navTabId` on any of them: they inherit the Marketing nav item's gate.\n * The per-card ENTITLEMENT checks are unchanged and still live in the cards —\n * overlays and A/B are distinct plan flags, and a plan is not a release flag.\n */\nexport const MARKETING_CONSOLE_SECTIONS: readonly ConsoleNavSection[] = [\n { id: 'overview', label: 'Overview' },\n /*\n * A campaign is a CONTAINER with a window of dates, a set of lists, a topic\n * and revenue attribution — a marketing object that reaches people by\n * email, rather than an email object. It sits beside A/B testing because\n * that is what campaign results are attributed against, and beside the\n * overlays because both are ways of putting the same offer in front of the\n * same audience.\n *\n * The individual MESSAGES stay on the Emails console, along with the\n * templates they are built from and the sending identities they leave on.\n * Each email's page links here for the campaign it belongs to.\n */\n { id: 'campaigns', label: 'Campaigns' },\n /*\n * What the campaigns CAUSED, from the conversions' end. A campaign's own\n * report answers this for that campaign; a section is what answers it for\n * the site, and it is the only place two things can be said that a campaign\n * report structurally cannot: the conversions credited to a tagged web link\n * rather than to a campaign document, and the ones credited to nothing at\n * all. A surface listing only the credited ones renders \"we credited nine\n * of these\" as \"nine of these happened\".\n */\n { id: 'conversions', label: 'Conversions' },\n { id: 'overlays', label: 'Overlays' },\n { id: 'experiments', label: 'A/B testing' },\n]\n\n/*\n * Rail ORDER decides where `/marketing` lands: the shell redirects a bare hub\n * URL to the first section in this list the reader may open (AGL-2501). There\n * is deliberately no separate default constant — a second place to say which\n * section is first is a second place for it to disagree with the rail.\n */\n\n/**\n * The ORGANIZATION-level Marketing hub's sections, at `/[orgSlug]/marketing`.\n *\n * Campaigns and their emails belong to the organization, so the org hub lists\n * every one of them over every site. The rest of the site rail stays on the\n * site: overlays and A/B tests are drawn on one site's pages, and the\n * conversions list is one site's visitors — each campaign's page links to\n * the conversions of the site it asks about.\n *\n * `emails` is here and not on the site rail because a site's messages\n * already have a page — the Emails console's Messages section, which this\n * plugin fills through a zone. With no site there is no Emails console, so\n * the org hub carries the same list itself.\n */\nexport const MARKETING_ORG_CONSOLE_SECTIONS: readonly ConsoleNavSection[] = [\n { id: 'campaigns', label: 'Campaigns' },\n { id: 'emails', label: 'Emails' },\n]\n"],"names":["MARKETING_CONSOLE_SECTIONS","id","label","MARKETING_ORG_CONSOLE_SECTIONS"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAYD;;;;;;;;;;;;;;CAcC,GACD,OAAO,MAAMA,6BAA2D;IACtE;QAAEC,IAAI;QAAYC,OAAO;IAAW;IACpC;;;;;;;;;;;GAWC,GACD;QAAED,IAAI;QAAaC,OAAO;IAAY;IACtC;;;;;;;;GAQC,GACD;QAAED,IAAI;QAAeC,OAAO;IAAc;IAC1C;QAAED,IAAI;QAAYC,OAAO;IAAW;IACpC;QAAED,IAAI;QAAeC,OAAO;IAAc;CAC3C,CAAA;AAED;;;;;CAKC,GAED;;;;;;;;;;;;;CAaC,GACD,OAAO,MAAMC,iCAA+D;IAC1E;QAAEF,IAAI;QAAaC,OAAO;IAAY;IACtC;QAAED,IAAI;QAAUC,OAAO;IAAS;CACjC,CAAA"}
@@ -0,0 +1,71 @@
1
+ import type { ConsolePluginOrgHost } from '@aglyn/aglyn';
2
+ import { type ReactNode } from 'react';
3
+ /**
4
+ * The organization the Marketing hub is mounted over, when it is mounted at
5
+ * the ORGANIZATION level rather than under one site.
6
+ *
7
+ * Campaigns, their sends and their sequence rollups belong to the org, so the
8
+ * same cards render at `/[orgSlug]/marketing/…` over every site and at
9
+ * `/[orgSlug]/hosts/[host]/marketing/…` over one. What differs at the org
10
+ * level is that there is no site to take a sender, a design or a conversion
11
+ * list from — so the page hands its cards the org and its sites instead, and
12
+ * each card asks for them here rather than through a prop threaded down
13
+ * every level of a detail page.
14
+ *
15
+ * `null` under a site. A card branches on that and nothing else: a
16
+ * `hostId` of `null` without a mount is a bug, not an org page.
17
+ */
18
+ export interface MarketingOrgMount {
19
+ orgId: string;
20
+ orgSlug: string;
21
+ /** The org's sites, as the shell resolved them. */
22
+ hosts: readonly ConsolePluginOrgHost[];
23
+ /** False until `hosts` has settled; an empty list before then means nothing. */
24
+ hostsReady: boolean;
25
+ /** `/[orgSlug]/hosts` — every site's own hub hangs beneath it. */
26
+ hostsPath: string;
27
+ /** The org Marketing hub's own path, `/[orgSlug]/marketing`. */
28
+ basePath: string;
29
+ }
30
+ export declare function MarketingOrgMountProvider(props: {
31
+ value: MarketingOrgMount | null;
32
+ children: ReactNode;
33
+ }): import("react").JSX.Element;
34
+ /** The org mount, or `null` when the hub is mounted under a site. */
35
+ export declare function useMarketingOrgMount(): MarketingOrgMount | null;
36
+ /**
37
+ * The org whose campaign collections a card reads, at either level.
38
+ *
39
+ * At the org level the mount names it and there is nothing to wait for. Under
40
+ * a site it is the site's owning org, resolved through `hostIndex` — and
41
+ * `ready` is false while that lookup is in flight, so a card that writes can
42
+ * hold rather than build `orgs/null/…`.
43
+ */
44
+ export declare function useMarketingOrgId(hostId: string | null | undefined): {
45
+ orgId: string | null;
46
+ ready: boolean;
47
+ };
48
+ /** A site's display name from the mount, or its id when the mount lacks it. */
49
+ export declare function orgSiteName(mount: MarketingOrgMount | null, hostId: string | null | undefined): string;
50
+ /**
51
+ * One site's own hub for a plugin — `/[orgSlug]/hosts/{subdomain}/{slug}` —
52
+ * or `null` when the site is not in the mount or has no subdomain yet.
53
+ *
54
+ * What an org page links to for anything that stays a SITE fact: a send's
55
+ * template, a site's conversions list, the screens and forms filed under a
56
+ * campaign.
57
+ */
58
+ export declare function orgSiteHubPath(mount: MarketingOrgMount | null, hostId: string | null | undefined, pluginSlug: string): string | null;
59
+ /** The org's sites as picker options, by name. */
60
+ export declare function orgSiteOptions(mount: MarketingOrgMount): Array<{
61
+ value: string;
62
+ label: string;
63
+ }>;
64
+ /**
65
+ * The site the topic catalog is resolved through.
66
+ *
67
+ * Topics are the organization's, and the zone that reports them resolves the
68
+ * org from a site. Under a site that is the site; on the org hub any of the
69
+ * org's sites answers with the same catalog, so the first one is asked.
70
+ */
71
+ export declare function topicCatalogHostId(hostId: string | null, mount: MarketingOrgMount | null): string;
@@ -0,0 +1,84 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ 'use client';
17
+ import { jsx as _jsx } from "react/jsx-runtime";
18
+ import { useOrgDataScope } from "@aglyn/tenant-feature-instance";
19
+ import { createContext, useContext } from "react";
20
+ const MarketingOrgMountContext = /*#__PURE__*/ createContext(null);
21
+ export function MarketingOrgMountProvider(props) {
22
+ return /*#__PURE__*/ _jsx(MarketingOrgMountContext.Provider, {
23
+ value: props.value,
24
+ children: props.children
25
+ });
26
+ }
27
+ /** The org mount, or `null` when the hub is mounted under a site. */ export function useMarketingOrgMount() {
28
+ return useContext(MarketingOrgMountContext);
29
+ }
30
+ /**
31
+ * The org whose campaign collections a card reads, at either level.
32
+ *
33
+ * At the org level the mount names it and there is nothing to wait for. Under
34
+ * a site it is the site's owning org, resolved through `hostIndex` — and
35
+ * `ready` is false while that lookup is in flight, so a card that writes can
36
+ * hold rather than build `orgs/null/…`.
37
+ */ export function useMarketingOrgId(hostId) {
38
+ const mount = useMarketingOrgMount();
39
+ const { orgId, ready } = useOrgDataScope({
40
+ hostId: hostId || undefined,
41
+ orgId: (mount == null ? void 0 : mount.orgId) || undefined
42
+ });
43
+ return {
44
+ orgId: orgId != null ? orgId : null,
45
+ ready
46
+ };
47
+ }
48
+ /** A site's display name from the mount, or its id when the mount lacks it. */ export function orgSiteName(mount, hostId) {
49
+ var _mount_hosts_find;
50
+ if (!hostId) return '';
51
+ return (mount == null ? void 0 : (_mount_hosts_find = mount.hosts.find((host)=>host.id === hostId)) == null ? void 0 : _mount_hosts_find.name) || hostId;
52
+ }
53
+ /**
54
+ * One site's own hub for a plugin — `/[orgSlug]/hosts/{subdomain}/{slug}` —
55
+ * or `null` when the site is not in the mount or has no subdomain yet.
56
+ *
57
+ * What an org page links to for anything that stays a SITE fact: a send's
58
+ * template, a site's conversions list, the screens and forms filed under a
59
+ * campaign.
60
+ */ export function orgSiteHubPath(mount, hostId, pluginSlug) {
61
+ var _mount_hosts_find;
62
+ if (!mount || !hostId) return null;
63
+ const subdomain = (_mount_hosts_find = mount.hosts.find((host)=>host.id === hostId)) == null ? void 0 : _mount_hosts_find.subdomain;
64
+ return subdomain ? `${mount.hostsPath}/${subdomain}/${pluginSlug}` : null;
65
+ }
66
+ /** The org's sites as picker options, by name. */ export function orgSiteOptions(mount) {
67
+ return mount.hosts.map((host)=>({
68
+ value: host.id,
69
+ label: host.name || host.subdomain || host.id
70
+ }));
71
+ }
72
+ /**
73
+ * The site the topic catalog is resolved through.
74
+ *
75
+ * Topics are the organization's, and the zone that reports them resolves the
76
+ * org from a site. Under a site that is the site; on the org hub any of the
77
+ * org's sites answers with the same catalog, so the first one is asked.
78
+ */ export function topicCatalogHostId(hostId, mount) {
79
+ var _ref;
80
+ var _mount_hosts_;
81
+ return (_ref = hostId != null ? hostId : mount == null ? void 0 : (_mount_hosts_ = mount.hosts[0]) == null ? void 0 : _mount_hosts_.id) != null ? _ref : '';
82
+ }
83
+
84
+ //# sourceMappingURL=marketing-org-mount.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/marketing/src/lib/components/marketing-org-mount.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport type { ConsolePluginOrgHost } from '@aglyn/aglyn'\nimport { useOrgDataScope } from '@aglyn/tenant-feature-instance'\nimport { createContext, useContext, type ReactNode } from 'react'\n\n/**\n * The organization the Marketing hub is mounted over, when it is mounted at\n * the ORGANIZATION level rather than under one site.\n *\n * Campaigns, their sends and their sequence rollups belong to the org, so the\n * same cards render at `/[orgSlug]/marketing/…` over every site and at\n * `/[orgSlug]/hosts/[host]/marketing/…` over one. What differs at the org\n * level is that there is no site to take a sender, a design or a conversion\n * list from — so the page hands its cards the org and its sites instead, and\n * each card asks for them here rather than through a prop threaded down\n * every level of a detail page.\n *\n * `null` under a site. A card branches on that and nothing else: a\n * `hostId` of `null` without a mount is a bug, not an org page.\n */\nexport interface MarketingOrgMount {\n orgId: string\n orgSlug: string\n /** The org's sites, as the shell resolved them. */\n hosts: readonly ConsolePluginOrgHost[]\n /** False until `hosts` has settled; an empty list before then means nothing. */\n hostsReady: boolean\n /** `/[orgSlug]/hosts` — every site's own hub hangs beneath it. */\n hostsPath: string\n /** The org Marketing hub's own path, `/[orgSlug]/marketing`. */\n basePath: string\n}\n\nconst MarketingOrgMountContext = createContext<MarketingOrgMount | null>(null)\n\nexport function MarketingOrgMountProvider(props: {\n value: MarketingOrgMount | null\n children: ReactNode\n}) {\n return (\n <MarketingOrgMountContext.Provider value={props.value}>\n {props.children}\n </MarketingOrgMountContext.Provider>\n )\n}\n\n/** The org mount, or `null` when the hub is mounted under a site. */\nexport function useMarketingOrgMount(): MarketingOrgMount | null {\n return useContext(MarketingOrgMountContext)\n}\n\n/**\n * The org whose campaign collections a card reads, at either level.\n *\n * At the org level the mount names it and there is nothing to wait for. Under\n * a site it is the site's owning org, resolved through `hostIndex` — and\n * `ready` is false while that lookup is in flight, so a card that writes can\n * hold rather than build `orgs/null/…`.\n */\nexport function useMarketingOrgId(hostId: string | null | undefined): {\n orgId: string | null\n ready: boolean\n} {\n const mount = useMarketingOrgMount()\n const { orgId, ready } = useOrgDataScope({\n hostId: hostId || undefined,\n orgId: mount?.orgId || undefined,\n })\n return { orgId: orgId ?? null, ready }\n}\n\n/** A site's display name from the mount, or its id when the mount lacks it. */\nexport function orgSiteName(\n mount: MarketingOrgMount | null,\n hostId: string | null | undefined,\n): string {\n if (!hostId) return ''\n return mount?.hosts.find((host) => host.id === hostId)?.name || hostId\n}\n\n/**\n * One site's own hub for a plugin — `/[orgSlug]/hosts/{subdomain}/{slug}` —\n * or `null` when the site is not in the mount or has no subdomain yet.\n *\n * What an org page links to for anything that stays a SITE fact: a send's\n * template, a site's conversions list, the screens and forms filed under a\n * campaign.\n */\nexport function orgSiteHubPath(\n mount: MarketingOrgMount | null,\n hostId: string | null | undefined,\n pluginSlug: string,\n): string | null {\n if (!mount || !hostId) return null\n const subdomain = mount.hosts.find((host) => host.id === hostId)?.subdomain\n return subdomain ? `${mount.hostsPath}/${subdomain}/${pluginSlug}` : null\n}\n\n/** The org's sites as picker options, by name. */\nexport function orgSiteOptions(\n mount: MarketingOrgMount,\n): Array<{ value: string; label: string }> {\n return mount.hosts.map((host) => ({\n value: host.id,\n label: host.name || host.subdomain || host.id,\n }))\n}\n\n/**\n * The site the topic catalog is resolved through.\n *\n * Topics are the organization's, and the zone that reports them resolves the\n * org from a site. Under a site that is the site; on the org hub any of the\n * org's sites answers with the same catalog, so the first one is asked.\n */\nexport function topicCatalogHostId(\n hostId: string | null,\n mount: MarketingOrgMount | null,\n): string {\n return hostId ?? mount?.hosts[0]?.id ?? ''\n}\n"],"names":["useOrgDataScope","createContext","useContext","MarketingOrgMountContext","MarketingOrgMountProvider","props","Provider","value","children","useMarketingOrgMount","useMarketingOrgId","hostId","mount","orgId","ready","undefined","orgSiteName","hosts","find","host","id","name","orgSiteHubPath","pluginSlug","subdomain","hostsPath","orgSiteOptions","map","label","topicCatalogHostId"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;AAGA,SAASA,eAAe,QAAQ,iCAAgC;AAChE,SAASC,aAAa,EAAEC,UAAU,QAAwB,QAAO;AA8BjE,MAAMC,yCAA2BF,cAAwC;AAEzE,OAAO,SAASG,0BAA0BC,KAGzC;IACC,qBACE,KAACF,yBAAyBG,QAAQ;QAACC,OAAOF,MAAME,KAAK;kBAClDF,MAAMG,QAAQ;;AAGrB;AAEA,mEAAmE,GACnE,OAAO,SAASC;IACd,OAAOP,WAAWC;AACpB;AAEA;;;;;;;CAOC,GACD,OAAO,SAASO,kBAAkBC,MAAiC;IAIjE,MAAMC,QAAQH;IACd,MAAM,EAAEI,KAAK,EAAEC,KAAK,EAAE,GAAGd,gBAAgB;QACvCW,QAAQA,UAAUI;QAClBF,OAAOD,CAAAA,yBAAAA,MAAOC,KAAK,KAAIE;IACzB;IACA,OAAO;QAAEF,KAAK,EAAEA,gBAAAA,QAAS;QAAMC;IAAM;AACvC;AAEA,6EAA6E,GAC7E,OAAO,SAASE,YACdJ,KAA+B,EAC/BD,MAAiC;QAG1BC;IADP,IAAI,CAACD,QAAQ,OAAO;IACpB,OAAOC,CAAAA,0BAAAA,oBAAAA,MAAOK,KAAK,CAACC,IAAI,CAAC,CAACC,OAASA,KAAKC,EAAE,KAAKT,4BAAxCC,kBAAiDS,IAAI,KAAIV;AAClE;AAEA;;;;;;;CAOC,GACD,OAAO,SAASW,eACdV,KAA+B,EAC/BD,MAAiC,EACjCY,UAAkB;QAGAX;IADlB,IAAI,CAACA,SAAS,CAACD,QAAQ,OAAO;IAC9B,MAAMa,aAAYZ,oBAAAA,MAAMK,KAAK,CAACC,IAAI,CAAC,CAACC,OAASA,KAAKC,EAAE,KAAKT,4BAAvCC,kBAAgDY,SAAS;IAC3E,OAAOA,YAAY,GAAGZ,MAAMa,SAAS,CAAC,CAAC,EAAED,UAAU,CAAC,EAAED,YAAY,GAAG;AACvE;AAEA,gDAAgD,GAChD,OAAO,SAASG,eACdd,KAAwB;IAExB,OAAOA,MAAMK,KAAK,CAACU,GAAG,CAAC,CAACR,OAAU,CAAA;YAChCZ,OAAOY,KAAKC,EAAE;YACdQ,OAAOT,KAAKE,IAAI,IAAIF,KAAKK,SAAS,IAAIL,KAAKC,EAAE;QAC/C,CAAA;AACF;AAEA;;;;;;CAMC,GACD,OAAO,SAASS,mBACdlB,MAAqB,EACrBC,KAA+B;QAExBD;QAAUC;IAAjB,QAAOD,OAAAA,iBAAAA,SAAUC,0BAAAA,gBAAAA,MAAOK,KAAK,CAAC,EAAE,qBAAfL,cAAiBQ,EAAE,YAA7BT,OAAiC;AAC1C"}
@@ -22,7 +22,7 @@ export interface CampaignSendApiResult {
22
22
  * at call time either way, so the ref costs nothing and the callback depends
23
23
  * on the one thing that really identifies the request.
24
24
  */
25
- export declare function useCampaignSendApi(hostId: string): (payload: Record<string, unknown>) => Promise<CampaignSendApiResult>;
25
+ export declare function useCampaignSendApi(hostId: string | null): (payload: Record<string, unknown>) => Promise<CampaignSendApiResult>;
26
26
  /**
27
27
  * ONE AUTHORIZED POST TO THE CAMPAIGN MANAGEMENT API.
28
28
  *
@@ -32,4 +32,4 @@ export declare function useCampaignSendApi(hostId: string): (payload: Record<str
32
32
  * — a draft's state and a container's emails are read there — so this is the
33
33
  * same one-POST caller pointed at a different path.
34
34
  */
35
- export declare function useCampaignManageApi(hostId: string): (payload: Record<string, unknown>) => Promise<CampaignSendApiResult>;
35
+ export declare function useCampaignManageApi(hostId: string | null, orgId?: string | null): (payload: Record<string, unknown>) => Promise<CampaignSendApiResult>;
@@ -47,13 +47,22 @@ import { resolveIdToken } from "@aglyn/shared-util-http/authorized-token";
47
47
  * that was never mailed. The route is what decides whether either is allowed
48
48
  * — a draft's state and a container's emails are read there — so this is the
49
49
  * same one-POST caller pointed at a different path.
50
- */ export function useCampaignManageApi(hostId) {
51
- return useCampaignApi(hostId, '/api/campaigns/manage');
50
+ */ export function useCampaignManageApi(hostId, orgId) {
51
+ return useCampaignApi(hostId, '/api/campaigns/manage', orgId);
52
52
  }
53
53
  /**
54
54
  * The POST both callers above make: the site id, the caller's ID token, and
55
55
  * whatever the action carries.
56
56
  *
57
+ * ## Which subject the body names
58
+ *
59
+ * Under a site, the site: the route resolves its org from it. On the org hub
60
+ * there is no site, so a request about a CONTAINER names the org instead —
61
+ * and a request about a SEND names the site that send is sent as, passed in
62
+ * the payload. A payload `hostId` therefore wins over the hook's own subject,
63
+ * and the org id is left off such a body, because a request that names both
64
+ * would leave the route to decide which one it is about.
65
+ *
57
66
  * The user is read through a ref for the reason given above, and the path is
58
67
  * a plain argument so the two hooks are one implementation. A hook taking the
59
68
  * URL from a component would be a way for a caller to post the console's
@@ -69,11 +78,19 @@ import { resolveIdToken } from "@aglyn/shared-util-http/authorized-token";
69
78
  * and a token that came back empty would otherwise post the send
70
79
  * unauthenticated and turn "you are signed out" into a refusal from the
71
80
  * route. Callers are expected to let the error reach the person.
72
- */ function useCampaignApi(hostId, path) {
81
+ */ function useCampaignApi(hostId, path, orgId) {
73
82
  const { data: user } = useUser();
74
83
  const userRef = useRef(user);
75
84
  userRef.current = user;
76
85
  return useCallback(async (payload)=>{
86
+ // Who the request is about: the payload's site, else the hook's own.
87
+ const subject = payload.hostId ? {
88
+ hostId: payload.hostId
89
+ } : hostId ? {
90
+ hostId
91
+ } : orgId ? {
92
+ orgId
93
+ } : {};
77
94
  const idToken = await resolveIdToken(userRef.current);
78
95
  const response = await fetch(path, {
79
96
  method: 'POST',
@@ -81,9 +98,7 @@ import { resolveIdToken } from "@aglyn/shared-util-http/authorized-token";
81
98
  'Content-Type': 'application/json',
82
99
  Authorization: `Bearer ${idToken}`
83
100
  },
84
- body: JSON.stringify(_extends({
85
- hostId
86
- }, payload))
101
+ body: JSON.stringify(_extends({}, payload, subject))
87
102
  });
88
103
  const json = await response.json().catch(()=>({}));
89
104
  return {
@@ -92,6 +107,7 @@ import { resolveIdToken } from "@aglyn/shared-util-http/authorized-token";
92
107
  };
93
108
  }, [
94
109
  hostId,
110
+ orgId,
95
111
  path
96
112
  ]);
97
113
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/marketing/src/lib/components/use-campaign-send-api.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport { useUser } from '@aglyn/tenant-feature-instance'\nimport { useCallback, useRef } from 'react'\nimport {\n resolveIdToken,\n type TokenSource,\n} from '@aglyn/shared-util-http/authorized-token'\n\n/** What one call to the campaign API answered with. */\nexport interface CampaignSendApiResult {\n response: Response\n payload: Record<string, any>\n}\n\n/**\n * ONE AUTHORIZED POST TO THE CAMPAIGN API.\n *\n * Every action the surface takes against a send — the audience count, the\n * message render, the test send, the send itself, and asking an email that\n * has already gone out to reach more people — is the same request with a\n * different `action`, so it is one caller rather than one per screen. The\n * route decides authorization from the token; nothing here does.\n *\n * ## The user is read through a REF, and that is load-bearing\n *\n * This callback is a dependency of the composer's two preview effects, so\n * anything that changes its identity re-issues their requests — and the user\n * object is a new object on most renders. Depending on it directly made the\n * count effect fire on every keystroke: clear the count, resolve the whole\n * audience again, for a number that cannot have moved. The token is fetched\n * at call time either way, so the ref costs nothing and the callback depends\n * on the one thing that really identifies the request.\n */\nexport function useCampaignSendApi(hostId: string) {\n return useCampaignApi(hostId, '/api/campaigns/send')\n}\n\n/**\n * ONE AUTHORIZED POST TO THE CAMPAIGN MANAGEMENT API.\n *\n * A second endpoint rather than a second action on the send route, because\n * nothing it does sends: it removes a campaign container, or discards a draft\n * that was never mailed. The route is what decides whether either is allowed\n * — a draft's state and a container's emails are read there — so this is the\n * same one-POST caller pointed at a different path.\n */\nexport function useCampaignManageApi(hostId: string) {\n return useCampaignApi(hostId, '/api/campaigns/manage')\n}\n\n/**\n * The POST both callers above make: the site id, the caller's ID token, and\n * whatever the action carries.\n *\n * The user is read through a ref for the reason given above, and the path is\n * a plain argument so the two hooks are one implementation. A hook taking the\n * URL from a component would be a way for a caller to post the console's\n * credentials somewhere the console does not own, which is why neither\n * exported hook accepts one.\n *\n * ## The token is obtained under a deadline, and its absence is an ERROR\n *\n * `resolveIdToken` throws rather than answering with nothing, and it gives up\n * rather than waiting forever. Both matter here because the token is awaited\n * IN FRONT of the request: a refresh that never answers means this call never\n * reaches `fetch` at all — no request, no response, no failure to report —\n * and a token that came back empty would otherwise post the send\n * unauthenticated and turn \"you are signed out\" into a refusal from the\n * route. Callers are expected to let the error reach the person.\n */\nfunction useCampaignApi(hostId: string, path: string) {\n const { data: user } = useUser()\n const userRef = useRef(user)\n userRef.current = user\n return useCallback(\n async (payload: Record<string, unknown>): Promise<CampaignSendApiResult> => {\n const idToken = await resolveIdToken(\n userRef.current as TokenSource | null | undefined,\n )\n const response = await fetch(path, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${idToken}`,\n },\n body: JSON.stringify({ hostId, ...payload }),\n })\n const json = await response.json().catch(() => ({}))\n return { response, payload: json as Record<string, any> }\n },\n [hostId, path],\n )\n}\n"],"names":["useUser","useCallback","useRef","resolveIdToken","useCampaignSendApi","hostId","useCampaignApi","useCampaignManageApi","path","data","user","userRef","current","payload","idToken","response","fetch","method","headers","Authorization","body","JSON","stringify","json","catch"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;AAEA,SAASA,OAAO,QAAQ,iCAAgC;AACxD,SAASC,WAAW,EAAEC,MAAM,QAAQ,QAAO;AAC3C,SACEC,cAAc,QAET,2CAA0C;AAQjD;;;;;;;;;;;;;;;;;;CAkBC,GACD,OAAO,SAASC,mBAAmBC,MAAc;IAC/C,OAAOC,eAAeD,QAAQ;AAChC;AAEA;;;;;;;;CAQC,GACD,OAAO,SAASE,qBAAqBF,MAAc;IACjD,OAAOC,eAAeD,QAAQ;AAChC;AAEA;;;;;;;;;;;;;;;;;;;CAmBC,GACD,SAASC,eAAeD,MAAc,EAAEG,IAAY;IAClD,MAAM,EAAEC,MAAMC,IAAI,EAAE,GAAGV;IACvB,MAAMW,UAAUT,OAAOQ;IACvBC,QAAQC,OAAO,GAAGF;IAClB,OAAOT,YACL,OAAOY;QACL,MAAMC,UAAU,MAAMX,eACpBQ,QAAQC,OAAO;QAEjB,MAAMG,WAAW,MAAMC,MAAMR,MAAM;YACjCS,QAAQ;YACRC,SAAS;gBACP,gBAAgB;gBAChBC,eAAe,CAAC,OAAO,EAAEL,SAAS;YACpC;YACAM,MAAMC,KAAKC,SAAS,CAAC;gBAAEjB;eAAWQ;QACpC;QACA,MAAMU,OAAO,MAAMR,SAASQ,IAAI,GAAGC,KAAK,CAAC,IAAO,CAAA,CAAC,CAAA;QACjD,OAAO;YAAET;YAAUF,SAASU;QAA4B;IAC1D,GACA;QAAClB;QAAQG;KAAK;AAElB"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/marketing/src/lib/components/use-campaign-send-api.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport { useUser } from '@aglyn/tenant-feature-instance'\nimport { useCallback, useRef } from 'react'\nimport {\n resolveIdToken,\n type TokenSource,\n} from '@aglyn/shared-util-http/authorized-token'\n\n/** What one call to the campaign API answered with. */\nexport interface CampaignSendApiResult {\n response: Response\n payload: Record<string, any>\n}\n\n/**\n * ONE AUTHORIZED POST TO THE CAMPAIGN API.\n *\n * Every action the surface takes against a send — the audience count, the\n * message render, the test send, the send itself, and asking an email that\n * has already gone out to reach more people — is the same request with a\n * different `action`, so it is one caller rather than one per screen. The\n * route decides authorization from the token; nothing here does.\n *\n * ## The user is read through a REF, and that is load-bearing\n *\n * This callback is a dependency of the composer's two preview effects, so\n * anything that changes its identity re-issues their requests — and the user\n * object is a new object on most renders. Depending on it directly made the\n * count effect fire on every keystroke: clear the count, resolve the whole\n * audience again, for a number that cannot have moved. The token is fetched\n * at call time either way, so the ref costs nothing and the callback depends\n * on the one thing that really identifies the request.\n */\nexport function useCampaignSendApi(hostId: string | null) {\n return useCampaignApi(hostId, '/api/campaigns/send')\n}\n\n/**\n * ONE AUTHORIZED POST TO THE CAMPAIGN MANAGEMENT API.\n *\n * A second endpoint rather than a second action on the send route, because\n * nothing it does sends: it removes a campaign container, or discards a draft\n * that was never mailed. The route is what decides whether either is allowed\n * — a draft's state and a container's emails are read there — so this is the\n * same one-POST caller pointed at a different path.\n */\nexport function useCampaignManageApi(\n hostId: string | null,\n orgId?: string | null,\n) {\n return useCampaignApi(hostId, '/api/campaigns/manage', orgId)\n}\n\n/**\n * The POST both callers above make: the site id, the caller's ID token, and\n * whatever the action carries.\n *\n * ## Which subject the body names\n *\n * Under a site, the site: the route resolves its org from it. On the org hub\n * there is no site, so a request about a CONTAINER names the org instead —\n * and a request about a SEND names the site that send is sent as, passed in\n * the payload. A payload `hostId` therefore wins over the hook's own subject,\n * and the org id is left off such a body, because a request that names both\n * would leave the route to decide which one it is about.\n *\n * The user is read through a ref for the reason given above, and the path is\n * a plain argument so the two hooks are one implementation. A hook taking the\n * URL from a component would be a way for a caller to post the console's\n * credentials somewhere the console does not own, which is why neither\n * exported hook accepts one.\n *\n * ## The token is obtained under a deadline, and its absence is an ERROR\n *\n * `resolveIdToken` throws rather than answering with nothing, and it gives up\n * rather than waiting forever. Both matter here because the token is awaited\n * IN FRONT of the request: a refresh that never answers means this call never\n * reaches `fetch` at all — no request, no response, no failure to report —\n * and a token that came back empty would otherwise post the send\n * unauthenticated and turn \"you are signed out\" into a refusal from the\n * route. Callers are expected to let the error reach the person.\n */\nfunction useCampaignApi(\n hostId: string | null,\n path: string,\n orgId?: string | null,\n) {\n const { data: user } = useUser()\n const userRef = useRef(user)\n userRef.current = user\n return useCallback(\n async (payload: Record<string, unknown>): Promise<CampaignSendApiResult> => {\n // Who the request is about: the payload's site, else the hook's own.\n const subject = payload.hostId\n ? { hostId: payload.hostId }\n : hostId\n ? { hostId }\n : orgId\n ? { orgId }\n : {}\n const idToken = await resolveIdToken(\n userRef.current as TokenSource | null | undefined,\n )\n const response = await fetch(path, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${idToken}`,\n },\n body: JSON.stringify({ ...payload, ...subject }),\n })\n const json = await response.json().catch(() => ({}))\n return { response, payload: json as Record<string, any> }\n },\n [hostId, orgId, path],\n )\n}\n"],"names":["useUser","useCallback","useRef","resolveIdToken","useCampaignSendApi","hostId","useCampaignApi","useCampaignManageApi","orgId","path","data","user","userRef","current","payload","subject","idToken","response","fetch","method","headers","Authorization","body","JSON","stringify","json","catch"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;AAEA,SAASA,OAAO,QAAQ,iCAAgC;AACxD,SAASC,WAAW,EAAEC,MAAM,QAAQ,QAAO;AAC3C,SACEC,cAAc,QAET,2CAA0C;AAQjD;;;;;;;;;;;;;;;;;;CAkBC,GACD,OAAO,SAASC,mBAAmBC,MAAqB;IACtD,OAAOC,eAAeD,QAAQ;AAChC;AAEA;;;;;;;;CAQC,GACD,OAAO,SAASE,qBACdF,MAAqB,EACrBG,KAAqB;IAErB,OAAOF,eAAeD,QAAQ,yBAAyBG;AACzD;AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4BC,GACD,SAASF,eACPD,MAAqB,EACrBI,IAAY,EACZD,KAAqB;IAErB,MAAM,EAAEE,MAAMC,IAAI,EAAE,GAAGX;IACvB,MAAMY,UAAUV,OAAOS;IACvBC,QAAQC,OAAO,GAAGF;IAClB,OAAOV,YACL,OAAOa;QACL,qEAAqE;QACrE,MAAMC,UAAUD,QAAQT,MAAM,GAC1B;YAAEA,QAAQS,QAAQT,MAAM;QAAC,IACzBA,SACE;YAAEA;QAAO,IACTG,QACE;YAAEA;QAAM,IACR,CAAC;QACT,MAAMQ,UAAU,MAAMb,eACpBS,QAAQC,OAAO;QAEjB,MAAMI,WAAW,MAAMC,MAAMT,MAAM;YACjCU,QAAQ;YACRC,SAAS;gBACP,gBAAgB;gBAChBC,eAAe,CAAC,OAAO,EAAEL,SAAS;YACpC;YACAM,MAAMC,KAAKC,SAAS,CAAC,aAAKV,SAAYC;QACxC;QACA,MAAMU,OAAO,MAAMR,SAASQ,IAAI,GAAGC,KAAK,CAAC,IAAO,CAAA,CAAC,CAAA;QACjD,OAAO;YAAET;YAAUH,SAASW;QAA4B;IAC1D,GACA;QAACpB;QAAQG;QAAOC;KAAK;AAEzB"}
@@ -15,25 +15,35 @@
15
15
  * open of a campaign, to render a link.
16
16
  *
17
17
  * `null` before the params resolve, so callers render plain text rather than
18
- * a link to nowhere.
18
+ * a link to nowhere — and on the org Marketing hub, where there is no site
19
+ * and so no Emails console; a card there builds its message links from the
20
+ * org mount instead.
19
21
  */
20
22
  export declare function useEmailsHubPath(): string | null;
21
23
  /**
22
- * The MARKETING console's URL under the site being read.
24
+ * The MARKETING console's URL: under the site being read, or the
25
+ * organization's own Marketing hub on a page that names no site.
23
26
  *
24
27
  * Named for the same reason the Emails one is, from the opposite direction: a
25
28
  * converted record — a form submission in the Inbox, a contact in Contacts —
26
29
  * links to the campaign it came from, and those surfaces are handed their own
27
30
  * hub's `basePath`, not this one's.
31
+ *
32
+ * Unlike the Emails hub it has an answer with no site in the URL. Campaigns
33
+ * belong to the organization, so the org-level CRM and the org Marketing hub
34
+ * itself link a campaign to `/[orgSlug]/marketing/campaigns/{id}`.
28
35
  */
29
36
  export declare function useMarketingHubPath(): string | null;
30
37
  /**
31
- * Any sibling hub's URL, by plugin slug.
38
+ * Any sibling hub's URL under the site being read, by plugin slug.
32
39
  *
33
40
  * One implementation, because "read the org slug and the subdomain off the
34
41
  * route rather than resolving the host document" is the property that keeps
35
42
  * these links free, and a second copy of it is where somebody pays for the
36
43
  * two `getDoc`s instead.
44
+ *
45
+ * `null` on a page that names no site: a site's hub is the only thing this
46
+ * builds.
37
47
  */
38
48
  export declare function useConsoleHubPath(pluginSlug: string): string | null;
39
49
  export default useEmailsHubPath;