@aglyn/tenant-feature-instance 1.0.0-beta.218 → 1.0.0-beta.220
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -2
- package/package.json +7 -7
- package/src/lib/components/container-picker.d.ts +70 -0
- package/src/lib/components/container-picker.js +140 -0
- package/src/lib/components/container-picker.js.map +1 -0
- package/src/lib/hooks/firebase/firebase-services.js +15 -3
- package/src/lib/hooks/firebase/firebase-services.js.map +1 -1
- package/src/lib/hooks/firebase/firestore-cache.d.ts +13 -0
- package/src/lib/hooks/firebase/firestore-cache.js +16 -0
- package/src/lib/hooks/firebase/firestore-cache.js.map +1 -1
- package/src/lib/hooks/firebase/firestore-multitab-wedge.d.ts +200 -0
- package/src/lib/hooks/firebase/firestore-multitab-wedge.js +325 -0
- package/src/lib/hooks/firebase/firestore-multitab-wedge.js.map +1 -0
- package/src/lib/hooks/firebase/firestore-stall-recovery.d.ts +29 -2
- package/src/lib/hooks/firebase/firestore-stall-recovery.js +41 -11
- package/src/lib/hooks/firebase/firestore-stall-recovery.js.map +1 -1
- package/src/lib/hooks/use-container-options.d.ts +98 -0
- package/src/lib/hooks/use-container-options.js +174 -0
- package/src/lib/hooks/use-container-options.js.map +1 -0
- package/src/lib/hooks/use-host-document-version.d.ts +52 -0
- package/src/lib/hooks/{use-form-version.js → use-host-document-version.js} +6 -16
- package/src/lib/hooks/use-host-document-version.js.map +1 -0
- package/src/lib/hooks/use-org-member-options.d.ts +1 -1
- package/src/lib/hooks/use-org-member-options.js +1 -1
- package/src/lib/hooks/use-org-member-options.js.map +1 -1
- package/src/lib/tenant.d.ts +2 -2
- package/src/lib/tenant.js +2 -2
- package/src/lib/tenant.js.map +1 -1
- package/src/lib/hooks/use-form-version.d.ts +0 -44
- package/src/lib/hooks/use-form-version.js.map +0 -1
- package/src/lib/hooks/use-host-campaigns.d.ts +0 -98
- package/src/lib/hooks/use-host-campaigns.js +0 -160
- package/src/lib/hooks/use-host-campaigns.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../../libs/tenant/feature/instance/src/lib/hooks/use-org-member-options.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'use client'\n\nimport type { AglynOrgMember } from '@aglyn/aglyn'\nimport { authorizedFetch } from '@aglyn/shared-util-http/authorized-token'\nimport { useEffect, useMemo, useState } from 'react'\nimport { useUser } from './firebase/firebase-services'\n\n/** One team member, as much of them as a picker needs. */\nexport interface OrgMemberOption {\n /** The account uid — what a record's `ownerUid` stores. */\n uid: string\n /** The name a colleague recognizes: display name, else address, else uid. */\n label: string\n email?: string\n}\n\nexport interface OrgMemberOptions {\n options: OrgMemberOption[]\n /** The read has answered, one way or the other — false while settling or disabled. */\n ready: boolean\n /** Why the roster is empty, when it is empty because the route refused. */\n error: string | null\n}\n\n/**\n * The roster as picker options, sorted by how they read.\n *\n * Exported on its own so the mapping has one definition: a bulk \"set owner\"\n * on the contacts list and an audience rule's owner filter must show the\n * same names for the same people, or a merchant assigning contacts to \"Ada\"\n * on one screen would be filtering for \"ada@example.com\" on the other.\n */\nexport function orgMemberOptions(\n members: ReadonlyArray<Partial<AglynOrgMember> & { $id: string }>,\n): OrgMemberOption[] {\n return members\n .map((member) => ({\n uid: String(member.$id),\n label: String(member.displayName || member.email || member.$id),\n email: member.email ? String(member.email) : undefined,\n }))\n .sort((a, b) => a.label.localeCompare(b.label))\n}\n\n/**\n * THE ORG'S TEAM, for an owner picker on some record's page (AGL-2603).\n *\n * ## Through the members route, not the collection\n *\n * `orgs/{orgId}/members` is readable by an org-wide member and by nobody\n * else: the rules evaluate a list against the QUERY, so a collaborator scoped\n * to one site reading the roster gets `permission-denied`, not a shorter\n * roster. `GET /api/orgs/members?orgId=` re-derives membership with the Admin\n * SDK and answers any member of the org, which is who assigns an owner.\n *\n * ## It is OFF unless a caller asks\n *\n * `enabled` defaults to false, the way `
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/tenant/feature/instance/src/lib/hooks/use-org-member-options.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'use client'\n\nimport type { AglynOrgMember } from '@aglyn/aglyn'\nimport { authorizedFetch } from '@aglyn/shared-util-http/authorized-token'\nimport { useEffect, useMemo, useState } from 'react'\nimport { useUser } from './firebase/firebase-services'\n\n/** One team member, as much of them as a picker needs. */\nexport interface OrgMemberOption {\n /** The account uid — what a record's `ownerUid` stores. */\n uid: string\n /** The name a colleague recognizes: display name, else address, else uid. */\n label: string\n email?: string\n}\n\nexport interface OrgMemberOptions {\n options: OrgMemberOption[]\n /** The read has answered, one way or the other — false while settling or disabled. */\n ready: boolean\n /** Why the roster is empty, when it is empty because the route refused. */\n error: string | null\n}\n\n/**\n * The roster as picker options, sorted by how they read.\n *\n * Exported on its own so the mapping has one definition: a bulk \"set owner\"\n * on the contacts list and an audience rule's owner filter must show the\n * same names for the same people, or a merchant assigning contacts to \"Ada\"\n * on one screen would be filtering for \"ada@example.com\" on the other.\n */\nexport function orgMemberOptions(\n members: ReadonlyArray<Partial<AglynOrgMember> & { $id: string }>,\n): OrgMemberOption[] {\n return members\n .map((member) => ({\n uid: String(member.$id),\n label: String(member.displayName || member.email || member.$id),\n email: member.email ? String(member.email) : undefined,\n }))\n .sort((a, b) => a.label.localeCompare(b.label))\n}\n\n/**\n * THE ORG'S TEAM, for an owner picker on some record's page (AGL-2603).\n *\n * ## Through the members route, not the collection\n *\n * `orgs/{orgId}/members` is readable by an org-wide member and by nobody\n * else: the rules evaluate a list against the QUERY, so a collaborator scoped\n * to one site reading the roster gets `permission-denied`, not a shorter\n * roster. `GET /api/orgs/members?orgId=` re-derives membership with the Admin\n * SDK and answers any member of the org, which is who assigns an owner.\n *\n * ## It is OFF unless a caller asks\n *\n * `enabled` defaults to false, the way `useSiteContainerOptions` does and for the\n * same reason: the picker this feeds sits on surfaces a reader opens for\n * something else, and a roster fetched on every arrival at the contacts list\n * is a request for a control nobody has opened. Callers turn it on when the\n * owner control is actually on screen.\n *\n * ## A refusal is reported, never rendered as an empty team\n *\n * An org with no members is impossible — it has an owner — so an empty list\n * with no error would always be a lie. `authorizedFetch` refuses to send a\n * request it cannot authorize and answers a 401 carrying the reason; that\n * reason, or the route's own, is what `error` holds.\n */\nexport function useOrgMemberOptions(\n orgId: string | undefined,\n options?: { enabled?: boolean },\n): OrgMemberOptions {\n const enabled = options?.enabled ?? false\n const { data: user } = useUser()\n const [state, setState] = useState<{\n orgId: string | null\n members: OrgMemberOption[]\n error: string | null\n }>({ orgId: null, members: [], error: null })\n\n useEffect(() => {\n if (!enabled || !orgId) return\n let active = true\n void (async () => {\n let members: OrgMemberOption[] = []\n let error: string | null = null\n try {\n const response = await authorizedFetch(\n user,\n `/api/orgs/members?orgId=${encodeURIComponent(orgId)}`,\n )\n const payload = await response.json().catch(() => ({}))\n if (!response.ok) {\n error = String(payload?.error ?? 'The team could not be read.')\n } else {\n members = orgMemberOptions(\n Array.isArray(payload?.members) ? payload.members : [],\n )\n }\n } catch {\n error = 'The team could not be read.'\n }\n if (active) setState({ orgId, members, error })\n })()\n return () => {\n active = false\n }\n }, [enabled, orgId, user])\n\n return useMemo(\n () => ({\n options: state.orgId === orgId ? state.members : [],\n ready: enabled && Boolean(orgId) && state.orgId === orgId,\n error: state.orgId === orgId ? state.error : null,\n }),\n [state, orgId, enabled],\n )\n}\n\nexport default useOrgMemberOptions\n"],"names":["authorizedFetch","useEffect","useMemo","useState","useUser","orgMemberOptions","members","map","member","uid","String","$id","label","displayName","email","undefined","sort","a","b","localeCompare","useOrgMemberOptions","orgId","options","enabled","data","user","state","setState","error","active","response","encodeURIComponent","payload","json","catch","ok","Array","isArray","ready","Boolean"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;AAGA,SAASA,eAAe,QAAQ,2CAA0C;AAC1E,SAASC,SAAS,EAAEC,OAAO,EAAEC,QAAQ,QAAQ,QAAO;AACpD,SAASC,OAAO,QAAQ,kCAA8B;AAmBtD;;;;;;;CAOC,GACD,OAAO,SAASC,iBACdC,OAAiE;IAEjE,OAAOA,QACJC,GAAG,CAAC,CAACC,SAAY,CAAA;YAChBC,KAAKC,OAAOF,OAAOG,GAAG;YACtBC,OAAOF,OAAOF,OAAOK,WAAW,IAAIL,OAAOM,KAAK,IAAIN,OAAOG,GAAG;YAC9DG,OAAON,OAAOM,KAAK,GAAGJ,OAAOF,OAAOM,KAAK,IAAIC;QAC/C,CAAA,GACCC,IAAI,CAAC,CAACC,GAAGC,IAAMD,EAAEL,KAAK,CAACO,aAAa,CAACD,EAAEN,KAAK;AACjD;AAEA;;;;;;;;;;;;;;;;;;;;;;;;;CAyBC,GACD,OAAO,SAASQ,oBACdC,KAAyB,EACzBC,OAA+B;;IAE/B,MAAMC,kBAAUD,2BAAAA,QAASC,OAAO,mBAAI;IACpC,MAAM,EAAEC,MAAMC,IAAI,EAAE,GAAGrB;IACvB,MAAM,CAACsB,OAAOC,SAAS,GAAGxB,SAIvB;QAAEkB,OAAO;QAAMf,SAAS,EAAE;QAAEsB,OAAO;IAAK;IAE3C3B,UAAU;QACR,IAAI,CAACsB,WAAW,CAACF,OAAO;QACxB,IAAIQ,SAAS;QACb,KAAK,AAAC,CAAA;YACJ,IAAIvB,UAA6B,EAAE;YACnC,IAAIsB,QAAuB;YAC3B,IAAI;gBACF,MAAME,WAAW,MAAM9B,gBACrByB,MACA,CAAC,wBAAwB,EAAEM,mBAAmBV,QAAQ;gBAExD,MAAMW,UAAU,MAAMF,SAASG,IAAI,GAAGC,KAAK,CAAC,IAAO,CAAA,CAAC,CAAA;gBACpD,IAAI,CAACJ,SAASK,EAAE,EAAE;;oBAChBP,QAAQlB,eAAOsB,2BAAAA,QAASJ,KAAK,mBAAI;gBACnC,OAAO;oBACLtB,UAAUD,iBACR+B,MAAMC,OAAO,CAACL,2BAAAA,QAAS1B,OAAO,IAAI0B,QAAQ1B,OAAO,GAAG,EAAE;gBAE1D;YACF,EAAE,eAAM;gBACNsB,QAAQ;YACV;YACA,IAAIC,QAAQF,SAAS;gBAAEN;gBAAOf;gBAASsB;YAAM;QAC/C,CAAA;QACA,OAAO;YACLC,SAAS;QACX;IACF,GAAG;QAACN;QAASF;QAAOI;KAAK;IAEzB,OAAOvB,QACL,IAAO,CAAA;YACLoB,SAASI,MAAML,KAAK,KAAKA,QAAQK,MAAMpB,OAAO,GAAG,EAAE;YACnDgC,OAAOf,WAAWgB,QAAQlB,UAAUK,MAAML,KAAK,KAAKA;YACpDO,OAAOF,MAAML,KAAK,KAAKA,QAAQK,MAAME,KAAK,GAAG;QAC/C,CAAA,GACA;QAACF;QAAOL;QAAOE;KAAQ;AAE3B;AAEA,eAAeH,oBAAmB"}
|
package/src/lib/tenant.d.ts
CHANGED
|
@@ -41,11 +41,11 @@ export * from './hooks/use-host-activity-logger';
|
|
|
41
41
|
export * from './hooks/use-live-artifact-count';
|
|
42
42
|
export * from './hooks/use-component';
|
|
43
43
|
export * from './hooks/use-component-version';
|
|
44
|
-
export * from './hooks/use-
|
|
44
|
+
export * from './hooks/use-host-document-version';
|
|
45
45
|
export * from './hooks/use-host-template';
|
|
46
46
|
export * from './hooks/use-console-host-route';
|
|
47
47
|
export * from './hooks/use-host-org-id';
|
|
48
|
-
export * from './hooks/use-
|
|
48
|
+
export * from './hooks/use-container-options';
|
|
49
49
|
export * from './hooks/use-org-member-options';
|
|
50
50
|
export * from './hooks/use-org-plan';
|
|
51
51
|
export * from './hooks/use-scope-tokens';
|
package/src/lib/tenant.js
CHANGED
|
@@ -40,11 +40,11 @@ export * from "./hooks/use-host-activity-logger.js";
|
|
|
40
40
|
export * from "./hooks/use-live-artifact-count.js";
|
|
41
41
|
export * from "./hooks/use-component.js";
|
|
42
42
|
export * from "./hooks/use-component-version.js";
|
|
43
|
-
export * from "./hooks/use-
|
|
43
|
+
export * from "./hooks/use-host-document-version.js";
|
|
44
44
|
export * from "./hooks/use-host-template.js";
|
|
45
45
|
export * from "./hooks/use-console-host-route.js";
|
|
46
46
|
export * from "./hooks/use-host-org-id.js";
|
|
47
|
-
export * from "./hooks/use-
|
|
47
|
+
export * from "./hooks/use-container-options.js";
|
|
48
48
|
export * from "./hooks/use-org-member-options.js";
|
|
49
49
|
export * from "./hooks/use-org-plan.js";
|
|
50
50
|
export * from "./hooks/use-scope-tokens.js";
|
package/src/lib/tenant.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/tenant/feature/instance/src/lib/tenant.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2022 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\nexport * from './constants/mock-data'\nexport * from './constants/site-paths'\nexport * from './constants/firebase-config'\nexport * from './constants/firebase-emulator-hosts'\n\nexport * from './hooks/firebase/auth-persistence'\nexport * from './hooks/firebase/firebase-services'\nexport * from './hooks/firebase/firestore-cache'\nexport * from './hooks/firebase/firestore-stall-recovery'\nexport * from './hooks/firebase/firestore-bounded-read'\nexport * from './hooks/helpers/use-doc'\nexport * from './hooks/helpers/use-modify-doc-callback'\nexport * from './hooks/helpers/besigner-nodes-converter'\nexport * from './hooks/helpers/guarded-nodes-save'\nexport * from './hooks/helpers/guarded-seed-write'\n\nexport * from './hooks/firestore-denial-reporter'\nexport * from './hooks/use-firestore-collection'\nexport * from './hooks/use-paged-collection'\nexport * from './hooks/use-sorted-paged-collection'\nexport * from './hooks/sorted-collection-window'\nexport * from './hooks/host-collection-queries'\nexport * from './hooks/list-filter-constraints'\nexport * from './hooks/use-firestore-doc'\nexport * from './hooks/use-switcher-collection'\nexport * from './hooks/use-host-activity-logger'\nexport * from './hooks/use-live-artifact-count'\nexport * from './hooks/use-component'\nexport * from './hooks/use-component-version'\nexport * from './hooks/use-
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/tenant/feature/instance/src/lib/tenant.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2022 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\nexport * from './constants/mock-data'\nexport * from './constants/site-paths'\nexport * from './constants/firebase-config'\nexport * from './constants/firebase-emulator-hosts'\n\nexport * from './hooks/firebase/auth-persistence'\nexport * from './hooks/firebase/firebase-services'\nexport * from './hooks/firebase/firestore-cache'\nexport * from './hooks/firebase/firestore-stall-recovery'\nexport * from './hooks/firebase/firestore-bounded-read'\nexport * from './hooks/helpers/use-doc'\nexport * from './hooks/helpers/use-modify-doc-callback'\nexport * from './hooks/helpers/besigner-nodes-converter'\nexport * from './hooks/helpers/guarded-nodes-save'\nexport * from './hooks/helpers/guarded-seed-write'\n\nexport * from './hooks/firestore-denial-reporter'\nexport * from './hooks/use-firestore-collection'\nexport * from './hooks/use-paged-collection'\nexport * from './hooks/use-sorted-paged-collection'\nexport * from './hooks/sorted-collection-window'\nexport * from './hooks/host-collection-queries'\nexport * from './hooks/list-filter-constraints'\nexport * from './hooks/use-firestore-doc'\nexport * from './hooks/use-switcher-collection'\nexport * from './hooks/use-host-activity-logger'\nexport * from './hooks/use-live-artifact-count'\nexport * from './hooks/use-component'\nexport * from './hooks/use-component-version'\nexport * from './hooks/use-host-document-version'\nexport * from './hooks/use-host-template'\nexport * from './hooks/use-console-host-route'\nexport * from './hooks/use-host-org-id'\nexport * from './hooks/use-container-options'\nexport * from './hooks/use-org-member-options'\nexport * from './hooks/use-org-plan'\nexport * from './hooks/use-scope-tokens'\nexport * from './hooks/use-host-resource-api'\nexport * from './hooks/use-host-version-api'\nexport * from './hooks/use-duplicate-resource-api'\nexport * from './hooks/use-plugin-api-post'\nexport * from './components/duplicate-resource-dialog'\nexport * from './hooks/use-plugin-config'\nexport * from './hooks/use-host'\nexport * from './hooks/use-layout'\nexport * from './hooks/use-layout-version'\nexport * from './hooks/use-screen'\nexport * from './hooks/use-screen-version'\nexport * from './hooks/use-user-name'\nexport * from './hooks/use-user-photo'\n"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,cAAc,2BAAuB;AACrC,cAAc,4BAAwB;AACtC,cAAc,iCAA6B;AAC3C,cAAc,yCAAqC;AAEnD,cAAc,uCAAmC;AACjD,cAAc,wCAAoC;AAClD,cAAc,sCAAkC;AAChD,cAAc,+CAA2C;AACzD,cAAc,6CAAyC;AACvD,cAAc,6BAAyB;AACvC,cAAc,6CAAyC;AACvD,cAAc,8CAA0C;AACxD,cAAc,wCAAoC;AAClD,cAAc,wCAAoC;AAElD,cAAc,uCAAmC;AACjD,cAAc,sCAAkC;AAChD,cAAc,kCAA8B;AAC5C,cAAc,yCAAqC;AACnD,cAAc,sCAAkC;AAChD,cAAc,qCAAiC;AAC/C,cAAc,qCAAiC;AAC/C,cAAc,+BAA2B;AACzC,cAAc,qCAAiC;AAC/C,cAAc,sCAAkC;AAChD,cAAc,qCAAiC;AAC/C,cAAc,2BAAuB;AACrC,cAAc,mCAA+B;AAC7C,cAAc,uCAAmC;AACjD,cAAc,+BAA2B;AACzC,cAAc,oCAAgC;AAC9C,cAAc,6BAAyB;AACvC,cAAc,mCAA+B;AAC7C,cAAc,oCAAgC;AAC9C,cAAc,0BAAsB;AACpC,cAAc,8BAA0B;AACxC,cAAc,mCAA+B;AAC7C,cAAc,kCAA8B;AAC5C,cAAc,wCAAoC;AAClD,cAAc,iCAA6B;AAC3C,cAAc,4CAAwC;AACtD,cAAc,+BAA2B;AACzC,cAAc,sBAAkB;AAChC,cAAc,wBAAoB;AAClC,cAAc,gCAA4B;AAC1C,cAAc,wBAAoB;AAClC,cAAc,gCAA4B;AAC1C,cAAc,2BAAuB;AACrC,cAAc,4BAAwB"}
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @license
|
|
3
|
-
* Copyright 2026 Aglyn LLC
|
|
4
|
-
*
|
|
5
|
-
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
-
* you may not use this file except in compliance with the License.
|
|
7
|
-
* You may obtain a copy of the License at
|
|
8
|
-
*
|
|
9
|
-
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
-
*
|
|
11
|
-
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
-
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
-
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
-
* See the License for the specific language governing permissions and
|
|
15
|
-
* limitations under the License.
|
|
16
|
-
*/
|
|
17
|
-
import type * as Aglyn from '@aglyn/aglyn';
|
|
18
|
-
import type { DocumentReference } from 'firebase/firestore';
|
|
19
|
-
import { type FirestoreDocOptions } from './firebase/firebase-services';
|
|
20
|
-
/**
|
|
21
|
-
* A form's working version, at
|
|
22
|
-
* `hosts/{hostId}/forms/{formId}/versions/{versionId}`.
|
|
23
|
-
*
|
|
24
|
-
* Compressed at rest, and carrying the same asymmetry a component version
|
|
25
|
-
* does: the PUBLISHED tree lives on the parent `forms/{formId}` document,
|
|
26
|
-
* because a placed form has to resolve on the hot path of a page render and
|
|
27
|
-
* one collection query reaches every form on the site. These documents are
|
|
28
|
-
* editing history — the besigner's draft surface — and nothing renders from
|
|
29
|
-
* them.
|
|
30
|
-
*/
|
|
31
|
-
export declare const useFormVersionRef: ({ hostId, formId, versionId, }: {
|
|
32
|
-
hostId: string;
|
|
33
|
-
formId: string;
|
|
34
|
-
versionId: string;
|
|
35
|
-
}) => DocumentReference<Aglyn.FormVersion>;
|
|
36
|
-
export declare const useFormVersion: (data: {
|
|
37
|
-
hostId: string;
|
|
38
|
-
formId: string;
|
|
39
|
-
versionId: string;
|
|
40
|
-
}, options?: FirestoreDocOptions<Aglyn.FormVersion>) => {
|
|
41
|
-
doc: import("./firebase/firebase-services").ObservableStatus<Aglyn.FormVersion<Aglyn.AglynNodeSchema<Partial<Record<JSX.Key.Any, unknown>>>>>;
|
|
42
|
-
setDoc: import("./helpers/use-modify-doc-callback").ModifyDocCallback<Aglyn.FormVersion<Aglyn.AglynNodeSchema<Partial<Record<JSX.Key.Any, unknown>>>>>;
|
|
43
|
-
};
|
|
44
|
-
export default useFormVersion;
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../../libs/tenant/feature/instance/src/lib/hooks/use-form-version.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// A TYPE-only namespace: every `Aglyn.` reference below is a type, and held\n// as a VALUE the namespace pins every module the barrel reaches into the\n// console shell that renders these hooks. `import type` erases instead.\nimport type * as Aglyn from '@aglyn/aglyn'\nimport { compress, decompress } from '@aglyn/aglyn'\nimport { Timestamp } from '@aglyn/shared-util-timestamp'\nimport type { DocumentReference } from 'firebase/firestore'\nimport { Bytes, doc } from 'firebase/firestore'\nimport {\n useFirestore,\n type FirestoreDocOptions,\n} from './firebase/firebase-services'\nimport useDoc from './helpers/use-doc'\n\n/**\n * A form's working version, at\n * `hosts/{hostId}/forms/{formId}/versions/{versionId}`.\n *\n * Compressed at rest, and carrying the same asymmetry a component version\n * does: the PUBLISHED tree lives on the parent `forms/{formId}` document,\n * because a placed form has to resolve on the hot path of a page render and\n * one collection query reaches every form on the site. These documents are\n * editing history — the besigner's draft surface — and nothing renders from\n * them.\n */\nexport const useFormVersionRef = ({\n hostId,\n formId,\n versionId,\n}: {\n hostId: string\n formId: string\n versionId: string\n}) => {\n const firestore = useFirestore()\n const ref = doc(\n firestore,\n 'hosts',\n hostId,\n 'forms',\n formId,\n 'versions',\n versionId,\n )\n return ref.withConverter({\n toFirestore(data) {\n const { $id, ...rest } = data\n // Only emit `nodes` when the write carries them — see the note in\n // use-screen-version (AGL-1250). A partial write that compressed\n // `undefined` would replace the stored tree with an empty one.\n if (rest?.nodes === undefined)\n return { ...rest, updatedAt: Timestamp.now() }\n const nodes =\n rest.nodes instanceof Bytes\n ? rest.nodes\n : Bytes.fromUint8Array(compress(rest.nodes))\n return { ...rest, nodes, updatedAt: Timestamp.now() }\n },\n fromFirestore(snapshot, options) {\n if (!snapshot.exists()) return undefined\n const data = snapshot.data(options)\n if (data?.nodes instanceof Bytes) {\n return {\n ...data,\n nodes: decompress(data.nodes),\n } as Aglyn.FormVersion\n }\n return data as Aglyn.FormVersion\n },\n }) as DocumentReference<Aglyn.FormVersion>\n}\n\nexport const useFormVersion = (\n data: {\n hostId: string\n formId: string\n versionId: string\n },\n options?: FirestoreDocOptions<Aglyn.FormVersion>,\n) => {\n return useDoc(useFormVersionRef(data), options)\n}\n\nexport default useFormVersion\n"],"names":["compress","decompress","Timestamp","Bytes","doc","useFirestore","useDoc","useFormVersionRef","hostId","formId","versionId","firestore","ref","withConverter","toFirestore","data","$id","rest","nodes","undefined","updatedAt","now","fromUint8Array","fromFirestore","snapshot","options","exists","useFormVersion"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,4EAA4E;AAC5E,yEAAyE;AACzE,wEAAwE;;;AAExE,SAASA,QAAQ,EAAEC,UAAU,QAAQ,eAAc;AACnD,SAASC,SAAS,QAAQ,+BAA8B;AAExD,SAASC,KAAK,EAAEC,GAAG,QAAQ,qBAAoB;AAC/C,SACEC,YAAY,QAEP,kCAA8B;AACrC,OAAOC,YAAY,uBAAmB;AAEtC;;;;;;;;;;CAUC,GACD,OAAO,MAAMC,oBAAoB,CAAC,EAChCC,MAAM,EACNC,MAAM,EACNC,SAAS,EAKV;IACC,MAAMC,YAAYN;IAClB,MAAMO,MAAMR,IACVO,WACA,SACAH,QACA,SACAC,QACA,YACAC;IAEF,OAAOE,IAAIC,aAAa,CAAC;QACvBC,aAAYC,IAAI;YACd,MAAM,EAAEC,GAAG,EAAW,GAAGD,MAATE,wCAASF;;;YACzB,kEAAkE;YAClE,iEAAiE;YACjE,+DAA+D;YAC/D,IAAIE,CAAAA,wBAAAA,KAAMC,KAAK,MAAKC,WAClB,OAAO,aAAKF;gBAAMG,WAAWlB,UAAUmB,GAAG;;YAC5C,MAAMH,QACJD,KAAKC,KAAK,YAAYf,QAClBc,KAAKC,KAAK,GACVf,MAAMmB,cAAc,CAACtB,SAASiB,KAAKC,KAAK;YAC9C,OAAO,aAAKD;gBAAMC;gBAAOE,WAAWlB,UAAUmB,GAAG;;QACnD;QACAE,eAAcC,QAAQ,EAAEC,OAAO;YAC7B,IAAI,CAACD,SAASE,MAAM,IAAI,OAAOP;YAC/B,MAAMJ,OAAOS,SAAST,IAAI,CAACU;YAC3B,IAAIV,CAAAA,wBAAAA,KAAMG,KAAK,aAAYf,OAAO;gBAChC,OAAO,aACFY;oBACHG,OAAOjB,WAAWc,KAAKG,KAAK;;YAEhC;YACA,OAAOH;QACT;IACF;AACF,EAAC;AAED,OAAO,MAAMY,iBAAiB,CAC5BZ,MAKAU;IAEA,OAAOnB,OAAOC,kBAAkBQ,OAAOU;AACzC,EAAC;AAED,eAAeE,eAAc"}
|
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* How many campaigns a picker offers.
|
|
3
|
-
*
|
|
4
|
-
* The same fifty the campaigns table draws, so a campaign a merchant can see
|
|
5
|
-
* in the list is a campaign they can file a record under. A picker with a
|
|
6
|
-
* larger window would offer campaigns the list they came from does not show;
|
|
7
|
-
* a smaller one would hide campaigns that are plainly there.
|
|
8
|
-
*/
|
|
9
|
-
export declare const HOST_CAMPAIGN_CEILING = 50;
|
|
10
|
-
/** One campaign, as much of it as a picker needs. */
|
|
11
|
-
export interface HostCampaignOption {
|
|
12
|
-
value: string;
|
|
13
|
-
label: string;
|
|
14
|
-
/** The campaign window, for a caller that wants to say when it ran. */
|
|
15
|
-
startAtMs?: number | null;
|
|
16
|
-
endAtMs?: number | null;
|
|
17
|
-
/**
|
|
18
|
-
* The sites the campaign is placed on, `null` when it is on every site.
|
|
19
|
-
* Read from its `visibleTo`; an unscoped campaign is on no site (`[]`).
|
|
20
|
-
*/
|
|
21
|
-
siteIds?: string[] | null;
|
|
22
|
-
}
|
|
23
|
-
export interface HostCampaigns {
|
|
24
|
-
options: HostCampaignOption[];
|
|
25
|
-
/** The org holds more matching campaigns than the ceiling offers. */
|
|
26
|
-
truncated: boolean;
|
|
27
|
-
/** The read has answered — false while it is still settling or disabled. */
|
|
28
|
-
ready: boolean;
|
|
29
|
-
}
|
|
30
|
-
/**
|
|
31
|
-
* The picker's shape, from a ceilinged read of campaign containers.
|
|
32
|
-
*
|
|
33
|
-
* One mapping for both hooks below, so a campaign reads the same on a site's
|
|
34
|
-
* picker and on the organization's.
|
|
35
|
-
*/
|
|
36
|
-
export declare function hostCampaignOptions(data: ReadonlyArray<Record<string, unknown>> | null | undefined, ready: boolean): HostCampaigns;
|
|
37
|
-
/**
|
|
38
|
-
* THE SITE'S CAMPAIGNS, for a picker on some other record's page.
|
|
39
|
-
*
|
|
40
|
-
* Lives here rather than in the marketing plugin because the console app may
|
|
41
|
-
* not import a feature plugin — a screen's detail page is an app route — and
|
|
42
|
-
* the form and contact surfaces that need the same list are two other
|
|
43
|
-
* libraries again. One read, one shape, three callers.
|
|
44
|
-
*
|
|
45
|
-
* ## The org's campaigns, as this site sees them
|
|
46
|
-
*
|
|
47
|
-
* Campaign containers belong to the organization
|
|
48
|
-
* (`orgs/{orgId}/emailCampaigns`), and each is placed on some of its sites
|
|
49
|
-
* by its `visibleTo`. A site's picker offers the ones placed on it: `'org'`
|
|
50
|
-
* (every site) or `host:{hostId}`, which is `scopeTokensForHost`. The clause
|
|
51
|
-
* is also what makes the read provable for a collaborator scoped to this
|
|
52
|
-
* site — the rules evaluate `visibleTo` per document on a list, and an
|
|
53
|
-
* unfiltered query is refused whole rather than narrowed.
|
|
54
|
-
*
|
|
55
|
-
* The org comes from the `hostIndex` lookup. Nothing is read until it
|
|
56
|
-
* answers, and a host with no org settles as ready with no campaigns.
|
|
57
|
-
*
|
|
58
|
-
* ## It is OFF unless a caller asks
|
|
59
|
-
*
|
|
60
|
-
* `enabled` defaults to false. The picker this feeds sits on a record's own
|
|
61
|
-
* page beside fields a reader came for; charging every open of a form, a
|
|
62
|
-
* screen or a contact for a hundred campaign documents they may not touch is
|
|
63
|
-
* the read-on-mount this console refuses. The callers turn it on when their
|
|
64
|
-
* editing surface opens.
|
|
65
|
-
*
|
|
66
|
-
* ## Ordered on the document name
|
|
67
|
-
*
|
|
68
|
-
* `collectionCeiling`, for the reason it exists: `startAtMs` and `endAtMs`
|
|
69
|
-
* are both optional on a campaign — an open-ended one carries neither — so
|
|
70
|
-
* ordering on a date would not mis-sort the picker, it would DROP the
|
|
71
|
-
* campaigns that have no dates. The labels are sorted by name here, over the
|
|
72
|
-
* whole ceiling rather than a slice of it, which is the one case sorting a
|
|
73
|
-
* window is honest. It is also the one ordering the automatic single-field
|
|
74
|
-
* index on `visibleTo` serves beside `array-contains-any`, so the query
|
|
75
|
-
* needs no composite index.
|
|
76
|
-
*/
|
|
77
|
-
export declare function useHostCampaigns(hostId: string | undefined, options?: {
|
|
78
|
-
enabled?: boolean;
|
|
79
|
-
}): HostCampaigns;
|
|
80
|
-
/**
|
|
81
|
-
* THE ORGANIZATION'S CAMPAIGNS, for a picker on an org-level record — a
|
|
82
|
-
* lead on the org's Leads page, an outbound sequence.
|
|
83
|
-
*
|
|
84
|
-
* Every live container, whichever sites it is placed on: the records these
|
|
85
|
-
* pickers file belong to the organization, and so does the campaign. The
|
|
86
|
-
* read is unfiltered because the surfaces that call it are the org-level
|
|
87
|
-
* hubs, whose readers are org-wide members — the rules admit them to every
|
|
88
|
-
* container, and a `visibleTo` clause would only need the org's whole site
|
|
89
|
-
* list and still miss a site it did not carry. Each option carries its
|
|
90
|
-
* `siteIds`, for a caller that wants to say where a campaign runs.
|
|
91
|
-
*
|
|
92
|
-
* Off unless a caller asks, and ceilinged and ordered, for the reasons
|
|
93
|
-
* {@link useHostCampaigns} gives.
|
|
94
|
-
*/
|
|
95
|
-
export declare function useOrgCampaigns(orgId: string | null | undefined, options?: {
|
|
96
|
-
enabled?: boolean;
|
|
97
|
-
}): HostCampaigns;
|
|
98
|
-
export default useHostCampaigns;
|
|
@@ -1,160 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @license
|
|
3
|
-
* Copyright 2026 Aglyn LLC
|
|
4
|
-
*
|
|
5
|
-
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
-
* you may not use this file except in compliance with the License.
|
|
7
|
-
* You may obtain a copy of the License at
|
|
8
|
-
*
|
|
9
|
-
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
-
*
|
|
11
|
-
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
-
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
-
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
-
* See the License for the specific language governing permissions and
|
|
15
|
-
* limitations under the License.
|
|
16
|
-
*/ 'use client';
|
|
17
|
-
import { hostIdsFromScope, isOrgWideScope, scopeTokensForHost } from "@aglyn/aglyn";
|
|
18
|
-
import { collection, query, where } from "firebase/firestore";
|
|
19
|
-
import { useMemo } from "react";
|
|
20
|
-
import { collectionCeiling } from "./host-collection-queries.js";
|
|
21
|
-
import { useFirestore } from "./firebase/firebase-services.js";
|
|
22
|
-
import { useFirestoreCollection } from "./use-firestore-collection.js";
|
|
23
|
-
import { useHostOrgIdState } from "./use-host-org-id.js";
|
|
24
|
-
/**
|
|
25
|
-
* How many campaigns a picker offers.
|
|
26
|
-
*
|
|
27
|
-
* The same fifty the campaigns table draws, so a campaign a merchant can see
|
|
28
|
-
* in the list is a campaign they can file a record under. A picker with a
|
|
29
|
-
* larger window would offer campaigns the list they came from does not show;
|
|
30
|
-
* a smaller one would hide campaigns that are plainly there.
|
|
31
|
-
*/ export const HOST_CAMPAIGN_CEILING = 50;
|
|
32
|
-
const NO_ROWS = Object.freeze([]);
|
|
33
|
-
/**
|
|
34
|
-
* The picker's shape, from a ceilinged read of campaign containers.
|
|
35
|
-
*
|
|
36
|
-
* One mapping for both hooks below, so a campaign reads the same on a site's
|
|
37
|
-
* picker and on the organization's.
|
|
38
|
-
*/ export function hostCampaignOptions(data, ready) {
|
|
39
|
-
const rows = data != null ? data : [];
|
|
40
|
-
const truncated = rows.length > HOST_CAMPAIGN_CEILING;
|
|
41
|
-
const live = rows.slice(0, HOST_CAMPAIGN_CEILING)// A campaign the console soft-deleted is not a campaign a record may be
|
|
42
|
-
// filed under. The campaigns table filters the same field for the same
|
|
43
|
-
// reason.
|
|
44
|
-
.filter((row)=>!row['deletedAt']);
|
|
45
|
-
return {
|
|
46
|
-
options: live.map((row)=>{
|
|
47
|
-
var _row_name, _row_startAtMs, _row_endAtMs;
|
|
48
|
-
const visibleTo = Array.isArray(row['visibleTo']) ? row['visibleTo'] : [];
|
|
49
|
-
return {
|
|
50
|
-
value: String(row['$id']),
|
|
51
|
-
label: String((_row_name = row['name']) != null ? _row_name : row['$id']),
|
|
52
|
-
startAtMs: (_row_startAtMs = row['startAtMs']) != null ? _row_startAtMs : null,
|
|
53
|
-
endAtMs: (_row_endAtMs = row['endAtMs']) != null ? _row_endAtMs : null,
|
|
54
|
-
siteIds: isOrgWideScope(visibleTo) ? null : hostIdsFromScope(visibleTo)
|
|
55
|
-
};
|
|
56
|
-
}).sort((a, b)=>a.label.localeCompare(b.label)),
|
|
57
|
-
truncated,
|
|
58
|
-
ready
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
/**
|
|
62
|
-
* THE SITE'S CAMPAIGNS, for a picker on some other record's page.
|
|
63
|
-
*
|
|
64
|
-
* Lives here rather than in the marketing plugin because the console app may
|
|
65
|
-
* not import a feature plugin — a screen's detail page is an app route — and
|
|
66
|
-
* the form and contact surfaces that need the same list are two other
|
|
67
|
-
* libraries again. One read, one shape, three callers.
|
|
68
|
-
*
|
|
69
|
-
* ## The org's campaigns, as this site sees them
|
|
70
|
-
*
|
|
71
|
-
* Campaign containers belong to the organization
|
|
72
|
-
* (`orgs/{orgId}/emailCampaigns`), and each is placed on some of its sites
|
|
73
|
-
* by its `visibleTo`. A site's picker offers the ones placed on it: `'org'`
|
|
74
|
-
* (every site) or `host:{hostId}`, which is `scopeTokensForHost`. The clause
|
|
75
|
-
* is also what makes the read provable for a collaborator scoped to this
|
|
76
|
-
* site — the rules evaluate `visibleTo` per document on a list, and an
|
|
77
|
-
* unfiltered query is refused whole rather than narrowed.
|
|
78
|
-
*
|
|
79
|
-
* The org comes from the `hostIndex` lookup. Nothing is read until it
|
|
80
|
-
* answers, and a host with no org settles as ready with no campaigns.
|
|
81
|
-
*
|
|
82
|
-
* ## It is OFF unless a caller asks
|
|
83
|
-
*
|
|
84
|
-
* `enabled` defaults to false. The picker this feeds sits on a record's own
|
|
85
|
-
* page beside fields a reader came for; charging every open of a form, a
|
|
86
|
-
* screen or a contact for a hundred campaign documents they may not touch is
|
|
87
|
-
* the read-on-mount this console refuses. The callers turn it on when their
|
|
88
|
-
* editing surface opens.
|
|
89
|
-
*
|
|
90
|
-
* ## Ordered on the document name
|
|
91
|
-
*
|
|
92
|
-
* `collectionCeiling`, for the reason it exists: `startAtMs` and `endAtMs`
|
|
93
|
-
* are both optional on a campaign — an open-ended one carries neither — so
|
|
94
|
-
* ordering on a date would not mis-sort the picker, it would DROP the
|
|
95
|
-
* campaigns that have no dates. The labels are sorted by name here, over the
|
|
96
|
-
* whole ceiling rather than a slice of it, which is the one case sorting a
|
|
97
|
-
* window is honest. It is also the one ordering the automatic single-field
|
|
98
|
-
* index on `visibleTo` serves beside `array-contains-any`, so the query
|
|
99
|
-
* needs no composite index.
|
|
100
|
-
*/ export function useHostCampaigns(hostId, options) {
|
|
101
|
-
var _ref;
|
|
102
|
-
const enabled = (_ref = options == null ? void 0 : options.enabled) != null ? _ref : false;
|
|
103
|
-
const firestore = useFirestore();
|
|
104
|
-
const org = useHostOrgIdState(enabled ? hostId : undefined);
|
|
105
|
-
const orgId = org.orgId;
|
|
106
|
-
// Memoised: a listener dependency, and a fresh array each render would
|
|
107
|
-
// reopen the subscription every time.
|
|
108
|
-
const tokens = useMemo(()=>hostId ? scopeTokensForHost(hostId) : null, [
|
|
109
|
-
hostId
|
|
110
|
-
]);
|
|
111
|
-
const { data, status } = useFirestoreCollection(()=>enabled && hostId && orgId && tokens ? collectionCeiling(query(collection(firestore, 'orgs', orgId, 'emailCampaigns'), where('visibleTo', 'array-contains-any', tokens)), HOST_CAMPAIGN_CEILING) : null, [
|
|
112
|
-
firestore,
|
|
113
|
-
hostId,
|
|
114
|
-
orgId,
|
|
115
|
-
tokens,
|
|
116
|
-
enabled
|
|
117
|
-
], {
|
|
118
|
-
idField: '$id'
|
|
119
|
-
});
|
|
120
|
-
const ready = enabled && Boolean(hostId) && org.loaded && (orgId ? status !== 'loading' : true);
|
|
121
|
-
return useMemo(()=>hostCampaignOptions(orgId ? data : NO_ROWS, ready), [
|
|
122
|
-
data,
|
|
123
|
-
orgId,
|
|
124
|
-
ready
|
|
125
|
-
]);
|
|
126
|
-
}
|
|
127
|
-
/**
|
|
128
|
-
* THE ORGANIZATION'S CAMPAIGNS, for a picker on an org-level record — a
|
|
129
|
-
* lead on the org's Leads page, an outbound sequence.
|
|
130
|
-
*
|
|
131
|
-
* Every live container, whichever sites it is placed on: the records these
|
|
132
|
-
* pickers file belong to the organization, and so does the campaign. The
|
|
133
|
-
* read is unfiltered because the surfaces that call it are the org-level
|
|
134
|
-
* hubs, whose readers are org-wide members — the rules admit them to every
|
|
135
|
-
* container, and a `visibleTo` clause would only need the org's whole site
|
|
136
|
-
* list and still miss a site it did not carry. Each option carries its
|
|
137
|
-
* `siteIds`, for a caller that wants to say where a campaign runs.
|
|
138
|
-
*
|
|
139
|
-
* Off unless a caller asks, and ceilinged and ordered, for the reasons
|
|
140
|
-
* {@link useHostCampaigns} gives.
|
|
141
|
-
*/ export function useOrgCampaigns(orgId, options) {
|
|
142
|
-
var _ref;
|
|
143
|
-
const enabled = (_ref = options == null ? void 0 : options.enabled) != null ? _ref : false;
|
|
144
|
-
const firestore = useFirestore();
|
|
145
|
-
const { data, status } = useFirestoreCollection(()=>enabled && orgId ? collectionCeiling(collection(firestore, 'orgs', orgId, 'emailCampaigns'), HOST_CAMPAIGN_CEILING) : null, [
|
|
146
|
-
firestore,
|
|
147
|
-
orgId,
|
|
148
|
-
enabled
|
|
149
|
-
], {
|
|
150
|
-
idField: '$id'
|
|
151
|
-
});
|
|
152
|
-
const ready = enabled && Boolean(orgId) && status !== 'loading';
|
|
153
|
-
return useMemo(()=>hostCampaignOptions(data, ready), [
|
|
154
|
-
data,
|
|
155
|
-
ready
|
|
156
|
-
]);
|
|
157
|
-
}
|
|
158
|
-
export default useHostCampaigns;
|
|
159
|
-
|
|
160
|
-
//# sourceMappingURL=use-host-campaigns.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../../libs/tenant/feature/instance/src/lib/hooks/use-host-campaigns.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'use client'\n\nimport {\n hostIdsFromScope,\n isOrgWideScope,\n scopeTokensForHost,\n} from '@aglyn/aglyn'\nimport { collection, query, where } from 'firebase/firestore'\nimport { useMemo } from 'react'\nimport { collectionCeiling } from './host-collection-queries'\nimport { useFirestore } from './firebase/firebase-services'\nimport { useFirestoreCollection } from './use-firestore-collection'\nimport { useHostOrgIdState } from './use-host-org-id'\n\n/**\n * How many campaigns a picker offers.\n *\n * The same fifty the campaigns table draws, so a campaign a merchant can see\n * in the list is a campaign they can file a record under. A picker with a\n * larger window would offer campaigns the list they came from does not show;\n * a smaller one would hide campaigns that are plainly there.\n */\nexport const HOST_CAMPAIGN_CEILING = 50\n\nconst NO_ROWS: ReadonlyArray<Record<string, unknown>> = Object.freeze([])\n\n/** One campaign, as much of it as a picker needs. */\nexport interface HostCampaignOption {\n value: string\n label: string\n /** The campaign window, for a caller that wants to say when it ran. */\n startAtMs?: number | null\n endAtMs?: number | null\n /**\n * The sites the campaign is placed on, `null` when it is on every site.\n * Read from its `visibleTo`; an unscoped campaign is on no site (`[]`).\n */\n siteIds?: string[] | null\n}\n\nexport interface HostCampaigns {\n options: HostCampaignOption[]\n /** The org holds more matching campaigns than the ceiling offers. */\n truncated: boolean\n /** The read has answered — false while it is still settling or disabled. */\n ready: boolean\n}\n\n/**\n * The picker's shape, from a ceilinged read of campaign containers.\n *\n * One mapping for both hooks below, so a campaign reads the same on a site's\n * picker and on the organization's.\n */\nexport function hostCampaignOptions(\n data: ReadonlyArray<Record<string, unknown>> | null | undefined,\n ready: boolean,\n): HostCampaigns {\n const rows = data ?? []\n const truncated = rows.length > HOST_CAMPAIGN_CEILING\n const live = rows\n .slice(0, HOST_CAMPAIGN_CEILING)\n // A campaign the console soft-deleted is not a campaign a record may be\n // filed under. The campaigns table filters the same field for the same\n // reason.\n .filter((row) => !row['deletedAt'])\n return {\n options: live\n .map((row) => {\n const visibleTo = Array.isArray(row['visibleTo'])\n ? (row['visibleTo'] as string[])\n : []\n return {\n value: String(row['$id']),\n label: String(row['name'] ?? row['$id']),\n startAtMs: (row['startAtMs'] as number | null | undefined) ?? null,\n endAtMs: (row['endAtMs'] as number | null | undefined) ?? null,\n siteIds: isOrgWideScope(visibleTo)\n ? null\n : hostIdsFromScope(visibleTo),\n }\n })\n .sort((a, b) => a.label.localeCompare(b.label)),\n truncated,\n ready,\n }\n}\n\n/**\n * THE SITE'S CAMPAIGNS, for a picker on some other record's page.\n *\n * Lives here rather than in the marketing plugin because the console app may\n * not import a feature plugin — a screen's detail page is an app route — and\n * the form and contact surfaces that need the same list are two other\n * libraries again. One read, one shape, three callers.\n *\n * ## The org's campaigns, as this site sees them\n *\n * Campaign containers belong to the organization\n * (`orgs/{orgId}/emailCampaigns`), and each is placed on some of its sites\n * by its `visibleTo`. A site's picker offers the ones placed on it: `'org'`\n * (every site) or `host:{hostId}`, which is `scopeTokensForHost`. The clause\n * is also what makes the read provable for a collaborator scoped to this\n * site — the rules evaluate `visibleTo` per document on a list, and an\n * unfiltered query is refused whole rather than narrowed.\n *\n * The org comes from the `hostIndex` lookup. Nothing is read until it\n * answers, and a host with no org settles as ready with no campaigns.\n *\n * ## It is OFF unless a caller asks\n *\n * `enabled` defaults to false. The picker this feeds sits on a record's own\n * page beside fields a reader came for; charging every open of a form, a\n * screen or a contact for a hundred campaign documents they may not touch is\n * the read-on-mount this console refuses. The callers turn it on when their\n * editing surface opens.\n *\n * ## Ordered on the document name\n *\n * `collectionCeiling`, for the reason it exists: `startAtMs` and `endAtMs`\n * are both optional on a campaign — an open-ended one carries neither — so\n * ordering on a date would not mis-sort the picker, it would DROP the\n * campaigns that have no dates. The labels are sorted by name here, over the\n * whole ceiling rather than a slice of it, which is the one case sorting a\n * window is honest. It is also the one ordering the automatic single-field\n * index on `visibleTo` serves beside `array-contains-any`, so the query\n * needs no composite index.\n */\nexport function useHostCampaigns(\n hostId: string | undefined,\n options?: { enabled?: boolean },\n): HostCampaigns {\n const enabled = options?.enabled ?? false\n const firestore = useFirestore()\n const org = useHostOrgIdState(enabled ? hostId : undefined)\n const orgId = org.orgId\n // Memoised: a listener dependency, and a fresh array each render would\n // reopen the subscription every time.\n const tokens = useMemo(\n () => (hostId ? scopeTokensForHost(hostId) : null),\n [hostId],\n )\n const { data, status } = useFirestoreCollection<Record<string, unknown>>(\n () =>\n enabled && hostId && orgId && tokens\n ? collectionCeiling(\n query(\n collection(firestore, 'orgs', orgId, 'emailCampaigns'),\n where('visibleTo', 'array-contains-any', tokens),\n ),\n HOST_CAMPAIGN_CEILING,\n )\n : null,\n [firestore, hostId, orgId, tokens, enabled],\n { idField: '$id' },\n )\n\n const ready =\n enabled &&\n Boolean(hostId) &&\n org.loaded &&\n (orgId ? status !== 'loading' : true)\n return useMemo(\n () => hostCampaignOptions(orgId ? data : NO_ROWS, ready),\n [data, orgId, ready],\n )\n}\n\n/**\n * THE ORGANIZATION'S CAMPAIGNS, for a picker on an org-level record — a\n * lead on the org's Leads page, an outbound sequence.\n *\n * Every live container, whichever sites it is placed on: the records these\n * pickers file belong to the organization, and so does the campaign. The\n * read is unfiltered because the surfaces that call it are the org-level\n * hubs, whose readers are org-wide members — the rules admit them to every\n * container, and a `visibleTo` clause would only need the org's whole site\n * list and still miss a site it did not carry. Each option carries its\n * `siteIds`, for a caller that wants to say where a campaign runs.\n *\n * Off unless a caller asks, and ceilinged and ordered, for the reasons\n * {@link useHostCampaigns} gives.\n */\nexport function useOrgCampaigns(\n orgId: string | null | undefined,\n options?: { enabled?: boolean },\n): HostCampaigns {\n const enabled = options?.enabled ?? false\n const firestore = useFirestore()\n const { data, status } = useFirestoreCollection<Record<string, unknown>>(\n () =>\n enabled && orgId\n ? collectionCeiling(\n collection(firestore, 'orgs', orgId, 'emailCampaigns'),\n HOST_CAMPAIGN_CEILING,\n )\n : null,\n [firestore, orgId, enabled],\n { idField: '$id' },\n )\n\n const ready = enabled && Boolean(orgId) && status !== 'loading'\n return useMemo(() => hostCampaignOptions(data, ready), [data, ready])\n}\n\nexport default useHostCampaigns\n"],"names":["hostIdsFromScope","isOrgWideScope","scopeTokensForHost","collection","query","where","useMemo","collectionCeiling","useFirestore","useFirestoreCollection","useHostOrgIdState","HOST_CAMPAIGN_CEILING","NO_ROWS","Object","freeze","hostCampaignOptions","data","ready","rows","truncated","length","live","slice","filter","row","options","map","visibleTo","Array","isArray","value","String","label","startAtMs","endAtMs","siteIds","sort","a","b","localeCompare","useHostCampaigns","hostId","enabled","firestore","org","undefined","orgId","tokens","status","idField","Boolean","loaded","useOrgCampaigns"],"mappings":"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"}
|