@aglyn/plugins-email 1.0.0-beta.178 → 1.0.0-beta.180
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.
- package/README.md +2 -1
- package/package.json +17 -17
- package/src/lib/components/email-org-mount.d.ts +116 -0
- package/src/lib/components/email-org-mount.js +211 -0
- package/src/lib/components/email-org-mount.js.map +1 -0
- package/src/lib/components/email-topic-detail.d.ts +2 -1
- package/src/lib/components/email-topic-detail.js +7 -1
- package/src/lib/components/email-topic-detail.js.map +1 -1
- package/src/lib/components/email-topics-card.d.ts +2 -1
- package/src/lib/components/email-topics-card.js +7 -1
- package/src/lib/components/email-topics-card.js.map +1 -1
- package/src/lib/components/email-zones.d.ts +28 -1
- package/src/lib/components/email-zones.js.map +1 -1
- package/src/lib/components/emails-console-page.d.ts +4 -0
- package/src/lib/components/emails-console-page.js +107 -5
- package/src/lib/components/emails-console-page.js.map +1 -1
- package/src/lib/components/list-detail-card.d.ts +19 -3
- package/src/lib/components/list-detail-card.js +52 -13
- package/src/lib/components/list-detail-card.js.map +1 -1
- package/src/lib/components/list-edit-card.d.ts +2 -1
- package/src/lib/components/list-edit-card.js +56 -17
- package/src/lib/components/list-edit-card.js.map +1 -1
- package/src/lib/components/lists-card.d.ts +2 -1
- package/src/lib/components/lists-card.js +5 -5
- package/src/lib/components/lists-card.js.map +1 -1
- package/src/lib/components/org-email-templates-card.d.ts +34 -0
- package/src/lib/components/org-email-templates-card.js +470 -0
- package/src/lib/components/org-email-templates-card.js.map +1 -0
- package/src/lib/components/org-sending-card.d.ts +30 -0
- package/src/lib/components/org-sending-card.js +680 -0
- package/src/lib/components/org-sending-card.js.map +1 -0
- package/src/lib/components/org-sending-domain-detail.d.ts +28 -0
- package/src/lib/components/org-sending-domain-detail.js +497 -0
- package/src/lib/components/org-sending-domain-detail.js.map +1 -0
- package/src/lib/components/org-site-suppressions.d.ts +20 -0
- package/src/lib/components/org-site-suppressions.js +99 -0
- package/src/lib/components/org-site-suppressions.js.map +1 -0
- package/src/lib/components/org-suppressions-card.d.ts +22 -0
- package/src/lib/components/org-suppressions-card.js +196 -0
- package/src/lib/components/org-suppressions-card.js.map +1 -0
- package/src/lib/components/sending-domain-detail.d.ts +12 -0
- package/src/lib/components/sending-domain-detail.js +4 -1
- package/src/lib/components/sending-domain-detail.js.map +1 -1
- package/src/lib/components/suppression-totals.d.ts +63 -0
- package/src/lib/components/suppression-totals.js +105 -0
- package/src/lib/components/suppression-totals.js.map +1 -0
- package/src/lib/components/suppressions-card.js +7 -80
- package/src/lib/components/suppressions-card.js.map +1 -1
- package/src/lib/components/use-org-email-topics.d.ts +9 -1
- package/src/lib/components/use-org-email-topics.js +4 -3
- package/src/lib/components/use-org-email-topics.js.map +1 -1
- package/src/lib/components/use-org-sending-views.d.ts +39 -0
- package/src/lib/components/use-org-sending-views.js +124 -0
- package/src/lib/components/use-org-sending-views.js.map +1 -0
- package/src/lib/model/sending-domain-status.d.ts +36 -0
- package/src/lib/model/sending-domain-status.js +46 -0
- package/src/lib/model/sending-domain-status.js.map +1 -1
- package/src/lib/plugin.js +29 -1
- package/src/lib/plugin.js.map +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/email/src/lib/model/sending-domain-status.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\n/**\n * WHAT EACH SENDING-DOMAIN STATE LOOKS LIKE, said once.\n *\n * The list and the domain's own page both render a state, and they must not\n * describe the same record differently — a chip saying Verified beside a page\n * saying Not published is the kind of disagreement that makes a customer stop\n * believing either. One module, read twice.\n *\n * ## THE DISTINCTION THIS FILE EXISTS FOR\n *\n * `inconclusive` is not `failed`, and it is not a stored status either.\n *\n * A stored status is the conclusion of a lookup that got answers.\n * `inconclusive` is what happens when NOBODY ANSWERED — a resolver outage, a\n * timeout, a zone that is temporarily unreachable. It is evidence of nothing,\n * so `verifySendingDomain` writes only the check time and leaves the status\n * exactly where it was, and the route answers `503` rather than `200 with\n * verified: false`.\n *\n * Rendering it as a failure would be the most expensive mistake this surface\n * could make. A customer whose DNS is perfect would be told their records are\n * missing, and would go and edit a zone that has nothing wrong with it —\n * possibly breaking the records that were already right. So it is modeled\n * here as a SEPARATE, TRANSIENT layer that sits beside the stored status\n * without replacing it: {@link describeSendingDomain} keeps reporting\n * `records-issued` or `verified`, and {@link INCONCLUSIVE_CHECK} is what the\n * surface adds next to it.\n *\n * ## THE OTHER DISTINCTION: A CLAIM THAT IS WAITING IS NOT A CLAIM THAT FAILED\n *\n * A dedicated platform subdomain is claimed when a merchant asks for one, and\n * the claim is filled only if the mail provider's account-wide domain\n * allowance has room. When it does not, the claim is refused before any call\n * is made and `at-capacity` is stored where a provider refusal would go.\n *\n * The record cannot tell those two apart — both leave a `requested` domain\n * with a reason and no key — but the reader has to, because the sentences\n * point at different people. A provider refusal is a fault to retry; this is a\n * queue, nothing is broken, and the retry does not move it until we have\n * bought more allowance. Describing it as a failed key request would tell a\n * merchant their sending setup is broken when the only thing that happened is\n * that they are waiting for something they were never promised outright.\n */\n\nimport { SENDING_DOMAIN_AT_CAPACITY } from '@aglyn/shared-util-email'\n\n/** The four states a record can be stored in. */\nexport type SendingDomainStatusId =\n | 'requested'\n | 'records-issued'\n | 'verified'\n | 'failed'\n\nexport interface SendingDomainStateView {\n /** Two or three words, for a chip. */\n label: string\n color: 'default' | 'info' | 'success' | 'warning' | 'error'\n severity: 'info' | 'success' | 'warning' | 'error'\n /** What is true, and what to do about it, in the customer's terms. */\n text: string\n /** Whether mail can leave on this domain right now. */\n sending: boolean\n}\n\n/**\n * One stored state, described.\n *\n * `pendingProvider` splits `requested` in two, and the split matters: both\n * are \"no records yet\", but one is a deployment that cannot issue signing\n * keys at all and the other is a claim whose issuing call has not happened or\n * did not succeed. Telling a customer to publish records that do not exist is\n * the failure the whole `records-issued` gate is arranged against, and saying\n * \"your DNS is wrong\" to an operator whose credential is missing points the\n * sentence at the wrong person entirely.\n */\nexport function describeSendingDomain(input: {\n status: SendingDomainStatusId\n /** Set when this deployment has no credential that can issue a key. */\n pendingProvider?: boolean\n /** A short code from the provider driver's fixed vocabulary. */\n issueError?: string | null\n /** Record keys the last conclusive lookup did not see. */\n missing?: readonly string[] | null\n}): SendingDomainStateView {\n switch (input?.status) {\n case 'verified':\n return {\n label: 'Verified',\n color: 'success',\n severity: 'success',\n text:\n 'Every required record is published and we can see it. Mail from ' +\n 'this site can leave on this domain, signed as you.',\n sending: true,\n }\n case 'records-issued':\n return {\n label: 'Publish the records',\n color: 'info',\n severity: 'info',\n text:\n 'The records below are yours to add at whoever hosts your DNS. ' +\n 'They usually take a few minutes to spread, sometimes longer — ' +\n 'add them, then press Check DNS. Nothing sends on this domain ' +\n 'until they are live.',\n sending: false,\n }\n case 'failed':\n return {\n label: 'Records not found',\n color: 'error',\n severity: 'warning',\n text: input?.missing?.length\n ? `We looked, and these records are not published yet: ` +\n `${input.missing.join(', ')}. Add them exactly as shown below ` +\n `and check again.`\n : 'We looked, and the required records are not published yet. Add ' +\n 'them exactly as shown below and check again.',\n sending: false,\n }\n default:\n /*\n * `requested`: claimed, with nothing to publish.\n *\n * An empty records table would read as our bug — which, from the\n * customer's side, it is — so this says so in words instead. The\n * distinction below is between a deployment that cannot issue keys and\n * one whose attempt failed, because those are two different people's\n * problems and only one of them is the customer's.\n */\n /*\n * WAITING FOR ROOM, and it is neither of those two.\n *\n * Read FIRST, because it arrives in the same field a provider refusal\n * does and the generic branch would otherwise print \"the mail provider\n * did not issue a signing key\" about a call that was never made.\n *\n * Three things this has to say and the failure copy gets all three\n * wrong: nothing is broken, nothing at a registrar is involved, and the\n * site's account email is still going out on the shared address. It\n * names the retry anyway — the button is on the screen either way, and\n * a state that says nothing about the one control beside it invites the\n * reader to assume it will help.\n */\n if (String(input?.issueError ?? '') === SENDING_DOMAIN_AT_CAPACITY) {\n return {\n label: 'Waiting for room',\n color: 'warning',\n severity: 'info',\n text:\n 'This domain has been asked for and is waiting. We are at our ' +\n 'mail provider’s limit on sending domains, so it has not been ' +\n 'created yet — nothing here is broken and nothing at your DNS ' +\n 'host is involved. This site keeps sending its receipts and ' +\n 'account email on the shared address meanwhile; campaigns wait ' +\n 'with the domain. Request records will keep answering the same ' +\n 'way until we have room, and a domain you own instead of this ' +\n 'one is never held this way.',\n sending: false,\n }\n }\n if (input?.pendingProvider !== false && !input?.issueError) {\n return {\n label: 'Waiting on a signing key',\n color: 'warning',\n severity: 'info',\n text:\n 'This domain is claimed, but no signing key has been issued for ' +\n 'it yet, so there is nothing to publish. This one is on us, not ' +\n 'on your DNS — nothing you can change at your registrar will ' +\n 'move it. Press Request records to try again.',\n sending: false,\n }\n }\n return {\n label: 'Key request failed',\n color: 'error',\n severity: 'error',\n text:\n `The mail provider did not issue a signing key for this domain ` +\n `(${input.issueError}). The claim is kept, so retrying costs ` +\n `nothing and creates no second domain — press Request records. If ` +\n `it keeps failing, this is ours to fix, not your DNS.`,\n sending: false,\n }\n }\n}\n\n/**\n * THE FIFTH SITUATION, and the one that is not a status.\n *\n * Held in the surface's own state after a check that nobody answered, and\n * rendered NEXT TO the stored state rather than in place of it. The record is\n * untouched, the previous conclusion still stands, and the only honest thing\n * to say is that the question could not be asked.\n */\nexport const INCONCLUSIVE_CHECK = {\n label: 'DNS unreachable',\n color: 'default' as const,\n severity: 'info' as const,\n text:\n 'We could not reach DNS to run that check, so nothing has changed — not ' +\n 'the records, and not this domain’s state. This is our lookup failing, ' +\n 'not a problem with your zone. Try again in a few minutes.',\n}\n\n/**\n * WHAT REMOVING ONE SENDING DOMAIN DOES TO THE SITE USING IT.\n *\n * Here for the reason {@link describeSendingDomain} is: the domain's own page\n * and the row menu on the list both ask to remove the same record, and two\n * confirmations describing one action differently is how a merchant comes to\n * dismiss the harsher one as boilerplate.\n *\n * ## Three answers, because releasing a claim does three different things\n *\n * `resolveHostSendingIdentity` reads WHOSE name a selection is from the domain\n * itself, so what a removal costs depends on which of the three the site is\n * standing on:\n *\n * - a domain the CUSTOMER owns, currently in use. It stops sending\n * altogether, receipts included. That is deliberate rather than a gap: the\n * customer published records saying what their recipients would see, and\n * falling back to any other address would contradict them.\n * - a domain WE set up, currently in use. It drops to the shared pool, so\n * all of its mail carries on, on an address whose reputation is shared.\n * - a domain nothing is sending as. The claim and the key go; no mail moves.\n *\n * Printing the harshest of the three for all of them would be the surface\n * warning about a consequence that is not going to happen, which is the same\n * failure as printing the gentlest.\n */\nexport function describeSendingDomainRemoval(input: {\n domain: string\n /** The domain this site currently sends as, as the route reported it. */\n selected?: string | null\n /** The site's own platform-provisioned domain, or `''` when it has none. */\n platformDomain?: string | null\n}): { title: string; description: string; confirmationText: string } {\n const domain = String(input?.domain ?? '')\n const inUse = Boolean(domain) && String(input?.selected ?? '') === domain\n const ours = Boolean(domain) && String(input?.platformDomain ?? '') === domain\n\n return {\n title: `Remove ${domain}?`,\n description: !inUse\n ? 'The claim and the signing key are dropped. The DNS records stay in ' +\n 'your zone — nothing is changed at your registrar — and you can add ' +\n 'the domain again later, which issues a new key.'\n : ours\n ? `This site is currently sending as ${domain}. Removing it moves ` +\n 'all of this site’s email back to the shared address, whose ' +\n 'delivery reputation is pooled with the other sites on it — so ' +\n 'campaigns there are held to tighter complaint and bounce limits. ' +\n 'Nothing in your own DNS is involved — we published these records ' +\n 'and we remove them.'\n : `This site is currently sending as ${domain}. Removing the domain ` +\n 'does not move it onto another address — not the one this site is ' +\n 'issued, and not the shared address. It stops this site sending at ' +\n 'all, receipts included, until you choose another identity. The ' +\n 'DNS records stay in your zone; nothing is changed at your ' +\n 'registrar.',\n confirmationText: 'Remove domain',\n }\n}\n"],"names":["SENDING_DOMAIN_AT_CAPACITY","describeSendingDomain","input","status","label","color","severity","text","sending","missing","length","join","String","issueError","pendingProvider","INCONCLUSIVE_CHECK","describeSendingDomainRemoval","domain","inUse","Boolean","selected","ours","platformDomain","title","description","confirmationText"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA0CC,GAED,SAASA,0BAA0B,QAAQ,2BAA0B;AAoBrE;;;;;;;;;;CAUC,GACD,OAAO,SAASC,sBAAsBC,KAQrC;IACC,OAAQA,yBAAAA,MAAOC,MAAM;QACnB,KAAK;YACH,OAAO;gBACLC,OAAO;gBACPC,OAAO;gBACPC,UAAU;gBACVC,MACE,qEACA;gBACFC,SAAS;YACX;QACF,KAAK;YACH,OAAO;gBACLJ,OAAO;gBACPC,OAAO;gBACPC,UAAU;gBACVC,MACE,mEACA,mEACA,kEACA;gBACFC,SAAS;YACX;QACF,KAAK;gBAKKN;YAJR,OAAO;gBACLE,OAAO;gBACPC,OAAO;gBACPC,UAAU;gBACVC,MAAML,CAAAA,0BAAAA,iBAAAA,MAAOO,OAAO,qBAAdP,eAAgBQ,MAAM,IACxB,CAAC,oDAAoD,CAAC,GACtD,GAAGR,MAAMO,OAAO,CAACE,IAAI,CAAC,MAAM,kCAAkC,CAAC,GAC/D,CAAC,gBAAgB,CAAC,GAClB,oEACA;gBACJH,SAAS;YACX;QACF;;YACE;;;;;;;;OAQC,GACD;;;;;;;;;;;;;OAaC,GACD,IAAII,eAAOV,yBAAAA,MAAOW,UAAU,mBAAI,QAAQb,4BAA4B;gBAClE,OAAO;oBACLI,OAAO;oBACPC,OAAO;oBACPC,UAAU;oBACVC,MACE,kEACA,kEACA,kEACA,gEACA,mEACA,mEACA,kEACA;oBACFC,SAAS;gBACX;YACF;YACA,IAAIN,CAAAA,yBAAAA,MAAOY,eAAe,MAAK,SAAS,EAACZ,yBAAAA,MAAOW,UAAU,GAAE;gBAC1D,OAAO;oBACLT,OAAO;oBACPC,OAAO;oBACPC,UAAU;oBACVC,MACE,oEACA,oEACA,iEACA;oBACFC,SAAS;gBACX;YACF;YACA,OAAO;gBACLJ,OAAO;gBACPC,OAAO;gBACPC,UAAU;gBACVC,MACE,CAAC,8DAA8D,CAAC,GAChE,CAAC,CAAC,EAAEL,MAAMW,UAAU,CAAC,wCAAwC,CAAC,GAC9D,CAAC,iEAAiE,CAAC,GACnE,CAAC,oDAAoD,CAAC;gBACxDL,SAAS;YACX;IACJ;AACF;AAEA;;;;;;;CAOC,GACD,OAAO,MAAMO,qBAAqB;IAChCX,OAAO;IACPC,OAAO;IACPC,UAAU;IACVC,MACE,4EACA,2EACA;AACJ,EAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;CAyBC,GACD,OAAO,SAASS,6BAA6Bd,KAM5C;;IACC,MAAMe,SAASL,eAAOV,yBAAAA,MAAOe,MAAM,mBAAI;IACvC,MAAMC,QAAQC,QAAQF,WAAWL,gBAAOV,yBAAAA,MAAOkB,QAAQ,oBAAI,QAAQH;IACnE,MAAMI,OAAOF,QAAQF,WAAWL,gBAAOV,yBAAAA,MAAOoB,cAAc,oBAAI,QAAQL;IAExE,OAAO;QACLM,OAAO,CAAC,OAAO,EAAEN,OAAO,CAAC,CAAC;QAC1BO,aAAa,CAACN,QACV,wEACA,wEACA,oDACAG,OACE,CAAC,kCAAkC,EAAEJ,OAAO,oBAAoB,CAAC,GACjE,gEACA,mEACA,sEACA,sEACA,wBACA,CAAC,kCAAkC,EAAEA,OAAO,sBAAsB,CAAC,GACnE,sEACA,uEACA,oEACA,+DACA;QACNQ,kBAAkB;IACpB;AACF"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/email/src/lib/model/sending-domain-status.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\n/**\n * WHAT EACH SENDING-DOMAIN STATE LOOKS LIKE, said once.\n *\n * The list and the domain's own page both render a state, and they must not\n * describe the same record differently — a chip saying Verified beside a page\n * saying Not published is the kind of disagreement that makes a customer stop\n * believing either. One module, read twice.\n *\n * ## THE DISTINCTION THIS FILE EXISTS FOR\n *\n * `inconclusive` is not `failed`, and it is not a stored status either.\n *\n * A stored status is the conclusion of a lookup that got answers.\n * `inconclusive` is what happens when NOBODY ANSWERED — a resolver outage, a\n * timeout, a zone that is temporarily unreachable. It is evidence of nothing,\n * so `verifySendingDomain` writes only the check time and leaves the status\n * exactly where it was, and the route answers `503` rather than `200 with\n * verified: false`.\n *\n * Rendering it as a failure would be the most expensive mistake this surface\n * could make. A customer whose DNS is perfect would be told their records are\n * missing, and would go and edit a zone that has nothing wrong with it —\n * possibly breaking the records that were already right. So it is modeled\n * here as a SEPARATE, TRANSIENT layer that sits beside the stored status\n * without replacing it: {@link describeSendingDomain} keeps reporting\n * `records-issued` or `verified`, and {@link INCONCLUSIVE_CHECK} is what the\n * surface adds next to it.\n *\n * ## THE OTHER DISTINCTION: A CLAIM THAT IS WAITING IS NOT A CLAIM THAT FAILED\n *\n * A dedicated platform subdomain is claimed when a merchant asks for one, and\n * the claim is filled only if the mail provider's account-wide domain\n * allowance has room. When it does not, the claim is refused before any call\n * is made and `at-capacity` is stored where a provider refusal would go.\n *\n * The record cannot tell those two apart — both leave a `requested` domain\n * with a reason and no key — but the reader has to, because the sentences\n * point at different people. A provider refusal is a fault to retry; this is a\n * queue, nothing is broken, and the retry does not move it until we have\n * bought more allowance. Describing it as a failed key request would tell a\n * merchant their sending setup is broken when the only thing that happened is\n * that they are waiting for something they were never promised outright.\n */\n\nimport { SENDING_DOMAIN_AT_CAPACITY } from '@aglyn/shared-util-email'\n\n/** The four states a record can be stored in. */\nexport type SendingDomainStatusId =\n | 'requested'\n | 'records-issued'\n | 'verified'\n | 'failed'\n\nexport interface SendingDomainStateView {\n /** Two or three words, for a chip. */\n label: string\n color: 'default' | 'info' | 'success' | 'warning' | 'error'\n severity: 'info' | 'success' | 'warning' | 'error'\n /** What is true, and what to do about it, in the customer's terms. */\n text: string\n /** Whether mail can leave on this domain right now. */\n sending: boolean\n}\n\n/**\n * One stored state, described.\n *\n * `pendingProvider` splits `requested` in two, and the split matters: both\n * are \"no records yet\", but one is a deployment that cannot issue signing\n * keys at all and the other is a claim whose issuing call has not happened or\n * did not succeed. Telling a customer to publish records that do not exist is\n * the failure the whole `records-issued` gate is arranged against, and saying\n * \"your DNS is wrong\" to an operator whose credential is missing points the\n * sentence at the wrong person entirely.\n */\nexport function describeSendingDomain(input: {\n status: SendingDomainStatusId\n /** Set when this deployment has no credential that can issue a key. */\n pendingProvider?: boolean\n /** A short code from the provider driver's fixed vocabulary. */\n issueError?: string | null\n /** Record keys the last conclusive lookup did not see. */\n missing?: readonly string[] | null\n}): SendingDomainStateView {\n switch (input?.status) {\n case 'verified':\n return {\n label: 'Verified',\n color: 'success',\n severity: 'success',\n text:\n 'Every required record is published and we can see it. Mail from ' +\n 'this site can leave on this domain, signed as you.',\n sending: true,\n }\n case 'records-issued':\n return {\n label: 'Publish the records',\n color: 'info',\n severity: 'info',\n text:\n 'The records below are yours to add at whoever hosts your DNS. ' +\n 'They usually take a few minutes to spread, sometimes longer — ' +\n 'add them, then press Check DNS. Nothing sends on this domain ' +\n 'until they are live.',\n sending: false,\n }\n case 'failed':\n return {\n label: 'Records not found',\n color: 'error',\n severity: 'warning',\n text: input?.missing?.length\n ? `We looked, and these records are not published yet: ` +\n `${input.missing.join(', ')}. Add them exactly as shown below ` +\n `and check again.`\n : 'We looked, and the required records are not published yet. Add ' +\n 'them exactly as shown below and check again.',\n sending: false,\n }\n default:\n /*\n * `requested`: claimed, with nothing to publish.\n *\n * An empty records table would read as our bug — which, from the\n * customer's side, it is — so this says so in words instead. The\n * distinction below is between a deployment that cannot issue keys and\n * one whose attempt failed, because those are two different people's\n * problems and only one of them is the customer's.\n */\n /*\n * WAITING FOR ROOM, and it is neither of those two.\n *\n * Read FIRST, because it arrives in the same field a provider refusal\n * does and the generic branch would otherwise print \"the mail provider\n * did not issue a signing key\" about a call that was never made.\n *\n * Three things this has to say and the failure copy gets all three\n * wrong: nothing is broken, nothing at a registrar is involved, and the\n * site's account email is still going out on the shared address. It\n * names the retry anyway — the button is on the screen either way, and\n * a state that says nothing about the one control beside it invites the\n * reader to assume it will help.\n */\n if (String(input?.issueError ?? '') === SENDING_DOMAIN_AT_CAPACITY) {\n return {\n label: 'Waiting for room',\n color: 'warning',\n severity: 'info',\n text:\n 'This domain has been asked for and is waiting. We are at our ' +\n 'mail provider’s limit on sending domains, so it has not been ' +\n 'created yet — nothing here is broken and nothing at your DNS ' +\n 'host is involved. This site keeps sending its receipts and ' +\n 'account email on the shared address meanwhile; campaigns wait ' +\n 'with the domain. Request records will keep answering the same ' +\n 'way until we have room, and a domain you own instead of this ' +\n 'one is never held this way.',\n sending: false,\n }\n }\n if (input?.pendingProvider !== false && !input?.issueError) {\n return {\n label: 'Waiting on a signing key',\n color: 'warning',\n severity: 'info',\n text:\n 'This domain is claimed, but no signing key has been issued for ' +\n 'it yet, so there is nothing to publish. This one is on us, not ' +\n 'on your DNS — nothing you can change at your registrar will ' +\n 'move it. Press Request records to try again.',\n sending: false,\n }\n }\n return {\n label: 'Key request failed',\n color: 'error',\n severity: 'error',\n text:\n `The mail provider did not issue a signing key for this domain ` +\n `(${input.issueError}). The claim is kept, so retrying costs ` +\n `nothing and creates no second domain — press Request records. If ` +\n `it keeps failing, this is ours to fix, not your DNS.`,\n sending: false,\n }\n }\n}\n\n/**\n * THE FIFTH SITUATION, and the one that is not a status.\n *\n * Held in the surface's own state after a check that nobody answered, and\n * rendered NEXT TO the stored state rather than in place of it. The record is\n * untouched, the previous conclusion still stands, and the only honest thing\n * to say is that the question could not be asked.\n */\nexport const INCONCLUSIVE_CHECK = {\n label: 'DNS unreachable',\n color: 'default' as const,\n severity: 'info' as const,\n text:\n 'We could not reach DNS to run that check, so nothing has changed — not ' +\n 'the records, and not this domain’s state. This is our lookup failing, ' +\n 'not a problem with your zone. Try again in a few minutes.',\n}\n\n/**\n * WHAT REMOVING ONE SENDING DOMAIN DOES TO THE SITE USING IT.\n *\n * Here for the reason {@link describeSendingDomain} is: the domain's own page\n * and the row menu on the list both ask to remove the same record, and two\n * confirmations describing one action differently is how a merchant comes to\n * dismiss the harsher one as boilerplate.\n *\n * ## Three answers, because releasing a claim does three different things\n *\n * `resolveHostSendingIdentity` reads WHOSE name a selection is from the domain\n * itself, so what a removal costs depends on which of the three the site is\n * standing on:\n *\n * - a domain the CUSTOMER owns, currently in use. It stops sending\n * altogether, receipts included. That is deliberate rather than a gap: the\n * customer published records saying what their recipients would see, and\n * falling back to any other address would contradict them.\n * - a domain WE set up, currently in use. It drops to the shared pool, so\n * all of its mail carries on, on an address whose reputation is shared.\n * - a domain nothing is sending as. The claim and the key go; no mail moves.\n *\n * Printing the harshest of the three for all of them would be the surface\n * warning about a consequence that is not going to happen, which is the same\n * failure as printing the gentlest.\n */\nexport function describeSendingDomainRemoval(input: {\n domain: string\n /** The domain this site currently sends as, as the route reported it. */\n selected?: string | null\n /** The site's own platform-provisioned domain, or `''` when it has none. */\n platformDomain?: string | null\n}): { title: string; description: string; confirmationText: string } {\n const domain = String(input?.domain ?? '')\n const inUse = Boolean(domain) && String(input?.selected ?? '') === domain\n const ours = Boolean(domain) && String(input?.platformDomain ?? '') === domain\n\n return {\n title: `Remove ${domain}?`,\n description: !inUse\n ? 'The claim and the signing key are dropped. The DNS records stay in ' +\n 'your zone — nothing is changed at your registrar — and you can add ' +\n 'the domain again later, which issues a new key.'\n : ours\n ? `This site is currently sending as ${domain}. Removing it moves ` +\n 'all of this site’s email back to the shared address, whose ' +\n 'delivery reputation is pooled with the other sites on it — so ' +\n 'campaigns there are held to tighter complaint and bounce limits. ' +\n 'Nothing in your own DNS is involved — we published these records ' +\n 'and we remove them.'\n : `This site is currently sending as ${domain}. Removing the domain ` +\n 'does not move it onto another address — not the one this site is ' +\n 'issued, and not the shared address. It stops this site sending at ' +\n 'all, receipts included, until you choose another identity. The ' +\n 'DNS records stay in your zone; nothing is changed at your ' +\n 'registrar.',\n confirmationText: 'Remove domain',\n }\n}\n\n/** One site that sends as a domain, as the organization's page read it. */\nexport interface SendingDomainSite {\n /** The site's name, as a person reads it. */\n name: string\n /**\n * True when the domain is the one Aglyn issued to THIS site — its\n * `platformDomain` — rather than a domain the customer owns.\n */\n issued: boolean\n}\n\n/** `A`, `A and B`, `A, B and C`. */\nfunction listNames(names: readonly string[]): string {\n if (names.length <= 1) return names[0] ?? ''\n return `${names.slice(0, -1).join(', ')} and ${names[names.length - 1]}`\n}\n\n/**\n * THE SAME THREE ANSWERS, asked over the ORGANIZATION rather than from one\n * site's page.\n *\n * On the organization's page a domain can be what several sites send as, and\n * the confirmation has to name them: \"this site\" there is nobody. The three\n * cases are {@link describeSendingDomainRemoval}'s, per site — a site standing\n * on the domain issued to it drops to the shared pool, a site on a domain the\n * customer owns stops sending, and when nothing sends as it only the claim\n * goes.\n *\n * `unchecked` is the number of sites the page did not read. The page reads a\n * batch of sites at a time, so \"nothing sends as it\" can only be said of the\n * sites it read, and the sentence says so rather than overstating it.\n */\nexport function describeOrgSendingDomainRemoval(input: {\n domain: string\n /** The sites, of those read, that send as this domain. */\n sites: readonly SendingDomainSite[]\n /** Sites the page has not read, so cannot say anything about. */\n unchecked?: number\n}): { title: string; description: string; confirmationText: string } {\n const domain = String(input?.domain ?? '')\n const sites = input?.sites ?? []\n const issued = sites.filter((site) => site.issued).map((site) => site.name)\n const owned = sites.filter((site) => !site.issued).map((site) => site.name)\n const unchecked = Math.max(0, Number(input?.unchecked ?? 0))\n\n const parts: string[] = []\n if (!sites.length) {\n parts.push(\n 'The claim and the signing key are dropped. The DNS records stay in ' +\n 'your zone — nothing is changed at your registrar — and you can add ' +\n 'the domain again later, which issues a new key.',\n )\n }\n if (issued.length) {\n parts.push(\n `${listNames(issued)} ${issued.length === 1 ? 'is' : 'are'} currently ` +\n `sending as ${domain}, the domain issued to ` +\n `${issued.length === 1 ? 'it' : 'each'}. Removing it moves all of ` +\n `that email back to the shared address, whose delivery reputation is ` +\n 'pooled with the other sites on it — so campaigns there are held to ' +\n 'tighter complaint and bounce limits. Nothing in your own DNS is ' +\n 'involved — we published these records and we remove them.',\n )\n }\n if (owned.length) {\n const one = owned.length === 1\n parts.push(\n `${listNames(owned)} ${one ? 'is' : 'are'} currently sending as ` +\n `${domain}. Removing the domain does not move ${one ? 'it' : 'them'} ` +\n 'onto another address — not the one each site is issued, and not the ' +\n `shared address. It stops ${one ? 'that site' : 'those sites'} ` +\n `sending at all, receipts included, until ${\n one ? 'it is' : 'each is'\n } given another identity. The DNS records stay in your zone; nothing ` +\n 'is changed at your registrar.',\n )\n }\n if (unchecked) {\n parts.push(\n unchecked === 1\n ? 'One site not shown on this page was not checked; if it sends as ' +\n 'this domain, it is affected the same way.'\n : `${unchecked} sites not shown on this page were not checked; any ` +\n 'of them sending as this domain is affected the same way.',\n )\n }\n\n return {\n title: `Remove ${domain}?`,\n description: parts.join(' '),\n confirmationText: 'Remove domain',\n }\n}\n\n"],"names":["SENDING_DOMAIN_AT_CAPACITY","describeSendingDomain","input","status","label","color","severity","text","sending","missing","length","join","String","issueError","pendingProvider","INCONCLUSIVE_CHECK","describeSendingDomainRemoval","domain","inUse","Boolean","selected","ours","platformDomain","title","description","confirmationText","listNames","names","slice","describeOrgSendingDomainRemoval","sites","issued","filter","site","map","name","owned","unchecked","Math","max","Number","parts","push","one"],"mappings":"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"}
|
package/src/lib/plugin.js
CHANGED
|
@@ -50,7 +50,7 @@ const EmailDesignPreview = lazy(()=>import("./components/email-design-preview.js
|
|
|
50
50
|
label: 'The Emails page’s Messages section',
|
|
51
51
|
surface: 'console',
|
|
52
52
|
layout: 'bare',
|
|
53
|
-
description: 'The whole body of `/emails/messages` and the routes under it. A widget here is handed the site, the Emails page’s base path and the segments under `messages`, and draws the list, one message’s report or its composer.'
|
|
53
|
+
description: 'The whole body of `/emails/messages` and the routes under it, under a site and on the organization’s Emails page. A widget here is handed the site (or `null` and the org mount at the organization level), the Emails page’s base path and the segments under `messages`, and draws the list, one message’s report or its composer.'
|
|
54
54
|
}, {
|
|
55
55
|
pluginId: BUNDLE_ID
|
|
56
56
|
});
|
|
@@ -155,6 +155,34 @@ const EmailDesignPreview = lazy(()=>import("./components/email-design-preview.js
|
|
|
155
155
|
},
|
|
156
156
|
Component: EmailsConsolePage
|
|
157
157
|
}
|
|
158
|
+
],
|
|
159
|
+
/*
|
|
160
|
+
* The ORGANIZATION's Emails page, at `/[orgSlug]/emails`: the same six
|
|
161
|
+
* sections over every site. The audiences and the topics are the org's
|
|
162
|
+
* already; the messages, the templates, the sending identities and the
|
|
163
|
+
* suppression lists are read site by site and link into the site that
|
|
164
|
+
* holds them. The page is the same component — handed no site, it renders
|
|
165
|
+
* the org sections.
|
|
166
|
+
*
|
|
167
|
+
* No `navTabId`, the same as the site tab: this plugin's release flag
|
|
168
|
+
* gates the plugin itself, so one flag already holds both levels.
|
|
169
|
+
*/ orgNavItems: [
|
|
170
|
+
{
|
|
171
|
+
label: 'Emails',
|
|
172
|
+
href: '/emails',
|
|
173
|
+
icon: {
|
|
174
|
+
path: mdiEmailOutline.path
|
|
175
|
+
},
|
|
176
|
+
sections: EMAILS_CONSOLE_SECTIONS,
|
|
177
|
+
header: {
|
|
178
|
+
title: 'Emails',
|
|
179
|
+
icon: {
|
|
180
|
+
path: mdiEmailOutline.path
|
|
181
|
+
},
|
|
182
|
+
docsTopic: 'emailCampaigns'
|
|
183
|
+
},
|
|
184
|
+
Component: EmailsConsolePage
|
|
185
|
+
}
|
|
158
186
|
]
|
|
159
187
|
});
|
|
160
188
|
}
|
package/src/lib/plugin.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/plugins/email/src/lib/plugin.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 * as Aglyn from '@aglyn/aglyn'\nimport { registerPluginZone } from '@aglyn/aglyn/plugin-manager/plugin-zones'\nimport { mdiEmailOutline } from '@aglyn/shared-data-mdi'\nimport { lazy } from 'react'\nimport {\n EMAIL_MESSAGES_ZONE,\n EMAIL_TEMPLATE_RECIPIENTS_ZONE,\n} from './components/email-zones'\nimport { EMAILS_CONSOLE_SECTIONS } from './components/emails-console-sections'\nimport { BUNDLE_ID } from './constants/bundle-common'\n\n/** Code-split: the Emails console page only loads when opened. */\nconst EmailsConsolePage = lazy(() => import('./components/emails-console-page'))\n\n/*\n * What this plugin draws inside a campaign's own pages, each loaded only where\n * the campaign owner's zone is on screen.\n */\nconst CampaignTopicSelect = lazy(\n () => import('./components/campaign-topic-select'),\n)\nconst CampaignTopicOptionsWidget = lazy(\n () => import('./components/campaign-topic-options-widget'),\n)\nconst CampaignSenderEditorWidget = lazy(\n () => import('./components/campaign-sender-editor-widget'),\n)\nconst CampaignDesignCreateWidget = lazy(\n () => import('./components/campaign-design-create-widget'),\n)\nconst EmailDesignPreview = lazy(\n () => import('./components/email-design-preview'),\n)\n\n/**\n * Console half (AGL-395): registers the Emails nav item + page in the\n * ConsoleExtension registry. Safe to call at console app load — the page is\n * lazy, so no besigner/canvas code loads. The shell renders the Emails nav\n * item and, via its generic plugin route, the page (the messages and their\n * composer, the templates, the audience lists, the topic catalog, the sending\n * identities and the suppression list) — with no edit to the console's own\n * nav or page files.\n */\nexport function registerEmailConsole(): void {\n /*\n * The two places this plugin's pages hand over to whichever plugin owns\n * campaigns. A message is one send of a campaign and every action on it is\n * that plugin's route, so the Messages section is a zone this page hosts\n * rather than pages this plugin imports; the same holds for the recipients\n * table under a template's report. Named, because a spec calls this\n * registrar without the loader.\n */\n registerPluginZone(\n {\n zone: EMAIL_MESSAGES_ZONE,\n label: 'The Emails page’s Messages section',\n surface: 'console',\n layout: 'bare',\n description:\n 'The whole body of `/emails/messages` and the routes under it. A widget here is handed the site, the Emails page’s base path and the segments under `messages`, and draws the list, one message’s report or its composer.',\n },\n { pluginId: BUNDLE_ID },\n )\n registerPluginZone(\n {\n zone: EMAIL_TEMPLATE_RECIPIENTS_ZONE,\n label: 'Who received a template’s emails',\n surface: 'console',\n description:\n 'On one template’s page, under its report. A widget here lists the recipients of every send built from that template; it is handed the site and the template’s screen id.',\n },\n { pluginId: BUNDLE_ID },\n )\n Aglyn.registerConsoleExtension({\n pluginId: BUNDLE_ID,\n displayName: 'Email',\n /*\n * WHO may open the email console, declared so the shell enforces it.\n *\n * The audiences section reads `orgs/{orgId}/lists/{listId}/members`, and\n * those members are enrolled CONTACTS — an address, a name, and the\n * consent basis recording why the person may be mailed. That is the same\n * org-shared people data the CRM holds, reached from a different page.\n *\n * The rules gate those reads on `isOrgWideMember()` ALONE, with no role\n * condition, so org-wide membership of any role is enough to list every\n * audience the organization has and everybody on it. An org VIEWER — the\n * role that exists to read and change nothing — therefore reads the whole\n * marketing audience today. `data.manage` is what closes that: it\n * defaults to owner, admin and editor, so the population it admits is\n * exactly the one `server-list-gate.ts` accepts a list write from, and\n * the viewer it excludes is the reader the rules never excluded.\n *\n * `data.manage` rather than a key of this plugin's own for the reason the\n * catalog gives for refusing a `marketing.manage`: campaigns are written\n * client-direct against rules that gate on the HOST role, so a new\n * org-level key would name an action with no org-level boundary under it.\n * `data.manage` is not in that position — it already governs the\n * org-shared data this surface exposes, and the list gate already reads\n * the roles it defaults to.\n *\n * A SITE COLLABORATOR holding it opens the page, and the answer is NOT\n * the one Contacts reached. There the listener is scoped and the rules\n * prove the same predicate per document; here the audiences read demands\n * `isOrgWideMember()`, which a collaborator is not, so the org-shared\n * half is already refused beneath the console and the half that remains —\n * this site's own messages, templates and sending identities — is theirs.\n * Refusing the surface outright would take that away to close nothing.\n */\n permission: 'data.manage',\n /*\n * The mail a campaign rides on is this plugin's — the topic catalog, the\n * sending identities, the design document and its renderer — so each is\n * drawn here, in a zone the campaign owner's composer and message page\n * host. Every one reports through a callback; none writes a campaign.\n */\n widgets: [\n {\n slot: 'campaignTopicSelect',\n widgetId: 'email-campaign-topic-select',\n title: 'Topic',\n Component: CampaignTopicSelect,\n },\n {\n slot: 'campaignTopicOptions',\n widgetId: 'email-campaign-topic-options',\n title: 'Topics',\n Component: CampaignTopicOptionsWidget,\n },\n {\n slot: 'campaignSenderEditor',\n widgetId: 'email-campaign-sender-editor',\n title: 'Add a sender',\n Component: CampaignSenderEditorWidget,\n },\n {\n slot: 'campaignDesignCreate',\n widgetId: 'email-campaign-design-create',\n title: 'Design this email',\n Component: CampaignDesignCreateWidget,\n },\n {\n slot: 'campaignDesignPreview',\n widgetId: 'email-campaign-design-preview',\n title: 'Preview',\n Component: EmailDesignPreview,\n },\n ],\n navItems: [\n {\n label: 'Emails',\n href: '/emails',\n icon: { path: mdiEmailOutline.path },\n // Sections as ROUTES (AGL-2501): `/emails/messages` and friends are\n // real URLs the shell resolves and gates, so the page mounts the one\n // being read instead of subscribing all six.\n sections: EMAILS_CONSOLE_SECTIONS,\n header: {\n title: 'Emails',\n icon: { path: mdiEmailOutline.path },\n docsTopic: 'emailCampaigns',\n },\n Component: EmailsConsolePage,\n },\n ],\n })\n}\n\nexport * from './site'\n"],"names":["Aglyn","registerPluginZone","mdiEmailOutline","lazy","EMAIL_MESSAGES_ZONE","EMAIL_TEMPLATE_RECIPIENTS_ZONE","EMAILS_CONSOLE_SECTIONS","BUNDLE_ID","EmailsConsolePage","CampaignTopicSelect","CampaignTopicOptionsWidget","CampaignSenderEditorWidget","CampaignDesignCreateWidget","EmailDesignPreview","registerEmailConsole","zone","label","surface","layout","description","pluginId","registerConsoleExtension","displayName","permission","widgets","slot","widgetId","title","Component","navItems","href","icon","path","sections","header","docsTopic"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,YAAYA,WAAW,eAAc;AACrC,SAASC,kBAAkB,QAAQ,2CAA0C;AAC7E,SAASC,eAAe,QAAQ,yBAAwB;AACxD,SAASC,IAAI,QAAQ,QAAO;AAC5B,SACEC,mBAAmB,EACnBC,8BAA8B,QACzB,8BAA0B;AACjC,SAASC,uBAAuB,QAAQ,0CAAsC;AAC9E,SAASC,SAAS,QAAQ,+BAA2B;AAErD,gEAAgE,GAChE,MAAMC,oBAAoBL,KAAK,IAAM,MAAM,CAAC;AAE5C;;;CAGC,GACD,MAAMM,sBAAsBN,KAC1B,IAAM,MAAM,CAAC;AAEf,MAAMO,6BAA6BP,KACjC,IAAM,MAAM,CAAC;AAEf,MAAMQ,6BAA6BR,KACjC,IAAM,MAAM,CAAC;AAEf,MAAMS,6BAA6BT,KACjC,IAAM,MAAM,CAAC;AAEf,MAAMU,qBAAqBV,KACzB,IAAM,MAAM,CAAC;AAGf;;;;;;;;CAQC,GACD,OAAO,SAASW;IACd;;;;;;;GAOC,GACDb,mBACE;QACEc,MAAMX;QACNY,OAAO;QACPC,SAAS;QACTC,QAAQ;QACRC,aACE;IACJ,GACA;QAAEC,UAAUb;IAAU;IAExBN,mBACE;QACEc,MAAMV;QACNW,OAAO;QACPC,SAAS;QACTE,aACE;IACJ,GACA;QAAEC,UAAUb;IAAU;IAExBP,MAAMqB,wBAAwB,CAAC;QAC7BD,UAAUb;QACVe,aAAa;QACb;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAgCC,GACDC,YAAY;QACZ;;;;;KAKC,GACDC,SAAS;YACP;gBACEC,MAAM;gBACNC,UAAU;gBACVC,OAAO;gBACPC,WAAWnB;YACb;YACA;gBACEgB,MAAM;gBACNC,UAAU;gBACVC,OAAO;gBACPC,WAAWlB;YACb;YACA;gBACEe,MAAM;gBACNC,UAAU;gBACVC,OAAO;gBACPC,WAAWjB;YACb;YACA;gBACEc,MAAM;gBACNC,UAAU;gBACVC,OAAO;gBACPC,WAAWhB;YACb;YACA;gBACEa,MAAM;gBACNC,UAAU;gBACVC,OAAO;gBACPC,WAAWf;YACb;SACD;QACDgB,UAAU;YACR;gBACEb,OAAO;gBACPc,MAAM;gBACNC,MAAM;oBAAEC,MAAM9B,gBAAgB8B,IAAI;gBAAC;gBACnC,oEAAoE;gBACpE,qEAAqE;gBACrE,6CAA6C;gBAC7CC,UAAU3B;gBACV4B,QAAQ;oBACNP,OAAO;oBACPI,MAAM;wBAAEC,MAAM9B,gBAAgB8B,IAAI;oBAAC;oBACnCG,WAAW;gBACb;gBACAP,WAAWpB;YACb;SACD;IACH;AACF;AAEA,cAAc,YAAQ"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/plugins/email/src/lib/plugin.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 * as Aglyn from '@aglyn/aglyn'\nimport { registerPluginZone } from '@aglyn/aglyn/plugin-manager/plugin-zones'\nimport { mdiEmailOutline } from '@aglyn/shared-data-mdi'\nimport { lazy } from 'react'\nimport {\n EMAIL_MESSAGES_ZONE,\n EMAIL_TEMPLATE_RECIPIENTS_ZONE,\n} from './components/email-zones'\nimport { EMAILS_CONSOLE_SECTIONS } from './components/emails-console-sections'\nimport { BUNDLE_ID } from './constants/bundle-common'\n\n/** Code-split: the Emails console page only loads when opened. */\nconst EmailsConsolePage = lazy(() => import('./components/emails-console-page'))\n\n/*\n * What this plugin draws inside a campaign's own pages, each loaded only where\n * the campaign owner's zone is on screen.\n */\nconst CampaignTopicSelect = lazy(\n () => import('./components/campaign-topic-select'),\n)\nconst CampaignTopicOptionsWidget = lazy(\n () => import('./components/campaign-topic-options-widget'),\n)\nconst CampaignSenderEditorWidget = lazy(\n () => import('./components/campaign-sender-editor-widget'),\n)\nconst CampaignDesignCreateWidget = lazy(\n () => import('./components/campaign-design-create-widget'),\n)\nconst EmailDesignPreview = lazy(\n () => import('./components/email-design-preview'),\n)\n\n/**\n * Console half (AGL-395): registers the Emails nav item + page in the\n * ConsoleExtension registry. Safe to call at console app load — the page is\n * lazy, so no besigner/canvas code loads. The shell renders the Emails nav\n * item and, via its generic plugin route, the page (the messages and their\n * composer, the templates, the audience lists, the topic catalog, the sending\n * identities and the suppression list) — with no edit to the console's own\n * nav or page files.\n */\nexport function registerEmailConsole(): void {\n /*\n * The two places this plugin's pages hand over to whichever plugin owns\n * campaigns. A message is one send of a campaign and every action on it is\n * that plugin's route, so the Messages section is a zone this page hosts\n * rather than pages this plugin imports; the same holds for the recipients\n * table under a template's report. Named, because a spec calls this\n * registrar without the loader.\n */\n registerPluginZone(\n {\n zone: EMAIL_MESSAGES_ZONE,\n label: 'The Emails page’s Messages section',\n surface: 'console',\n layout: 'bare',\n description:\n 'The whole body of `/emails/messages` and the routes under it, under a site and on the organization’s Emails page. A widget here is handed the site (or `null` and the org mount at the organization level), the Emails page’s base path and the segments under `messages`, and draws the list, one message’s report or its composer.',\n },\n { pluginId: BUNDLE_ID },\n )\n registerPluginZone(\n {\n zone: EMAIL_TEMPLATE_RECIPIENTS_ZONE,\n label: 'Who received a template’s emails',\n surface: 'console',\n description:\n 'On one template’s page, under its report. A widget here lists the recipients of every send built from that template; it is handed the site and the template’s screen id.',\n },\n { pluginId: BUNDLE_ID },\n )\n Aglyn.registerConsoleExtension({\n pluginId: BUNDLE_ID,\n displayName: 'Email',\n /*\n * WHO may open the email console, declared so the shell enforces it.\n *\n * The audiences section reads `orgs/{orgId}/lists/{listId}/members`, and\n * those members are enrolled CONTACTS — an address, a name, and the\n * consent basis recording why the person may be mailed. That is the same\n * org-shared people data the CRM holds, reached from a different page.\n *\n * The rules gate those reads on `isOrgWideMember()` ALONE, with no role\n * condition, so org-wide membership of any role is enough to list every\n * audience the organization has and everybody on it. An org VIEWER — the\n * role that exists to read and change nothing — therefore reads the whole\n * marketing audience today. `data.manage` is what closes that: it\n * defaults to owner, admin and editor, so the population it admits is\n * exactly the one `server-list-gate.ts` accepts a list write from, and\n * the viewer it excludes is the reader the rules never excluded.\n *\n * `data.manage` rather than a key of this plugin's own for the reason the\n * catalog gives for refusing a `marketing.manage`: campaigns are written\n * client-direct against rules that gate on the HOST role, so a new\n * org-level key would name an action with no org-level boundary under it.\n * `data.manage` is not in that position — it already governs the\n * org-shared data this surface exposes, and the list gate already reads\n * the roles it defaults to.\n *\n * A SITE COLLABORATOR holding it opens the page, and the answer is NOT\n * the one Contacts reached. There the listener is scoped and the rules\n * prove the same predicate per document; here the audiences read demands\n * `isOrgWideMember()`, which a collaborator is not, so the org-shared\n * half is already refused beneath the console and the half that remains —\n * this site's own messages, templates and sending identities — is theirs.\n * Refusing the surface outright would take that away to close nothing.\n */\n permission: 'data.manage',\n /*\n * The mail a campaign rides on is this plugin's — the topic catalog, the\n * sending identities, the design document and its renderer — so each is\n * drawn here, in a zone the campaign owner's composer and message page\n * host. Every one reports through a callback; none writes a campaign.\n */\n widgets: [\n {\n slot: 'campaignTopicSelect',\n widgetId: 'email-campaign-topic-select',\n title: 'Topic',\n Component: CampaignTopicSelect,\n },\n {\n slot: 'campaignTopicOptions',\n widgetId: 'email-campaign-topic-options',\n title: 'Topics',\n Component: CampaignTopicOptionsWidget,\n },\n {\n slot: 'campaignSenderEditor',\n widgetId: 'email-campaign-sender-editor',\n title: 'Add a sender',\n Component: CampaignSenderEditorWidget,\n },\n {\n slot: 'campaignDesignCreate',\n widgetId: 'email-campaign-design-create',\n title: 'Design this email',\n Component: CampaignDesignCreateWidget,\n },\n {\n slot: 'campaignDesignPreview',\n widgetId: 'email-campaign-design-preview',\n title: 'Preview',\n Component: EmailDesignPreview,\n },\n ],\n navItems: [\n {\n label: 'Emails',\n href: '/emails',\n icon: { path: mdiEmailOutline.path },\n // Sections as ROUTES (AGL-2501): `/emails/messages` and friends are\n // real URLs the shell resolves and gates, so the page mounts the one\n // being read instead of subscribing all six.\n sections: EMAILS_CONSOLE_SECTIONS,\n header: {\n title: 'Emails',\n icon: { path: mdiEmailOutline.path },\n docsTopic: 'emailCampaigns',\n },\n Component: EmailsConsolePage,\n },\n ],\n /*\n * The ORGANIZATION's Emails page, at `/[orgSlug]/emails`: the same six\n * sections over every site. The audiences and the topics are the org's\n * already; the messages, the templates, the sending identities and the\n * suppression lists are read site by site and link into the site that\n * holds them. The page is the same component — handed no site, it renders\n * the org sections.\n *\n * No `navTabId`, the same as the site tab: this plugin's release flag\n * gates the plugin itself, so one flag already holds both levels.\n */\n orgNavItems: [\n {\n label: 'Emails',\n href: '/emails',\n icon: { path: mdiEmailOutline.path },\n sections: EMAILS_CONSOLE_SECTIONS,\n header: {\n title: 'Emails',\n icon: { path: mdiEmailOutline.path },\n docsTopic: 'emailCampaigns',\n },\n Component: EmailsConsolePage,\n },\n ],\n })\n}\n\nexport * from './site'\n"],"names":["Aglyn","registerPluginZone","mdiEmailOutline","lazy","EMAIL_MESSAGES_ZONE","EMAIL_TEMPLATE_RECIPIENTS_ZONE","EMAILS_CONSOLE_SECTIONS","BUNDLE_ID","EmailsConsolePage","CampaignTopicSelect","CampaignTopicOptionsWidget","CampaignSenderEditorWidget","CampaignDesignCreateWidget","EmailDesignPreview","registerEmailConsole","zone","label","surface","layout","description","pluginId","registerConsoleExtension","displayName","permission","widgets","slot","widgetId","title","Component","navItems","href","icon","path","sections","header","docsTopic","orgNavItems"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,YAAYA,WAAW,eAAc;AACrC,SAASC,kBAAkB,QAAQ,2CAA0C;AAC7E,SAASC,eAAe,QAAQ,yBAAwB;AACxD,SAASC,IAAI,QAAQ,QAAO;AAC5B,SACEC,mBAAmB,EACnBC,8BAA8B,QACzB,8BAA0B;AACjC,SAASC,uBAAuB,QAAQ,0CAAsC;AAC9E,SAASC,SAAS,QAAQ,+BAA2B;AAErD,gEAAgE,GAChE,MAAMC,oBAAoBL,KAAK,IAAM,MAAM,CAAC;AAE5C;;;CAGC,GACD,MAAMM,sBAAsBN,KAC1B,IAAM,MAAM,CAAC;AAEf,MAAMO,6BAA6BP,KACjC,IAAM,MAAM,CAAC;AAEf,MAAMQ,6BAA6BR,KACjC,IAAM,MAAM,CAAC;AAEf,MAAMS,6BAA6BT,KACjC,IAAM,MAAM,CAAC;AAEf,MAAMU,qBAAqBV,KACzB,IAAM,MAAM,CAAC;AAGf;;;;;;;;CAQC,GACD,OAAO,SAASW;IACd;;;;;;;GAOC,GACDb,mBACE;QACEc,MAAMX;QACNY,OAAO;QACPC,SAAS;QACTC,QAAQ;QACRC,aACE;IACJ,GACA;QAAEC,UAAUb;IAAU;IAExBN,mBACE;QACEc,MAAMV;QACNW,OAAO;QACPC,SAAS;QACTE,aACE;IACJ,GACA;QAAEC,UAAUb;IAAU;IAExBP,MAAMqB,wBAAwB,CAAC;QAC7BD,UAAUb;QACVe,aAAa;QACb;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAgCC,GACDC,YAAY;QACZ;;;;;KAKC,GACDC,SAAS;YACP;gBACEC,MAAM;gBACNC,UAAU;gBACVC,OAAO;gBACPC,WAAWnB;YACb;YACA;gBACEgB,MAAM;gBACNC,UAAU;gBACVC,OAAO;gBACPC,WAAWlB;YACb;YACA;gBACEe,MAAM;gBACNC,UAAU;gBACVC,OAAO;gBACPC,WAAWjB;YACb;YACA;gBACEc,MAAM;gBACNC,UAAU;gBACVC,OAAO;gBACPC,WAAWhB;YACb;YACA;gBACEa,MAAM;gBACNC,UAAU;gBACVC,OAAO;gBACPC,WAAWf;YACb;SACD;QACDgB,UAAU;YACR;gBACEb,OAAO;gBACPc,MAAM;gBACNC,MAAM;oBAAEC,MAAM9B,gBAAgB8B,IAAI;gBAAC;gBACnC,oEAAoE;gBACpE,qEAAqE;gBACrE,6CAA6C;gBAC7CC,UAAU3B;gBACV4B,QAAQ;oBACNP,OAAO;oBACPI,MAAM;wBAAEC,MAAM9B,gBAAgB8B,IAAI;oBAAC;oBACnCG,WAAW;gBACb;gBACAP,WAAWpB;YACb;SACD;QACD;;;;;;;;;;KAUC,GACD4B,aAAa;YACX;gBACEpB,OAAO;gBACPc,MAAM;gBACNC,MAAM;oBAAEC,MAAM9B,gBAAgB8B,IAAI;gBAAC;gBACnCC,UAAU3B;gBACV4B,QAAQ;oBACNP,OAAO;oBACPI,MAAM;wBAAEC,MAAM9B,gBAAgB8B,IAAI;oBAAC;oBACnCG,WAAW;gBACb;gBACAP,WAAWpB;YACb;SACD;IACH;AACF;AAEA,cAAc,YAAQ"}
|