gloomberb 0.4.1 → 0.5.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 +2 -2
- package/package.json +1 -1
- package/src/app.tsx +168 -65
- package/src/cli/commands/ticker.ts +45 -15
- package/src/cli/helpers.ts +4 -2
- package/src/cli/registry.test.ts +42 -35
- package/src/cli.test.ts +4 -2
- package/src/components/chart/chart-control-hint.tsx +39 -0
- package/src/components/chart/chart-controller.test.ts +110 -0
- package/src/components/chart/chart-controller.ts +364 -2
- package/src/components/chart/chart-data.ts +177 -29
- package/src/components/chart/chart-indicator-selector.tsx +53 -0
- package/src/components/chart/chart-pane-settings.ts +20 -0
- package/src/components/chart/chart-renderer.ts +220 -30
- package/src/components/chart/chart-resolution.test.ts +67 -0
- package/src/components/chart/chart-resolution.ts +74 -4
- package/src/components/chart/chart-scroll.test.ts +69 -0
- package/src/components/chart/chart-scroll.ts +104 -0
- package/src/components/chart/chart-types.ts +12 -0
- package/src/components/chart/chart-viewport.test.ts +5 -1
- package/src/components/chart/chart-viewport.ts +1 -1
- package/src/components/chart/chart.test.ts +249 -10
- package/src/components/chart/comparison-chart-renderer.test.ts +71 -1
- package/src/components/chart/comparison-chart-renderer.ts +40 -3
- package/src/components/chart/comparison-stock-chart.tsx +218 -123
- package/src/components/chart/indicators/bands.test.ts +50 -0
- package/src/components/chart/indicators/bands.ts +29 -0
- package/src/components/chart/indicators/moving-averages.test.ts +54 -0
- package/src/components/chart/indicators/moving-averages.ts +42 -0
- package/src/components/chart/indicators/options.test.ts +32 -0
- package/src/components/chart/indicators/options.ts +107 -0
- package/src/components/chart/indicators/oscillators.test.ts +79 -0
- package/src/components/chart/indicators/oscillators.ts +92 -0
- package/src/components/chart/indicators/types.ts +36 -0
- package/src/components/chart/native/chart-rasterizer.ts +51 -0
- package/src/components/chart/native/kitty.test.ts +68 -0
- package/src/components/chart/stock-chart-renderer-switch.test.tsx +740 -9
- package/src/components/chart/stock-chart.test.ts +239 -0
- package/src/components/chart/stock-chart.tsx +2257 -372
- package/src/components/command-bar/command-bar.test.tsx +337 -5
- package/src/components/command-bar/command-bar.tsx +292 -120
- package/src/components/command-bar/helpers.ts +1 -1
- package/src/components/command-bar/root-shortcuts.ts +33 -4
- package/src/components/command-bar/view-model.test.ts +3 -3
- package/src/components/command-bar/view-model.ts +4 -4
- package/src/components/command-bar/workflow-ops.ts +10 -0
- package/src/components/layout/header.tsx +2 -1
- package/src/components/layout/shell.test.tsx +186 -27
- package/src/components/layout/shell.tsx +5 -4
- package/src/components/layout/status-bar.test.tsx +16 -7
- package/src/components/layout/status-bar.tsx +4 -1
- package/src/components/markdown-editor.tsx +1 -0
- package/src/components/pane-settings-dialog.tsx +13 -152
- package/src/components/pane-template-wizard.tsx +1 -0
- package/src/components/price-selector-dialog.tsx +12 -7
- package/src/components/ticker-badge-text.test.tsx +50 -0
- package/src/components/ticker-badge-text.tsx +17 -2
- package/src/components/ticker-badge.tsx +2 -2
- package/src/components/ticker-list-table.tsx +54 -1
- package/src/components/toggle-list.tsx +15 -2
- package/src/components/ui/data-table.tsx +60 -3
- package/src/components/ui/external-link.tsx +48 -11
- package/src/components/ui/fields.tsx +2 -1
- package/src/components/ui/index.ts +8 -1
- package/src/components/ui/multi-select-chips.tsx +69 -0
- package/src/components/ui/multi-select-dialog.tsx +267 -0
- package/src/components/ui/multi-select.ts +78 -0
- package/src/components/ui/ui.test.tsx +343 -1
- package/src/data/plugin-state-store.ts +30 -19
- package/src/data/resource-store.ts +133 -103
- package/src/data/session-store.ts +19 -12
- package/src/data/sqlite-cache.test.ts +37 -2
- package/src/data/sqlite-database.ts +8 -2
- package/src/data/sqlite-retry.test.ts +63 -0
- package/src/data/sqlite-retry.ts +66 -0
- package/src/data/ticker-store.ts +29 -18
- package/src/index.tsx +6 -1
- package/src/market-data/coordinator.test.ts +114 -5
- package/src/market-data/coordinator.ts +70 -1
- package/src/market-data/hooks.test.tsx +73 -2
- package/src/market-data/hooks.tsx +16 -4
- package/src/notifications/app-notifier.test.ts +177 -0
- package/src/notifications/app-notifier.ts +241 -0
- package/src/pane-settings.test.ts +2 -0
- package/src/plugins/builtin/ai/index.test.tsx +1 -1
- package/src/plugins/builtin/ai/screener-pane.test.tsx +2 -0
- package/src/plugins/builtin/ai/screener-pane.tsx +1 -0
- package/src/plugins/builtin/ai/ticker-context.ts +3 -2
- package/src/plugins/builtin/alerts/alert-engine.test.ts +90 -0
- package/src/plugins/builtin/alerts/alert-engine.ts +55 -0
- package/src/plugins/builtin/alerts/index.test.tsx +357 -0
- package/src/plugins/builtin/alerts/index.tsx +519 -0
- package/src/plugins/builtin/alerts/types.ts +14 -0
- package/src/plugins/builtin/analytics/index.test.tsx +300 -0
- package/src/plugins/builtin/analytics/index.tsx +775 -0
- package/src/plugins/builtin/analytics/metrics.test.ts +125 -0
- package/src/plugins/builtin/analytics/metrics.ts +155 -0
- package/src/plugins/builtin/ask-ai.test.tsx +2 -0
- package/src/plugins/builtin/chat-controller.test.ts +62 -18
- package/src/plugins/builtin/chat-controller.ts +23 -12
- package/src/plugins/builtin/chat.test.tsx +715 -4
- package/src/plugins/builtin/chat.tsx +415 -119
- package/src/plugins/builtin/comparison-chart.test.tsx +97 -57
- package/src/plugins/builtin/comparison-chart.tsx +1 -2
- package/src/plugins/builtin/correlation/compute.test.ts +147 -0
- package/src/plugins/builtin/correlation/compute.ts +115 -0
- package/src/plugins/builtin/correlation/index.tsx +385 -0
- package/src/plugins/builtin/correlation/plugin.test.ts +44 -0
- package/src/plugins/builtin/correlation/settings.test.ts +34 -0
- package/src/plugins/builtin/correlation/settings.ts +95 -0
- package/src/plugins/builtin/debug.tsx +36 -50
- package/src/plugins/builtin/earnings/earnings-cache.test.ts +181 -0
- package/src/plugins/builtin/earnings/earnings-cache.ts +136 -0
- package/src/plugins/builtin/earnings/earnings-data.test.ts +59 -0
- package/src/plugins/builtin/earnings/earnings-data.ts +50 -0
- package/src/plugins/builtin/earnings/index.tsx +337 -0
- package/src/plugins/builtin/econ/calendar-source.test.ts +219 -0
- package/src/plugins/builtin/econ/calendar-source.ts +121 -0
- package/src/plugins/builtin/econ/fred-client.test.ts +63 -0
- package/src/plugins/builtin/econ/fred-client.ts +79 -0
- package/src/plugins/builtin/econ/fred-series-map.ts +88 -0
- package/src/plugins/builtin/econ/index.tsx +999 -0
- package/src/plugins/builtin/econ/types.ts +13 -0
- package/src/plugins/builtin/fred-settings.ts +10 -0
- package/src/plugins/builtin/fx-matrix/index.tsx +178 -0
- package/src/plugins/builtin/fx-matrix/pairs.ts +30 -0
- package/src/plugins/builtin/help.test.tsx +1 -5
- package/src/plugins/builtin/help.tsx +1 -16
- package/src/plugins/builtin/insider/index.tsx +417 -0
- package/src/plugins/builtin/insider/insider-data.test.ts +55 -0
- package/src/plugins/builtin/insider/insider-data.ts +49 -0
- package/src/plugins/builtin/layout-manager.tsx +24 -16
- package/src/plugins/builtin/market-movers/index.tsx +573 -0
- package/src/plugins/builtin/market-movers/screener.test.ts +143 -0
- package/src/plugins/builtin/market-movers/screener.ts +105 -0
- package/src/plugins/builtin/options.tsx +4 -3
- package/src/plugins/builtin/portfolio-list/cli/commands.ts +11 -6
- package/src/plugins/builtin/portfolio-list/command-bar.ts +6 -3
- package/src/plugins/builtin/portfolio-list/header.tsx +3 -2
- package/src/plugins/builtin/portfolio-list/metrics.ts +22 -11
- package/src/plugins/builtin/portfolio-list/pane.tsx +3 -9
- package/src/plugins/builtin/portfolio-list/settings.ts +9 -0
- package/src/plugins/builtin/portfolio-list/table.tsx +3 -0
- package/src/plugins/builtin/portfolio-list.test.tsx +3 -1
- package/src/plugins/builtin/portfolio-metrics.test.ts +13 -1
- package/src/plugins/builtin/sec.tsx +114 -7
- package/src/plugins/builtin/sectors/index.tsx +359 -0
- package/src/plugins/builtin/sectors/sector-data.ts +18 -0
- package/src/plugins/builtin/ticker-detail/chart-tab.tsx +75 -13
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +316 -80
- package/src/plugins/builtin/ticker-detail/quote-monitor.tsx +4 -6
- package/src/plugins/builtin/ticker-detail-chart-tab-switch.test.tsx +17 -12
- package/src/plugins/builtin/ticker-detail.test.tsx +3 -3
- package/src/plugins/builtin/world-indices/index.tsx +410 -0
- package/src/plugins/builtin/world-indices/indices.ts +52 -0
- package/src/plugins/builtin/yield-curve/index.tsx +242 -0
- package/src/plugins/builtin/yield-curve/treasury-data.test.ts +212 -0
- package/src/plugins/builtin/yield-curve/treasury-data.ts +107 -0
- package/src/plugins/catalog.ts +22 -0
- package/src/plugins/gridlock-notification.ts +15 -0
- package/src/plugins/ibkr/__snapshots__/trade-tab.test.tsx.snap +5 -5
- package/src/plugins/ibkr/index.test.tsx +1 -1
- package/src/plugins/ibkr/index.tsx +1 -1
- package/src/plugins/ibkr/trade-tab.tsx +23 -5
- package/src/plugins/ibkr/trade-utils.test.ts +1 -1
- package/src/plugins/ibkr/trade-utils.ts +7 -6
- package/src/plugins/ibkr/trading-pane.tsx +8 -7
- package/src/plugins/plugin-runtime.test.tsx +2 -0
- package/src/plugins/plugin-runtime.tsx +10 -0
- package/src/plugins/prediction-markets/controller-data.ts +8 -0
- package/src/plugins/prediction-markets/detail/shared.tsx +7 -6
- package/src/plugins/prediction-markets/plugin.test.tsx +48 -1
- package/src/plugins/prediction-markets/services/fetch.ts +13 -7
- package/src/plugins/prediction-markets/services/polymarket-adapter.ts +29 -11
- package/src/plugins/prediction-markets/services/polymarket-ws.ts +20 -0
- package/src/plugins/prediction-markets/test-helpers.tsx +2 -0
- package/src/plugins/registry.ts +22 -13
- package/src/sources/provider-router.test.ts +3 -2
- package/src/sources/sec-edgar.ts +13 -1
- package/src/sources/yahoo-finance.ts +75 -1
- package/src/state/app-context-selector.test.tsx +17 -1
- package/src/state/app-context.test.tsx +24 -0
- package/src/state/app-context.tsx +29 -6
- package/src/theme/colors.ts +5 -0
- package/src/theme/themes.ts +25 -0
- package/src/types/config.test.ts +107 -0
- package/src/types/config.ts +30 -1
- package/src/types/data-provider.ts +13 -0
- package/src/types/plugin.ts +35 -1
- package/src/utils/format.test.ts +11 -1
- package/src/utils/format.ts +86 -8
- package/src/utils/inline-content-tokenizer.test.ts +22 -0
- package/src/utils/inline-content-tokenizer.ts +76 -0
- package/src/utils/link-tokenizer.test.ts +27 -0
- package/src/utils/link-tokenizer.ts +93 -0
- package/src/utils/market-format.test.ts +109 -0
- package/src/utils/market-format.ts +299 -0
- package/src/utils/market-status.ts +3 -2
- package/src/utils/selection-clipboard.test.ts +43 -0
- package/src/utils/selection-clipboard.ts +88 -0
- package/src/utils/terminal-input-reset.test.ts +50 -0
- package/src/utils/terminal-input-reset.ts +24 -0
- package/src/utils/throttled-fetch.test.ts +145 -0
- package/src/utils/throttled-fetch.ts +212 -0
- package/src/utils/ticker-tokenizer.test.ts +31 -0
- package/src/utils/ticker-tokenizer.ts +7 -42
- package/src/version.ts +1 -1
|
@@ -1,16 +1,28 @@
|
|
|
1
1
|
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
|
-
import { act, useState } from "react";
|
|
2
|
+
import { act, useEffect, useRef, useState } from "react";
|
|
3
3
|
import { testRender } from "@opentui/react/test-utils";
|
|
4
|
+
import { DialogProvider } from "@opentui-ui/dialog/react";
|
|
5
|
+
import type { BoxRenderable, ScrollBoxRenderable } from "@opentui/core";
|
|
4
6
|
import { Button } from "./button";
|
|
7
|
+
import { DataTable } from "./data-table";
|
|
5
8
|
import { TextField } from "./fields";
|
|
6
9
|
import { ListView } from "./list-view";
|
|
10
|
+
import { MultiSelectChips } from "./multi-select-chips";
|
|
11
|
+
import { MultiSelectDialogButton } from "./multi-select-dialog";
|
|
12
|
+
import { toggleMultiSelectValue } from "./multi-select";
|
|
7
13
|
import { ProgressBar } from "./loading";
|
|
8
14
|
import { Notice } from "./status";
|
|
9
15
|
import { Tabs } from "./tabs";
|
|
10
16
|
import { ToggleList } from "../toggle-list";
|
|
17
|
+
import { AppContext, PaneInstanceProvider, createInitialState } from "../../state/app-context";
|
|
18
|
+
import { createDefaultConfig } from "../../types/config";
|
|
11
19
|
|
|
12
20
|
let testSetup: Awaited<ReturnType<typeof testRender>> | undefined;
|
|
13
21
|
let setListSelection: ((index: number) => void) | null = null;
|
|
22
|
+
let selectedTableRow: string | null = null;
|
|
23
|
+
let activatedTableRow: string | null = null;
|
|
24
|
+
let selectedChips: string[] = [];
|
|
25
|
+
let tableHorizontalScrollbarVisible: boolean | null = null;
|
|
14
26
|
|
|
15
27
|
function ScrollableListHarness() {
|
|
16
28
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
@@ -29,12 +41,163 @@ function ScrollableListHarness() {
|
|
|
29
41
|
);
|
|
30
42
|
}
|
|
31
43
|
|
|
44
|
+
function DataTableActivationHarness() {
|
|
45
|
+
const headerScrollRef = useRef<ScrollBoxRenderable>(null);
|
|
46
|
+
const scrollRef = useRef<ScrollBoxRenderable>(null);
|
|
47
|
+
const [selectedId, setSelectedId] = useState<string | null>(null);
|
|
48
|
+
const [hoveredIdx, setHoveredIdx] = useState<number | null>(null);
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<DataTable
|
|
52
|
+
columns={[{ id: "name", label: "NAME", width: 12, align: "left" }]}
|
|
53
|
+
items={[{ id: "alpha", name: "Alpha" }]}
|
|
54
|
+
sortColumnId={null}
|
|
55
|
+
sortDirection="asc"
|
|
56
|
+
onHeaderClick={() => {}}
|
|
57
|
+
headerScrollRef={headerScrollRef}
|
|
58
|
+
scrollRef={scrollRef}
|
|
59
|
+
syncHeaderScroll={() => {}}
|
|
60
|
+
onBodyScrollActivity={() => {}}
|
|
61
|
+
hoveredIdx={hoveredIdx}
|
|
62
|
+
setHoveredIdx={setHoveredIdx}
|
|
63
|
+
getItemKey={(row) => row.id}
|
|
64
|
+
isSelected={(row) => selectedId === row.id}
|
|
65
|
+
onSelect={(row) => {
|
|
66
|
+
selectedTableRow = row.id;
|
|
67
|
+
setSelectedId(row.id);
|
|
68
|
+
}}
|
|
69
|
+
onActivate={(row) => {
|
|
70
|
+
activatedTableRow = row.id;
|
|
71
|
+
}}
|
|
72
|
+
renderCell={(row) => ({ text: row.name })}
|
|
73
|
+
emptyStateTitle="No rows."
|
|
74
|
+
/>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
type SectionTableRow =
|
|
79
|
+
| { kind: "header"; id: string; label: string }
|
|
80
|
+
| { kind: "row"; id: string; name: string };
|
|
81
|
+
|
|
82
|
+
function DataTableSectionHarness() {
|
|
83
|
+
const headerScrollRef = useRef<ScrollBoxRenderable>(null);
|
|
84
|
+
const scrollRef = useRef<ScrollBoxRenderable>(null);
|
|
85
|
+
const [selectedId, setSelectedId] = useState<string | null>(null);
|
|
86
|
+
const [hoveredIdx, setHoveredIdx] = useState<number | null>(null);
|
|
87
|
+
const rows: SectionTableRow[] = [
|
|
88
|
+
{ kind: "header", id: "macro", label: "Macro Releases" },
|
|
89
|
+
{ kind: "row", id: "cpi", name: "CPI" },
|
|
90
|
+
];
|
|
91
|
+
|
|
92
|
+
return (
|
|
93
|
+
<DataTable
|
|
94
|
+
columns={[{ id: "name", label: "NAME", width: 16, align: "left" }]}
|
|
95
|
+
items={rows}
|
|
96
|
+
sortColumnId={null}
|
|
97
|
+
sortDirection="asc"
|
|
98
|
+
onHeaderClick={() => {}}
|
|
99
|
+
headerScrollRef={headerScrollRef}
|
|
100
|
+
scrollRef={scrollRef}
|
|
101
|
+
syncHeaderScroll={() => {}}
|
|
102
|
+
onBodyScrollActivity={() => {}}
|
|
103
|
+
hoveredIdx={hoveredIdx}
|
|
104
|
+
setHoveredIdx={setHoveredIdx}
|
|
105
|
+
getItemKey={(row) => row.id}
|
|
106
|
+
isSelected={(row) => row.kind === "row" && row.id === selectedId}
|
|
107
|
+
onSelect={(row) => {
|
|
108
|
+
if (row.kind !== "row") return;
|
|
109
|
+
selectedTableRow = row.id;
|
|
110
|
+
setSelectedId(row.id);
|
|
111
|
+
}}
|
|
112
|
+
renderSectionHeader={(row) => (
|
|
113
|
+
row.kind === "header" ? { text: row.label } : null
|
|
114
|
+
)}
|
|
115
|
+
renderCell={(row) => ({ text: row.kind === "row" ? row.name : "" })}
|
|
116
|
+
emptyStateTitle="No rows."
|
|
117
|
+
/>
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function DataTableNoHorizontalScrollHarness() {
|
|
122
|
+
const headerScrollRef = useRef<ScrollBoxRenderable>(null);
|
|
123
|
+
const scrollRef = useRef<ScrollBoxRenderable>(null);
|
|
124
|
+
const [hoveredIdx, setHoveredIdx] = useState<number | null>(null);
|
|
125
|
+
|
|
126
|
+
useEffect(() => {
|
|
127
|
+
tableHorizontalScrollbarVisible = scrollRef.current?.horizontalScrollBar.visible ?? null;
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
return (
|
|
131
|
+
<DataTable
|
|
132
|
+
columns={[{ id: "name", label: "NAME", width: 12, align: "left" }]}
|
|
133
|
+
items={[{ id: "alpha", name: "Alpha" }]}
|
|
134
|
+
sortColumnId={null}
|
|
135
|
+
sortDirection="asc"
|
|
136
|
+
onHeaderClick={() => {}}
|
|
137
|
+
headerScrollRef={headerScrollRef}
|
|
138
|
+
scrollRef={scrollRef}
|
|
139
|
+
syncHeaderScroll={() => {}}
|
|
140
|
+
onBodyScrollActivity={() => {}}
|
|
141
|
+
hoveredIdx={hoveredIdx}
|
|
142
|
+
setHoveredIdx={setHoveredIdx}
|
|
143
|
+
getItemKey={(row) => row.id}
|
|
144
|
+
isSelected={() => false}
|
|
145
|
+
onSelect={() => {}}
|
|
146
|
+
renderCell={(row) => ({ text: row.name })}
|
|
147
|
+
emptyStateTitle="No rows."
|
|
148
|
+
showHorizontalScrollbar={false}
|
|
149
|
+
/>
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function MultiSelectChipsHarness() {
|
|
154
|
+
const [values, setValues] = useState(["sma"]);
|
|
155
|
+
selectedChips = values;
|
|
156
|
+
|
|
157
|
+
return (
|
|
158
|
+
<MultiSelectChips
|
|
159
|
+
label="IND"
|
|
160
|
+
selectedValues={values}
|
|
161
|
+
onChange={setValues}
|
|
162
|
+
idPrefix="indicator-chip"
|
|
163
|
+
options={[
|
|
164
|
+
{ value: "sma", label: "SMA" },
|
|
165
|
+
{ value: "ema", label: "EMA" },
|
|
166
|
+
]}
|
|
167
|
+
/>
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function MultiSelectDialogButtonHarness() {
|
|
172
|
+
const [values, setValues] = useState(["sma"]);
|
|
173
|
+
|
|
174
|
+
return (
|
|
175
|
+
<DialogProvider dialogOptions={{ style: { backgroundColor: "#000000", borderColor: "#ffffff", borderStyle: "single" } }}>
|
|
176
|
+
<MultiSelectDialogButton
|
|
177
|
+
label="IND"
|
|
178
|
+
title="Chart Indicators"
|
|
179
|
+
selectedValues={values}
|
|
180
|
+
onChange={setValues}
|
|
181
|
+
idPrefix="indicator-dialog"
|
|
182
|
+
options={[
|
|
183
|
+
{ value: "sma", label: "SMA" },
|
|
184
|
+
{ value: "ema", label: "EMA" },
|
|
185
|
+
]}
|
|
186
|
+
/>
|
|
187
|
+
</DialogProvider>
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
32
191
|
afterEach(() => {
|
|
33
192
|
if (testSetup) {
|
|
34
193
|
testSetup.renderer.destroy();
|
|
35
194
|
testSetup = undefined;
|
|
36
195
|
}
|
|
37
196
|
setListSelection = null;
|
|
197
|
+
selectedTableRow = null;
|
|
198
|
+
activatedTableRow = null;
|
|
199
|
+
selectedChips = [];
|
|
200
|
+
tableHorizontalScrollbarVisible = null;
|
|
38
201
|
});
|
|
39
202
|
|
|
40
203
|
describe("shared UI kit", () => {
|
|
@@ -90,6 +253,79 @@ describe("shared UI kit", () => {
|
|
|
90
253
|
expect(frame).toContain("Headlines and previews");
|
|
91
254
|
});
|
|
92
255
|
|
|
256
|
+
test("toggles multi-select chips with the mouse", async () => {
|
|
257
|
+
testSetup = await testRender(<MultiSelectChipsHarness />, { width: 32, height: 3 });
|
|
258
|
+
|
|
259
|
+
await act(async () => {
|
|
260
|
+
await testSetup!.renderOnce();
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
let frame = testSetup.captureCharFrame();
|
|
264
|
+
expect(frame).toContain("[x] SMA");
|
|
265
|
+
expect(frame).toContain("[ ] EMA");
|
|
266
|
+
const emaChip = testSetup.renderer.root.findDescendantById("indicator-chip:ema") as BoxRenderable | undefined;
|
|
267
|
+
expect(emaChip).toBeDefined();
|
|
268
|
+
|
|
269
|
+
await act(async () => {
|
|
270
|
+
await testSetup!.mockMouse.click(emaChip!.x + 1, emaChip!.y);
|
|
271
|
+
});
|
|
272
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
273
|
+
await act(async () => {
|
|
274
|
+
await testSetup!.renderOnce();
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
frame = testSetup.captureCharFrame();
|
|
278
|
+
expect(frame).toContain("[x] SMA");
|
|
279
|
+
expect(frame).toContain("[x] EMA");
|
|
280
|
+
expect(selectedChips).toEqual(["sma", "ema"]);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
test("opens compact multi-select dialogs from a button", async () => {
|
|
284
|
+
testSetup = await testRender(<MultiSelectDialogButtonHarness />, { width: 60, height: 18 });
|
|
285
|
+
|
|
286
|
+
await act(async () => {
|
|
287
|
+
await testSetup!.renderOnce();
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
let frame = testSetup.captureCharFrame();
|
|
291
|
+
expect(frame).toContain("IND: SMA");
|
|
292
|
+
const button = testSetup.renderer.root.findDescendantById("indicator-dialog:button") as BoxRenderable | undefined;
|
|
293
|
+
expect(button).toBeDefined();
|
|
294
|
+
expect(button!.width).toBe(" IND: SMA ".length);
|
|
295
|
+
|
|
296
|
+
await act(async () => {
|
|
297
|
+
await testSetup!.mockMouse.click(button!.x + 1, button!.y);
|
|
298
|
+
await Promise.resolve();
|
|
299
|
+
await testSetup!.renderOnce();
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
frame = testSetup.captureCharFrame();
|
|
303
|
+
expect(frame).toContain("Chart Indicators");
|
|
304
|
+
expect(frame).toContain("[✓] SMA");
|
|
305
|
+
expect(frame).toContain("[ ] EMA");
|
|
306
|
+
expect(frame).not.toContain("Toggle");
|
|
307
|
+
expect(frame).not.toContain("space toggle");
|
|
308
|
+
|
|
309
|
+
await act(async () => {
|
|
310
|
+
await testSetup!.mockMouse.click(0, 0);
|
|
311
|
+
await Promise.resolve();
|
|
312
|
+
await testSetup!.renderOnce();
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
frame = testSetup.captureCharFrame();
|
|
316
|
+
expect(frame).not.toContain("Chart Indicators");
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
test("keeps shared multi-select values in option order when toggling", () => {
|
|
320
|
+
const options = [
|
|
321
|
+
{ value: "sma", label: "SMA" },
|
|
322
|
+
{ value: "ema", label: "EMA" },
|
|
323
|
+
];
|
|
324
|
+
|
|
325
|
+
expect(toggleMultiSelectValue(options, ["sma"], "ema")).toEqual(["sma", "ema"]);
|
|
326
|
+
expect(toggleMultiSelectValue(options, ["sma", "ema"], "sma")).toEqual(["ema"]);
|
|
327
|
+
});
|
|
328
|
+
|
|
93
329
|
test("auto-scrolls a scrollable list to keep the selected row visible", async () => {
|
|
94
330
|
testSetup = await testRender(
|
|
95
331
|
<ScrollableListHarness />,
|
|
@@ -131,4 +367,110 @@ describe("shared UI kit", () => {
|
|
|
131
367
|
expect(frame).toContain("******");
|
|
132
368
|
expect(frame).not.toContain("secret");
|
|
133
369
|
});
|
|
370
|
+
|
|
371
|
+
test("does not allow selecting masked password fields", async () => {
|
|
372
|
+
testSetup = await testRender(
|
|
373
|
+
<TextField
|
|
374
|
+
type="password"
|
|
375
|
+
value="secret"
|
|
376
|
+
focused
|
|
377
|
+
width={12}
|
|
378
|
+
onChange={() => {}}
|
|
379
|
+
/>,
|
|
380
|
+
{ width: 16, height: 3 },
|
|
381
|
+
);
|
|
382
|
+
|
|
383
|
+
await testSetup.renderOnce();
|
|
384
|
+
|
|
385
|
+
await act(async () => {
|
|
386
|
+
await testSetup!.mockMouse.drag(1, 0, 6, 0);
|
|
387
|
+
await testSetup!.renderOnce();
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
expect(testSetup.renderer.getSelection()).toBeNull();
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
test("activates data table rows on a second click", async () => {
|
|
394
|
+
const state = createInitialState(createDefaultConfig("/tmp/gloomberb-test"));
|
|
395
|
+
testSetup = await testRender(
|
|
396
|
+
<AppContext value={{ state, dispatch: () => {} }}>
|
|
397
|
+
<PaneInstanceProvider paneId="portfolio-list:main">
|
|
398
|
+
<DataTableActivationHarness />
|
|
399
|
+
</PaneInstanceProvider>
|
|
400
|
+
</AppContext>,
|
|
401
|
+
{ width: 32, height: 5 },
|
|
402
|
+
);
|
|
403
|
+
|
|
404
|
+
await act(async () => {
|
|
405
|
+
await testSetup!.renderOnce();
|
|
406
|
+
});
|
|
407
|
+
await act(async () => {
|
|
408
|
+
await testSetup!.mockMouse.click(2, 1);
|
|
409
|
+
await testSetup!.renderOnce();
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
expect(selectedTableRow).toBe("alpha");
|
|
413
|
+
expect(activatedTableRow).toBeNull();
|
|
414
|
+
|
|
415
|
+
await act(async () => {
|
|
416
|
+
await testSetup!.mockMouse.click(2, 1);
|
|
417
|
+
await testSetup!.renderOnce();
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
expect(activatedTableRow).toBe("alpha");
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
test("renders data table section headers as non-selectable rows", async () => {
|
|
424
|
+
const state = createInitialState(createDefaultConfig("/tmp/gloomberb-test"));
|
|
425
|
+
testSetup = await testRender(
|
|
426
|
+
<AppContext value={{ state, dispatch: () => {} }}>
|
|
427
|
+
<PaneInstanceProvider paneId="portfolio-list:main">
|
|
428
|
+
<DataTableSectionHarness />
|
|
429
|
+
</PaneInstanceProvider>
|
|
430
|
+
</AppContext>,
|
|
431
|
+
{ width: 32, height: 6 },
|
|
432
|
+
);
|
|
433
|
+
|
|
434
|
+
await act(async () => {
|
|
435
|
+
await testSetup!.renderOnce();
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
let frame = testSetup.captureCharFrame();
|
|
439
|
+
expect(frame).toContain("Macro Releases");
|
|
440
|
+
expect(frame).toContain("CPI");
|
|
441
|
+
|
|
442
|
+
await act(async () => {
|
|
443
|
+
await testSetup!.mockMouse.click(2, 1);
|
|
444
|
+
await testSetup!.renderOnce();
|
|
445
|
+
});
|
|
446
|
+
expect(selectedTableRow).toBeNull();
|
|
447
|
+
|
|
448
|
+
await act(async () => {
|
|
449
|
+
await testSetup!.mockMouse.click(2, 2);
|
|
450
|
+
await testSetup!.renderOnce();
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
frame = testSetup.captureCharFrame();
|
|
454
|
+
expect(selectedTableRow).toBe("cpi");
|
|
455
|
+
expect(frame).toContain("CPI");
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
test("hides data table horizontal scrolling when disabled", async () => {
|
|
459
|
+
const state = createInitialState(createDefaultConfig("/tmp/gloomberb-test"));
|
|
460
|
+
testSetup = await testRender(
|
|
461
|
+
<AppContext value={{ state, dispatch: () => {} }}>
|
|
462
|
+
<PaneInstanceProvider paneId="portfolio-list:main">
|
|
463
|
+
<DataTableNoHorizontalScrollHarness />
|
|
464
|
+
</PaneInstanceProvider>
|
|
465
|
+
</AppContext>,
|
|
466
|
+
{ width: 32, height: 5 },
|
|
467
|
+
);
|
|
468
|
+
|
|
469
|
+
await act(async () => {
|
|
470
|
+
await testSetup!.renderOnce();
|
|
471
|
+
await testSetup!.renderOnce();
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
expect(tableHorizontalScrollbarVisible).toBe(false);
|
|
475
|
+
});
|
|
134
476
|
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Database } from "bun:sqlite";
|
|
2
2
|
import { safeParseJson, serializeJson } from "./sqlite-json";
|
|
3
|
+
import { withSqliteBusyRetry } from "./sqlite-retry";
|
|
3
4
|
|
|
4
5
|
export const DEFAULT_PLUGIN_STATE_SCHEMA_VERSION = 1;
|
|
5
6
|
|
|
@@ -23,11 +24,13 @@ export class PluginStateStore {
|
|
|
23
24
|
|
|
24
25
|
get<T>(pluginId: string, key: string, schemaVersion = DEFAULT_PLUGIN_STATE_SCHEMA_VERSION): PluginStateRecord<T> | null {
|
|
25
26
|
const cacheKey = `${pluginId}:${key}`;
|
|
26
|
-
const row =
|
|
27
|
-
.
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
27
|
+
const row = withSqliteBusyRetry("load plugin state", () => (
|
|
28
|
+
this.db
|
|
29
|
+
.query<{ schema_version: number; value: string; updated_at: number }, [string, string]>(
|
|
30
|
+
"SELECT schema_version, value, updated_at FROM plugin_state WHERE plugin_id = ? AND key = ?",
|
|
31
|
+
)
|
|
32
|
+
.get(pluginId, key)
|
|
33
|
+
));
|
|
31
34
|
if (!row) {
|
|
32
35
|
this.cache.delete(cacheKey);
|
|
33
36
|
return null;
|
|
@@ -63,12 +66,14 @@ export class PluginStateStore {
|
|
|
63
66
|
set(pluginId: string, key: string, value: unknown, schemaVersion = DEFAULT_PLUGIN_STATE_SCHEMA_VERSION): void {
|
|
64
67
|
const rawValue = serializeJson(value);
|
|
65
68
|
const updatedAt = Date.now();
|
|
66
|
-
|
|
67
|
-
.
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
69
|
+
withSqliteBusyRetry("save plugin state", () => {
|
|
70
|
+
this.db
|
|
71
|
+
.query(
|
|
72
|
+
`INSERT OR REPLACE INTO plugin_state (plugin_id, key, schema_version, value, updated_at)
|
|
73
|
+
VALUES (?, ?, ?, ?, ?)`,
|
|
74
|
+
)
|
|
75
|
+
.run(pluginId, key, schemaVersion, rawValue, updatedAt);
|
|
76
|
+
});
|
|
72
77
|
this.cache.set(`${pluginId}:${key}`, {
|
|
73
78
|
value,
|
|
74
79
|
schemaVersion,
|
|
@@ -78,21 +83,27 @@ export class PluginStateStore {
|
|
|
78
83
|
}
|
|
79
84
|
|
|
80
85
|
delete(pluginId: string, key: string): void {
|
|
81
|
-
|
|
86
|
+
withSqliteBusyRetry("delete plugin state", () => {
|
|
87
|
+
this.db.query("DELETE FROM plugin_state WHERE plugin_id = ? AND key = ?").run(pluginId, key);
|
|
88
|
+
});
|
|
82
89
|
this.cache.delete(`${pluginId}:${key}`);
|
|
83
90
|
}
|
|
84
91
|
|
|
85
92
|
keys(pluginId: string): string[] {
|
|
86
|
-
return
|
|
87
|
-
.
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
93
|
+
return withSqliteBusyRetry("list plugin state keys", () => (
|
|
94
|
+
this.db
|
|
95
|
+
.query<{ key: string }, [string]>(
|
|
96
|
+
"SELECT key FROM plugin_state WHERE plugin_id = ? ORDER BY key",
|
|
97
|
+
)
|
|
98
|
+
.all(pluginId)
|
|
99
|
+
.map((row) => row.key)
|
|
100
|
+
));
|
|
92
101
|
}
|
|
93
102
|
|
|
94
103
|
clear(pluginId: string): void {
|
|
95
|
-
|
|
104
|
+
withSqliteBusyRetry("clear plugin state", () => {
|
|
105
|
+
this.db.query("DELETE FROM plugin_state WHERE plugin_id = ?").run(pluginId);
|
|
106
|
+
});
|
|
96
107
|
for (const cacheKey of this.cache.keys()) {
|
|
97
108
|
if (cacheKey.startsWith(`${pluginId}:`)) {
|
|
98
109
|
this.cache.delete(cacheKey);
|