gloomberb 0.10.2 → 0.10.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/README.zh-CN.md +2 -2
- package/package.json +2 -1
- package/src/api-client/auth.ts +8 -0
- package/src/api-client/index.ts +70 -0
- package/src/api-client/types.ts +49 -0
- package/src/app/runtime/desktop-deeplink.test.ts +8 -0
- package/src/app/runtime/desktop-deeplink.ts +14 -1
- package/src/components/chart/composite/composite-chart.test.tsx +340 -17
- package/src/components/chart/composite/composite-chart.tsx +859 -68
- package/src/components/chart/composite/interactions.test.ts +78 -22
- package/src/components/chart/composite/interactions.ts +45 -17
- package/src/components/chart/composite/rasterizer.ts +8 -2
- package/src/components/chart/composite/renderers.test.ts +61 -1
- package/src/components/chart/composite/scene.ts +14 -4
- package/src/components/chart/composite/tools.test.ts +371 -0
- package/src/components/chart/composite/tools.ts +508 -0
- package/src/components/chart/composite/types.ts +4 -0
- package/src/components/chart/core/pointer.ts +2 -0
- package/src/components/chart/native/raster/primitives.ts +18 -0
- package/src/components/chart/price-axis-labels.tsx +42 -2
- package/src/components/chart/static/chart/axis-overlays.test.tsx +2 -0
- package/src/components/chart/static/chart/axis-overlays.tsx +46 -0
- package/src/components/command-bar/action-runtime.ts +3 -0
- package/src/components/command-bar/assist/inventory.test.ts +93 -0
- package/src/components/command-bar/assist/inventory.ts +92 -0
- package/src/components/command-bar/assist/model.test.ts +104 -0
- package/src/components/command-bar/assist/model.ts +169 -0
- package/src/components/command-bar/assist/runtime.ts +185 -0
- package/src/components/command-bar/commands/direct/index.ts +1 -1
- package/src/components/command-bar/keyboard-shortcuts.ts +5 -0
- package/src/components/command-bar/list/model.ts +15 -2
- package/src/components/command-bar/list/navigation.ts +6 -0
- package/src/components/command-bar/list/view.tsx +16 -3
- package/src/components/command-bar/pane-templates/workflow.ts +1 -0
- package/src/components/command-bar/panel/body.tsx +2 -0
- package/src/components/command-bar/panel/index.tsx +3 -0
- package/src/components/command-bar/panel/runtime.ts +6 -0
- package/src/components/command-bar/panel/types.ts +1 -0
- package/src/components/command-bar/routes/root/results.ts +28 -2
- package/src/components/command-bar/routes/root/runtime.ts +66 -36
- package/src/components/command-bar/routes/root/shortcuts.ts +8 -4
- package/src/components/command-bar/routing/navigation-state.ts +17 -0
- package/src/components/command-bar/selection-runtime.ts +26 -0
- package/src/components/command-bar/surface/assist.test.tsx +276 -0
- package/src/components/command-bar/surface/index.tsx +93 -2
- package/src/components/command-bar/surface/portfolio.test.tsx +2 -1
- package/src/components/command-bar/view-model.ts +7 -1
- package/src/components/command-bar/workflow/broker.test.ts +22 -0
- package/src/components/command-bar/workflow/broker.ts +1 -0
- package/src/components/index.ts +1 -1
- package/src/components/layout/detached-pane-shell.tsx +40 -2
- package/src/components/layout/floating-pane.tsx +4 -1
- package/src/components/layout/pane/header.tsx +64 -6
- package/src/components/layout/pane/index.tsx +4 -1
- package/src/components/layout/shell/index.tsx +23 -0
- package/src/components/layout/shell/pane/layers.tsx +5 -0
- package/src/components/onboarding/account-step/chooser-panel.tsx +74 -0
- package/src/components/onboarding/account-step/form-panel.tsx +157 -0
- package/src/components/onboarding/account-step/index.tsx +58 -0
- package/src/components/onboarding/account-step/model.test.ts +67 -0
- package/src/components/onboarding/account-step/model.ts +126 -0
- package/src/components/onboarding/account-step/signed-in-panel.tsx +31 -0
- package/src/components/onboarding/onboarding-steps.tsx +91 -55
- package/src/components/onboarding/onboarding-wizard.test.tsx +73 -6
- package/src/components/onboarding/onboarding-wizard.tsx +93 -9
- package/src/components/onboarding/portfolio-step/broker-fields-panel.tsx +3 -3
- package/src/components/onboarding/wizard-account.ts +235 -0
- package/src/components/onboarding/wizard-keyboard.ts +67 -1
- package/src/components/onboarding/wizard-model.ts +2 -2
- package/src/components/ui/data-table/index.tsx +1 -0
- package/src/components/ui/data-table/opentui/index.tsx +44 -2
- package/src/components/ui/data-table/types.ts +10 -0
- package/src/components/ui/data-table/visible-range.test.ts +29 -0
- package/src/components/ui/data-table/visible-range.ts +21 -0
- package/src/components/ui/ui.test.tsx +23 -1
- package/src/i18n/es.ts +1093 -0
- package/src/i18n/index.test.ts +5 -2
- package/src/i18n/index.ts +3 -0
- package/src/i18n/ja.ts +66 -3
- package/src/i18n/ko.ts +66 -3
- package/src/i18n/languages.test.ts +4 -1
- package/src/i18n/languages.ts +12 -1
- package/src/i18n/zh-cn.ts +66 -3
- package/src/i18n/zh-tw.ts +66 -3
- package/src/market-data/coordinator/index.ts +1 -1
- package/src/market-data/coordinator/quotes.ts +64 -1
- package/src/market-data/coordinator/subscriptions.test.ts +72 -0
- package/src/market-data/quotes/cadence.ts +2 -0
- package/src/plugins/builtin/account-management/footer.ts +9 -3
- package/src/plugins/builtin/account-management/model.ts +53 -1
- package/src/plugins/builtin/account-management/pane.tsx +107 -24
- package/src/plugins/builtin/ai/index.tsx +7 -2
- package/src/plugins/builtin/ai/screener/market-runtime.ts +4 -2
- package/src/plugins/builtin/ai/screener/pane.tsx +3 -0
- package/src/plugins/builtin/chart-composer/editor.tsx +0 -1
- package/src/plugins/builtin/chart-composer/index.tsx +10 -2
- package/src/plugins/builtin/chart-composer/pane.tsx +111 -44
- package/src/plugins/builtin/chart-composer/quick-add.tsx +27 -3
- package/src/plugins/builtin/chart-composer/semantic.ts +21 -0
- package/src/plugins/builtin/chat/controller/persistence.ts +5 -0
- package/src/plugins/builtin/cloud/plugin.tsx +9 -1
- package/src/plugins/builtin/cloud/upgrade-command.ts +28 -0
- package/src/plugins/builtin/cloud/upgrade-status-widget.tsx +54 -0
- package/src/plugins/builtin/help/index.tsx +38 -0
- package/src/plugins/builtin/news/wire/news/footer.ts +18 -38
- package/src/plugins/builtin/options/footer.test.ts +20 -112
- package/src/plugins/builtin/options/footer.ts +35 -78
- package/src/plugins/builtin/options/index.tsx +6 -0
- package/src/plugins/builtin/options/live-quotes.test.ts +117 -14
- package/src/plugins/builtin/options/live-quotes.ts +93 -39
- package/src/plugins/builtin/options/view.test.tsx +5 -2
- package/src/plugins/builtin/options/view.tsx +49 -9
- package/src/plugins/builtin/portfolio-list/index.tsx +12 -4
- package/src/plugins/builtin/portfolio-list/pane/index.tsx +22 -1
- package/src/plugins/builtin/portfolio-list/pane/streaming.ts +17 -13
- package/src/plugins/builtin/portfolio-list/use-throttled-ticker-order.test.tsx +76 -0
- package/src/plugins/builtin/portfolio-list/use-throttled-ticker-order.ts +78 -0
- package/src/plugins/builtin/shared/cloud-upgrade.ts +131 -0
- package/src/plugins/builtin/shared/live-streaming.ts +60 -0
- package/src/plugins/builtin/shared/plan-access.test.ts +69 -0
- package/src/plugins/builtin/shared/plan-access.ts +109 -0
- package/src/plugins/builtin/ticker-detail/chart-tab-switch.test.tsx +1 -1
- package/src/plugins/builtin/ticker-detail/index.tsx +11 -2
- package/src/plugins/builtin/ticker-detail/pane.tsx +21 -3
- package/src/plugins/builtin/ticker-detail/quote-monitor/index.tsx +4 -2
- package/src/plugins/registry/index.test.ts +51 -0
- package/src/plugins/registry/index.ts +12 -0
- package/src/plugins/registry/pane-settings.ts +29 -0
- package/src/renderers/electrobun/view/data-table/index.tsx +44 -3
- package/src/renderers/electrobun/view/host/box.test.tsx +97 -0
- package/src/renderers/electrobun/view/host/box.tsx +3 -0
- package/src/renderers/electrobun/view/host/chart-surface.tsx +85 -1
- package/src/renderers/electrobun/view/host/focus-scope.test.ts +37 -0
- package/src/renderers/electrobun/view/host/focus-scope.ts +56 -0
- package/src/renderers/electrobun/view/host/input.tsx +16 -1
- package/src/renderers/electrobun/view/host/mouse.ts +1 -0
- package/src/renderers/electrobun/view/host/style.ts +1 -1
- package/src/renderers/electrobun/view/host/tabs.tsx +4 -2
- package/src/renderers/electrobun/view/main.tsx +2 -0
- package/src/renderers/opentui/chart-surface.tsx +2 -0
- package/src/renderers/opentui/host.tsx +3 -0
- package/src/sources/gloomberb-cloud/index.test.ts +8 -0
- package/src/sources/gloomberb-cloud/index.ts +4 -4
- package/src/sources/provider-router/history.test.ts +3 -3
- package/src/sources/provider-router/history.ts +15 -4
- package/src/sources/yahoo-finance/history.test.ts +47 -0
- package/src/sources/yahoo-finance/history.ts +5 -1
- package/src/state/hooks/quote-streaming.test.tsx +181 -1
- package/src/state/hooks/quote-streaming.ts +93 -7
- package/src/theme/colors.ts +20 -1
- package/src/theme/themes.test.ts +3 -0
- package/src/{sources/gloomberb-cloud → time-series}/history-quality.ts +27 -3
- package/src/time-series/resolution.test.ts +51 -0
- package/src/time-series/resolution.ts +41 -0
- package/src/time-series/resolve.test.ts +207 -1
- package/src/time-series/resolve.ts +196 -29
- package/src/time-series/spec-studies.test.ts +4 -0
- package/src/time-series/spec.ts +15 -1
- package/src/time-series/types.ts +7 -0
- package/src/time-series/use-chart-resolution.test.tsx +37 -0
- package/src/time-series/use-chart-resolution.ts +17 -1
- package/src/types/plugin.ts +9 -0
- package/src/ui/host.tsx +17 -0
|
@@ -1 +1,132 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo } from "react";
|
|
2
|
+
import type { PaneFooterSegment } from "../../../components";
|
|
3
|
+
import { tf } from "../../../i18n";
|
|
4
|
+
import { useAppLanguage } from "../../../i18n/react";
|
|
5
|
+
import { useShortcut } from "../../../react/input";
|
|
6
|
+
import { useRendererHost } from "../../../ui";
|
|
7
|
+
import { getSharedRegistry } from "../../registry";
|
|
8
|
+
import { requestAccountManagementTab } from "../account-management/navigation";
|
|
9
|
+
import { usePlanAccess, type PlanAccess } from "./plan-access";
|
|
10
|
+
|
|
1
11
|
export const CLOUD_UPGRADE_URL = "https://gloom.sh/cloud?upgrade=pro";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The renderer host only exists inside React, so UI that uses it publishes an
|
|
15
|
+
* opener here for the command bar, which runs outside the tree.
|
|
16
|
+
*/
|
|
17
|
+
let cloudUpgradeOpener: ((url: string) => void) | null = null;
|
|
18
|
+
|
|
19
|
+
/** Opens the checkout page when any cloud UI has published an opener. */
|
|
20
|
+
export function openCloudUpgradeUrl(): boolean {
|
|
21
|
+
if (!cloudUpgradeOpener) return false;
|
|
22
|
+
cloudUpgradeOpener(CLOUD_UPGRADE_URL);
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Opens the Pro checkout page in the user's browser. */
|
|
27
|
+
export function useCloudUpgradeAction(): () => void {
|
|
28
|
+
const rendererHost = useRendererHost();
|
|
29
|
+
const openUpgrade = useCallback(() => {
|
|
30
|
+
void rendererHost.openExternal(CLOUD_UPGRADE_URL);
|
|
31
|
+
}, [rendererHost]);
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
cloudUpgradeOpener = (url) => { void rendererHost.openExternal(url); };
|
|
34
|
+
}, [rendererHost]);
|
|
35
|
+
return openUpgrade;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Opens the account pane on its Pro tab, where plan and billing live. */
|
|
39
|
+
export function useCloudPlanAction(): () => void {
|
|
40
|
+
return useCallback(() => {
|
|
41
|
+
requestAccountManagementTab("pro");
|
|
42
|
+
getSharedRegistry()?.showPane("account-management");
|
|
43
|
+
}, []);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface CloudAccessFooterOptions {
|
|
47
|
+
/** Compact delay the free tier gets on this pane's data, e.g. "15m" or "12h". */
|
|
48
|
+
delayLabel: string;
|
|
49
|
+
focused: boolean;
|
|
50
|
+
/** Pane-unique shortcut scope. Omit to render the CTA without a `u` binding. */
|
|
51
|
+
shortcutScope?: string;
|
|
52
|
+
/** Set false while the pane happens to show data that is not cloud-delayed. */
|
|
53
|
+
degraded?: boolean;
|
|
54
|
+
segmentId?: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface CloudAccessFooter {
|
|
58
|
+
access: PlanAccess;
|
|
59
|
+
/** Null while the account already pays for Pro, or when nothing is degraded. */
|
|
60
|
+
segment: PaneFooterSegment | null;
|
|
61
|
+
openUpgrade: () => void;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* One compact footer status for cloud data entitlements: the delay free accounts
|
|
66
|
+
* get on this pane, or the remaining trial days while Pro is on loan. Panes keep
|
|
67
|
+
* ownership of their own richer states (e.g. options stream coverage) and only
|
|
68
|
+
* render this segment when it is non-null.
|
|
69
|
+
*/
|
|
70
|
+
export function useCloudAccessFooter({
|
|
71
|
+
delayLabel,
|
|
72
|
+
degraded = true,
|
|
73
|
+
focused,
|
|
74
|
+
segmentId = "cloud-access",
|
|
75
|
+
shortcutScope,
|
|
76
|
+
}: CloudAccessFooterOptions): CloudAccessFooter {
|
|
77
|
+
const language = useAppLanguage();
|
|
78
|
+
const access = usePlanAccess();
|
|
79
|
+
const openUpgrade = useCloudUpgradeAction();
|
|
80
|
+
const openPlan = useCloudPlanAction();
|
|
81
|
+
|
|
82
|
+
const showTrial = access.isTrialActive;
|
|
83
|
+
const showUpgrade = !showTrial && !access.hasProAccess && degraded;
|
|
84
|
+
const bindsShortcut = !!shortcutScope && showUpgrade && focused;
|
|
85
|
+
|
|
86
|
+
useShortcut(
|
|
87
|
+
(event) => {
|
|
88
|
+
const key = (event.name ?? event.key ?? "").toLowerCase();
|
|
89
|
+
if (!bindsShortcut || key !== "u") return;
|
|
90
|
+
event.stopPropagation();
|
|
91
|
+
event.preventDefault();
|
|
92
|
+
openUpgrade();
|
|
93
|
+
},
|
|
94
|
+
{ scope: shortcutScope ?? "cloud-access:upgrade", enabled: bindsShortcut },
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
const segment = useMemo<PaneFooterSegment | null>(() => {
|
|
98
|
+
if (showTrial) {
|
|
99
|
+
return {
|
|
100
|
+
id: segmentId,
|
|
101
|
+
onPress: openPlan,
|
|
102
|
+
parts: [{
|
|
103
|
+
text: tf("Pro trial · {days}d left", { days: access.trialDaysLeft }),
|
|
104
|
+
tone: "positive",
|
|
105
|
+
}],
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
if (!showUpgrade) return null;
|
|
109
|
+
return {
|
|
110
|
+
id: segmentId,
|
|
111
|
+
onPress: openUpgrade,
|
|
112
|
+
parts: [{
|
|
113
|
+
text: shortcutScope
|
|
114
|
+
? tf("{delay} delayed · u upgrade", { delay: delayLabel })
|
|
115
|
+
: tf("{delay} delayed · upgrade", { delay: delayLabel }),
|
|
116
|
+
tone: "warning",
|
|
117
|
+
}],
|
|
118
|
+
};
|
|
119
|
+
}, [
|
|
120
|
+
access.trialDaysLeft,
|
|
121
|
+
delayLabel,
|
|
122
|
+
language,
|
|
123
|
+
openPlan,
|
|
124
|
+
openUpgrade,
|
|
125
|
+
segmentId,
|
|
126
|
+
shortcutScope,
|
|
127
|
+
showTrial,
|
|
128
|
+
showUpgrade,
|
|
129
|
+
]);
|
|
130
|
+
|
|
131
|
+
return { access, openUpgrade, segment };
|
|
132
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { useAppSelector, usePaneInstanceId } from "../../../state/app/context";
|
|
2
|
+
import { findPaneInstance, type LayoutConfig } from "../../../types/config";
|
|
3
|
+
import type {
|
|
4
|
+
PaneQuickSettingDef,
|
|
5
|
+
PaneSettingField,
|
|
6
|
+
PaneSettingsDef,
|
|
7
|
+
} from "../../../types/plugin";
|
|
8
|
+
|
|
9
|
+
export const LIVE_STREAMING_SETTING_KEY = "liveStreaming";
|
|
10
|
+
|
|
11
|
+
export const LIVE_STREAMING_QUICK_SETTING: PaneQuickSettingDef = {
|
|
12
|
+
type: "toggle",
|
|
13
|
+
key: LIVE_STREAMING_SETTING_KEY,
|
|
14
|
+
icon: "zap",
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const LIVE_STREAMING_SETTING_FIELD: PaneSettingField = {
|
|
18
|
+
type: "toggle",
|
|
19
|
+
key: LIVE_STREAMING_SETTING_KEY,
|
|
20
|
+
label: "Live streaming",
|
|
21
|
+
description: "Stream quote updates continuously. Turn off to refresh quotes once per minute. Follow panes inherit their source pane's setting.",
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export function withLiveStreamingSetting(
|
|
25
|
+
settingsDef: PaneSettingsDef,
|
|
26
|
+
settings: Record<string, unknown> | undefined,
|
|
27
|
+
): PaneSettingsDef {
|
|
28
|
+
return {
|
|
29
|
+
...settingsDef,
|
|
30
|
+
values: {
|
|
31
|
+
...settingsDef.values,
|
|
32
|
+
[LIVE_STREAMING_SETTING_KEY]: settings?.[LIVE_STREAMING_SETTING_KEY] !== false,
|
|
33
|
+
},
|
|
34
|
+
fields: [
|
|
35
|
+
...settingsDef.fields.filter((field) => field.key !== LIVE_STREAMING_SETTING_KEY),
|
|
36
|
+
LIVE_STREAMING_SETTING_FIELD,
|
|
37
|
+
],
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function resolveLiveStreamingSetting(
|
|
42
|
+
layout: LayoutConfig,
|
|
43
|
+
paneId: string,
|
|
44
|
+
seen = new Set<string>(),
|
|
45
|
+
): boolean {
|
|
46
|
+
if (seen.has(paneId)) return false;
|
|
47
|
+
|
|
48
|
+
const pane = findPaneInstance(layout, paneId);
|
|
49
|
+
if (!pane) return true;
|
|
50
|
+
if (pane.settings?.[LIVE_STREAMING_SETTING_KEY] === false) return false;
|
|
51
|
+
if (pane.binding?.kind !== "follow") return true;
|
|
52
|
+
|
|
53
|
+
seen.add(paneId);
|
|
54
|
+
return resolveLiveStreamingSetting(layout, pane.binding.sourceInstanceId, seen);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function useLiveStreamingSetting(): boolean {
|
|
58
|
+
const paneId = usePaneInstanceId();
|
|
59
|
+
return useAppSelector((state) => resolveLiveStreamingSetting(state.config.layout, paneId));
|
|
60
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { resolvePlanAccess, type PlanAccessUser } from "./plan-access";
|
|
3
|
+
|
|
4
|
+
const NOW = Date.parse("2026-08-04T12:00:00.000Z");
|
|
5
|
+
const HOUR = 60 * 60 * 1000;
|
|
6
|
+
const DAY = 24 * HOUR;
|
|
7
|
+
|
|
8
|
+
function user(overrides: Partial<PlanAccessUser> = {}): PlanAccessUser {
|
|
9
|
+
return { emailVerified: true, plan: "free", ...overrides };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
describe("plan access", () => {
|
|
13
|
+
test("rounds trial days up so the final hours still read as a day left", () => {
|
|
14
|
+
const cases: Array<[number, number]> = [
|
|
15
|
+
[7 * DAY, 7],
|
|
16
|
+
[6 * DAY + HOUR, 7],
|
|
17
|
+
[DAY, 1],
|
|
18
|
+
[HOUR, 1],
|
|
19
|
+
[60_000, 1],
|
|
20
|
+
];
|
|
21
|
+
for (const [remaining, expected] of cases) {
|
|
22
|
+
const access = resolvePlanAccess(
|
|
23
|
+
user({ trialEndsAt: new Date(NOW + remaining).toISOString(), effectivePlan: "pro" }),
|
|
24
|
+
NOW,
|
|
25
|
+
);
|
|
26
|
+
expect(access.isTrialActive).toBe(true);
|
|
27
|
+
expect(access.hasProAccess).toBe(true);
|
|
28
|
+
expect(access.trialDaysLeft).toBe(expected);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("drops Pro access the moment the trial clock runs out, even on a stale effectivePlan", () => {
|
|
33
|
+
const lapsed = resolvePlanAccess(
|
|
34
|
+
user({ trialEndsAt: new Date(NOW - 1).toISOString(), effectivePlan: "pro" }),
|
|
35
|
+
NOW,
|
|
36
|
+
);
|
|
37
|
+
expect(lapsed).toMatchObject({ hasProAccess: false, isTrialActive: false, trialDaysLeft: 0 });
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("treats plan pro with a live trialEndsAt as a card-backed trial, not a paying pro", () => {
|
|
41
|
+
// The server only sends trialEndsAt while the subscription is actually
|
|
42
|
+
// trialing, so plan "pro" + trialEndsAt = Stripe trial in progress.
|
|
43
|
+
const trialing = resolvePlanAccess(
|
|
44
|
+
user({ plan: "pro", effectivePlan: "pro", trialEndsAt: new Date(NOW + DAY).toISOString() }),
|
|
45
|
+
NOW,
|
|
46
|
+
);
|
|
47
|
+
expect(trialing).toMatchObject({
|
|
48
|
+
hasProAccess: true,
|
|
49
|
+
isPayingPro: false,
|
|
50
|
+
isTrialActive: true,
|
|
51
|
+
trialDaysLeft: 1,
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("treats plan pro without trialEndsAt as a paying pro", () => {
|
|
56
|
+
const paid = resolvePlanAccess(user({ plan: "pro", effectivePlan: "pro" }), NOW);
|
|
57
|
+
expect(paid).toMatchObject({ hasProAccess: true, isPayingPro: true, isTrialActive: false });
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("requires a verified email and falls back to the legacy plan field", () => {
|
|
61
|
+
expect(resolvePlanAccess(
|
|
62
|
+
user({ emailVerified: false, effectivePlan: "pro", trialEndsAt: new Date(NOW + DAY).toISOString() }),
|
|
63
|
+
NOW,
|
|
64
|
+
).hasProAccess).toBe(false);
|
|
65
|
+
// Sessions from a server without effectivePlan still resolve.
|
|
66
|
+
expect(resolvePlanAccess({ emailVerified: true, plan: "pro" }, NOW).hasProAccess).toBe(true);
|
|
67
|
+
expect(resolvePlanAccess(null, NOW)).toMatchObject({ signedIn: false, hasProAccess: false });
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { useSyncExternalStore } from "react";
|
|
2
|
+
import { apiClient } from "../../../api-client";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Minimum shape the plan helpers need. Both `AuthUser` (from `/auth/get-session`)
|
|
6
|
+
* and `AccountProfile` (from `/account/profile`) satisfy it.
|
|
7
|
+
*/
|
|
8
|
+
export interface PlanAccessUser {
|
|
9
|
+
emailVerified?: boolean;
|
|
10
|
+
plan?: "free" | "pro" | null;
|
|
11
|
+
/** Server-computed entitlement: "pro" while a trial runs or a subscription is active. */
|
|
12
|
+
effectivePlan?: "free" | "pro" | null;
|
|
13
|
+
/** ISO timestamp the free Pro trial ends; null once it lapsed or never started. */
|
|
14
|
+
trialEndsAt?: string | null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface PlanAccess {
|
|
18
|
+
signedIn: boolean;
|
|
19
|
+
/** Signed in *and* verified, i.e. account-gated cloud features will answer. */
|
|
20
|
+
emailVerified: boolean;
|
|
21
|
+
/** Real-time entitlement right now: paying subscriber or an active trial. */
|
|
22
|
+
hasProAccess: boolean;
|
|
23
|
+
/** Paying subscriber, i.e. an upgrade CTA is no longer relevant. */
|
|
24
|
+
isPayingPro: boolean;
|
|
25
|
+
isTrialActive: boolean;
|
|
26
|
+
/** Whole days remaining, rounded up; 1 on the final day, 0 when no trial runs. */
|
|
27
|
+
trialDaysLeft: number;
|
|
28
|
+
trialEndsAt: Date | null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Delay the cloud applies to free-tier equity and options quotes. */
|
|
32
|
+
export const CLOUD_QUOTE_DELAY_MINUTES = 15;
|
|
33
|
+
/** Delay the cloud applies to free-tier news wire articles. */
|
|
34
|
+
export const CLOUD_NEWS_DELAY_HOURS = 12;
|
|
35
|
+
|
|
36
|
+
const MS_PER_DAY = 24 * 60 * 60 * 1000;
|
|
37
|
+
|
|
38
|
+
export function resolveTrialEndsAt(user: PlanAccessUser | null | undefined): Date | null {
|
|
39
|
+
if (!user?.trialEndsAt) return null;
|
|
40
|
+
const endsAt = new Date(user.trialEndsAt);
|
|
41
|
+
return Number.isNaN(endsAt.getTime()) ? null : endsAt;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function isTrialActive(user: PlanAccessUser | null | undefined, now = Date.now()): boolean {
|
|
45
|
+
if (user?.emailVerified !== true) return false;
|
|
46
|
+
// Card-backed trials run as plan "pro" with a trialing subscription; the server
|
|
47
|
+
// only sends `trialEndsAt` while the subscription is actually in its trial.
|
|
48
|
+
const endsAt = resolveTrialEndsAt(user);
|
|
49
|
+
return !!endsAt && endsAt.getTime() > now;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function trialDaysLeft(user: PlanAccessUser | null | undefined, now = Date.now()): number {
|
|
53
|
+
const endsAt = resolveTrialEndsAt(user);
|
|
54
|
+
if (!endsAt) return 0;
|
|
55
|
+
const remainingMs = endsAt.getTime() - now;
|
|
56
|
+
if (remainingMs <= 0) return 0;
|
|
57
|
+
return Math.max(1, Math.ceil(remainingMs / MS_PER_DAY));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Whether the account is entitled to real-time cloud data. `effectivePlan` is the
|
|
62
|
+
* server's answer, but the trial clock is re-checked locally so a session captured
|
|
63
|
+
* before the trial lapsed cannot keep claiming Pro. Sessions from older servers
|
|
64
|
+
* (no `effectivePlan`) fall back to the legacy verified-and-paid check.
|
|
65
|
+
*/
|
|
66
|
+
export function hasProAccess(user: PlanAccessUser | null | undefined, now = Date.now()): boolean {
|
|
67
|
+
if (user?.emailVerified !== true) return false;
|
|
68
|
+
if (user.plan === "pro") return true;
|
|
69
|
+
if (isTrialActive(user, now)) return true;
|
|
70
|
+
return user.effectivePlan === "pro" && !user.trialEndsAt;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function resolvePlanAccess(
|
|
74
|
+
user: PlanAccessUser | null | undefined,
|
|
75
|
+
now = Date.now(),
|
|
76
|
+
): PlanAccess {
|
|
77
|
+
const trialActive = isTrialActive(user, now);
|
|
78
|
+
return {
|
|
79
|
+
signedIn: !!user,
|
|
80
|
+
emailVerified: user?.emailVerified === true,
|
|
81
|
+
hasProAccess: hasProAccess(user, now),
|
|
82
|
+
isPayingPro: user?.emailVerified === true && user.plan === "pro" && !trialActive,
|
|
83
|
+
isTrialActive: trialActive,
|
|
84
|
+
trialDaysLeft: trialActive ? trialDaysLeft(user, now) : 0,
|
|
85
|
+
trialEndsAt: resolveTrialEndsAt(user),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function planAccessKey(): string {
|
|
90
|
+
const user = apiClient.getCurrentUser();
|
|
91
|
+
if (!user) return "anonymous";
|
|
92
|
+
return [
|
|
93
|
+
user.id,
|
|
94
|
+
user.emailVerified === true ? "verified" : "unverified",
|
|
95
|
+
user.plan ?? "",
|
|
96
|
+
user.effectivePlan ?? "",
|
|
97
|
+
user.trialEndsAt ?? "",
|
|
98
|
+
].join(":");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Plan state of the signed-in cloud session, refreshed whenever the session changes. */
|
|
102
|
+
export function usePlanAccess(): PlanAccess {
|
|
103
|
+
useSyncExternalStore(
|
|
104
|
+
(onChange) => apiClient.subscribeCurrentUser(onChange),
|
|
105
|
+
planAccessKey,
|
|
106
|
+
planAccessKey,
|
|
107
|
+
);
|
|
108
|
+
return resolvePlanAccess(apiClient.getCurrentUser());
|
|
109
|
+
}
|
|
@@ -236,7 +236,7 @@ describe("Ticker detail chart tab switching", () => {
|
|
|
236
236
|
|
|
237
237
|
await flushFrames();
|
|
238
238
|
const chartTabFrame = testSetup.captureCharFrame();
|
|
239
|
-
expect(chartTabFrame).toContain("
|
|
239
|
+
expect(chartTabFrame).toContain("AAPL:XNAS Price");
|
|
240
240
|
expect(chartTabFrame).toContain("5Y");
|
|
241
241
|
expect(chartTabFrame).toContain("AUTO");
|
|
242
242
|
expect(chartTabFrame).not.toContain("AAPL -");
|
|
@@ -19,6 +19,10 @@ import {
|
|
|
19
19
|
getTickerResearchPaneSettings,
|
|
20
20
|
} from "./settings";
|
|
21
21
|
import { formatTickerListInput } from "../../../tickers/list";
|
|
22
|
+
import {
|
|
23
|
+
LIVE_STREAMING_QUICK_SETTING,
|
|
24
|
+
withLiveStreamingSetting,
|
|
25
|
+
} from "../shared/live-streaming";
|
|
22
26
|
|
|
23
27
|
function hasStatementFinancials(financials: TickerFinancials | null | undefined): boolean {
|
|
24
28
|
return (financials?.annualStatements.length ?? 0) > 0 || (financials?.quarterlyStatements.length ?? 0) > 0;
|
|
@@ -50,7 +54,11 @@ export const tickerDetailModule: PluginModule = {
|
|
|
50
54
|
component: TickerResearchPane,
|
|
51
55
|
defaultPosition: "right",
|
|
52
56
|
defaultMode: "floating",
|
|
53
|
-
|
|
57
|
+
quickSettings: [LIVE_STREAMING_QUICK_SETTING],
|
|
58
|
+
settings: (context) => withLiveStreamingSetting(
|
|
59
|
+
buildTickerResearchSettingsDef(getTickerResearchPaneSettings(context.settings)),
|
|
60
|
+
context.settings,
|
|
61
|
+
),
|
|
54
62
|
},
|
|
55
63
|
{
|
|
56
64
|
id: "financial-analysis",
|
|
@@ -69,7 +77,8 @@ export const tickerDetailModule: PluginModule = {
|
|
|
69
77
|
defaultPosition: "right",
|
|
70
78
|
defaultMode: "floating",
|
|
71
79
|
defaultFloatingSize: { width: 72, height: 10 },
|
|
72
|
-
|
|
80
|
+
quickSettings: [LIVE_STREAMING_QUICK_SETTING],
|
|
81
|
+
settings: (context) => withLiveStreamingSetting(buildQuoteMonitorSettingsDef(), context.settings),
|
|
73
82
|
},
|
|
74
83
|
{
|
|
75
84
|
id: "historical-prices",
|
|
@@ -11,10 +11,10 @@ import {
|
|
|
11
11
|
usePaneStateValue,
|
|
12
12
|
usePaneTicker,
|
|
13
13
|
} from "../../../state/app/context";
|
|
14
|
-
import {
|
|
14
|
+
import { useQuoteUpdates } from "../../../state/hooks/quote-streaming";
|
|
15
15
|
import { getCollectionName, getCollectionTickerCount } from "../../../state/selectors";
|
|
16
16
|
import { getSharedRegistry } from "../../registry";
|
|
17
|
-
import { EmptyState, PaneFooterScope, Tabs } from "../../../components";
|
|
17
|
+
import { EmptyState, PaneFooterScope, Tabs, usePaneFooter } from "../../../components";
|
|
18
18
|
import { useThrottledCommitValue } from "../../../react/use-throttled-commit-value";
|
|
19
19
|
import { resolveOptionsTarget } from "../../../utils/options";
|
|
20
20
|
import {
|
|
@@ -22,6 +22,9 @@ import {
|
|
|
22
22
|
getTickerResearchPaneSettings,
|
|
23
23
|
resolveLockedTabId,
|
|
24
24
|
} from "./settings";
|
|
25
|
+
import { useLiveStreamingSetting } from "../shared/live-streaming";
|
|
26
|
+
import { useCloudAccessFooter } from "../shared/cloud-upgrade";
|
|
27
|
+
import { CLOUD_QUOTE_DELAY_MINUTES } from "../shared/plan-access";
|
|
25
28
|
|
|
26
29
|
const TICKER_RESEARCH_TAB_COMMIT_DELAY_MS = 120;
|
|
27
30
|
|
|
@@ -61,6 +64,7 @@ export function TickerResearchPane({ focused, width, height }: PaneProps) {
|
|
|
61
64
|
const config = useAppSelector((state) => state.config);
|
|
62
65
|
const paneInstance = usePaneInstance();
|
|
63
66
|
const { ticker, financials } = usePaneTicker();
|
|
67
|
+
const liveStreaming = useLiveStreamingSetting();
|
|
64
68
|
const streamingTarget = quoteSubscriptionTargetFromTicker(ticker, ticker?.metadata.ticker, "provider");
|
|
65
69
|
const streamingTargets = useMemo(() => (
|
|
66
70
|
streamingTarget
|
|
@@ -80,7 +84,7 @@ export function TickerResearchPane({ focused, width, height }: PaneProps) {
|
|
|
80
84
|
streamingTarget?.context?.brokerInstanceId,
|
|
81
85
|
streamingTarget?.context?.instrument,
|
|
82
86
|
]);
|
|
83
|
-
|
|
87
|
+
useQuoteUpdates(streamingTargets, { liveStreaming });
|
|
84
88
|
|
|
85
89
|
const { collectionId } = usePaneCollection();
|
|
86
90
|
const [committedActiveTabId, setCommittedActiveTabId] = usePaneStateValue<string>("activeTabId", "overview");
|
|
@@ -100,6 +104,20 @@ export function TickerResearchPane({ focused, width, height }: PaneProps) {
|
|
|
100
104
|
const collectionTickerCount = useAppSelector((state) => getCollectionTickerCount(state, collectionId));
|
|
101
105
|
const collectionName = useAppSelector((state) => getCollectionName(state, collectionId));
|
|
102
106
|
|
|
107
|
+
// Cloud quotes are delayed on the free tier; a broker feed can still be live.
|
|
108
|
+
const cloudAccess = useCloudAccessFooter({
|
|
109
|
+
delayLabel: tf("{count}m", { count: CLOUD_QUOTE_DELAY_MINUTES }),
|
|
110
|
+
degraded: financials?.quote?.dataSource !== "live",
|
|
111
|
+
focused,
|
|
112
|
+
segmentId: "ticker-research-access",
|
|
113
|
+
shortcutScope: "ticker-research:upgrade",
|
|
114
|
+
});
|
|
115
|
+
usePaneFooter(
|
|
116
|
+
"ticker-research-access",
|
|
117
|
+
() => cloudAccess.segment ? { info: [cloudAccess.segment], order: -1 } : null,
|
|
118
|
+
[cloudAccess.segment],
|
|
119
|
+
);
|
|
120
|
+
|
|
103
121
|
const disabledPlugins = config.disabledPlugins;
|
|
104
122
|
const registry = getSharedRegistry();
|
|
105
123
|
const tickerResearchTabsSnapshot = useRegistryTickerResearchTabsSnapshot(registry);
|
|
@@ -5,13 +5,14 @@ import { TICKER_RESEARCH_PANE_ID } from "../../../../types/config";
|
|
|
5
5
|
import type { QuoteSubscriptionTarget } from "../../../../types/data-provider";
|
|
6
6
|
import { quoteSubscriptionTargetFromTicker } from "../../../../market-data/request-types";
|
|
7
7
|
import { useAppSelector, usePaneInstance, usePaneTicker } from "../../../../state/app/context";
|
|
8
|
-
import {
|
|
8
|
+
import { useQuoteUpdates } from "../../../../state/hooks/quote-streaming";
|
|
9
9
|
import { usePluginAppActions, usePluginTickerActions } from "../../../runtime";
|
|
10
10
|
import { colors } from "../../../../theme/colors";
|
|
11
11
|
import { EmptyState } from "../../../../components";
|
|
12
12
|
import { getQuoteMonitorPaneSettings } from "../settings";
|
|
13
13
|
import { useShortcut } from "../../../../react/input";
|
|
14
14
|
import { QuoteMonitorCard } from "./card";
|
|
15
|
+
import { useLiveStreamingSetting } from "../../shared/live-streaming";
|
|
15
16
|
|
|
16
17
|
function chunkSymbols(symbols: string[], columns: number): string[][] {
|
|
17
18
|
const rows: string[][] = [];
|
|
@@ -58,6 +59,7 @@ function resolveGridColumnCount(symbolCount: number, width: number, height: numb
|
|
|
58
59
|
export function QuoteMonitorPane({ paneId, focused, width, height }: PaneProps) {
|
|
59
60
|
const pane = usePaneInstance();
|
|
60
61
|
const { symbol: fallbackSymbol, ticker: fallbackTicker } = usePaneTicker();
|
|
62
|
+
const liveStreaming = useLiveStreamingSetting();
|
|
61
63
|
const settings = useMemo(
|
|
62
64
|
() => getQuoteMonitorPaneSettings(pane?.settings, fallbackSymbol),
|
|
63
65
|
[fallbackSymbol, pane?.settings],
|
|
@@ -77,7 +79,7 @@ export function QuoteMonitorPane({ paneId, focused, width, height }: PaneProps)
|
|
|
77
79
|
}
|
|
78
80
|
return targets;
|
|
79
81
|
}, [fallbackSymbol, fallbackTicker, symbols, tickersBySymbol]);
|
|
80
|
-
|
|
82
|
+
useQuoteUpdates(streamingTargets, { liveStreaming });
|
|
81
83
|
const { pinTicker } = usePluginTickerActions();
|
|
82
84
|
const { openPaneSettings } = usePluginAppActions();
|
|
83
85
|
const { nativePaneChrome } = useUiCapabilities();
|
|
@@ -414,6 +414,57 @@ describe("PluginRegistry pane settings", () => {
|
|
|
414
414
|
expect(descriptor?.pluginId).toBe("news");
|
|
415
415
|
expect(descriptor?.context.settings.breakingNewsNotificationsEnabled).toBe(true);
|
|
416
416
|
});
|
|
417
|
+
|
|
418
|
+
test("resolves and applies toggle-backed pane quick settings", async () => {
|
|
419
|
+
const registry = createRegistry();
|
|
420
|
+
const config = createDefaultConfig("/tmp/gloomberb-pane-quick-settings-test");
|
|
421
|
+
config.layout = {
|
|
422
|
+
dockRoot: { kind: "pane", instanceId: "test-pane:main" },
|
|
423
|
+
instances: [{
|
|
424
|
+
instanceId: "test-pane:main",
|
|
425
|
+
paneId: "test-pane",
|
|
426
|
+
binding: { kind: "none" },
|
|
427
|
+
settings: {},
|
|
428
|
+
}],
|
|
429
|
+
floating: [],
|
|
430
|
+
detached: [],
|
|
431
|
+
};
|
|
432
|
+
registry.getConfigFn = () => config;
|
|
433
|
+
registry.getLayoutFn = () => config.layout;
|
|
434
|
+
|
|
435
|
+
await registry.register(plugin("quotes", (ctx) => {
|
|
436
|
+
ctx.registerPane({
|
|
437
|
+
id: "test-pane",
|
|
438
|
+
name: "Test Pane",
|
|
439
|
+
defaultPosition: "right",
|
|
440
|
+
component: () => null,
|
|
441
|
+
quickSettings: [{ type: "toggle", key: "liveStreaming", icon: "zap" }],
|
|
442
|
+
settings: {
|
|
443
|
+
values: { liveStreaming: true },
|
|
444
|
+
fields: [{
|
|
445
|
+
key: "liveStreaming",
|
|
446
|
+
label: "Live streaming",
|
|
447
|
+
type: "toggle",
|
|
448
|
+
}],
|
|
449
|
+
},
|
|
450
|
+
});
|
|
451
|
+
}));
|
|
452
|
+
|
|
453
|
+
const applied: Array<{ key: string; value: unknown }> = [];
|
|
454
|
+
registry.applyPaneSettingValueFn = async (_paneId, field, value) => {
|
|
455
|
+
applied.push({ key: field.key, value });
|
|
456
|
+
};
|
|
457
|
+
|
|
458
|
+
expect(registry.resolvePaneQuickSettings("test-pane:main")).toMatchObject([{
|
|
459
|
+
key: "liveStreaming",
|
|
460
|
+
icon: "zap",
|
|
461
|
+
label: "Live streaming",
|
|
462
|
+
value: true,
|
|
463
|
+
}]);
|
|
464
|
+
|
|
465
|
+
await registry.togglePaneQuickSetting("test-pane:main", "liveStreaming");
|
|
466
|
+
expect(applied).toEqual([{ key: "liveStreaming", value: false }]);
|
|
467
|
+
});
|
|
417
468
|
});
|
|
418
469
|
|
|
419
470
|
describe("PluginRegistry broker runtime", () => {
|
|
@@ -42,7 +42,9 @@ import {
|
|
|
42
42
|
} from "../runtime";
|
|
43
43
|
import type { PaneRuntimeState } from "../../core/state/app/state";
|
|
44
44
|
import {
|
|
45
|
+
resolveRegistryPaneQuickSettings,
|
|
45
46
|
resolveRegistryPaneSettings,
|
|
47
|
+
type ResolvedRegistryPaneQuickSetting,
|
|
46
48
|
type ResolvedRegistryPaneSettings,
|
|
47
49
|
} from "./pane-settings";
|
|
48
50
|
import { RegistrySlots } from "./slots";
|
|
@@ -363,6 +365,16 @@ export class PluginRegistry implements PluginRuntimeAccess {
|
|
|
363
365
|
return this.resolvePaneSettings(paneId) !== null;
|
|
364
366
|
}
|
|
365
367
|
|
|
368
|
+
resolvePaneQuickSettings(paneId: string): ResolvedRegistryPaneQuickSetting[] {
|
|
369
|
+
return resolveRegistryPaneQuickSettings(this.resolvePaneSettings(paneId));
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
async togglePaneQuickSetting(paneId: string, key: string): Promise<void> {
|
|
373
|
+
const quickSetting = this.resolvePaneQuickSettings(paneId).find((setting) => setting.key === key);
|
|
374
|
+
if (!quickSetting) return;
|
|
375
|
+
await this.applyPaneSettingValueFn(paneId, quickSetting.field, !quickSetting.value);
|
|
376
|
+
}
|
|
377
|
+
|
|
366
378
|
getCommandPluginId(commandId: string): string | undefined {
|
|
367
379
|
return this.contributions.commandOwners.get(commandId);
|
|
368
380
|
}
|
|
@@ -7,6 +7,8 @@ import { getPaneSettings } from "../../pane-settings";
|
|
|
7
7
|
import type { AppConfig, LayoutConfig, PaneInstanceConfig } from "../../types/config";
|
|
8
8
|
import type {
|
|
9
9
|
PaneDef,
|
|
10
|
+
PaneQuickSettingDef,
|
|
11
|
+
PaneSettingField,
|
|
10
12
|
PaneSettingsContext,
|
|
11
13
|
PaneSettingsDef,
|
|
12
14
|
} from "../../types/plugin";
|
|
@@ -21,6 +23,33 @@ export interface ResolvedRegistryPaneSettings {
|
|
|
21
23
|
context: PaneSettingsContext;
|
|
22
24
|
}
|
|
23
25
|
|
|
26
|
+
export interface ResolvedRegistryPaneQuickSetting extends PaneQuickSettingDef {
|
|
27
|
+
label: string;
|
|
28
|
+
description?: string;
|
|
29
|
+
value: boolean;
|
|
30
|
+
field: PaneSettingField & { type: "toggle" };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function resolveRegistryPaneQuickSettings(
|
|
34
|
+
resolved: ResolvedRegistryPaneSettings | null,
|
|
35
|
+
): ResolvedRegistryPaneQuickSetting[] {
|
|
36
|
+
if (!resolved?.paneDef.quickSettings?.length) return [];
|
|
37
|
+
|
|
38
|
+
return resolved.paneDef.quickSettings.flatMap((quickSetting) => {
|
|
39
|
+
const field = resolved.settingsDef.fields.find((candidate) => (
|
|
40
|
+
candidate.key === quickSetting.key && candidate.type === "toggle"
|
|
41
|
+
));
|
|
42
|
+
if (!field || field.type !== "toggle") return [];
|
|
43
|
+
return [{
|
|
44
|
+
...quickSetting,
|
|
45
|
+
label: quickSetting.label ?? field.label,
|
|
46
|
+
description: field.description,
|
|
47
|
+
value: resolved.context.settings[quickSetting.key] === true,
|
|
48
|
+
field,
|
|
49
|
+
}];
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
24
53
|
export function resolveRegistryPaneSettings({
|
|
25
54
|
config,
|
|
26
55
|
getConfigState,
|