gloomberb 0.8.1 → 0.9.0
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 +106 -54
- package/package.json +14 -13
- package/patches/{@opentui%2Fcore@0.1.90.patch → @opentui%2Fcore@0.3.2.patch} +175 -90
- package/src/api-client/chat.ts +8 -0
- package/src/api-client/index.test.ts +30 -0
- package/src/api-client/index.ts +4 -0
- package/src/api-client/normalizers.ts +1 -0
- package/src/api-client/types.ts +1 -0
- package/src/app/global-shortcuts.test.tsx +54 -4
- package/src/app/global-shortcuts.ts +11 -5
- package/src/capabilities/factories.ts +4 -0
- package/src/components/chart/comparison/data.test.ts +54 -0
- package/src/components/chart/comparison/data.ts +47 -24
- package/src/components/chart/core/projection.ts +51 -27
- package/src/components/command-bar/action-runtime.ts +1 -0
- package/src/components/command-bar/layout-items/current-layout.test.ts +115 -0
- package/src/components/command-bar/layout-items/current-layout.ts +33 -0
- package/src/components/command-bar/list/view.tsx +3 -1
- package/src/components/command-bar/pane-templates/items.ts +4 -1
- package/src/components/command-bar/panel/body.tsx +1 -0
- package/src/components/command-bar/routes/root/results.ts +0 -30
- package/src/components/command-bar/routes/root/runtime.ts +2 -20
- package/src/components/command-bar/routes/ticker-search/actions.ts +42 -4
- package/src/components/command-bar/routes/ticker-search/route.ts +4 -1
- package/src/components/command-bar/routing/launch-request.ts +14 -7
- package/src/components/command-bar/routing/list-state.ts +1 -3
- package/src/components/command-bar/surface/index.test.tsx +76 -4
- package/src/components/command-bar/surface/index.tsx +0 -2
- package/src/components/command-bar/surface/pane.test.tsx +1 -0
- package/src/components/command-bar/surface/test-harness.tsx +1 -0
- package/src/components/command-bar/view-model.ts +3 -3
- package/src/components/data-table/stack-view.test.tsx +14 -11
- package/src/components/data-table/stack-view.tsx +1 -2
- package/src/components/data-table/view.test.tsx +24 -18
- package/src/components/data-table/view.tsx +313 -83
- package/src/components/feed-data-table/stack-view.tsx +6 -8
- package/src/components/layout/pane/footer/index.test.tsx +6 -0
- package/src/components/layout/pane/footer/index.tsx +14 -2
- package/src/components/layout/pane/title.ts +5 -1
- package/src/components/layout/shell/index.test.tsx +120 -2
- package/src/components/layout/shell/index.tsx +2 -0
- package/src/components/layout/shell/menu.ts +1 -7
- package/src/components/layout/shell/pane/actions.ts +8 -0
- package/src/components/layout/shell/pane/management-shortcuts.ts +12 -2
- package/src/components/layout/shell/shortcuts.ts +14 -6
- package/src/components/layout/shell/window-mode/index.ts +8 -3
- package/src/components/layout/window-edit/presentation.ts +4 -4
- package/src/components/onboarding/onboarding-steps.tsx +3 -2
- package/src/components/pane-settings-dialog/field-dialogs.tsx +0 -2
- package/src/components/pane-settings-dialog/tui.tsx +1 -1
- package/src/components/ticker/list-table-view.tsx +17 -25
- package/src/components/ui/choice-dialog.tsx +12 -2
- package/src/components/ui/multi-select/dialog.tsx +17 -5
- package/src/components/ui/remote-image.tsx +1 -14
- package/src/components/ui/shortcut-hint.tsx +14 -2
- package/src/components/ui/table-layout.test.ts +2 -2
- package/src/components/ui/table-layout.ts +8 -2
- package/src/components/ui/tabs.tsx +115 -86
- package/src/components/ui/ui.test.tsx +83 -4
- package/src/core/state/app/types.ts +12 -4
- package/src/market-data/coordinator/auxiliary.ts +29 -1
- package/src/market-data/coordinator/index.ts +17 -1
- package/src/market-data/hooks.tsx +17 -1
- package/src/market-data/selectors.ts +4 -0
- package/src/plugins/builtin/account-management/form-components.tsx +0 -4
- package/src/plugins/builtin/account-management/model.test.ts +54 -0
- package/src/plugins/builtin/account-management/model.ts +13 -3
- package/src/plugins/builtin/account-management/pane.tsx +5 -11
- package/src/plugins/builtin/account-management/password-dialog.tsx +1 -1
- package/src/plugins/builtin/alerts/pane.tsx +5 -3
- package/src/plugins/builtin/analytics/index.tsx +0 -3
- package/src/plugins/builtin/analytics/view.tsx +6 -5
- package/src/plugins/builtin/broker-manager/index.tsx +6 -6
- package/src/plugins/builtin/buildout/pane/index.tsx +6 -6
- package/src/plugins/builtin/changelog/index.tsx +40 -27
- package/src/plugins/builtin/changelog/model.test.ts +77 -0
- package/src/plugins/builtin/changelog/model.ts +98 -0
- package/src/plugins/builtin/chat/channel-labels.ts +53 -0
- package/src/plugins/builtin/chat/channels.test.ts +28 -0
- package/src/plugins/builtin/chat/channels.ts +36 -19
- package/src/plugins/builtin/chat/chat.test.tsx +264 -5
- package/src/plugins/builtin/chat/content/channel-navigation.ts +50 -11
- package/src/plugins/builtin/chat/content/composer-runtime.test.tsx +144 -0
- package/src/plugins/builtin/chat/content/composer-runtime.ts +151 -20
- package/src/plugins/builtin/chat/content/composer.tsx +23 -2
- package/src/plugins/builtin/chat/content/index.tsx +66 -4
- package/src/plugins/builtin/chat/content/layout-metrics.ts +3 -1
- package/src/plugins/builtin/chat/content/shortcuts.ts +27 -1
- package/src/plugins/builtin/chat/content/transcript.tsx +8 -0
- package/src/plugins/builtin/chat/content/types.ts +1 -0
- package/src/plugins/builtin/chat/controller/index.ts +41 -0
- package/src/plugins/builtin/chat/controller/messages.ts +18 -0
- package/src/plugins/builtin/chat/controller/notifications.ts +10 -4
- package/src/plugins/builtin/chat/controller/utils.ts +6 -3
- package/src/plugins/builtin/chat/controller/view.ts +1 -1
- package/src/plugins/builtin/chat/controller.test.ts +189 -0
- package/src/plugins/builtin/chat/edit-window.ts +24 -0
- package/src/plugins/builtin/chat/layout.ts +94 -31
- package/src/plugins/builtin/chat/message/action-chip.tsx +1 -0
- package/src/plugins/builtin/chat/message/desktop.tsx +53 -20
- package/src/plugins/builtin/chat/message/inline-tokens.tsx +16 -10
- package/src/plugins/builtin/chat/message/render-state.ts +5 -1
- package/src/plugins/builtin/chat/message/terminal.tsx +49 -20
- package/src/plugins/builtin/chat/message/types.ts +2 -0
- package/src/plugins/builtin/chat/pane.tsx +35 -7
- package/src/plugins/builtin/chat/sidebar.test.tsx +72 -0
- package/src/plugins/builtin/chat/status-widget.tsx +29 -11
- package/src/plugins/builtin/chat/test-harness.tsx +2 -0
- package/src/plugins/builtin/cloud/plugin.tsx +3 -0
- package/src/plugins/builtin/cloud-tweets/table.tsx +7 -11
- package/src/plugins/builtin/congress-trades/pane.tsx +14 -22
- package/src/plugins/builtin/earnings/index.tsx +8 -7
- package/src/plugins/builtin/econ/index.tsx +6 -8
- package/src/plugins/builtin/help/components.tsx +1 -1
- package/src/plugins/builtin/help/index.test.tsx +105 -30
- package/src/plugins/builtin/help/index.tsx +326 -164
- package/src/plugins/builtin/help/shortcut-model.ts +5 -6
- package/src/plugins/builtin/holders/pane.tsx +7 -6
- package/src/plugins/builtin/market-movers/index.tsx +6 -3
- package/src/plugins/builtin/news/wire/news/table.tsx +7 -6
- package/src/plugins/builtin/options/view.tsx +16 -8
- package/src/plugins/builtin/portfolio-list/column-values.ts +304 -1
- package/src/plugins/builtin/portfolio-list/metrics.test.ts +93 -0
- package/src/plugins/builtin/portfolio-list/pane/data.ts +7 -0
- package/src/plugins/builtin/portfolio-list/pane/index.tsx +25 -2
- package/src/plugins/builtin/portfolio-list/pane/supplemental.ts +281 -0
- package/src/plugins/builtin/portfolio-list/settings.ts +27 -0
- package/src/plugins/builtin/portfolio-list/table.tsx +4 -2
- package/src/plugins/builtin/portfolio-list/use-throttled-cursor-symbol.ts +10 -68
- package/src/plugins/builtin/research/analyst-pane.tsx +1 -2
- package/src/plugins/builtin/research/corporate-actions-pane.tsx +358 -7
- package/src/plugins/builtin/research/index.test.ts +30 -1
- package/src/plugins/builtin/research/relative-valuation-pane.tsx +6 -8
- package/src/plugins/builtin/sec/filing-documents.ts +66 -0
- package/src/plugins/builtin/sec/index.tsx +132 -20
- package/src/plugins/builtin/sectors/index.tsx +13 -23
- package/src/plugins/builtin/shared/ticker-request.ts +1 -0
- package/src/plugins/builtin/substack/api/auth.ts +185 -0
- package/src/plugins/builtin/substack/api/cache.ts +78 -0
- package/src/plugins/builtin/substack/api/loaders.ts +146 -0
- package/src/plugins/builtin/substack/api/store.ts +186 -0
- package/src/plugins/builtin/substack/api/types.ts +40 -0
- package/src/plugins/builtin/substack/api.test.ts +298 -0
- package/src/plugins/builtin/substack/article-detail.tsx +359 -0
- package/src/plugins/builtin/substack/article-stack.tsx +129 -0
- package/src/plugins/builtin/substack/content.ts +478 -0
- package/src/plugins/builtin/substack/feed-tabs.tsx +41 -0
- package/src/plugins/builtin/substack/index.tsx +49 -0
- package/src/plugins/builtin/substack/login-view.tsx +126 -0
- package/src/plugins/builtin/substack/model.test.ts +155 -0
- package/src/plugins/builtin/substack/normalize.ts +343 -0
- package/src/plugins/builtin/substack/pane-footer.ts +71 -0
- package/src/plugins/builtin/substack/pane-state.ts +170 -0
- package/src/plugins/builtin/substack/pane.tsx +500 -0
- package/src/plugins/builtin/substack/table.ts +111 -0
- package/src/plugins/builtin/substack/types.ts +71 -0
- package/src/plugins/builtin/substack/utils.ts +131 -0
- package/src/plugins/builtin/thirteenf/pane.tsx +26 -25
- package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/content.tsx +5 -4
- package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +5 -4
- package/src/plugins/builtin/ticker-detail/data-panes/provider-search.tsx +6 -5
- package/src/plugins/builtin/ticker-detail/financials/tab.tsx +10 -8
- package/src/plugins/builtin/ticker-detail/index.test.tsx +16 -2
- package/src/plugins/builtin/ticker-detail/index.tsx +1 -0
- package/src/plugins/builtin/ticker-detail/pane.tsx +13 -1
- package/src/plugins/builtin/world-indices/index.tsx +9 -6
- package/src/plugins/catalog-ui.ts +2 -0
- package/src/plugins/pane-manager/layout-state.ts +11 -0
- package/src/plugins/pane-manager.ts +1 -1
- package/src/plugins/prediction-markets/pane.tsx +10 -9
- package/src/plugins/registry/index.ts +3 -0
- package/src/plugins/runtime/context.tsx +1 -0
- package/src/plugins/runtime/index.tsx +4 -0
- package/src/react/use-throttled-commit-value.ts +116 -0
- package/src/renderers/electrobun/bun/desktop/http-fetch.test.ts +42 -0
- package/src/renderers/electrobun/bun/desktop/http-fetch.ts +5 -0
- package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +1 -0
- package/src/renderers/electrobun/view/data-table/index.tsx +3 -2
- package/src/renderers/electrobun/view/data-table/row.tsx +2 -2
- package/src/renderers/electrobun/view/host/input.tsx +23 -2
- package/src/renderers/electrobun/view/http-fetch.ts +10 -3
- package/src/renderers/electrobun/view/remote/asset-data-client.ts +1 -0
- package/src/sources/gloomberb-cloud/index.ts +4 -0
- package/src/sources/provider-router/cache.ts +1 -0
- package/src/sources/provider-router/document-routes.ts +45 -0
- package/src/sources/provider-router/index.ts +4 -0
- package/src/sources/sec-edgar/content.ts +9 -3
- package/src/sources/sec-edgar.test.ts +108 -1
- package/src/sources/sec-edgar.ts +105 -1
- package/src/sources/yahoo-finance.ts +4 -0
- package/src/state/app/context/index.test.tsx +16 -0
- package/src/test-support/plugin-runtime.ts +1 -0
- package/src/tickers/search/index.ts +1 -0
- package/src/types/data-provider.ts +11 -0
- package/src/ui/host.tsx +1 -0
- package/src/version.ts +1 -1
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
SubstackArticleSummary,
|
|
3
|
+
SubstackColumn,
|
|
4
|
+
SubstackPublication,
|
|
5
|
+
SubstackSortColumnId,
|
|
6
|
+
SubstackSortDirection,
|
|
7
|
+
} from "./types";
|
|
8
|
+
import { publicationKey } from "./normalize";
|
|
9
|
+
import { timestamp } from "./utils";
|
|
10
|
+
|
|
11
|
+
export function isSubstackSortColumnId(value: string): value is SubstackSortColumnId {
|
|
12
|
+
return value === "published" || value === "publication" || value === "title" || value === "read";
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function nextSubstackSort(
|
|
16
|
+
current: { columnId: SubstackSortColumnId; direction: SubstackSortDirection },
|
|
17
|
+
columnId: SubstackSortColumnId,
|
|
18
|
+
): { columnId: SubstackSortColumnId; direction: SubstackSortDirection } {
|
|
19
|
+
if (current.columnId === columnId) {
|
|
20
|
+
return { columnId, direction: current.direction === "desc" ? "asc" : "desc" };
|
|
21
|
+
}
|
|
22
|
+
return {
|
|
23
|
+
columnId,
|
|
24
|
+
direction: columnId === "title" || columnId === "publication" ? "asc" : "desc",
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function sortedSubstackArticles(
|
|
29
|
+
articles: SubstackArticleSummary[],
|
|
30
|
+
sort: { columnId: SubstackSortColumnId; direction: SubstackSortDirection },
|
|
31
|
+
): SubstackArticleSummary[] {
|
|
32
|
+
const direction = sort.direction === "asc" ? 1 : -1;
|
|
33
|
+
return [...articles].sort((a, b) => {
|
|
34
|
+
let result = 0;
|
|
35
|
+
switch (sort.columnId) {
|
|
36
|
+
case "published":
|
|
37
|
+
result = timestamp(a.publishedAt) - timestamp(b.publishedAt);
|
|
38
|
+
break;
|
|
39
|
+
case "publication":
|
|
40
|
+
result = (a.publicationName ?? "").localeCompare(b.publicationName ?? "");
|
|
41
|
+
break;
|
|
42
|
+
case "title":
|
|
43
|
+
result = a.title.localeCompare(b.title);
|
|
44
|
+
break;
|
|
45
|
+
case "read":
|
|
46
|
+
result = a.readMinutes - b.readMinutes;
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
return result === 0 ? timestamp(b.publishedAt) - timestamp(a.publishedAt) : result * direction;
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function formatPublishedAt(value: string | null): string {
|
|
54
|
+
if (!value) return "-";
|
|
55
|
+
const date = new Date(value);
|
|
56
|
+
if (Number.isNaN(date.getTime())) return "-";
|
|
57
|
+
const now = new Date();
|
|
58
|
+
const sameYear = date.getFullYear() === now.getFullYear();
|
|
59
|
+
const datePart = date.toLocaleDateString("en-US", {
|
|
60
|
+
month: "short",
|
|
61
|
+
day: "numeric",
|
|
62
|
+
...(sameYear ? {} : { year: "2-digit" as const }),
|
|
63
|
+
});
|
|
64
|
+
const timePart = date.toLocaleTimeString("en-US", {
|
|
65
|
+
hour: "2-digit",
|
|
66
|
+
minute: "2-digit",
|
|
67
|
+
hour12: false,
|
|
68
|
+
});
|
|
69
|
+
return `${datePart} ${timePart}`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function formatReadTime(minutes: number): string {
|
|
73
|
+
return `${Math.max(1, Math.ceil(minutes))}m`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function formatWordCount(words: number): string {
|
|
77
|
+
if (words >= 1000) return `${(words / 1000).toFixed(words >= 10_000 ? 0 : 1)}k words`;
|
|
78
|
+
return `${Math.max(0, Math.round(words))} words`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function buildSubstackColumns(width: number, includePublication: boolean): SubstackColumn[] {
|
|
82
|
+
const publishedWidth = 13;
|
|
83
|
+
const readWidth = 5;
|
|
84
|
+
const publicationWidth = includePublication ? Math.max(12, Math.min(21, Math.floor(width * 0.18))) : 0;
|
|
85
|
+
const fixed = publishedWidth + readWidth + publicationWidth;
|
|
86
|
+
const titleWidth = Math.max(18, width - fixed - 4);
|
|
87
|
+
const columns: SubstackColumn[] = [
|
|
88
|
+
{ id: "published", label: "Published", width: publishedWidth, align: "left" },
|
|
89
|
+
];
|
|
90
|
+
if (includePublication) {
|
|
91
|
+
columns.push({ id: "publication", label: "Publication", width: publicationWidth, align: "left" });
|
|
92
|
+
}
|
|
93
|
+
columns.push(
|
|
94
|
+
{ id: "title", label: "Title", width: titleWidth, align: "left", flexGrow: 1 },
|
|
95
|
+
{ id: "read", label: "Read", width: readWidth, align: "right" },
|
|
96
|
+
);
|
|
97
|
+
return columns;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function tabIdForPublication(publication: SubstackPublication): string {
|
|
101
|
+
return `pub:${publicationKey(publication)}`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function publicationFromTabId(
|
|
105
|
+
tabId: string,
|
|
106
|
+
publications: SubstackPublication[],
|
|
107
|
+
): SubstackPublication | null {
|
|
108
|
+
if (!tabId.startsWith("pub:")) return null;
|
|
109
|
+
const key = tabId.slice(4);
|
|
110
|
+
return publications.find((publication) => publicationKey(publication) === key) ?? null;
|
|
111
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { DataTableColumn } from "../../../components";
|
|
2
|
+
|
|
3
|
+
export const SUBSTACK_PANE_ID = "substack";
|
|
4
|
+
export const SUBSTACK_PLUGIN_ID = "substack";
|
|
5
|
+
export const SUBSTACK_FEED_TAB_ID = "feed";
|
|
6
|
+
|
|
7
|
+
export interface SubstackPublication {
|
|
8
|
+
id: string;
|
|
9
|
+
name: string;
|
|
10
|
+
subdomain: string | null;
|
|
11
|
+
baseUrl: string | null;
|
|
12
|
+
description: string | null;
|
|
13
|
+
logoUrl: string | null;
|
|
14
|
+
latestPublishedAt: string | null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ExtractedArticleContent {
|
|
18
|
+
text: string;
|
|
19
|
+
blocks: SubstackContentBlock[];
|
|
20
|
+
imageUrls: string[];
|
|
21
|
+
linkUrls: string[];
|
|
22
|
+
wordCount: number;
|
|
23
|
+
readMinutes: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type SubstackContentBlock =
|
|
27
|
+
| { type: "heading"; text: string; level: number }
|
|
28
|
+
| { type: "paragraph"; text: string }
|
|
29
|
+
| { type: "quote"; text: string }
|
|
30
|
+
| { type: "listItem"; text: string }
|
|
31
|
+
| { type: "image"; url: string; alt: string | null }
|
|
32
|
+
| {
|
|
33
|
+
type: "embed";
|
|
34
|
+
kind: "tweet";
|
|
35
|
+
text: string;
|
|
36
|
+
url: string | null;
|
|
37
|
+
username: string | null;
|
|
38
|
+
authorName: string | null;
|
|
39
|
+
dateLabel: string | null;
|
|
40
|
+
imageUrls: string[];
|
|
41
|
+
}
|
|
42
|
+
| { type: "embed"; kind: "link" | "media"; text: string; url: string | null }
|
|
43
|
+
| { type: "divider" };
|
|
44
|
+
|
|
45
|
+
export interface SubstackArticleSummary {
|
|
46
|
+
id: string;
|
|
47
|
+
title: string;
|
|
48
|
+
publicationId: string | null;
|
|
49
|
+
publicationName: string | null;
|
|
50
|
+
publicationSubdomain: string | null;
|
|
51
|
+
publicationBaseUrl: string | null;
|
|
52
|
+
url: string | null;
|
|
53
|
+
slug: string | null;
|
|
54
|
+
publishedAt: string | null;
|
|
55
|
+
subtitle: string | null;
|
|
56
|
+
previewText: string | null;
|
|
57
|
+
bodyHtml: string | null;
|
|
58
|
+
imageUrls: string[];
|
|
59
|
+
wordCount: number;
|
|
60
|
+
readMinutes: number;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface SubstackArticleDetail extends SubstackArticleSummary {
|
|
64
|
+
contentText: string;
|
|
65
|
+
contentBlocks: SubstackContentBlock[];
|
|
66
|
+
linkUrls: string[];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export type SubstackSortColumnId = "published" | "publication" | "title" | "read";
|
|
70
|
+
export type SubstackSortDirection = "asc" | "desc";
|
|
71
|
+
export type SubstackColumn = DataTableColumn & { id: SubstackSortColumnId };
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { decodeHtmlEntities } from "../../../utils/html-entities";
|
|
2
|
+
import { normalizedHttpUrl } from "../../../utils/url";
|
|
3
|
+
|
|
4
|
+
export type JsonRecord = Record<string, unknown>;
|
|
5
|
+
|
|
6
|
+
function isRecord(value: unknown): value is JsonRecord {
|
|
7
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function asRecord(value: unknown): JsonRecord | null {
|
|
11
|
+
return isRecord(value) ? value : null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function stringValue(value: unknown): string | null {
|
|
15
|
+
if (typeof value !== "string") return null;
|
|
16
|
+
const trimmed = decodeHtmlEntities(value).replace(/\s+/g, " ").trim();
|
|
17
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function rawStringValue(value: unknown): string | null {
|
|
21
|
+
if (typeof value !== "string") return null;
|
|
22
|
+
const trimmed = value.trim();
|
|
23
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function numberValue(value: unknown): number | null {
|
|
27
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
28
|
+
if (typeof value === "string" && value.trim()) {
|
|
29
|
+
const parsed = Number(value);
|
|
30
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function firstString(record: JsonRecord | null, keys: string[]): string | null {
|
|
36
|
+
if (!record) return null;
|
|
37
|
+
for (const key of keys) {
|
|
38
|
+
const value = stringValue(record[key]);
|
|
39
|
+
if (value) return value;
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function firstRawString(record: JsonRecord | null, keys: string[]): string | null {
|
|
45
|
+
if (!record) return null;
|
|
46
|
+
for (const key of keys) {
|
|
47
|
+
const value = rawStringValue(record[key]);
|
|
48
|
+
if (value) return value;
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function firstNumber(record: JsonRecord | null, keys: string[]): number | null {
|
|
54
|
+
if (!record) return null;
|
|
55
|
+
for (const key of keys) {
|
|
56
|
+
const value = numberValue(record[key]);
|
|
57
|
+
if (value != null) return value;
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function firstValue(record: JsonRecord | null, keys: string[]): unknown {
|
|
63
|
+
if (!record) return null;
|
|
64
|
+
for (const key of keys) {
|
|
65
|
+
if (record[key] != null) return record[key];
|
|
66
|
+
}
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function firstRecord(...values: unknown[]): JsonRecord | null {
|
|
71
|
+
for (const value of values) {
|
|
72
|
+
const record = asRecord(value);
|
|
73
|
+
if (record) return record;
|
|
74
|
+
}
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function parseDateIso(value: unknown): string | null {
|
|
79
|
+
if (value == null) return null;
|
|
80
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
81
|
+
const ms = value > 10_000_000_000 ? value : value * 1000;
|
|
82
|
+
const date = new Date(ms);
|
|
83
|
+
return Number.isNaN(date.getTime()) ? null : date.toISOString();
|
|
84
|
+
}
|
|
85
|
+
if (typeof value !== "string" || !value.trim()) return null;
|
|
86
|
+
const date = new Date(value);
|
|
87
|
+
return Number.isNaN(date.getTime()) ? null : date.toISOString();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function normalizeUrl(value: unknown, baseUrl?: string | null): string | null {
|
|
91
|
+
const raw = rawStringValue(value);
|
|
92
|
+
if (!raw) return null;
|
|
93
|
+
const direct = normalizedHttpUrl(raw);
|
|
94
|
+
if (direct) return direct;
|
|
95
|
+
if (!baseUrl) return null;
|
|
96
|
+
try {
|
|
97
|
+
const resolved = new URL(raw, baseUrl);
|
|
98
|
+
return normalizedHttpUrl(resolved.toString());
|
|
99
|
+
} catch {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function uniqueStrings(values: Array<string | null | undefined>): string[] {
|
|
105
|
+
const seen = new Set<string>();
|
|
106
|
+
const result: string[] = [];
|
|
107
|
+
for (const value of values) {
|
|
108
|
+
if (!value || seen.has(value)) continue;
|
|
109
|
+
seen.add(value);
|
|
110
|
+
result.push(value);
|
|
111
|
+
}
|
|
112
|
+
return result;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function timestamp(value: string | null | undefined): number {
|
|
116
|
+
if (!value) return 0;
|
|
117
|
+
const date = new Date(value);
|
|
118
|
+
return Number.isNaN(date.getTime()) ? 0 : date.getTime();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function maxIso(a: string | null, b: string | null): string | null {
|
|
122
|
+
if (!a) return b;
|
|
123
|
+
if (!b) return a;
|
|
124
|
+
return timestamp(a) >= timestamp(b) ? a : b;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function extractAttribute(tag: string, name: string): string | null {
|
|
128
|
+
const pattern = new RegExp(`\\b${name}\\s*=\\s*(?:"([^"]*)"|'([^']*)'|([^\\s>]+))`, "i");
|
|
129
|
+
const match = tag.match(pattern);
|
|
130
|
+
return rawStringValue(match?.[1] ?? match?.[2] ?? match?.[3] ?? null);
|
|
131
|
+
}
|
|
@@ -344,9 +344,13 @@ export function ThirteenFPane({ focused, width, height }: PaneProps) {
|
|
|
344
344
|
) : (
|
|
345
345
|
<Box flexGrow={1} />
|
|
346
346
|
)}
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
347
|
+
selection={{
|
|
348
|
+
kind: "id",
|
|
349
|
+
selectedId,
|
|
350
|
+
getId: (row) => row.id,
|
|
351
|
+
onChange: (id) => setSelectedId(id),
|
|
352
|
+
}}
|
|
353
|
+
onActivate={openDetail}
|
|
350
354
|
onRootKeyDown={handleRootKeyDown}
|
|
351
355
|
rootWidth={width}
|
|
352
356
|
rootBefore={rootBefore}
|
|
@@ -365,9 +369,6 @@ export function ThirteenFPane({ focused, width, height }: PaneProps) {
|
|
|
365
369
|
));
|
|
366
370
|
}}
|
|
367
371
|
getItemKey={(row) => row.id}
|
|
368
|
-
isSelected={(row) => row.id === selectedId}
|
|
369
|
-
onSelect={(row) => setSelectedId(row.id)}
|
|
370
|
-
onActivate={openDetail}
|
|
371
372
|
renderCell={renderBrowserCell}
|
|
372
373
|
emptyStateTitle={emptyTitle}
|
|
373
374
|
showHorizontalScrollbar={false}
|
|
@@ -625,11 +626,13 @@ function FundDetailView({
|
|
|
625
626
|
) : (
|
|
626
627
|
<Box flexGrow={1} />
|
|
627
628
|
)}
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
629
|
+
selection={{
|
|
630
|
+
kind: "id",
|
|
631
|
+
selectedId: filingSelectedId,
|
|
632
|
+
getId: (row) => row.id,
|
|
633
|
+
onChange: (id) => setFilingSelectedId(id),
|
|
632
634
|
}}
|
|
635
|
+
onActivate={openFilingInPane}
|
|
633
636
|
onDetailKeyDown={(event) => {
|
|
634
637
|
if (event.name !== "o" || !openFiling?.url) return false;
|
|
635
638
|
event.preventDefault?.();
|
|
@@ -648,11 +651,6 @@ function FundDetailView({
|
|
|
648
651
|
columnId === "period" || columnId === "filed" ? "desc" : "desc",
|
|
649
652
|
))}
|
|
650
653
|
getItemKey={(row) => row.id}
|
|
651
|
-
isSelected={(row) => row.id === filingSelectedId}
|
|
652
|
-
onSelect={(row) => setFilingSelectedId(row.id)}
|
|
653
|
-
onActivate={(row) => {
|
|
654
|
-
openFilingInPane(row);
|
|
655
|
-
}}
|
|
656
654
|
renderCell={renderTimelineCell}
|
|
657
655
|
emptyStateTitle="No 13F filings."
|
|
658
656
|
showHorizontalScrollbar={false}
|
|
@@ -660,8 +658,12 @@ function FundDetailView({
|
|
|
660
658
|
) : (
|
|
661
659
|
<DataTableView<FundHoldingRow, FundHoldingColumn>
|
|
662
660
|
focused={focused}
|
|
663
|
-
|
|
664
|
-
|
|
661
|
+
selection={{
|
|
662
|
+
kind: "id",
|
|
663
|
+
selectedId: holdingSelectedId,
|
|
664
|
+
getId: (row) => row.id,
|
|
665
|
+
onChange: (id) => setHoldingSelectedId(id),
|
|
666
|
+
}}
|
|
665
667
|
rootWidth={width}
|
|
666
668
|
columns={buildHoldingColumns(width)}
|
|
667
669
|
items={visibleHoldingRows}
|
|
@@ -675,8 +677,6 @@ function FundDetailView({
|
|
|
675
677
|
));
|
|
676
678
|
}}
|
|
677
679
|
getItemKey={(row) => row.id}
|
|
678
|
-
isSelected={(row) => row.id === holdingSelectedId}
|
|
679
|
-
onSelect={(row) => setHoldingSelectedId(row.id)}
|
|
680
680
|
onActivate={(row) => {
|
|
681
681
|
if (row.ticker) pinTicker(row.ticker, { floating: true, paneType: TICKER_RESEARCH_PANE_ID });
|
|
682
682
|
}}
|
|
@@ -795,9 +795,13 @@ function FilingDetailView({
|
|
|
795
795
|
<Box flexDirection="column" width={width} flexGrow={1} overflow="hidden">
|
|
796
796
|
<DataTableView<FilingPositionRow, FilingPositionColumn>
|
|
797
797
|
focused={focused}
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
798
|
+
selection={{
|
|
799
|
+
kind: "id",
|
|
800
|
+
selectedId: selectedPositionId,
|
|
801
|
+
getId: (row) => row.id,
|
|
802
|
+
onChange: (id) => setSelectedPositionId(id),
|
|
803
|
+
}}
|
|
804
|
+
onActivate={openPositionTicker}
|
|
801
805
|
onRootKeyDown={handlePositionKeyDown}
|
|
802
806
|
rootWidth={width}
|
|
803
807
|
rootBefore={summary}
|
|
@@ -814,9 +818,6 @@ function FilingDetailView({
|
|
|
814
818
|
));
|
|
815
819
|
}}
|
|
816
820
|
getItemKey={(row) => row.id}
|
|
817
|
-
isSelected={(row) => row.id === selectedPositionId}
|
|
818
|
-
onSelect={(row) => setSelectedPositionId(row.id)}
|
|
819
|
-
onActivate={openPositionTicker}
|
|
820
821
|
renderCell={renderFilingPositionCell}
|
|
821
822
|
emptyStateTitle={emptyTitle}
|
|
822
823
|
showHorizontalScrollbar={false}
|
|
@@ -137,8 +137,11 @@ export function FundamentalGraphContent({
|
|
|
137
137
|
<StaticBarChartSurface width={width} height={chartHeight} series={chartSeries} />
|
|
138
138
|
<DataTableView<FundamentalGraphRow, FundamentalColumn>
|
|
139
139
|
focused={focused}
|
|
140
|
-
|
|
141
|
-
|
|
140
|
+
selection={{
|
|
141
|
+
kind: "index",
|
|
142
|
+
selectedIndex: boundedSelectedIdx,
|
|
143
|
+
onChange: (index) => setSelectedIdx(index),
|
|
144
|
+
}}
|
|
142
145
|
onRootKeyDown={handleKeyDown}
|
|
143
146
|
rootWidth={width}
|
|
144
147
|
rootHeight={tableHeight}
|
|
@@ -148,8 +151,6 @@ export function FundamentalGraphContent({
|
|
|
148
151
|
sortDirection="asc"
|
|
149
152
|
onHeaderClick={() => {}}
|
|
150
153
|
getItemKey={(row) => row.key}
|
|
151
|
-
isSelected={(_row, index) => index === boundedSelectedIdx}
|
|
152
|
-
onSelect={(_row, index) => setSelectedIdx(index)}
|
|
153
154
|
renderCell={renderCell}
|
|
154
155
|
emptyStateTitle={loading ? "Loading fundamentals..." : "No graph data"}
|
|
155
156
|
/>
|
|
@@ -172,8 +172,11 @@ export function HistoricalPricesPane({ focused, width, height }: PaneProps) {
|
|
|
172
172
|
return (
|
|
173
173
|
<DataTableView<HistoricalPriceRow, HistoryColumn>
|
|
174
174
|
focused={focused}
|
|
175
|
-
|
|
176
|
-
|
|
175
|
+
selection={{
|
|
176
|
+
kind: "index",
|
|
177
|
+
selectedIndex: boundedSelectedIdx,
|
|
178
|
+
onChange: (index) => setSelectedIdx(index),
|
|
179
|
+
}}
|
|
177
180
|
onRootKeyDown={handleKeyDown}
|
|
178
181
|
rootWidth={width}
|
|
179
182
|
rootHeight={height}
|
|
@@ -183,8 +186,6 @@ export function HistoricalPricesPane({ focused, width, height }: PaneProps) {
|
|
|
183
186
|
sortDirection="desc"
|
|
184
187
|
onHeaderClick={() => {}}
|
|
185
188
|
getItemKey={(row) => row.key}
|
|
186
|
-
isSelected={(_row, index) => index === boundedSelectedIdx}
|
|
187
|
-
onSelect={(_row, index) => setSelectedIdx(index)}
|
|
188
189
|
renderCell={renderCell}
|
|
189
190
|
emptyStateTitle={loading ? "Loading historical prices..." : "No historical prices"}
|
|
190
191
|
/>
|
|
@@ -121,9 +121,12 @@ export function ProviderSearchPane({ focused, width, height }: PaneProps) {
|
|
|
121
121
|
return (
|
|
122
122
|
<DataTableView<InstrumentSearchResult, DataTableColumn & { id: "symbol" | "name" | "exchange" | "type" }>
|
|
123
123
|
focused={focused}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
124
|
+
selection={{
|
|
125
|
+
kind: "index",
|
|
126
|
+
selectedIndex: boundedSelectedIdx,
|
|
127
|
+
onChange: (index) => setSelectedIdx(index),
|
|
128
|
+
}}
|
|
129
|
+
onActivate={(row) => openResult(row)}
|
|
127
130
|
onRootKeyDown={handleKeyDown}
|
|
128
131
|
rootWidth={width}
|
|
129
132
|
rootHeight={height}
|
|
@@ -133,8 +136,6 @@ export function ProviderSearchPane({ focused, width, height }: PaneProps) {
|
|
|
133
136
|
sortDirection="asc"
|
|
134
137
|
onHeaderClick={() => {}}
|
|
135
138
|
getItemKey={(row, index) => `${row.providerId}:${row.symbol}:${row.exchange}:${row.type}:${index}`}
|
|
136
|
-
isSelected={(_row, index) => index === boundedSelectedIdx}
|
|
137
|
-
onSelect={(_row, index) => setSelectedIdx(index)}
|
|
138
139
|
renderCell={renderCell}
|
|
139
140
|
emptyStateTitle={state.loading ? "Searching..." : query ? "No search results" : "No search query"}
|
|
140
141
|
/>
|
|
@@ -380,19 +380,21 @@ export function ResolvedFinancialsTab({
|
|
|
380
380
|
bodyScrollId={bodyScrollId}
|
|
381
381
|
columns={columns}
|
|
382
382
|
items={rows}
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
383
|
+
selection={{
|
|
384
|
+
kind: "id",
|
|
385
|
+
selectedId: selectedRowId,
|
|
386
|
+
getId: (row) => row.id,
|
|
387
|
+
onChange: (_id, row, _index, reason) => {
|
|
388
|
+
setSelectedRowId(row.id);
|
|
389
|
+
if (reason === "pointer" && row.kind === "group" && row.toggleable) {
|
|
390
|
+
toggleGroup(row.id);
|
|
391
|
+
}
|
|
392
|
+
},
|
|
386
393
|
}}
|
|
387
394
|
sortColumnId={null}
|
|
388
395
|
sortDirection="desc"
|
|
389
396
|
onHeaderClick={() => {}}
|
|
390
397
|
getItemKey={(row) => row.id}
|
|
391
|
-
isSelected={(_row, index) => index === effectiveSelectedIndex}
|
|
392
|
-
onSelect={(row) => {
|
|
393
|
-
setSelectedRowId(row.id);
|
|
394
|
-
if (row.kind === "group" && row.toggleable) toggleGroup(row.id);
|
|
395
|
-
}}
|
|
396
398
|
onActivate={(row) => {
|
|
397
399
|
if (row.kind === "group" && row.toggleable) toggleGroup(row.id);
|
|
398
400
|
}}
|
|
@@ -41,6 +41,7 @@ let detailHarnessState: ReturnType<typeof createInitialState> | null = null;
|
|
|
41
41
|
let sharedCoordinator: MarketDataCoordinator | null = null;
|
|
42
42
|
|
|
43
43
|
const runtime = createTestPluginRuntime();
|
|
44
|
+
const TICKER_TAB_SETTLE_MS = 160;
|
|
44
45
|
|
|
45
46
|
const DetailPane = tickerDetailPlugin.panes![0]!.component as (props: {
|
|
46
47
|
paneId: string;
|
|
@@ -160,7 +161,7 @@ function setOptionsProvider(provider: DataProvider | undefined): void {
|
|
|
160
161
|
setSharedMarketDataCoordinator(sharedCoordinator);
|
|
161
162
|
}
|
|
162
163
|
|
|
163
|
-
function makeRegistry(): PluginRegistry {
|
|
164
|
+
function makeRegistry(options: { stubFundamentalGraphs?: boolean } = {}): PluginRegistry {
|
|
164
165
|
const stubTab = (_props: { width: number; height: number; focused: boolean; onCapture: (capturing: boolean) => void }) => (
|
|
165
166
|
<text>stub</text>
|
|
166
167
|
);
|
|
@@ -168,6 +169,10 @@ function makeRegistry(): PluginRegistry {
|
|
|
168
169
|
tickerDetailPlugin.setup?.({
|
|
169
170
|
registerTickerResearchTab: (tab: TickerResearchTabDef) => tickerResearchTabs.set(tab.id, tab),
|
|
170
171
|
} as any);
|
|
172
|
+
if (options.stubFundamentalGraphs) {
|
|
173
|
+
const tab = tickerResearchTabs.get("fundamental-graphs");
|
|
174
|
+
if (tab) tickerResearchTabs.set(tab.id, { ...tab, component: stubTab });
|
|
175
|
+
}
|
|
171
176
|
for (const tab of [
|
|
172
177
|
["ibkr-trade", {
|
|
173
178
|
id: "ibkr-trade",
|
|
@@ -280,6 +285,13 @@ async function flushFrame() {
|
|
|
280
285
|
});
|
|
281
286
|
}
|
|
282
287
|
|
|
288
|
+
async function settleTickerTabCommit() {
|
|
289
|
+
await act(async () => {
|
|
290
|
+
await new Promise((resolve) => setTimeout(resolve, TICKER_TAB_SETTLE_MS));
|
|
291
|
+
await testSetup!.renderOnce();
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
|
|
283
295
|
async function clickFrameText(text: string) {
|
|
284
296
|
const lines = testSetup!.captureCharFrame().split("\n");
|
|
285
297
|
const row = lines.findIndex((line) => line.includes(text));
|
|
@@ -482,7 +494,7 @@ describe("TickerResearchPane", () => {
|
|
|
482
494
|
});
|
|
483
495
|
|
|
484
496
|
test("lets ticker detail tab arrows leave the embedded financials tab", async () => {
|
|
485
|
-
setSharedRegistryForTests(makeRegistry());
|
|
497
|
+
setSharedRegistryForTests(makeRegistry({ stubFundamentalGraphs: true }));
|
|
486
498
|
setOptionsProvider(createProvider(false));
|
|
487
499
|
|
|
488
500
|
testSetup = await testRender(
|
|
@@ -506,6 +518,7 @@ describe("TickerResearchPane", () => {
|
|
|
506
518
|
});
|
|
507
519
|
await flushFrame();
|
|
508
520
|
|
|
521
|
+
await settleTickerTabCommit();
|
|
509
522
|
expect(detailHarnessState?.paneState[TEST_PANE_ID]?.activeTabId).toBe("fundamental-graphs");
|
|
510
523
|
|
|
511
524
|
await act(async () => {
|
|
@@ -514,6 +527,7 @@ describe("TickerResearchPane", () => {
|
|
|
514
527
|
});
|
|
515
528
|
await flushFrame();
|
|
516
529
|
|
|
530
|
+
await settleTickerTabCommit();
|
|
517
531
|
expect(detailHarnessState?.paneState[TEST_PANE_ID]?.activeTabId).toBe("financials");
|
|
518
532
|
});
|
|
519
533
|
|
|
@@ -146,6 +146,7 @@ export const tickerDetailPlugin: GloomPlugin = {
|
|
|
146
146
|
label: "Ticker Research",
|
|
147
147
|
description: "Open another research pane for the selected ticker or current collection",
|
|
148
148
|
keywords: ["new", "ticker", "research", "detail", "pane", "inspector"],
|
|
149
|
+
shortcut: { prefix: "T", argPlaceholder: "ticker", argKind: "ticker" },
|
|
149
150
|
canCreate: (context) => context.activeTicker !== null || context.activeCollectionId !== null,
|
|
150
151
|
createInstance: (context) => (
|
|
151
152
|
context.activeTicker
|
|
@@ -14,6 +14,7 @@ import { useQuoteStreaming } from "../../../state/hooks/quote-streaming";
|
|
|
14
14
|
import { getCollectionName, getCollectionTickerCount } from "../../../state/selectors";
|
|
15
15
|
import { getSharedRegistry } from "../../registry";
|
|
16
16
|
import { EmptyState, PaneFooterScope, Tabs } from "../../../components";
|
|
17
|
+
import { useThrottledCommitValue } from "../../../react/use-throttled-commit-value";
|
|
17
18
|
import { resolveOptionsTarget } from "../../../utils/options";
|
|
18
19
|
import {
|
|
19
20
|
buildVisibleTickerResearchTabs,
|
|
@@ -21,6 +22,8 @@ import {
|
|
|
21
22
|
resolveLockedTabId,
|
|
22
23
|
} from "./settings";
|
|
23
24
|
|
|
25
|
+
const TICKER_RESEARCH_TAB_COMMIT_DELAY_MS = 120;
|
|
26
|
+
|
|
24
27
|
function sameStringSet(left: Set<string>, right: Set<string>): boolean {
|
|
25
28
|
if (left.size !== right.size) return false;
|
|
26
29
|
for (const value of left) {
|
|
@@ -79,7 +82,16 @@ export function TickerResearchPane({ focused, width, height }: PaneProps) {
|
|
|
79
82
|
useQuoteStreaming(streamingTargets);
|
|
80
83
|
|
|
81
84
|
const { collectionId } = usePaneCollection();
|
|
82
|
-
const [
|
|
85
|
+
const [committedActiveTabId, setCommittedActiveTabId] = usePaneStateValue<string>("activeTabId", "overview");
|
|
86
|
+
const {
|
|
87
|
+
value: activeTabId,
|
|
88
|
+
setValue: setActiveTabId,
|
|
89
|
+
} = useThrottledCommitValue(
|
|
90
|
+
committedActiveTabId,
|
|
91
|
+
setCommittedActiveTabId,
|
|
92
|
+
TICKER_RESEARCH_TAB_COMMIT_DELAY_MS,
|
|
93
|
+
{ commitPendingOnUnmount: true },
|
|
94
|
+
);
|
|
83
95
|
const [pluginCaptured, setPluginCaptured] = useState(false);
|
|
84
96
|
const [mountedTabIds, setMountedTabIds] = useState<Set<string>>(() => new Set());
|
|
85
97
|
const paneSettings = getTickerResearchPaneSettings(paneInstance?.settings);
|
|
@@ -162,10 +162,16 @@ function WorldIndicesPane({ focused, width, height }: PaneProps) {
|
|
|
162
162
|
return (
|
|
163
163
|
<DataTableView<WorldIndexTableRow, WorldIndexColumn>
|
|
164
164
|
focused={focused}
|
|
165
|
-
|
|
165
|
+
selection={{
|
|
166
|
+
kind: "id",
|
|
167
|
+
selectedId: selectedSymbol,
|
|
168
|
+
getId: (row) => row.type === "row" ? row.entry.symbol : `header-${row.region}`,
|
|
169
|
+
onChange: (_id, row, index) => {
|
|
170
|
+
if (row.type === "row") selectFlatIndex(index);
|
|
171
|
+
},
|
|
172
|
+
}}
|
|
166
173
|
isNavigable={(row) => row.type === "row"}
|
|
167
|
-
|
|
168
|
-
onActivateIndex={(index) => openSelected(index)}
|
|
174
|
+
onActivate={(_row, index) => openSelected(index)}
|
|
169
175
|
rootWidth={width}
|
|
170
176
|
rootHeight={height}
|
|
171
177
|
columns={columns}
|
|
@@ -174,9 +180,6 @@ function WorldIndicesPane({ focused, width, height }: PaneProps) {
|
|
|
174
180
|
sortDirection={sortPreference.direction}
|
|
175
181
|
onHeaderClick={handleHeaderClick}
|
|
176
182
|
getItemKey={(row) => row.type === "header" ? `header-${row.region}` : row.entry.symbol}
|
|
177
|
-
isSelected={(row) => row.type === "row" && row.entry.symbol === selectedSymbol}
|
|
178
|
-
onSelect={(_row, index) => selectFlatIndex(index)}
|
|
179
|
-
onActivate={(_row, index) => openSelected(index)}
|
|
180
183
|
renderSectionHeader={(row) => row.type === "header"
|
|
181
184
|
? { text: REGION_LABELS[row.region] }
|
|
182
185
|
: null}
|
|
@@ -2,6 +2,7 @@ import type { GloomPlugin } from "../types/plugin";
|
|
|
2
2
|
import { portfolioListPlugin } from "./builtin/portfolio-list";
|
|
3
3
|
import { newsPlugin } from "./builtin/news";
|
|
4
4
|
import { notesPlugin } from "./builtin/notes";
|
|
5
|
+
import { substackPlugin } from "./builtin/substack";
|
|
5
6
|
import { aiPlugin } from "./builtin/ai";
|
|
6
7
|
import { gloomberbCloudPlugin } from "./builtin/cloud";
|
|
7
8
|
import { changelogPlugin } from "./builtin/changelog";
|
|
@@ -26,6 +27,7 @@ export const uiBuiltinPlugins: GloomPlugin[] = [
|
|
|
26
27
|
ibkrPlugin,
|
|
27
28
|
layoutManagerPlugin,
|
|
28
29
|
newsPlugin,
|
|
30
|
+
substackPlugin,
|
|
29
31
|
notesPlugin,
|
|
30
32
|
aiPlugin,
|
|
31
33
|
changelogPlugin,
|