@aglyn/aglyn 1.0.0-beta.222 → 1.0.0-beta.223
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/package.json +11 -11
- package/src/lib/app-utils/crm-email-templates.js +6 -4
- package/src/lib/app-utils/crm-email-templates.js.map +1 -1
- package/src/lib/app-utils/crm.d.ts +1 -1
- package/src/lib/app-utils/crm.js.map +1 -1
- package/src/lib/app-utils/docs-help.generated.d.ts +3 -3
- package/src/lib/app-utils/docs-help.generated.js +6 -0
- package/src/lib/app-utils/docs-help.generated.js.map +1 -1
- package/src/lib/app-utils/docs-index.generated.js +17 -10
- package/src/lib/app-utils/docs-index.generated.js.map +1 -1
- package/src/lib/app-utils/functions.d.ts +10 -0
- package/src/lib/app-utils/functions.js +78 -0
- package/src/lib/app-utils/functions.js.map +1 -1
- package/src/lib/app-utils/health-report.js +12 -0
- package/src/lib/app-utils/health-report.js.map +1 -1
- package/src/lib/app-utils/host-events.d.ts +14 -0
- package/src/lib/app-utils/host-events.js +8 -0
- package/src/lib/app-utils/host-events.js.map +1 -1
- package/src/lib/app-utils/operator-alerts.js +52 -0
- package/src/lib/app-utils/operator-alerts.js.map +1 -1
- package/src/lib/app-utils/plugin-host-events.generated.js +14 -5
- package/src/lib/app-utils/plugin-host-events.generated.js.map +1 -1
- package/src/lib/app-utils/realm-host-surface.generated.js +1 -0
- package/src/lib/app-utils/realm-host-surface.generated.js.map +1 -1
- package/src/lib/app-utils/screen-route.d.ts +11 -0
- package/src/lib/app-utils/screen-route.js +16 -0
- package/src/lib/app-utils/screen-route.js.map +1 -1
- package/src/lib/app-utils/server.d.ts +0 -2
- package/src/lib/app-utils/server.js +0 -5
- package/src/lib/app-utils/server.js.map +1 -1
- package/src/lib/app-utils/site-interactions.d.ts +23 -0
- package/src/lib/app-utils/site-interactions.js +39 -2
- package/src/lib/app-utils/site-interactions.js.map +1 -1
- package/src/lib/app-utils/utm-touch.d.ts +109 -4
- package/src/lib/app-utils/utm-touch.js +364 -33
- package/src/lib/app-utils/utm-touch.js.map +1 -1
- package/src/lib/plugin-manager/first-party-plugins.generated.js +5 -1
- package/src/lib/plugin-manager/first-party-plugins.generated.js.map +1 -1
- package/src/lib/plugin-manager/interaction-recipes.d.ts +7 -1
- package/src/lib/plugin-manager/interaction-recipes.js.map +1 -1
- package/src/lib/plugin-manager/plugin-conversion-credit.d.ts +51 -0
- package/src/lib/plugin-manager/plugin-conversion-credit.js +39 -0
- package/src/lib/plugin-manager/plugin-conversion-credit.js.map +1 -1
- package/src/lib/plugin-manager/plugin-record-pages.d.ts +15 -1
- package/src/lib/plugin-manager/plugin-record-pages.js +14 -2
- package/src/lib/plugin-manager/plugin-record-pages.js.map +1 -1
- package/src/lib/plugin-manager/plugin-record-timeline.d.ts +31 -0
- package/src/lib/plugin-manager/plugin-record-timeline.js +14 -0
- package/src/lib/plugin-manager/plugin-record-timeline.js.map +1 -1
- package/src/lib/plugin-manager/realm-host-aglyn.generated.js +1 -0
- package/src/lib/plugin-manager/realm-host-aglyn.generated.js.map +1 -1
- package/src/lib/app-utils/crm-next-activity.d.ts +0 -107
- package/src/lib/app-utils/crm-next-activity.js +0 -140
- package/src/lib/app-utils/crm-next-activity.js.map +0 -1
- package/src/lib/app-utils/crm-task-reminders.d.ts +0 -152
- package/src/lib/app-utils/crm-task-reminders.js +0 -161
- package/src/lib/app-utils/crm-task-reminders.js.map +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aglyn/aglyn",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.223",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"homepage": "https://aglyn.com",
|
|
6
6
|
"repository": {
|
|
@@ -37,16 +37,16 @@
|
|
|
37
37
|
"./package.json": "./package.json"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@aglyn/shared-data-enums": "1.0.0-beta.
|
|
41
|
-
"@aglyn/shared-data-mdi": "1.0.0-beta.
|
|
42
|
-
"@aglyn/shared-data-types": "1.0.0-beta.
|
|
43
|
-
"@aglyn/shared-util-email": "1.0.0-beta.
|
|
44
|
-
"@aglyn/shared-util-first-touch": "1.0.0-beta.
|
|
45
|
-
"@aglyn/shared-util-http": "1.0.0-beta.
|
|
46
|
-
"@aglyn/shared-util-logger": "1.0.0-beta.
|
|
47
|
-
"@aglyn/shared-util-timestamp": "1.0.0-beta.
|
|
48
|
-
"@aglyn/shared-util-tools": "1.0.0-beta.
|
|
49
|
-
"@aglyn/shared-util-vendor": "1.0.0-beta.
|
|
40
|
+
"@aglyn/shared-data-enums": "1.0.0-beta.223",
|
|
41
|
+
"@aglyn/shared-data-mdi": "1.0.0-beta.223",
|
|
42
|
+
"@aglyn/shared-data-types": "1.0.0-beta.223",
|
|
43
|
+
"@aglyn/shared-util-email": "1.0.0-beta.223",
|
|
44
|
+
"@aglyn/shared-util-first-touch": "1.0.0-beta.223",
|
|
45
|
+
"@aglyn/shared-util-http": "1.0.0-beta.223",
|
|
46
|
+
"@aglyn/shared-util-logger": "1.0.0-beta.223",
|
|
47
|
+
"@aglyn/shared-util-timestamp": "1.0.0-beta.223",
|
|
48
|
+
"@aglyn/shared-util-tools": "1.0.0-beta.223",
|
|
49
|
+
"@aglyn/shared-util-vendor": "1.0.0-beta.223",
|
|
50
50
|
"@data-driven-forms/react-form-renderer": "^4.2.0",
|
|
51
51
|
"@msgpack/msgpack": "^3.1.3",
|
|
52
52
|
"@types/unist": "^3.0.3",
|
|
@@ -226,9 +226,11 @@ const asMs = (value)=>typeof value === 'number' && Number.isFinite(value) ? valu
|
|
|
226
226
|
}
|
|
227
227
|
/*
|
|
228
228
|
* `{{ group.field }}`, spaces allowed inside the braces because a rep will
|
|
229
|
-
* type them
|
|
229
|
+
* type them, and an optional `|fallback` — `{{contact.firstName|there}}` —
|
|
230
|
+
* printed when the field has no value (AGL-3458), the campaign tags' own
|
|
231
|
+
* spelling. A single brace pair or a bare word is left alone: a letter may
|
|
230
232
|
* legitimately contain braces.
|
|
231
|
-
*/ const MERGE_FIELD_PATTERN = /\{\{\s*([A-Za-z][A-Za-z0-9]*\.[A-Za-z][A-Za-z0-9]*)\s
|
|
233
|
+
*/ const MERGE_FIELD_PATTERN = /\{\{\s*([A-Za-z][A-Za-z0-9]*\.[A-Za-z][A-Za-z0-9]*)\s*(?:\|([^}]*))?\}\}/g;
|
|
232
234
|
/** The merge fields a text names, unique, in order of first appearance. */ export function crmMergeFieldsIn(text) {
|
|
233
235
|
const seen = new Set();
|
|
234
236
|
const fields = [];
|
|
@@ -389,8 +391,8 @@ const text = (value)=>typeof value === 'string' ? value.trim() : '';
|
|
|
389
391
|
const source = String(text != null ? text : '');
|
|
390
392
|
const ctx = context != null ? context : {};
|
|
391
393
|
const unresolved = new Set();
|
|
392
|
-
const rendered = source.replace(MERGE_FIELD_PATTERN, (_match, key)=>{
|
|
393
|
-
const value = resolveCrmMergeField(key, ctx);
|
|
394
|
+
const rendered = source.replace(MERGE_FIELD_PATTERN, (_match, key, fallback)=>{
|
|
395
|
+
const value = resolveCrmMergeField(key, ctx) || String(fallback != null ? fallback : '').trim();
|
|
394
396
|
if (!value) unresolved.add(key);
|
|
395
397
|
return value;
|
|
396
398
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/crm-email-templates.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 * EMAIL TEMPLATES, SNIPPETS AND MERGE FIELDS (AGL-2658).\n *\n * A one-to-one email is a letter, and a rep writes the same three letters\n * all week: the follow-up, the proposal cover note, the \"still keen?\". A\n * TEMPLATE is one of those letters kept under a name — a subject and a body\n * the dialog fills in whole. A SNIPPET is a paragraph kept under a name —\n * a signature block, a pricing line — inserted where the caret is. Both\n * carry MERGE FIELDS, `{{contact.firstName}}` and the like, which the send\n * route fills from the record the email is about, so a template written\n * once reads as a letter to each person it is sent to.\n *\n * ## One resolver, run twice\n *\n * The server resolves at send time, off the documents it has already read\n * to find the recipient, and what it writes to the timeline is the letter\n * as it left. The dialog runs the SAME resolver over the same documents to\n * show the rep what will go out, so the preview and the message cannot\n * disagree. The resolver is pure and lives here, beside the model, so\n * neither caller can drift into its own grammar.\n *\n * ## What a field with no value does\n *\n * It renders as nothing. A template that greets `{{contact.firstName}}` is\n * sent to a contact known only by email as \"Hi ,\" — which is why the dialog\n * counts the empty fields and says so before the rep clicks Send, and why\n * an unknown field name renders as nothing too rather than as its own\n * braces: a typo in a merge field must not reach a customer as `{{contct.\n * firstName}}`. Nothing is escaped, because the mail is plain text.\n *\n * ## Shared or personal\n *\n * A template is either the team's — every CRM editor may use and change it\n * — or one person's, listed for its owner alone. The rules gate the writes\n * (`cloud/firebase-firestore.rules`); the listing hides a colleague's\n * personal rows the way a private saved view is hidden (AGL-2617), because\n * a rule per reader would need a query per reader.\n *==========================================*/\n\nimport {\n contactDisplayName,\n normalizeContactEmail,\n readContactFacet,\n} from './contacts'\nimport { CRM_EMAIL_BODY_MAX, CRM_EMAIL_SUBJECT_MAX, type CrmScoped } from './crm'\n\n/** The most a template's name may hold. */\nexport const CRM_EMAIL_TEMPLATE_NAME_MAX = 80\n\n/**\n * The listing's window, and the ceiling the console's create respects.\n *\n * A LOOKUP rather than a list: the picker is a menu a rep opens, and two\n * hundred named letters for one workspace is past what anybody scrolls.\n * Bounded so the listen cannot grow with the org.\n */\nexport const CRM_EMAIL_TEMPLATES_LIMIT = 200\n\n/** A whole letter under a name, or a paragraph under one. */\nexport const CRM_EMAIL_TEMPLATE_KINDS = ['template', 'snippet'] as const\nexport type CrmEmailTemplateKind = (typeof CRM_EMAIL_TEMPLATE_KINDS)[number]\n\nexport const CRM_EMAIL_TEMPLATE_KIND_LABELS: Record<CrmEmailTemplateKind, string> = {\n template: 'Template',\n snippet: 'Snippet',\n}\n\nexport function isCrmEmailTemplateKind(value: unknown): value is CrmEmailTemplateKind {\n return (\n typeof value === 'string' &&\n (CRM_EMAIL_TEMPLATE_KINDS as readonly string[]).includes(value)\n )\n}\n\n/** The team's, or one person's. */\nexport const CRM_EMAIL_TEMPLATE_VISIBILITIES = ['shared', 'personal'] as const\nexport type CrmEmailTemplateVisibility = (typeof CRM_EMAIL_TEMPLATE_VISIBILITIES)[number]\n\nexport const CRM_EMAIL_TEMPLATE_VISIBILITY_LABELS: Record<CrmEmailTemplateVisibility, string> = {\n shared: 'Shared',\n personal: 'Personal',\n}\n\nexport function isCrmEmailTemplateVisibility(\n value: unknown,\n): value is CrmEmailTemplateVisibility {\n return (\n typeof value === 'string' &&\n (CRM_EMAIL_TEMPLATE_VISIBILITIES as readonly string[]).includes(value)\n )\n}\n\n/**\n * `orgs/{orgId}/crmEmailTemplates/{templateId}`.\n *\n * Scoped like a task (AGL-2637): under a site it carries that site's tokens\n * and `hostId`; written from the organization's own hub it carries the org\n * token and `hostId: null`, because a letter the whole workspace keeps is\n * not any one brand's. `ownerUid` is set on a personal template only, and\n * is the one member the listing shows it to.\n */\nexport interface CrmEmailTemplate extends Omit<CrmScoped, 'hostId'> {\n /** The site it was written under, or `null` for the organization's own. */\n hostId: string | null\n name: string\n /** The subject line a template fills in; `''` on a snippet. */\n subject: string\n /** Plain text, with merge fields — see {@link renderCrmMergeFields}. */\n body: string\n kind: CrmEmailTemplateKind\n visibility: CrmEmailTemplateVisibility\n /** The one member a personal template is listed for; absent on a shared one. */\n ownerUid?: string\n /** Who made it — `'api'` for a key, which has no uid. */\n createdByUid: string\n createdAtMs: number\n updatedAtMs: number\n}\n\n/** A template as a listener hands it back: the document plus its id. */\nexport type CrmEmailTemplateRow = CrmEmailTemplate & { $id: string }\n\n/**\n * Whether a template belongs in `uid`'s menu: every shared one, and the\n * personal ones they own. A colleague's personal template is readable under\n * the rules and hidden here — see the module header.\n */\nexport function crmEmailTemplateIsListed(\n template: Pick<CrmEmailTemplate, 'visibility' | 'ownerUid'>,\n uid: string | null | undefined,\n): boolean {\n if (template.visibility !== 'personal') return true\n return Boolean(uid) && template.ownerUid === uid\n}\n\nconst asText = (value: unknown, max: number): string =>\n typeof value === 'string' ? value.slice(0, max) : ''\n\nconst asMs = (value: unknown): number =>\n typeof value === 'number' && Number.isFinite(value) ? value : 0\n\n/**\n * A stored document held to shape, for a listener that reads it raw.\n *\n * Lenient where the rules are not: a kind or a visibility the union does not\n * name reads as the default, because a row some other writer produced is\n * still a letter somebody kept, and dropping it from the menu would lose it.\n */\nexport function normalizeCrmEmailTemplate(\n row: Record<string, unknown> | null | undefined,\n): CrmEmailTemplate {\n const data = row ?? {}\n const visibility = isCrmEmailTemplateVisibility(data['visibility'])\n ? data['visibility']\n : 'shared'\n const ownerUid = asText(data['ownerUid'], 128)\n return {\n name: asText(data['name'], CRM_EMAIL_TEMPLATE_NAME_MAX).trim(),\n subject: asText(data['subject'], CRM_EMAIL_SUBJECT_MAX),\n body: asText(data['body'], CRM_EMAIL_BODY_MAX),\n kind: isCrmEmailTemplateKind(data['kind']) ? data['kind'] : 'template',\n visibility,\n ...(visibility === 'personal' && ownerUid ? { ownerUid } : {}),\n createdByUid: asText(data['createdByUid'], 128),\n createdAtMs: asMs(data['createdAtMs']),\n updatedAtMs: asMs(data['updatedAtMs']),\n hostId: typeof data['hostId'] === 'string' ? data['hostId'] : null,\n visibleTo: Array.isArray(data['visibleTo'])\n ? (data['visibleTo'] as unknown[]).map(String)\n : [],\n createdAt: data['createdAt'],\n updatedAt: data['updatedAt'],\n }\n}\n\n/*==========================================\n * MERGE FIELDS.\n *==========================================*/\n\n/** The group a field belongs to, as the picker headings it. */\nexport type CrmMergeFieldGroup = 'Contact' | 'Lead' | 'Deal' | 'You' | 'Site'\n\nexport interface CrmMergeFieldDefinition {\n key: string\n label: string\n group: CrmMergeFieldGroup\n}\n\n/**\n * Every field the resolver knows, in the order the picker offers them.\n *\n * A fixed list rather than a path into the document, because a merge field\n * is a promise about what a person will read: `contact.title` has to mean\n * the job title on every send, whatever the document happens to call it\n * this year, and a free path would let a template reach into a note or a\n * consent map.\n */\nexport const CRM_MERGE_FIELDS: readonly CrmMergeFieldDefinition[] = [\n { key: 'contact.firstName', label: 'First name', group: 'Contact' },\n { key: 'contact.lastName', label: 'Last name', group: 'Contact' },\n { key: 'contact.name', label: 'Full name', group: 'Contact' },\n { key: 'contact.email', label: 'Email address', group: 'Contact' },\n { key: 'contact.company', label: 'Company', group: 'Contact' },\n { key: 'contact.title', label: 'Job title', group: 'Contact' },\n { key: 'lead.firstName', label: 'First name', group: 'Lead' },\n { key: 'lead.lastName', label: 'Last name', group: 'Lead' },\n { key: 'lead.name', label: 'Full name', group: 'Lead' },\n { key: 'lead.email', label: 'Email address', group: 'Lead' },\n { key: 'lead.company', label: 'Company', group: 'Lead' },\n { key: 'lead.title', label: 'Job title', group: 'Lead' },\n { key: 'deal.name', label: 'Deal name', group: 'Deal' },\n { key: 'deal.amount', label: 'Deal amount', group: 'Deal' },\n { key: 'sender.firstName', label: 'Your first name', group: 'You' },\n { key: 'sender.name', label: 'Your name', group: 'You' },\n { key: 'sender.email', label: 'Your email address', group: 'You' },\n { key: 'site.name', label: 'Site name', group: 'Site' },\n]\n\n/** `{{key}}` — the one spelling the resolver reads and the picker inserts. */\nexport function crmMergeFieldToken(key: string): string {\n return `{{${key}}}`\n}\n\n/**\n * What the resolver reads from: the documents as they are stored, so the\n * send route hands over what it already read and the dialog what it\n * fetched for the preview, with no projection in between to drift.\n */\nexport interface CrmMergeContext {\n /** The contact document, `orgs/{orgId}/contacts/{id}`. */\n contact?: Record<string, unknown> | null\n /**\n * The holder group whose facet names the contact — the sending site's,\n * since a name, a job title and a company are one holder's knowledge of\n * a person (`readContactFacet`). The canonical name is the fallback.\n */\n contactGroupId?: string | null\n /** The lead document, `hosts/{hostId}/leads/{id}`. */\n lead?: Record<string, unknown> | null\n /** The deal document, `orgs/{orgId}/deals/{id}`. */\n deal?: Record<string, unknown> | null\n /** The person sending, as their token or session names them. */\n sender?: { name?: string | null; email?: string | null } | null\n /** The site the email leaves from. */\n site?: { name?: string | null } | null\n /**\n * Fields a caller brings from a record of its own, keyed by the whole\n * `group.field` name — a plugin's `enrollment.personalLine`, say.\n *\n * Read only for a group the resolver above does not know, so no caller can\n * change what `contact.email` means. The fixed list is a promise about what\n * a person will read, and this is where a caller that makes its own promise\n * keeps it: the value is passed as written, never looked up by path.\n */\n extra?: Readonly<Record<string, string | null | undefined>> | null\n}\n\nexport interface CrmMergeResult {\n /** The text with every field replaced — by its value, or by nothing. */\n text: string\n /** The fields that rendered as nothing, unique, in order of appearance. */\n unresolved: string[]\n}\n\n/*\n * `{{ group.field }}`, spaces allowed inside the braces because a rep will\n * type them. A single brace pair or a bare word is left alone: a letter may\n * legitimately contain braces.\n */\nconst MERGE_FIELD_PATTERN = /\\{\\{\\s*([A-Za-z][A-Za-z0-9]*\\.[A-Za-z][A-Za-z0-9]*)\\s*\\}\\}/g\n\n/** The merge fields a text names, unique, in order of first appearance. */\nexport function crmMergeFieldsIn(text: string): string[] {\n const seen = new Set<string>()\n const fields: string[] = []\n for (const match of String(text ?? '').matchAll(MERGE_FIELD_PATTERN)) {\n const key = match[1]\n if (seen.has(key)) continue\n seen.add(key)\n fields.push(key)\n }\n return fields\n}\n\nexport function hasCrmMergeFields(text: string): boolean {\n MERGE_FIELD_PATTERN.lastIndex = 0\n return MERGE_FIELD_PATTERN.test(String(text ?? ''))\n}\n\n/** A name as one string → its first word and the rest. */\nexport function splitPersonName(name: unknown): { firstName: string; lastName: string } {\n const words = String(name ?? '')\n .trim()\n .split(/\\s+/)\n .filter(Boolean)\n return {\n firstName: words[0] ?? '',\n lastName: words.slice(1).join(' '),\n }\n}\n\n/**\n * A deal's amount as a letter prints it.\n *\n * A fixed locale rather than the reader's, on purpose: the server has no\n * reader, and the preview has to print what the server will send. Cents\n * that are not a number render as nothing, so a deal with no value greets\n * nobody with \"$0.00\".\n */\nexport function formatCrmMergeAmount(cents: unknown, currency: unknown): string {\n if (typeof cents !== 'number' || !Number.isFinite(cents)) return ''\n const code = String(currency || 'usd').toUpperCase()\n const amount = cents / 100\n try {\n return new Intl.NumberFormat('en-US', { style: 'currency', currency: code }).format(amount)\n } catch {\n return `${amount.toFixed(2)} ${code}`\n }\n}\n\nconst text = (value: unknown): string => (typeof value === 'string' ? value.trim() : '')\n\n/** One field's value from the context, or `''` for anything it cannot answer. */\nfunction resolveCrmMergeField(key: string, context: CrmMergeContext): string {\n const [group, field] = key.split('.')\n switch (group) {\n case 'contact': {\n const contact = context.contact\n if (!contact) return ''\n const groupId = text(context.contactGroupId)\n const facet = readContactFacet(contact, groupId)\n const name = groupId\n ? contactDisplayName(contact, groupId)\n : text(contact['name'])\n /*\n * The holder's own first and last names when it keeps them (AGL-3515)\n * — a two-word first name or a particle in a surname is then what the\n * person wrote — and the display name split at its first space when\n * it keeps neither.\n */\n const parts =\n text(facet.firstName) || text(facet.lastName)\n ? { firstName: text(facet.firstName), lastName: text(facet.lastName) }\n : splitPersonName(name)\n switch (field) {\n case 'firstName':\n return parts.firstName\n case 'lastName':\n return parts.lastName\n case 'name':\n return name\n case 'email':\n return normalizeContactEmail(contact['email']) ?? ''\n case 'company':\n return text(facet.companyName)\n case 'title':\n return text(facet.jobTitle)\n default:\n return ''\n }\n }\n case 'lead': {\n const lead = context.lead\n if (!lead) return ''\n /*\n * The lead's own first and last names when it keeps them (AGL-3513),\n * as the contact's are read; the name split at its first space when\n * it keeps neither.\n */\n const parts =\n text(lead['firstName']) || text(lead['lastName'])\n ? { firstName: text(lead['firstName']), lastName: text(lead['lastName']) }\n : splitPersonName(lead['name'])\n switch (field) {\n case 'firstName':\n return parts.firstName\n case 'lastName':\n return parts.lastName\n case 'name':\n return text(lead['name'])\n case 'email':\n return normalizeContactEmail(lead['email']) ?? ''\n // The lead's own profile (AGL-3231): the company as text, the title.\n case 'company':\n return text(lead['company'])\n case 'title':\n return text(lead['jobTitle'])\n default:\n return ''\n }\n }\n case 'deal': {\n const deal = context.deal\n if (!deal) return ''\n switch (field) {\n case 'name':\n return text(deal['title'])\n case 'amount':\n return formatCrmMergeAmount(deal['amountCents'], deal['currency'])\n default:\n return ''\n }\n }\n case 'sender': {\n const sender = context.sender\n if (!sender) return ''\n switch (field) {\n case 'firstName':\n return splitPersonName(sender.name).firstName\n case 'name':\n return text(sender.name)\n case 'email':\n return normalizeContactEmail(sender.email) ?? ''\n default:\n return ''\n }\n }\n case 'site':\n return field === 'name' ? text(context.site?.name) : ''\n default: {\n const extra = context.extra\n return extra && Object.prototype.hasOwnProperty.call(extra, key)\n ? text(extra[key])\n : ''\n }\n }\n}\n\n/**\n * Every merge field in `text` replaced from `context`, and the ones that\n * had nothing to say. Pure, and never throws: a missing record, an unknown\n * field, a malformed context each render as nothing and are named in\n * `unresolved`.\n */\nexport function resolveCrmMergeFields(\n text: string,\n context: CrmMergeContext | null | undefined,\n): CrmMergeResult {\n const source = String(text ?? '')\n const ctx = context ?? {}\n const unresolved = new Set<string>()\n const rendered = source.replace(MERGE_FIELD_PATTERN, (_match, key: string) => {\n const value = resolveCrmMergeField(key, ctx)\n if (!value) unresolved.add(key)\n return value\n })\n return { text: rendered, unresolved: [...unresolved] }\n}\n\n/** {@link resolveCrmMergeFields}, text alone — what the send route sends. */\nexport function renderCrmMergeFields(\n text: string,\n context: CrmMergeContext | null | undefined,\n): string {\n return resolveCrmMergeFields(text, context).text\n}\n\n/** What the dialog says under a draft whose fields have nothing to fill. */\nexport function crmMergeUnresolvedMessage(unresolved: readonly string[]): string {\n if (unresolved.length === 0) return ''\n const count = unresolved.length\n const noun = count === 1 ? '1 field has no value' : `${count} fields have no value`\n return `${noun}: ${unresolved.map(crmMergeFieldToken).join(', ')}`\n}\n"],"names":["contactDisplayName","normalizeContactEmail","readContactFacet","CRM_EMAIL_BODY_MAX","CRM_EMAIL_SUBJECT_MAX","CRM_EMAIL_TEMPLATE_NAME_MAX","CRM_EMAIL_TEMPLATES_LIMIT","CRM_EMAIL_TEMPLATE_KINDS","CRM_EMAIL_TEMPLATE_KIND_LABELS","template","snippet","isCrmEmailTemplateKind","value","includes","CRM_EMAIL_TEMPLATE_VISIBILITIES","CRM_EMAIL_TEMPLATE_VISIBILITY_LABELS","shared","personal","isCrmEmailTemplateVisibility","crmEmailTemplateIsListed","uid","visibility","Boolean","ownerUid","asText","max","slice","asMs","Number","isFinite","normalizeCrmEmailTemplate","row","data","name","trim","subject","body","kind","createdByUid","createdAtMs","updatedAtMs","hostId","visibleTo","Array","isArray","map","String","createdAt","updatedAt","CRM_MERGE_FIELDS","key","label","group","crmMergeFieldToken","MERGE_FIELD_PATTERN","crmMergeFieldsIn","text","seen","Set","fields","match","matchAll","has","add","push","hasCrmMergeFields","lastIndex","test","splitPersonName","words","split","filter","firstName","lastName","join","formatCrmMergeAmount","cents","currency","code","toUpperCase","amount","Intl","NumberFormat","style","format","toFixed","resolveCrmMergeField","context","field","contact","groupId","contactGroupId","facet","parts","companyName","jobTitle","lead","deal","sender","email","site","extra","Object","prototype","hasOwnProperty","call","resolveCrmMergeFields","source","ctx","unresolved","rendered","replace","_match","renderCrmMergeFields","crmMergeUnresolvedMessage","length","count","noun"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/crm-email-templates.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 * EMAIL TEMPLATES, SNIPPETS AND MERGE FIELDS (AGL-2658).\n *\n * A one-to-one email is a letter, and a rep writes the same three letters\n * all week: the follow-up, the proposal cover note, the \"still keen?\". A\n * TEMPLATE is one of those letters kept under a name — a subject and a body\n * the dialog fills in whole. A SNIPPET is a paragraph kept under a name —\n * a signature block, a pricing line — inserted where the caret is. Both\n * carry MERGE FIELDS, `{{contact.firstName}}` and the like, which the send\n * route fills from the record the email is about, so a template written\n * once reads as a letter to each person it is sent to.\n *\n * ## One resolver, run twice\n *\n * The server resolves at send time, off the documents it has already read\n * to find the recipient, and what it writes to the timeline is the letter\n * as it left. The dialog runs the SAME resolver over the same documents to\n * show the rep what will go out, so the preview and the message cannot\n * disagree. The resolver is pure and lives here, beside the model, so\n * neither caller can drift into its own grammar.\n *\n * ## What a field with no value does\n *\n * It renders as nothing. A template that greets `{{contact.firstName}}` is\n * sent to a contact known only by email as \"Hi ,\" — which is why the dialog\n * counts the empty fields and says so before the rep clicks Send, and why\n * an unknown field name renders as nothing too rather than as its own\n * braces: a typo in a merge field must not reach a customer as `{{contct.\n * firstName}}`. Nothing is escaped, because the mail is plain text.\n *\n * ## Shared or personal\n *\n * A template is either the team's — every CRM editor may use and change it\n * — or one person's, listed for its owner alone. The rules gate the writes\n * (`cloud/firebase-firestore.rules`); the listing hides a colleague's\n * personal rows the way a private saved view is hidden (AGL-2617), because\n * a rule per reader would need a query per reader.\n *==========================================*/\n\nimport {\n contactDisplayName,\n normalizeContactEmail,\n readContactFacet,\n} from './contacts'\nimport { CRM_EMAIL_BODY_MAX, CRM_EMAIL_SUBJECT_MAX, type CrmScoped } from './crm'\n\n/** The most a template's name may hold. */\nexport const CRM_EMAIL_TEMPLATE_NAME_MAX = 80\n\n/**\n * The listing's window, and the ceiling the console's create respects.\n *\n * A LOOKUP rather than a list: the picker is a menu a rep opens, and two\n * hundred named letters for one workspace is past what anybody scrolls.\n * Bounded so the listen cannot grow with the org.\n */\nexport const CRM_EMAIL_TEMPLATES_LIMIT = 200\n\n/** A whole letter under a name, or a paragraph under one. */\nexport const CRM_EMAIL_TEMPLATE_KINDS = ['template', 'snippet'] as const\nexport type CrmEmailTemplateKind = (typeof CRM_EMAIL_TEMPLATE_KINDS)[number]\n\nexport const CRM_EMAIL_TEMPLATE_KIND_LABELS: Record<CrmEmailTemplateKind, string> = {\n template: 'Template',\n snippet: 'Snippet',\n}\n\nexport function isCrmEmailTemplateKind(value: unknown): value is CrmEmailTemplateKind {\n return (\n typeof value === 'string' &&\n (CRM_EMAIL_TEMPLATE_KINDS as readonly string[]).includes(value)\n )\n}\n\n/** The team's, or one person's. */\nexport const CRM_EMAIL_TEMPLATE_VISIBILITIES = ['shared', 'personal'] as const\nexport type CrmEmailTemplateVisibility = (typeof CRM_EMAIL_TEMPLATE_VISIBILITIES)[number]\n\nexport const CRM_EMAIL_TEMPLATE_VISIBILITY_LABELS: Record<CrmEmailTemplateVisibility, string> = {\n shared: 'Shared',\n personal: 'Personal',\n}\n\nexport function isCrmEmailTemplateVisibility(\n value: unknown,\n): value is CrmEmailTemplateVisibility {\n return (\n typeof value === 'string' &&\n (CRM_EMAIL_TEMPLATE_VISIBILITIES as readonly string[]).includes(value)\n )\n}\n\n/**\n * `orgs/{orgId}/crmEmailTemplates/{templateId}`.\n *\n * Scoped like a task (AGL-2637): under a site it carries that site's tokens\n * and `hostId`; written from the organization's own hub it carries the org\n * token and `hostId: null`, because a letter the whole workspace keeps is\n * not any one brand's. `ownerUid` is set on a personal template only, and\n * is the one member the listing shows it to.\n */\nexport interface CrmEmailTemplate extends Omit<CrmScoped, 'hostId'> {\n /** The site it was written under, or `null` for the organization's own. */\n hostId: string | null\n name: string\n /** The subject line a template fills in; `''` on a snippet. */\n subject: string\n /** Plain text, with merge fields — see {@link renderCrmMergeFields}. */\n body: string\n kind: CrmEmailTemplateKind\n visibility: CrmEmailTemplateVisibility\n /** The one member a personal template is listed for; absent on a shared one. */\n ownerUid?: string\n /** Who made it — `'api'` for a key, which has no uid. */\n createdByUid: string\n createdAtMs: number\n updatedAtMs: number\n}\n\n/** A template as a listener hands it back: the document plus its id. */\nexport type CrmEmailTemplateRow = CrmEmailTemplate & { $id: string }\n\n/**\n * Whether a template belongs in `uid`'s menu: every shared one, and the\n * personal ones they own. A colleague's personal template is readable under\n * the rules and hidden here — see the module header.\n */\nexport function crmEmailTemplateIsListed(\n template: Pick<CrmEmailTemplate, 'visibility' | 'ownerUid'>,\n uid: string | null | undefined,\n): boolean {\n if (template.visibility !== 'personal') return true\n return Boolean(uid) && template.ownerUid === uid\n}\n\nconst asText = (value: unknown, max: number): string =>\n typeof value === 'string' ? value.slice(0, max) : ''\n\nconst asMs = (value: unknown): number =>\n typeof value === 'number' && Number.isFinite(value) ? value : 0\n\n/**\n * A stored document held to shape, for a listener that reads it raw.\n *\n * Lenient where the rules are not: a kind or a visibility the union does not\n * name reads as the default, because a row some other writer produced is\n * still a letter somebody kept, and dropping it from the menu would lose it.\n */\nexport function normalizeCrmEmailTemplate(\n row: Record<string, unknown> | null | undefined,\n): CrmEmailTemplate {\n const data = row ?? {}\n const visibility = isCrmEmailTemplateVisibility(data['visibility'])\n ? data['visibility']\n : 'shared'\n const ownerUid = asText(data['ownerUid'], 128)\n return {\n name: asText(data['name'], CRM_EMAIL_TEMPLATE_NAME_MAX).trim(),\n subject: asText(data['subject'], CRM_EMAIL_SUBJECT_MAX),\n body: asText(data['body'], CRM_EMAIL_BODY_MAX),\n kind: isCrmEmailTemplateKind(data['kind']) ? data['kind'] : 'template',\n visibility,\n ...(visibility === 'personal' && ownerUid ? { ownerUid } : {}),\n createdByUid: asText(data['createdByUid'], 128),\n createdAtMs: asMs(data['createdAtMs']),\n updatedAtMs: asMs(data['updatedAtMs']),\n hostId: typeof data['hostId'] === 'string' ? data['hostId'] : null,\n visibleTo: Array.isArray(data['visibleTo'])\n ? (data['visibleTo'] as unknown[]).map(String)\n : [],\n createdAt: data['createdAt'],\n updatedAt: data['updatedAt'],\n }\n}\n\n/*==========================================\n * MERGE FIELDS.\n *==========================================*/\n\n/** The group a field belongs to, as the picker headings it. */\nexport type CrmMergeFieldGroup = 'Contact' | 'Lead' | 'Deal' | 'You' | 'Site'\n\nexport interface CrmMergeFieldDefinition {\n key: string\n label: string\n group: CrmMergeFieldGroup\n}\n\n/**\n * Every field the resolver knows, in the order the picker offers them.\n *\n * A fixed list rather than a path into the document, because a merge field\n * is a promise about what a person will read: `contact.title` has to mean\n * the job title on every send, whatever the document happens to call it\n * this year, and a free path would let a template reach into a note or a\n * consent map.\n */\nexport const CRM_MERGE_FIELDS: readonly CrmMergeFieldDefinition[] = [\n { key: 'contact.firstName', label: 'First name', group: 'Contact' },\n { key: 'contact.lastName', label: 'Last name', group: 'Contact' },\n { key: 'contact.name', label: 'Full name', group: 'Contact' },\n { key: 'contact.email', label: 'Email address', group: 'Contact' },\n { key: 'contact.company', label: 'Company', group: 'Contact' },\n { key: 'contact.title', label: 'Job title', group: 'Contact' },\n { key: 'lead.firstName', label: 'First name', group: 'Lead' },\n { key: 'lead.lastName', label: 'Last name', group: 'Lead' },\n { key: 'lead.name', label: 'Full name', group: 'Lead' },\n { key: 'lead.email', label: 'Email address', group: 'Lead' },\n { key: 'lead.company', label: 'Company', group: 'Lead' },\n { key: 'lead.title', label: 'Job title', group: 'Lead' },\n { key: 'deal.name', label: 'Deal name', group: 'Deal' },\n { key: 'deal.amount', label: 'Deal amount', group: 'Deal' },\n { key: 'sender.firstName', label: 'Your first name', group: 'You' },\n { key: 'sender.name', label: 'Your name', group: 'You' },\n { key: 'sender.email', label: 'Your email address', group: 'You' },\n { key: 'site.name', label: 'Site name', group: 'Site' },\n]\n\n/** `{{key}}` — the one spelling the resolver reads and the picker inserts. */\nexport function crmMergeFieldToken(key: string): string {\n return `{{${key}}}`\n}\n\n/**\n * What the resolver reads from: the documents as they are stored, so the\n * send route hands over what it already read and the dialog what it\n * fetched for the preview, with no projection in between to drift.\n */\nexport interface CrmMergeContext {\n /** The contact document, `orgs/{orgId}/contacts/{id}`. */\n contact?: Record<string, unknown> | null\n /**\n * The holder group whose facet names the contact — the sending site's,\n * since a name, a job title and a company are one holder's knowledge of\n * a person (`readContactFacet`). The canonical name is the fallback.\n */\n contactGroupId?: string | null\n /** The lead document, `hosts/{hostId}/leads/{id}`. */\n lead?: Record<string, unknown> | null\n /** The deal document, `orgs/{orgId}/deals/{id}`. */\n deal?: Record<string, unknown> | null\n /** The person sending, as their token or session names them. */\n sender?: { name?: string | null; email?: string | null } | null\n /** The site the email leaves from. */\n site?: { name?: string | null } | null\n /**\n * Fields a caller brings from a record of its own, keyed by the whole\n * `group.field` name — a plugin's `enrollment.personalLine`, say.\n *\n * Read only for a group the resolver above does not know, so no caller can\n * change what `contact.email` means. The fixed list is a promise about what\n * a person will read, and this is where a caller that makes its own promise\n * keeps it: the value is passed as written, never looked up by path.\n */\n extra?: Readonly<Record<string, string | null | undefined>> | null\n}\n\nexport interface CrmMergeResult {\n /** The text with every field replaced — by its value, or by nothing. */\n text: string\n /** The fields that rendered as nothing, unique, in order of appearance. */\n unresolved: string[]\n}\n\n/*\n * `{{ group.field }}`, spaces allowed inside the braces because a rep will\n * type them, and an optional `|fallback` — `{{contact.firstName|there}}` —\n * printed when the field has no value (AGL-3458), the campaign tags' own\n * spelling. A single brace pair or a bare word is left alone: a letter may\n * legitimately contain braces.\n */\nconst MERGE_FIELD_PATTERN =\n /\\{\\{\\s*([A-Za-z][A-Za-z0-9]*\\.[A-Za-z][A-Za-z0-9]*)\\s*(?:\\|([^}]*))?\\}\\}/g\n\n/** The merge fields a text names, unique, in order of first appearance. */\nexport function crmMergeFieldsIn(text: string): string[] {\n const seen = new Set<string>()\n const fields: string[] = []\n for (const match of String(text ?? '').matchAll(MERGE_FIELD_PATTERN)) {\n const key = match[1]\n if (seen.has(key)) continue\n seen.add(key)\n fields.push(key)\n }\n return fields\n}\n\nexport function hasCrmMergeFields(text: string): boolean {\n MERGE_FIELD_PATTERN.lastIndex = 0\n return MERGE_FIELD_PATTERN.test(String(text ?? ''))\n}\n\n/** A name as one string → its first word and the rest. */\nexport function splitPersonName(name: unknown): { firstName: string; lastName: string } {\n const words = String(name ?? '')\n .trim()\n .split(/\\s+/)\n .filter(Boolean)\n return {\n firstName: words[0] ?? '',\n lastName: words.slice(1).join(' '),\n }\n}\n\n/**\n * A deal's amount as a letter prints it.\n *\n * A fixed locale rather than the reader's, on purpose: the server has no\n * reader, and the preview has to print what the server will send. Cents\n * that are not a number render as nothing, so a deal with no value greets\n * nobody with \"$0.00\".\n */\nexport function formatCrmMergeAmount(cents: unknown, currency: unknown): string {\n if (typeof cents !== 'number' || !Number.isFinite(cents)) return ''\n const code = String(currency || 'usd').toUpperCase()\n const amount = cents / 100\n try {\n return new Intl.NumberFormat('en-US', { style: 'currency', currency: code }).format(amount)\n } catch {\n return `${amount.toFixed(2)} ${code}`\n }\n}\n\nconst text = (value: unknown): string => (typeof value === 'string' ? value.trim() : '')\n\n/** One field's value from the context, or `''` for anything it cannot answer. */\nfunction resolveCrmMergeField(key: string, context: CrmMergeContext): string {\n const [group, field] = key.split('.')\n switch (group) {\n case 'contact': {\n const contact = context.contact\n if (!contact) return ''\n const groupId = text(context.contactGroupId)\n const facet = readContactFacet(contact, groupId)\n const name = groupId\n ? contactDisplayName(contact, groupId)\n : text(contact['name'])\n /*\n * The holder's own first and last names when it keeps them (AGL-3515)\n * — a two-word first name or a particle in a surname is then what the\n * person wrote — and the display name split at its first space when\n * it keeps neither.\n */\n const parts =\n text(facet.firstName) || text(facet.lastName)\n ? { firstName: text(facet.firstName), lastName: text(facet.lastName) }\n : splitPersonName(name)\n switch (field) {\n case 'firstName':\n return parts.firstName\n case 'lastName':\n return parts.lastName\n case 'name':\n return name\n case 'email':\n return normalizeContactEmail(contact['email']) ?? ''\n case 'company':\n return text(facet.companyName)\n case 'title':\n return text(facet.jobTitle)\n default:\n return ''\n }\n }\n case 'lead': {\n const lead = context.lead\n if (!lead) return ''\n /*\n * The lead's own first and last names when it keeps them (AGL-3513),\n * as the contact's are read; the name split at its first space when\n * it keeps neither.\n */\n const parts =\n text(lead['firstName']) || text(lead['lastName'])\n ? { firstName: text(lead['firstName']), lastName: text(lead['lastName']) }\n : splitPersonName(lead['name'])\n switch (field) {\n case 'firstName':\n return parts.firstName\n case 'lastName':\n return parts.lastName\n case 'name':\n return text(lead['name'])\n case 'email':\n return normalizeContactEmail(lead['email']) ?? ''\n // The lead's own profile (AGL-3231): the company as text, the title.\n case 'company':\n return text(lead['company'])\n case 'title':\n return text(lead['jobTitle'])\n default:\n return ''\n }\n }\n case 'deal': {\n const deal = context.deal\n if (!deal) return ''\n switch (field) {\n case 'name':\n return text(deal['title'])\n case 'amount':\n return formatCrmMergeAmount(deal['amountCents'], deal['currency'])\n default:\n return ''\n }\n }\n case 'sender': {\n const sender = context.sender\n if (!sender) return ''\n switch (field) {\n case 'firstName':\n return splitPersonName(sender.name).firstName\n case 'name':\n return text(sender.name)\n case 'email':\n return normalizeContactEmail(sender.email) ?? ''\n default:\n return ''\n }\n }\n case 'site':\n return field === 'name' ? text(context.site?.name) : ''\n default: {\n const extra = context.extra\n return extra && Object.prototype.hasOwnProperty.call(extra, key)\n ? text(extra[key])\n : ''\n }\n }\n}\n\n/**\n * Every merge field in `text` replaced from `context`, and the ones that\n * had nothing to say. Pure, and never throws: a missing record, an unknown\n * field, a malformed context each render as nothing and are named in\n * `unresolved`.\n */\nexport function resolveCrmMergeFields(\n text: string,\n context: CrmMergeContext | null | undefined,\n): CrmMergeResult {\n const source = String(text ?? '')\n const ctx = context ?? {}\n const unresolved = new Set<string>()\n const rendered = source.replace(\n MERGE_FIELD_PATTERN,\n (_match, key: string, fallback: string | undefined) => {\n const value = resolveCrmMergeField(key, ctx) || String(fallback ?? '').trim()\n if (!value) unresolved.add(key)\n return value\n },\n )\n return { text: rendered, unresolved: [...unresolved] }\n}\n\n/** {@link resolveCrmMergeFields}, text alone — what the send route sends. */\nexport function renderCrmMergeFields(\n text: string,\n context: CrmMergeContext | null | undefined,\n): string {\n return resolveCrmMergeFields(text, context).text\n}\n\n/** What the dialog says under a draft whose fields have nothing to fill. */\nexport function crmMergeUnresolvedMessage(unresolved: readonly string[]): string {\n if (unresolved.length === 0) return ''\n const count = unresolved.length\n const noun = count === 1 ? '1 field has no value' : `${count} fields have no value`\n return `${noun}: ${unresolved.map(crmMergeFieldToken).join(', ')}`\n}\n"],"names":["contactDisplayName","normalizeContactEmail","readContactFacet","CRM_EMAIL_BODY_MAX","CRM_EMAIL_SUBJECT_MAX","CRM_EMAIL_TEMPLATE_NAME_MAX","CRM_EMAIL_TEMPLATES_LIMIT","CRM_EMAIL_TEMPLATE_KINDS","CRM_EMAIL_TEMPLATE_KIND_LABELS","template","snippet","isCrmEmailTemplateKind","value","includes","CRM_EMAIL_TEMPLATE_VISIBILITIES","CRM_EMAIL_TEMPLATE_VISIBILITY_LABELS","shared","personal","isCrmEmailTemplateVisibility","crmEmailTemplateIsListed","uid","visibility","Boolean","ownerUid","asText","max","slice","asMs","Number","isFinite","normalizeCrmEmailTemplate","row","data","name","trim","subject","body","kind","createdByUid","createdAtMs","updatedAtMs","hostId","visibleTo","Array","isArray","map","String","createdAt","updatedAt","CRM_MERGE_FIELDS","key","label","group","crmMergeFieldToken","MERGE_FIELD_PATTERN","crmMergeFieldsIn","text","seen","Set","fields","match","matchAll","has","add","push","hasCrmMergeFields","lastIndex","test","splitPersonName","words","split","filter","firstName","lastName","join","formatCrmMergeAmount","cents","currency","code","toUpperCase","amount","Intl","NumberFormat","style","format","toFixed","resolveCrmMergeField","context","field","contact","groupId","contactGroupId","facet","parts","companyName","jobTitle","lead","deal","sender","email","site","extra","Object","prototype","hasOwnProperty","call","resolveCrmMergeFields","source","ctx","unresolved","rendered","replace","_match","fallback","renderCrmMergeFields","crmMergeUnresolvedMessage","length","count","noun"],"mappings":"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"}
|
|
@@ -503,7 +503,7 @@ export interface CrmDeal extends CrmScoped {
|
|
|
503
503
|
* nothing scheduled" is asked of a LIST — a column, a filter, a report
|
|
504
504
|
* tile — and a list cannot afford a task query per row. Every server
|
|
505
505
|
* writer of a task recomputes it for the records the task names
|
|
506
|
-
* (`recomputeCrmNextTaskAt` in the
|
|
506
|
+
* (`recomputeCrmNextTaskAt` in the CRM's server), and a client-direct
|
|
507
507
|
* task write asks the console route to do the same; the Fields section
|
|
508
508
|
* offers a one-off recompute for the records written before the field
|
|
509
509
|
* existed. Absent on such a record, which every reader treats as `null`.
|