gloomberb 0.8.1 → 0.8.2
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 +102 -52
- package/package.json +1 -1
- 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/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/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/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 +11 -25
- package/src/components/ui/choice-dialog.tsx +12 -2
- package/src/components/ui/multi-select/dialog.tsx +17 -5
- 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 +20 -6
- package/src/components/ui/ui.test.tsx +30 -5
- 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 +6 -0
- 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 +61 -0
- package/src/plugins/builtin/chat/content/layout-metrics.ts +3 -1
- package/src/plugins/builtin/chat/content/shortcuts.ts +20 -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 +1 -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/index.tsx +6 -0
- 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/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/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/world-indices/index.tsx +9 -6
- 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/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/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/test-support/plugin-runtime.ts +1 -0
- package/src/types/data-provider.ts +11 -0
- package/src/ui/host.tsx +1 -0
- package/src/version.ts +1 -1
|
@@ -45,21 +45,27 @@ afterEach(async () => {
|
|
|
45
45
|
|
|
46
46
|
function Harness() {
|
|
47
47
|
const [selectedIndex, setSelectedIndex] = useState(1);
|
|
48
|
+
const [cursorIndex, setCursorIndex] = useState(1);
|
|
48
49
|
const [activatedTitle, setActivatedTitle] = useState("");
|
|
49
50
|
const state = createInitialState(
|
|
50
51
|
createDefaultConfig("/tmp/gloomberb-data-table-view-test"),
|
|
51
52
|
);
|
|
52
53
|
const selectedTitle = rows[selectedIndex]?.title ?? "none";
|
|
54
|
+
const cursorTitle = rows[cursorIndex]?.title ?? "none";
|
|
53
55
|
|
|
54
56
|
return (
|
|
55
57
|
<AppContext value={{ state, dispatch: () => {} }}>
|
|
56
58
|
<PaneInstanceProvider paneId="data-table-view-test">
|
|
57
59
|
<DataTableView<Row, Column>
|
|
58
60
|
focused
|
|
59
|
-
selectedIndex={selectedIndex}
|
|
60
61
|
isNavigable={(row) => row.type === "row"}
|
|
61
|
-
|
|
62
|
-
|
|
62
|
+
selection={{
|
|
63
|
+
kind: "index",
|
|
64
|
+
selectedIndex,
|
|
65
|
+
onChange: (index) => setSelectedIndex(index),
|
|
66
|
+
}}
|
|
67
|
+
onCursorChange={(_row, index) => setCursorIndex(index)}
|
|
68
|
+
onActivate={(row) => {
|
|
63
69
|
if (row.type === "row") setActivatedTitle(row.title);
|
|
64
70
|
}}
|
|
65
71
|
columns={columns}
|
|
@@ -68,8 +74,6 @@ function Harness() {
|
|
|
68
74
|
sortDirection="asc"
|
|
69
75
|
onHeaderClick={() => {}}
|
|
70
76
|
getItemKey={(row) => row.id}
|
|
71
|
-
isSelected={(_row, index) => index === selectedIndex}
|
|
72
|
-
onSelect={(_row, index) => setSelectedIndex(index)}
|
|
73
77
|
renderSectionHeader={(row) => row.type === "section"
|
|
74
78
|
? { text: row.title }
|
|
75
79
|
: null}
|
|
@@ -79,7 +83,7 @@ function Harness() {
|
|
|
79
83
|
emptyStateTitle="No rows"
|
|
80
84
|
rootAfter={
|
|
81
85
|
<Box height={1}>
|
|
82
|
-
<Text>{`selected=${selectedTitle} activated=${activatedTitle}`}</Text>
|
|
86
|
+
<Text>{`cursor=${cursorTitle} selected=${selectedTitle} activated=${activatedTitle}`}</Text>
|
|
83
87
|
</Box>
|
|
84
88
|
}
|
|
85
89
|
/>
|
|
@@ -102,19 +106,21 @@ function LargeSelectionHarness({
|
|
|
102
106
|
<PaneInstanceProvider paneId="data-table-view-large-test">
|
|
103
107
|
<DataTableView<Row, Column>
|
|
104
108
|
focused
|
|
105
|
-
|
|
109
|
+
selection={{
|
|
110
|
+
kind: "index",
|
|
111
|
+
selectedIndex: 500,
|
|
112
|
+
onChange: () => {},
|
|
113
|
+
}}
|
|
106
114
|
columns={columns}
|
|
107
115
|
items={largeRows}
|
|
108
116
|
sortColumnId={null}
|
|
109
117
|
sortDirection="asc"
|
|
110
118
|
onHeaderClick={() => {}}
|
|
111
119
|
getItemKey={(row) => row.id}
|
|
112
|
-
|
|
120
|
+
renderCell={(row, _column, index): DataTableCell => {
|
|
113
121
|
onIsSelected();
|
|
114
|
-
return index === 500;
|
|
122
|
+
return { text: row.title + (index === 500 ? "" : "") };
|
|
115
123
|
}}
|
|
116
|
-
onSelect={() => {}}
|
|
117
|
-
renderCell={(row): DataTableCell => ({ text: row.title })}
|
|
118
124
|
emptyStateTitle="No rows"
|
|
119
125
|
/>
|
|
120
126
|
</PaneInstanceProvider>
|
|
@@ -190,24 +196,24 @@ describe("DataTableView", () => {
|
|
|
190
196
|
testSetup = await testRender(<Harness />, { width: 60, height: 12 });
|
|
191
197
|
|
|
192
198
|
await renderSettled();
|
|
193
|
-
expect(testSetup.captureCharFrame()).toContain("selected=First row");
|
|
199
|
+
expect(testSetup.captureCharFrame()).toContain("cursor=First row selected=First row");
|
|
194
200
|
|
|
195
201
|
await emitKeypress({ name: "down", sequence: "\u001B[B" });
|
|
196
202
|
await renderSettled();
|
|
197
|
-
expect(testSetup.captureCharFrame()).toContain("
|
|
203
|
+
expect(testSetup.captureCharFrame()).toContain("cursor=Second row selected=First row");
|
|
198
204
|
|
|
199
205
|
await emitKeypress({ name: "up", sequence: "\u001B[A", meta: true });
|
|
200
206
|
await renderSettled();
|
|
201
|
-
expect(testSetup.captureCharFrame()).toContain("
|
|
207
|
+
expect(testSetup.captureCharFrame()).toContain("cursor=Second row selected=First row");
|
|
202
208
|
|
|
203
209
|
await emitKeypress({ name: "up", sequence: "\u001B[A" });
|
|
204
210
|
await renderSettled();
|
|
205
|
-
expect(testSetup.captureCharFrame()).toContain("selected=First row");
|
|
211
|
+
expect(testSetup.captureCharFrame()).toContain("cursor=First row selected=First row");
|
|
206
212
|
|
|
207
213
|
await emitKeypress({ name: "j", sequence: "j" });
|
|
208
214
|
await emitKeypress({ name: "k", sequence: "k" });
|
|
209
215
|
await renderSettled();
|
|
210
|
-
expect(testSetup.captureCharFrame()).toContain("selected=First row");
|
|
216
|
+
expect(testSetup.captureCharFrame()).toContain("cursor=First row selected=First row");
|
|
211
217
|
|
|
212
218
|
await emitKeypress({ name: "enter", sequence: "\r" });
|
|
213
219
|
await renderSettled();
|
|
@@ -215,11 +221,11 @@ describe("DataTableView", () => {
|
|
|
215
221
|
|
|
216
222
|
await emitKeypress({ name: "j", sequence: "j" });
|
|
217
223
|
await renderSettled();
|
|
218
|
-
expect(testSetup.captureCharFrame()).toContain("
|
|
224
|
+
expect(testSetup.captureCharFrame()).toContain("cursor=Second row selected=First row activated=First row");
|
|
219
225
|
|
|
220
226
|
await emitKeypress({ name: "enter", sequence: "\r", defaultPrevented: true });
|
|
221
227
|
await renderSettled();
|
|
222
|
-
expect(testSetup.captureCharFrame()).toContain("
|
|
228
|
+
expect(testSetup.captureCharFrame()).toContain("cursor=Second row selected=First row activated=First row");
|
|
223
229
|
});
|
|
224
230
|
|
|
225
231
|
test("keeps selection current across repeated keypresses before the next render", async () => {
|
|
@@ -24,6 +24,41 @@ import {
|
|
|
24
24
|
|
|
25
25
|
export type DataTableKeyEvent = TableViewKeyEvent;
|
|
26
26
|
|
|
27
|
+
const DATA_TABLE_SELECTION_COMMIT_DELAY_MS = 150;
|
|
28
|
+
|
|
29
|
+
export type DataTableSelectionChangeReason =
|
|
30
|
+
| "keyboard"
|
|
31
|
+
| "pointer"
|
|
32
|
+
| "activation";
|
|
33
|
+
|
|
34
|
+
export type DataTableSelection<T> =
|
|
35
|
+
| { kind: "none" }
|
|
36
|
+
| {
|
|
37
|
+
kind: "index";
|
|
38
|
+
selectedIndex: number | null;
|
|
39
|
+
onChange: (
|
|
40
|
+
index: number,
|
|
41
|
+
item: T,
|
|
42
|
+
reason: DataTableSelectionChangeReason,
|
|
43
|
+
) => void;
|
|
44
|
+
}
|
|
45
|
+
| {
|
|
46
|
+
kind: "id";
|
|
47
|
+
selectedId: string | null;
|
|
48
|
+
getId: (item: T, index: number) => string;
|
|
49
|
+
onChange: (
|
|
50
|
+
id: string,
|
|
51
|
+
item: T,
|
|
52
|
+
index: number,
|
|
53
|
+
reason: DataTableSelectionChangeReason,
|
|
54
|
+
) => void;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
interface SelectionCommitTarget {
|
|
58
|
+
index: number;
|
|
59
|
+
id?: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
27
62
|
export interface DataTableViewProps<
|
|
28
63
|
T,
|
|
29
64
|
C extends DataTableColumn = DataTableColumn,
|
|
@@ -35,11 +70,18 @@ export interface DataTableViewProps<
|
|
|
35
70
|
| "onBodyScrollActivity"
|
|
36
71
|
| "hoveredIdx"
|
|
37
72
|
| "setHoveredIdx"
|
|
73
|
+
| "isSelected"
|
|
74
|
+
| "onSelect"
|
|
75
|
+
| "onActivate"
|
|
38
76
|
> {
|
|
39
77
|
focused?: boolean;
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
78
|
+
selection: DataTableSelection<T>;
|
|
79
|
+
onActivate?: (item: T, index: number) => void;
|
|
80
|
+
onCursorChange?: (
|
|
81
|
+
item: T,
|
|
82
|
+
index: number,
|
|
83
|
+
reason: DataTableSelectionChangeReason,
|
|
84
|
+
) => void;
|
|
43
85
|
isNavigable?: (item: T, index: number) => boolean;
|
|
44
86
|
rootBefore?: ReactNode;
|
|
45
87
|
rootAfter?: ReactNode;
|
|
@@ -62,9 +104,9 @@ export function DataTableView<
|
|
|
62
104
|
C extends DataTableColumn = DataTableColumn,
|
|
63
105
|
>({
|
|
64
106
|
focused = false,
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
107
|
+
selection,
|
|
108
|
+
onActivate,
|
|
109
|
+
onCursorChange,
|
|
68
110
|
isNavigable,
|
|
69
111
|
rootBefore,
|
|
70
112
|
rootAfter,
|
|
@@ -97,29 +139,32 @@ export function DataTableView<
|
|
|
97
139
|
hoveredIdx,
|
|
98
140
|
setHoveredIdx,
|
|
99
141
|
});
|
|
100
|
-
const [
|
|
101
|
-
|
|
102
|
-
>(null);
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
? tableProps.items.findIndex(tableProps.isSelected)
|
|
106
|
-
: -1,
|
|
107
|
-
[selectedIndex, tableProps.items, tableProps.isSelected],
|
|
108
|
-
);
|
|
109
|
-
const usesFallbackSelection =
|
|
110
|
-
selectedIndex == null && selectedIndexFromPredicate < 0;
|
|
111
|
-
const effectiveSelectedIndex = selectedIndex
|
|
112
|
-
?? (selectedIndexFromPredicate >= 0
|
|
113
|
-
? selectedIndexFromPredicate
|
|
114
|
-
: fallbackSelectedIndex ?? -1);
|
|
115
|
-
const effectiveSelectedIndexRef = useRef(effectiveSelectedIndex);
|
|
116
|
-
effectiveSelectedIndexRef.current = effectiveSelectedIndex;
|
|
117
|
-
const effectiveScrollToIndex = scrollToIndex ?? (
|
|
118
|
-
effectiveSelectedIndex >= 0 ? effectiveSelectedIndex : null
|
|
142
|
+
const [cursorIndex, setCursorIndex] = useState<number | null>(null);
|
|
143
|
+
const pendingCommitRef = useRef(false);
|
|
144
|
+
const pendingCommitTargetRef = useRef<SelectionCommitTarget | null>(null);
|
|
145
|
+
const pendingCommitTimerRef = useRef<ReturnType<typeof setTimeout> | null>(
|
|
146
|
+
null,
|
|
119
147
|
);
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
148
|
+
const selectionKey = selection.kind === "id"
|
|
149
|
+
? selection.selectedId
|
|
150
|
+
: selection.kind === "index"
|
|
151
|
+
? selection.selectedIndex
|
|
152
|
+
: null;
|
|
153
|
+
const selectedIndexFromSelection = useMemo(() => {
|
|
154
|
+
if (selection.kind === "none") return -1;
|
|
155
|
+
if (selection.kind === "index") {
|
|
156
|
+
const index = selection.selectedIndex;
|
|
157
|
+
return typeof index === "number"
|
|
158
|
+
&& index >= 0
|
|
159
|
+
&& index < tableProps.items.length
|
|
160
|
+
? index
|
|
161
|
+
: -1;
|
|
162
|
+
}
|
|
163
|
+
if (selection.selectedId == null) return -1;
|
|
164
|
+
return tableProps.items.findIndex(
|
|
165
|
+
(item, index) => selection.getId(item, index) === selection.selectedId,
|
|
166
|
+
);
|
|
167
|
+
}, [selection, selectionKey, tableProps.items]);
|
|
123
168
|
const navigableIndices = useMemo(() => {
|
|
124
169
|
if (!isNavigable) return null;
|
|
125
170
|
return tableProps.items.reduce<number[]>((indices, item, index) => {
|
|
@@ -128,39 +173,82 @@ export function DataTableView<
|
|
|
128
173
|
}, []);
|
|
129
174
|
}, [isNavigable, tableProps.items]);
|
|
130
175
|
|
|
131
|
-
|
|
132
|
-
if (
|
|
133
|
-
|
|
134
|
-
return;
|
|
176
|
+
const isValidCursorIndex = useCallback((index: number | null) => {
|
|
177
|
+
if (index == null || index < 0 || index >= tableProps.items.length) {
|
|
178
|
+
return false;
|
|
135
179
|
}
|
|
136
|
-
if (
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
180
|
+
if (!isNavigable) return true;
|
|
181
|
+
const item = tableProps.items[index];
|
|
182
|
+
return item !== undefined && isNavigable(item, index);
|
|
183
|
+
}, [isNavigable, tableProps.items]);
|
|
184
|
+
|
|
185
|
+
const defaultCursorIndex = selection.kind === "none"
|
|
186
|
+
? -1
|
|
187
|
+
: isValidCursorIndex(selectedIndexFromSelection)
|
|
188
|
+
? selectedIndexFromSelection
|
|
189
|
+
: navigableIndices
|
|
190
|
+
? navigableIndices[0] ?? -1
|
|
191
|
+
: tableProps.items.length > 0
|
|
192
|
+
? 0
|
|
193
|
+
: -1;
|
|
194
|
+
const effectiveSelectedIndex = selection.kind === "none"
|
|
195
|
+
? -1
|
|
196
|
+
: isValidCursorIndex(cursorIndex)
|
|
197
|
+
? cursorIndex!
|
|
198
|
+
: defaultCursorIndex;
|
|
199
|
+
const effectiveSelectedIndexRef = useRef(effectiveSelectedIndex);
|
|
200
|
+
effectiveSelectedIndexRef.current = effectiveSelectedIndex;
|
|
201
|
+
const [selectionScrollVersion, setSelectionScrollVersion] = useState(0);
|
|
202
|
+
const [selectionScrollTarget, setSelectionScrollTarget] = useState<number | null>(null);
|
|
203
|
+
const lastExternalSelectionScrollRef = useRef<{
|
|
204
|
+
kind: DataTableSelection<T>["kind"];
|
|
205
|
+
key: string | number | null;
|
|
206
|
+
resolved: boolean;
|
|
207
|
+
} | null>(null);
|
|
208
|
+
const effectiveScrollToIndex = scrollToIndex !== undefined
|
|
209
|
+
? scrollToIndex
|
|
210
|
+
: selectionScrollTarget;
|
|
211
|
+
|
|
212
|
+
const requestSelectionScroll = useCallback((index: number) => {
|
|
213
|
+
if (scrollToIndex !== undefined || index < 0) return;
|
|
214
|
+
setSelectionScrollTarget(index);
|
|
215
|
+
setSelectionScrollVersion((current) => current + 1);
|
|
216
|
+
}, [scrollToIndex]);
|
|
217
|
+
|
|
218
|
+
const clearPendingCommit = useCallback(() => {
|
|
219
|
+
if (pendingCommitTimerRef.current) {
|
|
220
|
+
clearTimeout(pendingCommitTimerRef.current);
|
|
221
|
+
pendingCommitTimerRef.current = null;
|
|
222
|
+
}
|
|
223
|
+
pendingCommitRef.current = false;
|
|
224
|
+
pendingCommitTargetRef.current = null;
|
|
225
|
+
}, []);
|
|
226
|
+
|
|
227
|
+
useEffect(() => {
|
|
228
|
+
if (selection.kind === "none") {
|
|
229
|
+
clearPendingCommit();
|
|
230
|
+
setCursorIndex(null);
|
|
147
231
|
return;
|
|
148
232
|
}
|
|
149
|
-
if (
|
|
150
|
-
|
|
151
|
-
const nextIndex = navigableIndices.find(
|
|
152
|
-
(index) => index >= fallbackSelectedIndex,
|
|
153
|
-
)
|
|
154
|
-
?? navigableIndices.at(-1)
|
|
155
|
-
?? null;
|
|
156
|
-
setFallbackSelectedIndex(nextIndex);
|
|
233
|
+
if (pendingCommitRef.current) return;
|
|
234
|
+
setCursorIndex(defaultCursorIndex >= 0 ? defaultCursorIndex : null);
|
|
157
235
|
}, [
|
|
158
|
-
|
|
236
|
+
clearPendingCommit,
|
|
237
|
+
defaultCursorIndex,
|
|
159
238
|
navigableIndices,
|
|
239
|
+
selectedIndexFromSelection,
|
|
240
|
+
selection.kind,
|
|
241
|
+
selectionKey,
|
|
160
242
|
tableProps.items.length,
|
|
161
|
-
usesFallbackSelection,
|
|
162
243
|
]);
|
|
163
244
|
|
|
245
|
+
useEffect(() => () => {
|
|
246
|
+
if (pendingCommitTimerRef.current) {
|
|
247
|
+
clearTimeout(pendingCommitTimerRef.current);
|
|
248
|
+
pendingCommitTimerRef.current = null;
|
|
249
|
+
}
|
|
250
|
+
}, []);
|
|
251
|
+
|
|
164
252
|
const handleBodyScrollActivity = useTableBodyScrollActivity({
|
|
165
253
|
onBodyScrollActivity,
|
|
166
254
|
syncHeaderScroll: effectiveSyncHeaderScroll,
|
|
@@ -173,40 +261,156 @@ export function DataTableView<
|
|
|
173
261
|
});
|
|
174
262
|
|
|
175
263
|
useEffect(() => {
|
|
176
|
-
|
|
177
|
-
|
|
264
|
+
const current = {
|
|
265
|
+
kind: selection.kind,
|
|
266
|
+
key: selectionKey,
|
|
267
|
+
resolved: selectedIndexFromSelection >= 0,
|
|
268
|
+
};
|
|
269
|
+
const previous = lastExternalSelectionScrollRef.current;
|
|
270
|
+
lastExternalSelectionScrollRef.current = current;
|
|
271
|
+
|
|
272
|
+
if (selection.kind === "none") {
|
|
273
|
+
if (scrollToIndex === undefined) setSelectionScrollTarget(null);
|
|
178
274
|
return;
|
|
179
275
|
}
|
|
180
|
-
if (
|
|
181
|
-
lastSelectionScrollIndexRef.current = effectiveScrollToIndex;
|
|
182
|
-
setSelectionScrollVersion((current) => current + 1);
|
|
183
|
-
}, [effectiveScrollToIndex]);
|
|
276
|
+
if (effectiveSelectedIndex < 0) return;
|
|
184
277
|
|
|
185
|
-
|
|
278
|
+
const selectionChanged = !previous
|
|
279
|
+
|| previous.kind !== current.kind
|
|
280
|
+
|| previous.key !== current.key;
|
|
281
|
+
const selectedRowAppeared = !previous?.resolved && current.resolved;
|
|
282
|
+
if (selectionChanged || selectedRowAppeared) {
|
|
283
|
+
requestSelectionScroll(effectiveSelectedIndex);
|
|
284
|
+
}
|
|
285
|
+
}, [
|
|
286
|
+
effectiveSelectedIndex,
|
|
287
|
+
requestSelectionScroll,
|
|
288
|
+
scrollToIndex,
|
|
289
|
+
selectedIndexFromSelection,
|
|
290
|
+
selection.kind,
|
|
291
|
+
selectionKey,
|
|
292
|
+
]);
|
|
293
|
+
|
|
294
|
+
const commitIndex = useCallback((
|
|
295
|
+
index: number,
|
|
296
|
+
reason: DataTableSelectionChangeReason,
|
|
297
|
+
) => {
|
|
298
|
+
if (selection.kind === "none") return;
|
|
186
299
|
if (index < 0 || index >= tableProps.items.length) return;
|
|
187
300
|
const item = tableProps.items[index]!;
|
|
188
301
|
if (isNavigable && !isNavigable(item, index)) return;
|
|
189
|
-
|
|
190
|
-
if (
|
|
191
|
-
|
|
302
|
+
|
|
303
|
+
if (selection.kind === "index") {
|
|
304
|
+
selection.onChange(index, item, reason);
|
|
305
|
+
return;
|
|
192
306
|
}
|
|
193
|
-
|
|
194
|
-
|
|
307
|
+
|
|
308
|
+
selection.onChange(selection.getId(item, index), item, index, reason);
|
|
309
|
+
}, [isNavigable, selection, tableProps.items]);
|
|
310
|
+
const commitIndexRef = useRef(commitIndex);
|
|
311
|
+
|
|
312
|
+
useEffect(() => {
|
|
313
|
+
commitIndexRef.current = commitIndex;
|
|
314
|
+
}, [commitIndex]);
|
|
315
|
+
|
|
316
|
+
const getCommitTarget = useCallback((index: number): SelectionCommitTarget | null => {
|
|
317
|
+
if (selection.kind === "none") return null;
|
|
318
|
+
if (index < 0 || index >= tableProps.items.length) return null;
|
|
319
|
+
const item = tableProps.items[index]!;
|
|
320
|
+
if (isNavigable && !isNavigable(item, index)) return null;
|
|
321
|
+
return selection.kind === "id"
|
|
322
|
+
? { index, id: selection.getId(item, index) }
|
|
323
|
+
: { index };
|
|
324
|
+
}, [isNavigable, selection, tableProps.items]);
|
|
325
|
+
|
|
326
|
+
const commitTarget = useCallback((
|
|
327
|
+
target: SelectionCommitTarget | null,
|
|
328
|
+
reason: DataTableSelectionChangeReason,
|
|
329
|
+
) => {
|
|
330
|
+
if (!target) return;
|
|
331
|
+
if (selection.kind !== "id" || target.id == null) {
|
|
332
|
+
commitIndexRef.current(target.index, reason);
|
|
195
333
|
return;
|
|
196
334
|
}
|
|
197
|
-
tableProps.
|
|
198
|
-
|
|
335
|
+
const currentIndex = tableProps.items.findIndex(
|
|
336
|
+
(item, index) => selection.getId(item, index) === target.id,
|
|
337
|
+
);
|
|
338
|
+
if (currentIndex < 0) return;
|
|
339
|
+
commitIndexRef.current(currentIndex, reason);
|
|
340
|
+
}, [selection, tableProps.items]);
|
|
199
341
|
|
|
200
|
-
const
|
|
342
|
+
const commitIndexImmediately = useCallback((
|
|
343
|
+
index: number,
|
|
344
|
+
reason: DataTableSelectionChangeReason,
|
|
345
|
+
) => {
|
|
346
|
+
const target = getCommitTarget(index);
|
|
347
|
+
clearPendingCommit();
|
|
348
|
+
commitTarget(target, reason);
|
|
349
|
+
}, [clearPendingCommit, commitTarget, getCommitTarget]);
|
|
350
|
+
|
|
351
|
+
const scheduleCommitIndex = useCallback((index: number) => {
|
|
352
|
+
if (selection.kind === "none") return;
|
|
353
|
+
const target = getCommitTarget(index);
|
|
354
|
+
if (!target) return;
|
|
355
|
+
if (pendingCommitTimerRef.current) {
|
|
356
|
+
clearTimeout(pendingCommitTimerRef.current);
|
|
357
|
+
}
|
|
358
|
+
pendingCommitRef.current = true;
|
|
359
|
+
pendingCommitTargetRef.current = target;
|
|
360
|
+
pendingCommitTimerRef.current = setTimeout(() => {
|
|
361
|
+
pendingCommitTimerRef.current = null;
|
|
362
|
+
pendingCommitRef.current = false;
|
|
363
|
+
const pendingTarget = pendingCommitTargetRef.current;
|
|
364
|
+
pendingCommitTargetRef.current = null;
|
|
365
|
+
commitTarget(pendingTarget, "keyboard");
|
|
366
|
+
}, DATA_TABLE_SELECTION_COMMIT_DELAY_MS);
|
|
367
|
+
}, [commitTarget, getCommitTarget, selection.kind]);
|
|
368
|
+
|
|
369
|
+
const updateCursorIndex = useCallback((
|
|
370
|
+
index: number,
|
|
371
|
+
options: {
|
|
372
|
+
commit: "deferred" | "immediate" | "none";
|
|
373
|
+
reason?: DataTableSelectionChangeReason;
|
|
374
|
+
},
|
|
375
|
+
) => {
|
|
376
|
+
if (selection.kind === "none") return;
|
|
201
377
|
if (index < 0 || index >= tableProps.items.length) return;
|
|
202
378
|
const item = tableProps.items[index]!;
|
|
203
379
|
if (isNavigable && !isNavigable(item, index)) return;
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
380
|
+
effectiveSelectedIndexRef.current = index;
|
|
381
|
+
setCursorIndex(index);
|
|
382
|
+
const reason = options.reason ?? (options.commit === "deferred"
|
|
383
|
+
? "keyboard"
|
|
384
|
+
: options.commit === "immediate"
|
|
385
|
+
? "pointer"
|
|
386
|
+
: "keyboard");
|
|
387
|
+
onCursorChange?.(item, index, reason);
|
|
388
|
+
if (options.commit === "deferred") {
|
|
389
|
+
requestSelectionScroll(index);
|
|
207
390
|
}
|
|
208
|
-
|
|
209
|
-
|
|
391
|
+
if (options.commit === "immediate") {
|
|
392
|
+
commitIndexImmediately(index, reason);
|
|
393
|
+
} else if (options.commit === "deferred") {
|
|
394
|
+
scheduleCommitIndex(index);
|
|
395
|
+
}
|
|
396
|
+
}, [
|
|
397
|
+
commitIndexImmediately,
|
|
398
|
+
isNavigable,
|
|
399
|
+
onCursorChange,
|
|
400
|
+
requestSelectionScroll,
|
|
401
|
+
scheduleCommitIndex,
|
|
402
|
+
selection.kind,
|
|
403
|
+
tableProps.items,
|
|
404
|
+
]);
|
|
405
|
+
|
|
406
|
+
const activateIndex = useCallback((index: number) => {
|
|
407
|
+
if (index < 0 || index >= tableProps.items.length) return;
|
|
408
|
+
const item = tableProps.items[index]!;
|
|
409
|
+
if (isNavigable && !isNavigable(item, index)) return;
|
|
410
|
+
updateCursorIndex(index, { commit: "none", reason: "activation" });
|
|
411
|
+
commitIndexImmediately(index, "activation");
|
|
412
|
+
onActivate?.(item, index);
|
|
413
|
+
}, [commitIndexImmediately, isNavigable, onActivate, tableProps.items, updateCursorIndex]);
|
|
210
414
|
|
|
211
415
|
const selectByOffset = useCallback((offset: -1 | 1) => {
|
|
212
416
|
if (!navigableIndices) {
|
|
@@ -218,7 +422,7 @@ export function DataTableView<
|
|
|
218
422
|
Math.min(selectedIndex + offset, tableProps.items.length - 1),
|
|
219
423
|
)
|
|
220
424
|
: 0;
|
|
221
|
-
|
|
425
|
+
updateCursorIndex(nextIndex, { commit: "deferred" });
|
|
222
426
|
return;
|
|
223
427
|
}
|
|
224
428
|
if (navigableIndices.length === 0) return;
|
|
@@ -230,8 +434,10 @@ export function DataTableView<
|
|
|
230
434
|
)
|
|
231
435
|
: 0;
|
|
232
436
|
const nextIndex = navigableIndices[nextPosition];
|
|
233
|
-
if (nextIndex !== undefined)
|
|
234
|
-
|
|
437
|
+
if (nextIndex !== undefined) {
|
|
438
|
+
updateCursorIndex(nextIndex, { commit: "deferred" });
|
|
439
|
+
}
|
|
440
|
+
}, [navigableIndices, tableProps.items.length, updateCursorIndex]);
|
|
235
441
|
|
|
236
442
|
const activateSelection = useCallback(() => {
|
|
237
443
|
if (!navigableIndices) {
|
|
@@ -250,6 +456,32 @@ export function DataTableView<
|
|
|
250
456
|
activateIndex(selectedIsNavigable ? selectedIndex : navigableIndices[0]!);
|
|
251
457
|
}, [activateIndex, navigableIndices, tableProps.items.length]);
|
|
252
458
|
|
|
459
|
+
const isItemSelected = useCallback((item: T, index: number) => (
|
|
460
|
+
index === effectiveSelectedIndex
|
|
461
|
+
&& (!isNavigable || isNavigable(item, index))
|
|
462
|
+
), [effectiveSelectedIndex, isNavigable]);
|
|
463
|
+
|
|
464
|
+
const handleTableSelect = useCallback((_item: T, index: number) => {
|
|
465
|
+
updateCursorIndex(index, { commit: "immediate" });
|
|
466
|
+
}, [updateCursorIndex]);
|
|
467
|
+
|
|
468
|
+
const handleTableActivate = useCallback((_item: T, index: number) => {
|
|
469
|
+
activateIndex(index);
|
|
470
|
+
}, [activateIndex]);
|
|
471
|
+
|
|
472
|
+
const handleRowMouseDown = useCallback((item: T, index: number, event: any) => {
|
|
473
|
+
const handled = tableProps.onRowMouseDown?.(item, index, event);
|
|
474
|
+
if (handled === true) {
|
|
475
|
+
updateCursorIndex(index, { commit: "immediate" });
|
|
476
|
+
}
|
|
477
|
+
return handled;
|
|
478
|
+
}, [tableProps.onRowMouseDown, updateCursorIndex]);
|
|
479
|
+
|
|
480
|
+
const handleRowContextMenu = useCallback((item: T, index: number, event: any) => {
|
|
481
|
+
updateCursorIndex(index, { commit: "immediate" });
|
|
482
|
+
tableProps.onRowContextMenu?.(item, index, event);
|
|
483
|
+
}, [tableProps.onRowContextMenu, updateCursorIndex]);
|
|
484
|
+
|
|
253
485
|
useShortcut((event) => {
|
|
254
486
|
if (event.defaultPrevented || event.propagationStopped) return;
|
|
255
487
|
if (!focused || !keyboardNavigation) return;
|
|
@@ -293,13 +525,11 @@ export function DataTableView<
|
|
|
293
525
|
setHoveredIdx={effectiveSetHoveredIdx}
|
|
294
526
|
scrollToIndex={effectiveScrollToIndex}
|
|
295
527
|
scrollToIndexVersion={scrollToIndexVersion + selectionScrollVersion}
|
|
296
|
-
isSelected={
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
)
|
|
302
|
-
)}
|
|
528
|
+
isSelected={isItemSelected}
|
|
529
|
+
onSelect={handleTableSelect}
|
|
530
|
+
onActivate={handleTableActivate}
|
|
531
|
+
onRowMouseDown={handleRowMouseDown}
|
|
532
|
+
onRowContextMenu={handleRowContextMenu}
|
|
303
533
|
/>
|
|
304
534
|
</TableViewFrame>
|
|
305
535
|
);
|
|
@@ -327,9 +327,12 @@ export function FeedDataTableStackView({
|
|
|
327
327
|
onBack={() => setOpenItemId(null)}
|
|
328
328
|
detailContent={detailContent}
|
|
329
329
|
detailTitle={openItem ? openItem.detailTitle ?? openItem.title : undefined}
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
330
|
+
selection={{
|
|
331
|
+
kind: "index",
|
|
332
|
+
selectedIndex: activeRowIndex,
|
|
333
|
+
onChange: (_index, row) => onSelect(row.itemIndex),
|
|
334
|
+
}}
|
|
335
|
+
onActivate={(row) => openRow(row)}
|
|
333
336
|
rootBefore={rootBefore}
|
|
334
337
|
rootAfter={rootAfter}
|
|
335
338
|
rootWidth={width}
|
|
@@ -345,11 +348,6 @@ export function FeedDataTableStackView({
|
|
|
345
348
|
nextSortPreference(current, columnId as DetailColumnId)
|
|
346
349
|
)}
|
|
347
350
|
getItemKey={(row) => row.item.id}
|
|
348
|
-
isSelected={(row, index) =>
|
|
349
|
-
row.itemIndex === selectedIdx || (selectedIdx < 0 && index === 0)
|
|
350
|
-
}
|
|
351
|
-
onSelect={(row) => onSelect(row.itemIndex)}
|
|
352
|
-
onActivate={openRow}
|
|
353
351
|
renderCell={renderCell}
|
|
354
352
|
emptyStateTitle={emptyStateTitle}
|
|
355
353
|
emptyStateHint={emptyStateHint}
|
|
@@ -122,6 +122,12 @@ describe("PaneFooterBar", () => {
|
|
|
122
122
|
const col = line.indexOf("[r]efresh");
|
|
123
123
|
expect(col).toBeGreaterThanOrEqual(0);
|
|
124
124
|
|
|
125
|
+
await act(async () => {
|
|
126
|
+
await testSetup!.mockMouse.release(col + 1, 0);
|
|
127
|
+
await testSetup!.renderOnce();
|
|
128
|
+
});
|
|
129
|
+
expect(refreshCount).toBe(0);
|
|
130
|
+
|
|
125
131
|
await act(async () => {
|
|
126
132
|
await testSetup!.mockMouse.click(col + 1, 0);
|
|
127
133
|
await testSetup!.renderOnce();
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Box, Span, Text, TextAttributes, useUiCapabilities } from "../../../../ui";
|
|
2
|
+
import { useRef } from "react";
|
|
2
3
|
import { colors, blendHex } from "../../../../theme/colors";
|
|
3
4
|
import { getShortcutHintWidth, ShortcutHint } from "../../../ui/shortcut-hint";
|
|
4
5
|
import {
|
|
@@ -50,13 +51,24 @@ function stopMouseEvent(event?: { stopPropagation?: () => void; preventDefault?:
|
|
|
50
51
|
function SegmentView({ segment }: { segment: PaneFooterSegment }) {
|
|
51
52
|
const interactive = !!segment.onPress && !segment.disabled;
|
|
52
53
|
const attributes = segment.parts.some((part) => part.bold) || interactive ? TextAttributes.BOLD : 0;
|
|
54
|
+
const triggerMouseDownRef = useRef(false);
|
|
55
|
+
const startSegmentPress = (event?: { stopPropagation?: () => void; preventDefault?: () => void }) => {
|
|
56
|
+
triggerMouseDownRef.current = true;
|
|
57
|
+
stopMouseEvent(event);
|
|
58
|
+
};
|
|
59
|
+
const finishSegmentPress = (event?: { stopPropagation?: () => void; preventDefault?: () => void }) => {
|
|
60
|
+
const startedOnTrigger = triggerMouseDownRef.current;
|
|
61
|
+
triggerMouseDownRef.current = false;
|
|
62
|
+
if (startedOnTrigger) segment.onPress?.();
|
|
63
|
+
else stopMouseEvent(event);
|
|
64
|
+
};
|
|
53
65
|
|
|
54
66
|
return (
|
|
55
67
|
<Text
|
|
56
68
|
fg={segment.disabled ? colors.textMuted : colors.textDim}
|
|
57
69
|
attributes={attributes}
|
|
58
|
-
onMouseDown={interactive ?
|
|
59
|
-
onMouseUp={interactive ?
|
|
70
|
+
onMouseDown={interactive ? startSegmentPress : undefined}
|
|
71
|
+
onMouseUp={interactive ? finishSegmentPress : undefined}
|
|
60
72
|
{...(interactive ? { "data-gloom-interactive": "true" } : {})}
|
|
61
73
|
>
|
|
62
74
|
{segment.parts.map((part, index) => (
|