Skip to content

Commit 6d29934

Browse files
committed
Make context menu & rule creation work for multi-select
1 parent ac9c18f commit 6d29934

File tree

2 files changed

+247
-155
lines changed

2 files changed

+247
-155
lines changed
Lines changed: 173 additions & 121 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as _ from 'lodash';
2-
import { runInAction } from 'mobx';
2+
import { action, runInAction } from 'mobx';
33

44
import { CollectedEvent, HttpExchangeView, WebSocketStream } from '../../types';
55

@@ -25,143 +25,195 @@ export class ViewEventContextMenuBuilder {
2525
private accountStore: AccountStore,
2626
private uiStore: UiStore,
2727

28-
private onPin: (event: CollectedEvent) => void,
29-
private onDelete: (event: CollectedEvent) => void,
30-
private onBuildRuleFromExchange: (exchange: HttpExchangeView) => void,
31-
private onPrepareToResendRequest: (exchange: HttpExchangeView) => void,
32-
private onAddFilter: (filter: Filter) => void
33-
) {}
34-
35-
private readonly BaseOptions = {
36-
Pin: {
37-
type: 'option',
38-
label: 'Toggle Pinning',
39-
callback: this.onPin
40-
},
41-
Delete: {
42-
type: 'option',
43-
label: 'Delete',
44-
callback: this.onDelete
28+
private getSelectedEvents: () => ReadonlyArray<CollectedEvent>,
29+
private callbacks: {
30+
onPin: () => void,
31+
onDelete: (event: CollectedEvent) => void,
32+
onDeleteSelection: () => void,
33+
onBuildRuleFromExchange: (exchange: HttpExchangeView) => void,
34+
onBuildRuleFromSelectedExchanges: () => void,
35+
onPrepareToResendRequest: (exchange: HttpExchangeView) => void,
36+
onAddFilter: (filter: Filter) => void
4537
}
46-
} as const;
38+
) {}
4739

4840
getContextMenuCallback(event: CollectedEvent) {
4941
return (mouseEvent: React.MouseEvent) => {
50-
const isPaidUser = this.accountStore.user.isPaidUser();
42+
const { selectedEventIds } = this.uiStore;
43+
const isMultiSelected = selectedEventIds.size > 1 && selectedEventIds.has(event.id);
5144

52-
const preferredExportFormat = this.uiStore.exportSnippetFormat
53-
? getCodeSnippetOptionFromKey(this.uiStore.exportSnippetFormat)
54-
: undefined;
45+
if (isMultiSelected) {
46+
this.showMultiSelectionMenu(mouseEvent);
47+
} else if (event.isHttp()) {
48+
this.showHttpEventMenu(mouseEvent, event);
49+
} else {
50+
this.showBasicEventMenu(mouseEvent, event);
51+
}
52+
};
53+
}
5554

56-
if (event.isHttp()) {
57-
const menuOptions = [
58-
this.BaseOptions.Pin,
59-
{
60-
type: 'submenu',
61-
label: 'Filter traffic like this',
62-
items: [
63-
{
64-
type: 'option',
65-
label: 'Show only this hostname',
66-
callback: () => this.onAddFilter(
67-
new HostnameFilter(`hostname=${event.request.parsedUrl.hostname}`)
68-
)
69-
},
70-
{
71-
type: 'option',
72-
label: 'Hide this hostname',
73-
callback: () => this.onAddFilter(
74-
new HostnameFilter(`hostname!=${event.request.parsedUrl.hostname}`)
75-
)
76-
}
77-
]
78-
},
79-
{
80-
type: 'option',
81-
label: 'Copy Request URL',
82-
callback: (data: HttpExchangeView) => copyToClipboard(data.request.url)
83-
},
84-
...(!isPaidUser ? [
85-
{ type: 'separator' },
86-
{ type: 'option', label: 'With Pro:', enabled: false, callback: () => {} }
87-
] as const : []),
88-
...(this.onPrepareToResendRequest ? [
89-
{
90-
type: 'option',
91-
enabled: isPaidUser,
92-
label: 'Resend Request',
93-
callback: (data: HttpExchangeView) => this.onPrepareToResendRequest!(data)
94-
}
95-
] as const : []),
55+
private showMultiSelectionMenu(mouseEvent: React.MouseEvent) {
56+
const isPaidUser = this.accountStore.user.isPaidUser();
57+
const selectedEvents = this.getSelectedEvents();
58+
const count = selectedEvents.length;
59+
const httpCount = selectedEvents.filter(e => e.isHttp() && !e.isWebSocket()).length;
60+
61+
const menuOptions: Array<ContextMenuItem<void>> = [
62+
{
63+
type: 'option',
64+
label: 'Toggle Pinning',
65+
callback: () => this.callbacks.onPin()
66+
},
67+
...(httpCount > 0 ? [{
68+
type: 'option' as const,
69+
enabled: isPaidUser,
70+
label: `Create ${httpCount} Matching Rule${httpCount !== 1 ? 's' : ''}`,
71+
callback: () => this.callbacks.onBuildRuleFromSelectedExchanges()
72+
}] : []),
73+
{
74+
type: 'option',
75+
label: `Delete ${count} Event${count !== 1 ? 's' : ''}`,
76+
callback: () => this.callbacks.onDeleteSelection()
77+
}
78+
];
79+
80+
this.uiStore.handleContextMenuEvent(
81+
mouseEvent,
82+
menuOptions,
83+
undefined as void
84+
);
85+
}
86+
87+
private showHttpEventMenu(mouseEvent: React.MouseEvent, event: HttpExchange | WebSocketStream) {
88+
const isPaidUser = this.accountStore.user.isPaidUser();
89+
90+
const preferredExportFormat = this.uiStore.exportSnippetFormat
91+
? getCodeSnippetOptionFromKey(this.uiStore.exportSnippetFormat)
92+
: undefined;
93+
94+
const menuOptions = [
95+
{
96+
type: 'option',
97+
label: 'Toggle Pinning',
98+
callback: action((data: HttpExchangeView) => { data.pinned = !data.pinned; })
99+
},
100+
{
101+
type: 'submenu',
102+
label: 'Filter traffic like this',
103+
items: [
96104
{
97105
type: 'option',
98-
enabled: isPaidUser,
99-
label: `Create Matching Modify Rule`,
100-
callback: this.onBuildRuleFromExchange
106+
label: 'Show only this hostname',
107+
callback: () => this.callbacks.onAddFilter(
108+
new HostnameFilter(`hostname=${event.request.parsedUrl.hostname}`)
109+
)
101110
},
102111
{
103112
type: 'option',
104-
enabled: isPaidUser,
105-
label: `Export Exchange as HAR`,
106-
callback: exportHar
107-
},
108-
// If you have a preferred default format, we show that option at the top level:
109-
...(preferredExportFormat && isPaidUser ? [{
110-
type: 'option',
111-
label: `Copy as ${getCodeSnippetFormatName(preferredExportFormat)} Snippet`,
113+
label: 'Hide this hostname',
114+
callback: () => this.callbacks.onAddFilter(
115+
new HostnameFilter(`hostname!=${event.request.parsedUrl.hostname}`)
116+
)
117+
}
118+
]
119+
},
120+
{
121+
type: 'option',
122+
label: 'Copy Request URL',
123+
callback: (data: HttpExchangeView) => copyToClipboard(data.request.url)
124+
},
125+
...(!isPaidUser ? [
126+
{ type: 'separator' },
127+
{ type: 'option', label: 'With Pro:', enabled: false, callback: () => {} }
128+
] as const : []),
129+
...(this.callbacks.onPrepareToResendRequest ? [
130+
{
131+
type: 'option',
132+
enabled: isPaidUser,
133+
label: 'Resend Request',
134+
callback: (data: HttpExchangeView) => this.callbacks.onPrepareToResendRequest!(data)
135+
}
136+
] as const : []),
137+
{
138+
type: 'option',
139+
enabled: isPaidUser,
140+
label: `Create Matching Modify Rule`,
141+
callback: this.callbacks.onBuildRuleFromExchange
142+
},
143+
{
144+
type: 'option',
145+
enabled: isPaidUser,
146+
label: `Export Exchange as HAR`,
147+
callback: exportHar
148+
},
149+
// If you have a preferred default format, we show that option at the top level:
150+
...(preferredExportFormat && isPaidUser ? [{
151+
type: 'option',
152+
label: `Copy as ${getCodeSnippetFormatName(preferredExportFormat)} Snippet`,
153+
callback: async (data: HttpExchange) => {
154+
copyToClipboard(
155+
await generateCodeSnippet(data, preferredExportFormat, {
156+
waitForBodyDecoding: true
157+
})
158+
);
159+
}
160+
}] as const : []),
161+
{
162+
type: 'submenu',
163+
enabled: isPaidUser,
164+
label: `Copy as Code Snippet`,
165+
items: Object.keys(snippetExportOptions).map((snippetGroupName) => ({
166+
type: 'submenu' as const,
167+
label: snippetGroupName,
168+
items: snippetExportOptions[snippetGroupName].map((snippetOption) => ({
169+
type: 'option' as const,
170+
label: getCodeSnippetFormatName(snippetOption),
112171
callback: async (data: HttpExchange) => {
172+
// When you pick an option here, it updates your preferred default option
173+
runInAction(() => {
174+
this.uiStore.exportSnippetFormat = getCodeSnippetFormatKey(snippetOption);
175+
});
176+
113177
copyToClipboard(
114-
await generateCodeSnippet(data, preferredExportFormat, {
178+
await generateCodeSnippet(data, snippetOption, {
115179
waitForBodyDecoding: true
116180
})
117181
);
118182
}
119-
}] as const : []),
120-
{
121-
type: 'submenu',
122-
enabled: isPaidUser,
123-
label: `Copy as Code Snippet`,
124-
items: Object.keys(snippetExportOptions).map((snippetGroupName) => ({
125-
type: 'submenu' as const,
126-
label: snippetGroupName,
127-
items: snippetExportOptions[snippetGroupName].map((snippetOption) => ({
128-
type: 'option' as const,
129-
label: getCodeSnippetFormatName(snippetOption),
130-
callback: async (data: HttpExchange) => {
131-
// When you pick an option here, it updates your preferred default option
132-
runInAction(() => {
133-
this.uiStore.exportSnippetFormat = getCodeSnippetFormatKey(snippetOption);
134-
});
135-
136-
copyToClipboard(
137-
await generateCodeSnippet(data, snippetOption, {
138-
waitForBodyDecoding: true
139-
})
140-
);
141-
}
142-
}))
143-
}))
144-
},
145-
this.BaseOptions.Delete
146-
];
147-
148-
const sortedOptions = _.sortBy(menuOptions, (o: ContextMenuItem<any>) =>
149-
o.type === 'separator' || !(o.enabled ?? true)
150-
) as Array<ContextMenuItem<HttpExchange | WebSocketStream>>;
151-
152-
this.uiStore.handleContextMenuEvent(
153-
mouseEvent,
154-
sortedOptions,
155-
event
156-
)
157-
} else {
158-
// For non-HTTP events, we just show the super-basic globally supported options:
159-
this.uiStore.handleContextMenuEvent(mouseEvent, [
160-
this.BaseOptions.Pin,
161-
this.BaseOptions.Delete
162-
], event);
183+
}))
184+
}))
185+
},
186+
{
187+
type: 'option',
188+
label: 'Delete',
189+
callback: this.callbacks.onDelete
163190
}
164-
};
191+
];
192+
193+
const sortedOptions = _.sortBy(menuOptions, (o: ContextMenuItem<unknown>) =>
194+
o.type === 'separator' || !(o.enabled ?? true)
195+
) as Array<ContextMenuItem<HttpExchange | WebSocketStream>>;
196+
197+
this.uiStore.handleContextMenuEvent(
198+
mouseEvent,
199+
sortedOptions,
200+
event
201+
)
202+
}
203+
204+
private showBasicEventMenu(mouseEvent: React.MouseEvent, event: CollectedEvent) {
205+
this.uiStore.handleContextMenuEvent(mouseEvent, [
206+
{
207+
type: 'option',
208+
label: 'Toggle Pinning',
209+
callback: action((data: CollectedEvent) => { data.pinned = !data.pinned; })
210+
},
211+
{
212+
type: 'option',
213+
label: 'Delete',
214+
callback: this.callbacks.onDelete
215+
}
216+
], event);
165217
}
166218

167-
}
219+
}

0 commit comments

Comments
 (0)