@aglyn/aglyn 1.0.0-beta.164 → 1.0.0-beta.166

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aglyn/aglyn",
3
- "version": "1.0.0-beta.164",
3
+ "version": "1.0.0-beta.166",
4
4
  "license": "Apache-2.0",
5
5
  "homepage": "https://aglyn.com",
6
6
  "repository": {
@@ -33,15 +33,15 @@
33
33
  "./package.json": "./package.json"
34
34
  },
35
35
  "dependencies": {
36
- "@aglyn/shared-data-enums": "1.0.0-beta.164",
37
- "@aglyn/shared-data-mdi": "1.0.0-beta.164",
38
- "@aglyn/shared-data-types": "1.0.0-beta.164",
39
- "@aglyn/shared-util-email": "1.0.0-beta.164",
40
- "@aglyn/shared-util-http": "1.0.0-beta.164",
41
- "@aglyn/shared-util-logger": "1.0.0-beta.164",
42
- "@aglyn/shared-util-timestamp": "1.0.0-beta.164",
43
- "@aglyn/shared-util-tools": "1.0.0-beta.164",
44
- "@aglyn/shared-util-vendor": "1.0.0-beta.164",
36
+ "@aglyn/shared-data-enums": "1.0.0-beta.166",
37
+ "@aglyn/shared-data-mdi": "1.0.0-beta.166",
38
+ "@aglyn/shared-data-types": "1.0.0-beta.166",
39
+ "@aglyn/shared-util-email": "1.0.0-beta.166",
40
+ "@aglyn/shared-util-http": "1.0.0-beta.166",
41
+ "@aglyn/shared-util-logger": "1.0.0-beta.166",
42
+ "@aglyn/shared-util-timestamp": "1.0.0-beta.166",
43
+ "@aglyn/shared-util-tools": "1.0.0-beta.166",
44
+ "@aglyn/shared-util-vendor": "1.0.0-beta.166",
45
45
  "@data-driven-forms/react-form-renderer": "^4.2.0",
46
46
  "@msgpack/msgpack": "^3.1.3",
47
47
  "@types/unist": "^3.0.3",
@@ -91,14 +91,25 @@ export declare function crmOrgSectionHref(orgSlug: string, section: string): str
91
91
  /**
92
92
  * One record's page at the org level.
93
93
  *
94
- * A lead is excluded by the type rather than by a runtime check: a lead's id
95
- * is a PERSON KEY, the same on every site that met the person, and
96
- * `hosts/{hostId}/leads` is host-scoped by path — so an org-level address
97
- * has to name the site as well, which is {@link crmOrgLeadHref}.
94
+ * A LEAD IS NO LONGER EXCLUDED (AGL-3275). It was, by the type rather than a
95
+ * runtime check, because a lead's id is a person key — the same on every site
96
+ * that met the person — while `hosts/{hostId}/leads` was host-scoped by path,
97
+ * so two sites held two documents carrying one id and an org-level address
98
+ * had to name the site to tell them apart. One org collection makes the
99
+ * person key unambiguous, so a lead addresses like every other record.
98
100
  */
99
- export declare function crmOrgRecordHref(orgSlug: string, kind: Exclude<CrmRecordKind, 'lead'>, id: string): string;
100
- /** One site's lead, addressed from the org hub: the site, then the lead. */
101
- export declare function crmOrgLeadHref(orgSlug: string, hostId: string, leadId: string): string;
101
+ export declare function crmOrgRecordHref(orgSlug: string, kind: CrmRecordKind, id: string): string;
102
+ /**
103
+ * A lead addressed from the org hub.
104
+ *
105
+ * Kept as its own name so the callers that had a `hostId` in hand do not have
106
+ * to prove they no longer need one; it ignores the site and is exactly
107
+ * `crmOrgRecordHref(orgSlug, 'lead', leadId)`. New callers should use that.
108
+ *
109
+ * @deprecated Use {@link crmOrgRecordHref}. Removed with the host-path
110
+ * fallback in AGL-3277.
111
+ */
112
+ export declare function crmOrgLeadHref(orgSlug: string, _hostId: string, leadId: string): string;
102
113
  /**
103
114
  * The query keys a site record page reads on arrival, so a CRM timeline can
104
115
  * open the submission or the order it names rather than the list it sits
@@ -74,15 +74,26 @@
74
74
  /**
75
75
  * One record's page at the org level.
76
76
  *
77
- * A lead is excluded by the type rather than by a runtime check: a lead's id
78
- * is a PERSON KEY, the same on every site that met the person, and
79
- * `hosts/{hostId}/leads` is host-scoped by path — so an org-level address
80
- * has to name the site as well, which is {@link crmOrgLeadHref}.
77
+ * A LEAD IS NO LONGER EXCLUDED (AGL-3275). It was, by the type rather than a
78
+ * runtime check, because a lead's id is a person key — the same on every site
79
+ * that met the person — while `hosts/{hostId}/leads` was host-scoped by path,
80
+ * so two sites held two documents carrying one id and an org-level address
81
+ * had to name the site to tell them apart. One org collection makes the
82
+ * person key unambiguous, so a lead addresses like every other record.
81
83
  */ export function crmOrgRecordHref(orgSlug, kind, id) {
82
84
  return `${crmOrgSectionHref(orgSlug, CRM_RECORD_SECTIONS[kind])}/${encodeURIComponent(id)}`;
83
85
  }
84
- /** One site's lead, addressed from the org hub: the site, then the lead. */ export function crmOrgLeadHref(orgSlug, hostId, leadId) {
85
- return `${crmOrgSectionHref(orgSlug, CRM_RECORD_SECTIONS.lead)}/${encodeURIComponent(hostId)}/${encodeURIComponent(leadId)}`;
86
+ /**
87
+ * A lead addressed from the org hub.
88
+ *
89
+ * Kept as its own name so the callers that had a `hostId` in hand do not have
90
+ * to prove they no longer need one; it ignores the site and is exactly
91
+ * `crmOrgRecordHref(orgSlug, 'lead', leadId)`. New callers should use that.
92
+ *
93
+ * @deprecated Use {@link crmOrgRecordHref}. Removed with the host-path
94
+ * fallback in AGL-3277.
95
+ */ export function crmOrgLeadHref(orgSlug, _hostId, leadId) {
96
+ return crmOrgRecordHref(orgSlug, 'lead', leadId);
86
97
  }
87
98
  /**
88
99
  * The query keys a site record page reads on arrival, so a CRM timeline can
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/console-record-links.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { buildRoute, Route } from './console-routes'\nimport type { ContactInteraction, ContactSource } from './contacts'\n\n/**\n * Where the console's records live, for a surface that is not the one the\n * record belongs to (AGL-2622).\n *\n * ## Why these addresses are built here and not where they are used\n *\n * A CRM record is reached from six surfaces that are not the CRM — the\n * top-bar search, an Inbox row, a form's page, an order, a booking, a site\n * user — and a CRM timeline points back at four of them. Two of those\n * readers are the console APP, which the module boundaries forbid from\n * importing a feature plugin at all: a plugin reaches the app only through\n * the generated loader manifests, so the app cannot ask the CRM for its own\n * route builder. The CRM plugin's `crmRoutes(basePath)` stays the builder\n * inside the plugin family; this module is the same address spelled once\n * for the console side, in the shared library both may import, and the\n * plugin's builder is pinned against it by a spec so the two cannot drift.\n *\n * ## Why the arguments are the two route params and not a host document\n *\n * Every site console page is addressed by `/{orgSlug}/hosts/{host}/…`, and\n * both segments are already in the URL a surface is rendered on. Building\n * from them costs nothing; resolving them from a host id costs two document\n * reads (`hostIndex`, then the org) on every open of a card, to render a\n * link. `useConsoleHubPath` in the marketing plugin makes the same choice\n * for the same reason.\n */\nexport interface SiteConsoleContext {\n orgSlug: string\n host: string\n}\n\n/** The CRM's nav slug — the `[pluginSlug]` segment the shell resolves. */\nexport const CRM_CONSOLE_SLUG = 'crm'\n\n/** The four record kinds a surface outside the CRM may name. */\nexport type CrmRecordKind = 'contact' | 'lead' | 'company' | 'deal'\n\n/**\n * The hub section each kind's record lives under, which is also the list a\n * kind-only link lands on. Named here rather than in the plugin's section\n * registry so the console app can address a record without the plugin.\n */\nexport const CRM_RECORD_SECTIONS: Record<CrmRecordKind, string> = {\n contact: 'contacts',\n lead: 'leads',\n company: 'companies',\n deal: 'deals',\n}\n\n/** `/{orgSlug}/hosts/{host}/crm` — the CRM hub on the site being read. */\nexport function crmHubHref(context: SiteConsoleContext): string {\n return buildRoute(Route.HOST_PLUGIN, {\n orgSlug: context.orgSlug,\n host: context.host,\n pluginSlug: CRM_CONSOLE_SLUG,\n })\n}\n\n/** One section of the hub — the Leads list, the Deals board. */\nexport function crmSectionHref(\n context: SiteConsoleContext,\n section: string,\n): string {\n return `${crmHubHref(context)}/${section}`\n}\n\n/**\n * One record's own page. The id is URL-encoded because a Firestore id is\n * opaque — the console mints its own, but an import or an API caller may\n * not, and a slash in an id would otherwise read as a further segment.\n */\nexport function crmRecordHref(\n context: SiteConsoleContext,\n kind: CrmRecordKind,\n id: string,\n): string {\n return `${crmSectionHref(context, CRM_RECORD_SECTIONS[kind])}/${encodeURIComponent(id)}`\n}\n\n/**\n * The query key the Contacts list reads to OPEN the one person with an\n * address (AGL-2612). A surface that holds only an email — an order, a\n * booking, a site user — links to the list with this key, the list filters\n * on the address and moves on to the record when exactly one matches. The\n * plugin re-exports this constant as `CONTACTS_LIST_EMAIL_PARAM` so its\n * parser and the console's builders read one string.\n */\nexport const CRM_CONTACTS_EMAIL_PARAM = 'email'\n\n/** The Contacts list asked to open the person with this address. */\nexport function crmContactByEmailHref(\n context: SiteConsoleContext,\n email: string,\n): string {\n return `${crmSectionHref(context, CRM_RECORD_SECTIONS.contact)}?${new URLSearchParams(\n { [CRM_CONTACTS_EMAIL_PARAM]: email },\n ).toString()}`\n}\n\n/**\n * `/{orgSlug}/crm` — the ORGANIZATION-level CRM hub (AGL-2630), the same\n * sections mounted with no site.\n *\n * The org-level twin of {@link crmHubHref}, and separate from it because the\n * two take different arguments: a site address needs the subdomain, and a\n * surface standing at the org hub has none to give. A caller that faked one\n * would put a site's name on a page about all of them.\n */\nexport function crmOrgHubHref(orgSlug: string): string {\n return buildRoute(Route.ORG_CRM, { orgSlug })\n}\n\n/** One section of the org-level hub — the cross-site Deals board. */\nexport function crmOrgSectionHref(orgSlug: string, section: string): string {\n return `${crmOrgHubHref(orgSlug)}/${section}`\n}\n\n/**\n * One record's page at the org level.\n *\n * A lead is excluded by the type rather than by a runtime check: a lead's id\n * is a PERSON KEY, the same on every site that met the person, and\n * `hosts/{hostId}/leads` is host-scoped by path — so an org-level address\n * has to name the site as well, which is {@link crmOrgLeadHref}.\n */\nexport function crmOrgRecordHref(\n orgSlug: string,\n kind: Exclude<CrmRecordKind, 'lead'>,\n id: string,\n): string {\n return `${crmOrgSectionHref(orgSlug, CRM_RECORD_SECTIONS[kind])}/${encodeURIComponent(id)}`\n}\n\n/** One site's lead, addressed from the org hub: the site, then the lead. */\nexport function crmOrgLeadHref(\n orgSlug: string,\n hostId: string,\n leadId: string,\n): string {\n return `${crmOrgSectionHref(orgSlug, CRM_RECORD_SECTIONS.lead)}/${encodeURIComponent(\n hostId,\n )}/${encodeURIComponent(leadId)}`\n}\n\n/**\n * The query keys a site record page reads on arrival, so a CRM timeline can\n * open the submission or the order it names rather than the list it sits\n * in. Each page parses its own key from `useSearchParams`; the CRM writes\n * them through `siteRecordLinks`, and the constant is shared so neither\n * side can misspell the other.\n */\nexport const INBOX_SUBMISSION_PARAM = 'submission'\nexport const ORDERS_ORDER_PARAM = 'order'\n/**\n * The orders list narrowed to one buyer. The same word as the CRM's own\n * email key, deliberately: a reader who edits one URL into the other is\n * not surprised.\n */\nexport const ORDERS_CUSTOMER_PARAM = 'email'\n/**\n * The Bookings page narrowed to one booker (AGL-2660) — the same key as the\n * orders list and the CRM, for the same reason: one address, one word.\n */\nexport const BOOKINGS_BOOKER_PARAM = 'email'\n\n/**\n * Where the site records a contact's captured history names are read\n * (AGL-2622): the Inbox for a submission, the Orders section of the\n * Products hub for an order, the Bookings page and the Users page for the\n * rest. Built from the route table so a section that moves is found here\n * by its route name rather than by searching for a string.\n */\nexport function siteRecordLinks(context: SiteConsoleContext) {\n const { orgSlug, host } = context\n const inbox = `${buildRoute(Route.HOST_INBOX, { orgSlug, host })}/submissions`\n const orders = `${buildRoute(Route.HOST_PRODUCTS, { orgSlug, host })}/orders`\n const withQuery = (base: string, key: string, value: string) =>\n `${base}?${new URLSearchParams({ [key]: value }).toString()}`\n return {\n /** The Inbox's Submissions tab, with the reader open on one submission. */\n submission: (submissionId: string) =>\n withQuery(inbox, INBOX_SUBMISSION_PARAM, submissionId),\n /** The Orders list. */\n orders: () => orders,\n /** The Orders list with one order's dialog open. */\n order: (orderId: string) => withQuery(orders, ORDERS_ORDER_PARAM, orderId),\n /** The Orders list narrowed to one buyer's address. */\n ordersByCustomer: (email: string) =>\n withQuery(orders, ORDERS_CUSTOMER_PARAM, email),\n /** The Bookings page; a booking has no page of its own. */\n bookings: () => buildRoute(Route.HOST_BOOKINGS, { orgSlug, host }),\n /** The Bookings page narrowed to one booker's address (AGL-2660). */\n bookingsByBooker: (email: string) =>\n withQuery(\n buildRoute(Route.HOST_BOOKINGS, { orgSlug, host }),\n BOOKINGS_BOOKER_PARAM,\n email,\n ),\n /** The Users page, where a site account's row and drawer are. */\n members: () => buildRoute(Route.HOST_USERS, { orgSlug, host }),\n }\n}\n\nexport type SiteRecordLinks = ReturnType<typeof siteRecordLinks>\n\n/**\n * What the link on a captured timeline entry says. Only the doors that\n * leave a record the console can open are named; a newsletter opt-in, an\n * API create, an import or a by-hand add leave nothing to open, so they\n * carry no link and no label.\n */\nexport const INTERACTION_LINK_LABELS: Partial<Record<ContactSource, string>> = {\n form: 'Open submission',\n order: 'Open order',\n booking: 'Open bookings',\n member: 'Open site users',\n}\n\n/**\n * The console page a captured interaction points back at, or `null` when\n * the door left nothing to open.\n *\n * A submission and an order are addressed by the `refId` the capture door\n * stamped — `formSubmissions/{id}`, `orders/{id}` — and an interaction that\n * predates the stamp lands on nothing rather than on a list that would read\n * as \"the record is gone\". A booking and a member sign-up land on their\n * pages, which list them; neither page addresses one row.\n */\nexport function contactInteractionHref(\n interaction: Pick<ContactInteraction, 'type' | 'refId'>,\n context: SiteConsoleContext,\n): string | null {\n const links = siteRecordLinks(context)\n switch (interaction.type) {\n case 'form':\n return interaction.refId ? links.submission(interaction.refId) : null\n case 'order':\n return interaction.refId ? links.order(interaction.refId) : null\n case 'booking':\n return links.bookings()\n case 'member':\n return links.members()\n default:\n return null\n }\n}\n"],"names":["buildRoute","Route","CRM_CONSOLE_SLUG","CRM_RECORD_SECTIONS","contact","lead","company","deal","crmHubHref","context","HOST_PLUGIN","orgSlug","host","pluginSlug","crmSectionHref","section","crmRecordHref","kind","id","encodeURIComponent","CRM_CONTACTS_EMAIL_PARAM","crmContactByEmailHref","email","URLSearchParams","toString","crmOrgHubHref","ORG_CRM","crmOrgSectionHref","crmOrgRecordHref","crmOrgLeadHref","hostId","leadId","INBOX_SUBMISSION_PARAM","ORDERS_ORDER_PARAM","ORDERS_CUSTOMER_PARAM","BOOKINGS_BOOKER_PARAM","siteRecordLinks","inbox","HOST_INBOX","orders","HOST_PRODUCTS","withQuery","base","key","value","submission","submissionId","order","orderId","ordersByCustomer","bookings","HOST_BOOKINGS","bookingsByBooker","members","HOST_USERS","INTERACTION_LINK_LABELS","form","booking","member","contactInteractionHref","interaction","links","type","refId"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/console-record-links.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { buildRoute, Route } from './console-routes'\nimport type { ContactInteraction, ContactSource } from './contacts'\n\n/**\n * Where the console's records live, for a surface that is not the one the\n * record belongs to (AGL-2622).\n *\n * ## Why these addresses are built here and not where they are used\n *\n * A CRM record is reached from six surfaces that are not the CRM — the\n * top-bar search, an Inbox row, a form's page, an order, a booking, a site\n * user — and a CRM timeline points back at four of them. Two of those\n * readers are the console APP, which the module boundaries forbid from\n * importing a feature plugin at all: a plugin reaches the app only through\n * the generated loader manifests, so the app cannot ask the CRM for its own\n * route builder. The CRM plugin's `crmRoutes(basePath)` stays the builder\n * inside the plugin family; this module is the same address spelled once\n * for the console side, in the shared library both may import, and the\n * plugin's builder is pinned against it by a spec so the two cannot drift.\n *\n * ## Why the arguments are the two route params and not a host document\n *\n * Every site console page is addressed by `/{orgSlug}/hosts/{host}/…`, and\n * both segments are already in the URL a surface is rendered on. Building\n * from them costs nothing; resolving them from a host id costs two document\n * reads (`hostIndex`, then the org) on every open of a card, to render a\n * link. `useConsoleHubPath` in the marketing plugin makes the same choice\n * for the same reason.\n */\nexport interface SiteConsoleContext {\n orgSlug: string\n host: string\n}\n\n/** The CRM's nav slug — the `[pluginSlug]` segment the shell resolves. */\nexport const CRM_CONSOLE_SLUG = 'crm'\n\n/** The four record kinds a surface outside the CRM may name. */\nexport type CrmRecordKind = 'contact' | 'lead' | 'company' | 'deal'\n\n/**\n * The hub section each kind's record lives under, which is also the list a\n * kind-only link lands on. Named here rather than in the plugin's section\n * registry so the console app can address a record without the plugin.\n */\nexport const CRM_RECORD_SECTIONS: Record<CrmRecordKind, string> = {\n contact: 'contacts',\n lead: 'leads',\n company: 'companies',\n deal: 'deals',\n}\n\n/** `/{orgSlug}/hosts/{host}/crm` — the CRM hub on the site being read. */\nexport function crmHubHref(context: SiteConsoleContext): string {\n return buildRoute(Route.HOST_PLUGIN, {\n orgSlug: context.orgSlug,\n host: context.host,\n pluginSlug: CRM_CONSOLE_SLUG,\n })\n}\n\n/** One section of the hub — the Leads list, the Deals board. */\nexport function crmSectionHref(\n context: SiteConsoleContext,\n section: string,\n): string {\n return `${crmHubHref(context)}/${section}`\n}\n\n/**\n * One record's own page. The id is URL-encoded because a Firestore id is\n * opaque — the console mints its own, but an import or an API caller may\n * not, and a slash in an id would otherwise read as a further segment.\n */\nexport function crmRecordHref(\n context: SiteConsoleContext,\n kind: CrmRecordKind,\n id: string,\n): string {\n return `${crmSectionHref(context, CRM_RECORD_SECTIONS[kind])}/${encodeURIComponent(id)}`\n}\n\n/**\n * The query key the Contacts list reads to OPEN the one person with an\n * address (AGL-2612). A surface that holds only an email — an order, a\n * booking, a site user — links to the list with this key, the list filters\n * on the address and moves on to the record when exactly one matches. The\n * plugin re-exports this constant as `CONTACTS_LIST_EMAIL_PARAM` so its\n * parser and the console's builders read one string.\n */\nexport const CRM_CONTACTS_EMAIL_PARAM = 'email'\n\n/** The Contacts list asked to open the person with this address. */\nexport function crmContactByEmailHref(\n context: SiteConsoleContext,\n email: string,\n): string {\n return `${crmSectionHref(context, CRM_RECORD_SECTIONS.contact)}?${new URLSearchParams(\n { [CRM_CONTACTS_EMAIL_PARAM]: email },\n ).toString()}`\n}\n\n/**\n * `/{orgSlug}/crm` — the ORGANIZATION-level CRM hub (AGL-2630), the same\n * sections mounted with no site.\n *\n * The org-level twin of {@link crmHubHref}, and separate from it because the\n * two take different arguments: a site address needs the subdomain, and a\n * surface standing at the org hub has none to give. A caller that faked one\n * would put a site's name on a page about all of them.\n */\nexport function crmOrgHubHref(orgSlug: string): string {\n return buildRoute(Route.ORG_CRM, { orgSlug })\n}\n\n/** One section of the org-level hub — the cross-site Deals board. */\nexport function crmOrgSectionHref(orgSlug: string, section: string): string {\n return `${crmOrgHubHref(orgSlug)}/${section}`\n}\n\n/**\n * One record's page at the org level.\n *\n * A LEAD IS NO LONGER EXCLUDED (AGL-3275). It was, by the type rather than a\n * runtime check, because a lead's id is a person key — the same on every site\n * that met the person — while `hosts/{hostId}/leads` was host-scoped by path,\n * so two sites held two documents carrying one id and an org-level address\n * had to name the site to tell them apart. One org collection makes the\n * person key unambiguous, so a lead addresses like every other record.\n */\nexport function crmOrgRecordHref(\n orgSlug: string,\n kind: CrmRecordKind,\n id: string,\n): string {\n return `${crmOrgSectionHref(orgSlug, CRM_RECORD_SECTIONS[kind])}/${encodeURIComponent(id)}`\n}\n\n/**\n * A lead addressed from the org hub.\n *\n * Kept as its own name so the callers that had a `hostId` in hand do not have\n * to prove they no longer need one; it ignores the site and is exactly\n * `crmOrgRecordHref(orgSlug, 'lead', leadId)`. New callers should use that.\n *\n * @deprecated Use {@link crmOrgRecordHref}. Removed with the host-path\n * fallback in AGL-3277.\n */\nexport function crmOrgLeadHref(\n orgSlug: string,\n _hostId: string,\n leadId: string,\n): string {\n return crmOrgRecordHref(orgSlug, 'lead', leadId)\n}\n\n/**\n * The query keys a site record page reads on arrival, so a CRM timeline can\n * open the submission or the order it names rather than the list it sits\n * in. Each page parses its own key from `useSearchParams`; the CRM writes\n * them through `siteRecordLinks`, and the constant is shared so neither\n * side can misspell the other.\n */\nexport const INBOX_SUBMISSION_PARAM = 'submission'\nexport const ORDERS_ORDER_PARAM = 'order'\n/**\n * The orders list narrowed to one buyer. The same word as the CRM's own\n * email key, deliberately: a reader who edits one URL into the other is\n * not surprised.\n */\nexport const ORDERS_CUSTOMER_PARAM = 'email'\n/**\n * The Bookings page narrowed to one booker (AGL-2660) — the same key as the\n * orders list and the CRM, for the same reason: one address, one word.\n */\nexport const BOOKINGS_BOOKER_PARAM = 'email'\n\n/**\n * Where the site records a contact's captured history names are read\n * (AGL-2622): the Inbox for a submission, the Orders section of the\n * Products hub for an order, the Bookings page and the Users page for the\n * rest. Built from the route table so a section that moves is found here\n * by its route name rather than by searching for a string.\n */\nexport function siteRecordLinks(context: SiteConsoleContext) {\n const { orgSlug, host } = context\n const inbox = `${buildRoute(Route.HOST_INBOX, { orgSlug, host })}/submissions`\n const orders = `${buildRoute(Route.HOST_PRODUCTS, { orgSlug, host })}/orders`\n const withQuery = (base: string, key: string, value: string) =>\n `${base}?${new URLSearchParams({ [key]: value }).toString()}`\n return {\n /** The Inbox's Submissions tab, with the reader open on one submission. */\n submission: (submissionId: string) =>\n withQuery(inbox, INBOX_SUBMISSION_PARAM, submissionId),\n /** The Orders list. */\n orders: () => orders,\n /** The Orders list with one order's dialog open. */\n order: (orderId: string) => withQuery(orders, ORDERS_ORDER_PARAM, orderId),\n /** The Orders list narrowed to one buyer's address. */\n ordersByCustomer: (email: string) =>\n withQuery(orders, ORDERS_CUSTOMER_PARAM, email),\n /** The Bookings page; a booking has no page of its own. */\n bookings: () => buildRoute(Route.HOST_BOOKINGS, { orgSlug, host }),\n /** The Bookings page narrowed to one booker's address (AGL-2660). */\n bookingsByBooker: (email: string) =>\n withQuery(\n buildRoute(Route.HOST_BOOKINGS, { orgSlug, host }),\n BOOKINGS_BOOKER_PARAM,\n email,\n ),\n /** The Users page, where a site account's row and drawer are. */\n members: () => buildRoute(Route.HOST_USERS, { orgSlug, host }),\n }\n}\n\nexport type SiteRecordLinks = ReturnType<typeof siteRecordLinks>\n\n/**\n * What the link on a captured timeline entry says. Only the doors that\n * leave a record the console can open are named; a newsletter opt-in, an\n * API create, an import or a by-hand add leave nothing to open, so they\n * carry no link and no label.\n */\nexport const INTERACTION_LINK_LABELS: Partial<Record<ContactSource, string>> = {\n form: 'Open submission',\n order: 'Open order',\n booking: 'Open bookings',\n member: 'Open site users',\n}\n\n/**\n * The console page a captured interaction points back at, or `null` when\n * the door left nothing to open.\n *\n * A submission and an order are addressed by the `refId` the capture door\n * stamped — `formSubmissions/{id}`, `orders/{id}` — and an interaction that\n * predates the stamp lands on nothing rather than on a list that would read\n * as \"the record is gone\". A booking and a member sign-up land on their\n * pages, which list them; neither page addresses one row.\n */\nexport function contactInteractionHref(\n interaction: Pick<ContactInteraction, 'type' | 'refId'>,\n context: SiteConsoleContext,\n): string | null {\n const links = siteRecordLinks(context)\n switch (interaction.type) {\n case 'form':\n return interaction.refId ? links.submission(interaction.refId) : null\n case 'order':\n return interaction.refId ? links.order(interaction.refId) : null\n case 'booking':\n return links.bookings()\n case 'member':\n return links.members()\n default:\n return null\n }\n}\n"],"names":["buildRoute","Route","CRM_CONSOLE_SLUG","CRM_RECORD_SECTIONS","contact","lead","company","deal","crmHubHref","context","HOST_PLUGIN","orgSlug","host","pluginSlug","crmSectionHref","section","crmRecordHref","kind","id","encodeURIComponent","CRM_CONTACTS_EMAIL_PARAM","crmContactByEmailHref","email","URLSearchParams","toString","crmOrgHubHref","ORG_CRM","crmOrgSectionHref","crmOrgRecordHref","crmOrgLeadHref","_hostId","leadId","INBOX_SUBMISSION_PARAM","ORDERS_ORDER_PARAM","ORDERS_CUSTOMER_PARAM","BOOKINGS_BOOKER_PARAM","siteRecordLinks","inbox","HOST_INBOX","orders","HOST_PRODUCTS","withQuery","base","key","value","submission","submissionId","order","orderId","ordersByCustomer","bookings","HOST_BOOKINGS","bookingsByBooker","members","HOST_USERS","INTERACTION_LINK_LABELS","form","booking","member","contactInteractionHref","interaction","links","type","refId"],"mappings":"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"}
@@ -578,9 +578,18 @@ export interface CrmActivity extends CrmScoped {
578
578
  sourceActionId?: string;
579
579
  /**
580
580
  * The plugin that filed it on the record-timeline seam (AGL-2981) — an
581
- * email a sequence sent, a reply it read.
581
+ * email a sequence sent, a reply it read — or the CRM's own id on an
582
+ * entry it wrote about a filing (AGL-3274), so a reader can tell the
583
+ * bookkeeping entries from what a person logged.
582
584
  */
583
585
  sourcePluginId?: string;
586
+ /**
587
+ * The campaign a filing entry is about (AGL-3274): "Filed under" and
588
+ * "Removed from" name it in the body and carry the container's id here,
589
+ * so a campaign's own page can one day list what happened under it
590
+ * without parsing a sentence.
591
+ */
592
+ campaignId?: string;
584
593
  contactId?: string;
585
594
  companyId?: string;
586
595
  dealId?: string;
@@ -834,9 +843,12 @@ export interface ContactFieldDefinition extends CrmScoped {
834
843
  }
835
844
  /**
836
845
  * The records a custom field may describe, in the order the Fields
837
- * section tabs them (AGL-2661).
846
+ * section tabs them (AGL-2661), with `lead` since AGL-3272.
847
+ *
848
+ * A lead is last because it is the record a person leaves: the three
849
+ * before it are what a lead becomes, and the tabs read in that order.
838
850
  */
839
- export declare const CRM_FIELD_OBJECTS: readonly ["contact", "company", "deal"];
851
+ export declare const CRM_FIELD_OBJECTS: readonly ["contact", "company", "deal", "lead"];
840
852
  export type CrmFieldObject = (typeof CRM_FIELD_OBJECTS)[number];
841
853
  /** How each object reads on the Fields section's tabs and in a refusal. */
842
854
  export declare const CRM_FIELD_OBJECT_LABELS: Record<CrmFieldObject, string>;
@@ -1373,6 +1385,20 @@ export interface CrmLeadFields extends CrmLeadProfile {
1373
1385
  * handed to the contact's facet when the lead converts.
1374
1386
  */
1375
1387
  campaignIds?: string[];
1388
+ /**
1389
+ * The org's custom LEAD fields, keyed by each definition's `key`
1390
+ * (AGL-3272) — the same map a contact facet, a company and a deal keep,
1391
+ * judged against the definitions whose `object` is `lead`.
1392
+ *
1393
+ * It does NOT travel to the contact on convert. Keys are unique per
1394
+ * object, so a lead's `budget` and a contact's `budget` are two
1395
+ * definitions that may hold two types, and copying one map into the
1396
+ * other would write a value the receiving definition never agreed to.
1397
+ * Salesforce answers this with an explicit lead field mapping; until
1398
+ * there is one here the values stay on the lead, which a converted lead
1399
+ * is still read back for.
1400
+ */
1401
+ custom?: Record<string, CrmCustomValue>;
1376
1402
  }
1377
1403
  /** The longest text one profile field holds — the contact's own cap. */
1378
1404
  export declare const CRM_LEAD_TEXT_MAX = 120;
@@ -558,16 +558,21 @@ const DAY_MS = 24 * HOUR_MS;
558
558
  }
559
559
  /**
560
560
  * The records a custom field may describe, in the order the Fields
561
- * section tabs them (AGL-2661).
561
+ * section tabs them (AGL-2661), with `lead` since AGL-3272.
562
+ *
563
+ * A lead is last because it is the record a person leaves: the three
564
+ * before it are what a lead becomes, and the tabs read in that order.
562
565
  */ export const CRM_FIELD_OBJECTS = [
563
566
  'contact',
564
567
  'company',
565
- 'deal'
568
+ 'deal',
569
+ 'lead'
566
570
  ];
567
571
  /** How each object reads on the Fields section's tabs and in a refusal. */ export const CRM_FIELD_OBJECT_LABELS = {
568
572
  contact: 'Contacts',
569
573
  company: 'Companies',
570
- deal: 'Deals'
574
+ deal: 'Deals',
575
+ lead: 'Leads'
571
576
  };
572
577
  export function isCrmFieldObject(value) {
573
578
  return typeof value === 'string' && CRM_FIELD_OBJECTS.includes(value);