@aglyn/plugins-email 1.0.0-beta.220 → 1.0.0-beta.222
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 +5 -4
- package/package.json +16 -17
- package/src/lib/components/email-template-detail.d.ts +5 -13
- package/src/lib/components/email-template-detail.js +29 -426
- package/src/lib/components/email-template-detail.js.map +1 -1
- package/src/lib/components/email-zones.d.ts +15 -0
- package/src/lib/components/email-zones.js +1 -0
- package/src/lib/components/email-zones.js.map +1 -1
- package/src/lib/components/list-detail-card.d.ts +0 -36
- package/src/lib/components/list-detail-card.js +125 -84
- package/src/lib/components/list-detail-card.js.map +1 -1
- package/src/lib/components/list-members-panel.d.ts +17 -0
- package/src/lib/components/list-members-panel.js +14 -0
- package/src/lib/components/list-members-panel.js.map +1 -1
- package/src/lib/components/site-package-email-preview.d.ts +13 -0
- package/src/lib/components/site-package-email-preview.js +47 -0
- package/src/lib/components/site-package-email-preview.js.map +1 -0
- package/src/lib/components/suppressions-card.js +75 -6
- package/src/lib/components/suppressions-card.js.map +1 -1
- package/src/lib/components/use-org-email-topics.js +2 -9
- package/src/lib/components/use-org-email-topics.js.map +1 -1
- package/src/lib/constants/site-package.d.ts +21 -0
- package/src/lib/constants/site-package.js +21 -0
- package/src/lib/constants/site-package.js.map +1 -0
- package/src/lib/declarations.console-server.d.ts +12 -0
- package/src/lib/declarations.console-server.js +73 -0
- package/src/lib/declarations.console-server.js.map +1 -1
- package/src/lib/list-import.d.ts +64 -75
- package/src/lib/list-import.js +43 -158
- package/src/lib/list-import.js.map +1 -1
- package/src/lib/model/email-topic-catalog.d.ts +24 -0
- package/src/lib/model/email-topic-catalog.js +19 -0
- package/src/lib/model/email-topic-catalog.js.map +1 -1
- package/src/lib/model/index.d.ts +0 -25
- package/src/lib/model/index.js +0 -27
- package/src/lib/model/index.js.map +1 -1
- package/src/lib/plugin.d.ts +8 -0
- package/src/lib/plugin.js +76 -2
- package/src/lib/plugin.js.map +1 -1
- package/src/lib/server/person-eraser.d.ts +39 -0
- package/src/lib/server/person-eraser.js +44 -0
- package/src/lib/server/person-eraser.js.map +1 -0
- package/src/lib/server-console.js +9 -9
- package/src/lib/server-console.js.map +1 -1
- package/src/lib/server-list-gate.d.ts +7 -0
- package/src/lib/server-list-gate.js +10 -3
- package/src/lib/server-list-gate.js.map +1 -1
- package/src/lib/server-list-import.d.ts +20 -177
- package/src/lib/server-list-import.js +34 -594
- package/src/lib/server-list-import.js.map +1 -1
- package/src/lib/server.js +16 -29
- package/src/lib/server.js.map +1 -1
- package/src/lib/testing/stand-in-send-tally.d.ts +29 -0
- package/src/lib/testing/stand-in-send-tally.js +52 -0
- package/src/lib/testing/stand-in-send-tally.js.map +1 -0
- package/src/lib/transfer/email-transfer-catalog.d.ts +151 -0
- package/src/lib/transfer/email-transfer-catalog.js +536 -0
- package/src/lib/transfer/email-transfer-catalog.js.map +1 -0
- package/src/lib/transfer/list-import-steps.d.ts +44 -0
- package/src/lib/transfer/list-import-steps.js +201 -0
- package/src/lib/transfer/list-import-steps.js.map +1 -0
- package/src/lib/transfer/list-members.server.d.ts +165 -0
- package/src/lib/transfer/list-members.server.js +911 -0
- package/src/lib/transfer/list-members.server.js.map +1 -0
- package/src/lib/transfer/suppressions.server.d.ts +69 -0
- package/src/lib/transfer/suppressions.server.js +282 -0
- package/src/lib/transfer/suppressions.server.js.map +1 -0
- package/src/lib/transfer/topics-package.d.ts +56 -0
- package/src/lib/transfer/topics-package.js +87 -0
- package/src/lib/transfer/topics-package.js.map +1 -0
- package/src/lib/transfer/topics-package.server.d.ts +33 -0
- package/src/lib/transfer/topics-package.server.js +171 -0
- package/src/lib/transfer/topics-package.server.js.map +1 -0
- package/src/lib/transfer/transfer-reads.server.d.ts +25 -0
- package/src/lib/transfer/transfer-reads.server.js +34 -0
- package/src/lib/transfer/transfer-reads.server.js.map +1 -0
- package/src/lib/components/list-import-drawer.d.ts +0 -22
- package/src/lib/components/list-import-drawer.js +0 -662
- package/src/lib/components/list-import-drawer.js.map +0 -1
- package/src/lib/model/template-report.d.ts +0 -158
- package/src/lib/model/template-report.js +0 -249
- package/src/lib/model/template-report.js.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/email/src/lib/transfer/email-transfer-catalog.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 THE EMAIL PLUGIN IMPORTS AND EXPORTS (AGL-3529) — the half both the\n * server and the console read: the resource keys, every field, the match\n * keys, the header spellings, the locked rules and the answers of the\n * plugin's own wizard steps.\n *\n * Two resources, both a site's:\n *\n * - `email.list-members` — one list's members, one list at a time\n * (`instances`: `email.list-members:<listId>`): an import is INTO one list\n * and an export is OF one, and the console opens both from the list's page.\n * - `email.suppressions` — the site's own suppression list. An import only\n * ever ADDS: an address already on the list is left exactly as it is, and\n * nothing — the import, its undo — ever removes one.\n *\n * Types only from the transfer core, so the console registrar that reads\n * this costs no transfer code.\n */\n\nimport type {\n MatchKeySpec,\n TransferAliasDictionary,\n TransferCatalogInput,\n TransferField,\n TransferLockedRule,\n} from '@aglyn/aglyn/data-transfer'\nimport {\n LIST_IMPORT_EMAIL_COLUMNS,\n LIST_IMPORT_NAME_COLUMNS,\n LIST_IMPORT_OPT_IN_DATE_COLUMNS,\n LIST_IMPORT_OPT_IN_SOURCE_COLUMNS,\n} from '../list-import'\n\n/**\n * One list's members, imported into and exported from that list: declared\n * with `instances`, and reached per list as `email.list-members:<listId>`, so\n * a job, a person's remembered export fields and the one-running-import rule\n * each belong to one list.\n */\nexport const LIST_MEMBERS_RESOURCE = 'email.list-members'\n\n/**\n * The resource key one list's members move under — the core's instance key\n * (`transferResourceInstanceKey`), spelled out so the list's page names it\n * without loading the transfer core; `email-transfer-catalog.spec.ts` holds\n * the two to the same answer.\n */\nexport function listMembersResourceKey(listId: string): string {\n return `${LIST_MEMBERS_RESOURCE}:${listId}`\n}\n\n/** The list a list-member resource key names, or `null` for any other key. */\nexport function listIdOfResourceKey(key: string): string | null {\n const prefix = `${LIST_MEMBERS_RESOURCE}:`\n const listId = String(key ?? '').startsWith(prefix) ? String(key).slice(prefix.length) : ''\n return /^[A-Za-z0-9_-]{1,128}$/.test(listId) ? listId : null\n}\n\n/** A site's suppression list. */\nexport const SUPPRESSIONS_RESOURCE = 'email.suppressions'\n\n/*==========================================\n * THE PLUGIN'S OWN WIZARD STEPS\n *=========================================*/\n\n/**\n * \"People already on this list\" — after Matching: whether the file may\n * change the contact details of people the workspace already holds.\n */\nexport const LIST_EXISTING_STEP_ID = 'email-existing'\n\n/** \"Permission\" — after Conflicts: the statement of permission, with the screening. */\nexport const LIST_CONSENT_STEP_ID = 'email-consent'\n\n/** The answer of the {@link LIST_EXISTING_STEP_ID} step. */\nexport interface ListExistingAnswer {\n /**\n * The file's contact columns may change the details of a person the\n * workspace already holds, under the field choices made in Conflicts.\n * Otherwise they fill only a contact the import creates, and somebody\n * already on the list is left exactly as they are.\n */\n updateContacts: boolean\n}\n\n/** The answer of the {@link LIST_CONSENT_STEP_ID} step. */\nexport interface ListConsentAnswer {\n /**\n * The operator states they have permission to email everyone in the file.\n * Without it only people with an opt-in on record are added.\n */\n attest: boolean\n}\n\n/**\n * The existing-people answer in `extras`, or the default — leave everybody\n * the workspace holds as they are. Anything but a literal `true` is no.\n */\nexport function readListExistingAnswer(\n extras: Readonly<Record<string, unknown>> | undefined,\n): ListExistingAnswer {\n const value = extras?.[LIST_EXISTING_STEP_ID]\n return {\n updateContacts:\n !!value &&\n typeof value === 'object' &&\n (value as Record<string, unknown>)['updateContacts'] === true,\n }\n}\n\n/**\n * The permission answer in `extras`, or `null` when the step was never\n * answered. Anything but a literal `true` is no statement at all.\n */\nexport function readListConsentAnswer(\n extras: Readonly<Record<string, unknown>> | undefined,\n): ListConsentAnswer | null {\n const value = extras?.[LIST_CONSENT_STEP_ID]\n if (!value || typeof value !== 'object') return null\n const attest = (value as Record<string, unknown>)['attest']\n return typeof attest === 'boolean' ? { attest } : null\n}\n\n/** What the screening found, as the list's import ledger keeps it and the permission step shows it. */\nexport interface ListImportScreeningReport {\n roleAccounts: number\n roleAccountSamples: string[]\n purchaseTellColumns: string[]\n declaresBasis: boolean\n /** The consent gate over the first new addresses — a sample, with its size. */\n sample: { size: number; of: number; optedIn: number; needAttestation: number; refused: number }\n}\n\n/*==========================================\n * LIST MEMBERS\n *=========================================*/\n\n/** A contact-record column's field id: `contact:<the record's own field name>`. */\nexport const CONTACT_FIELD_PREFIX = 'contact:'\n\n/**\n * The contact record's fields a list file may carry, by the record's own\n * names — what the contact-capture door's `profile` takes. Written through\n * that door and never by this plugin (see `list-members.server.ts`).\n */\nexport const LIST_MEMBER_CONTACT_FIELDS: ReadonlyArray<{\n key: string\n label: string\n type: TransferField['type']\n aliases: readonly string[]\n}> = [\n { key: 'firstName', label: 'First name', type: 'text', aliases: ['first name', 'firstname', 'given name', 'fname', 'FNAME'] },\n { key: 'lastName', label: 'Last name', type: 'text', aliases: ['last name', 'lastname', 'surname', 'family name', 'lname', 'LNAME'] },\n { key: 'phone', label: 'Phone', type: 'phone', aliases: ['phone', 'phone number', 'telephone', 'PHONE'] },\n { key: 'mobilePhone', label: 'Mobile phone', type: 'phone', aliases: ['mobile', 'mobile phone', 'cell', 'cell phone'] },\n { key: 'jobTitle', label: 'Job title', type: 'text', aliases: ['title', 'job title', 'position', 'role'] },\n { key: 'companyName', label: 'Company', type: 'text', aliases: ['company', 'company name', 'organization', 'employer'] },\n { key: 'department', label: 'Department', type: 'text', aliases: ['department', 'team'] },\n]\n\n/** The field id of a contact-record column. */\nexport function contactFieldId(key: string): string {\n return `${CONTACT_FIELD_PREFIX}${key}`\n}\n\n/** The contact record's own name for a field id, or `null` for a membership field. */\nexport function contactFieldKey(fieldId: string): string | null {\n return fieldId.startsWith(CONTACT_FIELD_PREFIX)\n ? fieldId.slice(CONTACT_FIELD_PREFIX.length)\n : null\n}\n\n/** Every contact-record field id, in catalog order. */\nexport const LIST_MEMBER_CONTACT_FIELD_IDS: readonly string[] =\n LIST_MEMBER_CONTACT_FIELDS.map((field) => contactFieldId(field.key))\n\n/** The membership fields a file may write on a NEW member. */\nexport const LIST_MEMBER_WRITTEN_FIELDS = ['email', 'name', 'declaredSource', 'declaredAt'] as const\n\n/** The groups, in picker order. */\nconst LIST_MEMBER_GROUPS = [\n { id: 'membership', label: 'Membership' },\n { id: 'evidence', label: 'Where the address came from' },\n { id: 'consent', label: 'Consent' },\n { id: 'contact', label: 'Contact record' },\n]\n\n/** One list's members: every field, for the wizard and the export dialog. */\nexport function listMemberCatalog(): TransferCatalogInput {\n return {\n groups: LIST_MEMBER_GROUPS,\n standard: [\n {\n id: 'email',\n label: 'Email',\n group: 'membership',\n type: 'email',\n required: true,\n matchKey: true,\n aliases: LIST_IMPORT_EMAIL_COLUMNS,\n description: 'Who the member is. A person is on a list once, by address.',\n },\n {\n id: 'name',\n label: 'Name',\n group: 'membership',\n type: 'text',\n maxLength: 120,\n aliases: LIST_IMPORT_NAME_COLUMNS,\n description: 'The name the list shows. Composed from the first and last name when only those are mapped.',\n },\n {\n id: 'declaredSource',\n label: 'Opt-in source',\n group: 'evidence',\n type: 'text',\n maxLength: 200,\n aliases: LIST_IMPORT_OPT_IN_SOURCE_COLUMNS,\n description: 'Where your file says the person signed up. Kept with your statement of permission, never as their own opt-in.',\n },\n {\n id: 'declaredAt',\n label: 'Opt-in date',\n group: 'evidence',\n type: 'text',\n maxLength: 60,\n aliases: LIST_IMPORT_OPT_IN_DATE_COLUMNS,\n description: 'When your file says the person signed up, kept as written.',\n },\n {\n id: 'addedAt',\n label: 'Added',\n group: 'membership',\n type: 'datetime',\n readOnly: true,\n },\n {\n id: 'source',\n label: 'Added through',\n group: 'membership',\n type: 'text',\n readOnly: true,\n },\n {\n id: 'via',\n label: 'How',\n group: 'membership',\n type: 'text',\n readOnly: true,\n description: '`rule` for a live list’s match, `manual` for everyone added another way.',\n },\n {\n id: 'consent',\n label: 'Consent',\n group: 'consent',\n type: 'text',\n readOnly: true,\n description: '`granted`, `declined` or `unrecorded`, as the site the list is worked as reads it.',\n },\n {\n id: 'consentBasis',\n label: 'Consent basis',\n group: 'consent',\n type: 'text',\n readOnly: true,\n description: '`contact-opt-in` when the person opted in, `operator-attested` when a member of your team stated permission.',\n },\n {\n id: 'consentAt',\n label: 'Consent recorded',\n group: 'consent',\n type: 'datetime',\n readOnly: true,\n },\n {\n id: 'consentReason',\n label: 'Consent note',\n group: 'consent',\n type: 'longText',\n readOnly: true,\n },\n ...LIST_MEMBER_CONTACT_FIELDS.map(\n (field): TransferField => ({\n id: contactFieldId(field.key),\n label: field.label,\n group: 'contact',\n type: field.type,\n aliases: field.aliases,\n maxLength: 120,\n }),\n ),\n ],\n }\n}\n\n/** A row finds its member by address, or by the member id an export carries. */\nexport const LIST_MEMBER_MATCH_KEYS: readonly MatchKeySpec[] = [\n { fieldId: 'email', normalizer: 'email' },\n { fieldId: 'id', normalizer: 'aglynId' },\n]\n\n/**\n * Other products' list-export headers. Spellings only — every one of them\n * is read the same way as the platform's own.\n */\nexport const LIST_MEMBER_ALIASES: readonly TransferAliasDictionary[] = [\n {\n source: 'Mailchimp audience export',\n aliases: {\n email: ['Email Address'],\n [contactFieldId('firstName')]: ['First Name'],\n [contactFieldId('lastName')]: ['Last Name'],\n [contactFieldId('phone')]: ['Phone Number'],\n declaredAt: ['OPTIN_TIME', 'CONFIRM_TIME'],\n },\n },\n {\n source: 'Klaviyo list export',\n aliases: {\n email: ['Email'],\n [contactFieldId('firstName')]: ['First Name'],\n [contactFieldId('lastName')]: ['Last Name'],\n [contactFieldId('phone')]: ['Phone Number'],\n [contactFieldId('companyName')]: ['Organization'],\n [contactFieldId('jobTitle')]: ['Title'],\n declaredSource: ['Source', 'List Source'],\n declaredAt: ['Date Added', 'Consent Timestamp'],\n },\n },\n {\n source: 'Constant Contact export',\n aliases: {\n email: ['Email address'],\n [contactFieldId('firstName')]: ['First name'],\n [contactFieldId('lastName')]: ['Last name'],\n [contactFieldId('companyName')]: ['Company name'],\n [contactFieldId('jobTitle')]: ['Job title'],\n [contactFieldId('mobilePhone')]: ['Phone - mobile'],\n declaredSource: ['Source name'],\n declaredAt: ['Email permission date'],\n },\n },\n]\n\n/** Why somebody already on the list keeps their membership as it is. */\nexport const MEMBERSHIP_KEPT_REASON =\n 'Somebody already on this list keeps their membership as it is: an import never re-enrolls them or rewrites what their membership records.'\n\n/** Why a contact detail is never emptied by a file. */\nexport const CONTACT_NEVER_CLEARED_REASON =\n 'An import adds and replaces contact details; a blank cell never empties one.'\n\n/** Why the file holds no say over consent. */\nexport const CONSENT_FROM_FILE_REASON =\n 'A file never says who agreed to hear from you. People with an opt-in on record keep it; anybody else is added only on your statement of permission, which is stored against your account; people who unsubscribed, bounced, complained or said no are never added.'\n\n/** The list-member import's rules, shown locked in the wizard with their reasons. */\nexport const LIST_MEMBER_LOCKED_RULES: readonly TransferLockedRule[] = [\n {\n fieldId: 'email',\n forced: { mode: 'keepExisting' },\n reason: 'A member is their address: a row matched by its ID never moves the member to another one.',\n },\n ...(['name', 'declaredSource', 'declaredAt'] as const).map(\n (fieldId): TransferLockedRule => ({\n fieldId,\n forced: { mode: 'keepExisting' },\n reason: MEMBERSHIP_KEPT_REASON,\n }),\n ),\n { fieldId: 'consent', refuseValues: true, reason: CONSENT_FROM_FILE_REASON },\n ...LIST_MEMBER_CONTACT_FIELD_IDS.map(\n (fieldId): TransferLockedRule => ({\n fieldId,\n forced: { blank: 'leave' },\n reason: CONTACT_NEVER_CLEARED_REASON,\n }),\n ),\n]\n\n/*==========================================\n * SUPPRESSIONS\n *=========================================*/\n\n/** Why an entry already on the list is left alone. */\nexport const SUPPRESSION_KEPT_REASON =\n 'An address already suppressed keeps its entry exactly as it is — the reason and date it was suppressed with are the record of why — and an import never removes one.'\n\n/** A site's suppression list: every field. */\nexport function suppressionCatalog(): TransferCatalogInput {\n return {\n groups: [{ id: 'suppression', label: 'Suppression' }],\n standard: [\n {\n id: 'email',\n label: 'Email',\n group: 'suppression',\n type: 'email',\n required: true,\n matchKey: true,\n aliases: LIST_IMPORT_EMAIL_COLUMNS,\n },\n {\n id: 'note',\n label: 'Note',\n group: 'suppression',\n type: 'text',\n maxLength: 200,\n aliases: ['note', 'notes', 'comment', 'why'],\n description: 'How the request reached you. Kept on a new entry only.',\n },\n {\n id: 'reason',\n label: 'Reason',\n group: 'suppression',\n type: 'text',\n readOnly: true,\n description: '`unsubscribe`, `bounce`, `complaint` or `manual`. An imported address is `manual`: a person on your team added it.',\n },\n {\n id: 'suppressedAt',\n label: 'Since',\n group: 'suppression',\n type: 'datetime',\n readOnly: true,\n },\n {\n id: 'carriedFromHostId',\n label: 'Copied from site',\n group: 'suppression',\n type: 'text',\n readOnly: true,\n },\n ],\n }\n}\n\n/** A suppression is the address. */\nexport const SUPPRESSION_MATCH_KEYS: readonly MatchKeySpec[] = [\n { fieldId: 'email', normalizer: 'email' },\n]\n\n/** The suppression import's rule: nothing already on the list is changed. */\nexport const SUPPRESSION_LOCKED_RULES: readonly TransferLockedRule[] = [\n { fieldId: 'note', forced: { mode: 'keepExisting', blank: 'leave' }, reason: SUPPRESSION_KEPT_REASON },\n]\n\n/** Other products' suppression-export headers. */\nexport const SUPPRESSION_ALIASES: readonly TransferAliasDictionary[] = [\n {\n source: 'Mailchimp unsubscribed export',\n aliases: { email: ['Email Address'], note: ['UNSUB_REASON', 'UNSUB_REASON_OTHER'] },\n },\n {\n source: 'Klaviyo suppression export',\n aliases: { email: ['Email'], note: ['Suppression Reason'] },\n },\n]\n"],"names":["LIST_IMPORT_EMAIL_COLUMNS","LIST_IMPORT_NAME_COLUMNS","LIST_IMPORT_OPT_IN_DATE_COLUMNS","LIST_IMPORT_OPT_IN_SOURCE_COLUMNS","LIST_MEMBERS_RESOURCE","listMembersResourceKey","listId","listIdOfResourceKey","key","prefix","String","startsWith","slice","length","test","SUPPRESSIONS_RESOURCE","LIST_EXISTING_STEP_ID","LIST_CONSENT_STEP_ID","readListExistingAnswer","extras","value","updateContacts","readListConsentAnswer","attest","CONTACT_FIELD_PREFIX","LIST_MEMBER_CONTACT_FIELDS","label","type","aliases","contactFieldId","contactFieldKey","fieldId","LIST_MEMBER_CONTACT_FIELD_IDS","map","field","LIST_MEMBER_WRITTEN_FIELDS","LIST_MEMBER_GROUPS","id","listMemberCatalog","groups","standard","group","required","matchKey","description","maxLength","readOnly","LIST_MEMBER_MATCH_KEYS","normalizer","LIST_MEMBER_ALIASES","source","email","declaredAt","declaredSource","MEMBERSHIP_KEPT_REASON","CONTACT_NEVER_CLEARED_REASON","CONSENT_FROM_FILE_REASON","LIST_MEMBER_LOCKED_RULES","forced","mode","reason","refuseValues","blank","SUPPRESSION_KEPT_REASON","suppressionCatalog","SUPPRESSION_MATCH_KEYS","SUPPRESSION_LOCKED_RULES","SUPPRESSION_ALIASES","note"],"mappings":"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"}
|
|
@@ -0,0 +1,44 @@
|
|
|
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
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* THE LIST IMPORT'S OWN WIZARD STEPS (AGL-3529), drawn inside the console's
|
|
19
|
+
* import wizard (`registerPluginTransferResourceUi`, in `plugin.ts`).
|
|
20
|
+
*
|
|
21
|
+
* - "People already on this list", after Matching: whether the file may
|
|
22
|
+
* change the contact details of people the workspace already holds. The
|
|
23
|
+
* default is no — somebody already on the list is left exactly as they
|
|
24
|
+
* are — so this step is complete before anybody touches it.
|
|
25
|
+
* - "Permission", after Conflicts: the statement of permission, under what
|
|
26
|
+
* the dry run found in the file. The evidence comes first and the act
|
|
27
|
+
* last, on purpose: an act offered above its own evidence is an act taken
|
|
28
|
+
* without it. The step is not complete until the operator has answered
|
|
29
|
+
* either way, and the answer is enforced by the server, which records who
|
|
30
|
+
* gave it — the browser only says which way they answered.
|
|
31
|
+
*
|
|
32
|
+
* Each answer rides in the wizard's `extras` under the step's id.
|
|
33
|
+
*/
|
|
34
|
+
import type { TransferWizardStepProps } from '@aglyn/aglyn/plugin-manager/plugin-transfer-resources';
|
|
35
|
+
/** "People already on this list": leave them, or update their contact details. */
|
|
36
|
+
export declare function ListExistingStep(props: TransferWizardStepProps): import("react").JSX.Element;
|
|
37
|
+
export declare namespace ListExistingStep {
|
|
38
|
+
var displayName: string;
|
|
39
|
+
}
|
|
40
|
+
/** "Permission": what the file holds, then the statement. */
|
|
41
|
+
export declare function ListConsentStep(props: TransferWizardStepProps): import("react").JSX.Element;
|
|
42
|
+
export declare namespace ListConsentStep {
|
|
43
|
+
var displayName: string;
|
|
44
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
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, jsxs as _jsxs } from "react/jsx-runtime";
|
|
18
|
+
import { authorizedFetch } from "@aglyn/shared-util-http/authorized-token";
|
|
19
|
+
import { useUser } from "@aglyn/tenant-feature-instance";
|
|
20
|
+
import { Alert, AlertTitle, FormControl, FormControlLabel, Radio, RadioGroup, Stack, Typography } from "@mui/material";
|
|
21
|
+
import { useEffect, useState } from "react";
|
|
22
|
+
import { listIdOfResourceKey, readListConsentAnswer, readListExistingAnswer, LIST_CONSENT_STEP_ID, LIST_EXISTING_STEP_ID } from "./email-transfer-catalog.js";
|
|
23
|
+
/** "People already on this list": leave them, or update their contact details. */ export function ListExistingStep(props) {
|
|
24
|
+
const { value, setValue, setComplete } = props;
|
|
25
|
+
const answer = readListExistingAnswer({
|
|
26
|
+
[LIST_EXISTING_STEP_ID]: value
|
|
27
|
+
});
|
|
28
|
+
useEffect(()=>{
|
|
29
|
+
// The default is an answer: nobody already held is changed.
|
|
30
|
+
if (value === undefined || value === null) setValue({
|
|
31
|
+
updateContacts: false
|
|
32
|
+
});
|
|
33
|
+
setComplete(true);
|
|
34
|
+
}, [
|
|
35
|
+
value,
|
|
36
|
+
setValue,
|
|
37
|
+
setComplete
|
|
38
|
+
]);
|
|
39
|
+
return /*#__PURE__*/ _jsxs(Stack, {
|
|
40
|
+
spacing: 2,
|
|
41
|
+
children: [
|
|
42
|
+
/*#__PURE__*/ _jsx(Typography, {
|
|
43
|
+
variant: "body2",
|
|
44
|
+
color: "text.secondary",
|
|
45
|
+
children: 'Somebody already on this list stays on it exactly as they are: the ' + 'import never re-adds them or rewrites their membership. What the ' + 'file can change is the contact record of people the workspace ' + 'already holds — their name, phone, company and title.'
|
|
46
|
+
}),
|
|
47
|
+
/*#__PURE__*/ _jsx(FormControl, {
|
|
48
|
+
children: /*#__PURE__*/ _jsxs(RadioGroup, {
|
|
49
|
+
value: answer.updateContacts ? 'update' : 'leave',
|
|
50
|
+
onChange: (event)=>setValue({
|
|
51
|
+
updateContacts: event.target.value === 'update'
|
|
52
|
+
}),
|
|
53
|
+
children: [
|
|
54
|
+
/*#__PURE__*/ _jsx(FormControlLabel, {
|
|
55
|
+
value: "leave",
|
|
56
|
+
control: /*#__PURE__*/ _jsx(Radio, {}),
|
|
57
|
+
label: 'Leave them as they are. The file’s contact details fill in ' + 'only a contact the import creates.'
|
|
58
|
+
}),
|
|
59
|
+
/*#__PURE__*/ _jsx(FormControlLabel, {
|
|
60
|
+
value: "update",
|
|
61
|
+
control: /*#__PURE__*/ _jsx(Radio, {}),
|
|
62
|
+
label: 'Update their contact details from the file, under the field ' + 'choices on the Conflicts step. A blank cell never empties one.'
|
|
63
|
+
})
|
|
64
|
+
]
|
|
65
|
+
})
|
|
66
|
+
})
|
|
67
|
+
]
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
ListExistingStep.displayName = 'ListExistingStep';
|
|
71
|
+
/** "Permission": what the file holds, then the statement. */ export function ListConsentStep(props) {
|
|
72
|
+
var _ref;
|
|
73
|
+
const { resource, hostId, jobId, value, setValue, setComplete } = props;
|
|
74
|
+
const listId = listIdOfResourceKey(resource);
|
|
75
|
+
const { data: user } = useUser();
|
|
76
|
+
const answer = readListConsentAnswer({
|
|
77
|
+
[LIST_CONSENT_STEP_ID]: value
|
|
78
|
+
});
|
|
79
|
+
const answered = answer === null ? null : answer.attest;
|
|
80
|
+
const [found, setFound] = useState(null);
|
|
81
|
+
const [error, setError] = useState(null);
|
|
82
|
+
useEffect(()=>{
|
|
83
|
+
setComplete(answered !== null);
|
|
84
|
+
}, [
|
|
85
|
+
answered,
|
|
86
|
+
setComplete
|
|
87
|
+
]);
|
|
88
|
+
useEffect(()=>{
|
|
89
|
+
if (!user || !hostId || !listId || !jobId) return undefined;
|
|
90
|
+
let live = true;
|
|
91
|
+
void (async ()=>{
|
|
92
|
+
try {
|
|
93
|
+
var _ref;
|
|
94
|
+
const response = await authorizedFetch(user, '/api/email/list-import-screening', {
|
|
95
|
+
method: 'POST',
|
|
96
|
+
headers: {
|
|
97
|
+
'Content-Type': 'application/json'
|
|
98
|
+
},
|
|
99
|
+
body: JSON.stringify({
|
|
100
|
+
hostId,
|
|
101
|
+
listId,
|
|
102
|
+
jobId
|
|
103
|
+
})
|
|
104
|
+
});
|
|
105
|
+
const payload = await response.json().catch(()=>({}));
|
|
106
|
+
if (!live) return;
|
|
107
|
+
if (!response.ok) setError((_ref = payload == null ? void 0 : payload.error) != null ? _ref : 'What the file holds could not be read.');
|
|
108
|
+
else setFound(payload);
|
|
109
|
+
} catch (unused) {
|
|
110
|
+
if (live) setError('What the file holds could not be read.');
|
|
111
|
+
}
|
|
112
|
+
})();
|
|
113
|
+
return ()=>{
|
|
114
|
+
live = false;
|
|
115
|
+
};
|
|
116
|
+
}, [
|
|
117
|
+
user,
|
|
118
|
+
hostId,
|
|
119
|
+
listId,
|
|
120
|
+
jobId
|
|
121
|
+
]);
|
|
122
|
+
const screening = (_ref = found == null ? void 0 : found.screening) != null ? _ref : null;
|
|
123
|
+
const sample = screening == null ? void 0 : screening.sample;
|
|
124
|
+
return /*#__PURE__*/ _jsxs(Stack, {
|
|
125
|
+
spacing: 2,
|
|
126
|
+
children: [
|
|
127
|
+
error ? /*#__PURE__*/ _jsx(Alert, {
|
|
128
|
+
severity: "error",
|
|
129
|
+
children: error
|
|
130
|
+
}) : null,
|
|
131
|
+
(screening == null ? void 0 : screening.purchaseTellColumns.length) ? /*#__PURE__*/ _jsxs(Alert, {
|
|
132
|
+
severity: "warning",
|
|
133
|
+
children: [
|
|
134
|
+
/*#__PURE__*/ _jsx(AlertTitle, {
|
|
135
|
+
children: 'This file looks bought or appended'
|
|
136
|
+
}),
|
|
137
|
+
`Its columns include ${screening.purchaseTellColumns.join(', ')}. ` + 'Purchased, rented and appended lists are not allowed, and ' + 'importing one puts every site sending through this domain at ' + 'risk. If the column name is a coincidence, carry on.'
|
|
138
|
+
]
|
|
139
|
+
}) : null,
|
|
140
|
+
(screening == null ? void 0 : screening.roleAccounts) ? /*#__PURE__*/ _jsxs(Alert, {
|
|
141
|
+
severity: "warning",
|
|
142
|
+
children: [
|
|
143
|
+
/*#__PURE__*/ _jsx(AlertTitle, {
|
|
144
|
+
children: `${screening.roleAccounts} shared mailboxes`
|
|
145
|
+
}),
|
|
146
|
+
`Addresses like ${screening.roleAccountSamples.slice(0, 3).join(', ')} ` + 'reach a mailbox several people read, or nobody. They are a ' + 'common sign of a list built by collecting addresses rather than ' + 'by people signing up. They are added if you go ahead.'
|
|
147
|
+
]
|
|
148
|
+
}) : null,
|
|
149
|
+
sample && sample.size ? /*#__PURE__*/ _jsxs(Alert, {
|
|
150
|
+
severity: sample.needAttestation ? 'warning' : 'success',
|
|
151
|
+
children: [
|
|
152
|
+
/*#__PURE__*/ _jsx(AlertTitle, {
|
|
153
|
+
children: sample.size < sample.of ? `Checked the first ${sample.size} of ${sample.of} new addresses` : `Checked all ${sample.size} new addresses`
|
|
154
|
+
}),
|
|
155
|
+
`${sample.optedIn} already have an opt-in on record, ` + `${sample.needAttestation} have no opt-in on record, and ` + `${sample.refused} cannot be added at all. ` + (sample.size < sample.of ? 'The rest are checked the same way as they are added.' : '')
|
|
156
|
+
]
|
|
157
|
+
}) : !error ? /*#__PURE__*/ _jsx(Typography, {
|
|
158
|
+
variant: "body2",
|
|
159
|
+
color: "text.secondary",
|
|
160
|
+
children: found ? 'Nobody new is added by this file.' : 'Reading what the file holds…'
|
|
161
|
+
}) : null,
|
|
162
|
+
/*#__PURE__*/ _jsx(FormControl, {
|
|
163
|
+
children: /*#__PURE__*/ _jsxs(RadioGroup, {
|
|
164
|
+
value: answered === null ? '' : answered ? 'attest' : 'only',
|
|
165
|
+
onChange: (event)=>setValue({
|
|
166
|
+
attest: event.target.value === 'attest'
|
|
167
|
+
}),
|
|
168
|
+
children: [
|
|
169
|
+
/*#__PURE__*/ _jsx(FormControlLabel, {
|
|
170
|
+
value: "attest",
|
|
171
|
+
control: /*#__PURE__*/ _jsx(Radio, {}),
|
|
172
|
+
label: /*#__PURE__*/ _jsxs(Typography, {
|
|
173
|
+
variant: "body2",
|
|
174
|
+
children: [
|
|
175
|
+
'I have these people’s permission to send them marketing ' + 'email, and I can produce the record of it if asked. This ' + 'statement is stored against my account, with today’s ' + 'date, on every address it admits.',
|
|
176
|
+
(screening == null ? void 0 : screening.declaresBasis) ? ' The opt-in source and date your file declares are kept with it.' : ''
|
|
177
|
+
]
|
|
178
|
+
})
|
|
179
|
+
}),
|
|
180
|
+
/*#__PURE__*/ _jsx(FormControlLabel, {
|
|
181
|
+
value: "only",
|
|
182
|
+
control: /*#__PURE__*/ _jsx(Radio, {}),
|
|
183
|
+
label: /*#__PURE__*/ _jsx(Typography, {
|
|
184
|
+
variant: "body2",
|
|
185
|
+
children: 'Only add the people who already have an opt-in on record. ' + 'Nobody else is added, and nothing is deleted.'
|
|
186
|
+
})
|
|
187
|
+
})
|
|
188
|
+
]
|
|
189
|
+
})
|
|
190
|
+
}),
|
|
191
|
+
/*#__PURE__*/ _jsx(Typography, {
|
|
192
|
+
variant: "body2",
|
|
193
|
+
color: "text.secondary",
|
|
194
|
+
children: 'Whichever you choose, people who unsubscribed, bounced, marked a ' + 'message as spam or said no are never added, and an import never ' + 'sends a confirmation email.'
|
|
195
|
+
})
|
|
196
|
+
]
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
ListConsentStep.displayName = 'ListConsentStep';
|
|
200
|
+
|
|
201
|
+
//# sourceMappingURL=list-import-steps.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/email/src/lib/transfer/list-import-steps.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\n'use client'\n\n/**\n * THE LIST IMPORT'S OWN WIZARD STEPS (AGL-3529), drawn inside the console's\n * import wizard (`registerPluginTransferResourceUi`, in `plugin.ts`).\n *\n * - \"People already on this list\", after Matching: whether the file may\n * change the contact details of people the workspace already holds. The\n * default is no — somebody already on the list is left exactly as they\n * are — so this step is complete before anybody touches it.\n * - \"Permission\", after Conflicts: the statement of permission, under what\n * the dry run found in the file. The evidence comes first and the act\n * last, on purpose: an act offered above its own evidence is an act taken\n * without it. The step is not complete until the operator has answered\n * either way, and the answer is enforced by the server, which records who\n * gave it — the browser only says which way they answered.\n *\n * Each answer rides in the wizard's `extras` under the step's id.\n */\n\nimport type { TransferWizardStepProps } from '@aglyn/aglyn/plugin-manager/plugin-transfer-resources'\nimport { authorizedFetch } from '@aglyn/shared-util-http/authorized-token'\nimport { useUser } from '@aglyn/tenant-feature-instance'\nimport {\n Alert,\n AlertTitle,\n FormControl,\n FormControlLabel,\n Radio,\n RadioGroup,\n Stack,\n Typography,\n} from '@mui/material'\nimport { useEffect, useState } from 'react'\nimport {\n listIdOfResourceKey,\n type ListImportScreeningReport,\n readListConsentAnswer,\n readListExistingAnswer,\n LIST_CONSENT_STEP_ID,\n LIST_EXISTING_STEP_ID,\n} from './email-transfer-catalog'\n\n/** \"People already on this list\": leave them, or update their contact details. */\nexport function ListExistingStep(props: TransferWizardStepProps) {\n const { value, setValue, setComplete } = props\n const answer = readListExistingAnswer({ [LIST_EXISTING_STEP_ID]: value })\n useEffect(() => {\n // The default is an answer: nobody already held is changed.\n if (value === undefined || value === null) setValue({ updateContacts: false })\n setComplete(true)\n }, [value, setValue, setComplete])\n return (\n <Stack spacing={2}>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Somebody already on this list stays on it exactly as they are: the ' +\n 'import never re-adds them or rewrites their membership. What the ' +\n 'file can change is the contact record of people the workspace ' +\n 'already holds — their name, phone, company and title.'}\n </Typography>\n <FormControl>\n <RadioGroup\n value={answer.updateContacts ? 'update' : 'leave'}\n onChange={(event) =>\n setValue({ updateContacts: event.target.value === 'update' })\n }\n >\n <FormControlLabel\n value=\"leave\"\n control={<Radio />}\n label={\n 'Leave them as they are. The file’s contact details fill in ' +\n 'only a contact the import creates.'\n }\n />\n <FormControlLabel\n value=\"update\"\n control={<Radio />}\n label={\n 'Update their contact details from the file, under the field ' +\n 'choices on the Conflicts step. A blank cell never empties one.'\n }\n />\n </RadioGroup>\n </FormControl>\n </Stack>\n )\n}\nListExistingStep.displayName = 'ListExistingStep'\n\n/** What the screening route answers. */\ninterface ScreeningAnswer {\n listName: string\n screening: ListImportScreeningReport | null\n}\n\n/** \"Permission\": what the file holds, then the statement. */\nexport function ListConsentStep(props: TransferWizardStepProps) {\n const { resource, hostId, jobId, value, setValue, setComplete } = props\n const listId = listIdOfResourceKey(resource)\n const { data: user } = useUser()\n const answer = readListConsentAnswer({ [LIST_CONSENT_STEP_ID]: value })\n const answered = answer === null ? null : answer.attest\n const [found, setFound] = useState<ScreeningAnswer | null>(null)\n const [error, setError] = useState<string | null>(null)\n\n useEffect(() => {\n setComplete(answered !== null)\n }, [answered, setComplete])\n\n useEffect(() => {\n if (!user || !hostId || !listId || !jobId) return undefined\n let live = true\n void (async () => {\n try {\n const response = await authorizedFetch(\n user,\n '/api/email/list-import-screening',\n {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ hostId, listId, jobId }),\n },\n )\n const payload = await response.json().catch(() => ({}))\n if (!live) return\n if (!response.ok) setError(payload?.error ?? 'What the file holds could not be read.')\n else setFound(payload as ScreeningAnswer)\n } catch {\n if (live) setError('What the file holds could not be read.')\n }\n })()\n return () => {\n live = false\n }\n }, [user, hostId, listId, jobId])\n\n const screening = found?.screening ?? null\n const sample = screening?.sample\n return (\n <Stack spacing={2}>\n {error ? <Alert severity=\"error\">{error}</Alert> : null}\n {screening?.purchaseTellColumns.length ? (\n <Alert severity=\"warning\">\n <AlertTitle>{'This file looks bought or appended'}</AlertTitle>\n {`Its columns include ${screening.purchaseTellColumns.join(', ')}. ` +\n 'Purchased, rented and appended lists are not allowed, and ' +\n 'importing one puts every site sending through this domain at ' +\n 'risk. If the column name is a coincidence, carry on.'}\n </Alert>\n ) : null}\n {screening?.roleAccounts ? (\n <Alert severity=\"warning\">\n <AlertTitle>{`${screening.roleAccounts} shared mailboxes`}</AlertTitle>\n {`Addresses like ${screening.roleAccountSamples.slice(0, 3).join(', ')} ` +\n 'reach a mailbox several people read, or nobody. They are a ' +\n 'common sign of a list built by collecting addresses rather than ' +\n 'by people signing up. They are added if you go ahead.'}\n </Alert>\n ) : null}\n {sample && sample.size ? (\n <Alert severity={sample.needAttestation ? 'warning' : 'success'}>\n <AlertTitle>\n {sample.size < sample.of\n ? `Checked the first ${sample.size} of ${sample.of} new addresses`\n : `Checked all ${sample.size} new addresses`}\n </AlertTitle>\n {`${sample.optedIn} already have an opt-in on record, ` +\n `${sample.needAttestation} have no opt-in on record, and ` +\n `${sample.refused} cannot be added at all. ` +\n (sample.size < sample.of\n ? 'The rest are checked the same way as they are added.'\n : '')}\n </Alert>\n ) : !error ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {found\n ? 'Nobody new is added by this file.'\n : 'Reading what the file holds…'}\n </Typography>\n ) : null}\n <FormControl>\n <RadioGroup\n value={answered === null ? '' : answered ? 'attest' : 'only'}\n onChange={(event) => setValue({ attest: event.target.value === 'attest' })}\n >\n <FormControlLabel\n value=\"attest\"\n control={<Radio />}\n label={\n <Typography variant=\"body2\">\n {'I have these people’s permission to send them marketing ' +\n 'email, and I can produce the record of it if asked. This ' +\n 'statement is stored against my account, with today’s ' +\n 'date, on every address it admits.'}\n {screening?.declaresBasis\n ? ' The opt-in source and date your file declares are kept with it.'\n : ''}\n </Typography>\n }\n />\n <FormControlLabel\n value=\"only\"\n control={<Radio />}\n label={\n <Typography variant=\"body2\">\n {'Only add the people who already have an opt-in on record. ' +\n 'Nobody else is added, and nothing is deleted.'}\n </Typography>\n }\n />\n </RadioGroup>\n </FormControl>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Whichever you choose, people who unsubscribed, bounced, marked a ' +\n 'message as spam or said no are never added, and an import never ' +\n 'sends a confirmation email.'}\n </Typography>\n </Stack>\n )\n}\nListConsentStep.displayName = 'ListConsentStep'\n"],"names":["authorizedFetch","useUser","Alert","AlertTitle","FormControl","FormControlLabel","Radio","RadioGroup","Stack","Typography","useEffect","useState","listIdOfResourceKey","readListConsentAnswer","readListExistingAnswer","LIST_CONSENT_STEP_ID","LIST_EXISTING_STEP_ID","ListExistingStep","props","value","setValue","setComplete","answer","undefined","updateContacts","spacing","variant","color","onChange","event","target","control","label","displayName","ListConsentStep","resource","hostId","jobId","listId","data","user","answered","attest","found","setFound","error","setError","live","response","method","headers","body","JSON","stringify","payload","json","catch","ok","screening","sample","severity","purchaseTellColumns","length","join","roleAccounts","roleAccountSamples","slice","size","needAttestation","of","optedIn","refused","declaresBasis"],"mappings":"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"}
|
|
@@ -0,0 +1,165 @@
|
|
|
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
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* ONE LIST'S MEMBERS, IMPORTED AND EXPORTED (AGL-3529) — the server half of
|
|
19
|
+
* `email.list-members` on the platform's transfer framework.
|
|
20
|
+
*
|
|
21
|
+
* The framework reads the file, maps its columns, finds the rows that repeat
|
|
22
|
+
* an address, plans every row before anything is written, applies the plan
|
|
23
|
+
* a chunk at a time through a ledger that never writes a row twice, resumes
|
|
24
|
+
* where a closed tab stopped, and keeps an undo for seven days. What it
|
|
25
|
+
* cannot know is a list's rules, and every one of them the importer this
|
|
26
|
+
* replaces kept is kept here:
|
|
27
|
+
*
|
|
28
|
+
* - EVERY ADDRESS GOES THROUGH THE CHECKS A TYPED ONE DOES — the same
|
|
29
|
+
* functions, not similar ones. `resolveAddresses` is the resolution the
|
|
30
|
+
* audience card's add runs, `assignmentBasis` is the policy it applies,
|
|
31
|
+
* and `enrollListMember` is the one writer of the membership collection.
|
|
32
|
+
* Suppressed people and people who said no are refused at the write, with
|
|
33
|
+
* the reason, whatever the file or the operator says.
|
|
34
|
+
* - THE STATEMENT OF PERMISSION is the plugin's own wizard step. Its answer
|
|
35
|
+
* reaches the dry run in `extras`; the dry run records WHO made it on the
|
|
36
|
+
* list's import ledger (`orgs/{orgId}/lists/{listId}/imports/{jobId}`),
|
|
37
|
+
* and every write reads the attester from there — never from whoever
|
|
38
|
+
* pressed Resume, and never from the browser. Without it only people with
|
|
39
|
+
* an opt-in on record are added. Neither the statement nor anything in a
|
|
40
|
+
* file can make a refusal an opt-in (`CONSENT_FROM_FILE_REASON`).
|
|
41
|
+
* - THE SCREENING — role accounts and column names that read as a bought or
|
|
42
|
+
* appended list — and a consent check of a bounded sample are a warning on
|
|
43
|
+
* the dry run that has to be acknowledged before anything is written, and
|
|
44
|
+
* the sample is shown again on the permission step.
|
|
45
|
+
* - SOMEBODY ALREADY ON THE LIST is unchanged: their membership is never
|
|
46
|
+
* re-enrolled or rewritten. Their contact details change only when the
|
|
47
|
+
* operator says so on the "People already on this list" step.
|
|
48
|
+
* - A FILE'S CONTACT COLUMNS go to the contact record through the platform's
|
|
49
|
+
* contact-capture door (`capturePluginContact`), the record system's own
|
|
50
|
+
* write path — never written here. Each is planned against what the record
|
|
51
|
+
* holds, under the person's field choices; a blank cell never empties one.
|
|
52
|
+
* - 50,000 rows a file (`limits.maxRows`), refused at the upload before
|
|
53
|
+
* anything is staged; repeats of an address are skipped as repeats; a line
|
|
54
|
+
* that is not an address fails its row, by name, rather than vanishing.
|
|
55
|
+
*
|
|
56
|
+
* ## Who may
|
|
57
|
+
*
|
|
58
|
+
* The transfer gate admits a member who may manage the SITE's data. A list
|
|
59
|
+
* is the organization's, and every site in it can mail one, so changing who
|
|
60
|
+
* is on it — or reading their consent through the matching — needs an
|
|
61
|
+
* org-wide owner, admin or editor (`isOrgWideListWriter`), the rule the
|
|
62
|
+
* list routes apply. An export reads through `scopeTokens`: a collaborator
|
|
63
|
+
* scoped to some sites reads no member at all, which is what the rules
|
|
64
|
+
* would answer them.
|
|
65
|
+
*/
|
|
66
|
+
import { type ConsentGroup } from '@aglyn/aglyn/server';
|
|
67
|
+
import { type BuildTransferPlanInput, type MatchLookupRequest, type PlannedTransferRow, type TransferPlan, type TransferUndoEntry, type TransferWarning } from '@aglyn/aglyn/data-transfer';
|
|
68
|
+
import type { TransferApplyResult, TransferLookupResult, TransferReadOptions, TransferReadPage, TransferRecordsHooks, TransferResourceContext, TransferRevertResult } from '@aglyn/aglyn/plugin-manager/plugin-transfer-resources';
|
|
69
|
+
import { Timestamp } from 'firebase-admin/firestore';
|
|
70
|
+
import { type ListImportScreeningReport } from './email-transfer-catalog';
|
|
71
|
+
/** `source` stamped on every membership an import writes. */
|
|
72
|
+
export declare const CONSOLE_IMPORT_SOURCE = "console:list-import";
|
|
73
|
+
/** A list's import ledger: `orgs/{orgId}/lists/{listId}/imports/{jobId}`. */
|
|
74
|
+
export declare const LIST_IMPORTS_SUBCOLLECTION = "imports";
|
|
75
|
+
/**
|
|
76
|
+
* How long a list's import ledger is kept (AGL-3549): it holds sample
|
|
77
|
+
* shared-mailbox addresses and who stated permission, and is read only
|
|
78
|
+
* while its job can still write — the longest a planned import may wait
|
|
79
|
+
* to be applied (`TRANSFER_DRAFT_RETENTION_MS`, after which the job itself
|
|
80
|
+
* expires), its undo window, and a day. The Firestore TTL policy on
|
|
81
|
+
* `imports.expiresAt` deletes it; nothing else does.
|
|
82
|
+
*/
|
|
83
|
+
export declare const LIST_IMPORT_LEDGER_RETENTION_MS: number;
|
|
84
|
+
/** The `expiresAt` of a list import ledger written at `nowMs`. */
|
|
85
|
+
export declare function listImportLedgerExpiry(nowMs: number): Timestamp;
|
|
86
|
+
/** Why a contact detail stays as the record holds it. */
|
|
87
|
+
export declare const CONTACTS_KEPT_REASON = "You chose to leave the contact details of people the workspace already holds as they are.";
|
|
88
|
+
/** The list an import or export is of, proved to exist. */
|
|
89
|
+
export interface ListTarget {
|
|
90
|
+
hostId: string;
|
|
91
|
+
listRef: FirebaseFirestore.DocumentReference;
|
|
92
|
+
listName: string;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* The list a request names, after proving the asker may change who is on
|
|
96
|
+
* it. `write: false` is an export read: the route has already scoped it.
|
|
97
|
+
*/
|
|
98
|
+
export declare function listTargetFor(ctx: TransferResourceContext, options: {
|
|
99
|
+
write: boolean;
|
|
100
|
+
}): Promise<ListTarget>;
|
|
101
|
+
/**
|
|
102
|
+
* One member as the framework reads it: the membership by field id, and the
|
|
103
|
+
* consent as the site the list is worked as reads it.
|
|
104
|
+
*/
|
|
105
|
+
export declare function memberValues(id: string, data: Record<string, unknown>, group: ConsentGroup): Record<string, unknown>;
|
|
106
|
+
/**
|
|
107
|
+
* The contact record's values for these addresses, as this site's holder
|
|
108
|
+
* keeps them, by `contact:<field>` — and only for addresses a record exists
|
|
109
|
+
* for, so "no record" and "a record with nothing in it" stay apart.
|
|
110
|
+
*/
|
|
111
|
+
export declare function readContactValues(hostId: string, group: ConsentGroup, emails: readonly string[]): Promise<Map<string, Record<string, unknown>>>;
|
|
112
|
+
/** The warnings of several plans as one list, class by class. */
|
|
113
|
+
export declare function mergeTransferWarnings(lists: ReadonlyArray<readonly TransferWarning[]>): TransferWarning[];
|
|
114
|
+
/**
|
|
115
|
+
* The dry run (see the module note): the membership planned by the core,
|
|
116
|
+
* the contact columns planned against the contact records, the two joined
|
|
117
|
+
* row by row, and the screening added as one warning to acknowledge.
|
|
118
|
+
* Records who stated permission on the list's import ledger; writes no
|
|
119
|
+
* member and no contact.
|
|
120
|
+
*/
|
|
121
|
+
export declare function planListMembers(ctx: TransferResourceContext, input: BuildTransferPlanInput): Promise<TransferPlan>;
|
|
122
|
+
/**
|
|
123
|
+
* The screening as one warning, or `null` when it found nothing worth a
|
|
124
|
+
* word: the purchase tells first, then what the consent sample says will
|
|
125
|
+
* happen, then the role accounts.
|
|
126
|
+
*/
|
|
127
|
+
export declare function screeningWarning(report: ListImportScreeningReport, roles: ReadonlyArray<{
|
|
128
|
+
index: number;
|
|
129
|
+
email: string;
|
|
130
|
+
}>, attested: boolean): TransferWarning | null;
|
|
131
|
+
/** One chunk's writes (see the module note). */
|
|
132
|
+
export declare function applyListMembers(ctx: TransferResourceContext, chunk: {
|
|
133
|
+
rows: PlannedTransferRow[];
|
|
134
|
+
}, writer: Parameters<TransferRecordsHooks['apply']>[2]): Promise<TransferApplyResult>;
|
|
135
|
+
/**
|
|
136
|
+
* Takes the import's new members off the list again. A membership that now
|
|
137
|
+
* records a REFUSAL is never deleted, whatever the person decided: deleting
|
|
138
|
+
* it would delete the record that the person said no.
|
|
139
|
+
*/
|
|
140
|
+
export declare function revertListMembers(ctx: TransferResourceContext, snapshot: {
|
|
141
|
+
entries: TransferUndoEntry[];
|
|
142
|
+
}, decisions?: Readonly<Record<string, 'revert' | 'keep'>>): Promise<TransferRevertResult>;
|
|
143
|
+
/**
|
|
144
|
+
* The list table's filter as the export reads it — `{ filters }`, each a
|
|
145
|
+
* predicate the table put on its own query — checked clause by clause:
|
|
146
|
+
* only the table's fields and operators, so a request cannot ask the Admin
|
|
147
|
+
* SDK a question the table never could.
|
|
148
|
+
*/
|
|
149
|
+
export declare function readMemberFilter(filter: Readonly<Record<string, unknown>> | undefined): Array<{
|
|
150
|
+
path: string;
|
|
151
|
+
op: '==' | 'array-contains';
|
|
152
|
+
value: string;
|
|
153
|
+
}>;
|
|
154
|
+
/** One page of members, holding only the chosen fields. */
|
|
155
|
+
export declare function readListMemberPage(ctx: TransferResourceContext, cursor: string | null, fieldIds: readonly string[], options?: TransferReadOptions): Promise<TransferReadPage>;
|
|
156
|
+
/** How many members an export reads. */
|
|
157
|
+
export declare function countListMembers(ctx: TransferResourceContext, options: TransferReadOptions): Promise<number>;
|
|
158
|
+
/**
|
|
159
|
+
* The members holding each requested address or id, and what each holds now
|
|
160
|
+
* — the membership, and their contact record's columns, so the conflicts
|
|
161
|
+
* step can show what an update would replace.
|
|
162
|
+
*/
|
|
163
|
+
export declare function lookupListMembers(ctx: TransferResourceContext, requests: readonly MatchLookupRequest[]): Promise<TransferLookupResult>;
|
|
164
|
+
/** `email.list-members`, as the framework asks it. */
|
|
165
|
+
export declare const listMembersTransferResource: TransferRecordsHooks;
|