@aglyn/aglyn 1.0.0-beta.224 → 1.0.0-beta.225

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.224",
3
+ "version": "1.0.0-beta.225",
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.224",
41
- "@aglyn/shared-data-mdi": "1.0.0-beta.224",
42
- "@aglyn/shared-data-types": "1.0.0-beta.224",
43
- "@aglyn/shared-util-email": "1.0.0-beta.224",
44
- "@aglyn/shared-util-first-touch": "1.0.0-beta.224",
45
- "@aglyn/shared-util-http": "1.0.0-beta.224",
46
- "@aglyn/shared-util-logger": "1.0.0-beta.224",
47
- "@aglyn/shared-util-timestamp": "1.0.0-beta.224",
48
- "@aglyn/shared-util-tools": "1.0.0-beta.224",
49
- "@aglyn/shared-util-vendor": "1.0.0-beta.224",
40
+ "@aglyn/shared-data-enums": "1.0.0-beta.225",
41
+ "@aglyn/shared-data-mdi": "1.0.0-beta.225",
42
+ "@aglyn/shared-data-types": "1.0.0-beta.225",
43
+ "@aglyn/shared-util-email": "1.0.0-beta.225",
44
+ "@aglyn/shared-util-first-touch": "1.0.0-beta.225",
45
+ "@aglyn/shared-util-http": "1.0.0-beta.225",
46
+ "@aglyn/shared-util-logger": "1.0.0-beta.225",
47
+ "@aglyn/shared-util-timestamp": "1.0.0-beta.225",
48
+ "@aglyn/shared-util-tools": "1.0.0-beta.225",
49
+ "@aglyn/shared-util-vendor": "1.0.0-beta.225",
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",
@@ -25,6 +25,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
25
25
  // `site-analytics-independence.spec.ts` walks and which must stay independent
26
26
  // of the site-plugin gate.
27
27
  import { ADVERTISING_TAG_ATTRIBUTE, restoreAdvertisingTags, revokeAdvertisingTags } from "./advertising-tags.js";
28
+ import { usePageIdle } from "./page-idle.js";
28
29
  import { VISITOR_CONSENT_CHANGED_EVENT } from "./visitor-consent.js";
29
30
  import Script from "next/script";
30
31
  import { Fragment, useEffect, useRef } from "react";
@@ -83,7 +84,14 @@ export default function AdvertisingTagMounts({ active, tags, resolve, nonce, sha
83
84
  }, [
84
85
  active
85
86
  ]);
86
- if (!active || tags.length === 0) return null;
87
+ // Nothing is injected before the page has loaded and gone idle (AGL-3581).
88
+ // The withdrawal listener above is NOT behind this: a visitor who refuses
89
+ // before the page idles has nothing to tear down, and one who refuses after
90
+ // needs the listener already there. Every surface reads the same store, so
91
+ // a page that mounts its own loader flips in the same render as this one
92
+ // and `sharedLibraries` still describes it (AGL-2681).
93
+ const pageIsIdle = usePageIdle();
94
+ if (!active || tags.length === 0 || !pageIsIdle) return null;
87
95
  return /*#__PURE__*/ _jsx(_Fragment, {
88
96
  children: tags.map(({ vendor, accountId })=>// A PAIR per vendor, inline boot first and library second — the same
89
97
  // shape as the GA `ga-init` / `ga-src` pair, and for the same reason:
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/advertising-tag-mounts.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// NO `'use client'` here, and it is a lint rule rather than a preference\n// (AGL-52): a directive inside `@aglyn/aglyn` makes the bundler split a\n// duplicate module graph, and the second canvas/emitter singleton renders the\n// tenant site blank. Every component that mounts this one carries the\n// directive itself, which is where the client boundary belongs.\n//\n// Deep app-utils modules, never the `@aglyn/aglyn` barrel (AGL-1550): this\n// file is reached from `site-analytics.tsx`, whose import closure\n// `site-analytics-independence.spec.ts` walks and which must stay independent\n// of the site-plugin gate.\nimport {\n ADVERTISING_TAG_ATTRIBUTE,\n type ResolvedAdvertisingTag,\n restoreAdvertisingTags,\n revokeAdvertisingTags,\n} from './advertising-tags'\nimport { VISITOR_CONSENT_CHANGED_EVENT } from './visitor-consent'\nimport Script from 'next/script'\nimport { Fragment, useEffect, useRef, type ReactElement } from 'react'\n\n/**\n * The MOUNT and the WITHDRAWAL for consent-gated advertising tags, with no\n * opinion about where the verdict came from.\n *\n * ## Why this is a shared component and not a second copy per surface\n *\n * Aglyn runs advertising tags on three first-party surfaces and they resolve\n * consent through three different mechanisms: the tenant runtime reads a host\n * document and a per-host record, the console reads the platform record its\n * own posture machinery wrote, and the docs site reads the registrable-domain\n * mirror of that record. Those are genuinely different questions.\n *\n * What is NOT different is what happens once the answer is known: mount an\n * inline boot and a library per vendor, and — the half that is easy to forget\n * and impossible to retrofit — stop them the moment the answer changes. A\n * second copy of that half is how one surface comes to keep firing after\n * consent is withdrawn on another, because the copy that was not updated\n * still looks exactly like the one that was.\n *\n * So the verdict is a PROP and the machinery is shared. Each surface answers\n * its own question with its own resolver; none of them owns a teardown.\n *\n * ## Why it renders even when the answer is no\n *\n * Because the withdrawal path needs a listener. A visitor who accepts and then\n * turns advertising off must stop being tracked in THAT pageview, and by then\n * the vendor library has executed — React dropping the `<Script>` does not\n * unload it (AGL-1608). So this component stays mounted whenever the surface\n * participates at all, and subscribes to\n * {@link VISITOR_CONSENT_CHANGED_EVENT}; the teardown runs from the event,\n * synchronously with the visitor's click, rather than waiting on a re-render.\n *\n * Both paths run and they agree, which is deliberate: the render gate is what\n * keeps the tag out of a fresh pageview, the listener is what removes one that\n * is already there, and neither can do the other's job.\n */\nexport interface AdvertisingTagMountsProps {\n /**\n * Whether this surface participates in the gate AT ALL.\n *\n * False installs nothing — no listener, no scripts. That is the clause that\n * keeps the tenant runtime's teardown off a customer's site: we did not load\n * their pixel, we do not know what basis it runs on, and reaching into their\n * page to kill it would be its own breach. `revokeAdvertisingTags` is\n * additionally attribute-scoped, so there are two independent scopes.\n */\n readonly active: boolean\n /** The verdict for THIS render: the tags that may exist right now. */\n readonly tags: readonly ResolvedAdvertisingTag[]\n /**\n * Re-read the verdict from live state, for the withdrawal listener.\n *\n * A callback rather than the `tags` prop, because the listener fires from\n * the visitor's own click in the same tick as the record is written — the\n * props for the current render are by definition the state before it.\n */\n readonly resolve: () => readonly ResolvedAdvertisingTag[]\n /**\n * The request's CSP nonce, stamped onto BOTH elements of every pair.\n *\n * A surface that enforces a nonce'd `script-src` refuses an inline script\n * that does not carry it, and the boot half of every pair is inline. Next's\n * `<Script>` stamps a nonce only when one reaches it: the automatic path\n * reads `HeadManagerContext`, and the App Router's client provider carries\n * no nonce at all — so a pair mounted after hydration, which is the only\n * time this component ever mounts one, is unnonced unless the caller hands\n * the value down. An explicit prop wins inside `next/script`\n * (`restProps.nonce || nonce`), which is what makes this the one door.\n *\n * The failure without it is silent and lopsided: the library beside the\n * boot has a `src` the policy allows, so the vendor's code loads and runs\n * against an account nobody configured, and the conversions this surface\n * sends are lost with nothing in the page but a CSP violation.\n *\n * Absent on a surface that sends no `script-src`: nothing is stamped, and\n * nothing is refused.\n */\n readonly nonce?: string\n /**\n * Libraries the HOST PAGE mounts itself, named by the needle a vendor\n * declares in `sharesLibrary` (AGL-2681).\n *\n * {@link sharedLibraryPresent} reads the document at render time, and that\n * is the right instrument for a loader some other party put there. It is\n * the wrong one for a loader the SAME render is about to create. On the\n * tenant the GA pair and this component both wait on `consent.ready`, so\n * the first render that may emit either emits both — GA's `<Script>` is not\n * in the document yet when this component looks, the check honestly says\n * \"absent\", and the visitor downloads `gtag.js` twice: once as ours with\n * `?id=AW-…`, once as gtag's own destination fetch for the `config` that\n * follows. Measured on `aglyn.com/solutions/small-business` with\n * advertising granted: two 147 KiB loaders for one account.\n *\n * A page that KNOWS it renders the library says so here, from the same\n * condition that renders it, and the document check stays as the fallback\n * for loaders it does not know about. Naming the needle rather than passing\n * a boolean keeps the declaration per library: a page that mounts gtag has\n * said nothing about the Meta pixel.\n */\n readonly sharedLibraries?: readonly string[]\n}\n\n/**\n * Is a library matching `needle` already in the document?\n *\n * Read at RENDER time rather than in an effect: the decision is whether to\n * emit a `<Script>` at all, and by the time an effect could answer, Next has\n * already appended it. `document` is guarded because this component renders on\n * the server too, where nothing is mounted and the honest answer is \"no\" — the\n * client render then re-evaluates with the real document.\n *\n * Blind to a loader the current render is creating alongside this one — see\n * `sharedLibraries` on the props for the case that needs the page to say so.\n */\nexport function sharedLibraryPresent(needle: string): boolean {\n if (typeof document === 'undefined') return false\n try {\n return Boolean(document.querySelector(`script[src*=\"${needle}\"]`))\n } catch {\n // A hostile or absent DOM: assume nothing is mounted, which mounts our\n // own copy — the cost is a duplicate fetch, never a missing tag.\n return false\n }\n}\n\nexport default function AdvertisingTagMounts({\n active,\n tags,\n resolve,\n nonce,\n sharedLibraries,\n}: AdvertisingTagMountsProps): ReactElement | null {\n /** Declared by the page, or found in the document: either means \"ride it\". */\n const libraryProvided = (needle: string): boolean =>\n (sharedLibraries?.includes(needle) ?? false) || sharedLibraryPresent(needle)\n /*\n * The resolver is held in a ref rather than listed as an effect dependency.\n *\n * Every caller passes a closure over its own live consent state, so the\n * function identity changes on every render; depending on it would tear the\n * listener down and re-install it each time, and a withdrawal that landed in\n * that window would find no subscriber. The ref makes the subscription's\n * lifetime `active`, which is the only thing that genuinely changes it,\n * while the callback it invokes is always the newest one.\n */\n const resolveRef = useRef(resolve)\n resolveRef.current = resolve\n\n useEffect(() => {\n if (!active) return undefined\n const sync = () => {\n if (resolveRef.current().length === 0) {\n revokeAdvertisingTags()\n } else {\n // Symmetric: a visitor who withdrew and changed their mind inside one\n // pageview would otherwise stay un-tracked until they navigated,\n // because a re-rendered `<Script>` cannot re-execute a library the\n // browser already ran.\n restoreAdvertisingTags()\n }\n }\n window.addEventListener(VISITOR_CONSENT_CHANGED_EVENT, sync)\n return () => window.removeEventListener(VISITOR_CONSENT_CHANGED_EVENT, sync)\n }, [active])\n\n if (!active || tags.length === 0) return null\n\n return (\n <>\n {tags.map(({ vendor, accountId }) => (\n // A PAIR per vendor, inline boot first and library second — the same\n // shape as the GA `ga-init` / `ga-src` pair, and for the same reason:\n // the boot defines the vendor's queue shim and declares the consent\n // state, so nothing the library later drains was queued under a state\n // nobody chose. Both elements carry the teardown's scope marker; only\n // elements carrying it are ever revoked, removed or cookie-swept.\n <Fragment key={vendor.id}>\n <Script\n id={`ad-tag-${vendor.id}-init`}\n strategy=\"afterInteractive\"\n nonce={nonce}\n {...{ [ADVERTISING_TAG_ATTRIBUTE]: vendor.id }}\n >\n {vendor.bootSnippet ? vendor.bootSnippet(accountId) : ''}\n </Script>\n {/* Skipped when another loader already brought this library in\n (AGL-1152). Google Ads shares `gtag.js` with the GA4 measurement\n id and with a GTM container, so a surface with both configured\n would fetch it twice and define `gtag()` twice — and the boot\n above would be the second voice in a consent conversation the\n first one already had. One library, several `config` calls, is\n how gtag carries several products. The page's own declaration is\n consulted first (AGL-2681): the document cannot yet show a loader\n this same render is creating. */}\n {vendor.sharesLibrary && libraryProvided(vendor.sharesLibrary) ? null : (\n <Script\n id={`ad-tag-${vendor.id}-src`}\n strategy=\"afterInteractive\"\n nonce={nonce}\n {...{ [ADVERTISING_TAG_ATTRIBUTE]: vendor.id }}\n // `scriptSrcFor` where the vendor has one: gtag reads the\n // container out of the loader's query, so the copy we bring\n // ourselves has to name the account. See `scriptSrcFor`.\n src={\n vendor.scriptSrcFor\n ? vendor.scriptSrcFor(accountId)\n : vendor.scriptSrc\n }\n />\n )}\n </Fragment>\n ))}\n </>\n )\n}\n"],"names":["ADVERTISING_TAG_ATTRIBUTE","restoreAdvertisingTags","revokeAdvertisingTags","VISITOR_CONSENT_CHANGED_EVENT","Script","Fragment","useEffect","useRef","sharedLibraryPresent","needle","document","Boolean","querySelector","AdvertisingTagMounts","active","tags","resolve","nonce","sharedLibraries","libraryProvided","includes","resolveRef","current","undefined","sync","length","window","addEventListener","removeEventListener","map","vendor","accountId","id","strategy","bootSnippet","sharesLibrary","src","scriptSrcFor","scriptSrc"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,yEAAyE;AACzE,wEAAwE;AACxE,8EAA8E;AAC9E,sEAAsE;AACtE,gEAAgE;AAChE,EAAE;AACF,2EAA2E;AAC3E,kEAAkE;AAClE,8EAA8E;AAC9E,2BAA2B;AAC3B,SACEA,yBAAyB,EAEzBC,sBAAsB,EACtBC,qBAAqB,QAChB,wBAAoB;AAC3B,SAASC,6BAA6B,QAAQ,uBAAmB;AACjE,OAAOC,YAAY,cAAa;AAChC,SAASC,QAAQ,EAAEC,SAAS,EAAEC,MAAM,QAA2B,QAAO;AAwGtE;;;;;;;;;;;CAWC,GACD,OAAO,SAASC,qBAAqBC,MAAc;IACjD,IAAI,OAAOC,aAAa,aAAa,OAAO;IAC5C,IAAI;QACF,OAAOC,QAAQD,SAASE,aAAa,CAAC,CAAC,aAAa,EAAEH,OAAO,EAAE,CAAC;IAClE,EAAE,eAAM;QACN,uEAAuE;QACvE,iEAAiE;QACjE,OAAO;IACT;AACF;AAEA,eAAe,SAASI,qBAAqB,EAC3CC,MAAM,EACNC,IAAI,EACJC,OAAO,EACPC,KAAK,EACLC,eAAe,EACW;IAC1B,4EAA4E,GAC5E,MAAMC,kBAAkB,CAACV;;eACvB,SAACS,mCAAAA,gBAAiBE,QAAQ,CAACX,0BAAW,UAAUD,qBAAqBC;;IACvE;;;;;;;;;GASC,GACD,MAAMY,aAAad,OAAOS;IAC1BK,WAAWC,OAAO,GAAGN;IAErBV,UAAU;QACR,IAAI,CAACQ,QAAQ,OAAOS;QACpB,MAAMC,OAAO;YACX,IAAIH,WAAWC,OAAO,GAAGG,MAAM,KAAK,GAAG;gBACrCvB;YACF,OAAO;gBACL,sEAAsE;gBACtE,iEAAiE;gBACjE,mEAAmE;gBACnE,uBAAuB;gBACvBD;YACF;QACF;QACAyB,OAAOC,gBAAgB,CAACxB,+BAA+BqB;QACvD,OAAO,IAAME,OAAOE,mBAAmB,CAACzB,+BAA+BqB;IACzE,GAAG;QAACV;KAAO;IAEX,IAAI,CAACA,UAAUC,KAAKU,MAAM,KAAK,GAAG,OAAO;IAEzC,qBACE;kBACGV,KAAKc,GAAG,CAAC,CAAC,EAAEC,MAAM,EAAEC,SAAS,EAAE,GAC9B,qEAAqE;YACrE,sEAAsE;YACtE,oEAAoE;YACpE,sEAAsE;YACtE,sEAAsE;YACtE,kEAAkE;0BAClE,MAAC1B;;kCACC,KAACD;wBACC4B,IAAI,CAAC,OAAO,EAAEF,OAAOE,EAAE,CAAC,KAAK,CAAC;wBAC9BC,UAAS;wBACThB,OAAOA;wBACD,CAACjB,0BAA0B,EAAE8B,OAAOE,EAAE;kCAE3CF,OAAOI,WAAW,GAAGJ,OAAOI,WAAW,CAACH,aAAa;;oBAWvDD,OAAOK,aAAa,IAAIhB,gBAAgBW,OAAOK,aAAa,IAAI,qBAC/D,KAAC/B;wBACC4B,IAAI,CAAC,OAAO,EAAEF,OAAOE,EAAE,CAAC,IAAI,CAAC;wBAC7BC,UAAS;wBACThB,OAAOA;wBACD,CAACjB,0BAA0B,EAAE8B,OAAOE,EAAE;wBAC5C,0DAA0D;wBAC1D,4DAA4D;wBAC5D,yDAAyD;wBACzDI,KACEN,OAAOO,YAAY,GACfP,OAAOO,YAAY,CAACN,aACpBD,OAAOQ,SAAS;;;eA9BbR,OAAOE,EAAE;;AAsChC"}
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/advertising-tag-mounts.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// NO `'use client'` here, and it is a lint rule rather than a preference\n// (AGL-52): a directive inside `@aglyn/aglyn` makes the bundler split a\n// duplicate module graph, and the second canvas/emitter singleton renders the\n// tenant site blank. Every component that mounts this one carries the\n// directive itself, which is where the client boundary belongs.\n//\n// Deep app-utils modules, never the `@aglyn/aglyn` barrel (AGL-1550): this\n// file is reached from `site-analytics.tsx`, whose import closure\n// `site-analytics-independence.spec.ts` walks and which must stay independent\n// of the site-plugin gate.\nimport {\n ADVERTISING_TAG_ATTRIBUTE,\n type ResolvedAdvertisingTag,\n restoreAdvertisingTags,\n revokeAdvertisingTags,\n} from './advertising-tags'\nimport { usePageIdle } from './page-idle'\nimport { VISITOR_CONSENT_CHANGED_EVENT } from './visitor-consent'\nimport Script from 'next/script'\nimport { Fragment, useEffect, useRef, type ReactElement } from 'react'\n\n/**\n * The MOUNT and the WITHDRAWAL for consent-gated advertising tags, with no\n * opinion about where the verdict came from.\n *\n * ## Why this is a shared component and not a second copy per surface\n *\n * Aglyn runs advertising tags on three first-party surfaces and they resolve\n * consent through three different mechanisms: the tenant runtime reads a host\n * document and a per-host record, the console reads the platform record its\n * own posture machinery wrote, and the docs site reads the registrable-domain\n * mirror of that record. Those are genuinely different questions.\n *\n * What is NOT different is what happens once the answer is known: mount an\n * inline boot and a library per vendor, and — the half that is easy to forget\n * and impossible to retrofit — stop them the moment the answer changes. A\n * second copy of that half is how one surface comes to keep firing after\n * consent is withdrawn on another, because the copy that was not updated\n * still looks exactly like the one that was.\n *\n * So the verdict is a PROP and the machinery is shared. Each surface answers\n * its own question with its own resolver; none of them owns a teardown.\n *\n * ## Why it renders even when the answer is no\n *\n * Because the withdrawal path needs a listener. A visitor who accepts and then\n * turns advertising off must stop being tracked in THAT pageview, and by then\n * the vendor library has executed — React dropping the `<Script>` does not\n * unload it (AGL-1608). So this component stays mounted whenever the surface\n * participates at all, and subscribes to\n * {@link VISITOR_CONSENT_CHANGED_EVENT}; the teardown runs from the event,\n * synchronously with the visitor's click, rather than waiting on a re-render.\n *\n * Both paths run and they agree, which is deliberate: the render gate is what\n * keeps the tag out of a fresh pageview, the listener is what removes one that\n * is already there, and neither can do the other's job.\n */\nexport interface AdvertisingTagMountsProps {\n /**\n * Whether this surface participates in the gate AT ALL.\n *\n * False installs nothing — no listener, no scripts. That is the clause that\n * keeps the tenant runtime's teardown off a customer's site: we did not load\n * their pixel, we do not know what basis it runs on, and reaching into their\n * page to kill it would be its own breach. `revokeAdvertisingTags` is\n * additionally attribute-scoped, so there are two independent scopes.\n */\n readonly active: boolean\n /** The verdict for THIS render: the tags that may exist right now. */\n readonly tags: readonly ResolvedAdvertisingTag[]\n /**\n * Re-read the verdict from live state, for the withdrawal listener.\n *\n * A callback rather than the `tags` prop, because the listener fires from\n * the visitor's own click in the same tick as the record is written — the\n * props for the current render are by definition the state before it.\n */\n readonly resolve: () => readonly ResolvedAdvertisingTag[]\n /**\n * The request's CSP nonce, stamped onto BOTH elements of every pair.\n *\n * A surface that enforces a nonce'd `script-src` refuses an inline script\n * that does not carry it, and the boot half of every pair is inline. Next's\n * `<Script>` stamps a nonce only when one reaches it: the automatic path\n * reads `HeadManagerContext`, and the App Router's client provider carries\n * no nonce at all — so a pair mounted after hydration, which is the only\n * time this component ever mounts one, is unnonced unless the caller hands\n * the value down. An explicit prop wins inside `next/script`\n * (`restProps.nonce || nonce`), which is what makes this the one door.\n *\n * The failure without it is silent and lopsided: the library beside the\n * boot has a `src` the policy allows, so the vendor's code loads and runs\n * against an account nobody configured, and the conversions this surface\n * sends are lost with nothing in the page but a CSP violation.\n *\n * Absent on a surface that sends no `script-src`: nothing is stamped, and\n * nothing is refused.\n */\n readonly nonce?: string\n /**\n * Libraries the HOST PAGE mounts itself, named by the needle a vendor\n * declares in `sharesLibrary` (AGL-2681).\n *\n * {@link sharedLibraryPresent} reads the document at render time, and that\n * is the right instrument for a loader some other party put there. It is\n * the wrong one for a loader the SAME render is about to create. On the\n * tenant the GA pair and this component both wait on `consent.ready`, so\n * the first render that may emit either emits both — GA's `<Script>` is not\n * in the document yet when this component looks, the check honestly says\n * \"absent\", and the visitor downloads `gtag.js` twice: once as ours with\n * `?id=AW-…`, once as gtag's own destination fetch for the `config` that\n * follows. Measured on `aglyn.com/solutions/small-business` with\n * advertising granted: two 147 KiB loaders for one account.\n *\n * A page that KNOWS it renders the library says so here, from the same\n * condition that renders it, and the document check stays as the fallback\n * for loaders it does not know about. Naming the needle rather than passing\n * a boolean keeps the declaration per library: a page that mounts gtag has\n * said nothing about the Meta pixel.\n */\n readonly sharedLibraries?: readonly string[]\n}\n\n/**\n * Is a library matching `needle` already in the document?\n *\n * Read at RENDER time rather than in an effect: the decision is whether to\n * emit a `<Script>` at all, and by the time an effect could answer, Next has\n * already appended it. `document` is guarded because this component renders on\n * the server too, where nothing is mounted and the honest answer is \"no\" — the\n * client render then re-evaluates with the real document.\n *\n * Blind to a loader the current render is creating alongside this one — see\n * `sharedLibraries` on the props for the case that needs the page to say so.\n */\nexport function sharedLibraryPresent(needle: string): boolean {\n if (typeof document === 'undefined') return false\n try {\n return Boolean(document.querySelector(`script[src*=\"${needle}\"]`))\n } catch {\n // A hostile or absent DOM: assume nothing is mounted, which mounts our\n // own copy — the cost is a duplicate fetch, never a missing tag.\n return false\n }\n}\n\nexport default function AdvertisingTagMounts({\n active,\n tags,\n resolve,\n nonce,\n sharedLibraries,\n}: AdvertisingTagMountsProps): ReactElement | null {\n /** Declared by the page, or found in the document: either means \"ride it\". */\n const libraryProvided = (needle: string): boolean =>\n (sharedLibraries?.includes(needle) ?? false) || sharedLibraryPresent(needle)\n /*\n * The resolver is held in a ref rather than listed as an effect dependency.\n *\n * Every caller passes a closure over its own live consent state, so the\n * function identity changes on every render; depending on it would tear the\n * listener down and re-install it each time, and a withdrawal that landed in\n * that window would find no subscriber. The ref makes the subscription's\n * lifetime `active`, which is the only thing that genuinely changes it,\n * while the callback it invokes is always the newest one.\n */\n const resolveRef = useRef(resolve)\n resolveRef.current = resolve\n\n useEffect(() => {\n if (!active) return undefined\n const sync = () => {\n if (resolveRef.current().length === 0) {\n revokeAdvertisingTags()\n } else {\n // Symmetric: a visitor who withdrew and changed their mind inside one\n // pageview would otherwise stay un-tracked until they navigated,\n // because a re-rendered `<Script>` cannot re-execute a library the\n // browser already ran.\n restoreAdvertisingTags()\n }\n }\n window.addEventListener(VISITOR_CONSENT_CHANGED_EVENT, sync)\n return () => window.removeEventListener(VISITOR_CONSENT_CHANGED_EVENT, sync)\n }, [active])\n\n // Nothing is injected before the page has loaded and gone idle (AGL-3581).\n // The withdrawal listener above is NOT behind this: a visitor who refuses\n // before the page idles has nothing to tear down, and one who refuses after\n // needs the listener already there. Every surface reads the same store, so\n // a page that mounts its own loader flips in the same render as this one\n // and `sharedLibraries` still describes it (AGL-2681).\n const pageIsIdle = usePageIdle()\n\n if (!active || tags.length === 0 || !pageIsIdle) return null\n\n return (\n <>\n {tags.map(({ vendor, accountId }) => (\n // A PAIR per vendor, inline boot first and library second — the same\n // shape as the GA `ga-init` / `ga-src` pair, and for the same reason:\n // the boot defines the vendor's queue shim and declares the consent\n // state, so nothing the library later drains was queued under a state\n // nobody chose. Both elements carry the teardown's scope marker; only\n // elements carrying it are ever revoked, removed or cookie-swept.\n <Fragment key={vendor.id}>\n <Script\n id={`ad-tag-${vendor.id}-init`}\n strategy=\"afterInteractive\"\n nonce={nonce}\n {...{ [ADVERTISING_TAG_ATTRIBUTE]: vendor.id }}\n >\n {vendor.bootSnippet ? vendor.bootSnippet(accountId) : ''}\n </Script>\n {/* Skipped when another loader already brought this library in\n (AGL-1152). Google Ads shares `gtag.js` with the GA4 measurement\n id and with a GTM container, so a surface with both configured\n would fetch it twice and define `gtag()` twice — and the boot\n above would be the second voice in a consent conversation the\n first one already had. One library, several `config` calls, is\n how gtag carries several products. The page's own declaration is\n consulted first (AGL-2681): the document cannot yet show a loader\n this same render is creating. */}\n {vendor.sharesLibrary && libraryProvided(vendor.sharesLibrary) ? null : (\n <Script\n id={`ad-tag-${vendor.id}-src`}\n strategy=\"afterInteractive\"\n nonce={nonce}\n {...{ [ADVERTISING_TAG_ATTRIBUTE]: vendor.id }}\n // `scriptSrcFor` where the vendor has one: gtag reads the\n // container out of the loader's query, so the copy we bring\n // ourselves has to name the account. See `scriptSrcFor`.\n src={\n vendor.scriptSrcFor\n ? vendor.scriptSrcFor(accountId)\n : vendor.scriptSrc\n }\n />\n )}\n </Fragment>\n ))}\n </>\n )\n}\n"],"names":["ADVERTISING_TAG_ATTRIBUTE","restoreAdvertisingTags","revokeAdvertisingTags","usePageIdle","VISITOR_CONSENT_CHANGED_EVENT","Script","Fragment","useEffect","useRef","sharedLibraryPresent","needle","document","Boolean","querySelector","AdvertisingTagMounts","active","tags","resolve","nonce","sharedLibraries","libraryProvided","includes","resolveRef","current","undefined","sync","length","window","addEventListener","removeEventListener","pageIsIdle","map","vendor","accountId","id","strategy","bootSnippet","sharesLibrary","src","scriptSrcFor","scriptSrc"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,yEAAyE;AACzE,wEAAwE;AACxE,8EAA8E;AAC9E,sEAAsE;AACtE,gEAAgE;AAChE,EAAE;AACF,2EAA2E;AAC3E,kEAAkE;AAClE,8EAA8E;AAC9E,2BAA2B;AAC3B,SACEA,yBAAyB,EAEzBC,sBAAsB,EACtBC,qBAAqB,QAChB,wBAAoB;AAC3B,SAASC,WAAW,QAAQ,iBAAa;AACzC,SAASC,6BAA6B,QAAQ,uBAAmB;AACjE,OAAOC,YAAY,cAAa;AAChC,SAASC,QAAQ,EAAEC,SAAS,EAAEC,MAAM,QAA2B,QAAO;AAwGtE;;;;;;;;;;;CAWC,GACD,OAAO,SAASC,qBAAqBC,MAAc;IACjD,IAAI,OAAOC,aAAa,aAAa,OAAO;IAC5C,IAAI;QACF,OAAOC,QAAQD,SAASE,aAAa,CAAC,CAAC,aAAa,EAAEH,OAAO,EAAE,CAAC;IAClE,EAAE,eAAM;QACN,uEAAuE;QACvE,iEAAiE;QACjE,OAAO;IACT;AACF;AAEA,eAAe,SAASI,qBAAqB,EAC3CC,MAAM,EACNC,IAAI,EACJC,OAAO,EACPC,KAAK,EACLC,eAAe,EACW;IAC1B,4EAA4E,GAC5E,MAAMC,kBAAkB,CAACV;;eACvB,SAACS,mCAAAA,gBAAiBE,QAAQ,CAACX,0BAAW,UAAUD,qBAAqBC;;IACvE;;;;;;;;;GASC,GACD,MAAMY,aAAad,OAAOS;IAC1BK,WAAWC,OAAO,GAAGN;IAErBV,UAAU;QACR,IAAI,CAACQ,QAAQ,OAAOS;QACpB,MAAMC,OAAO;YACX,IAAIH,WAAWC,OAAO,GAAGG,MAAM,KAAK,GAAG;gBACrCxB;YACF,OAAO;gBACL,sEAAsE;gBACtE,iEAAiE;gBACjE,mEAAmE;gBACnE,uBAAuB;gBACvBD;YACF;QACF;QACA0B,OAAOC,gBAAgB,CAACxB,+BAA+BqB;QACvD,OAAO,IAAME,OAAOE,mBAAmB,CAACzB,+BAA+BqB;IACzE,GAAG;QAACV;KAAO;IAEX,2EAA2E;IAC3E,0EAA0E;IAC1E,4EAA4E;IAC5E,2EAA2E;IAC3E,yEAAyE;IACzE,uDAAuD;IACvD,MAAMe,aAAa3B;IAEnB,IAAI,CAACY,UAAUC,KAAKU,MAAM,KAAK,KAAK,CAACI,YAAY,OAAO;IAExD,qBACE;kBACGd,KAAKe,GAAG,CAAC,CAAC,EAAEC,MAAM,EAAEC,SAAS,EAAE,GAC9B,qEAAqE;YACrE,sEAAsE;YACtE,oEAAoE;YACpE,sEAAsE;YACtE,sEAAsE;YACtE,kEAAkE;0BAClE,MAAC3B;;kCACC,KAACD;wBACC6B,IAAI,CAAC,OAAO,EAAEF,OAAOE,EAAE,CAAC,KAAK,CAAC;wBAC9BC,UAAS;wBACTjB,OAAOA;wBACD,CAAClB,0BAA0B,EAAEgC,OAAOE,EAAE;kCAE3CF,OAAOI,WAAW,GAAGJ,OAAOI,WAAW,CAACH,aAAa;;oBAWvDD,OAAOK,aAAa,IAAIjB,gBAAgBY,OAAOK,aAAa,IAAI,qBAC/D,KAAChC;wBACC6B,IAAI,CAAC,OAAO,EAAEF,OAAOE,EAAE,CAAC,IAAI,CAAC;wBAC7BC,UAAS;wBACTjB,OAAOA;wBACD,CAAClB,0BAA0B,EAAEgC,OAAOE,EAAE;wBAC5C,0DAA0D;wBAC1D,4DAA4D;wBAC5D,yDAAyD;wBACzDI,KACEN,OAAOO,YAAY,GACfP,OAAOO,YAAY,CAACN,aACpBD,OAAOQ,SAAS;;;eA9BbR,OAAOE,EAAE;;AAsChC"}
@@ -1887,6 +1887,12 @@ export interface CrmPicklistTarget {
1887
1887
  keyField?: string;
1888
1888
  facet?: true;
1889
1889
  arrayKey?: string;
1890
+ /**
1891
+ * The field holding the GROUP of the value the record holds (AGL-3577) —
1892
+ * a lead's `leadSourceDirection` — which every move that can change a
1893
+ * label's group (a regroup, an add, a rename, a delete) rewrites.
1894
+ */
1895
+ groupField?: string;
1890
1896
  }
1891
1897
  /** A standard picklist field of the CRM. */
1892
1898
  export interface CrmPicklistDefinition extends PicklistSpec {
@@ -2006,6 +2012,7 @@ export declare const CRM_PICKLIST_DEFINITIONS: readonly [{
2006
2012
  readonly object: "lead";
2007
2013
  readonly field: "leadSource";
2008
2014
  readonly keyField: "leadSourceKey";
2015
+ readonly groupField: "leadSourceDirection";
2009
2016
  }, {
2010
2017
  readonly object: "contact";
2011
2018
  readonly field: "leadSource";
@@ -2950,6 +2957,22 @@ export declare function crmPicklistKey(value: unknown): string | null;
2950
2957
  /** A lead's lead source as the list compares it: the picklist's key, `null` for none. */
2951
2958
  export declare const CRM_LEAD_SOURCE_KEY_FIELD = "leadSourceKey";
2952
2959
  export declare function crmLeadSourceKey(value: unknown): string | null;
2960
+ export declare const CRM_LEAD_SOURCE_DIRECTION_FIELD = "leadSourceDirection";
2961
+ /** The directions a lead source can have: the Lead source picklist's groups. */
2962
+ export declare const CRM_LEAD_SOURCE_DIRECTIONS: readonly ["inbound", "outbound"];
2963
+ export type CrmLeadSourceDirection = (typeof CRM_LEAD_SOURCE_DIRECTIONS)[number];
2964
+ export declare function isCrmLeadSourceDirection(value: unknown): value is CrmLeadSourceDirection;
2965
+ /** The direction `label` has in the org's lead source list — `null` for none. */
2966
+ export declare function crmLeadSourceDirection(leadSources: PicklistValueSet, label: unknown): CrmLeadSourceDirection | null;
2967
+ /** What a list-field writer knows beyond the record. */
2968
+ export interface CrmListFieldsContext {
2969
+ /**
2970
+ * The org's Lead source list as every reader takes it
2971
+ * (`effectiveCrmLeadSourcePicklist`), which a lead's direction is read
2972
+ * off. Without it a lead holding a lead source keeps the direction it has.
2973
+ */
2974
+ leadSources?: PicklistValueSet | null;
2975
+ }
2953
2976
  /** What a lead's search box reads (AGL-3246). */
2954
2977
  export declare const CRM_LEAD_SEARCH_SOURCES: readonly ["name", "email", "company", "jobTitle", "tags"];
2955
2978
  /**
@@ -2966,12 +2989,14 @@ export declare const CRM_LEAD_SCOPED_CAMPAIGNS_FIELD = "scopedCampaignIds";
2966
2989
  * the status — `new` for a lead nobody has touched, written so `status in
2967
2990
  * [new, working]` finds it — the lead source key and the address verdict.
2968
2991
  */
2969
- export declare function crmLeadListFields(record: object): {
2992
+ export declare function crmLeadListFields(record: object, context?: CrmListFieldsContext): {
2970
2993
  searchTokens: string[];
2971
2994
  scopedSearchTokens: string[];
2972
2995
  scopedCampaignIds: string[];
2973
2996
  status: CrmLeadStatus;
2974
2997
  leadSourceKey: string | null;
2998
+ /** Absent when the lead holds a lead source and no list was given. */
2999
+ leadSourceDirection?: CrmLeadSourceDirection | null;
2975
3000
  emailStatus: string;
2976
3001
  industryKey: string | null;
2977
3002
  ratingKey: string | null;
@@ -3065,13 +3090,16 @@ export declare function crmContactListFields(record: object): {
3065
3090
  * {@link crmListFields} over the document it wrote.
3066
3091
  */
3067
3092
  export declare const CRM_LIST_FIELD_INPUTS: Readonly<Record<CrmListCollection, readonly string[]>>;
3068
- /** Every list field of one record, from the record as stored. */
3069
- export declare function crmListFields(collection: CrmListCollection, record: object): Record<string, unknown>;
3093
+ /**
3094
+ * Every list field of one record, from the record as stored — and, for a
3095
+ * lead's direction, the org's lead source list when `context` holds it.
3096
+ */
3097
+ export declare function crmListFields(collection: CrmListCollection, record: object, context?: CrmListFieldsContext): Record<string, unknown>;
3070
3098
  /**
3071
3099
  * The list fields a stored record carries WRONGLY — the patch that brings
3072
3100
  * it level, `{}` when it already is. What a restamp and a backfill write.
3073
3101
  */
3074
- export declare function crmListFieldsPatch(collection: CrmListCollection, record: object): Record<string, unknown>;
3102
+ export declare function crmListFieldsPatch(collection: CrmListCollection, record: object, context?: CrmListFieldsContext): Record<string, unknown>;
3075
3103
  /**
3076
3104
  * The field each collection's list ORDERS or asks `null` of, which a record
3077
3105
  * must therefore carry even when it has no value: a contact, a company and a
@@ -3087,6 +3115,6 @@ export declare const CRM_LIST_NULLABLE_FIELD: Readonly<Record<CrmListCollection,
3087
3115
  * does not set it — nothing is scheduled against a record that did not
3088
3116
  * exist, and a task created undated is still one its views must reach.
3089
3117
  */
3090
- export declare function crmNewRecordListFields(collection: CrmListCollection, record: object): Record<string, unknown>;
3118
+ export declare function crmNewRecordListFields(collection: CrmListCollection, record: object, context?: CrmListFieldsContext): Record<string, unknown>;
3091
3119
  /** Whether a patch touches a field the collection's list fields are computed from. */
3092
3120
  export declare function crmListFieldsTouched(collection: CrmListCollection, patch: Record<string, unknown>): boolean;
@@ -2384,10 +2384,12 @@ export function isCrmPicklistObject(value) {
2384
2384
  ],
2385
2385
  targets: [
2386
2386
  // The key the Leads list filters by moves with the label (AGL-3321).
2387
+ // So is its direction, the value's group (AGL-3577).
2387
2388
  {
2388
2389
  object: 'lead',
2389
2390
  field: 'leadSource',
2390
- keyField: 'leadSourceKey'
2391
+ keyField: 'leadSourceKey',
2392
+ groupField: 'leadSourceDirection'
2391
2393
  },
2392
2394
  // A contact's lead source is each holder's own, like the rest of its profile.
2393
2395
  {
@@ -3832,9 +3834,11 @@ const CRM_VIEW_SOURCE_FIELD = CRM_CONTACT_VIEW_FIELDS.source;
3832
3834
  * address (`none` when there is none) and a contact's
3833
3835
  * per-holder facet values (`facetKeys`).
3834
3836
  *
3835
- * All of them are DERIVED: a pure function of the document, computed here
3836
- * and nowhere else, and written by every path that writes one of its inputs
3837
- * — see `CRM_LIST_FIELD_INPUTS` — so a record a list cannot find is a writer
3837
+ * All of them are DERIVED: a pure function of the document — save a lead's
3838
+ * `leadSourceDirection`, which is the document and the org's lead source
3839
+ * list (see `crmLeadSourceDirection`) — computed here and nowhere else,
3840
+ * and written by every path that writes one of its inputs — see
3841
+ * `CRM_LIST_FIELD_INPUTS` — so a record a list cannot find is a writer
3838
3842
  * that forgot, never a second formula that disagrees. A server writer that
3839
3843
  * holds the whole document spreads `crmListFields`; one that patched a field
3840
3844
  * restamps the record from what was stored (`restampCrmListFields` in the
@@ -3922,6 +3926,45 @@ export function crmEmailStatusKey(record) {
3922
3926
  export function crmLeadSourceKey(value) {
3923
3927
  return crmPicklistKey(value);
3924
3928
  }
3929
+ /*------------------------------------------
3930
+ * A LEAD'S LEAD SOURCE DIRECTION (AGL-3577).
3931
+ *
3932
+ * The group of the Lead source picklist its value sits in — Inbound for a
3933
+ * person who came to the organization, Outbound for one it went to — or
3934
+ * `null` for no lead source, a value in no group, or a label the list does
3935
+ * not hold. STORED on the lead as `leadSourceDirection`, so the Leads list
3936
+ * asks `leadSourceDirection == inbound` — one equality, however many values
3937
+ * the group holds. (It was asked as `leadSourceKey in [...]` over the
3938
+ * group's values, and Inbound holds more than one `in` may.)
3939
+ *
3940
+ * The one list field that is not a function of the record alone: it is
3941
+ * read off the org's list. So a writer passes that list
3942
+ * ({@link CrmListFieldsContext}) or, without it, writes the direction only
3943
+ * where it needs no list — `null` for a lead with no lead source — and
3944
+ * leaves a held value's direction as stored. Every move of the list that
3945
+ * can change a label's group rewrites the leads holding it (the
3946
+ * `groupField` of the picklist's lead target).
3947
+ *-----------------------------------------*/ export const CRM_LEAD_SOURCE_DIRECTION_FIELD = 'leadSourceDirection';
3948
+ /** The directions a lead source can have: the Lead source picklist's groups. */ export const CRM_LEAD_SOURCE_DIRECTIONS = [
3949
+ 'inbound',
3950
+ 'outbound'
3951
+ ];
3952
+ export function isCrmLeadSourceDirection(value) {
3953
+ return CRM_LEAD_SOURCE_DIRECTIONS.includes(value);
3954
+ }
3955
+ /** The direction `label` has in the org's lead source list — `null` for none. */ export function crmLeadSourceDirection(leadSources, label) {
3956
+ var _picklistValueByLabel;
3957
+ const group = (_picklistValueByLabel = picklistValueByLabel(leadSources, label)) == null ? void 0 : _picklistValueByLabel.group;
3958
+ return isCrmLeadSourceDirection(group) ? group : null;
3959
+ }
3960
+ /** A lead's direction field — see the block above. */ function leadSourceDirectionField(lead, context) {
3961
+ if (!crmLeadSourceKey(lead['leadSource'])) return {
3962
+ leadSourceDirection: null
3963
+ };
3964
+ return context.leadSources ? {
3965
+ leadSourceDirection: crmLeadSourceDirection(context.leadSources, lead['leadSource'])
3966
+ } : {};
3967
+ }
3925
3968
  /** What a lead's search box reads (AGL-3246). */ export const CRM_LEAD_SEARCH_SOURCES = [
3926
3969
  'name',
3927
3970
  'email',
@@ -3945,12 +3988,13 @@ function leadCampaignIds(lead) {
3945
3988
  * will be once written): the search tokens, its campaigns behind its scope,
3946
3989
  * the status — `new` for a lead nobody has touched, written so `status in
3947
3990
  * [new, working]` finds it — the lead source key and the address verdict.
3948
- */ export function crmLeadListFields(record) {
3991
+ */ export function crmLeadListFields(record, context = {}) {
3949
3992
  const lead = record;
3950
3993
  return _extends({}, crmSearchFields(lead['visibleTo'], CRM_LEAD_SEARCH_SOURCES.map((field)=>lead[field])), {
3951
3994
  scopedCampaignIds: scopedSearchTokens(lead['visibleTo'], leadCampaignIds(lead)),
3952
3995
  status: crmLeadStatus(lead),
3953
- leadSourceKey: crmLeadSourceKey(lead['leadSource']),
3996
+ leadSourceKey: crmLeadSourceKey(lead['leadSource'])
3997
+ }, leadSourceDirectionField(lead, context), {
3954
3998
  emailStatus: crmEmailStatusKey(lead),
3955
3999
  // The Industry and Rating the Leads list filters by (AGL-3513).
3956
4000
  industryKey: crmPicklistKey(lead['industry']),
@@ -4186,10 +4230,13 @@ export function crmContactListFields(record) {
4186
4230
  'title'
4187
4231
  ]
4188
4232
  };
4189
- /** Every list field of one record, from the record as stored. */ export function crmListFields(collection, record) {
4233
+ /**
4234
+ * Every list field of one record, from the record as stored — and, for a
4235
+ * lead's direction, the org's lead source list when `context` holds it.
4236
+ */ export function crmListFields(collection, record, context = {}) {
4190
4237
  switch(collection){
4191
4238
  case 'leads':
4192
- return crmLeadListFields(record);
4239
+ return crmLeadListFields(record, context);
4193
4240
  case 'contacts':
4194
4241
  return crmContactListFields(record);
4195
4242
  case 'companies':
@@ -4209,8 +4256,8 @@ const sameValue = (a, b)=>{
4209
4256
  /**
4210
4257
  * The list fields a stored record carries WRONGLY — the patch that brings
4211
4258
  * it level, `{}` when it already is. What a restamp and a backfill write.
4212
- */ export function crmListFieldsPatch(collection, record) {
4213
- const fields = crmListFields(collection, record);
4259
+ */ export function crmListFieldsPatch(collection, record, context = {}) {
4260
+ const fields = crmListFields(collection, record, context);
4214
4261
  const stored = record;
4215
4262
  const patch = {};
4216
4263
  for (const [field, value] of Object.entries(fields)){
@@ -4237,8 +4284,8 @@ const sameValue = (a, b)=>{
4237
4284
  * the collection's {@link CRM_LIST_NULLABLE_FIELD} as `null` when the record
4238
4285
  * does not set it — nothing is scheduled against a record that did not
4239
4286
  * exist, and a task created undated is still one its views must reach.
4240
- */ export function crmNewRecordListFields(collection, record) {
4241
- const fields = crmListFields(collection, record);
4287
+ */ export function crmNewRecordListFields(collection, record, context = {}) {
4288
+ const fields = crmListFields(collection, record, context);
4242
4289
  const nullable = CRM_LIST_NULLABLE_FIELD[collection];
4243
4290
  const stored = record[nullable != null ? nullable : ''];
4244
4291
  if (!nullable || nullable in record && stored !== undefined) return fields;