Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 21 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@
"exifr": "^7.1.3",
"fast-json-patch": "^3.1.1",
"fast-xml-parser": "^5.7.0",
"fflate": "^0.8.2",
"graphql": "^15.8.0",
"har-validator": "^5.1.3",
"http-encoding": "^2.0.1",
Expand Down
41 changes: 34 additions & 7 deletions src/components/app.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as _ from 'lodash';
import * as React from 'react';
import { computed } from 'mobx';
import { autorun, computed, IReactionDisposer } from 'mobx';
import { observer, inject } from 'mobx-react';
import {
Router,
Expand Down Expand Up @@ -38,6 +38,7 @@ import { CheckoutSpinner } from './account/checkout-spinner';
import { HtmlContextMenu } from './html-context-menu';
import { DisconnectedWarning } from './disconnected-warning';
import { McpModal } from './mcp/mcp-modal';
import { ZipExportDialog } from './view/zip-export-dialog';

const AppContainer = styled.div<{ inert?: boolean }>`
display: flex;
Expand Down Expand Up @@ -100,6 +101,20 @@ class App extends React.Component<{
proxyStore: ProxyStore
}> {

private closeZipExportOnAccountModal?: IReactionDisposer;

componentDidMount() {
this.closeZipExportOnAccountModal = autorun(() => {
if (this.props.accountStore.modal) {
this.props.uiStore.closeZipExport();
}
});
}

componentWillUnmount() {
this.closeZipExportOnAccountModal?.();
}

@computed
get canUseMcp() {
const mcpPath = this.props.proxyStore.toolPaths?.mcp;
Expand Down Expand Up @@ -225,19 +240,23 @@ class App extends React.Component<{

const {
contextMenuState,
clearHtmlContextMenu
clearHtmlContextMenu,
zipExportRequest,
closeZipExport
} = this.props.uiStore;

const hasModal = !!modal || !!zipExportRequest;

return <LocationProvider history={appHistory}>
<AppKeyboardShortcuts
navigate={appHistory.navigate}
canVisitSettings={this.canVisitSettings}
/>
<AppContainer
aria-hidden={!!modal}
inert={!!modal}
aria-hidden={hasModal}
inert={hasModal}
// 'inert' doesn't actually work - it's non-standard, so we need this:
ref={node => node && (!!modal ?
ref={node => node && (hasModal ?
node.setAttribute('inert', '') : node.removeAttribute('inert')
)}
>
Expand All @@ -257,7 +276,7 @@ class App extends React.Component<{
<DisconnectedWarning />
</AppContainer>

{ !!modal && <ModalOverlay /> }
{ hasModal && <ModalOverlay /> }

{
modal === 'login' &&
Expand Down Expand Up @@ -286,6 +305,14 @@ class App extends React.Component<{
<McpModal onClose={this.props.uiStore.closeMcpModal} />
}

{ zipExportRequest &&
<ZipExportDialog
events={zipExportRequest.events}
titleSuffix={zipExportRequest.titleSuffix}
onClose={closeZipExport}
/>
}

{
contextMenuState &&
<HtmlContextMenu
Expand All @@ -302,4 +329,4 @@ const AppWithStoreInjected = (
App as unknown as WithInjected<typeof App, 'accountStore' | 'uiStore' | 'proxyStore'>
);

export { AppWithStoreInjected as App };
export { AppWithStoreInjected as App };
111 changes: 66 additions & 45 deletions src/components/view/http/http-export-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { action, computed } from "mobx";
import { inject, observer } from "mobx-react";
import dedent from 'dedent';

import { HttpExchangeView } from '../../../types';
import { CollectedEvent, HttpExchangeView } from '../../../types';
import { styled } from '../../../styles';
import { Icon } from '../../../icons';
import { logError } from '../../../errors';
Expand All @@ -15,7 +15,7 @@ import {
generateCodeSnippet,
getCodeSnippetFormatKey,
getCodeSnippetFormatName,
getCodeSnippetOptionFromKey,
getSafeCodeSnippetOptionFromKey,
DEFAULT_SNIPPET_FORMAT_KEY,
snippetExportOptions,
SnippetOption
Expand Down Expand Up @@ -144,59 +144,80 @@ export class HttpExportCard extends React.Component<ExportCardProps> {
const { exchange, accountStore } = this.props;
const isPaidUser = accountStore!.user.isPaidUser();

return <CollapsibleCard {...this.props}>
<header>
{ isPaidUser
? <ExportHarPill exchange={exchange} />
: <ProHeaderPill />
}

<PillSelector<SnippetOption>
onChange={this.setSnippetOption}
value={this.snippetOption}
optGroups={snippetExportOptions}
keyFormatter={getCodeSnippetFormatKey}
nameFormatter={getCodeSnippetFormatName}
/>

<CollapsibleCardHeading onCollapseToggled={this.props.onCollapseToggled}>
Export
</CollapsibleCardHeading>
</header>

{ isPaidUser ?
<div>
<ExportSnippetEditor
exchange={exchange}
exportOption={this.snippetOption}
return <>
<CollapsibleCard {...this.props}>
<header>
{ isPaidUser
? <>
<ExportHarPill exchange={exchange} />
{/*
* ZIP PillButton is active immediately (even when
* the card is collapsed). The click stops propagation
* so a header click underneath does not inadvertently
* toggle the card.
*/}
<PillButton
onClick={(e) => {
e.stopPropagation();
this.props.uiStore!.openZipExport(
[exchange as unknown as CollectedEvent],
'1 request'
);
}}
>
<Icon icon={['fas', 'file-archive']} /> ZIP
</PillButton>
</>
: <ProHeaderPill />
}

<PillSelector<SnippetOption>
onChange={this.setSnippetOption}
value={this.snippetOption}
optGroups={snippetExportOptions}
keyFormatter={getCodeSnippetFormatKey}
nameFormatter={getCodeSnippetFormatName}
/>
</div>
:
<CardSalesPitch source='export'>
<p>
Instantly export requests as code, for languages and tools including cURL, wget, JS
(XHR, Node HTTP, Request, ...), Python (native or Requests), Ruby, Java (OkHttp
or Unirest), Go, PHP, Swift, HTTPie, and a whole lot more.
</p>
<p>
Want to save the exchange itself? Export one or all requests as HAR (the{' '}
<a href="https://en.wikipedia.org/wiki/.har">HTTP Archive Format</a>), to import
and examine elsewhere, share with your team, or store for future reference.
</p>
</CardSalesPitch>
}
</CollapsibleCard>;

<CollapsibleCardHeading onCollapseToggled={this.props.onCollapseToggled}>
Export
</CollapsibleCardHeading>
</header>

{ isPaidUser ?
<div>
<ExportSnippetEditor
exchange={exchange}
exportOption={this.snippetOption}
/>
</div>
:
<CardSalesPitch source='export'>
<p>
Instantly export requests as code, for languages and tools including cURL, wget, JS
(XHR, Node HTTP, Request, ...), Python (native or Requests), Ruby, Java (OkHttp
or Unirest), Go, PHP, Swift, HTTPie, and a whole lot more.
</p>
<p>
Want to save the exchange itself? Export one or all requests as HAR (the{' '}
<a href="https://en.wikipedia.org/wiki/.har">HTTP Archive Format</a>), to import
and examine elsewhere, share with your team, or store for future reference.
</p>
</CardSalesPitch>
}
</CollapsibleCard>
</>;
}

@computed
private get snippetOption(): SnippetOption {
let exportSnippetFormat = this.props.uiStore!.exportSnippetFormat ||
DEFAULT_SNIPPET_FORMAT_KEY;
return getCodeSnippetOptionFromKey(exportSnippetFormat);
return getSafeCodeSnippetOptionFromKey(exportSnippetFormat);
}

@action.bound
setSnippetOption(optionKey: string) {
this.props.uiStore!.exportSnippetFormat = optionKey;
}
};
};
24 changes: 21 additions & 3 deletions src/components/view/multi-selection-summary-pane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { css, styled } from '../../styles';
import { Ctrl, saveFile } from '../../util/ui';
import { CollectedEvent } from '../../types';
import { AccountStore } from '../../model/account/account-store';
import { UiStore } from '../../model/ui/ui-store';
import { generateHar } from '../../model/http/har';

import { Icon } from '../../icons';
Expand Down Expand Up @@ -162,8 +163,9 @@ const ProActionsOverlay = styled(GetProOverlay)`

const PREVIEW_COUNT = 10;

export const MultiSelectionSummaryPane = inject('accountStore')(observer((props: {
export const MultiSelectionSummaryPane = inject('accountStore', 'uiStore')(observer((props: {
accountStore?: AccountStore,
uiStore?: UiStore,
selectedEvents: ReadonlyArray<CollectedEvent>,
onPin: () => void,
onDelete: () => void,
Expand All @@ -173,9 +175,10 @@ export const MultiSelectionSummaryPane = inject('accountStore')(observer((props:
const count = selectedEvents.length;
const isPaidUser = props.accountStore!.user.isPaidUser();

const httpCount = selectedEvents.filter(e =>
const exportableEvents = selectedEvents.filter(e =>
e.isHttp() && !e.isWebSocket()
).length;
);
const httpCount = exportableEvents.length;

const allHttp = count > 0 && selectedEvents.every(e => e.isHttp());
const allPinned = selectedEvents.every(e => e.pinned);
Expand Down Expand Up @@ -213,6 +216,20 @@ export const MultiSelectionSummaryPane = inject('accountStore')(observer((props:
<Icon icon={['fas', 'save']} fixedWidth />
Export as HAR
</ActionButton>
<ActionButton
title={isPaidUser
? 'Export selected exchanges as a ZIP (HAR + snippets + manifest)'
: 'With Pro: export as ZIP'
}
disabled={!isPaidUser || httpCount === 0}
onClick={() => props.uiStore!.openZipExport(
exportableEvents,
`${exportableEvents.length} request${exportableEvents.length !== 1 ? 's' : ''}`
)}
>
<Icon icon={['fas', 'file-archive']} fixedWidth />
Export as ZIP
</ActionButton>
</>;

return <SummaryContainer>
Expand Down Expand Up @@ -263,5 +280,6 @@ export const MultiSelectionSummaryPane = inject('accountStore')(observer((props:
</>
}
</ActionsContainer>

</SummaryContainer>;
}));
Loading