Skip to content

Commit 4107473

Browse files
author
stephanbuettig
committed
feat: Add bulk ZIP export (#867)
Adds ZIP archive export for HTTP exchanges with 37 code snippet formats via @httptoolkit/httpsnippet. Includes format picker panel, Web Worker generation, and safe filename conventions. Features: - ZIP export with selectable snippet formats (37 languages/clients) - Format picker with category grouping and popular defaults - Web Worker-based generation for non-blocking UI - Safe filename conventions matching existing HAR export pattern New files: snippet-formats registry, export-filenames utility, download helper, zip-metadata model, zip-download-panel component. Unit tests for snippet-formats and export-filenames included. Extracted from #219 as requested by @pimterry.
1 parent 23a9952 commit 4107473

12 files changed

Lines changed: 1558 additions & 12 deletions

File tree

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@
9494
"dompurify": "^3.3.3",
9595
"fast-json-patch": "^3.1.1",
9696
"fast-xml-parser": "^5.5.7",
97+
"fflate": "^0.8.2",
9798
"graphql": "^15.8.0",
9899
"har-validator": "^5.1.3",
99100
"http-encoding": "^2.0.1",

src/components/view/http/http-export-card.tsx

Lines changed: 60 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import * as _ from 'lodash';
12
import React from "react";
23
import { action, computed } from "mobx";
34
import { inject, observer } from "mobx-react";
@@ -20,6 +21,8 @@ import {
2021
snippetExportOptions,
2122
SnippetOption
2223
} from '../../../model/ui/export';
24+
import { ZIP_ALL_FORMAT_KEY } from '../../../model/ui/snippet-formats';
25+
import { ZipDownloadPanel } from '../zip-download-panel';
2326

2427
import { ProHeaderPill, CardSalesPitch } from '../../account/pro-placeholders';
2528
import {
@@ -135,6 +138,32 @@ const ExportHarPill = styled(observer((p: {
135138
margin-right: auto;
136139
`;
137140

141+
// Virtual SnippetOption used as the PillSelector value when ZIP is selected.
142+
// This is never passed to httpsnippet — it's only used for dropdown rendering.
143+
const ZIP_SNIPPET_OPTION: SnippetOption = {
144+
target: ZIP_ALL_FORMAT_KEY as any,
145+
client: '' as any,
146+
name: 'ZIP (Selected Formats)',
147+
description: 'Download selected code snippet formats in a single ZIP archive',
148+
link: ''
149+
};
150+
151+
// Build extended optGroups with ZIP at the top
152+
const exportOptionsWithZip: _.Dictionary<SnippetOption[]> = {
153+
'Archive': [ZIP_SNIPPET_OPTION],
154+
...snippetExportOptions
155+
};
156+
157+
const getExportFormatKey = (option: SnippetOption): string => {
158+
if (option === ZIP_SNIPPET_OPTION) return ZIP_ALL_FORMAT_KEY;
159+
return getCodeSnippetFormatKey(option);
160+
};
161+
162+
const getExportFormatName = (option: SnippetOption): string => {
163+
if (option === ZIP_SNIPPET_OPTION) return ZIP_SNIPPET_OPTION.name;
164+
return getCodeSnippetFormatName(option);
165+
};
166+
138167
@inject('accountStore')
139168
@inject('uiStore')
140169
@observer
@@ -143,6 +172,7 @@ export class HttpExportCard extends React.Component<ExportCardProps> {
143172
render() {
144173
const { exchange, accountStore } = this.props;
145174
const isPaidUser = accountStore!.user.isPaidUser();
175+
const isZipSelected = this.isZipSelected;
146176

147177
return <CollapsibleCard {...this.props}>
148178
<header>
@@ -153,10 +183,10 @@ export class HttpExportCard extends React.Component<ExportCardProps> {
153183

154184
<PillSelector<SnippetOption>
155185
onChange={this.setSnippetOption}
156-
value={this.snippetOption}
157-
optGroups={snippetExportOptions}
158-
keyFormatter={getCodeSnippetFormatKey}
159-
nameFormatter={getCodeSnippetFormatName}
186+
value={this.currentDropdownValue}
187+
optGroups={exportOptionsWithZip}
188+
keyFormatter={getExportFormatKey}
189+
nameFormatter={getExportFormatName}
160190
/>
161191

162192
<CollapsibleCardHeading onCollapseToggled={this.props.onCollapseToggled}>
@@ -166,10 +196,13 @@ export class HttpExportCard extends React.Component<ExportCardProps> {
166196

167197
{ isPaidUser ?
168198
<div>
169-
<ExportSnippetEditor
170-
exchange={exchange}
171-
exportOption={this.snippetOption}
172-
/>
199+
{ isZipSelected
200+
? <ZipDownloadPanel exchanges={[exchange]} />
201+
: <ExportSnippetEditor
202+
exchange={exchange}
203+
exportOption={this.snippetOption}
204+
/>
205+
}
173206
</div>
174207
:
175208
<CardSalesPitch source='export'>
@@ -188,11 +221,29 @@ export class HttpExportCard extends React.Component<ExportCardProps> {
188221
</CollapsibleCard>;
189222
}
190223

224+
@computed
225+
private get isZipSelected(): boolean {
226+
return (this.props.uiStore!.exportSnippetFormat || '') === ZIP_ALL_FORMAT_KEY;
227+
}
228+
229+
@computed
230+
private get currentDropdownValue(): SnippetOption {
231+
if (this.isZipSelected) return ZIP_SNIPPET_OPTION;
232+
return this.snippetOption;
233+
}
234+
191235
@computed
192236
private get snippetOption(): SnippetOption {
193237
let exportSnippetFormat = this.props.uiStore!.exportSnippetFormat ||
194238
DEFAULT_SNIPPET_FORMAT_KEY;
195-
return getCodeSnippetOptionFromKey(exportSnippetFormat);
239+
// If ZIP is selected, fall back to default for the snippet option
240+
if (exportSnippetFormat === ZIP_ALL_FORMAT_KEY) {
241+
exportSnippetFormat = DEFAULT_SNIPPET_FORMAT_KEY;
242+
}
243+
// Guard: if the format key doesn't resolve (e.g. deleted/invalid key),
244+
// fall back to the default cURL option
245+
return getCodeSnippetOptionFromKey(exportSnippetFormat)
246+
?? getCodeSnippetOptionFromKey(DEFAULT_SNIPPET_FORMAT_KEY);
196247
}
197248

198249
@action.bound

0 commit comments

Comments
 (0)