@aglyn/aglyn 1.0.0-beta.165 → 1.0.0-beta.167
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.
|
|
3
|
+
"version": "1.0.0-beta.167",
|
|
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.
|
|
37
|
-
"@aglyn/shared-data-mdi": "1.0.0-beta.
|
|
38
|
-
"@aglyn/shared-data-types": "1.0.0-beta.
|
|
39
|
-
"@aglyn/shared-util-email": "1.0.0-beta.
|
|
40
|
-
"@aglyn/shared-util-http": "1.0.0-beta.
|
|
41
|
-
"@aglyn/shared-util-logger": "1.0.0-beta.
|
|
42
|
-
"@aglyn/shared-util-timestamp": "1.0.0-beta.
|
|
43
|
-
"@aglyn/shared-util-tools": "1.0.0-beta.
|
|
44
|
-
"@aglyn/shared-util-vendor": "1.0.0-beta.
|
|
36
|
+
"@aglyn/shared-data-enums": "1.0.0-beta.167",
|
|
37
|
+
"@aglyn/shared-data-mdi": "1.0.0-beta.167",
|
|
38
|
+
"@aglyn/shared-data-types": "1.0.0-beta.167",
|
|
39
|
+
"@aglyn/shared-util-email": "1.0.0-beta.167",
|
|
40
|
+
"@aglyn/shared-util-http": "1.0.0-beta.167",
|
|
41
|
+
"@aglyn/shared-util-logger": "1.0.0-beta.167",
|
|
42
|
+
"@aglyn/shared-util-timestamp": "1.0.0-beta.167",
|
|
43
|
+
"@aglyn/shared-util-tools": "1.0.0-beta.167",
|
|
44
|
+
"@aglyn/shared-util-vendor": "1.0.0-beta.167",
|
|
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
|
|
95
|
-
* is a
|
|
96
|
-
* `hosts/{hostId}/leads`
|
|
97
|
-
*
|
|
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:
|
|
100
|
-
/**
|
|
101
|
-
|
|
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
|
|
78
|
-
* is a
|
|
79
|
-
* `hosts/{hostId}/leads`
|
|
80
|
-
*
|
|
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
|
-
/**
|
|
85
|
-
|
|
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":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,UAAU,EAAEC,KAAK,QAAQ,sBAAkB;AAkCpD,wEAAwE,GACxE,OAAO,MAAMC,mBAAmB,MAAK;AAKrC;;;;CAIC,GACD,OAAO,MAAMC,sBAAqD;IAChEC,SAAS;IACTC,MAAM;IACNC,SAAS;IACTC,MAAM;AACR,EAAC;AAED,wEAAwE,GACxE,OAAO,SAASC,WAAWC,OAA2B;IACpD,OAAOT,WAAWC,MAAMS,WAAW,EAAE;QACnCC,SAASF,QAAQE,OAAO;QACxBC,MAAMH,QAAQG,IAAI;QAClBC,YAAYX;IACd;AACF;AAEA,8DAA8D,GAC9D,OAAO,SAASY,eACdL,OAA2B,EAC3BM,OAAe;IAEf,OAAO,GAAGP,WAAWC,SAAS,CAAC,EAAEM,SAAS;AAC5C;AAEA;;;;CAIC,GACD,OAAO,SAASC,cACdP,OAA2B,EAC3BQ,IAAmB,EACnBC,EAAU;IAEV,OAAO,GAAGJ,eAAeL,SAASN,mBAAmB,CAACc,KAAK,EAAE,CAAC,EAAEE,mBAAmBD,KAAK;AAC1F;AAEA;;;;;;;CAOC,GACD,OAAO,MAAME,2BAA2B,QAAO;AAE/C,kEAAkE,GAClE,OAAO,SAASC,sBACdZ,OAA2B,EAC3Ba,KAAa;IAEb,OAAO,GAAGR,eAAeL,SAASN,oBAAoBC,OAAO,EAAE,CAAC,EAAE,IAAImB,gBACpE;QAAE,CAACH,yBAAyB,EAAEE;IAAM,GACpCE,QAAQ,IAAI;AAChB;AAEA;;;;;;;;CAQC,GACD,OAAO,SAASC,cAAcd,OAAe;IAC3C,OAAOX,WAAWC,MAAMyB,OAAO,EAAE;QAAEf;IAAQ;AAC7C;AAEA,mEAAmE,GACnE,OAAO,SAASgB,kBAAkBhB,OAAe,EAAEI,OAAe;IAChE,OAAO,GAAGU,cAAcd,SAAS,CAAC,EAAEI,SAAS;AAC/C;AAEA;;;;;;;CAOC,GACD,OAAO,SAASa,iBACdjB,OAAe,EACfM,IAAoC,EACpCC,EAAU;IAEV,OAAO,GAAGS,kBAAkBhB,SAASR,mBAAmB,CAACc,KAAK,EAAE,CAAC,EAAEE,mBAAmBD,KAAK;AAC7F;AAEA,0EAA0E,GAC1E,OAAO,SAASW,eACdlB,OAAe,EACfmB,MAAc,EACdC,MAAc;IAEd,OAAO,GAAGJ,kBAAkBhB,SAASR,oBAAoBE,IAAI,EAAE,CAAC,EAAEc,mBAChEW,QACA,CAAC,EAAEX,mBAAmBY,SAAS;AACnC;AAEA;;;;;;CAMC,GACD,OAAO,MAAMC,yBAAyB,aAAY;AAClD,OAAO,MAAMC,qBAAqB,QAAO;AACzC;;;;CAIC,GACD,OAAO,MAAMC,wBAAwB,QAAO;AAC5C;;;CAGC,GACD,OAAO,MAAMC,wBAAwB,QAAO;AAE5C;;;;;;CAMC,GACD,OAAO,SAASC,gBAAgB3B,OAA2B;IACzD,MAAM,EAAEE,OAAO,EAAEC,IAAI,EAAE,GAAGH;IAC1B,MAAM4B,QAAQ,GAAGrC,WAAWC,MAAMqC,UAAU,EAAE;QAAE3B;QAASC;IAAK,GAAG,YAAY,CAAC;IAC9E,MAAM2B,SAAS,GAAGvC,WAAWC,MAAMuC,aAAa,EAAE;QAAE7B;QAASC;IAAK,GAAG,OAAO,CAAC;IAC7E,MAAM6B,YAAY,CAACC,MAAcC,KAAaC,QAC5C,GAAGF,KAAK,CAAC,EAAE,IAAInB,gBAAgB;YAAE,CAACoB,IAAI,EAAEC;QAAM,GAAGpB,QAAQ,IAAI;IAC/D,OAAO;QACL,yEAAyE,GACzEqB,YAAY,CAACC,eACXL,UAAUJ,OAAOL,wBAAwBc;QAC3C,qBAAqB,GACrBP,QAAQ,IAAMA;QACd,kDAAkD,GAClDQ,OAAO,CAACC,UAAoBP,UAAUF,QAAQN,oBAAoBe;QAClE,qDAAqD,GACrDC,kBAAkB,CAAC3B,QACjBmB,UAAUF,QAAQL,uBAAuBZ;QAC3C,yDAAyD,GACzD4B,UAAU,IAAMlD,WAAWC,MAAMkD,aAAa,EAAE;gBAAExC;gBAASC;YAAK;QAChE,mEAAmE,GACnEwC,kBAAkB,CAAC9B,QACjBmB,UACEzC,WAAWC,MAAMkD,aAAa,EAAE;gBAAExC;gBAASC;YAAK,IAChDuB,uBACAb;QAEJ,+DAA+D,GAC/D+B,SAAS,IAAMrD,WAAWC,MAAMqD,UAAU,EAAE;gBAAE3C;gBAASC;YAAK;IAC9D;AACF;AAIA;;;;;CAKC,GACD,OAAO,MAAM2C,0BAAkE;IAC7EC,MAAM;IACNT,OAAO;IACPU,SAAS;IACTC,QAAQ;AACV,EAAC;AAED;;;;;;;;;CASC,GACD,OAAO,SAASC,uBACdC,WAAuD,EACvDnD,OAA2B;IAE3B,MAAMoD,QAAQzB,gBAAgB3B;IAC9B,OAAQmD,YAAYE,IAAI;QACtB,KAAK;YACH,OAAOF,YAAYG,KAAK,GAAGF,MAAMhB,UAAU,CAACe,YAAYG,KAAK,IAAI;QACnE,KAAK;YACH,OAAOH,YAAYG,KAAK,GAAGF,MAAMd,KAAK,CAACa,YAAYG,KAAK,IAAI;QAC9D,KAAK;YACH,OAAOF,MAAMX,QAAQ;QACvB,KAAK;YACH,OAAOW,MAAMR,OAAO;QACtB;YACE,OAAO;IACX;AACF"}
|
|
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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"}
|