@aglyn/aglyn 1.0.0-beta.147 → 1.0.0-beta.150
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 +10 -10
- package/src/lib/app-utils/binding-token-catalog.js +9 -2
- package/src/lib/app-utils/binding-token-catalog.js.map +1 -1
- package/src/lib/app-utils/collection-entries.d.ts +52 -3
- package/src/lib/app-utils/collection-entries.js +98 -9
- package/src/lib/app-utils/collection-entries.js.map +1 -1
- package/src/lib/app-utils/console-routes.d.ts +2 -34
- package/src/lib/app-utils/console-routes.js +29 -11
- package/src/lib/app-utils/console-routes.js.map +1 -1
- package/src/lib/app-utils/contexts.d.ts +1 -0
- package/src/lib/app-utils/contexts.js +1 -0
- package/src/lib/app-utils/contexts.js.map +1 -1
- package/src/lib/app-utils/docs-help.generated.d.ts +13 -1
- package/src/lib/app-utils/docs-help.generated.js +37 -0
- package/src/lib/app-utils/docs-help.generated.js.map +1 -1
- package/src/lib/app-utils/docs-index.generated.js +1 -1
- package/src/lib/app-utils/docs-index.generated.js.map +1 -1
- package/src/lib/app-utils/media-src.d.ts +93 -0
- package/src/lib/app-utils/media-src.js +114 -0
- package/src/lib/app-utils/media-src.js.map +1 -0
- package/src/lib/app-utils/notifications.d.ts +136 -2
- package/src/lib/app-utils/notifications.js +139 -3
- package/src/lib/app-utils/notifications.js.map +1 -1
- package/src/lib/app-utils/page-header-help-context.d.ts +83 -0
- package/src/lib/app-utils/page-header-help-context.js +56 -0
- package/src/lib/app-utils/page-header-help-context.js.map +1 -0
- package/src/lib/app-utils/release-flags.js +5 -4
- package/src/lib/app-utils/release-flags.js.map +1 -1
- package/src/lib/foundation/definitions/host-content-collections.d.ts +42 -40
- package/src/lib/foundation/definitions/host-content-collections.js +47 -109
- package/src/lib/foundation/definitions/host-content-collections.js.map +1 -1
- package/src/lib/plugin-manager/feature-plugins.d.ts +121 -22
- package/src/lib/plugin-manager/feature-plugins.js +41 -18
- package/src/lib/plugin-manager/feature-plugins.js.map +1 -1
- package/src/lib/plugin-manager/first-party-plugins.generated.d.ts +13 -0
- package/src/lib/plugin-manager/first-party-plugins.generated.js +257 -0
- package/src/lib/plugin-manager/first-party-plugins.generated.js.map +1 -1
- package/src/lib/plugin-manager/plugin-contact-capture.d.ts +16 -0
- package/src/lib/plugin-manager/plugin-contact-capture.js.map +1 -1
- package/src/lib/plugin-manager/plugin-host-collections.d.ts +27 -5
- package/src/lib/plugin-manager/plugin-host-collections.js +25 -16
- package/src/lib/plugin-manager/plugin-host-collections.js.map +1 -1
- package/src/lib/plugin-manager/plugin-media-publish.d.ts +88 -0
- package/src/lib/plugin-manager/plugin-media-publish.js +118 -0
- package/src/lib/plugin-manager/plugin-media-publish.js.map +1 -0
- package/src/lib/plugin-manager/plugin-org-capacity.d.ts +57 -0
- package/src/lib/plugin-manager/plugin-org-capacity.js +68 -0
- package/src/lib/plugin-manager/plugin-org-capacity.js.map +1 -0
- package/src/lib/plugin-manager/plugin-route-metadata.d.ts +141 -0
- package/src/lib/plugin-manager/plugin-route-metadata.js +140 -0
- package/src/lib/plugin-manager/plugin-route-metadata.js.map +1 -0
- package/src/lib/plugin-manager/record-captured-contact.d.ts +89 -0
- package/src/lib/plugin-manager/record-captured-contact.js +128 -0
- package/src/lib/plugin-manager/record-captured-contact.js.map +1 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* A usable `src` for a picked media asset.
|
|
19
|
+
*
|
|
20
|
+
* Lifted out of the listing detail editor when the publish form gained a
|
|
21
|
+
* media picker of its own (AGL-1080).
|
|
22
|
+
*
|
|
23
|
+
* NO LONGER THE WRITER FOR MARKDOWN BODIES (AGL-1705). This helper used to
|
|
24
|
+
* justify prefixing the origin by saying the markdown "renders elsewhere".
|
|
25
|
+
* That claim was checked and is false: the marketplace README's only renderer
|
|
26
|
+
* is the console's own `marketplaceListing` slot, and `apps/tenant` does not
|
|
27
|
+
* import the marketplace plugin. The two README writers and the blog body now
|
|
28
|
+
* store `mediaNodeSrc`'s `media:` reference, which `resolveMediaSrc` turns
|
|
29
|
+
* back into this same `/api/media/cdn/…` path at render — without freezing
|
|
30
|
+
* either the origin or the route shape into the document.
|
|
31
|
+
*
|
|
32
|
+
* What is left for this helper is the listing's image FIELDS —
|
|
33
|
+
* `previewImageUrl`, `logoUrl`, `screenshots` — read out of band by
|
|
34
|
+
* `og:image` and by `resolveSocialImage`, where an absolute URL is the point
|
|
35
|
+
* (AGL-1701). The origin prefix stays for them.
|
|
36
|
+
*/
|
|
37
|
+
export declare function mediaSrc(media: {
|
|
38
|
+
url?: string;
|
|
39
|
+
cdnPath?: string;
|
|
40
|
+
}): string;
|
|
41
|
+
/**
|
|
42
|
+
* A `src` for a THUMBNAIL — a grid tile, a picker cell, a preview strip.
|
|
43
|
+
*
|
|
44
|
+
* Separate from {@link mediaSrc} because the two answer different questions.
|
|
45
|
+
* `mediaSrc` produces a URL that gets *persisted* (markdown bodies, listing
|
|
46
|
+
* images), so it must stay width-free. This one produces a URL that is only
|
|
47
|
+
* ever rendered, so it can ask for the smallest generated WebP variant.
|
|
48
|
+
*
|
|
49
|
+
* The DAM grid read `media.url` directly, which is the raw
|
|
50
|
+
* `firebasestorage.googleapis.com` download URL: every tile fetched the
|
|
51
|
+
* FULL-SIZE original straight from Cloud Storage, bypassing both the WebP
|
|
52
|
+
* variants (AGL-175) and the Vercel edge cache. A library page of 24 assets
|
|
53
|
+
* at ~200 KB each is ~4.8 MB of Storage egress per view, re-fetched by every
|
|
54
|
+
* viewer, where the 320px variants are ~15 KB each.
|
|
55
|
+
*
|
|
56
|
+
* `?w=` degrades safely: `serveMediaCdn` only serves a variant when the
|
|
57
|
+
* asset's `variants` array actually contains that width, and otherwise
|
|
58
|
+
* serves the original — so an SVG, a small logo, or an asset uploaded before
|
|
59
|
+
* variants existed still renders, just without the saving.
|
|
60
|
+
*/
|
|
61
|
+
export declare function mediaThumbnailSrc(media: {
|
|
62
|
+
url?: string;
|
|
63
|
+
cdnPath?: string;
|
|
64
|
+
}, width: number): string;
|
|
65
|
+
/**
|
|
66
|
+
* A `src` for a VIDEO's poster still, or `undefined` when it has none
|
|
67
|
+
* (AGL-2742).
|
|
68
|
+
*
|
|
69
|
+
* The DAM grid drew a video tile as a `<video src>` pointing at the master.
|
|
70
|
+
* A browser paints a first frame by fetching enough of the file to decode
|
|
71
|
+
* one, so a folder of eight clips was tens of megabytes of Storage egress
|
|
72
|
+
* per view — the same defect {@link mediaThumbnailSrc} was written to fix for
|
|
73
|
+
* images, on the type where each tile costs a hundred times more. And unlike
|
|
74
|
+
* an image tile it could not be helped by a variant, because until now a
|
|
75
|
+
* video had nothing smaller to serve.
|
|
76
|
+
*
|
|
77
|
+
* Returns `undefined` rather than a fallback URL on purpose: the caller has
|
|
78
|
+
* to branch anyway (a `<video>` with no poster still needs `preload="none"`
|
|
79
|
+
* so it stops fetching), and an "always answers" helper would hand back a URL
|
|
80
|
+
* that 404s for every video uploaded before this shipped.
|
|
81
|
+
*
|
|
82
|
+
* Mirrors `Aglyn.mediaPosterSrc` for a media DOCUMENT rather than a stored
|
|
83
|
+
* `media:` reference — the console holds documents, the renderer holds
|
|
84
|
+
* references, and neither can use the other's input.
|
|
85
|
+
*/
|
|
86
|
+
export declare function mediaPosterThumbnailSrc(media: {
|
|
87
|
+
url?: string;
|
|
88
|
+
cdnPath?: string;
|
|
89
|
+
poster?: {
|
|
90
|
+
variants?: number[];
|
|
91
|
+
} | null;
|
|
92
|
+
}, width?: number): string | undefined;
|
|
93
|
+
export default mediaSrc;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ /**
|
|
17
|
+
* A usable `src` for a picked media asset.
|
|
18
|
+
*
|
|
19
|
+
* Lifted out of the listing detail editor when the publish form gained a
|
|
20
|
+
* media picker of its own (AGL-1080).
|
|
21
|
+
*
|
|
22
|
+
* NO LONGER THE WRITER FOR MARKDOWN BODIES (AGL-1705). This helper used to
|
|
23
|
+
* justify prefixing the origin by saying the markdown "renders elsewhere".
|
|
24
|
+
* That claim was checked and is false: the marketplace README's only renderer
|
|
25
|
+
* is the console's own `marketplaceListing` slot, and `apps/tenant` does not
|
|
26
|
+
* import the marketplace plugin. The two README writers and the blog body now
|
|
27
|
+
* store `mediaNodeSrc`'s `media:` reference, which `resolveMediaSrc` turns
|
|
28
|
+
* back into this same `/api/media/cdn/…` path at render — without freezing
|
|
29
|
+
* either the origin or the route shape into the document.
|
|
30
|
+
*
|
|
31
|
+
* What is left for this helper is the listing's image FIELDS —
|
|
32
|
+
* `previewImageUrl`, `logoUrl`, `screenshots` — read out of band by
|
|
33
|
+
* `og:image` and by `resolveSocialImage`, where an absolute URL is the point
|
|
34
|
+
* (AGL-1701). The origin prefix stays for them.
|
|
35
|
+
*/ export function mediaSrc(media) {
|
|
36
|
+
// `cdnPath` FIRST (AGL-1215). It is keyed by media id, so it survives a
|
|
37
|
+
// folder move — which physically copies the object, rewrites `url` and
|
|
38
|
+
// deletes the original, permanently breaking anything holding the old
|
|
39
|
+
// raw URL. Preferring `url` meant this helper emitted the fragile form
|
|
40
|
+
// for every paid org, where both fields are always populated. `url`
|
|
41
|
+
// stays the fallback for free-tier orgs (`cdnPath` is a paid `mediaCdn`
|
|
42
|
+
// entitlement) and legacy uploads that predate it.
|
|
43
|
+
if (media.cdnPath) return typeof window === 'undefined' ? media.cdnPath : `${window.location.origin}${media.cdnPath}`;
|
|
44
|
+
if (media.url) return media.url;
|
|
45
|
+
return '';
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* A `src` for a THUMBNAIL — a grid tile, a picker cell, a preview strip.
|
|
49
|
+
*
|
|
50
|
+
* Separate from {@link mediaSrc} because the two answer different questions.
|
|
51
|
+
* `mediaSrc` produces a URL that gets *persisted* (markdown bodies, listing
|
|
52
|
+
* images), so it must stay width-free. This one produces a URL that is only
|
|
53
|
+
* ever rendered, so it can ask for the smallest generated WebP variant.
|
|
54
|
+
*
|
|
55
|
+
* The DAM grid read `media.url` directly, which is the raw
|
|
56
|
+
* `firebasestorage.googleapis.com` download URL: every tile fetched the
|
|
57
|
+
* FULL-SIZE original straight from Cloud Storage, bypassing both the WebP
|
|
58
|
+
* variants (AGL-175) and the Vercel edge cache. A library page of 24 assets
|
|
59
|
+
* at ~200 KB each is ~4.8 MB of Storage egress per view, re-fetched by every
|
|
60
|
+
* viewer, where the 320px variants are ~15 KB each.
|
|
61
|
+
*
|
|
62
|
+
* `?w=` degrades safely: `serveMediaCdn` only serves a variant when the
|
|
63
|
+
* asset's `variants` array actually contains that width, and otherwise
|
|
64
|
+
* serves the original — so an SVG, a small logo, or an asset uploaded before
|
|
65
|
+
* variants existed still renders, just without the saving.
|
|
66
|
+
*/ export function mediaThumbnailSrc(media, width) {
|
|
67
|
+
const cdnPath = media.cdnPath;
|
|
68
|
+
// No `cdnPath` means a free-tier or private asset, and a width parameter
|
|
69
|
+
// on a raw storage URL means nothing — appending one would only fork the
|
|
70
|
+
// browser cache for identical bytes.
|
|
71
|
+
if (!cdnPath) return mediaSrc(media);
|
|
72
|
+
// A signed private URL already carries `?exp=&sig=`; a second `?` makes a
|
|
73
|
+
// path the route rejects outright.
|
|
74
|
+
if (cdnPath.includes('?')) return mediaSrc(media);
|
|
75
|
+
return `${mediaSrc(media)}?w=${width}`;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* A `src` for a VIDEO's poster still, or `undefined` when it has none
|
|
79
|
+
* (AGL-2742).
|
|
80
|
+
*
|
|
81
|
+
* The DAM grid drew a video tile as a `<video src>` pointing at the master.
|
|
82
|
+
* A browser paints a first frame by fetching enough of the file to decode
|
|
83
|
+
* one, so a folder of eight clips was tens of megabytes of Storage egress
|
|
84
|
+
* per view — the same defect {@link mediaThumbnailSrc} was written to fix for
|
|
85
|
+
* images, on the type where each tile costs a hundred times more. And unlike
|
|
86
|
+
* an image tile it could not be helped by a variant, because until now a
|
|
87
|
+
* video had nothing smaller to serve.
|
|
88
|
+
*
|
|
89
|
+
* Returns `undefined` rather than a fallback URL on purpose: the caller has
|
|
90
|
+
* to branch anyway (a `<video>` with no poster still needs `preload="none"`
|
|
91
|
+
* so it stops fetching), and an "always answers" helper would hand back a URL
|
|
92
|
+
* that 404s for every video uploaded before this shipped.
|
|
93
|
+
*
|
|
94
|
+
* Mirrors `Aglyn.mediaPosterSrc` for a media DOCUMENT rather than a stored
|
|
95
|
+
* `media:` reference — the console holds documents, the renderer holds
|
|
96
|
+
* references, and neither can use the other's input.
|
|
97
|
+
*/ export function mediaPosterThumbnailSrc(media, width) {
|
|
98
|
+
var _media_poster_variants;
|
|
99
|
+
// A poster is only reachable through the CDN route, so a free-tier or
|
|
100
|
+
// private asset has no URL that could serve one — the same reason
|
|
101
|
+
// `mediaThumbnailSrc` gives up on `?w=` there.
|
|
102
|
+
if (!media.poster || !media.cdnPath || media.cdnPath.includes('?')) {
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
const variants = (_media_poster_variants = media.poster.variants) != null ? _media_poster_variants : [];
|
|
106
|
+
// Only a width that was actually generated. `serveMediaCdn` would fall
|
|
107
|
+
// back to the full poster for any other, which works and defeats the point
|
|
108
|
+
// — a 1920px still in a 116px tile.
|
|
109
|
+
const narrowed = width && variants.includes(width) ? `&w=${width}` : '';
|
|
110
|
+
return `${mediaSrc(media)}?poster=1${narrowed}`;
|
|
111
|
+
}
|
|
112
|
+
export default mediaSrc;
|
|
113
|
+
|
|
114
|
+
//# sourceMappingURL=media-src.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/media-src.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\n/**\n * A usable `src` for a picked media asset.\n *\n * Lifted out of the listing detail editor when the publish form gained a\n * media picker of its own (AGL-1080).\n *\n * NO LONGER THE WRITER FOR MARKDOWN BODIES (AGL-1705). This helper used to\n * justify prefixing the origin by saying the markdown \"renders elsewhere\".\n * That claim was checked and is false: the marketplace README's only renderer\n * is the console's own `marketplaceListing` slot, and `apps/tenant` does not\n * import the marketplace plugin. The two README writers and the blog body now\n * store `mediaNodeSrc`'s `media:` reference, which `resolveMediaSrc` turns\n * back into this same `/api/media/cdn/…` path at render — without freezing\n * either the origin or the route shape into the document.\n *\n * What is left for this helper is the listing's image FIELDS —\n * `previewImageUrl`, `logoUrl`, `screenshots` — read out of band by\n * `og:image` and by `resolveSocialImage`, where an absolute URL is the point\n * (AGL-1701). The origin prefix stays for them.\n */\nexport function mediaSrc(media: {\n url?: string\n cdnPath?: string\n}): string {\n // `cdnPath` FIRST (AGL-1215). It is keyed by media id, so it survives a\n // folder move — which physically copies the object, rewrites `url` and\n // deletes the original, permanently breaking anything holding the old\n // raw URL. Preferring `url` meant this helper emitted the fragile form\n // for every paid org, where both fields are always populated. `url`\n // stays the fallback for free-tier orgs (`cdnPath` is a paid `mediaCdn`\n // entitlement) and legacy uploads that predate it.\n if (media.cdnPath)\n return typeof window === 'undefined'\n ? media.cdnPath\n : `${window.location.origin}${media.cdnPath}`\n if (media.url) return media.url\n return ''\n}\n\n/**\n * A `src` for a THUMBNAIL — a grid tile, a picker cell, a preview strip.\n *\n * Separate from {@link mediaSrc} because the two answer different questions.\n * `mediaSrc` produces a URL that gets *persisted* (markdown bodies, listing\n * images), so it must stay width-free. This one produces a URL that is only\n * ever rendered, so it can ask for the smallest generated WebP variant.\n *\n * The DAM grid read `media.url` directly, which is the raw\n * `firebasestorage.googleapis.com` download URL: every tile fetched the\n * FULL-SIZE original straight from Cloud Storage, bypassing both the WebP\n * variants (AGL-175) and the Vercel edge cache. A library page of 24 assets\n * at ~200 KB each is ~4.8 MB of Storage egress per view, re-fetched by every\n * viewer, where the 320px variants are ~15 KB each.\n *\n * `?w=` degrades safely: `serveMediaCdn` only serves a variant when the\n * asset's `variants` array actually contains that width, and otherwise\n * serves the original — so an SVG, a small logo, or an asset uploaded before\n * variants existed still renders, just without the saving.\n */\nexport function mediaThumbnailSrc(\n media: { url?: string; cdnPath?: string },\n width: number,\n): string {\n const cdnPath = media.cdnPath\n // No `cdnPath` means a free-tier or private asset, and a width parameter\n // on a raw storage URL means nothing — appending one would only fork the\n // browser cache for identical bytes.\n if (!cdnPath) return mediaSrc(media)\n // A signed private URL already carries `?exp=&sig=`; a second `?` makes a\n // path the route rejects outright.\n if (cdnPath.includes('?')) return mediaSrc(media)\n return `${mediaSrc(media)}?w=${width}`\n}\n\n/**\n * A `src` for a VIDEO's poster still, or `undefined` when it has none\n * (AGL-2742).\n *\n * The DAM grid drew a video tile as a `<video src>` pointing at the master.\n * A browser paints a first frame by fetching enough of the file to decode\n * one, so a folder of eight clips was tens of megabytes of Storage egress\n * per view — the same defect {@link mediaThumbnailSrc} was written to fix for\n * images, on the type where each tile costs a hundred times more. And unlike\n * an image tile it could not be helped by a variant, because until now a\n * video had nothing smaller to serve.\n *\n * Returns `undefined` rather than a fallback URL on purpose: the caller has\n * to branch anyway (a `<video>` with no poster still needs `preload=\"none\"`\n * so it stops fetching), and an \"always answers\" helper would hand back a URL\n * that 404s for every video uploaded before this shipped.\n *\n * Mirrors `Aglyn.mediaPosterSrc` for a media DOCUMENT rather than a stored\n * `media:` reference — the console holds documents, the renderer holds\n * references, and neither can use the other's input.\n */\nexport function mediaPosterThumbnailSrc(\n media: {\n url?: string\n cdnPath?: string\n poster?: { variants?: number[] } | null\n },\n width?: number,\n): string | undefined {\n // A poster is only reachable through the CDN route, so a free-tier or\n // private asset has no URL that could serve one — the same reason\n // `mediaThumbnailSrc` gives up on `?w=` there.\n if (!media.poster || !media.cdnPath || media.cdnPath.includes('?')) {\n return undefined\n }\n const variants = media.poster.variants ?? []\n // Only a width that was actually generated. `serveMediaCdn` would fall\n // back to the full poster for any other, which works and defeats the point\n // — a 1920px still in a 116px tile.\n const narrowed = width && variants.includes(width) ? `&w=${width}` : ''\n return `${mediaSrc(media)}?poster=1${narrowed}`\n}\n\nexport default mediaSrc\n"],"names":["mediaSrc","media","cdnPath","window","location","origin","url","mediaThumbnailSrc","width","includes","mediaPosterThumbnailSrc","poster","undefined","variants","narrowed"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;CAmBC,GACD,OAAO,SAASA,SAASC,KAGxB;IACC,wEAAwE;IACxE,uEAAuE;IACvE,sEAAsE;IACtE,uEAAuE;IACvE,oEAAoE;IACpE,wEAAwE;IACxE,mDAAmD;IACnD,IAAIA,MAAMC,OAAO,EACf,OAAO,OAAOC,WAAW,cACrBF,MAAMC,OAAO,GACb,GAAGC,OAAOC,QAAQ,CAACC,MAAM,GAAGJ,MAAMC,OAAO,EAAE;IACjD,IAAID,MAAMK,GAAG,EAAE,OAAOL,MAAMK,GAAG;IAC/B,OAAO;AACT;AAEA;;;;;;;;;;;;;;;;;;;CAmBC,GACD,OAAO,SAASC,kBACdN,KAAyC,EACzCO,KAAa;IAEb,MAAMN,UAAUD,MAAMC,OAAO;IAC7B,yEAAyE;IACzE,yEAAyE;IACzE,qCAAqC;IACrC,IAAI,CAACA,SAAS,OAAOF,SAASC;IAC9B,0EAA0E;IAC1E,mCAAmC;IACnC,IAAIC,QAAQO,QAAQ,CAAC,MAAM,OAAOT,SAASC;IAC3C,OAAO,GAAGD,SAASC,OAAO,GAAG,EAAEO,OAAO;AACxC;AAEA;;;;;;;;;;;;;;;;;;;;CAoBC,GACD,OAAO,SAASE,wBACdT,KAIC,EACDO,KAAc;QAQGP;IANjB,sEAAsE;IACtE,kEAAkE;IAClE,+CAA+C;IAC/C,IAAI,CAACA,MAAMU,MAAM,IAAI,CAACV,MAAMC,OAAO,IAAID,MAAMC,OAAO,CAACO,QAAQ,CAAC,MAAM;QAClE,OAAOG;IACT;IACA,MAAMC,YAAWZ,yBAAAA,MAAMU,MAAM,CAACE,QAAQ,YAArBZ,yBAAyB,EAAE;IAC5C,uEAAuE;IACvE,2EAA2E;IAC3E,oCAAoC;IACpC,MAAMa,WAAWN,SAASK,SAASJ,QAAQ,CAACD,SAAS,CAAC,GAAG,EAAEA,OAAO,GAAG;IACrE,OAAO,GAAGR,SAASC,OAAO,SAAS,EAAEa,UAAU;AACjD;AAEA,eAAed,SAAQ"}
|
|
@@ -22,7 +22,7 @@ import type { ITimestamp } from '@aglyn/shared-util-timestamp';
|
|
|
22
22
|
* their owner. Types follow the common SaaS taxonomy so the console can
|
|
23
23
|
* icon/group them without a registry.
|
|
24
24
|
*/
|
|
25
|
-
export type AglynNotificationType = 'billing.invoice' | 'billing.paymentFailed' | 'billing.subscriptionCanceled' | 'billing.usage' | 'team.invite' | 'team.roleChanged' | 'team.hostAccessGranted' | 'content.formSubmission' | 'content.booking' | 'content.order' | 'content.lowStock' | 'content.taskAssigned' | 'content.taskReminder' | 'content.contactAssigned' | 'content.leadAssigned' | 'content.crmDailyDigest' | 'content.insightsDigest' | 'marketplace.review' | 'support.ticketOpened' | 'support.ticketReply' | 'system.announcement' | 'system.pluginVerifierRegression' | 'system.signInMethodRemoved' | 'system.scopeDrift' | 'system.ssoDomainUnverified' | 'system.formSubmissionsPaused' | 'system.visitorRecordsPaused' | 'system.abuseReportUrgent' | 'system.dmcaCounterNotice' | 'system.bandwidthCeilingTripped' | 'system.bandwidthCapEngaged' | 'system.billingWebhookHalfApplied' | 'system.disputeUnattributed';
|
|
25
|
+
export type AglynNotificationType = 'billing.invoice' | 'billing.paymentFailed' | 'billing.subscriptionCanceled' | 'billing.usage' | 'team.invite' | 'team.roleChanged' | 'team.hostAccessGranted' | 'content.formSubmission' | 'content.booking' | 'content.order' | 'content.lowStock' | 'content.taskAssigned' | 'content.taskReminder' | 'content.contactAssigned' | 'content.leadAssigned' | 'content.crmDailyDigest' | 'content.insightsDigest' | 'marketplace.review' | 'support.ticketOpened' | 'support.ticketReply' | 'system.announcement' | 'system.pluginVerifierRegression' | 'system.signInMethodRemoved' | 'system.scopeDrift' | 'system.ssoDomainUnverified' | 'system.formSubmissionsPaused' | 'system.visitorRecordsPaused' | 'system.abuseReportUrgent' | 'system.dmcaCounterNotice' | 'system.bandwidthCeilingTripped' | 'system.bandwidthCapEngaged' | 'system.billingWebhookHalfApplied' | 'system.disputeUnattributed' | 'staff.userSignedUp' | 'staff.orgCreated';
|
|
26
26
|
export interface AglynNotification {
|
|
27
27
|
$id?: string;
|
|
28
28
|
type: AglynNotificationType;
|
|
@@ -38,8 +38,17 @@ export interface AglynNotification {
|
|
|
38
38
|
}
|
|
39
39
|
export declare const NOTIFICATION_TYPE_LABELS: Record<AglynNotificationType, string>;
|
|
40
40
|
/** Preference buckets (AGL-267): the prefix before the dot. */
|
|
41
|
-
export type NotificationCategory = 'billing' | 'team' | 'content' | 'marketplace' | 'support' | 'system';
|
|
41
|
+
export type NotificationCategory = 'billing' | 'team' | 'content' | 'marketplace' | 'support' | 'system' | 'staff';
|
|
42
42
|
export declare const NOTIFICATION_CATEGORY_LABELS: Record<NotificationCategory, string>;
|
|
43
|
+
/**
|
|
44
|
+
* The categories only staff can receive (AGL-3225).
|
|
45
|
+
*
|
|
46
|
+
* The settings page hides these rows from everybody else, because a switch
|
|
47
|
+
* for mail that can never arrive is a promise the product does not keep. It
|
|
48
|
+
* is presentation only: `notifyStaff` is what decides the audience, and it
|
|
49
|
+
* enumerates the `staff` claim rather than reading this.
|
|
50
|
+
*/
|
|
51
|
+
export declare const STAFF_NOTIFICATION_CATEGORIES: ReadonlySet<NotificationCategory>;
|
|
43
52
|
export declare function notificationCategory(type: AglynNotificationType | string): NotificationCategory;
|
|
44
53
|
/**
|
|
45
54
|
* Per-user mute map stored at `users/{uid}.notificationPrefs`
|
|
@@ -69,3 +78,128 @@ export declare function crmDailyDigestEnabled(prefs: Record<string, boolean> | n
|
|
|
69
78
|
export declare const INSIGHT_DIGESTS_FIELD = "insightDigests";
|
|
70
79
|
/** Whether a person asked for a workspace's weekly insights. */
|
|
71
80
|
export declare function insightDigestSubscribed(value: Record<string, boolean> | null | undefined, orgId: string): boolean;
|
|
81
|
+
/**
|
|
82
|
+
* The channels a notification can travel on (AGL-3223).
|
|
83
|
+
*
|
|
84
|
+
* `console` is the feed at `/manage/notifications` and the app-bar dropdown
|
|
85
|
+
* that reads it — the only channel that existed. `email` is the message the
|
|
86
|
+
* fan-out sends beside that doc when the recipient asked for one.
|
|
87
|
+
*/
|
|
88
|
+
export type NotificationChannel = 'console' | 'email';
|
|
89
|
+
/**
|
|
90
|
+
* One scope's answer for one category. **Tri-state on purpose**: `true` and
|
|
91
|
+
* `false` decide, and an ABSENT key inherits from the scope above.
|
|
92
|
+
*
|
|
93
|
+
* Inheritance is the whole reason this is a partial rather than a pair of
|
|
94
|
+
* booleans. A person who wants form submissions from one busy site and not
|
|
95
|
+
* from the other five has to be able to say that about the one site without
|
|
96
|
+
* restating their answer for every other category at every other scope — and
|
|
97
|
+
* a two-valued leaf cannot express "I have not said", so every override would
|
|
98
|
+
* have to be written out in full and would then stop tracking the account
|
|
99
|
+
* default it was never meant to detach from.
|
|
100
|
+
*/
|
|
101
|
+
export interface NotificationChannelPrefs {
|
|
102
|
+
console?: boolean;
|
|
103
|
+
email?: boolean;
|
|
104
|
+
}
|
|
105
|
+
export type NotificationCategoryPrefs = Partial<Record<NotificationCategory, NotificationChannelPrefs>>;
|
|
106
|
+
/**
|
|
107
|
+
* Per-scope, per-channel notification preferences (AGL-3223), stored at
|
|
108
|
+
* `users/{uid}.notificationSettings`.
|
|
109
|
+
*
|
|
110
|
+
* Three layers, narrowest first when resolving: the SITE a notification
|
|
111
|
+
* concerns, then the WORKSPACE, then the account. Anything none of them
|
|
112
|
+
* answers falls to {@link NOTIFICATION_CHANNEL_DEFAULTS}.
|
|
113
|
+
*
|
|
114
|
+
* ON THE USER DOCUMENT, not on `orgs/{orgId}/members/{uid}`, and that is a
|
|
115
|
+
* cost decision rather than a modelling preference. `notifyUsers` already
|
|
116
|
+
* does exactly one `getAll` over the recipients' user docs to read their
|
|
117
|
+
* category mutes, so every layer living here means per-site preferences are
|
|
118
|
+
* read for free on the fan-out's hot path. The member row would add a read
|
|
119
|
+
* per recipient per notification to answer a question the document already in
|
|
120
|
+
* hand could have answered.
|
|
121
|
+
*/
|
|
122
|
+
export interface NotificationSettings {
|
|
123
|
+
account?: NotificationCategoryPrefs;
|
|
124
|
+
/** Keyed by org id. */
|
|
125
|
+
orgs?: Record<string, NotificationCategoryPrefs>;
|
|
126
|
+
/** Keyed by host id — a site's own answer, narrower than its workspace's. */
|
|
127
|
+
hosts?: Record<string, NotificationCategoryPrefs>;
|
|
128
|
+
}
|
|
129
|
+
export declare const NOTIFICATION_SETTINGS_FIELD = "notificationSettings";
|
|
130
|
+
/**
|
|
131
|
+
* What a category does when nobody has said otherwise.
|
|
132
|
+
*
|
|
133
|
+
* Console on, email OFF, everywhere. Email defaults off because the inbox is
|
|
134
|
+
* not ours to fill: the product already sends transactional mail nobody opted
|
|
135
|
+
* into — welcome, verification, invites, dunning, usage alerts — and every one
|
|
136
|
+
* of those leaves on the same domain a customer's password reset depends on.
|
|
137
|
+
* Turning a busy site's form submissions into mail by default would put that
|
|
138
|
+
* domain's reputation behind traffic the recipient never asked for.
|
|
139
|
+
*
|
|
140
|
+
* Exhaustive `Record` deliberately: a new {@link NotificationCategory} is a
|
|
141
|
+
* compile error here until somebody decides what it does by default, which is
|
|
142
|
+
* the one question a new category must not be able to ship without answering.
|
|
143
|
+
*/
|
|
144
|
+
export declare const NOTIFICATION_CHANNEL_DEFAULTS: Record<NotificationCategory, Record<NotificationChannel, boolean>>;
|
|
145
|
+
/**
|
|
146
|
+
* The types that send their OWN email and must never be mailed again by the
|
|
147
|
+
* generic channel (AGL-3224).
|
|
148
|
+
*
|
|
149
|
+
* Both digests compose a message the fan-out could not reproduce — a day's
|
|
150
|
+
* owed tasks, a week's figures — and send it from their own route under their
|
|
151
|
+
* own switch (`digestPrefs.crmDaily`, `insightDigests.{orgId}`). A recipient
|
|
152
|
+
* who switches the `content` email channel on would otherwise receive the
|
|
153
|
+
* digest twice: once as the digest, once as a one-line "Daily CRM digest"
|
|
154
|
+
* notification saying that the digest happened.
|
|
155
|
+
*/
|
|
156
|
+
export declare const NOTIFICATION_SELF_SENT_EMAIL_TYPES: ReadonlySet<string>;
|
|
157
|
+
/**
|
|
158
|
+
* Whether a channel is on for one notification, at its own scope.
|
|
159
|
+
*
|
|
160
|
+
* Resolution order, first answer wins: the notification's SITE, its
|
|
161
|
+
* WORKSPACE, the account, then {@link NOTIFICATION_CHANNEL_DEFAULTS}. A scope
|
|
162
|
+
* that holds no entry for the category, or an entry with the channel key
|
|
163
|
+
* absent, does not answer — see {@link NotificationChannelPrefs}.
|
|
164
|
+
*
|
|
165
|
+
* `legacyPrefs` is the flat `notificationPrefs` mute map this replaces
|
|
166
|
+
* (AGL-267), and it sits BELOW the account layer rather than beside it.
|
|
167
|
+
* Nothing migrates it: a person who never opens the new settings page keeps
|
|
168
|
+
* being governed by the mutes they set years ago, and the first thing they do
|
|
169
|
+
* set on the account layer overrides the old map for that category without
|
|
170
|
+
* disturbing the rest of it. It answers for `console` only, because the map
|
|
171
|
+
* predates there being a second channel and reading a console mute as an
|
|
172
|
+
* email preference would be inventing an answer its author never gave.
|
|
173
|
+
*/
|
|
174
|
+
export declare function notificationChannelEnabled(settings: NotificationSettings | null | undefined, channel: NotificationChannel, type: AglynNotificationType | string, scope?: {
|
|
175
|
+
orgId?: string | null;
|
|
176
|
+
hostId?: string | null;
|
|
177
|
+
}, legacyPrefs?: Record<string, boolean> | null): boolean;
|
|
178
|
+
/**
|
|
179
|
+
* What one scope says, with no inheritance applied — what the settings page
|
|
180
|
+
* renders as Inherit / On / Off, and `undefined` is Inherit.
|
|
181
|
+
*
|
|
182
|
+
* `scope` names the layer directly rather than being derived from a
|
|
183
|
+
* notification, because the page edits a layer whether or not anything has
|
|
184
|
+
* ever arrived from it.
|
|
185
|
+
*/
|
|
186
|
+
export declare function notificationScopePref(settings: NotificationSettings | null | undefined, scope: {
|
|
187
|
+
kind: 'account';
|
|
188
|
+
} | {
|
|
189
|
+
kind: 'org' | 'host';
|
|
190
|
+
id: string;
|
|
191
|
+
}, category: NotificationCategory, channel: NotificationChannel): boolean | undefined;
|
|
192
|
+
/**
|
|
193
|
+
* The scopes this person has said something different about — what the
|
|
194
|
+
* settings page lists so an override is never somewhere you have to go
|
|
195
|
+
* looking for.
|
|
196
|
+
*
|
|
197
|
+
* Returns ids, not names: the page holds the org and host rosters and this
|
|
198
|
+
* module holds no lookups. An id with no name is still worth listing, because
|
|
199
|
+
* a preference about a workspace somebody left is exactly the kind of
|
|
200
|
+
* leftover that is invisible until it is listed.
|
|
201
|
+
*/
|
|
202
|
+
export declare function notificationOverriddenScopes(settings: NotificationSettings | null | undefined): {
|
|
203
|
+
orgIds: string[];
|
|
204
|
+
hostIds: string[];
|
|
205
|
+
};
|
|
@@ -46,7 +46,9 @@
|
|
|
46
46
|
'system.bandwidthCeilingTripped': 'Bandwidth ceiling reached',
|
|
47
47
|
'system.bandwidthCapEngaged': 'Monthly traffic limit reached',
|
|
48
48
|
'system.billingWebhookHalfApplied': 'Billing webhook half applied',
|
|
49
|
-
'system.disputeUnattributed': 'Card dispute with no owner'
|
|
49
|
+
'system.disputeUnattributed': 'Card dispute with no owner',
|
|
50
|
+
'staff.userSignedUp': 'New account',
|
|
51
|
+
'staff.orgCreated': 'New workspace'
|
|
50
52
|
};
|
|
51
53
|
export const NOTIFICATION_CATEGORY_LABELS = {
|
|
52
54
|
billing: 'Billing',
|
|
@@ -54,8 +56,19 @@ export const NOTIFICATION_CATEGORY_LABELS = {
|
|
|
54
56
|
content: 'Forms & bookings',
|
|
55
57
|
marketplace: 'Marketplace',
|
|
56
58
|
support: 'Support',
|
|
57
|
-
system: 'Product & system'
|
|
59
|
+
system: 'Product & system',
|
|
60
|
+
staff: 'Platform growth'
|
|
58
61
|
};
|
|
62
|
+
/**
|
|
63
|
+
* The categories only staff can receive (AGL-3225).
|
|
64
|
+
*
|
|
65
|
+
* The settings page hides these rows from everybody else, because a switch
|
|
66
|
+
* for mail that can never arrive is a promise the product does not keep. It
|
|
67
|
+
* is presentation only: `notifyStaff` is what decides the audience, and it
|
|
68
|
+
* enumerates the `staff` claim rather than reading this.
|
|
69
|
+
*/ export const STAFF_NOTIFICATION_CATEGORIES = new Set([
|
|
70
|
+
'staff'
|
|
71
|
+
]);
|
|
59
72
|
export function notificationCategory(type) {
|
|
60
73
|
const prefix = String(type).split('.')[0];
|
|
61
74
|
return [
|
|
@@ -64,7 +77,8 @@ export function notificationCategory(type) {
|
|
|
64
77
|
'content',
|
|
65
78
|
'marketplace',
|
|
66
79
|
'support',
|
|
67
|
-
'system'
|
|
80
|
+
'system',
|
|
81
|
+
'staff'
|
|
68
82
|
].includes(prefix) ? prefix : 'system';
|
|
69
83
|
}
|
|
70
84
|
/**
|
|
@@ -97,5 +111,127 @@ export function crmDailyDigestEnabled(prefs) {
|
|
|
97
111
|
/** Whether a person asked for a workspace's weekly insights. */ export function insightDigestSubscribed(value, orgId) {
|
|
98
112
|
return Boolean(orgId) && (value == null ? void 0 : value[orgId]) === true;
|
|
99
113
|
}
|
|
114
|
+
export const NOTIFICATION_SETTINGS_FIELD = 'notificationSettings';
|
|
115
|
+
/**
|
|
116
|
+
* What a category does when nobody has said otherwise.
|
|
117
|
+
*
|
|
118
|
+
* Console on, email OFF, everywhere. Email defaults off because the inbox is
|
|
119
|
+
* not ours to fill: the product already sends transactional mail nobody opted
|
|
120
|
+
* into — welcome, verification, invites, dunning, usage alerts — and every one
|
|
121
|
+
* of those leaves on the same domain a customer's password reset depends on.
|
|
122
|
+
* Turning a busy site's form submissions into mail by default would put that
|
|
123
|
+
* domain's reputation behind traffic the recipient never asked for.
|
|
124
|
+
*
|
|
125
|
+
* Exhaustive `Record` deliberately: a new {@link NotificationCategory} is a
|
|
126
|
+
* compile error here until somebody decides what it does by default, which is
|
|
127
|
+
* the one question a new category must not be able to ship without answering.
|
|
128
|
+
*/ export const NOTIFICATION_CHANNEL_DEFAULTS = {
|
|
129
|
+
billing: {
|
|
130
|
+
console: true,
|
|
131
|
+
email: false
|
|
132
|
+
},
|
|
133
|
+
team: {
|
|
134
|
+
console: true,
|
|
135
|
+
email: false
|
|
136
|
+
},
|
|
137
|
+
content: {
|
|
138
|
+
console: true,
|
|
139
|
+
email: false
|
|
140
|
+
},
|
|
141
|
+
marketplace: {
|
|
142
|
+
console: true,
|
|
143
|
+
email: false
|
|
144
|
+
},
|
|
145
|
+
support: {
|
|
146
|
+
console: true,
|
|
147
|
+
email: false
|
|
148
|
+
},
|
|
149
|
+
system: {
|
|
150
|
+
console: true,
|
|
151
|
+
email: false
|
|
152
|
+
},
|
|
153
|
+
// Console ON, because the complaint this answers is that staff never heard
|
|
154
|
+
// about a sign-up at all; email off, like everything else, because that is
|
|
155
|
+
// what the channel defaults to and a sign-up is not urgent enough to be the
|
|
156
|
+
// exception that starts filling inboxes by default.
|
|
157
|
+
staff: {
|
|
158
|
+
console: true,
|
|
159
|
+
email: false
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
/**
|
|
163
|
+
* The types that send their OWN email and must never be mailed again by the
|
|
164
|
+
* generic channel (AGL-3224).
|
|
165
|
+
*
|
|
166
|
+
* Both digests compose a message the fan-out could not reproduce — a day's
|
|
167
|
+
* owed tasks, a week's figures — and send it from their own route under their
|
|
168
|
+
* own switch (`digestPrefs.crmDaily`, `insightDigests.{orgId}`). A recipient
|
|
169
|
+
* who switches the `content` email channel on would otherwise receive the
|
|
170
|
+
* digest twice: once as the digest, once as a one-line "Daily CRM digest"
|
|
171
|
+
* notification saying that the digest happened.
|
|
172
|
+
*/ export const NOTIFICATION_SELF_SENT_EMAIL_TYPES = new Set([
|
|
173
|
+
'content.crmDailyDigest',
|
|
174
|
+
'content.insightsDigest'
|
|
175
|
+
]);
|
|
176
|
+
/**
|
|
177
|
+
* Whether a channel is on for one notification, at its own scope.
|
|
178
|
+
*
|
|
179
|
+
* Resolution order, first answer wins: the notification's SITE, its
|
|
180
|
+
* WORKSPACE, the account, then {@link NOTIFICATION_CHANNEL_DEFAULTS}. A scope
|
|
181
|
+
* that holds no entry for the category, or an entry with the channel key
|
|
182
|
+
* absent, does not answer — see {@link NotificationChannelPrefs}.
|
|
183
|
+
*
|
|
184
|
+
* `legacyPrefs` is the flat `notificationPrefs` mute map this replaces
|
|
185
|
+
* (AGL-267), and it sits BELOW the account layer rather than beside it.
|
|
186
|
+
* Nothing migrates it: a person who never opens the new settings page keeps
|
|
187
|
+
* being governed by the mutes they set years ago, and the first thing they do
|
|
188
|
+
* set on the account layer overrides the old map for that category without
|
|
189
|
+
* disturbing the rest of it. It answers for `console` only, because the map
|
|
190
|
+
* predates there being a second channel and reading a console mute as an
|
|
191
|
+
* email preference would be inventing an answer its author never gave.
|
|
192
|
+
*/ export function notificationChannelEnabled(settings, channel, type, scope, legacyPrefs) {
|
|
193
|
+
var _settings_hosts, _settings_orgs;
|
|
194
|
+
const category = notificationCategory(type);
|
|
195
|
+
const layers = [
|
|
196
|
+
(scope == null ? void 0 : scope.hostId) ? settings == null ? void 0 : (_settings_hosts = settings.hosts) == null ? void 0 : _settings_hosts[scope.hostId] : undefined,
|
|
197
|
+
(scope == null ? void 0 : scope.orgId) ? settings == null ? void 0 : (_settings_orgs = settings.orgs) == null ? void 0 : _settings_orgs[scope.orgId] : undefined,
|
|
198
|
+
settings == null ? void 0 : settings.account
|
|
199
|
+
];
|
|
200
|
+
for (const layer of layers){
|
|
201
|
+
var _layer_category;
|
|
202
|
+
const answer = layer == null ? void 0 : (_layer_category = layer[category]) == null ? void 0 : _layer_category[channel];
|
|
203
|
+
if (typeof answer === 'boolean') return answer;
|
|
204
|
+
}
|
|
205
|
+
if (channel === 'console' && notificationMuted(legacyPrefs, type)) return false;
|
|
206
|
+
return NOTIFICATION_CHANNEL_DEFAULTS[category][channel];
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* What one scope says, with no inheritance applied — what the settings page
|
|
210
|
+
* renders as Inherit / On / Off, and `undefined` is Inherit.
|
|
211
|
+
*
|
|
212
|
+
* `scope` names the layer directly rather than being derived from a
|
|
213
|
+
* notification, because the page edits a layer whether or not anything has
|
|
214
|
+
* ever arrived from it.
|
|
215
|
+
*/ export function notificationScopePref(settings, scope, category, channel) {
|
|
216
|
+
var _settings_orgs, _settings_hosts, _layer_category;
|
|
217
|
+
const layer = scope.kind === 'account' ? settings == null ? void 0 : settings.account : scope.kind === 'org' ? settings == null ? void 0 : (_settings_orgs = settings.orgs) == null ? void 0 : _settings_orgs[scope.id] : settings == null ? void 0 : (_settings_hosts = settings.hosts) == null ? void 0 : _settings_hosts[scope.id];
|
|
218
|
+
return layer == null ? void 0 : (_layer_category = layer[category]) == null ? void 0 : _layer_category[channel];
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* The scopes this person has said something different about — what the
|
|
222
|
+
* settings page lists so an override is never somewhere you have to go
|
|
223
|
+
* looking for.
|
|
224
|
+
*
|
|
225
|
+
* Returns ids, not names: the page holds the org and host rosters and this
|
|
226
|
+
* module holds no lookups. An id with no name is still worth listing, because
|
|
227
|
+
* a preference about a workspace somebody left is exactly the kind of
|
|
228
|
+
* leftover that is invisible until it is listed.
|
|
229
|
+
*/ export function notificationOverriddenScopes(settings) {
|
|
230
|
+
const named = (layers)=>Object.entries(layers != null ? layers : {}).filter(([, prefs])=>Object.values(prefs != null ? prefs : {}).some((channels)=>Object.values(channels != null ? channels : {}).some((value)=>typeof value === 'boolean'))).map(([id])=>id).sort();
|
|
231
|
+
return {
|
|
232
|
+
orgIds: named(settings == null ? void 0 : settings.orgs),
|
|
233
|
+
hostIds: named(settings == null ? void 0 : settings.hosts)
|
|
234
|
+
};
|
|
235
|
+
}
|
|
100
236
|
|
|
101
237
|
//# sourceMappingURL=notifications.js.map
|