@aglyn/tenant-feature-instance 1.0.0-beta.275 → 1.0.0-beta.282
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +9 -8
- package/src/lib/hooks/use-confirmed-doc.d.ts +73 -0
- package/src/lib/hooks/use-confirmed-doc.js +204 -0
- package/src/lib/hooks/use-confirmed-doc.js.map +1 -0
- package/src/lib/hooks/use-host-component-definitions.d.ts +40 -0
- package/src/lib/hooks/use-host-component-definitions.js +92 -0
- package/src/lib/hooks/use-host-component-definitions.js.map +1 -0
- package/src/lib/hooks/use-host-form-designs.d.ts +42 -0
- package/src/lib/hooks/use-host-form-designs.js +84 -0
- package/src/lib/hooks/use-host-form-designs.js.map +1 -0
- package/src/lib/hooks/use-org-plans.d.ts +47 -0
- package/src/lib/hooks/use-org-plans.js +164 -0
- package/src/lib/hooks/use-org-plans.js.map +1 -0
- package/src/lib/hooks/use-plugin-drawer-registration.d.ts +9 -0
- package/src/lib/hooks/use-plugin-drawer-registration.js +133 -0
- package/src/lib/hooks/use-plugin-drawer-registration.js.map +1 -0
- package/src/lib/hooks/use-site-marketplace-plugins.d.ts +35 -0
- package/src/lib/hooks/use-site-marketplace-plugins.js +99 -0
- package/src/lib/hooks/use-site-marketplace-plugins.js.map +1 -0
- package/src/lib/hooks/use-theme-library.d.ts +7 -0
- package/src/lib/hooks/use-theme-library.js +58 -0
- package/src/lib/hooks/use-theme-library.js.map +1 -0
- package/src/lib/hooks/use-user-name.js +8 -10
- package/src/lib/hooks/use-user-name.js.map +1 -1
- package/src/lib/hooks/use-user-photo.js +8 -2
- package/src/lib/hooks/use-user-photo.js.map +1 -1
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The billing plan/tier for each of the user's orgs, read on demand (AGL-631).
|
|
3
|
+
* The org doc is the plan source of truth (webhook-mirrored); the membership
|
|
4
|
+
* mirror the org switcher lists carries only role/name/slug, so the switcher
|
|
5
|
+
* reads each org's plan directly. Per-doc `getDoc` keeps it rules-safe — a
|
|
6
|
+
* member can read their own org doc, whereas an `in` query over `orgs` cannot
|
|
7
|
+
* be proven allowed and is denied. `enabled` gates the reads to when the menu
|
|
8
|
+
* opens, so nothing is fetched until the user actually looks.
|
|
9
|
+
*
|
|
10
|
+
* AGL-2336 — this fan-out is what capped memberships at 50, not the size of
|
|
11
|
+
* the membership docs. Two rules now bound it:
|
|
12
|
+
*
|
|
13
|
+
* - **Ask only for what is not already answered.** Resolved tiers accumulate
|
|
14
|
+
* for the life of the hook, so re-opening the switcher costs ZERO reads.
|
|
15
|
+
* Previously every open re-read every id, so a 50-workspace agency paid 50
|
|
16
|
+
* reads per glance at the menu and a 500-workspace one would pay 500.
|
|
17
|
+
* - **The caller passes the rows it is actually showing**, not the whole
|
|
18
|
+
* membership list — see `org-switcher-nav`'s reveal window. Cost is then
|
|
19
|
+
* proportional to what a human looked at and is independent of how many
|
|
20
|
+
* workspaces they belong to, which is the precondition for raising the cap.
|
|
21
|
+
*
|
|
22
|
+
* A tier resolved once is not re-read while the hook is mounted, so a plan
|
|
23
|
+
* change elsewhere shows on the next load. That is fine for a row badge: the
|
|
24
|
+
* workspace you are actually IN takes its badge from `useCurrentOrg`, which
|
|
25
|
+
* is a live subscription.
|
|
26
|
+
*
|
|
27
|
+
* A FAILED read is deliberately not memoised — it is not an answer, and the
|
|
28
|
+
* next open should ask again.
|
|
29
|
+
*/
|
|
30
|
+
export declare function useOrgPlans(orgIds: string[], enabled?: boolean): Record<string, string>;
|
|
31
|
+
/** What one read of an org doc tells a row about that workspace. */
|
|
32
|
+
export interface OrgRow {
|
|
33
|
+
/** The plan the workspace gets, as `useOrgPlans` reports it. */
|
|
34
|
+
plan: string;
|
|
35
|
+
/** The org's logo (AGL-363), when it set one. */
|
|
36
|
+
logoUrl?: string;
|
|
37
|
+
/** The org's dark-mode logo, when it set one. */
|
|
38
|
+
logoDarkUrl?: string;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Each org's plan and logos from the SAME per-doc read `useOrgPlans` makes,
|
|
42
|
+
* so a row that shows a workspace's logo costs nothing more than its badge.
|
|
43
|
+
* The membership mirror carries no logo, so without this only the workspace
|
|
44
|
+
* the reader is in (whose doc `useCurrentOrg` holds) could show its own.
|
|
45
|
+
*/
|
|
46
|
+
export declare function useOrgRows(orgIds: string[], enabled?: boolean): Record<string, OrgRow>;
|
|
47
|
+
export default useOrgPlans;
|
|
@@ -0,0 +1,164 @@
|
|
|
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 { _ as _extends } from "@swc/helpers/_/_extends";
|
|
18
|
+
import { isEnterpriseOrg, resolveEffectivePlan } from "@aglyn/aglyn";
|
|
19
|
+
import { useFirestore } from "./firebase/firebase-services.js";
|
|
20
|
+
import { doc, getDoc } from "firebase/firestore";
|
|
21
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
22
|
+
/**
|
|
23
|
+
* The billing plan/tier for each of the user's orgs, read on demand (AGL-631).
|
|
24
|
+
* The org doc is the plan source of truth (webhook-mirrored); the membership
|
|
25
|
+
* mirror the org switcher lists carries only role/name/slug, so the switcher
|
|
26
|
+
* reads each org's plan directly. Per-doc `getDoc` keeps it rules-safe — a
|
|
27
|
+
* member can read their own org doc, whereas an `in` query over `orgs` cannot
|
|
28
|
+
* be proven allowed and is denied. `enabled` gates the reads to when the menu
|
|
29
|
+
* opens, so nothing is fetched until the user actually looks.
|
|
30
|
+
*
|
|
31
|
+
* AGL-2336 — this fan-out is what capped memberships at 50, not the size of
|
|
32
|
+
* the membership docs. Two rules now bound it:
|
|
33
|
+
*
|
|
34
|
+
* - **Ask only for what is not already answered.** Resolved tiers accumulate
|
|
35
|
+
* for the life of the hook, so re-opening the switcher costs ZERO reads.
|
|
36
|
+
* Previously every open re-read every id, so a 50-workspace agency paid 50
|
|
37
|
+
* reads per glance at the menu and a 500-workspace one would pay 500.
|
|
38
|
+
* - **The caller passes the rows it is actually showing**, not the whole
|
|
39
|
+
* membership list — see `org-switcher-nav`'s reveal window. Cost is then
|
|
40
|
+
* proportional to what a human looked at and is independent of how many
|
|
41
|
+
* workspaces they belong to, which is the precondition for raising the cap.
|
|
42
|
+
*
|
|
43
|
+
* A tier resolved once is not re-read while the hook is mounted, so a plan
|
|
44
|
+
* change elsewhere shows on the next load. That is fine for a row badge: the
|
|
45
|
+
* workspace you are actually IN takes its badge from `useCurrentOrg`, which
|
|
46
|
+
* is a live subscription.
|
|
47
|
+
*
|
|
48
|
+
* A FAILED read is deliberately not memoised — it is not an answer, and the
|
|
49
|
+
* next open should ask again.
|
|
50
|
+
*/ export function useOrgPlans(orgIds, enabled = true) {
|
|
51
|
+
const rows = useOrgRows(orgIds, enabled);
|
|
52
|
+
return useMemo(()=>Object.fromEntries(Object.entries(rows).map(([id, row])=>[
|
|
53
|
+
id,
|
|
54
|
+
row.plan
|
|
55
|
+
])), [
|
|
56
|
+
rows
|
|
57
|
+
]);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Each org's plan and logos from the SAME per-doc read `useOrgPlans` makes,
|
|
61
|
+
* so a row that shows a workspace's logo costs nothing more than its badge.
|
|
62
|
+
* The membership mirror carries no logo, so without this only the workspace
|
|
63
|
+
* the reader is in (whose doc `useCurrentOrg` holds) could show its own.
|
|
64
|
+
*/ export function useOrgRows(orgIds, enabled = true) {
|
|
65
|
+
const firestore = useFirestore();
|
|
66
|
+
const [plans, setPlans] = useState({});
|
|
67
|
+
// What has been asked for already — answered or in flight. A ref, not
|
|
68
|
+
// derived from `plans`, so an id whose read is still in flight is not
|
|
69
|
+
// re-requested by the next render.
|
|
70
|
+
const requested = useRef(new Set());
|
|
71
|
+
const mounted = useRef(true);
|
|
72
|
+
const key = orgIds.join(',');
|
|
73
|
+
useEffect(()=>{
|
|
74
|
+
mounted.current = true;
|
|
75
|
+
return ()=>{
|
|
76
|
+
mounted.current = false;
|
|
77
|
+
};
|
|
78
|
+
}, []);
|
|
79
|
+
// A different Firestore instance is a different world; drop the memo with
|
|
80
|
+
// it rather than serve another instance's answers. Declared BEFORE the
|
|
81
|
+
// fetch effect so it runs first on the render that swaps the instance, and
|
|
82
|
+
// skipped on the first run so mounting does not cost a wasted re-render.
|
|
83
|
+
const instance = useRef(firestore);
|
|
84
|
+
useEffect(()=>{
|
|
85
|
+
if (instance.current === firestore) return;
|
|
86
|
+
instance.current = firestore;
|
|
87
|
+
requested.current = new Set();
|
|
88
|
+
setPlans({});
|
|
89
|
+
}, [
|
|
90
|
+
firestore
|
|
91
|
+
]);
|
|
92
|
+
useEffect(()=>{
|
|
93
|
+
if (!enabled) return undefined;
|
|
94
|
+
const wanted = orgIds.filter((id)=>id && !requested.current.has(id));
|
|
95
|
+
if (wanted.length === 0) return undefined;
|
|
96
|
+
wanted.forEach((id)=>requested.current.add(id));
|
|
97
|
+
void Promise.all(wanted.map(async (id)=>{
|
|
98
|
+
try {
|
|
99
|
+
var _snap_data;
|
|
100
|
+
const snap = await getDoc(doc(firestore, 'orgs', id));
|
|
101
|
+
const data = (_snap_data = snap.data()) != null ? _snap_data : {};
|
|
102
|
+
const logos = _extends({}, typeof data.logoUrl === 'string' && data.logoUrl ? {
|
|
103
|
+
logoUrl: data.logoUrl
|
|
104
|
+
} : {}, typeof data.logoDarkUrl === 'string' && data.logoDarkUrl ? {
|
|
105
|
+
logoDarkUrl: data.logoDarkUrl
|
|
106
|
+
} : {});
|
|
107
|
+
// A custom-priced enterprise deal reads as "enterprise" regardless of
|
|
108
|
+
// its base plan (AGL-1110).
|
|
109
|
+
if (isEnterpriseOrg(data)) {
|
|
110
|
+
return [
|
|
111
|
+
id,
|
|
112
|
+
_extends({
|
|
113
|
+
plan: 'enterprise'
|
|
114
|
+
}, logos)
|
|
115
|
+
];
|
|
116
|
+
}
|
|
117
|
+
// An org on the free tier has no `plan` field at all — the webhook
|
|
118
|
+
// only writes one for a paid subscription. A successful read with
|
|
119
|
+
// no plan therefore MEANS free, and must be reported as such;
|
|
120
|
+
// leaving it absent made "free" look identical to "not loaded"
|
|
121
|
+
// (AGL-646). Only a failed read stays unknown.
|
|
122
|
+
//
|
|
123
|
+
// RESOLVED, not read (AGL-3034): a staff comp grants a plan the
|
|
124
|
+
// field does not name, and a dead subscription's stored plan is
|
|
125
|
+
// Free. The resolver answers `free` for the missing field too.
|
|
126
|
+
return [
|
|
127
|
+
id,
|
|
128
|
+
_extends({
|
|
129
|
+
plan: resolveEffectivePlan(data)
|
|
130
|
+
}, logos)
|
|
131
|
+
];
|
|
132
|
+
} catch (unused) {
|
|
133
|
+
// Not an answer — let the next open ask again.
|
|
134
|
+
requested.current.delete(id);
|
|
135
|
+
return [
|
|
136
|
+
id,
|
|
137
|
+
undefined
|
|
138
|
+
];
|
|
139
|
+
}
|
|
140
|
+
})).then((entries)=>{
|
|
141
|
+
// Deliberately NOT gated on an effect-scoped `cancelled` flag: closing
|
|
142
|
+
// the switcher flips `enabled` and tears this effect down, and dropping
|
|
143
|
+
// an in-flight answer there would leave the id marked requested and
|
|
144
|
+
// permanently unanswerable. Only an unmounted hook stops caring.
|
|
145
|
+
if (!mounted.current) return;
|
|
146
|
+
const resolved = entries.filter((entry)=>Boolean(entry[1]));
|
|
147
|
+
if (resolved.length === 0) return;
|
|
148
|
+
// MERGE, never replace: the map spans every row revealed so far, and
|
|
149
|
+
// replacing it would blank the badges on rows already on screen.
|
|
150
|
+
setPlans((current)=>_extends({}, current, Object.fromEntries(resolved)));
|
|
151
|
+
});
|
|
152
|
+
return undefined;
|
|
153
|
+
// `key` captures the org-id list; enabled toggles the fetch.
|
|
154
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
155
|
+
}, [
|
|
156
|
+
firestore,
|
|
157
|
+
key,
|
|
158
|
+
enabled
|
|
159
|
+
]);
|
|
160
|
+
return plans;
|
|
161
|
+
}
|
|
162
|
+
export default useOrgPlans;
|
|
163
|
+
|
|
164
|
+
//# sourceMappingURL=use-org-plans.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/tenant/feature/instance/src/lib/hooks/use-org-plans.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 { isEnterpriseOrg, resolveEffectivePlan } from '@aglyn/aglyn'\nimport { useFirestore } from './firebase/firebase-services'\nimport { doc, getDoc } from 'firebase/firestore'\nimport { useEffect, useMemo, useRef, useState } from 'react'\n\n/**\n * The billing plan/tier for each of the user's orgs, read on demand (AGL-631).\n * The org doc is the plan source of truth (webhook-mirrored); the membership\n * mirror the org switcher lists carries only role/name/slug, so the switcher\n * reads each org's plan directly. Per-doc `getDoc` keeps it rules-safe — a\n * member can read their own org doc, whereas an `in` query over `orgs` cannot\n * be proven allowed and is denied. `enabled` gates the reads to when the menu\n * opens, so nothing is fetched until the user actually looks.\n *\n * AGL-2336 — this fan-out is what capped memberships at 50, not the size of\n * the membership docs. Two rules now bound it:\n *\n * - **Ask only for what is not already answered.** Resolved tiers accumulate\n * for the life of the hook, so re-opening the switcher costs ZERO reads.\n * Previously every open re-read every id, so a 50-workspace agency paid 50\n * reads per glance at the menu and a 500-workspace one would pay 500.\n * - **The caller passes the rows it is actually showing**, not the whole\n * membership list — see `org-switcher-nav`'s reveal window. Cost is then\n * proportional to what a human looked at and is independent of how many\n * workspaces they belong to, which is the precondition for raising the cap.\n *\n * A tier resolved once is not re-read while the hook is mounted, so a plan\n * change elsewhere shows on the next load. That is fine for a row badge: the\n * workspace you are actually IN takes its badge from `useCurrentOrg`, which\n * is a live subscription.\n *\n * A FAILED read is deliberately not memoised — it is not an answer, and the\n * next open should ask again.\n */\nexport function useOrgPlans(\n orgIds: string[],\n enabled = true,\n): Record<string, string> {\n const rows = useOrgRows(orgIds, enabled)\n return useMemo(\n () =>\n Object.fromEntries(Object.entries(rows).map(([id, row]) => [id, row.plan])),\n [rows],\n )\n}\n\n/** What one read of an org doc tells a row about that workspace. */\nexport interface OrgRow {\n /** The plan the workspace gets, as `useOrgPlans` reports it. */\n plan: string\n /** The org's logo (AGL-363), when it set one. */\n logoUrl?: string\n /** The org's dark-mode logo, when it set one. */\n logoDarkUrl?: string\n}\n\n/**\n * Each org's plan and logos from the SAME per-doc read `useOrgPlans` makes,\n * so a row that shows a workspace's logo costs nothing more than its badge.\n * The membership mirror carries no logo, so without this only the workspace\n * the reader is in (whose doc `useCurrentOrg` holds) could show its own.\n */\nexport function useOrgRows(\n orgIds: string[],\n enabled = true,\n): Record<string, OrgRow> {\n const firestore = useFirestore()\n const [plans, setPlans] = useState<Record<string, OrgRow>>({})\n // What has been asked for already — answered or in flight. A ref, not\n // derived from `plans`, so an id whose read is still in flight is not\n // re-requested by the next render.\n const requested = useRef(new Set<string>())\n const mounted = useRef(true)\n const key = orgIds.join(',')\n\n useEffect(() => {\n mounted.current = true\n return () => {\n mounted.current = false\n }\n }, [])\n\n // A different Firestore instance is a different world; drop the memo with\n // it rather than serve another instance's answers. Declared BEFORE the\n // fetch effect so it runs first on the render that swaps the instance, and\n // skipped on the first run so mounting does not cost a wasted re-render.\n const instance = useRef(firestore)\n useEffect(() => {\n if (instance.current === firestore) return\n instance.current = firestore\n requested.current = new Set<string>()\n setPlans({})\n }, [firestore])\n\n useEffect(() => {\n if (!enabled) return undefined\n const wanted = orgIds.filter((id) => id && !requested.current.has(id))\n if (wanted.length === 0) return undefined\n wanted.forEach((id) => requested.current.add(id))\n void Promise.all(\n wanted.map(async (id) => {\n try {\n const snap = await getDoc(doc(firestore, 'orgs', id))\n const data = (snap.data() ?? {}) as Record<string, unknown>\n const logos = {\n ...(typeof data.logoUrl === 'string' && data.logoUrl\n ? { logoUrl: data.logoUrl }\n : {}),\n ...(typeof data.logoDarkUrl === 'string' && data.logoDarkUrl\n ? { logoDarkUrl: data.logoDarkUrl }\n : {}),\n }\n // A custom-priced enterprise deal reads as \"enterprise\" regardless of\n // its base plan (AGL-1110).\n if (isEnterpriseOrg(data as never)) {\n return [id, { plan: 'enterprise', ...logos }] as const\n }\n // An org on the free tier has no `plan` field at all — the webhook\n // only writes one for a paid subscription. A successful read with\n // no plan therefore MEANS free, and must be reported as such;\n // leaving it absent made \"free\" look identical to \"not loaded\"\n // (AGL-646). Only a failed read stays unknown.\n //\n // RESOLVED, not read (AGL-3034): a staff comp grants a plan the\n // field does not name, and a dead subscription's stored plan is\n // Free. The resolver answers `free` for the missing field too.\n return [\n id,\n { plan: resolveEffectivePlan(data as never) as string, ...logos },\n ] as const\n } catch {\n // Not an answer — let the next open ask again.\n requested.current.delete(id)\n return [id, undefined] as const\n }\n }),\n ).then((entries) => {\n // Deliberately NOT gated on an effect-scoped `cancelled` flag: closing\n // the switcher flips `enabled` and tears this effect down, and dropping\n // an in-flight answer there would leave the id marked requested and\n // permanently unanswerable. Only an unmounted hook stops caring.\n if (!mounted.current) return\n const resolved = entries.filter((entry): entry is [string, OrgRow] =>\n Boolean(entry[1]),\n )\n if (resolved.length === 0) return\n // MERGE, never replace: the map spans every row revealed so far, and\n // replacing it would blank the badges on rows already on screen.\n setPlans((current) => ({ ...current, ...Object.fromEntries(resolved) }))\n })\n return undefined\n // `key` captures the org-id list; enabled toggles the fetch.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [firestore, key, enabled])\n\n return plans\n}\n\nexport default useOrgPlans\n"],"names":["isEnterpriseOrg","resolveEffectivePlan","useFirestore","doc","getDoc","useEffect","useMemo","useRef","useState","useOrgPlans","orgIds","enabled","rows","useOrgRows","Object","fromEntries","entries","map","id","row","plan","firestore","plans","setPlans","requested","Set","mounted","key","join","current","instance","undefined","wanted","filter","has","length","forEach","add","Promise","all","snap","data","logos","logoUrl","logoDarkUrl","delete","then","resolved","entry","Boolean"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;AAEA,SAASA,eAAe,EAAEC,oBAAoB,QAAQ,eAAc;AACpE,SAASC,YAAY,QAAQ,kCAA8B;AAC3D,SAASC,GAAG,EAAEC,MAAM,QAAQ,qBAAoB;AAChD,SAASC,SAAS,EAAEC,OAAO,EAAEC,MAAM,EAAEC,QAAQ,QAAQ,QAAO;AAE5D;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4BC,GACD,OAAO,SAASC,YACdC,MAAgB,EAChBC,UAAU,IAAI;IAEd,MAAMC,OAAOC,WAAWH,QAAQC;IAChC,OAAOL,QACL,IACEQ,OAAOC,WAAW,CAACD,OAAOE,OAAO,CAACJ,MAAMK,GAAG,CAAC,CAAC,CAACC,IAAIC,IAAI,GAAK;gBAACD;gBAAIC,IAAIC,IAAI;aAAC,IAC3E;QAACR;KAAK;AAEV;AAYA;;;;;CAKC,GACD,OAAO,SAASC,WACdH,MAAgB,EAChBC,UAAU,IAAI;IAEd,MAAMU,YAAYnB;IAClB,MAAM,CAACoB,OAAOC,SAAS,GAAGf,SAAiC,CAAC;IAC5D,sEAAsE;IACtE,sEAAsE;IACtE,mCAAmC;IACnC,MAAMgB,YAAYjB,OAAO,IAAIkB;IAC7B,MAAMC,UAAUnB,OAAO;IACvB,MAAMoB,MAAMjB,OAAOkB,IAAI,CAAC;IAExBvB,UAAU;QACRqB,QAAQG,OAAO,GAAG;QAClB,OAAO;YACLH,QAAQG,OAAO,GAAG;QACpB;IACF,GAAG,EAAE;IAEL,0EAA0E;IAC1E,uEAAuE;IACvE,2EAA2E;IAC3E,yEAAyE;IACzE,MAAMC,WAAWvB,OAAOc;IACxBhB,UAAU;QACR,IAAIyB,SAASD,OAAO,KAAKR,WAAW;QACpCS,SAASD,OAAO,GAAGR;QACnBG,UAAUK,OAAO,GAAG,IAAIJ;QACxBF,SAAS,CAAC;IACZ,GAAG;QAACF;KAAU;IAEdhB,UAAU;QACR,IAAI,CAACM,SAAS,OAAOoB;QACrB,MAAMC,SAAStB,OAAOuB,MAAM,CAAC,CAACf,KAAOA,MAAM,CAACM,UAAUK,OAAO,CAACK,GAAG,CAAChB;QAClE,IAAIc,OAAOG,MAAM,KAAK,GAAG,OAAOJ;QAChCC,OAAOI,OAAO,CAAC,CAAClB,KAAOM,UAAUK,OAAO,CAACQ,GAAG,CAACnB;QAC7C,KAAKoB,QAAQC,GAAG,CACdP,OAAOf,GAAG,CAAC,OAAOC;YAChB,IAAI;oBAEYsB;gBADd,MAAMA,OAAO,MAAMpC,OAAOD,IAAIkB,WAAW,QAAQH;gBACjD,MAAMuB,QAAQD,aAAAA,KAAKC,IAAI,cAATD,aAAe,CAAC;gBAC9B,MAAME,QAAQ,aACR,OAAOD,KAAKE,OAAO,KAAK,YAAYF,KAAKE,OAAO,GAChD;oBAAEA,SAASF,KAAKE,OAAO;gBAAC,IACxB,CAAC,GACD,OAAOF,KAAKG,WAAW,KAAK,YAAYH,KAAKG,WAAW,GACxD;oBAAEA,aAAaH,KAAKG,WAAW;gBAAC,IAChC,CAAC;gBAEP,sEAAsE;gBACtE,4BAA4B;gBAC5B,IAAI5C,gBAAgByC,OAAgB;oBAClC,OAAO;wBAACvB;wBAAI;4BAAEE,MAAM;2BAAiBsB;qBAAQ;gBAC/C;gBACA,mEAAmE;gBACnE,kEAAkE;gBAClE,8DAA8D;gBAC9D,+DAA+D;gBAC/D,+CAA+C;gBAC/C,EAAE;gBACF,gEAAgE;gBAChE,gEAAgE;gBAChE,+DAA+D;gBAC/D,OAAO;oBACLxB;oBACA;wBAAEE,MAAMnB,qBAAqBwC;uBAA6BC;iBAC3D;YACH,EAAE,eAAM;gBACN,+CAA+C;gBAC/ClB,UAAUK,OAAO,CAACgB,MAAM,CAAC3B;gBACzB,OAAO;oBAACA;oBAAIa;iBAAU;YACxB;QACF,IACAe,IAAI,CAAC,CAAC9B;YACN,uEAAuE;YACvE,wEAAwE;YACxE,oEAAoE;YACpE,iEAAiE;YACjE,IAAI,CAACU,QAAQG,OAAO,EAAE;YACtB,MAAMkB,WAAW/B,QAAQiB,MAAM,CAAC,CAACe,QAC/BC,QAAQD,KAAK,CAAC,EAAE;YAElB,IAAID,SAASZ,MAAM,KAAK,GAAG;YAC3B,qEAAqE;YACrE,iEAAiE;YACjEZ,SAAS,CAACM,UAAa,aAAKA,SAAYf,OAAOC,WAAW,CAACgC;QAC7D;QACA,OAAOhB;IACP,6DAA6D;IAC7D,uDAAuD;IACzD,GAAG;QAACV;QAAWM;QAAKhB;KAAQ;IAE5B,OAAOW;AACT;AAEA,eAAeb,YAAW"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registers the host's installed plugins as named besigner drawer entries
|
|
3
|
+
* (AGL-190): one preset per install under the Marketplace category, dropping
|
|
4
|
+
* a `marketplacePlugin` node with the listing id pre-pinned. Presets are
|
|
5
|
+
* re-synced whenever installs change and removed on host change/unmount so
|
|
6
|
+
* switching hosts never leaks another host's plugins into the drawer.
|
|
7
|
+
*/
|
|
8
|
+
export declare function usePluginDrawerRegistration(hostId: string): void;
|
|
9
|
+
export default usePluginDrawerRegistration;
|
|
@@ -0,0 +1,133 @@
|
|
|
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 { _ as _extends } from "@swc/helpers/_/_extends";
|
|
18
|
+
import { components } from "@aglyn/aglyn";
|
|
19
|
+
import { pluginInstallToPresets, setKnownPluginInstalls } from "@aglyn/aglyn";
|
|
20
|
+
import { collection, limit, query } from "firebase/firestore";
|
|
21
|
+
import { runInAction } from "mobx";
|
|
22
|
+
import { useEffect, useMemo, useRef } from "react";
|
|
23
|
+
import { useFirestore } from "./firebase/firebase-services.js";
|
|
24
|
+
import { useHostOrgId } from "./use-host-org-id.js";
|
|
25
|
+
import useFirestoreCollection from "./use-firestore-collection.js";
|
|
26
|
+
/**
|
|
27
|
+
* Registers the host's installed plugins as named besigner drawer entries
|
|
28
|
+
* (AGL-190): one preset per install under the Marketplace category, dropping
|
|
29
|
+
* a `marketplacePlugin` node with the listing id pre-pinned. Presets are
|
|
30
|
+
* re-synced whenever installs change and removed on host change/unmount so
|
|
31
|
+
* switching hosts never leaks another host's plugins into the drawer.
|
|
32
|
+
*/ export function usePluginDrawerRegistration(hostId) {
|
|
33
|
+
const firestore = useFirestore();
|
|
34
|
+
const { data: installDocs } = useFirestoreCollection(()=>query(collection(firestore, 'hosts', hostId, 'installs'), limit(50)), [
|
|
35
|
+
firestore,
|
|
36
|
+
hostId
|
|
37
|
+
], {
|
|
38
|
+
idField: '$id'
|
|
39
|
+
});
|
|
40
|
+
// Org-wide pins too (AGL-1029). One org pin covers every site, so a plugin
|
|
41
|
+
// installed that way is genuinely available here — but the drawer only ever
|
|
42
|
+
// read the host's own installs, so those plugins had no entry, and the
|
|
43
|
+
// element could not be told they were installed either.
|
|
44
|
+
const orgId = useHostOrgId(hostId);
|
|
45
|
+
// Held at null while `useHostOrgId` is in flight, never `orgs/-pending-`
|
|
46
|
+
// (AGL-1440): installs are member-gated, so the sentinel was a
|
|
47
|
+
// guaranteed-denied listen on every besigner editor session.
|
|
48
|
+
const { data: orgInstallDocs } = useFirestoreCollection(()=>orgId ? query(collection(firestore, 'orgs', orgId, 'installs'), limit(50)) : null, [
|
|
49
|
+
firestore,
|
|
50
|
+
orgId
|
|
51
|
+
], {
|
|
52
|
+
idField: '$id'
|
|
53
|
+
});
|
|
54
|
+
/**
|
|
55
|
+
* Host pins shadow org pins for the same listing (AGL-656), so the host
|
|
56
|
+
* entry wins — the drawer must not offer the same plugin twice, and the
|
|
57
|
+
* scope reported for it has to be the one actually in force.
|
|
58
|
+
*/ const installs = useMemo(()=>{
|
|
59
|
+
const byListing = new Map();
|
|
60
|
+
for (const install of orgInstallDocs != null ? orgInstallDocs : []){
|
|
61
|
+
var _ref;
|
|
62
|
+
const id = (_ref = install == null ? void 0 : install.listingId) != null ? _ref : install == null ? void 0 : install.$id;
|
|
63
|
+
if (id) byListing.set(id, _extends({}, install, {
|
|
64
|
+
scope: 'org'
|
|
65
|
+
}));
|
|
66
|
+
}
|
|
67
|
+
for (const install of installDocs != null ? installDocs : []){
|
|
68
|
+
var _ref1;
|
|
69
|
+
const id = (_ref1 = install == null ? void 0 : install.listingId) != null ? _ref1 : install == null ? void 0 : install.$id;
|
|
70
|
+
if (id) byListing.set(id, _extends({}, install, {
|
|
71
|
+
scope: 'host'
|
|
72
|
+
}));
|
|
73
|
+
}
|
|
74
|
+
return [
|
|
75
|
+
...byListing.values()
|
|
76
|
+
];
|
|
77
|
+
}, [
|
|
78
|
+
installDocs,
|
|
79
|
+
orgInstallDocs
|
|
80
|
+
]);
|
|
81
|
+
// Track the ids we registered so we can unregister exactly those.
|
|
82
|
+
const registeredIds = useRef([]);
|
|
83
|
+
// What the canvas may claim about installation (AGL-1029). Published
|
|
84
|
+
// separately from the presets because the element asks by listing id, and
|
|
85
|
+
// an unknown id is only "not installed" if there is a list to be missing
|
|
86
|
+
// from — on the tenant nothing publishes one.
|
|
87
|
+
useEffect(()=>{
|
|
88
|
+
setKnownPluginInstalls(installs.map((install)=>{
|
|
89
|
+
var _install_listingId, _install_displayName;
|
|
90
|
+
var _install_manifest, _install_manifest1;
|
|
91
|
+
return {
|
|
92
|
+
listingId: (_install_listingId = install.listingId) != null ? _install_listingId : install.$id,
|
|
93
|
+
displayName: (_install_displayName = install.displayName) != null ? _install_displayName : (_install_manifest = install.manifest) == null ? void 0 : _install_manifest.name,
|
|
94
|
+
scope: install.scope,
|
|
95
|
+
// The declared props (and, where a publisher supplied one, how to edit
|
|
96
|
+
// them) so the attributes panel can render real fields — AGL-1049.
|
|
97
|
+
capabilities: (_install_manifest1 = install.manifest) == null ? void 0 : _install_manifest1.capabilities
|
|
98
|
+
};
|
|
99
|
+
}));
|
|
100
|
+
return ()=>setKnownPluginInstalls(undefined);
|
|
101
|
+
}, [
|
|
102
|
+
installs
|
|
103
|
+
]);
|
|
104
|
+
useEffect(()=>{
|
|
105
|
+
// Single mobx transaction (AGL-371): the drawer re-renders once per
|
|
106
|
+
// install sync, not once for the unregister and again per preset.
|
|
107
|
+
runInAction(()=>{
|
|
108
|
+
if (registeredIds.current.length) {
|
|
109
|
+
components.unregisterPreset(registeredIds.current);
|
|
110
|
+
registeredIds.current = [];
|
|
111
|
+
}
|
|
112
|
+
// One preset per install PLUS one per element the pinned version
|
|
113
|
+
// declares (AGL-1031). Built from the pin, so a declared element appears
|
|
114
|
+
// only where the plugin is installed and leaves with a revoked version.
|
|
115
|
+
const presets = installs.flatMap((install)=>pluginInstallToPresets(install)).filter((preset)=>Boolean(preset));
|
|
116
|
+
if (presets.length) {
|
|
117
|
+
components.registerPreset(presets);
|
|
118
|
+
registeredIds.current = presets.map((preset)=>preset.$id);
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
return ()=>{
|
|
122
|
+
if (registeredIds.current.length) {
|
|
123
|
+
components.unregisterPreset(registeredIds.current);
|
|
124
|
+
registeredIds.current = [];
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
}, [
|
|
128
|
+
installs
|
|
129
|
+
]);
|
|
130
|
+
}
|
|
131
|
+
export default usePluginDrawerRegistration;
|
|
132
|
+
|
|
133
|
+
//# sourceMappingURL=use-plugin-drawer-registration.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/tenant/feature/instance/src/lib/hooks/use-plugin-drawer-registration.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 { components } from '@aglyn/aglyn'\nimport { pluginInstallToPresets, setKnownPluginInstalls } from '@aglyn/aglyn'\nimport { collection, limit, query } from 'firebase/firestore'\nimport { runInAction } from 'mobx'\nimport { useEffect, useMemo, useRef } from 'react'\nimport { useFirestore } from './firebase/firebase-services'\nimport { useHostOrgId } from './use-host-org-id'\nimport useFirestoreCollection from './use-firestore-collection'\n\n/**\n * Registers the host's installed plugins as named besigner drawer entries\n * (AGL-190): one preset per install under the Marketplace category, dropping\n * a `marketplacePlugin` node with the listing id pre-pinned. Presets are\n * re-synced whenever installs change and removed on host change/unmount so\n * switching hosts never leaks another host's plugins into the drawer.\n */\nexport function usePluginDrawerRegistration(hostId: string): void {\n const firestore = useFirestore()\n const { data: installDocs } = useFirestoreCollection<any>(\n () => query(collection(firestore, 'hosts', hostId, 'installs'), limit(50)),\n [firestore, hostId],\n { idField: '$id' },\n )\n // Org-wide pins too (AGL-1029). One org pin covers every site, so a plugin\n // installed that way is genuinely available here — but the drawer only ever\n // read the host's own installs, so those plugins had no entry, and the\n // element could not be told they were installed either.\n const orgId = useHostOrgId(hostId)\n // Held at null while `useHostOrgId` is in flight, never `orgs/-pending-`\n // (AGL-1440): installs are member-gated, so the sentinel was a\n // guaranteed-denied listen on every besigner editor session.\n const { data: orgInstallDocs } = useFirestoreCollection<any>(\n () =>\n orgId\n ? query(collection(firestore, 'orgs', orgId, 'installs'), limit(50))\n : null,\n [firestore, orgId],\n { idField: '$id' },\n )\n /**\n * Host pins shadow org pins for the same listing (AGL-656), so the host\n * entry wins — the drawer must not offer the same plugin twice, and the\n * scope reported for it has to be the one actually in force.\n */\n const installs = useMemo(() => {\n const byListing = new Map<string, any>()\n for (const install of (orgInstallDocs as any[]) ?? []) {\n const id = install?.listingId ?? install?.$id\n if (id) byListing.set(id, { ...install, scope: 'org' })\n }\n for (const install of (installDocs as any[]) ?? []) {\n const id = install?.listingId ?? install?.$id\n if (id) byListing.set(id, { ...install, scope: 'host' })\n }\n return [...byListing.values()]\n }, [installDocs, orgInstallDocs])\n // Track the ids we registered so we can unregister exactly those.\n const registeredIds = useRef<string[]>([])\n\n // What the canvas may claim about installation (AGL-1029). Published\n // separately from the presets because the element asks by listing id, and\n // an unknown id is only \"not installed\" if there is a list to be missing\n // from — on the tenant nothing publishes one.\n useEffect(() => {\n setKnownPluginInstalls(\n installs.map((install) => ({\n listingId: install.listingId ?? install.$id,\n displayName: install.displayName ?? install.manifest?.name,\n scope: install.scope,\n // The declared props (and, where a publisher supplied one, how to edit\n // them) so the attributes panel can render real fields — AGL-1049.\n capabilities: install.manifest?.capabilities,\n })),\n )\n return () => setKnownPluginInstalls(undefined)\n }, [installs])\n\n useEffect(() => {\n // Single mobx transaction (AGL-371): the drawer re-renders once per\n // install sync, not once for the unregister and again per preset.\n runInAction(() => {\n if (registeredIds.current.length) {\n components.unregisterPreset(registeredIds.current)\n registeredIds.current = []\n }\n // One preset per install PLUS one per element the pinned version\n // declares (AGL-1031). Built from the pin, so a declared element appears\n // only where the plugin is installed and leaves with a revoked version.\n const presets = installs\n .flatMap((install) => pluginInstallToPresets(install))\n .filter(\n (preset): preset is NonNullable<typeof preset> => Boolean(preset),\n )\n if (presets.length) {\n components.registerPreset(presets)\n registeredIds.current = presets.map((preset) => preset.$id)\n }\n })\n return () => {\n if (registeredIds.current.length) {\n components.unregisterPreset(registeredIds.current)\n registeredIds.current = []\n }\n }\n }, [installs])\n}\n\nexport default usePluginDrawerRegistration\n"],"names":["components","pluginInstallToPresets","setKnownPluginInstalls","collection","limit","query","runInAction","useEffect","useMemo","useRef","useFirestore","useHostOrgId","useFirestoreCollection","usePluginDrawerRegistration","hostId","firestore","data","installDocs","idField","orgId","orgInstallDocs","installs","byListing","Map","install","id","listingId","$id","set","scope","values","registeredIds","map","displayName","manifest","name","capabilities","undefined","current","length","unregisterPreset","presets","flatMap","filter","preset","Boolean","registerPreset"],"mappings":"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"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** One marketplace plugin as a site sees it. */
|
|
2
|
+
export interface SiteMarketplacePlugin {
|
|
3
|
+
/** The listing document id — the id `enabledPlugins` and the pins key by. */
|
|
4
|
+
listingId: string;
|
|
5
|
+
/** The publisher's name for it, denormalized onto the pin. */
|
|
6
|
+
displayName: string;
|
|
7
|
+
version?: string;
|
|
8
|
+
/**
|
|
9
|
+
* `org` — pinned for every site in the workspace; `site` — pinned for this
|
|
10
|
+
* one. The distinction changes what switching it off here means, so it is
|
|
11
|
+
* carried rather than flattened.
|
|
12
|
+
*/
|
|
13
|
+
scope: 'org' | 'site';
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Every marketplace plugin that can run on one site (AGL-1014).
|
|
17
|
+
*
|
|
18
|
+
* The site's plugin switchboard used to derive this from `org.enabledPlugins`
|
|
19
|
+
* alone, and that field is EMPTY for most workspaces: `syncEnabledPlugins`
|
|
20
|
+
* appends a listing id only for an org that has already configured the
|
|
21
|
+
* switchboard, because an absent field means default-open and adding the first
|
|
22
|
+
* id would switch off every plugin not in it. So a workspace that never
|
|
23
|
+
* touched the built-in switches had its marketplace installs running on every
|
|
24
|
+
* site with nothing in the console listing them, and no way to switch one off
|
|
25
|
+
* for one site.
|
|
26
|
+
*
|
|
27
|
+
* The pins are the durable record, so they are the source here: org-tier pins
|
|
28
|
+
* apply to every site, host-tier pins to this one, and the ids already on
|
|
29
|
+
* `enabledPlugins` are unioned in so a listing whose pin this reader cannot
|
|
30
|
+
* see still gets a row rather than silently vanishing.
|
|
31
|
+
*/
|
|
32
|
+
export declare function useSiteMarketplacePlugins(orgId: string, hostId: string,
|
|
33
|
+
/** Non-first-party ids already on the org's switchboard. */
|
|
34
|
+
switchboardIds?: readonly string[]): SiteMarketplacePlugin[];
|
|
35
|
+
export default useSiteMarketplacePlugins;
|
|
@@ -0,0 +1,99 @@
|
|
|
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 { collection, limit, query } from "firebase/firestore";
|
|
18
|
+
import { useMemo } from "react";
|
|
19
|
+
import { useFirestore } from "./firebase/firebase-services.js";
|
|
20
|
+
import { useFirestoreCollection } from "./use-firestore-collection.js";
|
|
21
|
+
/**
|
|
22
|
+
* Every marketplace plugin that can run on one site (AGL-1014).
|
|
23
|
+
*
|
|
24
|
+
* The site's plugin switchboard used to derive this from `org.enabledPlugins`
|
|
25
|
+
* alone, and that field is EMPTY for most workspaces: `syncEnabledPlugins`
|
|
26
|
+
* appends a listing id only for an org that has already configured the
|
|
27
|
+
* switchboard, because an absent field means default-open and adding the first
|
|
28
|
+
* id would switch off every plugin not in it. So a workspace that never
|
|
29
|
+
* touched the built-in switches had its marketplace installs running on every
|
|
30
|
+
* site with nothing in the console listing them, and no way to switch one off
|
|
31
|
+
* for one site.
|
|
32
|
+
*
|
|
33
|
+
* The pins are the durable record, so they are the source here: org-tier pins
|
|
34
|
+
* apply to every site, host-tier pins to this one, and the ids already on
|
|
35
|
+
* `enabledPlugins` are unioned in so a listing whose pin this reader cannot
|
|
36
|
+
* see still gets a row rather than silently vanishing.
|
|
37
|
+
*/ export function useSiteMarketplacePlugins(orgId, hostId, /** Non-first-party ids already on the org's switchboard. */ switchboardIds = []) {
|
|
38
|
+
const firestore = useFirestore();
|
|
39
|
+
// Held at null while the scope is unknown, never `orgs/-pending-`
|
|
40
|
+
// (AGL-1440): installs are member-gated, so a sentinel id is a
|
|
41
|
+
// guaranteed-denied listen on every mount.
|
|
42
|
+
const { data: orgInstalls } = useFirestoreCollection(()=>orgId ? query(collection(firestore, 'orgs', orgId, 'installs'), limit(100)) : null, [
|
|
43
|
+
firestore,
|
|
44
|
+
orgId
|
|
45
|
+
], {
|
|
46
|
+
idField: '$id'
|
|
47
|
+
});
|
|
48
|
+
const { data: hostInstalls } = useFirestoreCollection(()=>hostId ? query(collection(firestore, 'hosts', hostId, 'installs'), limit(100)) : null, [
|
|
49
|
+
firestore,
|
|
50
|
+
hostId
|
|
51
|
+
], {
|
|
52
|
+
idField: '$id'
|
|
53
|
+
});
|
|
54
|
+
const switchboardKey = [
|
|
55
|
+
...switchboardIds
|
|
56
|
+
].sort().join('|');
|
|
57
|
+
return useMemo(()=>{
|
|
58
|
+
const byListing = new Map();
|
|
59
|
+
const add = (pin, scope)=>{
|
|
60
|
+
var _ref, _ref1, _ref2, _ref3;
|
|
61
|
+
var _pin_manifest;
|
|
62
|
+
const listingId = String((_ref = pin == null ? void 0 : pin.$id) != null ? _ref : '');
|
|
63
|
+
if (!listingId) return;
|
|
64
|
+
// A host pin is the more specific fact about THIS site, so it wins the
|
|
65
|
+
// scope; the display name and version are the same either way.
|
|
66
|
+
const existing = byListing.get(listingId);
|
|
67
|
+
if (existing && scope === 'org') return;
|
|
68
|
+
byListing.set(listingId, {
|
|
69
|
+
listingId,
|
|
70
|
+
displayName: String((_ref1 = (_ref2 = (_ref3 = pin == null ? void 0 : pin.displayName) != null ? _ref3 : pin == null ? void 0 : (_pin_manifest = pin.manifest) == null ? void 0 : _pin_manifest.name) != null ? _ref2 : pin == null ? void 0 : pin.pluginId) != null ? _ref1 : listingId),
|
|
71
|
+
version: (pin == null ? void 0 : pin.version) ? String(pin.version) : undefined,
|
|
72
|
+
scope
|
|
73
|
+
});
|
|
74
|
+
};
|
|
75
|
+
for (const pin of orgInstalls != null ? orgInstalls : [])add(pin, 'org');
|
|
76
|
+
for (const pin of hostInstalls != null ? hostInstalls : [])add(pin, 'site');
|
|
77
|
+
// An id the switchboard carries with no readable pin still gets a row,
|
|
78
|
+
// named by its id. A row that says less is better than a plugin running on
|
|
79
|
+
// the site with no row at all — which is the state this hook exists to end.
|
|
80
|
+
for (const listingId of switchboardKey ? switchboardKey.split('|') : []){
|
|
81
|
+
if (byListing.has(listingId)) continue;
|
|
82
|
+
byListing.set(listingId, {
|
|
83
|
+
listingId,
|
|
84
|
+
displayName: listingId,
|
|
85
|
+
scope: 'org'
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
return [
|
|
89
|
+
...byListing.values()
|
|
90
|
+
].sort((left, right)=>left.displayName.localeCompare(right.displayName));
|
|
91
|
+
}, [
|
|
92
|
+
orgInstalls,
|
|
93
|
+
hostInstalls,
|
|
94
|
+
switchboardKey
|
|
95
|
+
]);
|
|
96
|
+
}
|
|
97
|
+
export default useSiteMarketplacePlugins;
|
|
98
|
+
|
|
99
|
+
//# sourceMappingURL=use-site-marketplace-plugins.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/tenant/feature/instance/src/lib/hooks/use-site-marketplace-plugins.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 { collection, limit, query } from 'firebase/firestore'\nimport { useMemo } from 'react'\nimport { useFirestore } from './firebase/firebase-services'\nimport { useFirestoreCollection } from './use-firestore-collection'\n\n/** One marketplace plugin as a site sees it. */\nexport interface SiteMarketplacePlugin {\n /** The listing document id — the id `enabledPlugins` and the pins key by. */\n listingId: string\n /** The publisher's name for it, denormalized onto the pin. */\n displayName: string\n version?: string\n /**\n * `org` — pinned for every site in the workspace; `site` — pinned for this\n * one. The distinction changes what switching it off here means, so it is\n * carried rather than flattened.\n */\n scope: 'org' | 'site'\n}\n\n/**\n * Every marketplace plugin that can run on one site (AGL-1014).\n *\n * The site's plugin switchboard used to derive this from `org.enabledPlugins`\n * alone, and that field is EMPTY for most workspaces: `syncEnabledPlugins`\n * appends a listing id only for an org that has already configured the\n * switchboard, because an absent field means default-open and adding the first\n * id would switch off every plugin not in it. So a workspace that never\n * touched the built-in switches had its marketplace installs running on every\n * site with nothing in the console listing them, and no way to switch one off\n * for one site.\n *\n * The pins are the durable record, so they are the source here: org-tier pins\n * apply to every site, host-tier pins to this one, and the ids already on\n * `enabledPlugins` are unioned in so a listing whose pin this reader cannot\n * see still gets a row rather than silently vanishing.\n */\nexport function useSiteMarketplacePlugins(\n orgId: string,\n hostId: string,\n /** Non-first-party ids already on the org's switchboard. */\n switchboardIds: readonly string[] = [],\n): SiteMarketplacePlugin[] {\n const firestore = useFirestore()\n\n // Held at null while the scope is unknown, never `orgs/-pending-`\n // (AGL-1440): installs are member-gated, so a sentinel id is a\n // guaranteed-denied listen on every mount.\n const { data: orgInstalls } = useFirestoreCollection<any>(\n () =>\n orgId\n ? query(collection(firestore, 'orgs', orgId, 'installs'), limit(100))\n : null,\n [firestore, orgId],\n { idField: '$id' },\n )\n const { data: hostInstalls } = useFirestoreCollection<any>(\n () =>\n hostId\n ? query(collection(firestore, 'hosts', hostId, 'installs'), limit(100))\n : null,\n [firestore, hostId],\n { idField: '$id' },\n )\n\n const switchboardKey = [...switchboardIds].sort().join('|')\n\n return useMemo(() => {\n const byListing = new Map<string, SiteMarketplacePlugin>()\n const add = (pin: any, scope: 'org' | 'site') => {\n const listingId = String(pin?.$id ?? '')\n if (!listingId) return\n // A host pin is the more specific fact about THIS site, so it wins the\n // scope; the display name and version are the same either way.\n const existing = byListing.get(listingId)\n if (existing && scope === 'org') return\n byListing.set(listingId, {\n listingId,\n displayName: String(\n pin?.displayName ?? pin?.manifest?.name ?? pin?.pluginId ?? listingId,\n ),\n version: pin?.version ? String(pin.version) : undefined,\n scope,\n })\n }\n for (const pin of orgInstalls ?? []) add(pin, 'org')\n for (const pin of hostInstalls ?? []) add(pin, 'site')\n // An id the switchboard carries with no readable pin still gets a row,\n // named by its id. A row that says less is better than a plugin running on\n // the site with no row at all — which is the state this hook exists to end.\n for (const listingId of switchboardKey ? switchboardKey.split('|') : []) {\n if (byListing.has(listingId)) continue\n byListing.set(listingId, { listingId, displayName: listingId, scope: 'org' })\n }\n return [...byListing.values()].sort((left, right) =>\n left.displayName.localeCompare(right.displayName),\n )\n }, [orgInstalls, hostInstalls, switchboardKey])\n}\n\nexport default useSiteMarketplacePlugins\n"],"names":["collection","limit","query","useMemo","useFirestore","useFirestoreCollection","useSiteMarketplacePlugins","orgId","hostId","switchboardIds","firestore","data","orgInstalls","idField","hostInstalls","switchboardKey","sort","join","byListing","Map","add","pin","scope","listingId","String","$id","existing","get","set","displayName","manifest","name","pluginId","version","undefined","split","has","values","left","right","localeCompare"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;AAEA,SAASA,UAAU,EAAEC,KAAK,EAAEC,KAAK,QAAQ,qBAAoB;AAC7D,SAASC,OAAO,QAAQ,QAAO;AAC/B,SAASC,YAAY,QAAQ,kCAA8B;AAC3D,SAASC,sBAAsB,QAAQ,gCAA4B;AAiBnE;;;;;;;;;;;;;;;;CAgBC,GACD,OAAO,SAASC,0BACdC,KAAa,EACbC,MAAc,EACd,0DAA0D,GAC1DC,iBAAoC,EAAE;IAEtC,MAAMC,YAAYN;IAElB,kEAAkE;IAClE,+DAA+D;IAC/D,2CAA2C;IAC3C,MAAM,EAAEO,MAAMC,WAAW,EAAE,GAAGP,uBAC5B,IACEE,QACIL,MAAMF,WAAWU,WAAW,QAAQH,OAAO,aAAaN,MAAM,QAC9D,MACN;QAACS;QAAWH;KAAM,EAClB;QAAEM,SAAS;IAAM;IAEnB,MAAM,EAAEF,MAAMG,YAAY,EAAE,GAAGT,uBAC7B,IACEG,SACIN,MAAMF,WAAWU,WAAW,SAASF,QAAQ,aAAaP,MAAM,QAChE,MACN;QAACS;QAAWF;KAAO,EACnB;QAAEK,SAAS;IAAM;IAGnB,MAAME,iBAAiB;WAAIN;KAAe,CAACO,IAAI,GAAGC,IAAI,CAAC;IAEvD,OAAOd,QAAQ;QACb,MAAMe,YAAY,IAAIC;QACtB,MAAMC,MAAM,CAACC,KAAUC;sBAUjBD,OAAAA;gBAAoBA;YATxB,MAAME,YAAYC,eAAOH,uBAAAA,IAAKI,GAAG,mBAAI;YACrC,IAAI,CAACF,WAAW;YAChB,uEAAuE;YACvE,+DAA+D;YAC/D,MAAMG,WAAWR,UAAUS,GAAG,CAACJ;YAC/B,IAAIG,YAAYJ,UAAU,OAAO;YACjCJ,UAAUU,GAAG,CAACL,WAAW;gBACvBA;gBACAM,aAAaL,QACXH,SAAAA,iBAAAA,uBAAAA,IAAKQ,WAAW,oBAAIR,wBAAAA,gBAAAA,IAAKS,QAAQ,qBAAbT,cAAeU,IAAI,YAAvCV,QAA2CA,uBAAAA,IAAKW,QAAQ,YAAxDX,QAA4DE;gBAE9DU,SAASZ,CAAAA,uBAAAA,IAAKY,OAAO,IAAGT,OAAOH,IAAIY,OAAO,IAAIC;gBAC9CZ;YACF;QACF;QACA,KAAK,MAAMD,OAAOT,sBAAAA,cAAe,EAAE,CAAEQ,IAAIC,KAAK;QAC9C,KAAK,MAAMA,OAAOP,uBAAAA,eAAgB,EAAE,CAAEM,IAAIC,KAAK;QAC/C,uEAAuE;QACvE,2EAA2E;QAC3E,4EAA4E;QAC5E,KAAK,MAAME,aAAaR,iBAAiBA,eAAeoB,KAAK,CAAC,OAAO,EAAE,CAAE;YACvE,IAAIjB,UAAUkB,GAAG,CAACb,YAAY;YAC9BL,UAAUU,GAAG,CAACL,WAAW;gBAAEA;gBAAWM,aAAaN;gBAAWD,OAAO;YAAM;QAC7E;QACA,OAAO;eAAIJ,UAAUmB,MAAM;SAAG,CAACrB,IAAI,CAAC,CAACsB,MAAMC,QACzCD,KAAKT,WAAW,CAACW,aAAa,CAACD,MAAMV,WAAW;IAEpD,GAAG;QAACjB;QAAaE;QAAcC;KAAe;AAChD;AAEA,eAAeT,0BAAyB"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type ThemeLibraryEntry } from '@aglyn/aglyn/app-utils/theme-library';
|
|
2
|
+
/**
|
|
3
|
+
* A site's theme library (AGL-3404), live, by entry id. `undefined` until the
|
|
4
|
+
* first snapshot, so the picker does not list a library it has not read.
|
|
5
|
+
*/
|
|
6
|
+
export declare function useThemeLibrary(hostId: string | undefined): Record<string, ThemeLibraryEntry> | undefined;
|
|
7
|
+
export default useThemeLibrary;
|
|
@@ -0,0 +1,58 @@
|
|
|
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 { _ as _object_without_properties_loose } from "@swc/helpers/_/_object_without_properties_loose";
|
|
18
|
+
import { THEME_LIBRARY_COLLECTION } from "@aglyn/aglyn/app-utils/theme-library";
|
|
19
|
+
import { useFirestore } from "./firebase/firebase-services.js";
|
|
20
|
+
import { collection, limit, query } from "firebase/firestore";
|
|
21
|
+
import { useMemo } from "react";
|
|
22
|
+
import useFirestoreCollection from "./use-firestore-collection.js";
|
|
23
|
+
/**
|
|
24
|
+
* Well above what a site holds — `THEME_LIBRARY_MAX_CUSTOM` custom themes, a
|
|
25
|
+
* stash per built-in theme and one entry per marketplace theme — and a bound
|
|
26
|
+
* on the read however the collection grows.
|
|
27
|
+
*/ const LIBRARY_READ_LIMIT = 100;
|
|
28
|
+
/**
|
|
29
|
+
* A site's theme library (AGL-3404), live, by entry id. `undefined` until the
|
|
30
|
+
* first snapshot, so the picker does not list a library it has not read.
|
|
31
|
+
*/ export function useThemeLibrary(hostId) {
|
|
32
|
+
const firestore = useFirestore();
|
|
33
|
+
const { data, status } = useFirestoreCollection(()=>hostId ? query(collection(firestore, 'hosts', hostId, THEME_LIBRARY_COLLECTION), limit(LIBRARY_READ_LIMIT)) : null, [
|
|
34
|
+
firestore,
|
|
35
|
+
hostId
|
|
36
|
+
], {
|
|
37
|
+
idField: '$id'
|
|
38
|
+
});
|
|
39
|
+
return useMemo(()=>{
|
|
40
|
+
// A refused read lists nothing rather than holding the picker on a
|
|
41
|
+
// spinner: the built-in themes and the current one still render.
|
|
42
|
+
if (status === 'loading') return undefined;
|
|
43
|
+
const entries = {};
|
|
44
|
+
for (const _ref of data != null ? data : []){
|
|
45
|
+
const { $id } = _ref, entry = _object_without_properties_loose(_ref, [
|
|
46
|
+
"$id"
|
|
47
|
+
]);
|
|
48
|
+
entries[$id] = entry;
|
|
49
|
+
}
|
|
50
|
+
return entries;
|
|
51
|
+
}, [
|
|
52
|
+
data,
|
|
53
|
+
status
|
|
54
|
+
]);
|
|
55
|
+
}
|
|
56
|
+
export default useThemeLibrary;
|
|
57
|
+
|
|
58
|
+
//# sourceMappingURL=use-theme-library.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/tenant/feature/instance/src/lib/hooks/use-theme-library.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 THEME_LIBRARY_COLLECTION,\n type ThemeLibraryEntry,\n} from '@aglyn/aglyn/app-utils/theme-library'\nimport { useFirestore } from './firebase/firebase-services'\nimport { collection, limit, query } from 'firebase/firestore'\nimport { useMemo } from 'react'\nimport useFirestoreCollection from './use-firestore-collection'\n\n/**\n * Well above what a site holds — `THEME_LIBRARY_MAX_CUSTOM` custom themes, a\n * stash per built-in theme and one entry per marketplace theme — and a bound\n * on the read however the collection grows.\n */\nconst LIBRARY_READ_LIMIT = 100\n\n/**\n * A site's theme library (AGL-3404), live, by entry id. `undefined` until the\n * first snapshot, so the picker does not list a library it has not read.\n */\nexport function useThemeLibrary(\n hostId: string | undefined,\n): Record<string, ThemeLibraryEntry> | undefined {\n const firestore = useFirestore()\n const { data, status } = useFirestoreCollection<\n ThemeLibraryEntry & { $id: string }\n >(\n () =>\n hostId\n ? query(\n collection(firestore, 'hosts', hostId, THEME_LIBRARY_COLLECTION),\n limit(LIBRARY_READ_LIMIT),\n )\n : null,\n [firestore, hostId],\n { idField: '$id' },\n )\n return useMemo(() => {\n // A refused read lists nothing rather than holding the picker on a\n // spinner: the built-in themes and the current one still render.\n if (status === 'loading') return undefined\n const entries: Record<string, ThemeLibraryEntry> = {}\n for (const { $id, ...entry } of data ?? []) entries[$id] = entry\n return entries\n }, [data, status])\n}\n\nexport default useThemeLibrary\n"],"names":["THEME_LIBRARY_COLLECTION","useFirestore","collection","limit","query","useMemo","useFirestoreCollection","LIBRARY_READ_LIMIT","useThemeLibrary","hostId","firestore","data","status","idField","undefined","entries","$id","entry"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;AAEA,SACEA,wBAAwB,QAEnB,uCAAsC;AAC7C,SAASC,YAAY,QAAQ,kCAA8B;AAC3D,SAASC,UAAU,EAAEC,KAAK,EAAEC,KAAK,QAAQ,qBAAoB;AAC7D,SAASC,OAAO,QAAQ,QAAO;AAC/B,OAAOC,4BAA4B,gCAA4B;AAE/D;;;;CAIC,GACD,MAAMC,qBAAqB;AAE3B;;;CAGC,GACD,OAAO,SAASC,gBACdC,MAA0B;IAE1B,MAAMC,YAAYT;IAClB,MAAM,EAAEU,IAAI,EAAEC,MAAM,EAAE,GAAGN,uBAGvB,IACEG,SACIL,MACEF,WAAWQ,WAAW,SAASD,QAAQT,2BACvCG,MAAMI,uBAER,MACN;QAACG;QAAWD;KAAO,EACnB;QAAEI,SAAS;IAAM;IAEnB,OAAOR,QAAQ;QACb,mEAAmE;QACnE,iEAAiE;QACjE,IAAIO,WAAW,WAAW,OAAOE;QACjC,MAAMC,UAA6C,CAAC;QACpD,KAAK,MAAM,QAAqBJ,eAAAA,OAAQ,EAAE;kBAA/B,EAAEK,GAAG,EAAY,SAAPC;;;YAAuBF,OAAO,CAACC,IAAI,GAAGC;;QAC3D,OAAOF;IACT,GAAG;QAACJ;QAAMC;KAAO;AACnB;AAEA,eAAeJ,gBAAe"}
|