@aglyn/tenant-feature-instance 1.0.0-beta.219 → 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 CHANGED
@@ -49,8 +49,12 @@ Typed hooks over the platform model in `@aglyn/aglyn`:
49
49
  `...Ref` counterparts (`useHostRef`, `useScreenRef` and so on).
50
50
  - `usePluginConfig` and `useSitePluginConfig` for a plugin's stored
51
51
  configuration.
52
- - `useOrgPlan`, the host organization id, organization member options, scope
53
- tokens and host campaigns hooks.
52
+ - `useOrgPlan`, the host organization id, organization member options and
53
+ scope tokens hooks.
54
+ - `useSiteContainerOptions` and `useOrgContainerOptions`, the containers of a
55
+ kind a plugin declares (a campaign) that a record may be filed under, and
56
+ `ContainerPicker` (`components/container-picker`, by subpath), the control
57
+ every filing surface draws them in.
54
58
  - `useHostResourceApi`, `useHostVersionApi` and the duplicate-resource hook
55
59
  and dialog (`DuplicateResourceDialog`) call the console's resource APIs.
56
60
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aglyn/tenant-feature-instance",
3
- "version": "1.0.0-beta.219",
3
+ "version": "1.0.0-beta.220",
4
4
  "license": "Apache-2.0",
5
5
  "homepage": "https://aglyn.com",
6
6
  "repository": {
@@ -25,12 +25,12 @@
25
25
  "./package.json": "./package.json"
26
26
  },
27
27
  "dependencies": {
28
- "@aglyn/aglyn": "1.0.0-beta.219",
29
- "@aglyn/shared-data-enums": "1.0.0-beta.219",
30
- "@aglyn/shared-ui-jsx": "1.0.0-beta.219",
31
- "@aglyn/shared-util-email": "1.0.0-beta.219",
32
- "@aglyn/shared-util-http": "1.0.0-beta.219",
33
- "@aglyn/shared-util-timestamp": "1.0.0-beta.219",
28
+ "@aglyn/aglyn": "1.0.0-beta.220",
29
+ "@aglyn/shared-data-enums": "1.0.0-beta.220",
30
+ "@aglyn/shared-ui-jsx": "1.0.0-beta.220",
31
+ "@aglyn/shared-util-email": "1.0.0-beta.220",
32
+ "@aglyn/shared-util-http": "1.0.0-beta.220",
33
+ "@aglyn/shared-util-timestamp": "1.0.0-beta.220",
34
34
  "@swc/helpers": "0.5.23",
35
35
  "lodash-es": "^4.18.1"
36
36
  },
@@ -0,0 +1,70 @@
1
+ /** One container a record may be filed under. */
2
+ export interface ContainerPickerOption {
3
+ value: string;
4
+ label: string;
5
+ }
6
+ export interface ContainerPickerProps {
7
+ /**
8
+ * The kind of container offered, as its owner declares it
9
+ * (`plugin-manager/plugin-containers.ts`); the default wording is its
10
+ * labels.
11
+ */
12
+ kind: string;
13
+ /** The containers this record may be filed under, in the order to offer them. */
14
+ options: readonly ContainerPickerOption[];
15
+ /** The container ids currently selected. */
16
+ value: readonly string[];
17
+ onChange: (next: string[]) => void;
18
+ /** Overrides the default label — a contact says "filed under", not "in". */
19
+ label?: string;
20
+ /** Overrides the default helper line. */
21
+ helperText?: string;
22
+ disabled?: boolean;
23
+ /**
24
+ * The site has no containers of this kind at all, so the picker has
25
+ * nothing to offer.
26
+ *
27
+ * Distinct from "still loading": an empty select with no explanation reads
28
+ * as a control that is broken, and the two states look identical.
29
+ */
30
+ empty?: boolean;
31
+ /** What to say in place of the picker when {@link empty}. */
32
+ emptyText?: string;
33
+ }
34
+ /** The words a picker uses for a kind, from its declaration. */
35
+ export declare function containerPickerWords(kind: string): {
36
+ label: string;
37
+ helperText: string;
38
+ none: string;
39
+ emptyText: string;
40
+ };
41
+ /**
42
+ * WHAT THIS RECORD IS FILED UNDER.
43
+ *
44
+ * The one control behind every filing of a record under a container — a
45
+ * form's page, a screen's page, a contact's and a lead's — so that several
46
+ * surfaces editing one stored field cannot come to present it several ways.
47
+ * The kind is another plugin's (a campaign is Marketing's), which is why the
48
+ * control is here and not in that plugin: an app route and the other plugins
49
+ * may not import it.
50
+ *
51
+ * ## It renders ids as NAMES and stores ids
52
+ *
53
+ * The chips read the label out of {@link ContainerPickerProps.options}, and
54
+ * an id with no option left falls back to the id itself rather than
55
+ * disappearing. A container that has been deleted is exactly that case, and
56
+ * a chip that vanished would tell a merchant the filing was gone when the
57
+ * document still carries it.
58
+ *
59
+ * ## Presentational, deliberately
60
+ *
61
+ * It reads no collection of its own. The callers already hold a site scope
62
+ * and their own read budget (`useSiteContainerOptions`,
63
+ * `useOrgContainerOptions`), and a control that opened a listener per
64
+ * placement is the unrequested read on mount this console refuses.
65
+ */
66
+ export declare function ContainerPicker(props: ContainerPickerProps): import("react").JSX.Element;
67
+ export declare namespace ContainerPicker {
68
+ var displayName: string;
69
+ }
70
+ export default ContainerPicker;
@@ -0,0 +1,140 @@
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 { jsx as _jsx } from "react/jsx-runtime";
18
+ import { pluginContainerKind } from "@aglyn/aglyn/plugin-manager/plugin-containers";
19
+ import { Chip, MenuItem, Stack, TextField, Typography } from "@mui/material";
20
+ /** The words a picker uses for a kind, from its declaration. */ export function containerPickerWords(kind) {
21
+ var _ref, _ref1;
22
+ const declared = pluginContainerKind(kind);
23
+ const one = ((_ref = declared == null ? void 0 : declared.label) != null ? _ref : kind).trim();
24
+ const many = ((_ref1 = declared == null ? void 0 : declared.pluralLabel) != null ? _ref1 : one).trim();
25
+ const lowerOne = one.toLowerCase();
26
+ const lowerMany = many.toLowerCase();
27
+ return {
28
+ label: many,
29
+ helperText: `The ${lowerMany} this belongs to. Clearing them all takes it out of every ${lowerOne}.`,
30
+ none: `No ${lowerOne}`,
31
+ emptyText: declared ? `This site has no ${lowerMany} yet. Create one from ${declared.ownerLabel} to file records under it.` : `This site has no ${lowerMany}.`
32
+ };
33
+ }
34
+ /**
35
+ * WHAT THIS RECORD IS FILED UNDER.
36
+ *
37
+ * The one control behind every filing of a record under a container — a
38
+ * form's page, a screen's page, a contact's and a lead's — so that several
39
+ * surfaces editing one stored field cannot come to present it several ways.
40
+ * The kind is another plugin's (a campaign is Marketing's), which is why the
41
+ * control is here and not in that plugin: an app route and the other plugins
42
+ * may not import it.
43
+ *
44
+ * ## It renders ids as NAMES and stores ids
45
+ *
46
+ * The chips read the label out of {@link ContainerPickerProps.options}, and
47
+ * an id with no option left falls back to the id itself rather than
48
+ * disappearing. A container that has been deleted is exactly that case, and
49
+ * a chip that vanished would tell a merchant the filing was gone when the
50
+ * document still carries it.
51
+ *
52
+ * ## Presentational, deliberately
53
+ *
54
+ * It reads no collection of its own. The callers already hold a site scope
55
+ * and their own read budget (`useSiteContainerOptions`,
56
+ * `useOrgContainerOptions`), and a control that opened a listener per
57
+ * placement is the unrequested read on mount this console refuses.
58
+ */ export function ContainerPicker(props) {
59
+ const words = containerPickerWords(props.kind);
60
+ const { options, value, onChange, label = words.label, helperText = words.helperText, disabled, empty, emptyText = words.emptyText } = props;
61
+ if (empty) {
62
+ return /*#__PURE__*/ _jsx(Typography, {
63
+ variant: "body2",
64
+ color: "text.secondary",
65
+ children: emptyText
66
+ });
67
+ }
68
+ const labels = new Map(options.map((option)=>[
69
+ option.value,
70
+ option.label
71
+ ]));
72
+ return /*#__PURE__*/ _jsx(TextField, {
73
+ select: true,
74
+ size: "small",
75
+ label: label,
76
+ value: value,
77
+ disabled: disabled,
78
+ onChange: (event)=>onChange(// A multiple select hands back an array; the change event's typing
79
+ // does not know that.
80
+ event.target.value.map(String)),
81
+ helperText: helperText,
82
+ fullWidth: true,
83
+ slotProps: {
84
+ /*
85
+ * `shrink`, because `displayEmpty` below draws the "none" placeholder
86
+ * in the closed box while the value is still empty (AGL-2486's
87
+ * pairing).
88
+ *
89
+ * MUI shrinks a label when the input reports itself filled, and an
90
+ * empty multiple select reports the opposite — so the label sat at
91
+ * full size, inside the box, on top of the placeholder it had just
92
+ * drawn: two lines of text one on the other, on every page that
93
+ * renders this picker. Both halves have to be said together;
94
+ * `displayEmpty` alone IS the overlap.
95
+ *
96
+ * Nothing sets `notched` here on purpose: `TextField` derives it from
97
+ * this `shrink` for the outlined variant, so the outline's gap and the
98
+ * label can never disagree.
99
+ */ inputLabel: {
100
+ shrink: true
101
+ },
102
+ select: {
103
+ multiple: true,
104
+ displayEmpty: true,
105
+ renderValue: (selected)=>{
106
+ const ids = selected != null ? selected : [];
107
+ if (!ids.length) {
108
+ return /*#__PURE__*/ _jsx(Typography, {
109
+ variant: "body2",
110
+ color: "text.secondary",
111
+ children: words.none
112
+ });
113
+ }
114
+ return /*#__PURE__*/ _jsx(Stack, {
115
+ direction: "row",
116
+ spacing: 0.5,
117
+ sx: {
118
+ flexWrap: 'wrap'
119
+ },
120
+ children: ids.map((id)=>{
121
+ var _labels_get;
122
+ return /*#__PURE__*/ _jsx(Chip, {
123
+ size: "small",
124
+ label: (_labels_get = labels.get(id)) != null ? _labels_get : id
125
+ }, id);
126
+ })
127
+ });
128
+ }
129
+ }
130
+ },
131
+ children: options.map((option)=>/*#__PURE__*/ _jsx(MenuItem, {
132
+ value: option.value,
133
+ children: option.label
134
+ }, option.value))
135
+ });
136
+ }
137
+ ContainerPicker.displayName = 'ContainerPicker';
138
+ export default ContainerPicker;
139
+
140
+ //# sourceMappingURL=container-picker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../../libs/tenant/feature/instance/src/lib/components/container-picker.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'use client'\n\nimport { pluginContainerKind } from '@aglyn/aglyn/plugin-manager/plugin-containers'\nimport { Chip, MenuItem, Stack, TextField, Typography } from '@mui/material'\n\n/** One container a record may be filed under. */\nexport interface ContainerPickerOption {\n value: string\n label: string\n}\n\nexport interface ContainerPickerProps {\n /**\n * The kind of container offered, as its owner declares it\n * (`plugin-manager/plugin-containers.ts`); the default wording is its\n * labels.\n */\n kind: string\n /** The containers this record may be filed under, in the order to offer them. */\n options: readonly ContainerPickerOption[]\n /** The container ids currently selected. */\n value: readonly string[]\n onChange: (next: string[]) => void\n /** Overrides the default label — a contact says \"filed under\", not \"in\". */\n label?: string\n /** Overrides the default helper line. */\n helperText?: string\n disabled?: boolean\n /**\n * The site has no containers of this kind at all, so the picker has\n * nothing to offer.\n *\n * Distinct from \"still loading\": an empty select with no explanation reads\n * as a control that is broken, and the two states look identical.\n */\n empty?: boolean\n /** What to say in place of the picker when {@link empty}. */\n emptyText?: string\n}\n\n/** The words a picker uses for a kind, from its declaration. */\nexport function containerPickerWords(kind: string): {\n label: string\n helperText: string\n none: string\n emptyText: string\n} {\n const declared = pluginContainerKind(kind)\n const one = (declared?.label ?? kind).trim()\n const many = (declared?.pluralLabel ?? one).trim()\n const lowerOne = one.toLowerCase()\n const lowerMany = many.toLowerCase()\n return {\n label: many,\n helperText: `The ${lowerMany} this belongs to. Clearing them all takes it out of every ${lowerOne}.`,\n none: `No ${lowerOne}`,\n emptyText: declared\n ? `This site has no ${lowerMany} yet. Create one from ${declared.ownerLabel} to file records under it.`\n : `This site has no ${lowerMany}.`,\n }\n}\n\n/**\n * WHAT THIS RECORD IS FILED UNDER.\n *\n * The one control behind every filing of a record under a container — a\n * form's page, a screen's page, a contact's and a lead's — so that several\n * surfaces editing one stored field cannot come to present it several ways.\n * The kind is another plugin's (a campaign is Marketing's), which is why the\n * control is here and not in that plugin: an app route and the other plugins\n * may not import it.\n *\n * ## It renders ids as NAMES and stores ids\n *\n * The chips read the label out of {@link ContainerPickerProps.options}, and\n * an id with no option left falls back to the id itself rather than\n * disappearing. A container that has been deleted is exactly that case, and\n * a chip that vanished would tell a merchant the filing was gone when the\n * document still carries it.\n *\n * ## Presentational, deliberately\n *\n * It reads no collection of its own. The callers already hold a site scope\n * and their own read budget (`useSiteContainerOptions`,\n * `useOrgContainerOptions`), and a control that opened a listener per\n * placement is the unrequested read on mount this console refuses.\n */\nexport function ContainerPicker(props: ContainerPickerProps) {\n const words = containerPickerWords(props.kind)\n const {\n options,\n value,\n onChange,\n label = words.label,\n helperText = words.helperText,\n disabled,\n empty,\n emptyText = words.emptyText,\n } = props\n\n if (empty) {\n return (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {emptyText}\n </Typography>\n )\n }\n\n const labels = new Map(options.map((option) => [option.value, option.label]))\n\n return (\n <TextField\n select\n size=\"small\"\n label={label}\n value={value as string[]}\n disabled={disabled}\n onChange={(event) =>\n onChange(\n // A multiple select hands back an array; the change event's typing\n // does not know that.\n (event.target.value as unknown as string[]).map(String),\n )\n }\n helperText={helperText}\n fullWidth\n slotProps={{\n /*\n * `shrink`, because `displayEmpty` below draws the \"none\" placeholder\n * in the closed box while the value is still empty (AGL-2486's\n * pairing).\n *\n * MUI shrinks a label when the input reports itself filled, and an\n * empty multiple select reports the opposite — so the label sat at\n * full size, inside the box, on top of the placeholder it had just\n * drawn: two lines of text one on the other, on every page that\n * renders this picker. Both halves have to be said together;\n * `displayEmpty` alone IS the overlap.\n *\n * Nothing sets `notched` here on purpose: `TextField` derives it from\n * this `shrink` for the outlined variant, so the outline's gap and the\n * label can never disagree.\n */\n inputLabel: { shrink: true },\n select: {\n multiple: true,\n displayEmpty: true,\n renderValue: (selected: unknown) => {\n const ids = (selected as string[]) ?? []\n if (!ids.length) {\n return (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {words.none}\n </Typography>\n )\n }\n return (\n <Stack direction=\"row\" spacing={0.5} sx={{ flexWrap: 'wrap' }}>\n {ids.map((id) => (\n <Chip key={id} size=\"small\" label={labels.get(id) ?? id} />\n ))}\n </Stack>\n )\n },\n },\n }}\n >\n {options.map((option) => (\n <MenuItem key={option.value} value={option.value}>\n {option.label}\n </MenuItem>\n ))}\n </TextField>\n )\n}\nContainerPicker.displayName = 'ContainerPicker'\n\nexport default ContainerPicker\n"],"names":["pluginContainerKind","Chip","MenuItem","Stack","TextField","Typography","containerPickerWords","kind","declared","one","label","trim","many","pluralLabel","lowerOne","toLowerCase","lowerMany","helperText","none","emptyText","ownerLabel","ContainerPicker","props","words","options","value","onChange","disabled","empty","variant","color","labels","Map","map","option","select","size","event","target","String","fullWidth","slotProps","inputLabel","shrink","multiple","displayEmpty","renderValue","selected","ids","length","direction","spacing","sx","flexWrap","id","get","displayName"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;AAEA,SAASA,mBAAmB,QAAQ,gDAA+C;AACnF,SAASC,IAAI,EAAEC,QAAQ,EAAEC,KAAK,EAAEC,SAAS,EAAEC,UAAU,QAAQ,gBAAe;AAqC5E,8DAA8D,GAC9D,OAAO,SAASC,qBAAqBC,IAAY;;IAM/C,MAAMC,WAAWR,oBAAoBO;IACrC,MAAME,MAAM,SAACD,4BAAAA,SAAUE,KAAK,mBAAIH,MAAMI,IAAI;IAC1C,MAAMC,OAAO,UAACJ,4BAAAA,SAAUK,WAAW,oBAAIJ,KAAKE,IAAI;IAChD,MAAMG,WAAWL,IAAIM,WAAW;IAChC,MAAMC,YAAYJ,KAAKG,WAAW;IAClC,OAAO;QACLL,OAAOE;QACPK,YAAY,CAAC,IAAI,EAAED,UAAU,0DAA0D,EAAEF,SAAS,CAAC,CAAC;QACpGI,MAAM,CAAC,GAAG,EAAEJ,UAAU;QACtBK,WAAWX,WACP,CAAC,iBAAiB,EAAEQ,UAAU,sBAAsB,EAAER,SAASY,UAAU,CAAC,0BAA0B,CAAC,GACrG,CAAC,iBAAiB,EAAEJ,UAAU,CAAC,CAAC;IACtC;AACF;AAEA;;;;;;;;;;;;;;;;;;;;;;;;CAwBC,GACD,OAAO,SAASK,gBAAgBC,KAA2B;IACzD,MAAMC,QAAQjB,qBAAqBgB,MAAMf,IAAI;IAC7C,MAAM,EACJiB,OAAO,EACPC,KAAK,EACLC,QAAQ,EACRhB,QAAQa,MAAMb,KAAK,EACnBO,aAAaM,MAAMN,UAAU,EAC7BU,QAAQ,EACRC,KAAK,EACLT,YAAYI,MAAMJ,SAAS,EAC5B,GAAGG;IAEJ,IAAIM,OAAO;QACT,qBACE,KAACvB;YAAWwB,SAAQ;YAAQC,OAAM;sBAC/BX;;IAGP;IAEA,MAAMY,SAAS,IAAIC,IAAIR,QAAQS,GAAG,CAAC,CAACC,SAAW;YAACA,OAAOT,KAAK;YAAES,OAAOxB,KAAK;SAAC;IAE3E,qBACE,KAACN;QACC+B,MAAM;QACNC,MAAK;QACL1B,OAAOA;QACPe,OAAOA;QACPE,UAAUA;QACVD,UAAU,CAACW,QACTX,SAGE,AAFA,mEAAmE;YACnE,sBAAsB;YACrBW,MAAMC,MAAM,CAACb,KAAK,CAAyBQ,GAAG,CAACM;QAGpDtB,YAAYA;QACZuB,SAAS;QACTC,WAAW;YACT;;;;;;;;;;;;;;;SAeC,GACDC,YAAY;gBAAEC,QAAQ;YAAK;YAC3BR,QAAQ;gBACNS,UAAU;gBACVC,cAAc;gBACdC,aAAa,CAACC;oBACZ,MAAMC,MAAOD,mBAAAA,WAAyB,EAAE;oBACxC,IAAI,CAACC,IAAIC,MAAM,EAAE;wBACf,qBACE,KAAC5C;4BAAWwB,SAAQ;4BAAQC,OAAM;sCAC/BP,MAAML,IAAI;;oBAGjB;oBACA,qBACE,KAACf;wBAAM+C,WAAU;wBAAMC,SAAS;wBAAKC,IAAI;4BAAEC,UAAU;wBAAO;kCACzDL,IAAIf,GAAG,CAAC,CAACqB;gCAC2BvB;iDAAnC,KAAC9B;gCAAcmC,MAAK;gCAAQ1B,KAAK,GAAEqB,cAAAA,OAAOwB,GAAG,CAACD,eAAXvB,cAAkBuB;+BAA1CA;;;gBAInB;YACF;QACF;kBAEC9B,QAAQS,GAAG,CAAC,CAACC,uBACZ,KAAChC;gBAA4BuB,OAAOS,OAAOT,KAAK;0BAC7CS,OAAOxB,KAAK;eADAwB,OAAOT,KAAK;;AAMnC;AACAJ,gBAAgBmC,WAAW,GAAG;AAE9B,eAAenC,gBAAe"}
@@ -0,0 +1,98 @@
1
+ import { type PluginContainerKind } from '@aglyn/aglyn/plugin-manager/plugin-containers';
2
+ /**
3
+ * How many containers a picker offers.
4
+ *
5
+ * The same fifty an owner's container table draws, so a container a merchant
6
+ * can see in its list is one they can file a record under. A picker with a
7
+ * larger window would offer containers the list they came from does not
8
+ * show; a smaller one would hide containers that are plainly there.
9
+ */
10
+ export declare const CONTAINER_OPTION_CEILING = 50;
11
+ /** One container, as much of it as a picker needs. */
12
+ export interface ContainerOption {
13
+ value: string;
14
+ label: string;
15
+ /**
16
+ * The sites the container is placed on, `null` when it is on every site.
17
+ * Read from its `visibleTo`; an unscoped container is on no site (`[]`).
18
+ */
19
+ siteIds?: string[] | null;
20
+ }
21
+ export interface ContainerOptions {
22
+ options: ContainerOption[];
23
+ /** The org holds more matching containers than the ceiling offers. */
24
+ truncated: boolean;
25
+ /** The read has answered — false while it is still settling or disabled. */
26
+ ready: boolean;
27
+ }
28
+ /**
29
+ * The picker's shape, from a ceilinged read of one kind's containers.
30
+ *
31
+ * One mapping for both hooks below, so a container reads the same on a
32
+ * site's picker and on the organization's. The name is read from the field
33
+ * the owner declares, falling back to the id.
34
+ */
35
+ export declare function containerOptionsFrom(data: ReadonlyArray<Record<string, unknown>> | null | undefined, declared: Pick<PluginContainerKind, 'nameField'> | null, ready: boolean): ContainerOptions;
36
+ /**
37
+ * THE CONTAINERS OF ONE KIND A SITE OFFERS, for a picker on some other
38
+ * record's page.
39
+ *
40
+ * Lives here rather than in the plugin that keeps the kind because the
41
+ * console app may not import a feature plugin — a screen's detail page is an
42
+ * app route — and the form, contact and lead surfaces that file records are
43
+ * other plugins again. The kind is declared by its owner
44
+ * (`plugin-manager/plugin-containers.ts`); this reads where the declaration
45
+ * says, and a kind no plugin keeps settles as ready with nothing to offer.
46
+ *
47
+ * ## The org's containers, as this site sees them
48
+ *
49
+ * Containers belong to the organization (`orgs/{orgId}/{orgCollection}`),
50
+ * and each is placed on some of its sites by its `visibleTo`. A site's
51
+ * picker offers the ones placed on it: `'org'` (every site) or
52
+ * `host:{hostId}`, which is `scopeTokensForHost`. The clause is also what
53
+ * makes the read provable for a collaborator scoped to this site — the rules
54
+ * evaluate `visibleTo` per document on a list, and an unfiltered query is
55
+ * refused whole rather than narrowed.
56
+ *
57
+ * The org comes from the `hostIndex` lookup. Nothing is read until it
58
+ * answers, and a host with no org settles as ready with nothing.
59
+ *
60
+ * ## It is OFF unless a caller asks
61
+ *
62
+ * `enabled` defaults to false. The picker this feeds sits on a record's own
63
+ * page beside fields a reader came for; charging every open of a form, a
64
+ * screen or a contact for fifty container documents they may not touch is
65
+ * the read-on-mount this console refuses. The callers turn it on when their
66
+ * editing surface opens.
67
+ *
68
+ * ## Ordered on the document name
69
+ *
70
+ * `collectionCeiling`, for the reason it exists: a container's own dates are
71
+ * optional, so ordering on one would not mis-sort the picker, it would DROP
72
+ * the containers that have none. The labels are sorted by name here, over the
73
+ * whole ceiling rather than a slice of it, which is the one case sorting a
74
+ * window is honest. It is also the one ordering the automatic single-field
75
+ * index on `visibleTo` serves beside `array-contains-any`, so the query
76
+ * needs no composite index.
77
+ */
78
+ export declare function useSiteContainerOptions(kind: string, hostId: string | undefined, options?: {
79
+ enabled?: boolean;
80
+ }): ContainerOptions;
81
+ /**
82
+ * THE ORGANIZATION'S CONTAINERS OF ONE KIND, for a picker on an org-level
83
+ * record — a lead on the org's Leads page, an outbound sequence.
84
+ *
85
+ * Every live container, whichever sites it is placed on: the records these
86
+ * pickers file belong to the organization, and so does the container. The
87
+ * read is unfiltered because the surfaces that call it are the org-level
88
+ * hubs, whose readers are org-wide members — the rules admit them to every
89
+ * container, and a `visibleTo` clause would only need the org's whole site
90
+ * list and still miss a site it did not carry. Each option carries its
91
+ * `siteIds`, for a caller that wants to say where a container runs.
92
+ *
93
+ * Off unless a caller asks, and ceilinged and ordered, for the reasons
94
+ * {@link useSiteContainerOptions} gives.
95
+ */
96
+ export declare function useOrgContainerOptions(kind: string, orgId: string | null | undefined, options?: {
97
+ enabled?: boolean;
98
+ }): ContainerOptions;
@@ -0,0 +1,174 @@
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 { pluginContainerKind } from "@aglyn/aglyn/plugin-manager/plugin-containers";
19
+ import { collection, query, where } from "firebase/firestore";
20
+ import { useMemo } from "react";
21
+ import { collectionCeiling } from "./host-collection-queries.js";
22
+ import { useFirestore } from "./firebase/firebase-services.js";
23
+ import { useFirestoreCollection } from "./use-firestore-collection.js";
24
+ import { useHostOrgIdState } from "./use-host-org-id.js";
25
+ /**
26
+ * How many containers a picker offers.
27
+ *
28
+ * The same fifty an owner's container table draws, so a container a merchant
29
+ * can see in its list is one they can file a record under. A picker with a
30
+ * larger window would offer containers the list they came from does not
31
+ * show; a smaller one would hide containers that are plainly there.
32
+ */ export const CONTAINER_OPTION_CEILING = 50;
33
+ const NO_ROWS = Object.freeze([]);
34
+ /**
35
+ * The picker's shape, from a ceilinged read of one kind's containers.
36
+ *
37
+ * One mapping for both hooks below, so a container reads the same on a
38
+ * site's picker and on the organization's. The name is read from the field
39
+ * the owner declares, falling back to the id.
40
+ */ export function containerOptionsFrom(data, declared, ready) {
41
+ var _ref;
42
+ const rows = declared ? data != null ? data : [] : [];
43
+ const truncated = rows.length > CONTAINER_OPTION_CEILING;
44
+ const nameField = (_ref = declared == null ? void 0 : declared.nameField) != null ? _ref : '';
45
+ const live = rows.slice(0, CONTAINER_OPTION_CEILING)// A container its owner retired is not one a record may be filed under.
46
+ // The owner's own table filters the same field for the same reason.
47
+ .filter((row)=>!row['deletedAt']);
48
+ return {
49
+ options: live.map((row)=>{
50
+ var _row_nameField;
51
+ const visibleTo = Array.isArray(row['visibleTo']) ? row['visibleTo'] : [];
52
+ return {
53
+ value: String(row['$id']),
54
+ label: String((_row_nameField = row[nameField]) != null ? _row_nameField : row['$id']),
55
+ siteIds: isOrgWideScope(visibleTo) ? null : hostIdsFromScope(visibleTo)
56
+ };
57
+ }).sort((a, b)=>a.label.localeCompare(b.label)),
58
+ truncated,
59
+ ready
60
+ };
61
+ }
62
+ /**
63
+ * THE CONTAINERS OF ONE KIND A SITE OFFERS, for a picker on some other
64
+ * record's page.
65
+ *
66
+ * Lives here rather than in the plugin that keeps the kind because the
67
+ * console app may not import a feature plugin — a screen's detail page is an
68
+ * app route — and the form, contact and lead surfaces that file records are
69
+ * other plugins again. The kind is declared by its owner
70
+ * (`plugin-manager/plugin-containers.ts`); this reads where the declaration
71
+ * says, and a kind no plugin keeps settles as ready with nothing to offer.
72
+ *
73
+ * ## The org's containers, as this site sees them
74
+ *
75
+ * Containers belong to the organization (`orgs/{orgId}/{orgCollection}`),
76
+ * and each is placed on some of its sites by its `visibleTo`. A site's
77
+ * picker offers the ones placed on it: `'org'` (every site) or
78
+ * `host:{hostId}`, which is `scopeTokensForHost`. The clause is also what
79
+ * makes the read provable for a collaborator scoped to this site — the rules
80
+ * evaluate `visibleTo` per document on a list, and an unfiltered query is
81
+ * refused whole rather than narrowed.
82
+ *
83
+ * The org comes from the `hostIndex` lookup. Nothing is read until it
84
+ * answers, and a host with no org settles as ready with nothing.
85
+ *
86
+ * ## It is OFF unless a caller asks
87
+ *
88
+ * `enabled` defaults to false. The picker this feeds sits on a record's own
89
+ * page beside fields a reader came for; charging every open of a form, a
90
+ * screen or a contact for fifty container documents they may not touch is
91
+ * the read-on-mount this console refuses. The callers turn it on when their
92
+ * editing surface opens.
93
+ *
94
+ * ## Ordered on the document name
95
+ *
96
+ * `collectionCeiling`, for the reason it exists: a container's own dates are
97
+ * optional, so ordering on one would not mis-sort the picker, it would DROP
98
+ * the containers that have none. The labels are sorted by name here, over the
99
+ * whole ceiling rather than a slice of it, which is the one case sorting a
100
+ * window is honest. It is also the one ordering the automatic single-field
101
+ * index on `visibleTo` serves beside `array-contains-any`, so the query
102
+ * needs no composite index.
103
+ */ export function useSiteContainerOptions(kind, hostId, options) {
104
+ var _ref, _ref1;
105
+ const declared = useMemo(()=>pluginContainerKind(kind), [
106
+ kind
107
+ ]);
108
+ const enabled = (_ref = options == null ? void 0 : options.enabled) != null ? _ref : false;
109
+ const firestore = useFirestore();
110
+ const org = useHostOrgIdState(enabled && declared ? hostId : undefined);
111
+ const orgId = org.orgId;
112
+ const collectionName = (_ref1 = declared == null ? void 0 : declared.orgCollection) != null ? _ref1 : '';
113
+ // Memoised: a listener dependency, and a fresh array each render would
114
+ // reopen the subscription every time.
115
+ const tokens = useMemo(()=>hostId ? scopeTokensForHost(hostId) : null, [
116
+ hostId
117
+ ]);
118
+ const { data, status } = useFirestoreCollection(()=>enabled && collectionName && hostId && orgId && tokens ? collectionCeiling(query(collection(firestore, 'orgs', orgId, collectionName), where('visibleTo', 'array-contains-any', tokens)), CONTAINER_OPTION_CEILING) : null, [
119
+ firestore,
120
+ hostId,
121
+ orgId,
122
+ tokens,
123
+ enabled,
124
+ collectionName
125
+ ], {
126
+ idField: '$id'
127
+ });
128
+ const ready = enabled && Boolean(hostId) && (!declared || org.loaded && (orgId ? status !== 'loading' : true));
129
+ return useMemo(()=>containerOptionsFrom(orgId ? data : NO_ROWS, declared, ready), [
130
+ data,
131
+ orgId,
132
+ declared,
133
+ ready
134
+ ]);
135
+ }
136
+ /**
137
+ * THE ORGANIZATION'S CONTAINERS OF ONE KIND, for a picker on an org-level
138
+ * record — a lead on the org's Leads page, an outbound sequence.
139
+ *
140
+ * Every live container, whichever sites it is placed on: the records these
141
+ * pickers file belong to the organization, and so does the container. The
142
+ * read is unfiltered because the surfaces that call it are the org-level
143
+ * hubs, whose readers are org-wide members — the rules admit them to every
144
+ * container, and a `visibleTo` clause would only need the org's whole site
145
+ * list and still miss a site it did not carry. Each option carries its
146
+ * `siteIds`, for a caller that wants to say where a container runs.
147
+ *
148
+ * Off unless a caller asks, and ceilinged and ordered, for the reasons
149
+ * {@link useSiteContainerOptions} gives.
150
+ */ export function useOrgContainerOptions(kind, orgId, options) {
151
+ var _ref, _ref1;
152
+ const declared = useMemo(()=>pluginContainerKind(kind), [
153
+ kind
154
+ ]);
155
+ const enabled = (_ref = options == null ? void 0 : options.enabled) != null ? _ref : false;
156
+ const firestore = useFirestore();
157
+ const collectionName = (_ref1 = declared == null ? void 0 : declared.orgCollection) != null ? _ref1 : '';
158
+ const { data, status } = useFirestoreCollection(()=>enabled && collectionName && orgId ? collectionCeiling(collection(firestore, 'orgs', orgId, collectionName), CONTAINER_OPTION_CEILING) : null, [
159
+ firestore,
160
+ orgId,
161
+ enabled,
162
+ collectionName
163
+ ], {
164
+ idField: '$id'
165
+ });
166
+ const ready = enabled && Boolean(orgId) && (!declared || status !== 'loading');
167
+ return useMemo(()=>containerOptionsFrom(data, declared, ready), [
168
+ data,
169
+ declared,
170
+ ready
171
+ ]);
172
+ }
173
+
174
+ //# sourceMappingURL=use-container-options.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../../libs/tenant/feature/instance/src/lib/hooks/use-container-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 {\n hostIdsFromScope,\n isOrgWideScope,\n scopeTokensForHost,\n} from '@aglyn/aglyn'\nimport {\n pluginContainerKind,\n type PluginContainerKind,\n} from '@aglyn/aglyn/plugin-manager/plugin-containers'\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 containers a picker offers.\n *\n * The same fifty an owner's container table draws, so a container a merchant\n * can see in its list is one they can file a record under. A picker with a\n * larger window would offer containers the list they came from does not\n * show; a smaller one would hide containers that are plainly there.\n */\nexport const CONTAINER_OPTION_CEILING = 50\n\nconst NO_ROWS: ReadonlyArray<Record<string, unknown>> = Object.freeze([])\n\n/** One container, as much of it as a picker needs. */\nexport interface ContainerOption {\n value: string\n label: string\n /**\n * The sites the container is placed on, `null` when it is on every site.\n * Read from its `visibleTo`; an unscoped container is on no site (`[]`).\n */\n siteIds?: string[] | null\n}\n\nexport interface ContainerOptions {\n options: ContainerOption[]\n /** The org holds more matching containers 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 one kind's containers.\n *\n * One mapping for both hooks below, so a container reads the same on a\n * site's picker and on the organization's. The name is read from the field\n * the owner declares, falling back to the id.\n */\nexport function containerOptionsFrom(\n data: ReadonlyArray<Record<string, unknown>> | null | undefined,\n declared: Pick<PluginContainerKind, 'nameField'> | null,\n ready: boolean,\n): ContainerOptions {\n const rows = declared ? (data ?? []) : []\n const truncated = rows.length > CONTAINER_OPTION_CEILING\n const nameField = declared?.nameField ?? ''\n const live = rows\n .slice(0, CONTAINER_OPTION_CEILING)\n // A container its owner retired is not one a record may be filed under.\n // The owner's own table filters the same field for the same 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[nameField] ?? row['$id']),\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 CONTAINERS OF ONE KIND A SITE OFFERS, for a picker on some other\n * record's page.\n *\n * Lives here rather than in the plugin that keeps the kind because the\n * console app may not import a feature plugin — a screen's detail page is an\n * app route — and the form, contact and lead surfaces that file records are\n * other plugins again. The kind is declared by its owner\n * (`plugin-manager/plugin-containers.ts`); this reads where the declaration\n * says, and a kind no plugin keeps settles as ready with nothing to offer.\n *\n * ## The org's containers, as this site sees them\n *\n * Containers belong to the organization (`orgs/{orgId}/{orgCollection}`),\n * and each is placed on some of its sites by its `visibleTo`. A site's\n * picker offers the ones placed on it: `'org'` (every site) or\n * `host:{hostId}`, which is `scopeTokensForHost`. The clause is also what\n * makes the read provable for a collaborator scoped to this site — the rules\n * evaluate `visibleTo` per document on a list, and an unfiltered query is\n * 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 nothing.\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 fifty container 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: a container's own dates are\n * optional, so ordering on one would not mis-sort the picker, it would DROP\n * the containers that have none. 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 useSiteContainerOptions(\n kind: string,\n hostId: string | undefined,\n options?: { enabled?: boolean },\n): ContainerOptions {\n const declared = useMemo(() => pluginContainerKind(kind), [kind])\n const enabled = options?.enabled ?? false\n const firestore = useFirestore()\n const org = useHostOrgIdState(enabled && declared ? hostId : undefined)\n const orgId = org.orgId\n const collectionName = declared?.orgCollection ?? ''\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 && collectionName && hostId && orgId && tokens\n ? collectionCeiling(\n query(\n collection(firestore, 'orgs', orgId, collectionName),\n where('visibleTo', 'array-contains-any', tokens),\n ),\n CONTAINER_OPTION_CEILING,\n )\n : null,\n [firestore, hostId, orgId, tokens, enabled, collectionName],\n { idField: '$id' },\n )\n\n const ready =\n enabled &&\n Boolean(hostId) &&\n (!declared || (org.loaded && (orgId ? status !== 'loading' : true)))\n return useMemo(\n () => containerOptionsFrom(orgId ? data : NO_ROWS, declared, ready),\n [data, orgId, declared, ready],\n )\n}\n\n/**\n * THE ORGANIZATION'S CONTAINERS OF ONE KIND, for a picker on an org-level\n * record — a 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 container. 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 container runs.\n *\n * Off unless a caller asks, and ceilinged and ordered, for the reasons\n * {@link useSiteContainerOptions} gives.\n */\nexport function useOrgContainerOptions(\n kind: string,\n orgId: string | null | undefined,\n options?: { enabled?: boolean },\n): ContainerOptions {\n const declared = useMemo(() => pluginContainerKind(kind), [kind])\n const enabled = options?.enabled ?? false\n const firestore = useFirestore()\n const collectionName = declared?.orgCollection ?? ''\n const { data, status } = useFirestoreCollection<Record<string, unknown>>(\n () =>\n enabled && collectionName && orgId\n ? collectionCeiling(\n collection(firestore, 'orgs', orgId, collectionName),\n CONTAINER_OPTION_CEILING,\n )\n : null,\n [firestore, orgId, enabled, collectionName],\n { idField: '$id' },\n )\n\n const ready = enabled && Boolean(orgId) && (!declared || status !== 'loading')\n return useMemo(() => containerOptionsFrom(data, declared, ready), [data, declared, ready])\n}\n"],"names":["hostIdsFromScope","isOrgWideScope","scopeTokensForHost","pluginContainerKind","collection","query","where","useMemo","collectionCeiling","useFirestore","useFirestoreCollection","useHostOrgIdState","CONTAINER_OPTION_CEILING","NO_ROWS","Object","freeze","containerOptionsFrom","data","declared","ready","rows","truncated","length","nameField","live","slice","filter","row","options","map","visibleTo","Array","isArray","value","String","label","siteIds","sort","a","b","localeCompare","useSiteContainerOptions","kind","hostId","enabled","firestore","org","undefined","orgId","collectionName","orgCollection","tokens","status","idField","Boolean","loaded","useOrgContainerOptions"],"mappings":"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"}
@@ -38,7 +38,7 @@ export declare function orgMemberOptions(members: ReadonlyArray<Partial<AglynOrg
38
38
  *
39
39
  * ## It is OFF unless a caller asks
40
40
  *
41
- * `enabled` defaults to false, the way `useHostCampaigns` does and for the
41
+ * `enabled` defaults to false, the way `useSiteContainerOptions` does and for the
42
42
  * same reason: the picker this feeds sits on surfaces a reader opens for
43
43
  * something else, and a roster fetched on every arrival at the contacts list
44
44
  * is a request for a control nobody has opened. Callers turn it on when the
@@ -44,7 +44,7 @@ import { useUser } from "./firebase/firebase-services.js";
44
44
  *
45
45
  * ## It is OFF unless a caller asks
46
46
  *
47
- * `enabled` defaults to false, the way `useHostCampaigns` does and for the
47
+ * `enabled` defaults to false, the way `useSiteContainerOptions` does and for the
48
48
  * same reason: the picker this feeds sits on surfaces a reader opens for
49
49
  * something else, and a roster fetched on every arrival at the contacts list
50
50
  * is a request for a control nobody has opened. Callers turn it on when the
@@ -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 `useHostCampaigns` 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"}
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"}
@@ -45,7 +45,7 @@ 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-host-campaigns';
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
@@ -44,7 +44,7 @@ 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-host-campaigns.js";
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";
@@ -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-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-host-campaigns'\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,gCAA4B;AAC1C,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
+ {"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,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":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;AAEA,SACEA,gBAAgB,EAChBC,cAAc,EACdC,kBAAkB,QACb,eAAc;AACrB,SAASC,UAAU,EAAEC,KAAK,EAAEC,KAAK,QAAQ,qBAAoB;AAC7D,SAASC,OAAO,QAAQ,QAAO;AAC/B,SAASC,iBAAiB,QAAQ,+BAA2B;AAC7D,SAASC,YAAY,QAAQ,kCAA8B;AAC3D,SAASC,sBAAsB,QAAQ,gCAA4B;AACnE,SAASC,iBAAiB,QAAQ,uBAAmB;AAErD;;;;;;;CAOC,GACD,OAAO,MAAMC,wBAAwB,GAAE;AAEvC,MAAMC,UAAkDC,OAAOC,MAAM,CAAC,EAAE;AAwBxE;;;;;CAKC,GACD,OAAO,SAASC,oBACdC,IAA+D,EAC/DC,KAAc;IAEd,MAAMC,OAAOF,eAAAA,OAAQ,EAAE;IACvB,MAAMG,YAAYD,KAAKE,MAAM,GAAGT;IAChC,MAAMU,OAAOH,KACVI,KAAK,CAAC,GAAGX,sBACV,wEAAwE;IACxE,uEAAuE;IACvE,UAAU;KACTY,MAAM,CAAC,CAACC,MAAQ,CAACA,GAAG,CAAC,YAAY;IACpC,OAAO;QACLC,SAASJ,KACNK,GAAG,CAAC,CAACF;gBAMYA,WACFA,gBACFA;YAPZ,MAAMG,YAAYC,MAAMC,OAAO,CAACL,GAAG,CAAC,YAAY,IAC3CA,GAAG,CAAC,YAAY,GACjB,EAAE;YACN,OAAO;gBACLM,OAAOC,OAAOP,GAAG,CAAC,MAAM;gBACxBQ,OAAOD,QAAOP,YAAAA,GAAG,CAAC,OAAO,YAAXA,YAAeA,GAAG,CAAC,MAAM;gBACvCS,SAAS,GAAGT,iBAAAA,GAAG,CAAC,YAAY,YAAhBA,iBAAkD;gBAC9DU,OAAO,GAAGV,eAAAA,GAAG,CAAC,UAAU,YAAdA,eAAgD;gBAC1DW,SAASlC,eAAe0B,aACpB,OACA3B,iBAAiB2B;YACvB;QACF,GACCS,IAAI,CAAC,CAACC,GAAGC,IAAMD,EAAEL,KAAK,CAACO,aAAa,CAACD,EAAEN,KAAK;QAC/Cb;QACAF;IACF;AACF;AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAuCC,GACD,OAAO,SAASuB,iBACdC,MAA0B,EAC1BhB,OAA+B;;IAE/B,MAAMiB,kBAAUjB,2BAAAA,QAASiB,OAAO,mBAAI;IACpC,MAAMC,YAAYnC;IAClB,MAAMoC,MAAMlC,kBAAkBgC,UAAUD,SAASI;IACjD,MAAMC,QAAQF,IAAIE,KAAK;IACvB,uEAAuE;IACvE,sCAAsC;IACtC,MAAMC,SAASzC,QACb,IAAOmC,SAASvC,mBAAmBuC,UAAU,MAC7C;QAACA;KAAO;IAEV,MAAM,EAAEzB,IAAI,EAAEgC,MAAM,EAAE,GAAGvC,uBACvB,IACEiC,WAAWD,UAAUK,SAASC,SAC1BxC,kBACEH,MACED,WAAWwC,WAAW,QAAQG,OAAO,mBACrCzC,MAAM,aAAa,sBAAsB0C,UAE3CpC,yBAEF,MACN;QAACgC;QAAWF;QAAQK;QAAOC;QAAQL;KAAQ,EAC3C;QAAEO,SAAS;IAAM;IAGnB,MAAMhC,QACJyB,WACAQ,QAAQT,WACRG,IAAIO,MAAM,IACTL,CAAAA,QAAQE,WAAW,YAAY,IAAG;IACrC,OAAO1C,QACL,IAAMS,oBAAoB+B,QAAQ9B,OAAOJ,SAASK,QAClD;QAACD;QAAM8B;QAAO7B;KAAM;AAExB;AAEA;;;;;;;;;;;;;;CAcC,GACD,OAAO,SAASmC,gBACdN,KAAgC,EAChCrB,OAA+B;;IAE/B,MAAMiB,kBAAUjB,2BAAAA,QAASiB,OAAO,mBAAI;IACpC,MAAMC,YAAYnC;IAClB,MAAM,EAAEQ,IAAI,EAAEgC,MAAM,EAAE,GAAGvC,uBACvB,IACEiC,WAAWI,QACPvC,kBACEJ,WAAWwC,WAAW,QAAQG,OAAO,mBACrCnC,yBAEF,MACN;QAACgC;QAAWG;QAAOJ;KAAQ,EAC3B;QAAEO,SAAS;IAAM;IAGnB,MAAMhC,QAAQyB,WAAWQ,QAAQJ,UAAUE,WAAW;IACtD,OAAO1C,QAAQ,IAAMS,oBAAoBC,MAAMC,QAAQ;QAACD;QAAMC;KAAM;AACtE;AAEA,eAAeuB,iBAAgB"}