gloomberb 0.6.1 → 0.7.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/package.json +1 -1
- package/patches/@opentui%2Fcore@0.1.90.patch +48 -24
- package/src/app.tsx +158 -57
- package/src/capabilities/factories.ts +3 -0
- package/src/cli/registry.test.ts +0 -1
- package/src/components/chart/chart-controller.ts +0 -1
- package/src/components/chart/chart-data.ts +1 -31
- package/src/components/chart/chart-renderer.ts +94 -12
- package/src/components/chart/chart-resolution.test.ts +0 -36
- package/src/components/chart/chart-resolution.ts +0 -27
- package/src/components/chart/chart.test.ts +60 -45
- package/src/components/chart/comparison-chart-data.test.ts +1 -5
- package/src/components/chart/comparison-chart-data.ts +1 -4
- package/src/components/chart/comparison-chart-renderer.test.ts +26 -0
- package/src/components/chart/comparison-chart-renderer.ts +13 -6
- package/src/components/chart/comparison-stock-chart.tsx +63 -39
- package/src/components/chart/price-axis-labels.tsx +114 -0
- package/src/components/chart/static-chart-surface.tsx +30 -4
- package/src/components/chart/stock-chart.test.ts +0 -16
- package/src/components/chart/stock-chart.tsx +69 -41
- package/src/components/chart/time-axis-label.tsx +120 -0
- package/src/components/command-bar/__snapshots__/command-bar.test.tsx.snap +0 -28
- package/src/components/command-bar/command-bar.test.tsx +182 -36
- package/src/components/command-bar/command-bar.tsx +943 -1016
- package/src/components/command-bar/command-registry.ts +0 -10
- package/src/components/command-bar/confirm-body.tsx +51 -0
- package/src/components/command-bar/helpers.ts +2 -5
- package/src/components/command-bar/list-model.ts +76 -0
- package/src/components/command-bar/theme-picker.tsx +254 -0
- package/src/components/command-bar/view-model.test.ts +4 -4
- package/src/components/command-bar/view-model.ts +1 -20
- package/src/components/command-bar/workflow-ops.ts +4 -0
- package/src/components/command-bar/workflow-types.ts +1 -2
- package/src/components/data-table-stack-view.test.tsx +39 -4
- package/src/components/data-table-stack-view.tsx +11 -104
- package/src/components/data-table-view.tsx +43 -79
- package/src/components/feed-data-table-stack-view.tsx +10 -17
- package/src/components/feed-stack-controller.ts +39 -0
- package/src/components/index.ts +5 -1
- package/src/components/layout/detached-pane-shell.tsx +29 -10
- package/src/components/layout/floating-pane.tsx +35 -4
- package/src/components/layout/header.tsx +12 -7
- package/src/components/layout/pane-content.test.tsx +64 -0
- package/src/components/layout/pane-content.tsx +2 -0
- package/src/components/layout/pane-cycle-order.test.ts +30 -0
- package/src/components/layout/pane-cycle-order.ts +32 -0
- package/src/components/layout/pane-footer.tsx +11 -4
- package/src/components/layout/pane-header.tsx +5 -5
- package/src/components/layout/pane-sizing.ts +9 -2
- package/src/components/layout/pane-title.ts +4 -1
- package/src/components/layout/pane.tsx +11 -5
- package/src/components/layout/shell.test.tsx +264 -22
- package/src/components/layout/shell.tsx +46 -8
- package/src/components/layout/status-bar.tsx +2 -0
- package/src/components/onboarding/onboarding-wizard.tsx +1 -2
- package/src/components/pane-settings-dialog.tsx +12 -4
- package/src/components/pane-template-wizard.test.tsx +3 -4
- package/src/components/speedometer-gauge.test.tsx +45 -0
- package/src/components/speedometer-gauge.tsx +562 -0
- package/src/components/table-view-shared.tsx +218 -0
- package/src/components/ticker-list-table-view.tsx +57 -73
- package/src/components/ticker-list-table.test.tsx +82 -1
- package/src/components/ticker-list-table.tsx +22 -9
- package/src/components/ui/confirm-dialog.tsx +66 -0
- package/src/components/ui/data-table.tsx +30 -19
- package/src/components/ui/frame.tsx +2 -0
- package/src/components/ui/index.ts +0 -1
- package/src/components/ui/page-stack-view.tsx +39 -10
- package/src/components/ui/table-layout.test.ts +42 -0
- package/src/components/ui/table-layout.ts +49 -4
- package/src/components/ui/tabs.tsx +34 -1
- package/src/components/ui/ui.test.tsx +168 -21
- package/src/core/state/app-state.ts +10 -17
- package/src/market-data/coordinator.test.ts +76 -1
- package/src/market-data/coordinator.ts +218 -49
- package/src/news/aggregator.test.ts +49 -1
- package/src/news/aggregator.ts +48 -3
- package/src/news/hooks.ts +6 -2
- package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +46 -16
- package/src/plugins/builtin/ai/providers.ts +0 -17
- package/src/plugins/builtin/ai/screener-pane.test.tsx +3 -4
- package/src/plugins/builtin/ai/screener-pane.tsx +3 -5
- package/src/plugins/builtin/analytics/index.tsx +1 -18
- package/src/plugins/builtin/ask-ai.test.tsx +89 -2
- package/src/plugins/builtin/broker-manager/index.test.tsx +2 -2
- package/src/plugins/builtin/broker-manager/index.tsx +19 -36
- package/src/plugins/builtin/chat-controller.test.ts +188 -1
- package/src/plugins/builtin/chat-controller.ts +537 -220
- package/src/plugins/builtin/chat.test.tsx +431 -13
- package/src/plugins/builtin/chat.tsx +1134 -227
- package/src/plugins/builtin/cloud-tweets.tsx +1 -1
- package/src/plugins/builtin/comparison-chart.test.tsx +53 -2
- package/src/plugins/builtin/comparison-chart.tsx +11 -5
- package/src/plugins/builtin/correlation/index.tsx +1 -6
- package/src/plugins/builtin/debug.tsx +1 -3
- package/src/plugins/builtin/econ/index.tsx +1 -2
- package/src/plugins/builtin/fear-greed/fear-greed-data.test.ts +72 -0
- package/src/plugins/builtin/fear-greed/fear-greed-data.ts +366 -0
- package/src/plugins/builtin/fear-greed/index.tsx +587 -0
- package/src/plugins/builtin/fx-matrix/index.tsx +2 -2
- package/src/plugins/builtin/help.test.tsx +2 -2
- package/src/plugins/builtin/help.tsx +2 -2
- package/src/plugins/builtin/holders/index.test.ts +13 -1
- package/src/plugins/builtin/holders/index.tsx +64 -29
- package/src/plugins/builtin/insider/index.tsx +22 -89
- package/src/plugins/builtin/market-movers/index.tsx +13 -19
- package/src/plugins/builtin/market-movers/screener.test.ts +39 -1
- package/src/plugins/builtin/market-movers/screener.ts +70 -17
- package/src/plugins/builtin/news-wire/breaking-pane.tsx +3 -2
- package/src/plugins/builtin/news-wire/index.tsx +41 -2
- package/src/plugins/builtin/news-wire/industry-pane.tsx +4 -18
- package/src/plugins/builtin/news-wire/news-detail-view.tsx +46 -4
- package/src/plugins/builtin/news-wire/news-preset-pane.tsx +3 -2
- package/src/plugins/builtin/news-wire/news-table.tsx +11 -11
- package/src/plugins/builtin/news.tsx +4 -16
- package/src/plugins/builtin/notes.tsx +19 -55
- package/src/plugins/builtin/options.test.tsx +58 -2
- package/src/plugins/builtin/options.tsx +110 -65
- package/src/plugins/builtin/plugin-groups.ts +2 -0
- package/src/plugins/builtin/portfolio-list/cli/commands.ts +1 -1
- package/src/plugins/builtin/portfolio-list/pane.tsx +16 -39
- package/src/plugins/builtin/research/index.test.ts +94 -0
- package/src/plugins/builtin/research/index.tsx +164 -33
- package/src/plugins/builtin/sec-filing-display.tsx +84 -0
- package/src/plugins/builtin/sec.tsx +13 -66
- package/src/plugins/builtin/sectors/index.tsx +1 -1
- package/src/plugins/builtin/ticker-detail/chart-tab.tsx +0 -1
- package/src/plugins/builtin/ticker-detail/index.tsx +0 -1
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +0 -1
- package/src/plugins/builtin/ticker-detail/pane.tsx +28 -22
- package/src/plugins/builtin/ticker-detail-chart-tab-switch.test.tsx +3 -4
- package/src/plugins/builtin/ticker-detail.test.tsx +38 -0
- package/src/plugins/builtin/world-indices/index.tsx +1 -1
- package/src/plugins/builtin/yield-curve/index.tsx +1 -1
- package/src/plugins/ibkr/broker-adapter.ts +1 -3
- package/src/plugins/ibkr/gateway-service-native.ts +1 -5
- package/src/plugins/ibkr/gateway-service.ts +1 -1
- package/src/plugins/ibkr/index.test.tsx +0 -2
- package/src/plugins/ibkr/trade-tab.test.tsx +3 -4
- package/src/plugins/pane-manager.ts +0 -13
- package/src/plugins/plugin-runtime.test.tsx +40 -6
- package/src/plugins/plugin-runtime.tsx +20 -10
- package/src/plugins/prediction-markets/cli.ts +0 -1
- package/src/plugins/prediction-markets/controller-data.ts +0 -2
- package/src/plugins/prediction-markets/controller.ts +0 -15
- package/src/plugins/prediction-markets/detail/pane.tsx +1 -0
- package/src/plugins/prediction-markets/pane.test.tsx +3 -11
- package/src/plugins/prediction-markets/test-helpers.tsx +1 -1
- package/src/plugins/registry.ts +13 -42
- package/src/plugins/ticker-navigation.test.ts +66 -0
- package/src/plugins/ticker-navigation.ts +44 -0
- package/src/react/input.ts +2 -1
- package/src/renderers/electrobun/bun/index.ts +32 -11
- package/src/renderers/electrobun/bun/window-focus.ts +37 -0
- package/src/renderers/electrobun/shared/protocol.ts +7 -1
- package/src/renderers/electrobun/view/app-services.ts +88 -31
- package/src/renderers/electrobun/view/backend-rpc.ts +15 -0
- package/src/renderers/electrobun/view/data-table.tsx +138 -101
- package/src/renderers/electrobun/view/desktop-controls.tsx +42 -20
- package/src/renderers/electrobun/view/desktop-window-bridge.ts +12 -1
- package/src/renderers/electrobun/view/dialog-host.tsx +13 -1
- package/src/renderers/electrobun/view/input-host.tsx +74 -10
- package/src/renderers/electrobun/view/key-event.ts +1 -1
- package/src/renderers/electrobun/view/main.tsx +18 -1
- package/src/renderers/electrobun/view/styles.css +329 -0
- package/src/renderers/electrobun/view/toast-host.tsx +29 -3
- package/src/renderers/electrobun/view/ui-host.tsx +42 -0
- package/src/renderers/opentui/chart-surface.tsx +179 -0
- package/src/renderers/opentui/input-host.tsx +1 -1
- package/src/renderers/opentui/start.tsx +48 -30
- package/src/renderers/opentui/test-utils.tsx +17 -0
- package/src/renderers/opentui/ui-host.tsx +5 -2
- package/src/sources/gloomberb-cloud.test.ts +86 -1
- package/src/sources/gloomberb-cloud.ts +117 -21
- package/src/sources/provider-router.test.ts +122 -0
- package/src/sources/provider-router.ts +198 -15
- package/src/sources/yahoo-finance.test.ts +44 -0
- package/src/sources/yahoo-finance.ts +6 -0
- package/src/state/app-bootstrap.ts +20 -7
- package/src/state/app-context-selector.test.tsx +189 -4
- package/src/state/app-context.test.ts +65 -0
- package/src/state/app-context.tsx +48 -16
- package/src/state/use-inline-tickers.test.tsx +73 -0
- package/src/state/use-inline-tickers.ts +19 -10
- package/src/test-support/plugin-runtime.ts +3 -2
- package/src/theme/colors.test.ts +75 -0
- package/src/theme/colors.ts +76 -6
- package/src/theme/theme-context.tsx +26 -0
- package/src/theme/themes.test.ts +23 -1
- package/src/theme/themes.ts +115 -0
- package/src/types/broker.ts +1 -1
- package/src/types/capability-route-source.ts +1 -0
- package/src/types/data-provider.ts +15 -2
- package/src/types/desktop-window.ts +6 -0
- package/src/types/financials.ts +2 -0
- package/src/types/plugin.ts +1 -3
- package/src/ui/host.tsx +7 -0
- package/src/utils/api-client.test.ts +32 -0
- package/src/utils/api-client.ts +119 -18
- package/src/utils/back-navigation.ts +24 -1
- package/src/utils/double-escape-close.test.ts +33 -0
- package/src/utils/double-escape-close.ts +40 -0
- package/src/utils/throttled-fetch.test.ts +22 -0
- package/src/utils/throttled-fetch.ts +3 -1
- package/src/utils/ticker-search.ts +53 -9
- package/src/version.ts +1 -1
- package/src/plugins/builtin/news-wire/feed-pane.tsx +0 -21
- package/src/plugins/builtin/news-wire/top-pane.tsx +0 -21
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
2
|
import { act, useEffect, useRef, useState } from "react";
|
|
3
|
-
import { testRender } from "../../renderers/opentui/test-utils";
|
|
4
|
-
import { DialogProvider } from "@opentui-ui/dialog/react";
|
|
3
|
+
import { TestDialogProvider, testRender } from "../../renderers/opentui/test-utils";
|
|
5
4
|
import type { BoxRenderable, ScrollBoxRenderable } from "@opentui/core";
|
|
6
5
|
import { Button } from "./button";
|
|
7
6
|
import { ChoiceDialog } from "./choice-dialog";
|
|
8
|
-
import { DataTable } from "./data-table";
|
|
7
|
+
import { DataTable, type DataTableColumn } from "./data-table";
|
|
9
8
|
import { TextField } from "./fields";
|
|
10
9
|
import { ListView } from "./list-view";
|
|
11
10
|
import { MultiSelectDialogButton } from "./multi-select-dialog";
|
|
@@ -21,12 +20,13 @@ import { Tabs } from "./tabs";
|
|
|
21
20
|
import { ToggleList } from "../toggle-list";
|
|
22
21
|
import { AppContext, PaneInstanceProvider, createInitialState } from "../../state/app-context";
|
|
23
22
|
import { createDefaultConfig } from "../../types/config";
|
|
23
|
+
import { DataTableView } from "../data-table-view";
|
|
24
24
|
|
|
25
25
|
let testSetup: Awaited<ReturnType<typeof testRender>> | undefined;
|
|
26
26
|
let setListSelection: ((index: number) => void) | null = null;
|
|
27
27
|
let selectedTableRow: string | null = null;
|
|
28
28
|
let activatedTableRow: string | null = null;
|
|
29
|
-
let
|
|
29
|
+
let tableScrollBoxForTest: ScrollBoxRenderable | null = null;
|
|
30
30
|
let closedTab: string | null = null;
|
|
31
31
|
let addedTab = false;
|
|
32
32
|
let resolvedChoice: string | null = null;
|
|
@@ -125,34 +125,47 @@ function DataTableSectionHarness() {
|
|
|
125
125
|
);
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
-
function
|
|
128
|
+
function DataTableHorizontalScrollHarness({
|
|
129
|
+
columns = [{ id: "name", label: "NAME", width: 12, align: "left" }],
|
|
130
|
+
containerWidth = 32,
|
|
131
|
+
containerHeight = 5,
|
|
132
|
+
rowCount = 1,
|
|
133
|
+
showHorizontalScrollbar,
|
|
134
|
+
}: {
|
|
135
|
+
columns?: DataTableColumn[];
|
|
136
|
+
containerWidth?: number;
|
|
137
|
+
containerHeight?: number;
|
|
138
|
+
rowCount?: number;
|
|
139
|
+
showHorizontalScrollbar?: boolean;
|
|
140
|
+
}) {
|
|
129
141
|
const headerScrollRef = useRef<ScrollBoxRenderable>(null);
|
|
130
142
|
const scrollRef = useRef<ScrollBoxRenderable>(null);
|
|
131
|
-
const
|
|
143
|
+
const items = Array.from({ length: rowCount }, (_, index) => ({
|
|
144
|
+
id: `row-${index}`,
|
|
145
|
+
name: index === 0 ? "Alpha" : `Alpha ${index + 1}`,
|
|
146
|
+
}));
|
|
132
147
|
|
|
133
148
|
useEffect(() => {
|
|
134
|
-
|
|
149
|
+
tableScrollBoxForTest = scrollRef.current;
|
|
135
150
|
});
|
|
136
151
|
|
|
137
152
|
return (
|
|
138
|
-
<
|
|
139
|
-
|
|
140
|
-
|
|
153
|
+
<DataTableView
|
|
154
|
+
rootWidth={containerWidth}
|
|
155
|
+
rootHeight={containerHeight}
|
|
156
|
+
columns={columns}
|
|
157
|
+
items={items}
|
|
141
158
|
sortColumnId={null}
|
|
142
159
|
sortDirection="asc"
|
|
143
160
|
onHeaderClick={() => {}}
|
|
144
161
|
headerScrollRef={headerScrollRef}
|
|
145
162
|
scrollRef={scrollRef}
|
|
146
|
-
syncHeaderScroll={() => {}}
|
|
147
|
-
onBodyScrollActivity={() => {}}
|
|
148
|
-
hoveredIdx={hoveredIdx}
|
|
149
|
-
setHoveredIdx={setHoveredIdx}
|
|
150
163
|
getItemKey={(row) => row.id}
|
|
151
164
|
isSelected={() => false}
|
|
152
165
|
onSelect={() => {}}
|
|
153
166
|
renderCell={(row) => ({ text: row.name })}
|
|
154
167
|
emptyStateTitle="No rows."
|
|
155
|
-
showHorizontalScrollbar={
|
|
168
|
+
showHorizontalScrollbar={showHorizontalScrollbar}
|
|
156
169
|
/>
|
|
157
170
|
);
|
|
158
171
|
}
|
|
@@ -166,6 +179,15 @@ function DataTableVirtualizationHarness() {
|
|
|
166
179
|
name: `Row ${index}`,
|
|
167
180
|
}));
|
|
168
181
|
|
|
182
|
+
useEffect(() => {
|
|
183
|
+
tableScrollBoxForTest = scrollRef.current;
|
|
184
|
+
return () => {
|
|
185
|
+
if (tableScrollBoxForTest === scrollRef.current) {
|
|
186
|
+
tableScrollBoxForTest = null;
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
});
|
|
190
|
+
|
|
169
191
|
return (
|
|
170
192
|
<DataTable
|
|
171
193
|
columns={[{ id: "name", label: "NAME", width: 12, align: "left" }]}
|
|
@@ -192,7 +214,7 @@ function MultiSelectDialogButtonHarness() {
|
|
|
192
214
|
const [values, setValues] = useState(["sma"]);
|
|
193
215
|
|
|
194
216
|
return (
|
|
195
|
-
<
|
|
217
|
+
<TestDialogProvider>
|
|
196
218
|
<MultiSelectDialogButton
|
|
197
219
|
label="IND"
|
|
198
220
|
title="Chart Indicators"
|
|
@@ -204,7 +226,7 @@ function MultiSelectDialogButtonHarness() {
|
|
|
204
226
|
{ value: "ema", label: "EMA" },
|
|
205
227
|
]}
|
|
206
228
|
/>
|
|
207
|
-
</
|
|
229
|
+
</TestDialogProvider>
|
|
208
230
|
);
|
|
209
231
|
}
|
|
210
232
|
|
|
@@ -253,7 +275,7 @@ afterEach(() => {
|
|
|
253
275
|
setListSelection = null;
|
|
254
276
|
selectedTableRow = null;
|
|
255
277
|
activatedTableRow = null;
|
|
256
|
-
|
|
278
|
+
tableScrollBoxForTest = null;
|
|
257
279
|
closedTab = null;
|
|
258
280
|
addedTab = false;
|
|
259
281
|
resolvedChoice = null;
|
|
@@ -320,6 +342,46 @@ describe("shared UI kit", () => {
|
|
|
320
342
|
expect(frame).toContain("Insider");
|
|
321
343
|
});
|
|
322
344
|
|
|
345
|
+
test("moves focused tabs with arrow keys and leaves Tab for pane focus", async () => {
|
|
346
|
+
let lastSelected = "overview";
|
|
347
|
+
|
|
348
|
+
function KeyboardTabsHarness() {
|
|
349
|
+
const [activeValue, setActiveValue] = useState("overview");
|
|
350
|
+
return (
|
|
351
|
+
<Tabs
|
|
352
|
+
tabs={[
|
|
353
|
+
{ label: "Overview", value: "overview" },
|
|
354
|
+
{ label: "News", value: "news" },
|
|
355
|
+
{ label: "Chart", value: "chart" },
|
|
356
|
+
]}
|
|
357
|
+
activeValue={activeValue}
|
|
358
|
+
onSelect={(value) => {
|
|
359
|
+
lastSelected = value;
|
|
360
|
+
setActiveValue(value);
|
|
361
|
+
}}
|
|
362
|
+
focused
|
|
363
|
+
/>
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
testSetup = await testRender(<KeyboardTabsHarness />, { width: 40, height: 4 });
|
|
368
|
+
|
|
369
|
+
await act(async () => {
|
|
370
|
+
await testSetup!.renderOnce();
|
|
371
|
+
testSetup!.mockInput.pressArrow("right");
|
|
372
|
+
await testSetup!.renderOnce();
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
expect(lastSelected).toBe("news");
|
|
376
|
+
|
|
377
|
+
await act(async () => {
|
|
378
|
+
testSetup!.mockInput.pressTab();
|
|
379
|
+
await testSetup!.renderOnce();
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
expect(lastSelected).toBe("news");
|
|
383
|
+
});
|
|
384
|
+
|
|
323
385
|
test("renders tab actions for editable tab sets", async () => {
|
|
324
386
|
testSetup = await testRender(
|
|
325
387
|
<Tabs
|
|
@@ -650,10 +712,77 @@ describe("shared UI kit", () => {
|
|
|
650
712
|
|
|
651
713
|
test("hides data table horizontal scrolling when disabled", async () => {
|
|
652
714
|
const state = createInitialState(createDefaultConfig("/tmp/gloomberb-test"));
|
|
715
|
+
tableScrollBoxForTest = null;
|
|
716
|
+
testSetup = await testRender(
|
|
717
|
+
<AppContext value={{ state, dispatch: () => {} }}>
|
|
718
|
+
<PaneInstanceProvider paneId="portfolio-list:main">
|
|
719
|
+
<DataTableHorizontalScrollHarness showHorizontalScrollbar={false} />
|
|
720
|
+
</PaneInstanceProvider>
|
|
721
|
+
</AppContext>,
|
|
722
|
+
{ width: 32, height: 5 },
|
|
723
|
+
);
|
|
724
|
+
|
|
725
|
+
await act(async () => {
|
|
726
|
+
await testSetup!.renderOnce();
|
|
727
|
+
await testSetup!.renderOnce();
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
expect(tableScrollBoxForTest?.horizontalScrollBar.visible).toBe(false);
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
test("hides data table horizontal scrolling when content fits", async () => {
|
|
734
|
+
const state = createInitialState(createDefaultConfig("/tmp/gloomberb-test"));
|
|
735
|
+
tableScrollBoxForTest = null;
|
|
736
|
+
testSetup = await testRender(
|
|
737
|
+
<AppContext value={{ state, dispatch: () => {} }}>
|
|
738
|
+
<PaneInstanceProvider paneId="portfolio-list:main">
|
|
739
|
+
<DataTableHorizontalScrollHarness />
|
|
740
|
+
</PaneInstanceProvider>
|
|
741
|
+
</AppContext>,
|
|
742
|
+
{ width: 32, height: 5 },
|
|
743
|
+
);
|
|
744
|
+
|
|
745
|
+
await act(async () => {
|
|
746
|
+
await testSetup!.renderOnce();
|
|
747
|
+
await testSetup!.renderOnce();
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
expect(tableScrollBoxForTest?.horizontalScrollBar.visible).toBe(false);
|
|
751
|
+
});
|
|
752
|
+
|
|
753
|
+
test("shows data table horizontal scrolling when content overflows", async () => {
|
|
754
|
+
const state = createInitialState(createDefaultConfig("/tmp/gloomberb-test"));
|
|
755
|
+
tableScrollBoxForTest = null;
|
|
756
|
+
testSetup = await testRender(
|
|
757
|
+
<AppContext value={{ state, dispatch: () => {} }}>
|
|
758
|
+
<PaneInstanceProvider paneId="portfolio-list:main">
|
|
759
|
+
<DataTableHorizontalScrollHarness
|
|
760
|
+
containerWidth={24}
|
|
761
|
+
columns={[
|
|
762
|
+
{ id: "name", label: "NAME", width: 24, align: "left" },
|
|
763
|
+
{ id: "price", label: "PRICE", width: 18, align: "right" },
|
|
764
|
+
]}
|
|
765
|
+
/>
|
|
766
|
+
</PaneInstanceProvider>
|
|
767
|
+
</AppContext>,
|
|
768
|
+
{ width: 24, height: 5 },
|
|
769
|
+
);
|
|
770
|
+
|
|
771
|
+
await act(async () => {
|
|
772
|
+
await testSetup!.renderOnce();
|
|
773
|
+
await testSetup!.renderOnce();
|
|
774
|
+
});
|
|
775
|
+
|
|
776
|
+
expect(tableScrollBoxForTest?.horizontalScrollBar.visible).toBe(true);
|
|
777
|
+
});
|
|
778
|
+
|
|
779
|
+
test("shows data table vertical scrolling when rows overflow", async () => {
|
|
780
|
+
const state = createInitialState(createDefaultConfig("/tmp/gloomberb-test"));
|
|
781
|
+
tableScrollBoxForTest = null;
|
|
653
782
|
testSetup = await testRender(
|
|
654
783
|
<AppContext value={{ state, dispatch: () => {} }}>
|
|
655
784
|
<PaneInstanceProvider paneId="portfolio-list:main">
|
|
656
|
-
<
|
|
785
|
+
<DataTableHorizontalScrollHarness rowCount={20} containerHeight={5} />
|
|
657
786
|
</PaneInstanceProvider>
|
|
658
787
|
</AppContext>,
|
|
659
788
|
{ width: 32, height: 5 },
|
|
@@ -664,11 +793,12 @@ describe("shared UI kit", () => {
|
|
|
664
793
|
await testSetup!.renderOnce();
|
|
665
794
|
});
|
|
666
795
|
|
|
667
|
-
expect(
|
|
796
|
+
expect(tableScrollBoxForTest?.verticalScrollBar.visible).toBe(true);
|
|
668
797
|
});
|
|
669
798
|
|
|
670
|
-
test("virtualizes data table rows
|
|
799
|
+
test("virtualizes data table rows and refreshes after wheel scrolling", async () => {
|
|
671
800
|
const state = createInitialState(createDefaultConfig("/tmp/gloomberb-test"));
|
|
801
|
+
tableScrollBoxForTest = null;
|
|
672
802
|
testSetup = await testRender(
|
|
673
803
|
<AppContext value={{ state, dispatch: () => {} }}>
|
|
674
804
|
<PaneInstanceProvider paneId="portfolio-list:main">
|
|
@@ -685,5 +815,22 @@ describe("shared UI kit", () => {
|
|
|
685
815
|
const frame = testSetup.captureCharFrame();
|
|
686
816
|
expect(frame).toContain("Row 0");
|
|
687
817
|
expect(frame).not.toContain("Row 99");
|
|
818
|
+
expect(tableScrollBoxForTest?.scrollTop).toBe(0);
|
|
819
|
+
|
|
820
|
+
await act(async () => {
|
|
821
|
+
for (let index = 0; index < 12; index++) {
|
|
822
|
+
await testSetup!.mockMouse.scroll(2, 2, "down");
|
|
823
|
+
}
|
|
824
|
+
await Promise.resolve();
|
|
825
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
826
|
+
await testSetup!.renderOnce();
|
|
827
|
+
});
|
|
828
|
+
await testSetup.renderOnce();
|
|
829
|
+
|
|
830
|
+
const scrollTop = tableScrollBoxForTest?.scrollTop ?? 0;
|
|
831
|
+
expect(scrollTop).toBeGreaterThan(0);
|
|
832
|
+
|
|
833
|
+
const scrolledFrame = testSetup.captureCharFrame();
|
|
834
|
+
expect(scrolledFrame).toContain(`Row ${scrollTop}`);
|
|
688
835
|
});
|
|
689
836
|
});
|
|
@@ -318,23 +318,13 @@ function getTopFloatingPaneId(layout: LayoutConfig): string | null {
|
|
|
318
318
|
return topInstanceId;
|
|
319
319
|
}
|
|
320
320
|
|
|
321
|
-
function resolveFocusedPaneId(
|
|
322
|
-
previousLayout: LayoutConfig,
|
|
323
|
-
nextLayout: LayoutConfig,
|
|
324
|
-
focusedPaneId: string | null,
|
|
325
|
-
): string | null {
|
|
321
|
+
function resolveFocusedPaneId(nextLayout: LayoutConfig, focusedPaneId: string | null): string | null {
|
|
326
322
|
const paneOrder = getPaneOrder(nextLayout);
|
|
327
323
|
if (paneOrder.length === 0) return null;
|
|
328
324
|
if (focusedPaneId && paneOrder.includes(focusedPaneId)) {
|
|
329
325
|
return focusedPaneId;
|
|
330
326
|
}
|
|
331
|
-
|
|
332
|
-
? previousLayout.floating.some((entry) => entry.instanceId === focusedPaneId)
|
|
333
|
-
: false;
|
|
334
|
-
if (previouslyFloating) {
|
|
335
|
-
return getTopFloatingPaneId(nextLayout) ?? paneOrder[0] ?? null;
|
|
336
|
-
}
|
|
337
|
-
return paneOrder[0] ?? null;
|
|
327
|
+
return getTopFloatingPaneId(nextLayout) ?? paneOrder[0] ?? null;
|
|
338
328
|
}
|
|
339
329
|
|
|
340
330
|
function getPanelFocusTarget(layout: LayoutConfig, panel: "left" | "right"): string | null {
|
|
@@ -387,10 +377,12 @@ function removeHistoryIndex(layoutHistory: Record<number, LayoutHistoryEntry>, r
|
|
|
387
377
|
|
|
388
378
|
/** If paneId is a floating pane, bump its zIndex to the top. */
|
|
389
379
|
function bringFloatingToFront(layout: LayoutConfig, paneId: string): LayoutConfig {
|
|
390
|
-
const
|
|
380
|
+
const entryIndex = layout.floating.findIndex((e) => e.instanceId === paneId);
|
|
381
|
+
const entry = entryIndex >= 0 ? layout.floating[entryIndex] : undefined;
|
|
391
382
|
if (!entry) return layout;
|
|
392
383
|
const maxZ = layout.floating.reduce((max, e) => Math.max(max, e.zIndex ?? 50), 0);
|
|
393
|
-
|
|
384
|
+
const topEqualIndex = layout.floating.findLastIndex((e) => (e.zIndex ?? 50) === maxZ);
|
|
385
|
+
if ((entry.zIndex ?? 50) === maxZ && entryIndex === topEqualIndex) return layout; // already on top
|
|
394
386
|
return {
|
|
395
387
|
...layout,
|
|
396
388
|
floating: layout.floating.map((e) =>
|
|
@@ -444,7 +436,7 @@ function withFocusedPane(state: AppState, config: AppConfig): AppState {
|
|
|
444
436
|
layouts: syncLayouts(config.layouts, config.activeLayoutIndex, normalizedLayout),
|
|
445
437
|
};
|
|
446
438
|
const nextPaneState = reconcilePaneState(nextConfig, state.paneState);
|
|
447
|
-
const focusedPaneId = resolveFocusedPaneId(
|
|
439
|
+
const focusedPaneId = resolveFocusedPaneId(nextConfig.layout, state.focusedPaneId);
|
|
448
440
|
const focusedConfig = bringFocusedFloatingPaneToFront(nextConfig, focusedPaneId);
|
|
449
441
|
return {
|
|
450
442
|
...state,
|
|
@@ -792,7 +784,8 @@ export function appReducer(state: AppState, action: AppAction): AppState {
|
|
|
792
784
|
|
|
793
785
|
case "FOCUS_PREV": {
|
|
794
786
|
if (action.paneOrder.length === 0) return state;
|
|
795
|
-
const
|
|
787
|
+
const focusedIndex = state.focusedPaneId ? action.paneOrder.indexOf(state.focusedPaneId) : -1;
|
|
788
|
+
const currentIndex = focusedIndex >= 0 ? focusedIndex : 0;
|
|
796
789
|
const nextPaneId = action.paneOrder[(currentIndex - 1 + action.paneOrder.length) % action.paneOrder.length]!;
|
|
797
790
|
return focusPaneState(state, nextPaneId);
|
|
798
791
|
}
|
|
@@ -851,7 +844,7 @@ export function appReducer(state: AppState, action: AppAction): AppState {
|
|
|
851
844
|
|
|
852
845
|
export function createInitialState(config: AppConfig, sessionSnapshot: AppSessionSnapshot | null = null): AppState {
|
|
853
846
|
const paneState = reconcilePaneState(config, sessionSnapshot?.paneState ?? {});
|
|
854
|
-
const defaultFocusedPaneId = getPaneOrder(config.layout)[0] ?? null;
|
|
847
|
+
const defaultFocusedPaneId = getTopFloatingPaneId(config.layout) ?? getPaneOrder(config.layout)[0] ?? null;
|
|
855
848
|
const focusedPaneId = sessionSnapshot?.focusedPaneId
|
|
856
849
|
&& config.layout.instances.some((instance) => instance.instanceId === sessionSnapshot.focusedPaneId)
|
|
857
850
|
? sessionSnapshot.focusedPaneId
|
|
@@ -2,7 +2,7 @@ import { describe, expect, it } from "bun:test";
|
|
|
2
2
|
import { MarketDataCoordinator } from "./coordinator";
|
|
3
3
|
import type { DataProvider, QuoteSubscriptionTarget } from "../types/data-provider";
|
|
4
4
|
import type { InstrumentSearchResult } from "../types/instrument";
|
|
5
|
-
import type {
|
|
5
|
+
import type { PricePoint, Quote, TickerFinancials } from "../types/financials";
|
|
6
6
|
import { createTestDataProvider } from "../test-support/data-provider";
|
|
7
7
|
|
|
8
8
|
function createProvider(overrides: Partial<DataProvider> = {}): DataProvider {
|
|
@@ -632,4 +632,79 @@ describe("MarketDataCoordinator", () => {
|
|
|
632
632
|
expect(financials?.quote?.marketCap).toBe(3_640_775_908_600);
|
|
633
633
|
expect(financials?.fundamentals?.trailingPE).toBe(31.4);
|
|
634
634
|
});
|
|
635
|
+
|
|
636
|
+
it("batch loads only missing quotes when fresh quote data is already stored", async () => {
|
|
637
|
+
const batchTargets: QuoteSubscriptionTarget[][] = [];
|
|
638
|
+
const provider = createProvider({
|
|
639
|
+
getQuote: async () => ({
|
|
640
|
+
symbol: "AAPL",
|
|
641
|
+
price: 100,
|
|
642
|
+
currency: "USD",
|
|
643
|
+
change: 0,
|
|
644
|
+
changePercent: 0,
|
|
645
|
+
lastUpdated: 1_700_000_000_000,
|
|
646
|
+
}),
|
|
647
|
+
getQuotesBatch: async (targets) => {
|
|
648
|
+
batchTargets.push(targets);
|
|
649
|
+
return targets.map((target) => ({
|
|
650
|
+
target,
|
|
651
|
+
quote: {
|
|
652
|
+
symbol: target.symbol,
|
|
653
|
+
price: target.symbol === "MSFT" ? 200 : 300,
|
|
654
|
+
currency: "USD",
|
|
655
|
+
change: 0,
|
|
656
|
+
changePercent: 0,
|
|
657
|
+
lastUpdated: 1_700_000_000_000,
|
|
658
|
+
},
|
|
659
|
+
}));
|
|
660
|
+
},
|
|
661
|
+
});
|
|
662
|
+
const coordinator = new MarketDataCoordinator(provider);
|
|
663
|
+
const aapl = { symbol: "AAPL", exchange: "NASDAQ" };
|
|
664
|
+
const msft = { symbol: "MSFT", exchange: "NASDAQ" };
|
|
665
|
+
|
|
666
|
+
await coordinator.loadQuote(aapl);
|
|
667
|
+
await coordinator.loadQuotesBatch([aapl, msft]);
|
|
668
|
+
|
|
669
|
+
expect(batchTargets).toHaveLength(1);
|
|
670
|
+
expect(batchTargets[0]?.map((target) => target.symbol)).toEqual(["MSFT"]);
|
|
671
|
+
expect(coordinator.getQuoteEntry(aapl).data?.price).toBe(100);
|
|
672
|
+
expect(coordinator.getQuoteEntry(msft).data?.price).toBe(200);
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
it("batch loads snapshots through provider batch support", async () => {
|
|
676
|
+
const batchSymbols: string[][] = [];
|
|
677
|
+
const provider = createProvider({
|
|
678
|
+
getTickerFinancialsBatch: async (targets) => {
|
|
679
|
+
batchSymbols.push(targets.map((target) => target.symbol));
|
|
680
|
+
return targets.map((target) => ({
|
|
681
|
+
target,
|
|
682
|
+
financials: {
|
|
683
|
+
quote: {
|
|
684
|
+
symbol: target.symbol,
|
|
685
|
+
price: target.symbol === "AAPL" ? 150 : 250,
|
|
686
|
+
currency: "USD",
|
|
687
|
+
change: 0,
|
|
688
|
+
changePercent: 0,
|
|
689
|
+
lastUpdated: 1_700_000_000_000,
|
|
690
|
+
},
|
|
691
|
+
fundamentals: { trailingPE: target.symbol === "AAPL" ? 20 : 30 },
|
|
692
|
+
annualStatements: [],
|
|
693
|
+
quarterlyStatements: [],
|
|
694
|
+
priceHistory: [],
|
|
695
|
+
},
|
|
696
|
+
}));
|
|
697
|
+
},
|
|
698
|
+
});
|
|
699
|
+
const coordinator = new MarketDataCoordinator(provider);
|
|
700
|
+
|
|
701
|
+
await coordinator.loadSnapshotsBatch([
|
|
702
|
+
{ symbol: "AAPL", exchange: "NASDAQ" },
|
|
703
|
+
{ symbol: "MSFT", exchange: "NASDAQ" },
|
|
704
|
+
]);
|
|
705
|
+
|
|
706
|
+
expect(batchSymbols).toEqual([["AAPL", "MSFT"]]);
|
|
707
|
+
expect(coordinator.getTickerFinancialsSync({ symbol: "AAPL", exchange: "NASDAQ" })?.quote?.price).toBe(150);
|
|
708
|
+
expect(coordinator.getTickerFinancialsSync({ symbol: "MSFT", exchange: "NASDAQ" })?.fundamentals?.trailingPE).toBe(30);
|
|
709
|
+
});
|
|
635
710
|
});
|