gloomberb 0.15.6 → 0.15.8
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 -2
- package/README.zh-CN.md +1 -1
- package/package.json +1 -1
- package/src/api-client/data.ts +42 -10
- package/src/api-client/doe.ts +75 -0
- package/src/api-client/estimate-revisions.ts +1 -1
- package/src/api-client/futures-curve.ts +1 -1
- package/src/api-client/index.ts +7 -0
- package/src/api-client/market-discovery.ts +84 -0
- package/src/api-client/paths.ts +17 -11
- package/src/api-client/request.ts +12 -2
- package/src/api-client/tape.ts +2 -2
- package/src/api-client/types.ts +4 -0
- package/src/app/dialog-bridge.tsx +33 -0
- package/src/app/pane-runtime/index.tsx +7 -2
- package/src/app/pane-runtime/pane-settings-runtime.tsx +2 -1
- package/src/app/pane-runtime/pane-template-runtime.ts +8 -28
- package/src/app/pane-runtime/plugin-bindings.ts +34 -12
- package/src/app/pane-runtime/ticker-open-runtime.ts +5 -3
- package/src/app/runtime/broker-import.ts +1 -1
- package/src/app/runtime/desktop-menu.ts +9 -6
- package/src/app/runtime/plugin-bindings.ts +7 -1
- package/src/app/runtime/ticker-refresh.ts +1 -1
- package/src/app.tsx +12 -0
- package/src/brokers/directory.ts +11 -2
- package/src/brokers/profile-form.ts +2 -1
- package/src/brokers/remove-profile.ts +61 -0
- package/src/brokers/signed-in/connect.ts +20 -7
- package/src/brokers/signed-in/sign-in-dialog.tsx +68 -30
- package/src/cli/desktop-pane-shot.ts +2 -2
- package/src/cli/history-rows.ts +1 -1
- package/src/cli/pane-functions/function-help.ts +18 -3
- package/src/cli/ticker-resolution.ts +2 -2
- package/src/components/chart/composite/composite-chart.tsx +15 -2
- package/src/components/chart/composite/format.ts +24 -11
- package/src/components/chart/composite/rasterizer.ts +67 -16
- package/src/components/chart/composite/scene.ts +53 -2
- package/src/components/chart/composite/session-shading.ts +3 -2
- package/src/components/chart/composite/text-renderer.ts +52 -1
- package/src/components/chart/composite/time-axis.ts +100 -30
- package/src/components/chart/composite/tools.ts +2 -1
- package/src/components/chart/composite/types.ts +4 -0
- package/src/components/command-bar/action-runtime.ts +7 -67
- package/src/components/command-bar/commands/direct/index.ts +9 -5
- package/src/components/command-bar/commands/direct/runtime.ts +12 -11
- package/src/components/command-bar/commands/plugin/actions.ts +6 -35
- package/src/components/command-bar/helpers.ts +3 -31
- package/src/components/command-bar/keyboard-handlers.ts +7 -219
- package/src/components/command-bar/keyboard-shortcuts.ts +25 -99
- package/src/components/command-bar/layout-items/current-layout.ts +2 -1
- package/src/components/command-bar/layout-items/focused-pane.ts +2 -1
- package/src/components/command-bar/layout-items/types.ts +2 -0
- package/src/components/command-bar/layout-items.ts +3 -0
- package/src/components/command-bar/list/model.ts +1 -1
- package/src/components/command-bar/list/navigation.ts +5 -12
- package/src/components/command-bar/pane-actions.ts +17 -14
- package/src/components/command-bar/pane-settings.ts +7 -250
- package/src/components/command-bar/pane-templates/workflow.ts +6 -13
- package/src/components/command-bar/panel/index.tsx +4 -110
- package/src/components/command-bar/panel/layout.ts +4 -16
- package/src/components/command-bar/panel/runtime.ts +1 -87
- package/src/components/command-bar/panel/state.ts +12 -29
- package/src/components/command-bar/panel/types.ts +2 -26
- package/src/components/command-bar/picker-activation.ts +7 -56
- package/src/components/command-bar/routes/root/plugin-install.ts +3 -3
- package/src/components/command-bar/routes/root/search-report.ts +2 -1
- package/src/components/command-bar/routing/actions.ts +21 -59
- package/src/components/command-bar/routing/confirm.ts +15 -88
- package/src/components/command-bar/routing/effects.ts +1 -20
- package/src/components/command-bar/routing/launch-request.ts +1 -36
- package/src/components/command-bar/routing/list-state.ts +5 -36
- package/src/components/command-bar/selection-runtime.ts +6 -35
- package/src/components/command-bar/surface/environment.ts +10 -6
- package/src/components/command-bar/surface/index.tsx +4 -37
- package/src/components/command-bar/workflow/alert.ts +23 -17
- package/src/components/command-bar/workflow/broker.ts +30 -40
- package/src/components/command-bar/workflow/builtin.ts +33 -38
- package/src/components/command-bar/workflow/collection-actions.ts +27 -28
- package/src/components/command-bar/workflow/collection-ticker.ts +4 -4
- package/src/components/command-bar/workflow/coordinator.ts +27 -156
- package/src/components/command-bar/workflow/fields.ts +6 -22
- package/src/components/command-bar/workflow/submit.ts +28 -43
- package/src/components/command-bar/workflow/ticker-listing-picker.ts +12 -9
- package/src/components/command-bar/workflow/tickers.ts +2 -2
- package/src/components/command-bar/workflow/types.ts +10 -35
- package/src/components/feedback-dialog.tsx +1 -13
- package/src/components/form-modal/broker-step.tsx +119 -0
- package/src/components/form-modal/confirm.tsx +145 -0
- package/src/components/form-modal/content.tsx +698 -0
- package/src/components/form-modal/deps.ts +77 -0
- package/src/components/form-modal/field-row.tsx +318 -0
- package/src/components/form-modal/host.tsx +169 -0
- package/src/components/form-modal/index.ts +2 -0
- package/src/components/form-modal/model.ts +131 -0
- package/src/components/form-modal/request.ts +67 -0
- package/src/components/form-modal/routes.ts +139 -0
- package/src/components/layout/shell/index.tsx +49 -28
- package/src/components/layout/status-bar.tsx +2 -1
- package/src/components/onboarding/first-run-workspace.ts +1 -1
- package/src/components/onboarding/onboarding-frame.tsx +4 -1
- package/src/components/onboarding/onboarding-wizard.tsx +1 -1
- package/src/components/onboarding/portfolio-step/positions-panel.tsx +14 -5
- package/src/components/onboarding/wizard-model.ts +13 -5
- package/src/components/onboarding/wizard-positions.ts +56 -7
- package/src/components/pane-settings-dialog.tsx +7 -1
- package/src/components/pane-template-wizard.tsx +4 -149
- package/src/components/sign-in-code-panel.tsx +22 -9
- package/src/components/textarea-keys.ts +13 -0
- package/src/components/ui/button.tsx +3 -2
- package/src/components/ui/confirm-dialog.tsx +62 -10
- package/src/components/ui/display.tsx +6 -3
- package/src/components/ui/fields.tsx +5 -0
- package/src/components/ui/frame.tsx +7 -2
- package/src/components/ui/header-case.ts +25 -3
- package/src/components/ui/list-view.tsx +16 -9
- package/src/components/ui/query-bar.tsx +8 -1
- package/src/core/app-runtime.ts +1 -1
- package/src/core/state/app/types.ts +1 -15
- package/src/data/config/home.ts +82 -11
- package/src/data/config/store/node.ts +29 -16
- package/src/i18n/es.ts +54 -7
- package/src/i18n/ja.ts +54 -7
- package/src/i18n/ko.ts +54 -7
- package/src/i18n/zh-cn.ts +54 -7
- package/src/i18n/zh-tw.ts +54 -7
- package/src/layout-marketplace/gallery.tsx +11 -3
- package/src/market-data/coordinator/fx-legs.ts +1 -1
- package/src/market-data/history-session.ts +3 -5
- package/src/market-data/market/freshness.ts +1 -1
- package/src/market-data/market/status.ts +1 -1
- package/src/market-data/market/trading-sessions.ts +65 -1
- package/src/market-data/quotes/metadata.ts +3 -3
- package/src/market-data/quotes/resolution.ts +3 -3
- package/src/news/types.ts +1 -1
- package/src/plugins/builtin/alerts/alert-engine.ts +2 -0
- package/src/plugins/builtin/alerts/cli.ts +2 -2
- package/src/plugins/builtin/alerts/types.ts +2 -0
- package/src/plugins/builtin/broker-manager/add-flow.tsx +625 -0
- package/src/plugins/builtin/broker-manager/add-request.ts +34 -0
- package/src/plugins/builtin/broker-manager/detail.tsx +128 -50
- package/src/plugins/builtin/broker-manager/footer.ts +6 -5
- package/src/plugins/builtin/broker-manager/index.tsx +62 -21
- package/src/plugins/builtin/broker-manager/model.ts +7 -0
- package/src/plugins/builtin/broker-manager/pane-actions.tsx +47 -44
- package/src/plugins/builtin/chart-composer/cli-options.ts +4 -0
- package/src/plugins/builtin/chart-composer/headless-schema.ts +6 -0
- package/src/plugins/builtin/chart-composer/pane.tsx +28 -1
- package/src/plugins/builtin/cloud/company-picker.tsx +658 -0
- package/src/plugins/builtin/composite-plugins.ts +2 -0
- package/src/plugins/builtin/credit-boards/model.ts +1 -1
- package/src/plugins/builtin/dividend-yield/browser.tsx +2 -25
- package/src/plugins/builtin/dividend-yield/client.ts +34 -122
- package/src/plugins/builtin/dividend-yield/index.tsx +9 -9
- package/src/plugins/builtin/dividend-yield/provider-client.ts +1 -1
- package/src/plugins/builtin/doe/client.ts +73 -0
- package/src/plugins/builtin/doe/headless.ts +93 -0
- package/src/plugins/builtin/doe/index.tsx +41 -0
- package/src/plugins/builtin/doe/model.ts +215 -0
- package/src/plugins/builtin/doe/pane.tsx +148 -0
- package/src/plugins/builtin/doe/range-bar.tsx +45 -0
- package/src/plugins/builtin/earnings-calls/transcript-view.tsx +7 -7
- package/src/plugins/builtin/estimate-revisions/client.ts +3 -3
- package/src/plugins/builtin/estimate-revisions/pane.tsx +2 -2
- package/src/plugins/builtin/executives/pane.tsx +17 -8
- package/src/plugins/builtin/fear-greed/charts.tsx +1 -1
- package/src/plugins/builtin/filing-events/pane.tsx +23 -6
- package/src/plugins/builtin/futures/contracts.ts +3 -3
- package/src/plugins/builtin/futures/returns.ts +1 -1
- package/src/plugins/builtin/futures-curve/client.ts +1 -1
- package/src/plugins/builtin/futures-curve/model.ts +1 -1
- package/src/plugins/builtin/help/index.tsx +8 -3
- package/src/plugins/builtin/holders/pane.tsx +1 -1
- package/src/plugins/builtin/kelly-sizer/pane.tsx +1 -1
- package/src/plugins/builtin/market-heatmap/data.ts +13 -256
- package/src/plugins/builtin/market-heatmap/index.tsx +5 -15
- package/src/plugins/builtin/market-movers/client.ts +1 -1
- package/src/plugins/builtin/market-movers/screener.ts +60 -142
- package/src/plugins/builtin/market-valuation/defs.ts +1 -2
- package/src/plugins/builtin/market-valuation/sources.ts +1 -3
- package/src/plugins/builtin/news/wire/default-feeds.ts +2 -8
- package/src/plugins/builtin/news/wire/feed-config.ts +5 -1
- package/src/plugins/builtin/news/wire/persisted-articles.ts +9 -5
- package/src/plugins/builtin/news/wire/rss/cli.ts +1 -1
- package/src/plugins/builtin/news/wire/rss/managed-feed.ts +19 -0
- package/src/plugins/builtin/news/wire/rss/parser.ts +18 -9
- package/src/plugins/builtin/news/wire/rss/source.ts +1 -1
- package/src/plugins/builtin/options/view.tsx +1 -1
- package/src/plugins/builtin/plugin-marketplace/model.ts +5 -0
- package/src/plugins/builtin/plugin-marketplace/pane.tsx +4 -2
- package/src/plugins/builtin/portfolio-list/cli/render.ts +1 -1
- package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +6 -20
- package/src/plugins/builtin/portfolio-list/command-bar.ts +6 -2
- package/src/plugins/builtin/portfolio-list/mutations.ts +53 -1
- package/src/plugins/builtin/relative-rotation/client.ts +1 -1
- package/src/plugins/builtin/research/analyst-model.ts +1 -1
- package/src/plugins/builtin/research/equity-diagnostic-pane.tsx +6 -6
- package/src/plugins/builtin/research/event-model.ts +1 -1
- package/src/plugins/builtin/risk-factors/pane.tsx +29 -14
- package/src/plugins/builtin/shared/pro-feature-wall.tsx +47 -0
- package/src/plugins/builtin/shared/ticker-request.ts +1 -1
- package/src/plugins/builtin/short-interest/client.ts +14 -106
- package/src/plugins/builtin/short-interest/headless.ts +2 -2
- package/src/plugins/builtin/short-interest/index.tsx +5 -5
- package/src/plugins/builtin/short-interest/pane.tsx +4 -4
- package/src/plugins/builtin/ticker-detail/financials/ratios.ts +42 -10
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +1 -1
- package/src/plugins/builtin/ticker-detail/pane.tsx +2 -2
- package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +1 -1
- package/src/plugins/builtin/time-sales/client.ts +3 -3
- package/src/plugins/builtin/vol-surface/model.ts +1 -1
- package/src/plugins/builtin/volatility/model.ts +1 -1
- package/src/plugins/bundle.ts +2 -2
- package/src/plugins/catalog-browser.ts +2 -0
- package/src/plugins/catalog-manifest.ts +0 -1
- package/src/plugins/catalog-ui.ts +1 -1
- package/src/plugins/catalog.ts +0 -2
- package/src/plugins/host-link.ts +1 -1
- package/src/plugins/host-resolver.ts +1 -1
- package/src/plugins/loader.ts +6 -5
- package/src/plugins/registry/host-actions.ts +6 -1
- package/src/plugins/registry/index.ts +1 -0
- package/src/plugins/runtime/context.tsx +2 -0
- package/src/plugins/runtime/index.tsx +2 -0
- package/src/public/__snapshots__/public-api.test.ts.snap +0 -1
- package/src/public/market-data.ts +0 -7
- package/src/remote/controller-utils.ts +1 -1
- package/src/remote/controller.ts +27 -3
- package/src/remote/form.ts +20 -0
- package/src/remote/resources.ts +7 -0
- package/src/remote/schema.ts +25 -3
- package/src/remote/semantic-tree.tsx +15 -0
- package/src/remote/types.ts +2 -0
- package/src/renderers/browser/research-entry.ts +2 -2
- package/src/renderers/cloudflare/http-proxy.ts +157 -33
- package/src/renderers/cloudflare/proxy-session.ts +270 -0
- package/src/renderers/cloudflare/worker.ts +4 -1
- package/src/renderers/dom/desktop/controls.tsx +2 -1
- package/src/renderers/dom/dialog-host.tsx +17 -8
- package/src/renderers/dom/styles.css +4 -0
- package/src/renderers/electrobun/bun/application-menu/index.ts +4 -4
- package/src/renderers/electrobun/bun/desktop/backend.ts +7 -1
- package/src/renderers/electrobun/bun/desktop/initialization.ts +2 -2
- package/src/renderers/electrobun/bun/external-plugins.ts +1 -1
- package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +3 -6
- package/src/renderers/electrobun/view/cli-pane-shot-health.ts +3 -3
- package/src/renderers/opentui/dialog-host.tsx +14 -2
- package/src/renderers/opentui/start.tsx +2 -2
- package/src/renderers/opentui/terminal-media.ts +2 -2
- package/src/renderers/opentui/test-utils.tsx +9 -2
- package/src/renderers/opentui/toast-host.tsx +1 -1
- package/src/sources/gloomberb-cloud/index.ts +70 -37
- package/src/sources/gloomberb-cloud/news.ts +3 -3
- package/src/sources/history-result.ts +1 -0
- package/src/sources/history-retention.ts +22 -6
- package/src/sources/{yahoo-finance/symbols.ts → listing-symbols.ts} +16 -41
- package/src/sources/listing-target.ts +4 -4
- package/src/sources/provider-router/cache.ts +16 -6
- package/src/sources/provider-router/financials.ts +1 -1
- package/src/sources/provider-router/history.ts +7 -4
- package/src/state/app/bootstrap.ts +1 -1
- package/src/state/app/context/index.tsx +13 -0
- package/src/sync/core-contributors.ts +181 -18
- package/src/test-support/plugin-runtime.ts +1 -0
- package/src/tickers/instrument-kind.ts +1 -1
- package/src/tickers/open-target.ts +5 -5
- package/src/tickers/search/index.ts +46 -13
- package/src/tickers/search/ranking.ts +4 -4
- package/src/tickers/search/upsert.ts +4 -1
- package/src/time-series/live-bars.ts +8 -3
- package/src/time-series/resolution.ts +1 -1
- package/src/time-series/resolve.ts +225 -18
- package/src/time-series/spec.ts +1 -0
- package/src/time-series/types.ts +9 -1
- package/src/types/data-provider.ts +2 -0
- package/src/types/financials.ts +5 -5
- package/src/types/instrument.ts +1 -1
- package/src/types/plugin.ts +3 -3
- package/src/types/price-history.ts +3 -1
- package/src/ui/dialog-bridge.tsx +50 -0
- package/src/ui/dialog-context.ts +24 -0
- package/src/ui/dialog-stack.ts +47 -1
- package/src/ui/dialog.tsx +2 -10
- package/src/ui/host.tsx +6 -1
- package/src/ui/index.tsx +7 -2
- package/src/utils/exchanges.ts +1 -0
- package/src/utils/latest-financial-period.ts +1 -1
- package/src/utils/plugin-proxy-hosts.json +1 -5
- package/src/utils/price-history.ts +35 -1
- package/src/utils/sec.ts +3 -2
- package/src/utils/zoned-date-time.ts +67 -0
- package/src/app/pane-template-dialog-wizard.tsx +0 -67
- package/src/components/command-bar/multi-select-picker.tsx +0 -225
- package/src/components/command-bar/multi-select-runtime.ts +0 -91
- package/src/components/command-bar/workflow/body.tsx +0 -164
- package/src/components/command-bar/workflow/field-row.tsx +0 -231
- package/src/components/command-bar/workflow/route-actions.ts +0 -235
- package/src/components/command-bar/workflow/runtime.ts +0 -247
- package/src/plugins/builtin/yahoo/index.ts +0 -38
- package/src/sources/yahoo-finance/chart-period.ts +0 -138
- package/src/sources/yahoo-finance/financials.ts +0 -370
- package/src/sources/yahoo-finance/history.ts +0 -150
- package/src/sources/yahoo-finance/http.ts +0 -119
- package/src/sources/yahoo-finance/mappers.ts +0 -549
- package/src/sources/yahoo-finance/names.ts +0 -28
- package/src/sources/yahoo-finance/options.ts +0 -190
- package/src/sources/yahoo-finance/quote-summary.ts +0 -176
- package/src/sources/yahoo-finance/requests.ts +0 -245
- package/src/sources/yahoo-finance/snapshots.ts +0 -336
- package/src/sources/yahoo-finance/types.ts +0 -198
- package/src/sources/yahoo-finance/yahoo-chart-interval.ts +0 -18
- package/src/sources/yahoo-finance.ts +0 -339
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useCallback, type Dispatch } from "react";
|
|
2
2
|
import { getPaneTemplateDisplayLabel } from "../../components/command-bar/pane-templates/items";
|
|
3
3
|
import { createPaneTemplateOrThrow } from "../../components/command-bar/workflow/ops";
|
|
4
|
+
import { openFormModal } from "../../components/form-modal";
|
|
4
5
|
import type { AppTickerRepositoryPort } from "../../core/app-service-ports";
|
|
5
6
|
import type { PluginRegistry } from "../../plugins/registry";
|
|
6
7
|
import type { AppAction, AppState } from "../../state/app/context";
|
|
@@ -10,10 +11,7 @@ import type {
|
|
|
10
11
|
PaneDef,
|
|
11
12
|
PaneTemplateCreateOptions,
|
|
12
13
|
PaneTemplateInstanceConfig,
|
|
13
|
-
WizardStep,
|
|
14
14
|
} from "../../types/plugin";
|
|
15
|
-
import type { DialogApi } from "../../ui/dialog";
|
|
16
|
-
import { runPaneTemplateDialogWizard } from "../pane-template-dialog-wizard";
|
|
17
15
|
|
|
18
16
|
interface UseAppPaneTemplateRuntimeOptions {
|
|
19
17
|
buildPaneInstance: (paneType: string, options?: {
|
|
@@ -24,7 +22,6 @@ interface UseAppPaneTemplateRuntimeOptions {
|
|
|
24
22
|
instanceId?: string;
|
|
25
23
|
}) => PaneInstanceConfig | null;
|
|
26
24
|
dataProvider: DataProvider;
|
|
27
|
-
dialog: DialogApi;
|
|
28
25
|
dispatch: Dispatch<AppAction>;
|
|
29
26
|
notify: (body: string, options?: { type?: "info" | "success" | "error" }) => void;
|
|
30
27
|
placePaneInstance: (
|
|
@@ -40,7 +37,6 @@ interface UseAppPaneTemplateRuntimeOptions {
|
|
|
40
37
|
export function useAppPaneTemplateRuntime({
|
|
41
38
|
buildPaneInstance,
|
|
42
39
|
dataProvider,
|
|
43
|
-
dialog,
|
|
44
40
|
dispatch,
|
|
45
41
|
notify,
|
|
46
42
|
placePaneInstance,
|
|
@@ -48,38 +44,23 @@ export function useAppPaneTemplateRuntime({
|
|
|
48
44
|
stateRef,
|
|
49
45
|
tickerRepository,
|
|
50
46
|
}: UseAppPaneTemplateRuntimeOptions) {
|
|
51
|
-
const runPaneTemplateWizard = useCallback((steps: WizardStep[]) => (
|
|
52
|
-
runPaneTemplateDialogWizard(dialog, steps)
|
|
53
|
-
), [dialog]);
|
|
54
|
-
|
|
55
47
|
const createPaneFromTemplate = useCallback(async (templateId: string, options?: PaneTemplateCreateOptions) => {
|
|
56
48
|
const template = pluginRegistry.paneTemplates.get(templateId);
|
|
57
49
|
if (!template) return;
|
|
58
50
|
|
|
59
|
-
|
|
60
|
-
|
|
51
|
+
// A template that asks for settings asks in the same form the command bar
|
|
52
|
+
// opens, which creates the pane when it is sent.
|
|
53
|
+
const asksForSettings = !!template.wizard
|
|
61
54
|
&& template.wizard.length > 0
|
|
62
55
|
&& !options?.values
|
|
63
56
|
&& (!options?.arg || template.wizard.some((step) => step.type === "textarea"));
|
|
64
|
-
if (
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
step.key === template.shortcut?.argPlaceholder
|
|
68
|
-
? { ...step, defaultValue: options.arg }
|
|
69
|
-
: step
|
|
70
|
-
))
|
|
71
|
-
: template.wizard;
|
|
72
|
-
const values = await runPaneTemplateWizard(wizardSteps);
|
|
73
|
-
if (!values) return;
|
|
74
|
-
resolvedOptions = {
|
|
75
|
-
...options,
|
|
76
|
-
values,
|
|
77
|
-
arg: template.shortcut?.argPlaceholder ? values[template.shortcut.argPlaceholder] : options?.arg,
|
|
78
|
-
};
|
|
57
|
+
if (asksForSettings) {
|
|
58
|
+
openFormModal({ kind: "pane-template", templateId, arg: options?.arg, options });
|
|
59
|
+
return;
|
|
79
60
|
}
|
|
80
61
|
|
|
81
62
|
try {
|
|
82
|
-
await createPaneTemplateOrThrow(templateId,
|
|
63
|
+
await createPaneTemplateOrThrow(templateId, options, {
|
|
83
64
|
dataProvider,
|
|
84
65
|
tickerRepository,
|
|
85
66
|
pluginRegistry,
|
|
@@ -101,7 +82,6 @@ export function useAppPaneTemplateRuntime({
|
|
|
101
82
|
notify,
|
|
102
83
|
placePaneInstance,
|
|
103
84
|
pluginRegistry,
|
|
104
|
-
runPaneTemplateWizard,
|
|
105
85
|
stateRef,
|
|
106
86
|
tickerRepository,
|
|
107
87
|
]);
|
|
@@ -4,6 +4,9 @@ import {
|
|
|
4
4
|
createPaneTemplateOrThrow,
|
|
5
5
|
} from "../../components/command-bar/workflow/ops";
|
|
6
6
|
import type { AppTickerRepositoryPort } from "../../core/app-service-ports";
|
|
7
|
+
import { openFormModal } from "../../components/form-modal";
|
|
8
|
+
import { t } from "../../i18n";
|
|
9
|
+
import { openBrokerAddFlow } from "../../plugins/builtin/broker-manager/add-request";
|
|
7
10
|
import { getPanelFocusTarget } from "../../core/state/app/layout";
|
|
8
11
|
import { setLayoutManagerDispatch } from "../../plugins/builtin/layout-manager";
|
|
9
12
|
import { setMarketplaceHost } from "../../plugins/builtin/plugin-marketplace/store";
|
|
@@ -48,6 +51,7 @@ import type {
|
|
|
48
51
|
import type { TickerOpenTarget } from "../../tickers/open-target";
|
|
49
52
|
import { instrumentFromTicker } from "../../market-data/request-types";
|
|
50
53
|
import { tickerInstrumentLabel } from "../../tickers/instrument-label";
|
|
54
|
+
import { isDialogOpen } from "../../ui/dialog-stack";
|
|
51
55
|
import { stableStringify } from "../../utils/hash";
|
|
52
56
|
|
|
53
57
|
// Registry callbacks are rebound on renders. Request ownership must survive
|
|
@@ -70,7 +74,7 @@ interface BindAppPanePluginRegistryOptions {
|
|
|
70
74
|
externalPlugins: readonly LoadedExternalPlugin[];
|
|
71
75
|
focusVisiblePane: (paneId: string, layout?: LayoutConfig) => void;
|
|
72
76
|
isDetachedWindow: boolean;
|
|
73
|
-
openPaneSettings: (paneId?: string) => Promise<void>;
|
|
77
|
+
openPaneSettings: (paneId?: string, options?: { fieldKey?: string }) => Promise<void>;
|
|
74
78
|
openPinnedTicker: (rawSymbol: string, options?: PinTickerOptions) => Promise<void>;
|
|
75
79
|
persistConfig: (nextConfig: AppState["config"]) => void;
|
|
76
80
|
persistLayout: (layout: LayoutConfig, options?: { pushHistory?: boolean }) => void;
|
|
@@ -131,25 +135,43 @@ export function bindAppPanePluginRegistry({
|
|
|
131
135
|
if (paneId) dispatch({ type: "FOCUS_PANE", paneId });
|
|
132
136
|
},
|
|
133
137
|
switchTab: (tabId, paneId) => switchTickerResearchTab(tabId, paneId),
|
|
138
|
+
// The bar would open over a dialog without its keys, so it waits for none to be open.
|
|
134
139
|
openCommandBar: (query) => {
|
|
135
|
-
if (isDetachedWindow) return;
|
|
140
|
+
if (isDetachedWindow || isDialogOpen()) return;
|
|
136
141
|
dispatch({ type: "SET_COMMAND_BAR", open: true, query });
|
|
137
142
|
},
|
|
143
|
+
// Forms submit through the main window's state, so a detached window says where to go.
|
|
138
144
|
openPluginCommandWorkflow: (commandId) => {
|
|
139
|
-
if (isDetachedWindow)
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
145
|
+
if (isDetachedWindow) {
|
|
146
|
+
pluginRegistry.notify({ body: t("Open this from the main window."), type: "info" });
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const command = pluginRegistry.commands.get(commandId);
|
|
150
|
+
if (!command?.wizard || command.wizard.length === 0) {
|
|
151
|
+
// Nothing to fill in: the bar opens, as it always has for such a command.
|
|
152
|
+
if (!isDialogOpen()) dispatch({ type: "SET_COMMAND_BAR", open: true, query: "" });
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
openFormModal({ kind: "plugin-command", commandId });
|
|
156
|
+
},
|
|
157
|
+
openBuiltInWorkflow: (actionId) => {
|
|
158
|
+
if (isDetachedWindow) {
|
|
159
|
+
pluginRegistry.notify({ body: t("Open this from the main window."), type: "info" });
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
// Profiles are added in the Brokers pane, not in a form.
|
|
163
|
+
if (actionId === "add-broker-account") {
|
|
164
|
+
openBrokerAddFlow(pluginRegistry);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
openFormModal({ kind: "builtin", actionId });
|
|
146
168
|
},
|
|
147
169
|
getLayout: () => stateRef.current.config.layout,
|
|
148
170
|
updateLayout: (layout) => {
|
|
149
171
|
if (isDetachedWindow) return;
|
|
150
172
|
persistLayout(layout);
|
|
151
173
|
},
|
|
152
|
-
openPaneSettings: (paneId) => { void openPaneSettings(paneId); },
|
|
174
|
+
openPaneSettings: (paneId, options) => { void openPaneSettings(paneId, options); },
|
|
153
175
|
showPane: (paneId) => {
|
|
154
176
|
if (isDetachedWindow) return;
|
|
155
177
|
showPane(paneId);
|
|
@@ -335,7 +357,7 @@ export function bindAppPanePluginRegistry({
|
|
|
335
357
|
toggleable: plugin.toggleable === true,
|
|
336
358
|
enabled: !disabled.has(plugin.id),
|
|
337
359
|
source: external ? "external" : "builtin",
|
|
338
|
-
...(external?.directory ? { directory: external.directory } : {}),
|
|
360
|
+
...(external?.directory ? { directory: external.directory, path: external.path } : {}),
|
|
339
361
|
...(external?.commit ? { commit: external.commit } : {}),
|
|
340
362
|
...(external?.linked ? { linked: true } : {}),
|
|
341
363
|
hasSetup,
|
|
@@ -358,7 +380,7 @@ export function bindAppPanePluginRegistry({
|
|
|
358
380
|
toggleable: true,
|
|
359
381
|
enabled: !disabled.has(entry.plugin.id),
|
|
360
382
|
source: "external",
|
|
361
|
-
...(entry.directory ? { directory: entry.directory } : {}),
|
|
383
|
+
...(entry.directory ? { directory: entry.directory, path: entry.path } : {}),
|
|
362
384
|
...(entry.commit ? { commit: entry.commit } : {}),
|
|
363
385
|
...(entry.linked ? { linked: true } : {}),
|
|
364
386
|
...(entry.unsupportedTarget ? { unsupportedTarget: entry.unsupportedTarget } : {}),
|
|
@@ -23,8 +23,9 @@ import {
|
|
|
23
23
|
type TickerOpenTarget,
|
|
24
24
|
} from "../../tickers/open-target";
|
|
25
25
|
import { AmbiguousTickerError, findExactTickerSearchMatch } from "../../tickers/search";
|
|
26
|
+
import { isDialogOpen } from "../../ui/dialog-stack";
|
|
26
27
|
import { parsePublicTickerKey } from "../../utils/exchanges";
|
|
27
|
-
import {
|
|
28
|
+
import { tickerHasListingSuffix } from "../../sources/listing-symbols";
|
|
28
29
|
import { instrumentFromTicker } from "../../market-data/request-types";
|
|
29
30
|
import { tickerInstrumentLabel } from "../../tickers/instrument-label";
|
|
30
31
|
|
|
@@ -75,7 +76,8 @@ export function useAppTickerOpenRuntime({
|
|
|
75
76
|
return target;
|
|
76
77
|
} catch (err) {
|
|
77
78
|
if (!canPresentFeedback()) return null;
|
|
78
|
-
|
|
79
|
+
// Ticker search offers the listings, unless a dialog would sit over the bar.
|
|
80
|
+
if (err instanceof AmbiguousTickerError && !isDialogOpen()) {
|
|
79
81
|
dispatch({ type: "SET_COMMAND_BAR", open: true, query: rawSymbol,
|
|
80
82
|
launch: { kind: "ticker-search", query: rawSymbol } });
|
|
81
83
|
}
|
|
@@ -114,7 +116,7 @@ export function useAppTickerOpenRuntime({
|
|
|
114
116
|
if (!options?.forceNewPane && !existing && instrument === null && !target.ticker.metadata.broker_contracts?.length) {
|
|
115
117
|
existing = findFixedTickerPaneForSymbol(currentLayout, paneType, symbol);
|
|
116
118
|
}
|
|
117
|
-
if (!options?.forceNewPane && !existing && !instrument && (parsePublicTickerKey(symbol).exchange ||
|
|
119
|
+
if (!options?.forceNewPane && !existing && !instrument && (parsePublicTickerKey(symbol).exchange || tickerHasListingSuffix(symbol))) {
|
|
118
120
|
existing = currentLayout.instances.find((instance) => {
|
|
119
121
|
if (instance.paneId !== paneType || instance.binding?.kind !== "fixed" || instance.binding.instrument || !isPaneInLayout(currentLayout, instance.instanceId)) return false;
|
|
120
122
|
const ticker = currentState.tickers.get(instance.binding.symbol);
|
|
@@ -87,7 +87,7 @@ export function useBrokerImportRuntime({
|
|
|
87
87
|
|
|
88
88
|
for (const position of result.positions) {
|
|
89
89
|
throwIfBrokerImportCancelled(options?.signal);
|
|
90
|
-
// Skip
|
|
90
|
+
// Skip market data for broker option symbols; position marks are already available.
|
|
91
91
|
// Position data (markPrice, marketValue, unrealizedPnl) is used directly.
|
|
92
92
|
if (options?.refreshImportedTickers !== false && position.assetCategory !== "OPT") {
|
|
93
93
|
refreshQuote(position.ticker, position.exchange, undefined, 1);
|
|
@@ -4,9 +4,12 @@ import type { PluginRegistry } from "../../plugins/registry";
|
|
|
4
4
|
import type { AppAction, AppState } from "../../state/app/context";
|
|
5
5
|
import type { DesktopApplicationMenuBridge } from "../../types/desktop-menu";
|
|
6
6
|
import type { DesktopWindowBridge } from "../../types/desktop-window";
|
|
7
|
+
import { isDialogOpen } from "../../ui/dialog-stack";
|
|
7
8
|
import type { RendererHost } from "../../ui/host";
|
|
8
9
|
import { recordFunctionOpen } from "../../telemetry/usage-counts";
|
|
9
10
|
|
|
11
|
+
const OPENS_OVER_DIALOGS = new Set<string>(["open-command-bar", "open-plugin-workflow", "open-builtin-workflow"]);
|
|
12
|
+
|
|
10
13
|
export function useDesktopApplicationMenuRuntime({
|
|
11
14
|
desktopApplicationMenuBridge,
|
|
12
15
|
desktopWindowKind,
|
|
@@ -27,6 +30,11 @@ export function useDesktopApplicationMenuRuntime({
|
|
|
27
30
|
useEffect(() => {
|
|
28
31
|
if (desktopWindowKind !== "main" || !desktopApplicationMenuBridge) return;
|
|
29
32
|
return desktopApplicationMenuBridge.subscribe((command) => {
|
|
33
|
+
// The native menu stays reachable over a dialog, accelerators included.
|
|
34
|
+
// A bar opened there would sit over the dialog without its keys, and Esc
|
|
35
|
+
// would close the dialog under it; a form would stack over one the user
|
|
36
|
+
// has not finished. Both wait for the dialog to close.
|
|
37
|
+
if (OPENS_OVER_DIALOGS.has(command.type) && isDialogOpen()) return;
|
|
30
38
|
switch (command.type) {
|
|
31
39
|
case "open-command-bar":
|
|
32
40
|
dispatch({ type: "SET_COMMAND_BAR", open: true, query: command.query });
|
|
@@ -35,12 +43,7 @@ export function useDesktopApplicationMenuRuntime({
|
|
|
35
43
|
pluginRegistry.openPluginCommandWorkflow(command.commandId);
|
|
36
44
|
break;
|
|
37
45
|
case "open-builtin-workflow":
|
|
38
|
-
|
|
39
|
-
type: "SET_COMMAND_BAR",
|
|
40
|
-
open: true,
|
|
41
|
-
query: "",
|
|
42
|
-
launch: { kind: "builtin-workflow", actionId: command.actionId },
|
|
43
|
-
});
|
|
46
|
+
pluginRegistry.openBuiltInWorkflow(command.actionId);
|
|
44
47
|
break;
|
|
45
48
|
case "open-url":
|
|
46
49
|
void rendererHost.openExternal(command.url).catch((error) => {
|
|
@@ -9,6 +9,7 @@ import type { MarketDataCoordinator } from "../../market-data/coordinator";
|
|
|
9
9
|
import { instrumentFromTicker } from "../../market-data/request-types";
|
|
10
10
|
import type { PluginRegistry } from "../../plugins/registry";
|
|
11
11
|
import { saveConfigImmediately } from "../../state/config-save-scheduler";
|
|
12
|
+
import { settleWithin } from "../../utils/async-deadline";
|
|
12
13
|
import type { AppAction, AppState } from "../../state/app/context";
|
|
13
14
|
import type { AppConfig, BrokerInstanceConfig } from "../../types/config";
|
|
14
15
|
import type { DataProvider } from "../../types/data-provider";
|
|
@@ -18,6 +19,9 @@ import {
|
|
|
18
19
|
getBrokerInstance,
|
|
19
20
|
} from "../../utils/broker-instances";
|
|
20
21
|
|
|
22
|
+
/** How long removing a profile waits for its adapter to disconnect before going on without it. */
|
|
23
|
+
const ADAPTER_DISCONNECT_WAIT_MS = 5_000;
|
|
24
|
+
|
|
21
25
|
export function bindPluginRegistryRuntimeAccess({
|
|
22
26
|
dataProvider,
|
|
23
27
|
dispatch,
|
|
@@ -186,8 +190,10 @@ export function bindPluginRegistryRuntimeAccess({
|
|
|
186
190
|
|
|
187
191
|
clearPersistedBrokerAccounts(pluginRegistry.persistence.resources, instance);
|
|
188
192
|
|
|
193
|
+
// Best effort: an adapter that throws, rejects or never answers (a desktop
|
|
194
|
+
// call to a busy Bun process) must not keep the profile.
|
|
189
195
|
const broker = pluginRegistry.brokers.get(instance.brokerType);
|
|
190
|
-
await
|
|
196
|
+
await settleWithin(Promise.resolve().then(() => broker?.disconnect?.(instance)), ADAPTER_DISCONNECT_WAIT_MS);
|
|
191
197
|
|
|
192
198
|
// A portfolio another profile of the account can keep (the account switched
|
|
193
199
|
// to or from sign-in) goes to that profile; only this profile's positions go.
|
|
@@ -96,7 +96,7 @@ export function useTickerRefreshRuntime({
|
|
|
96
96
|
if (quote.currency) void marketData.loadFxRate(quote.currency).catch(() => {});
|
|
97
97
|
void marketData.loadFxRate(baseCurrency).catch(() => {});
|
|
98
98
|
} catch {
|
|
99
|
-
// Silently fail - the list can fall back to stale cache or
|
|
99
|
+
// Silently fail - the list can fall back to stale cache or the backend.
|
|
100
100
|
} finally {
|
|
101
101
|
quoteRefreshInFlight.delete(key);
|
|
102
102
|
}
|
package/src/app.tsx
CHANGED
|
@@ -19,6 +19,7 @@ import { DetachedPaneShell } from "./components/layout/detached-pane-shell";
|
|
|
19
19
|
import { TransientLayoutProvider } from "./components/layout/transient-layout";
|
|
20
20
|
import { CommandBar } from "./components/command-bar/surface";
|
|
21
21
|
import { OnboardingWizard } from "./components/onboarding/onboarding-wizard";
|
|
22
|
+
import { CompanyPickerHost } from "./plugins/builtin/cloud/company-picker";
|
|
22
23
|
import { SignInGate } from "./components/sign-in-gate";
|
|
23
24
|
import { useDialog } from "./ui/dialog";
|
|
24
25
|
import { PluginRegistry } from "./plugins/registry";
|
|
@@ -39,6 +40,7 @@ import { useBrokerImportRuntime } from "./app/runtime/broker-import";
|
|
|
39
40
|
import { useDesktopDeepLinkRuntime } from "./app/runtime/desktop-deeplink";
|
|
40
41
|
import { useDesktopApplicationMenuRuntime } from "./app/runtime/desktop-menu";
|
|
41
42
|
import { useAppGlobalShortcuts } from "./app/global-shortcuts";
|
|
43
|
+
import { AppDialogBridge } from "./app/dialog-bridge";
|
|
42
44
|
import { KeybindingsProvider, useResolvedKeybindings } from "./app/keybindings";
|
|
43
45
|
import { useAppPaneRuntime } from "./app/pane-runtime";
|
|
44
46
|
import { bindPluginRegistryRuntimeAccess } from "./app/runtime/plugin-bindings";
|
|
@@ -181,6 +183,10 @@ function AppInner({
|
|
|
181
183
|
const toast = useToastHost();
|
|
182
184
|
const isDetachedWindow = desktopWindowBridge?.kind === "detached";
|
|
183
185
|
const detachedPaneId = isDetachedWindow ? desktopWindowBridge.paneId ?? null : null;
|
|
186
|
+
const focusDetachedPane = useMemo(() => {
|
|
187
|
+
const focus = desktopWindowBridge?.kind === "main" ? desktopWindowBridge.focusDetachedPane : undefined;
|
|
188
|
+
return focus ? (paneId: string) => { void focus(paneId).catch(() => {}); } : undefined;
|
|
189
|
+
}, [desktopWindowBridge]);
|
|
184
190
|
const [desktopDockPreview, setDesktopDockPreview] = useState<DesktopDockPreviewState | null>(null);
|
|
185
191
|
const [commandBarNativeOccluder, setCommandBarNativeOccluder] = useState<LayoutBounds | null>(null);
|
|
186
192
|
appActiveRef.current = appActive;
|
|
@@ -354,6 +360,7 @@ function AppInner({
|
|
|
354
360
|
dialog,
|
|
355
361
|
dispatch,
|
|
356
362
|
externalPlugins,
|
|
363
|
+
focusDetachedPane,
|
|
357
364
|
isDetachedWindow,
|
|
358
365
|
notify,
|
|
359
366
|
persistConfig,
|
|
@@ -409,6 +416,7 @@ function AppInner({
|
|
|
409
416
|
if (desktopWindowBridge?.kind === "detached" && desktopWindowBridge.paneId) {
|
|
410
417
|
return (
|
|
411
418
|
<KeybindingsProvider value={keybindings}>
|
|
419
|
+
<AppDialogBridge />
|
|
412
420
|
<ContextMenuProvider pluginRegistry={pluginRegistry}>
|
|
413
421
|
<RemoteControlHost
|
|
414
422
|
adapter={remoteControlAdapter}
|
|
@@ -432,6 +440,7 @@ function AppInner({
|
|
|
432
440
|
|
|
433
441
|
return (
|
|
434
442
|
<KeybindingsProvider value={keybindings}>
|
|
443
|
+
<AppDialogBridge />
|
|
435
444
|
<ContextMenuProvider pluginRegistry={pluginRegistry}>
|
|
436
445
|
<RemoteControlHost
|
|
437
446
|
adapter={remoteControlAdapter}
|
|
@@ -448,6 +457,8 @@ function AppInner({
|
|
|
448
457
|
<TransientLayoutProvider>
|
|
449
458
|
<Shell
|
|
450
459
|
pluginRegistry={pluginRegistry}
|
|
460
|
+
dataProvider={dataProvider}
|
|
461
|
+
tickerRepository={tickerRepository}
|
|
451
462
|
desktopWindowBridge={desktopWindowBridge}
|
|
452
463
|
desktopDockPreview={desktopDockPreview}
|
|
453
464
|
commandBarNativeOccluder={commandBarNativeOccluder}
|
|
@@ -460,6 +471,7 @@ function AppInner({
|
|
|
460
471
|
}}
|
|
461
472
|
/>
|
|
462
473
|
</TransientLayoutProvider>
|
|
474
|
+
{!onboardingActive ? <CompanyPickerHost pluginRegistry={pluginRegistry} /> : null}
|
|
463
475
|
{onboardingActive && onOnboardingComplete ? (
|
|
464
476
|
<OnboardingWizard
|
|
465
477
|
pluginRegistry={pluginRegistry}
|
package/src/brokers/directory.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* adapter. One broker offered both ways is one entry with two methods, so Add
|
|
5
5
|
* Broker and onboarding list "Interactive Brokers" once.
|
|
6
6
|
*/
|
|
7
|
+
import { t, tf } from "../i18n";
|
|
7
8
|
import type { BrokerAdapter } from "../types/broker";
|
|
8
9
|
import type { SignedInBroker } from "./signed-in/client";
|
|
9
10
|
import { SIGNED_IN_BROKER_TYPE } from "./signed-in/profile";
|
|
@@ -48,9 +49,17 @@ export function buildBrokerDirectory({
|
|
|
48
49
|
return [...entries.values()].sort((left, right) => left.name.localeCompare(right.name));
|
|
49
50
|
}
|
|
50
51
|
|
|
52
|
+
/** How a broker connects, beside its name in a list of brokers. */
|
|
53
|
+
export function brokerMethodSummary(entry: BrokerDirectoryEntry): string {
|
|
54
|
+
const signsIn = entry.methods.some((method) => method.kind === "signed-in");
|
|
55
|
+
const onDevice = entry.methods.some((method) => method.kind === "device");
|
|
56
|
+
if (signsIn && onDevice) return t("Sign in, or on this device");
|
|
57
|
+
return signsIn ? t("Sign in") : t("On this device");
|
|
58
|
+
}
|
|
59
|
+
|
|
51
60
|
export function brokerMethodLabel(entry: BrokerDirectoryEntry, method: BrokerMethod): string {
|
|
52
61
|
if (method.kind === "signed-in") {
|
|
53
|
-
return entry.methods.some((candidate) => candidate.kind === "device") ? "Sign in (recommended)" : "Sign in";
|
|
62
|
+
return entry.methods.some((candidate) => candidate.kind === "device") ? t("Sign in (recommended)") : t("Sign in");
|
|
54
63
|
}
|
|
55
|
-
return entry.methods.length > 1 ?
|
|
64
|
+
return entry.methods.length > 1 ? tf("On this device ({broker})", { broker: method.adapter.name }) : method.adapter.name;
|
|
56
65
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { BrokerAdapter, BrokerConfigField } from "../types/broker";
|
|
2
2
|
import type { BrokerInstanceConfig } from "../types/config";
|
|
3
3
|
import { resolveBrokerConfigFields } from "../types/broker";
|
|
4
|
+
import { t, tf } from "../i18n";
|
|
4
5
|
|
|
5
6
|
export interface BrokerProfileDraft {
|
|
6
7
|
label: string;
|
|
@@ -93,7 +94,7 @@ export function validateBrokerProfileValues(
|
|
|
93
94
|
for (const field of getVisibleBrokerConfigFields(adapter, resolvedValues)) {
|
|
94
95
|
if (!field.required) continue;
|
|
95
96
|
if (!normalizeFormValue(resolvedValues[field.key]).trim()) {
|
|
96
|
-
return
|
|
97
|
+
return tf("{label} is required.", { label: t(field.label) });
|
|
97
98
|
}
|
|
98
99
|
}
|
|
99
100
|
return null;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Removing a broker profile, from the Brokers pane or the command bar. A
|
|
3
|
+
* signed-in profile's connection belongs to the Gloom account, so it is
|
|
4
|
+
* disconnected there too, as far as Gloom answers: the profile leaves this
|
|
5
|
+
* device either way, and the message says what the account kept.
|
|
6
|
+
*/
|
|
7
|
+
import type { ConfirmModalOptions } from "../components/form-modal";
|
|
8
|
+
import { t, tf } from "../i18n";
|
|
9
|
+
import type { BrokerInstanceConfig } from "../types/config";
|
|
10
|
+
import { disconnectSignedInProfile, signedInBrokerForProfile } from "./signed-in/connect";
|
|
11
|
+
import { isSignedInBrokerProfile } from "./signed-in/profile";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The confirm before `onConfirm` removes `instance`. `brokerName` names the
|
|
15
|
+
* broker when the connector list does not.
|
|
16
|
+
*/
|
|
17
|
+
export function brokerProfileRemovalConfirm(
|
|
18
|
+
instance: BrokerInstanceConfig,
|
|
19
|
+
brokerName: string,
|
|
20
|
+
onConfirm: () => Promise<void>,
|
|
21
|
+
): ConfirmModalOptions {
|
|
22
|
+
const signedIn = isSignedInBrokerProfile(instance) ? signedInBrokerForProfile(instance, brokerName) : null;
|
|
23
|
+
return {
|
|
24
|
+
confirmId: "remove-broker-profile",
|
|
25
|
+
title: "Disconnect broker?",
|
|
26
|
+
body: [
|
|
27
|
+
tf("Remove \"{label}\" and imported broker data?", { label: instance.label }),
|
|
28
|
+
t("Broker-managed portfolios, positions, and contracts will be removed."),
|
|
29
|
+
...(signedIn
|
|
30
|
+
? [tf("This also disconnects {broker} from your other devices and agents.", { broker: signedIn.name })]
|
|
31
|
+
: []),
|
|
32
|
+
],
|
|
33
|
+
confirmLabel: "Disconnect",
|
|
34
|
+
tone: "danger",
|
|
35
|
+
onConfirm,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface BrokerProfileRemoval {
|
|
40
|
+
message: string;
|
|
41
|
+
/** The Gloom account still holds the broker, so its other devices and agents keep it. */
|
|
42
|
+
accountKept: boolean;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function removeBrokerProfile(
|
|
46
|
+
instance: BrokerInstanceConfig,
|
|
47
|
+
brokerName: string,
|
|
48
|
+
removeBrokerInstance: (instanceId: string) => Promise<void>,
|
|
49
|
+
): Promise<BrokerProfileRemoval> {
|
|
50
|
+
const account = await disconnectSignedInProfile(instance);
|
|
51
|
+
await removeBrokerInstance(instance.id);
|
|
52
|
+
const label = instance.label;
|
|
53
|
+
if (!account.stillConnected) return { message: tf("Removed {label}.", { label }), accountKept: false };
|
|
54
|
+
const broker = signedInBrokerForProfile(instance, brokerName).name;
|
|
55
|
+
return {
|
|
56
|
+
accountKept: true,
|
|
57
|
+
message: account.signedOut
|
|
58
|
+
? tf("Removed {label}. {broker} is still connected to your Gloom account. Sign in to Gloom to disconnect {broker} from your account.", { label, broker })
|
|
59
|
+
: tf("Removed {label}. {broker} is still connected to your Gloom account.", { label, broker }),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* connecting again syncs the profile it already has.
|
|
5
5
|
*/
|
|
6
6
|
import { ApiRequestError } from "../../api-client/errors";
|
|
7
|
+
import { withDeadline } from "../../utils/async-deadline";
|
|
7
8
|
import type { AppConfig, BrokerInstanceConfig } from "../../types/config";
|
|
8
9
|
import { findSignedInBroker } from "./catalog";
|
|
9
10
|
import { disconnectSignedInBroker, type SignedInBroker } from "./client";
|
|
@@ -31,7 +32,7 @@ export interface ConnectSignedInBrokerDeps<T> {
|
|
|
31
32
|
getConfig(): AppConfig;
|
|
32
33
|
createBrokerInstance(brokerType: string, label: string, values: Record<string, unknown>): Promise<BrokerInstanceConfig>;
|
|
33
34
|
syncBrokerInstance(instanceId: string): Promise<T>;
|
|
34
|
-
/**
|
|
35
|
+
/** Asks the user to connect: a form's own connect step, or the connect dialog when missing. */
|
|
35
36
|
requestSignIn?: (broker: SignedInBroker) => Promise<boolean>;
|
|
36
37
|
}
|
|
37
38
|
|
|
@@ -50,20 +51,32 @@ export async function connectSignedInBrokerProfile<T>(
|
|
|
50
51
|
return { instance, synced: await deps.syncBrokerInstance(instance.id) };
|
|
51
52
|
}
|
|
52
53
|
|
|
54
|
+
/** Long enough for a slow answer, short enough that removing a profile never hangs on one. */
|
|
55
|
+
const DISCONNECT_TIMEOUT_MS = 10_000;
|
|
56
|
+
|
|
57
|
+
/** What became of the Gloom account's connection behind a profile being removed. */
|
|
58
|
+
export type SignedInProfileDisconnect =
|
|
59
|
+
| { stillConnected: false }
|
|
60
|
+
/** The account keeps the broker: signed out of Gloom, or Gloom did not do it. */
|
|
61
|
+
| { stillConnected: true; signedOut: boolean };
|
|
62
|
+
|
|
53
63
|
/**
|
|
54
64
|
* Disconnects the Gloom account's connection behind a signed-in profile before
|
|
55
|
-
* the profile goes. Already gone is fine
|
|
56
|
-
*
|
|
65
|
+
* the profile goes. Already gone is fine. It never throws: the profile leaves
|
|
66
|
+
* this device whatever Gloom answers, and the caller says what the account kept.
|
|
57
67
|
*/
|
|
58
|
-
export async function disconnectSignedInProfile(instance: BrokerInstanceConfig): Promise<
|
|
68
|
+
export async function disconnectSignedInProfile(instance: BrokerInstanceConfig): Promise<SignedInProfileDisconnect> {
|
|
59
69
|
if (!isSignedInBrokerProfile(instance)) return { stillConnected: false };
|
|
60
70
|
try {
|
|
61
|
-
await
|
|
71
|
+
await withDeadline(
|
|
72
|
+
disconnectSignedInBroker(signedInBrokerId(instance)),
|
|
73
|
+
DISCONNECT_TIMEOUT_MS,
|
|
74
|
+
"Gloom did not answer the disconnect.",
|
|
75
|
+
);
|
|
62
76
|
return { stillConnected: false };
|
|
63
77
|
} catch (error) {
|
|
64
78
|
if (error instanceof ApiRequestError && error.status === 404) return { stillConnected: false };
|
|
65
|
-
|
|
66
|
-
throw error;
|
|
79
|
+
return { stillConnected: true, signedOut: error instanceof ApiRequestError && error.status === 401 };
|
|
67
80
|
}
|
|
68
81
|
}
|
|
69
82
|
|