gloomberb 0.1.0 → 0.3.1
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/LICENSE +25 -0
- package/README.md +68 -121
- package/bin/gloomberb +3 -0
- package/package.json +13 -4
- package/patches/@opentui%2Fcore@0.1.90.patch +356 -0
- package/src/app.tsx +1330 -0
- package/src/brokers/sync-broker-instance.test.ts +100 -0
- package/src/brokers/sync-broker-instance.ts +341 -0
- package/src/cli/commands/plugins.ts +173 -0
- package/src/cli/commands/portfolio.ts +210 -0
- package/src/cli/commands/search.ts +120 -0
- package/src/cli/commands/ticker.ts +375 -0
- package/src/cli/commands/watchlist.ts +210 -0
- package/src/cli/context.ts +62 -0
- package/src/cli/errors.ts +13 -0
- package/src/cli/helpers.ts +85 -0
- package/src/cli/index.ts +143 -0
- package/src/cli/ticker-resolution.ts +39 -0
- package/src/cli/types.ts +15 -0
- package/src/cli.test.ts +361 -0
- package/src/components/chart/chart-data.ts +239 -0
- package/src/components/chart/chart-renderer.ts +1199 -0
- package/src/components/chart/chart-types.ts +82 -0
- package/src/components/chart/chart-viewport.ts +21 -0
- package/src/components/chart/chart.test.ts +439 -0
- package/src/components/chart/comparison-chart-data.test.ts +109 -0
- package/src/components/chart/comparison-chart-data.ts +316 -0
- package/src/components/chart/comparison-chart-renderer.test.ts +84 -0
- package/src/components/chart/comparison-chart-renderer.ts +354 -0
- package/src/components/chart/comparison-stock-chart.tsx +1486 -0
- package/src/components/chart/cursor-motion.ts +48 -0
- package/src/components/chart/native/chart-rasterizer.ts +732 -0
- package/src/components/chart/native/kitty-adapter.ts +12 -0
- package/src/components/chart/native/kitty-manager.ts +142 -0
- package/src/components/chart/native/kitty-protocol.ts +110 -0
- package/src/components/chart/native/kitty-support.ts +71 -0
- package/src/components/chart/native/kitty.test.ts +620 -0
- package/src/components/chart/native/renderer-selection.ts +24 -0
- package/src/components/chart/native/surface-manager.ts +225 -0
- package/src/components/chart/native/surface-sync.ts +34 -0
- package/src/components/chart/stock-chart-renderer-switch.test.tsx +165 -0
- package/src/components/chart/stock-chart.test.ts +107 -0
- package/src/components/chart/stock-chart.tsx +1556 -0
- package/src/components/command-bar/__snapshots__/command-bar.test.tsx.snap +137 -0
- package/src/components/command-bar/command-bar.test.tsx +1524 -0
- package/src/components/command-bar/command-bar.tsx +4823 -0
- package/src/components/command-bar/command-registry.ts +261 -0
- package/src/components/command-bar/helpers.test.ts +224 -0
- package/src/components/command-bar/helpers.ts +371 -0
- package/src/components/command-bar/root-shortcuts.ts +158 -0
- package/src/components/command-bar/view-model.test.ts +152 -0
- package/src/components/command-bar/view-model.ts +185 -0
- package/src/components/command-bar/workflow-ops.test.ts +70 -0
- package/src/components/command-bar/workflow-ops.ts +433 -0
- package/src/components/command-bar/workflow-types.ts +127 -0
- package/src/components/detail-feed-view.test.tsx +105 -0
- package/src/components/detail-feed-view.tsx +295 -0
- package/src/components/index.ts +32 -0
- package/src/components/layout/floating-pane.tsx +49 -0
- package/src/components/layout/header.tsx +129 -0
- package/src/components/layout/pane-header.tsx +44 -0
- package/src/components/layout/pane.tsx +49 -0
- package/src/components/layout/pixel-mouse-mode.test.ts +122 -0
- package/src/components/layout/pixel-mouse.test.ts +40 -0
- package/src/components/layout/shell.test.tsx +625 -0
- package/src/components/layout/shell.tsx +1200 -0
- package/src/components/layout/status-bar.test.tsx +173 -0
- package/src/components/layout/status-bar.tsx +138 -0
- package/src/components/markdown-editor.tsx +134 -0
- package/src/components/onboarding/onboarding-wizard.test.tsx +184 -0
- package/src/components/onboarding/onboarding-wizard.tsx +1365 -0
- package/src/components/pane-settings-dialog.tsx +468 -0
- package/src/components/pane-template-wizard.test.tsx +52 -0
- package/src/components/pane-template-wizard.tsx +199 -0
- package/src/components/price-selector-dialog.tsx +195 -0
- package/src/components/spinner.tsx +2 -0
- package/src/components/tab-bar.tsx +17 -0
- package/src/components/ticker-badge-text.test.tsx +160 -0
- package/src/components/ticker-badge-text.tsx +58 -0
- package/src/components/ticker-badge.tsx +88 -0
- package/src/components/ticker-list-table.tsx +188 -0
- package/src/components/toggle-list.tsx +74 -0
- package/src/components/ui/button.tsx +77 -0
- package/src/components/ui/data-table.tsx +210 -0
- package/src/components/ui/external-link.tsx +22 -0
- package/src/components/ui/fields.tsx +187 -0
- package/src/components/ui/frame.tsx +73 -0
- package/src/components/ui/index.ts +53 -0
- package/src/components/ui/list-view.tsx +166 -0
- package/src/components/ui/loading.tsx +70 -0
- package/src/components/ui/status.tsx +83 -0
- package/src/components/ui/tabs.tsx +47 -0
- package/src/components/ui/toggle.tsx +164 -0
- package/src/components/ui/ui.test.tsx +134 -0
- package/src/data/app-persistence.ts +25 -0
- package/src/data/config-store.test.ts +310 -0
- package/src/data/config-store.ts +469 -0
- package/src/data/plugin-state-store.ts +102 -0
- package/src/data/resource-store.ts +323 -0
- package/src/data/session-store.ts +52 -0
- package/src/data/sqlite-cache.test.ts +103 -0
- package/src/data/sqlite-database.ts +62 -0
- package/src/data/sqlite-json.ts +19 -0
- package/src/data/ticker-repository.ts +45 -0
- package/src/data/ticker-store.ts +46 -0
- package/src/index.tsx +67 -0
- package/src/market-data/coordinator.test.ts +369 -0
- package/src/market-data/coordinator.ts +580 -0
- package/src/market-data/hooks.tsx +234 -0
- package/src/market-data/query-store.ts +28 -0
- package/src/market-data/request-types.test.ts +64 -0
- package/src/market-data/request-types.ts +70 -0
- package/src/market-data/result-types.ts +56 -0
- package/src/market-data/selectors.ts +166 -0
- package/src/market-data/trace.ts +7 -0
- package/src/pane-settings.test.ts +71 -0
- package/src/pane-settings.ts +73 -0
- package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +338 -0
- package/src/plugins/builtin/ai/gloomberb-cli.ts +70 -0
- package/src/plugins/builtin/ai/index.test.tsx +139 -0
- package/src/plugins/builtin/ai/index.tsx +86 -0
- package/src/plugins/builtin/ai/providers.ts +99 -0
- package/src/plugins/builtin/ai/runner.ts +82 -0
- package/src/plugins/builtin/ai/screener-contract.test.ts +117 -0
- package/src/plugins/builtin/ai/screener-contract.ts +174 -0
- package/src/plugins/builtin/ai/screener-pane.test.tsx +365 -0
- package/src/plugins/builtin/ai/screener-pane.tsx +1161 -0
- package/src/plugins/builtin/ai/settings.ts +79 -0
- package/src/plugins/builtin/ai/ticker-context.ts +64 -0
- package/src/plugins/builtin/ai/utils.ts +71 -0
- package/src/plugins/builtin/ask-ai.test.tsx +258 -0
- package/src/plugins/builtin/ask-ai.tsx +9 -0
- package/src/plugins/builtin/chat-controller.test.ts +217 -0
- package/src/plugins/builtin/chat-controller.ts +340 -0
- package/src/plugins/builtin/chat.test.tsx +451 -0
- package/src/plugins/builtin/chat.tsx +620 -0
- package/src/plugins/builtin/comparison-chart.test.tsx +277 -0
- package/src/plugins/builtin/comparison-chart.tsx +169 -0
- package/src/plugins/builtin/data-providers.ts +16 -0
- package/src/plugins/builtin/debug.tsx +351 -0
- package/src/plugins/builtin/help.test.tsx +110 -0
- package/src/plugins/builtin/help.tsx +281 -0
- package/src/plugins/builtin/layout-manager.tsx +264 -0
- package/src/plugins/builtin/manual-entry.tsx +25 -0
- package/src/plugins/builtin/news.tsx +143 -0
- package/src/plugins/builtin/notes-files.ts +58 -0
- package/src/plugins/builtin/notes.tsx +396 -0
- package/src/plugins/builtin/options-availability.test.tsx +179 -0
- package/src/plugins/builtin/options-availability.ts +158 -0
- package/src/plugins/builtin/options.tsx +364 -0
- package/src/plugins/builtin/portfolio-list/header.tsx +227 -0
- package/src/plugins/builtin/portfolio-list/index.tsx +99 -0
- package/src/plugins/builtin/portfolio-list/metrics.ts +436 -0
- package/src/plugins/builtin/portfolio-list/pane.tsx +533 -0
- package/src/plugins/builtin/portfolio-list/settings.ts +280 -0
- package/src/plugins/builtin/portfolio-list/summary.tsx +270 -0
- package/src/plugins/builtin/portfolio-list/table.tsx +64 -0
- package/src/plugins/builtin/portfolio-list.test.ts +74 -0
- package/src/plugins/builtin/portfolio-list.test.tsx +1164 -0
- package/src/plugins/builtin/portfolio-metrics.test.ts +153 -0
- package/src/plugins/builtin/quote-monitor.test.tsx +114 -0
- package/src/plugins/builtin/sec.tsx +250 -0
- package/src/plugins/builtin/ticker-detail/chart-tab.tsx +43 -0
- package/src/plugins/builtin/ticker-detail/financials-tab.tsx +390 -0
- package/src/plugins/builtin/ticker-detail/index.tsx +80 -0
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +242 -0
- package/src/plugins/builtin/ticker-detail/pane.tsx +183 -0
- package/src/plugins/builtin/ticker-detail/quote-monitor.tsx +83 -0
- package/src/plugins/builtin/ticker-detail/settings.ts +132 -0
- package/src/plugins/builtin/ticker-detail-chart-tab-switch.test.tsx +193 -0
- package/src/plugins/builtin/ticker-detail.test.tsx +578 -0
- package/src/plugins/builtin/yahoo.ts +18 -0
- package/src/plugins/event-bus.ts +37 -0
- package/src/plugins/ibkr/__snapshots__/trade-tab.test.tsx.snap +35 -0
- package/src/plugins/ibkr/account-cache.test.ts +101 -0
- package/src/plugins/ibkr/account-cache.ts +137 -0
- package/src/plugins/ibkr/broker-adapter.ts +168 -0
- package/src/plugins/ibkr/config.test.ts +68 -0
- package/src/plugins/ibkr/config.ts +262 -0
- package/src/plugins/ibkr/dialogs.tsx +83 -0
- package/src/plugins/ibkr/flex.test.ts +107 -0
- package/src/plugins/ibkr/flex.ts +267 -0
- package/src/plugins/ibkr/fundamental-parser.ts +149 -0
- package/src/plugins/ibkr/gateway-helpers.ts +28 -0
- package/src/plugins/ibkr/gateway-selection.ts +51 -0
- package/src/plugins/ibkr/gateway-service.test.ts +450 -0
- package/src/plugins/ibkr/gateway-service.ts +2228 -0
- package/src/plugins/ibkr/index.test.tsx +183 -0
- package/src/plugins/ibkr/index.tsx +153 -0
- package/src/plugins/ibkr/instance-selection.test.ts +121 -0
- package/src/plugins/ibkr/instance-selection.ts +36 -0
- package/src/plugins/ibkr/price-normalization.test.ts +38 -0
- package/src/plugins/ibkr/price-normalization.ts +59 -0
- package/src/plugins/ibkr/trade-badge.tsx +27 -0
- package/src/plugins/ibkr/trade-tab.test.tsx +225 -0
- package/src/plugins/ibkr/trade-tab.tsx +1050 -0
- package/src/plugins/ibkr/trade-utils.test.ts +175 -0
- package/src/plugins/ibkr/trade-utils.ts +149 -0
- package/src/plugins/ibkr/trading-pane.tsx +438 -0
- package/src/plugins/ibkr/trading-state.test.ts +109 -0
- package/src/plugins/ibkr/trading-state.ts +339 -0
- package/src/plugins/loader.ts +71 -0
- package/src/plugins/pane-manager.test.ts +103 -0
- package/src/plugins/pane-manager.ts +974 -0
- package/src/plugins/plugin-persistence.ts +85 -0
- package/src/plugins/plugin-runtime.test.tsx +185 -0
- package/src/plugins/plugin-runtime.tsx +175 -0
- package/src/plugins/prediction-markets/cache.ts +114 -0
- package/src/plugins/prediction-markets/categories.ts +223 -0
- package/src/plugins/prediction-markets/chart.tsx +512 -0
- package/src/plugins/prediction-markets/columns.ts +133 -0
- package/src/plugins/prediction-markets/controller-data.ts +614 -0
- package/src/plugins/prediction-markets/controller.ts +725 -0
- package/src/plugins/prediction-markets/detail/book.tsx +118 -0
- package/src/plugins/prediction-markets/detail/outcomes.tsx +102 -0
- package/src/plugins/prediction-markets/detail/overview.tsx +73 -0
- package/src/plugins/prediction-markets/detail/pane.tsx +308 -0
- package/src/plugins/prediction-markets/detail/rules.tsx +16 -0
- package/src/plugins/prediction-markets/detail/shared.tsx +29 -0
- package/src/plugins/prediction-markets/detail/trades.tsx +30 -0
- package/src/plugins/prediction-markets/detail.test.tsx +47 -0
- package/src/plugins/prediction-markets/index.tsx +102 -0
- package/src/plugins/prediction-markets/keyboard.ts +64 -0
- package/src/plugins/prediction-markets/launch.test.ts +126 -0
- package/src/plugins/prediction-markets/launch.ts +240 -0
- package/src/plugins/prediction-markets/metrics.ts +311 -0
- package/src/plugins/prediction-markets/navigation.ts +90 -0
- package/src/plugins/prediction-markets/outcome-order.ts +19 -0
- package/src/plugins/prediction-markets/pane.test.tsx +425 -0
- package/src/plugins/prediction-markets/pane.tsx +294 -0
- package/src/plugins/prediction-markets/plugin.test.tsx +570 -0
- package/src/plugins/prediction-markets/rows.ts +229 -0
- package/src/plugins/prediction-markets/services/fetch.ts +97 -0
- package/src/plugins/prediction-markets/services/kalshi-adapter.ts +555 -0
- package/src/plugins/prediction-markets/services/polymarket-adapter.ts +660 -0
- package/src/plugins/prediction-markets/services/polymarket-ws.ts +108 -0
- package/src/plugins/prediction-markets/settings.ts +164 -0
- package/src/plugins/prediction-markets/status.ts +24 -0
- package/src/plugins/prediction-markets/table.tsx +84 -0
- package/src/plugins/prediction-markets/test-helpers.tsx +636 -0
- package/src/plugins/prediction-markets/types.ts +186 -0
- package/src/plugins/registry.ts +633 -0
- package/src/sources/exchange-rates.ts +18 -0
- package/src/sources/gloomberb-cloud.test.ts +199 -0
- package/src/sources/gloomberb-cloud.ts +326 -0
- package/src/sources/provider-errors.ts +19 -0
- package/src/sources/provider-router.test.ts +1476 -0
- package/src/sources/provider-router.ts +1405 -0
- package/src/sources/sec-edgar.test.ts +286 -0
- package/src/sources/sec-edgar.ts +526 -0
- package/src/sources/yahoo-finance.test.ts +14 -0
- package/src/sources/yahoo-finance.ts +1017 -0
- package/src/state/app-activity.test.ts +41 -0
- package/src/state/app-activity.ts +106 -0
- package/src/state/app-bootstrap.test.ts +471 -0
- package/src/state/app-bootstrap.ts +308 -0
- package/src/state/app-context.test.ts +406 -0
- package/src/state/app-context.test.tsx +132 -0
- package/src/state/app-context.tsx +969 -0
- package/src/state/selectors.ts +67 -0
- package/src/state/session-persistence.test.ts +128 -0
- package/src/state/session-persistence.ts +161 -0
- package/src/state/ticker-refresh-queue.test.ts +46 -0
- package/src/state/ticker-refresh-queue.ts +40 -0
- package/src/state/use-inline-tickers.ts +174 -0
- package/src/state/use-quote-streaming.test.ts +37 -0
- package/src/state/use-quote-streaming.test.tsx +70 -0
- package/src/state/use-quote-streaming.ts +80 -0
- package/src/state/use-session-snapshot.ts +84 -0
- package/src/test-support/data-provider.ts +22 -0
- package/src/theme/color-utils.ts +67 -0
- package/src/theme/colors.ts +128 -0
- package/src/theme/themes.test.ts +45 -0
- package/src/theme/themes.ts +491 -0
- package/src/types/broker.ts +105 -0
- package/src/types/config.ts +434 -0
- package/src/types/data-provider.ts +81 -0
- package/src/types/financials.ts +142 -0
- package/src/types/instrument.ts +29 -0
- package/src/types/persistence.ts +21 -0
- package/src/types/plugin.ts +354 -0
- package/src/types/ticker.ts +58 -0
- package/src/types/trading.ts +85 -0
- package/src/updater.test.ts +162 -0
- package/src/updater.ts +247 -0
- package/src/utils/api-client.ts +666 -0
- package/src/utils/ascii-chart.ts +172 -0
- package/src/utils/broker-instances.ts +47 -0
- package/src/utils/cli-output.ts +83 -0
- package/src/utils/currency-units.test.ts +49 -0
- package/src/utils/currency-units.ts +82 -0
- package/src/utils/debug-log.ts +133 -0
- package/src/utils/exchange-rate-map.ts +8 -0
- package/src/utils/format.ts +158 -0
- package/src/utils/fuzzy-search.ts +34 -0
- package/src/utils/market-status.ts +95 -0
- package/src/utils/options.ts +86 -0
- package/src/utils/price-history.ts +39 -0
- package/src/utils/quote-time.test.ts +22 -0
- package/src/utils/quote-time.ts +50 -0
- package/src/utils/sec.test.ts +55 -0
- package/src/utils/sec.ts +45 -0
- package/src/utils/ticker-list.test.ts +22 -0
- package/src/utils/ticker-list.ts +35 -0
- package/src/utils/ticker-metadata.ts +51 -0
- package/src/utils/ticker-search.test.ts +227 -0
- package/src/utils/ticker-search.ts +764 -0
- package/src/utils/ticker-tokenizer.test.ts +48 -0
- package/src/utils/ticker-tokenizer.ts +72 -0
- package/src/version.ts +1 -0
- package/install.mjs +0 -69
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { matchPrefix } from "./command-registry";
|
|
2
|
+
export { rankTickerSearchItems } from "../../utils/ticker-search";
|
|
3
|
+
|
|
4
|
+
export type CommandBarMode = "default" | "search" | "themes" | "plugins" | "layout" | "new-pane" | "direct-command";
|
|
5
|
+
|
|
6
|
+
export interface CommandBarModeInfo {
|
|
7
|
+
kind: CommandBarMode;
|
|
8
|
+
badge: string;
|
|
9
|
+
hint: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface CommandBarModeStripEntry {
|
|
13
|
+
prefix: string;
|
|
14
|
+
label: string;
|
|
15
|
+
active: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface CommandBarFooterHints {
|
|
19
|
+
left: string;
|
|
20
|
+
right: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface CommandBarSection<T> {
|
|
24
|
+
category: string;
|
|
25
|
+
items: T[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface CommandBarItemView {
|
|
29
|
+
id: string;
|
|
30
|
+
label: string;
|
|
31
|
+
detail: string;
|
|
32
|
+
category: string;
|
|
33
|
+
kind: "command" | "ticker" | "search" | "theme" | "plugin" | "action" | "info";
|
|
34
|
+
right?: string;
|
|
35
|
+
checked?: boolean;
|
|
36
|
+
current?: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface CommandBarRowPresentation {
|
|
40
|
+
glyph: string;
|
|
41
|
+
label: string;
|
|
42
|
+
trailing: string;
|
|
43
|
+
selected: boolean;
|
|
44
|
+
primaryMuted: boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const MODE_STRIP_ENTRIES = [
|
|
48
|
+
{ prefix: "T", label: "Search ticker", kind: "search" },
|
|
49
|
+
{ prefix: "TH", label: "Change theme", kind: "themes" },
|
|
50
|
+
{ prefix: "PL", label: "Toggle plugins", kind: "plugins" },
|
|
51
|
+
{ prefix: "LAY", label: "Layout actions", kind: "layout" },
|
|
52
|
+
{ prefix: "NP", label: "New pane", kind: "new-pane" },
|
|
53
|
+
] as const;
|
|
54
|
+
|
|
55
|
+
export function resolveCommandBarMode(query: string): CommandBarModeInfo {
|
|
56
|
+
const match = matchPrefix(query);
|
|
57
|
+
|
|
58
|
+
if (!query.trim()) {
|
|
59
|
+
return { kind: "default", badge: "BROWSE", hint: "Type a command, ticker, or prefix" };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (!match) {
|
|
63
|
+
return { kind: "default", badge: "FILTER", hint: `Filtering for "${query.trim()}"` };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
switch (match.command.id) {
|
|
67
|
+
case "search-ticker":
|
|
68
|
+
return { kind: "search", badge: "SEARCH", hint: "Search Yahoo Finance and broker-backed symbols" };
|
|
69
|
+
case "theme":
|
|
70
|
+
return { kind: "themes", badge: "THEMES", hint: "Preview with arrows, Enter to save, Esc to revert" };
|
|
71
|
+
case "plugins":
|
|
72
|
+
return { kind: "plugins", badge: "PLUGINS", hint: "Toggle plugins without leaving the list" };
|
|
73
|
+
case "layout":
|
|
74
|
+
return { kind: "layout", badge: "LAYOUT", hint: "Organize panes, history, and saved layouts" };
|
|
75
|
+
case "new-pane":
|
|
76
|
+
return { kind: "new-pane", badge: "NEW PANE", hint: "Create plugin-defined panes for the current workspace" };
|
|
77
|
+
default:
|
|
78
|
+
return { kind: "direct-command", badge: "COMMAND", hint: `Run ${match.command.label}` };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function getModeStrip(mode: CommandBarMode): CommandBarModeStripEntry[] {
|
|
83
|
+
return MODE_STRIP_ENTRIES.map((entry) => ({
|
|
84
|
+
prefix: entry.prefix,
|
|
85
|
+
label: entry.label,
|
|
86
|
+
active: entry.kind === mode,
|
|
87
|
+
}));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function buildSections<T extends { category: string }>(items: T[]): Array<CommandBarSection<T>> {
|
|
91
|
+
const sections: Array<CommandBarSection<T>> = [];
|
|
92
|
+
for (const item of items) {
|
|
93
|
+
let section = sections.find((candidate) => candidate.category === item.category);
|
|
94
|
+
if (!section) {
|
|
95
|
+
section = { category: item.category, items: [] };
|
|
96
|
+
sections.push(section);
|
|
97
|
+
}
|
|
98
|
+
section.items.push(item);
|
|
99
|
+
}
|
|
100
|
+
return sections
|
|
101
|
+
.map((section, index) => ({ section, index }))
|
|
102
|
+
.sort((a, b) => {
|
|
103
|
+
const priorityDiff = getCategoryPriority(a.section.category) - getCategoryPriority(b.section.category);
|
|
104
|
+
return priorityDiff !== 0 ? priorityDiff : a.index - b.index;
|
|
105
|
+
})
|
|
106
|
+
.map(({ section }) => section);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function getFooterHints(mode: CommandBarMode, isNarrow: boolean): CommandBarFooterHints {
|
|
110
|
+
const moveAndSelect = isNarrow ? "up/down move enter select" : "up/down move enter select";
|
|
111
|
+
if (mode === "plugins") {
|
|
112
|
+
return {
|
|
113
|
+
left: isNarrow ? "space toggle" : `${moveAndSelect} space toggle`,
|
|
114
|
+
right: "esc close",
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
if (mode === "layout") {
|
|
118
|
+
return { left: moveAndSelect, right: "esc close" };
|
|
119
|
+
}
|
|
120
|
+
if (mode === "new-pane") {
|
|
121
|
+
return { left: moveAndSelect, right: "esc close" };
|
|
122
|
+
}
|
|
123
|
+
return { left: moveAndSelect, right: "esc close" };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function getEmptyState(mode: CommandBarMode, query: string, searchQuery?: string): { label: string; detail: string } {
|
|
127
|
+
switch (mode) {
|
|
128
|
+
case "search":
|
|
129
|
+
if (!searchQuery) {
|
|
130
|
+
return { label: "Type a ticker symbol", detail: "Search Yahoo Finance and connected brokers" };
|
|
131
|
+
}
|
|
132
|
+
return { label: `No matches for "${searchQuery}"`, detail: "Try a symbol, company name, or exchange variant" };
|
|
133
|
+
case "plugins":
|
|
134
|
+
return { label: "No plugins match", detail: query.trim() || "Toggleable plugins will appear here" };
|
|
135
|
+
case "themes":
|
|
136
|
+
return { label: "No themes match", detail: query.trim() || "Installed themes will appear here" };
|
|
137
|
+
case "layout":
|
|
138
|
+
return { label: "No layout actions match", detail: query.trim() || "Focused-pane and layout actions will appear here" };
|
|
139
|
+
case "new-pane":
|
|
140
|
+
return { label: "No pane templates match", detail: query.trim() || "Plugin-defined pane templates will appear here" };
|
|
141
|
+
default:
|
|
142
|
+
if (query.trim()) {
|
|
143
|
+
return { label: `No matches for "${query.trim()}"`, detail: "Try a ticker, command name, or prefix" };
|
|
144
|
+
}
|
|
145
|
+
return { label: "No results yet", detail: "Recent tickers and suggested commands will appear here" };
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function getRowPresentation(item: CommandBarItemView, selected: boolean, showTrailing: boolean): CommandBarRowPresentation {
|
|
150
|
+
const glyph = selected ? "\u203a" : " ";
|
|
151
|
+
const primaryMuted = item.kind === "plugin" && !item.checked;
|
|
152
|
+
let trailing = "";
|
|
153
|
+
|
|
154
|
+
if (showTrailing) {
|
|
155
|
+
if (item.current) trailing = "current";
|
|
156
|
+
else if (item.kind === "plugin") trailing = item.checked ? "on" : "off";
|
|
157
|
+
else trailing = item.right || "";
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
glyph,
|
|
162
|
+
label: item.label,
|
|
163
|
+
trailing,
|
|
164
|
+
selected,
|
|
165
|
+
primaryMuted,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function truncateText(text: string, width: number): string {
|
|
170
|
+
if (width <= 0) return "";
|
|
171
|
+
if (text.length <= width) return text;
|
|
172
|
+
if (width <= 3) return ".".repeat(width);
|
|
173
|
+
return `${text.slice(0, width - 3)}...`;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function getCategoryPriority(category: string): number {
|
|
177
|
+
const normalized = category.trim().toLowerCase();
|
|
178
|
+
if (normalized === "saved") return -40;
|
|
179
|
+
if (normalized === "primary listing") return -30;
|
|
180
|
+
if (normalized === "other listings") return -20;
|
|
181
|
+
if (normalized === "funds & derivatives") return -10;
|
|
182
|
+
if (normalized.includes("danger")) return 900;
|
|
183
|
+
if (normalized.includes("debug")) return 910;
|
|
184
|
+
return 0;
|
|
185
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { createDefaultConfig } from "../../types/config";
|
|
3
|
+
import { createInitialState } from "../../state/app-context";
|
|
4
|
+
import { createTestDataProvider } from "../../test-support/data-provider";
|
|
5
|
+
import { createPaneTemplateOrThrow } from "./workflow-ops";
|
|
6
|
+
|
|
7
|
+
function makeDataProvider() {
|
|
8
|
+
return createTestDataProvider({ id: "test" });
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function makeTickerRepository() {
|
|
12
|
+
return {
|
|
13
|
+
getTicker: async () => null,
|
|
14
|
+
saveTicker: async () => {},
|
|
15
|
+
createTicker: async () => { throw new Error("unused"); },
|
|
16
|
+
deleteTicker: async () => {},
|
|
17
|
+
getAllTickers: async () => [],
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe("createPaneTemplateOrThrow", () => {
|
|
22
|
+
test("treats createInstance null as cancellation and does not create a pane", async () => {
|
|
23
|
+
const config = createDefaultConfig("/tmp/gloomberb-workflow-ops-test");
|
|
24
|
+
const state = createInitialState(config);
|
|
25
|
+
const buildCalls: unknown[] = [];
|
|
26
|
+
const placeCalls: unknown[] = [];
|
|
27
|
+
|
|
28
|
+
await createPaneTemplateOrThrow("cancelled-pane", undefined, {
|
|
29
|
+
dataProvider: makeDataProvider() as any,
|
|
30
|
+
tickerRepository: makeTickerRepository() as any,
|
|
31
|
+
dispatch: () => {},
|
|
32
|
+
getState: () => state,
|
|
33
|
+
pluginRegistry: {
|
|
34
|
+
paneTemplates: new Map([
|
|
35
|
+
["cancelled-pane", {
|
|
36
|
+
id: "cancelled-pane",
|
|
37
|
+
paneId: "test-pane",
|
|
38
|
+
label: "Cancelled Pane",
|
|
39
|
+
description: "Should cancel cleanly",
|
|
40
|
+
createInstance: async () => null,
|
|
41
|
+
}],
|
|
42
|
+
]),
|
|
43
|
+
panes: new Map([
|
|
44
|
+
["test-pane", {
|
|
45
|
+
id: "test-pane",
|
|
46
|
+
name: "Test Pane",
|
|
47
|
+
component: () => null,
|
|
48
|
+
defaultPosition: "right",
|
|
49
|
+
}],
|
|
50
|
+
]),
|
|
51
|
+
getPaneTemplatePluginId: () => undefined,
|
|
52
|
+
events: { emit: () => {} },
|
|
53
|
+
} as any,
|
|
54
|
+
buildPaneInstance: (...args) => {
|
|
55
|
+
buildCalls.push(args);
|
|
56
|
+
return {
|
|
57
|
+
instanceId: "test-pane:1",
|
|
58
|
+
paneId: "test-pane",
|
|
59
|
+
title: "Broken Pane",
|
|
60
|
+
} as any;
|
|
61
|
+
},
|
|
62
|
+
placePaneInstance: (...args) => {
|
|
63
|
+
placeCalls.push(args);
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
expect(buildCalls).toHaveLength(0);
|
|
68
|
+
expect(placeCalls).toHaveLength(0);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
import type { Dispatch } from "react";
|
|
2
|
+
import { type LayoutConfig, type PaneBinding, type PaneInstanceConfig } from "../../types/config";
|
|
3
|
+
import type { DataProvider } from "../../types/data-provider";
|
|
4
|
+
import type { Portfolio, TickerRecord, Watchlist } from "../../types/ticker";
|
|
5
|
+
import type { PaneSettingField, PaneTemplateContext, PaneTemplateCreateOptions, PaneTemplateInstanceConfig, PaneTemplateDef } from "../../types/plugin";
|
|
6
|
+
import { getFocusedCollectionId, getFocusedTickerSymbol, type AppAction, type AppState } from "../../state/app-context";
|
|
7
|
+
import type { PluginRegistry } from "../../plugins/registry";
|
|
8
|
+
import type { TickerRepository } from "../../data/ticker-repository";
|
|
9
|
+
import { resolveTickerSearch, upsertTickerFromSearchResult } from "../../utils/ticker-search";
|
|
10
|
+
import { formatTickerListInput, parseTickerListInput } from "../../utils/ticker-list";
|
|
11
|
+
import { updatePaneInstance, setPaneSettings } from "../../pane-settings";
|
|
12
|
+
import {
|
|
13
|
+
buildComparisonChartPaneTitle,
|
|
14
|
+
COMPARISON_CHART_PANE_ID,
|
|
15
|
+
} from "../../plugins/builtin/comparison-chart";
|
|
16
|
+
|
|
17
|
+
export interface SharedWorkflowDeps {
|
|
18
|
+
dataProvider: DataProvider;
|
|
19
|
+
tickerRepository: TickerRepository;
|
|
20
|
+
pluginRegistry: PluginRegistry;
|
|
21
|
+
dispatch: Dispatch<AppAction>;
|
|
22
|
+
getState: () => AppState;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface CreatePaneTemplateDeps extends SharedWorkflowDeps {
|
|
26
|
+
buildPaneInstance: (
|
|
27
|
+
paneType: string,
|
|
28
|
+
options?: {
|
|
29
|
+
title?: string;
|
|
30
|
+
binding?: PaneBinding;
|
|
31
|
+
params?: Record<string, string>;
|
|
32
|
+
settings?: Record<string, unknown>;
|
|
33
|
+
instanceId?: string;
|
|
34
|
+
},
|
|
35
|
+
) => PaneInstanceConfig | null;
|
|
36
|
+
placePaneInstance: (
|
|
37
|
+
instance: PaneInstanceConfig,
|
|
38
|
+
paneDef: NonNullable<ReturnType<PluginRegistry["panes"]["get"]>>,
|
|
39
|
+
options?: PaneTemplateInstanceConfig,
|
|
40
|
+
) => void;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface ApplyPaneSettingDeps extends SharedWorkflowDeps {
|
|
44
|
+
persistLayout: (layout: LayoutConfig, options?: { pushHistory?: boolean }) => void;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export type CollectionKind = "watchlist" | "portfolio";
|
|
48
|
+
export type CollectionMembershipAction = "add" | "remove";
|
|
49
|
+
|
|
50
|
+
export interface CollectionTargetOption {
|
|
51
|
+
id: string;
|
|
52
|
+
label: string;
|
|
53
|
+
description?: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface ResolvedTickerInput {
|
|
57
|
+
symbol: string;
|
|
58
|
+
ticker: TickerRecord;
|
|
59
|
+
created: boolean;
|
|
60
|
+
source: "local" | "provider";
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function getTickerSearchContext(state: AppState, collectionId: string | null) {
|
|
64
|
+
const activePortfolio = state.config.portfolios.find((portfolio) => portfolio.id === collectionId);
|
|
65
|
+
return {
|
|
66
|
+
preferBroker: true,
|
|
67
|
+
brokerId: activePortfolio?.brokerId,
|
|
68
|
+
brokerInstanceId: activePortfolio?.brokerInstanceId,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function materializeResolvedTicker(
|
|
73
|
+
resolvedTicker: NonNullable<Awaited<ReturnType<typeof resolveTickerSearch>>>,
|
|
74
|
+
deps: SharedWorkflowDeps,
|
|
75
|
+
): Promise<ResolvedTickerInput> {
|
|
76
|
+
if (resolvedTicker.kind === "local") {
|
|
77
|
+
return {
|
|
78
|
+
symbol: resolvedTicker.ticker.metadata.ticker,
|
|
79
|
+
ticker: resolvedTicker.ticker,
|
|
80
|
+
created: false,
|
|
81
|
+
source: "local",
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const { ticker, created } = await upsertTickerFromSearchResult(
|
|
86
|
+
deps.tickerRepository,
|
|
87
|
+
resolvedTicker.result,
|
|
88
|
+
);
|
|
89
|
+
deps.dispatch({ type: "UPDATE_TICKER", ticker });
|
|
90
|
+
if (created) {
|
|
91
|
+
deps.pluginRegistry.events.emit("ticker:added", {
|
|
92
|
+
symbol: ticker.metadata.ticker,
|
|
93
|
+
ticker,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
symbol: ticker.metadata.ticker,
|
|
99
|
+
ticker,
|
|
100
|
+
created,
|
|
101
|
+
source: "provider",
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function getCollectionOptions(
|
|
106
|
+
state: AppState,
|
|
107
|
+
kind: CollectionKind,
|
|
108
|
+
): Array<Portfolio | Watchlist> {
|
|
109
|
+
if (kind === "watchlist") {
|
|
110
|
+
return state.config.watchlists;
|
|
111
|
+
}
|
|
112
|
+
return state.config.portfolios.filter((portfolio) => !portfolio.brokerId && !portfolio.brokerInstanceId);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function getCollectionTargetOptions(
|
|
116
|
+
state: AppState,
|
|
117
|
+
kind: CollectionKind,
|
|
118
|
+
action: CollectionMembershipAction,
|
|
119
|
+
ticker: TickerRecord | null,
|
|
120
|
+
): CollectionTargetOption[] {
|
|
121
|
+
const collections = getCollectionOptions(state, kind);
|
|
122
|
+
const membershipIds = ticker
|
|
123
|
+
? new Set(kind === "watchlist" ? ticker.metadata.watchlists : ticker.metadata.portfolios)
|
|
124
|
+
: new Set<string>();
|
|
125
|
+
|
|
126
|
+
return collections
|
|
127
|
+
.filter((collection) => {
|
|
128
|
+
if (!ticker) return true;
|
|
129
|
+
const isMember = membershipIds.has(collection.id);
|
|
130
|
+
return action === "add" ? !isMember : isMember;
|
|
131
|
+
})
|
|
132
|
+
.map((collection) => ({
|
|
133
|
+
id: collection.id,
|
|
134
|
+
label: collection.name,
|
|
135
|
+
description: action === "add"
|
|
136
|
+
? `Add to "${collection.name}"`
|
|
137
|
+
: `Remove from "${collection.name}"`,
|
|
138
|
+
}));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function resolvePreferredCollectionTarget(
|
|
142
|
+
state: AppState,
|
|
143
|
+
kind: CollectionKind,
|
|
144
|
+
activeCollectionId: string | null,
|
|
145
|
+
action: CollectionMembershipAction,
|
|
146
|
+
ticker: TickerRecord | null,
|
|
147
|
+
): string | null {
|
|
148
|
+
if (!activeCollectionId) return null;
|
|
149
|
+
const options = getCollectionTargetOptions(state, kind, action, ticker);
|
|
150
|
+
return options.some((option) => option.id === activeCollectionId) ? activeCollectionId : null;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export async function resolveTickerInput(
|
|
154
|
+
rawInput: string | undefined,
|
|
155
|
+
activeTicker: string | null,
|
|
156
|
+
collectionId: string | null,
|
|
157
|
+
deps: SharedWorkflowDeps,
|
|
158
|
+
): Promise<ResolvedTickerInput | null> {
|
|
159
|
+
const state = deps.getState();
|
|
160
|
+
const resolvedTicker = await resolveTickerSearch({
|
|
161
|
+
query: rawInput,
|
|
162
|
+
activeTicker,
|
|
163
|
+
tickers: state.tickers,
|
|
164
|
+
dataProvider: deps.dataProvider,
|
|
165
|
+
searchContext: getTickerSearchContext(state, collectionId),
|
|
166
|
+
});
|
|
167
|
+
if (!resolvedTicker) return null;
|
|
168
|
+
return materializeResolvedTicker(resolvedTicker, deps);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export async function resolveTickerInputOrThrow(
|
|
172
|
+
rawInput: string | undefined,
|
|
173
|
+
activeTicker: string | null,
|
|
174
|
+
collectionId: string | null,
|
|
175
|
+
deps: SharedWorkflowDeps,
|
|
176
|
+
): Promise<ResolvedTickerInput> {
|
|
177
|
+
const resolved = await resolveTickerInput(rawInput, activeTicker, collectionId, deps);
|
|
178
|
+
if (!resolved) {
|
|
179
|
+
throw new Error(`No ticker match found for "${rawInput ?? activeTicker ?? ""}".`);
|
|
180
|
+
}
|
|
181
|
+
return resolved;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export async function applyCollectionMembershipChange(
|
|
185
|
+
ticker: TickerRecord,
|
|
186
|
+
kind: CollectionKind,
|
|
187
|
+
action: CollectionMembershipAction,
|
|
188
|
+
collectionId: string,
|
|
189
|
+
deps: SharedWorkflowDeps,
|
|
190
|
+
): Promise<{ changed: boolean; ticker: TickerRecord }> {
|
|
191
|
+
const field = kind === "watchlist" ? "watchlists" : "portfolios";
|
|
192
|
+
const currentValues = ticker.metadata[field];
|
|
193
|
+
const nextValues = action === "add"
|
|
194
|
+
? (currentValues.includes(collectionId) ? currentValues : [...currentValues, collectionId])
|
|
195
|
+
: currentValues.filter((entry) => entry !== collectionId);
|
|
196
|
+
|
|
197
|
+
if (nextValues.length === currentValues.length && nextValues.every((entry, index) => entry === currentValues[index])) {
|
|
198
|
+
return { changed: false, ticker };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const nextTicker: TickerRecord = {
|
|
202
|
+
...ticker,
|
|
203
|
+
metadata: {
|
|
204
|
+
...ticker.metadata,
|
|
205
|
+
[field]: nextValues,
|
|
206
|
+
},
|
|
207
|
+
};
|
|
208
|
+
await deps.tickerRepository.saveTicker(nextTicker);
|
|
209
|
+
deps.dispatch({ type: "UPDATE_TICKER", ticker: nextTicker });
|
|
210
|
+
return { changed: true, ticker: nextTicker };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export async function resolveTickerListInput(
|
|
214
|
+
rawInput: string,
|
|
215
|
+
collectionId: string | null,
|
|
216
|
+
deps: SharedWorkflowDeps,
|
|
217
|
+
): Promise<string[]> {
|
|
218
|
+
const tokens = parseTickerListInput(rawInput);
|
|
219
|
+
const symbols: string[] = [];
|
|
220
|
+
const seen = new Set<string>();
|
|
221
|
+
|
|
222
|
+
for (const token of tokens) {
|
|
223
|
+
const resolvedTicker = await resolveTickerInputOrThrow(token, null, collectionId, deps);
|
|
224
|
+
const symbol = resolvedTicker.symbol;
|
|
225
|
+
|
|
226
|
+
if (seen.has(symbol)) continue;
|
|
227
|
+
seen.add(symbol);
|
|
228
|
+
symbols.push(symbol);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return symbols;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async function resolvePaneTemplateOptions(
|
|
235
|
+
template: PaneTemplateDef,
|
|
236
|
+
options: PaneTemplateCreateOptions | undefined,
|
|
237
|
+
deps: SharedWorkflowDeps,
|
|
238
|
+
): Promise<{
|
|
239
|
+
context: PaneTemplateContext;
|
|
240
|
+
resolvedOptions: PaneTemplateCreateOptions | undefined;
|
|
241
|
+
}> {
|
|
242
|
+
const state = deps.getState();
|
|
243
|
+
const baseContext: PaneTemplateContext = {
|
|
244
|
+
config: state.config,
|
|
245
|
+
layout: state.config.layout,
|
|
246
|
+
focusedPaneId: state.focusedPaneId,
|
|
247
|
+
activeTicker: getFocusedTickerSymbol(state),
|
|
248
|
+
activeCollectionId: getFocusedCollectionId(state),
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
let resolvedOptions = options;
|
|
252
|
+
if (template.shortcut?.argPlaceholder === "ticker") {
|
|
253
|
+
const resolvedTicker = await resolveTickerInputOrThrow(
|
|
254
|
+
resolvedOptions?.arg,
|
|
255
|
+
baseContext.activeTicker,
|
|
256
|
+
baseContext.activeCollectionId,
|
|
257
|
+
deps,
|
|
258
|
+
);
|
|
259
|
+
resolvedOptions = {
|
|
260
|
+
...resolvedOptions,
|
|
261
|
+
symbol: resolvedTicker.symbol,
|
|
262
|
+
ticker: resolvedTicker.ticker,
|
|
263
|
+
searchResult: null,
|
|
264
|
+
};
|
|
265
|
+
} else if (template.shortcut?.argPlaceholder === "tickers") {
|
|
266
|
+
const rawInput = resolvedOptions?.arg ?? resolvedOptions?.values?.tickers ?? "";
|
|
267
|
+
const symbols = await resolveTickerListInput(rawInput, baseContext.activeCollectionId, deps);
|
|
268
|
+
resolvedOptions = {
|
|
269
|
+
...resolvedOptions,
|
|
270
|
+
arg: rawInput,
|
|
271
|
+
symbols,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const context: PaneTemplateContext = {
|
|
276
|
+
...baseContext,
|
|
277
|
+
activeTicker: resolvedOptions?.symbol ?? baseContext.activeTicker,
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
return {
|
|
281
|
+
context,
|
|
282
|
+
resolvedOptions,
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export async function createPaneTemplateOrThrow(
|
|
287
|
+
templateId: string,
|
|
288
|
+
options: PaneTemplateCreateOptions | undefined,
|
|
289
|
+
deps: CreatePaneTemplateDeps,
|
|
290
|
+
): Promise<void> {
|
|
291
|
+
const template = deps.pluginRegistry.paneTemplates.get(templateId);
|
|
292
|
+
if (!template) {
|
|
293
|
+
throw new Error(`Unknown pane template "${templateId}".`);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const state = deps.getState();
|
|
297
|
+
const pluginId = deps.pluginRegistry.getPaneTemplatePluginId(templateId);
|
|
298
|
+
if (pluginId && state.config.disabledPlugins.includes(pluginId)) {
|
|
299
|
+
throw new Error("Enable this plugin before creating its pane.");
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const { context, resolvedOptions } = await resolvePaneTemplateOptions(template, options, deps);
|
|
303
|
+
|
|
304
|
+
if (template.canCreate && !template.canCreate(context, resolvedOptions)) {
|
|
305
|
+
throw new Error(`Can't create ${template.label.toLowerCase()} right now.`);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const createInstanceResult = await template.createInstance?.(context, resolvedOptions);
|
|
309
|
+
if (createInstanceResult === null) {
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
const spec = createInstanceResult ?? {};
|
|
313
|
+
|
|
314
|
+
const paneDef = deps.pluginRegistry.panes.get(template.paneId);
|
|
315
|
+
if (!paneDef) {
|
|
316
|
+
throw new Error(`Unknown pane "${template.paneId}".`);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const instance = deps.buildPaneInstance(template.paneId, {
|
|
320
|
+
title: spec.title,
|
|
321
|
+
binding: spec.binding,
|
|
322
|
+
params: spec.params,
|
|
323
|
+
settings: spec.settings,
|
|
324
|
+
});
|
|
325
|
+
if (!instance) {
|
|
326
|
+
throw new Error("Open a matching ticker or collection context first.");
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
deps.placePaneInstance(instance, paneDef, spec);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
export async function applyPaneSettingFieldValue(
|
|
333
|
+
targetId: string,
|
|
334
|
+
field: PaneSettingField,
|
|
335
|
+
value: unknown,
|
|
336
|
+
deps: ApplyPaneSettingDeps,
|
|
337
|
+
options?: { pushHistory?: boolean },
|
|
338
|
+
): Promise<void> {
|
|
339
|
+
const descriptor = deps.pluginRegistry.resolvePaneSettings(targetId);
|
|
340
|
+
if (!descriptor) {
|
|
341
|
+
throw new Error("This pane does not expose settings.");
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const state = deps.getState();
|
|
345
|
+
const shouldPushHistory = options?.pushHistory !== false;
|
|
346
|
+
|
|
347
|
+
if (descriptor.pane.paneId === "quote-monitor" && field.key === "symbol") {
|
|
348
|
+
const rawQuery = typeof value === "string" ? value.trim() : "";
|
|
349
|
+
const resolvedTicker = await resolveTickerSearch({
|
|
350
|
+
query: rawQuery,
|
|
351
|
+
activeTicker: null,
|
|
352
|
+
tickers: state.tickers,
|
|
353
|
+
dataProvider: deps.dataProvider,
|
|
354
|
+
});
|
|
355
|
+
if (!resolvedTicker) {
|
|
356
|
+
throw new Error(`No ticker match found for "${rawQuery}".`);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const symbol = resolvedTicker.kind === "local"
|
|
360
|
+
? resolvedTicker.ticker.metadata.ticker
|
|
361
|
+
: resolvedTicker.symbol;
|
|
362
|
+
|
|
363
|
+
if (resolvedTicker.kind === "provider") {
|
|
364
|
+
const { ticker, created } = await upsertTickerFromSearchResult(
|
|
365
|
+
deps.tickerRepository,
|
|
366
|
+
resolvedTicker.result,
|
|
367
|
+
);
|
|
368
|
+
deps.dispatch({ type: "UPDATE_TICKER", ticker });
|
|
369
|
+
if (created) {
|
|
370
|
+
deps.pluginRegistry.events.emit("ticker:added", {
|
|
371
|
+
symbol: ticker.metadata.ticker,
|
|
372
|
+
ticker,
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const nextLayout = updatePaneInstance(state.config.layout, targetId, (instance) => ({
|
|
378
|
+
...instance,
|
|
379
|
+
title: symbol,
|
|
380
|
+
binding: { kind: "fixed", symbol },
|
|
381
|
+
settings: {
|
|
382
|
+
...(instance.settings ?? {}),
|
|
383
|
+
symbol,
|
|
384
|
+
},
|
|
385
|
+
}));
|
|
386
|
+
deps.persistLayout(nextLayout, { pushHistory: shouldPushHistory });
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if (descriptor.pane.paneId === COMPARISON_CHART_PANE_ID && field.key === "symbolsText") {
|
|
391
|
+
const rawInput = typeof value === "string" ? value : "";
|
|
392
|
+
const symbols = await resolveTickerListInput(
|
|
393
|
+
rawInput,
|
|
394
|
+
descriptor.context.activeCollectionId,
|
|
395
|
+
deps,
|
|
396
|
+
);
|
|
397
|
+
const nextLayout = updatePaneInstance(state.config.layout, targetId, (instance) => ({
|
|
398
|
+
...instance,
|
|
399
|
+
title: buildComparisonChartPaneTitle(symbols),
|
|
400
|
+
settings: {
|
|
401
|
+
...(instance.settings ?? {}),
|
|
402
|
+
symbols,
|
|
403
|
+
symbolsText: formatTickerListInput(symbols),
|
|
404
|
+
},
|
|
405
|
+
}));
|
|
406
|
+
deps.persistLayout(nextLayout, { pushHistory: shouldPushHistory });
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const nextSettings = {
|
|
411
|
+
...descriptor.context.settings,
|
|
412
|
+
[field.key]: value,
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
if (descriptor.pane.paneId === "portfolio-list" && field.key === "hideTabs" && value === true) {
|
|
416
|
+
const lockedCollectionId = typeof descriptor.context.paneState.collectionId === "string"
|
|
417
|
+
? descriptor.context.paneState.collectionId
|
|
418
|
+
: descriptor.context.activeCollectionId;
|
|
419
|
+
if (lockedCollectionId) {
|
|
420
|
+
nextSettings.lockedCollectionId = lockedCollectionId;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
if (descriptor.pane.paneId === "ticker-detail" && field.key === "hideTabs" && value === true) {
|
|
425
|
+
const lockedTabId = typeof descriptor.context.paneState.activeTabId === "string"
|
|
426
|
+
? descriptor.context.paneState.activeTabId
|
|
427
|
+
: "overview";
|
|
428
|
+
nextSettings.lockedTabId = lockedTabId;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const nextLayout = setPaneSettings(state.config.layout, targetId, nextSettings);
|
|
432
|
+
deps.persistLayout(nextLayout, { pushHistory: shouldPushHistory });
|
|
433
|
+
}
|