Skip to content

Commit b8a4719

Browse files
committed
- Add import/export feature
- Fix bug: cannot parameterise pathnames that contains certain characters
1 parent 501d3f7 commit b8a4719

14 files changed

Lines changed: 208 additions & 18 deletions

package-lock.json

Lines changed: 32 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,12 @@
1616
"@chakra-ui/react": "^2.8.1",
1717
"@emotion/react": "^11.11.1",
1818
"@emotion/styled": "^11.11.0",
19+
"copy-to-clipboard": "^3.3.3",
1920
"decode-uri-component": "^0.4.1",
2021
"framer-motion": "^10.16.4",
2122
"fuse.js": "^6.6.2",
2223
"genson-js": "^0.0.8",
24+
"json-stable-stringify": "^1.0.2",
2325
"lodash": "^4.17.21",
2426
"openapi3-ts": "^4.1.2",
2527
"radix3": "^1.1.0",
@@ -28,13 +30,14 @@
2830
"react-select": "^5.7.7",
2931
"react-virtualized-auto-sizer": "^1.0.20",
3032
"react-window": "^1.8.9",
31-
"redoc": "^2.1.2",
33+
"redoc": "^2.1.3",
3234
"store2": "^2.14.2"
3335
},
3436
"devDependencies": {
3537
"@crxjs/vite-plugin": "^2.0.0-beta.19",
3638
"@seriousme/openapi-schema-validator": "^2.1.2",
3739
"@types/chrome": "^0.0.246",
40+
"@types/json-stable-stringify": "^1.0.35",
3841
"@types/lodash": "^4.14.199",
3942
"@types/react": "^18.2.28",
4043
"@types/react-dom": "^18.2.13",

resources/dist.zip

19.5 KB
Binary file not shown.

src/lib/RequestStore.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { JSONType, RouterMap, LeafMap, Endpoint } from "../utils/types";
22
import { parameterise, insertLeafMap, upsert } from "./store-helpers";
33
import { omit, unset } from "lodash";
44
import leafMapToEndpoints from "./leafmap-to-endpoints";
5+
import stringify from "json-stable-stringify";
56

67
/**
78
* RequestStore handles routing to endpoints
@@ -18,6 +19,26 @@ export default class RequestStore {
1819
this.disabledHosts = new Set();
1920
}
2021

22+
public import(json: string): boolean {
23+
try {
24+
const { store, leafMap, disabledHosts } = JSON.parse(json);
25+
this.disabledHosts = new Set(disabledHosts);
26+
this.store = store;
27+
this.leafMap = leafMap;
28+
return true;
29+
} catch {
30+
return false;
31+
}
32+
}
33+
34+
public export = (): string => {
35+
return stringify({
36+
store: this.store,
37+
leafMap: this.leafMap,
38+
disabledHosts: Array.from(this.disabledHosts),
39+
});
40+
};
41+
2142
public clear(): void {
2243
this.store = {};
2344
this.leafMap = {};

src/lib/leafmap-to-endpoints.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import decodeUriComponent from "decode-uri-component";
21
import { Endpoint, LeafMap } from "../utils/types";
32
import { pathToParts } from "./store-helpers/helpers";
43

@@ -9,7 +8,7 @@ const leafMapToEndpoints = (leafMap: LeafMap): Array<Endpoint> => {
98
const endpoint: Endpoint = {
109
host,
1110
parts: pathToParts(path),
12-
pathname: decodeUriComponent(path),
11+
pathname: path,
1312
data: leaf.data,
1413
};
1514
endpoints.push(endpoint);

src/lib/store-helpers/create-leaf.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,18 @@
1+
import decodeUriComponent from "decode-uri-component";
12
import {
23
createSchemaElseUndefined,
34
entriesToJSONType,
45
parseJSON,
56
} from "../../utils/helpers";
67
import { JSONType, Leaf } from "../../utils/types";
78
import { parseAuthHeader } from "../../utils/httpauthentication";
8-
import decodeUriComponent from "decode-uri-component";
99
import { filterIgnoreHeaders } from '../../utils/headers';
1010

1111
function createLeaf(
1212
harRequest: chrome.devtools.network.Request,
1313
responseBody: JSONType
1414
): Leaf {
1515
const authentication = parseAuthHeader(harRequest.request.headers);
16-
harRequest.request.url = decodeUriComponent(harRequest.request.url);
1716
harRequest.request.headers = filterIgnoreHeaders(harRequest.request.headers);
1817
harRequest.response.headers = filterIgnoreHeaders(harRequest.response.headers);
1918
const method = harRequest.request.method;
@@ -22,7 +21,7 @@ function createLeaf(
2221
const requestHeaders = entriesToJSONType(harRequest.request.headers);
2322
const responseHeaders = entriesToJSONType(harRequest.response.headers);
2423
const queryParameters = entriesToJSONType(harRequest.request.queryString);
25-
const pathname = new URL(harRequest.request.url).pathname;
24+
const pathname = decodeUriComponent(new URL(harRequest.request.url).pathname);
2625
const leafPart: Leaf = {
2726
...(authentication && { authentication }),
2827
pathname,

src/lib/store-helpers/helpers.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { MatchedRoute } from "radix3";
22
import { arrayToPath, getParamName, pathToArray } from "../../utils/helpers";
33
import { Leaf, LeafMap, PartType, Parts, RouteData } from "../../utils/types";
4-
import decodeUriComponent from "decode-uri-component";
54

65
const paramRe = /:param(\d+)/;
76
const paramReExact = /^:param(\d+)$/;
@@ -58,7 +57,7 @@ const getPartType = (part: string): PartType => {
5857

5958
export const pathToParts = (path: string): Parts => {
6059
return pathToArray(path).map((part) => ({
61-
part: decodeUriComponent(part),
60+
part,
6261
type: getPartType(part),
6362
}));
6463
};

src/lib/store-helpers/upsert.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,9 @@ export default function upsert({
2323
store,
2424
}: Params): Returns | null {
2525
const url = new URL(harRequest.request.url);
26-
const { host, pathname } = url;
27-
const parts = pathToArray(decodeUriComponent(pathname));
26+
const { host } = url;
27+
const pathname = decodeUriComponent(url.pathname);
28+
const parts = pathToArray(pathname);
2829
if (parts.length === 0) return null;
2930
// Set the host on first visit
3031
if (!store[host]) {

src/ui/ControlConfig.tsx

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { useContext } from "react";
2+
import { Button, Tooltip, HStack, useToast } from "@chakra-ui/react";
3+
import copy from 'copy-to-clipboard';
4+
import Context from './context';
5+
import ControlConfigImport from './ControlConfigImport';
6+
7+
const ControlConfig = () => {
8+
const ctx = useContext(Context);
9+
const toast = useToast();
10+
const onExport = () => {
11+
const stateStr = ctx.export();
12+
copy(stateStr);
13+
toast({
14+
title: "Copied to clipboard.",
15+
status: "success",
16+
duration: 2000,
17+
isClosable: true,
18+
});
19+
};
20+
return (
21+
<HStack>
22+
<ControlConfigImport />
23+
<Tooltip label="Export will copy a state string to your clipboard. Save this somewhere on your computer. You can load it by clicking import and pasting the string.">
24+
<Button size="md" onClick={onExport} colorScheme="teal">
25+
Export
26+
</Button>
27+
</Tooltip>
28+
</HStack>
29+
);
30+
};
31+
32+
export default ControlConfig;

src/ui/ControlConfigImport.tsx

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { useRef, useState, useContext } from "react";
2+
import {
3+
useDisclosure,
4+
Button,
5+
Modal,
6+
ModalOverlay,
7+
ModalContent,
8+
ModalHeader,
9+
ModalCloseButton,
10+
ModalBody,
11+
FormControl,
12+
Textarea,
13+
ModalFooter,
14+
Tooltip,
15+
useToast,
16+
} from "@chakra-ui/react";
17+
import Context from "./context";
18+
19+
function ControlConfigImport() {
20+
const ctx = useContext(Context);
21+
const toast = useToast();
22+
const { isOpen, onOpen, onClose } = useDisclosure();
23+
const [value, setValue] = useState("");
24+
25+
const initialRef = useRef(null);
26+
const finalRef = useRef(null);
27+
28+
const onImport = () => {
29+
const result = ctx.import(value);
30+
if (result) {
31+
toast({
32+
title: "Imported.",
33+
status: "success",
34+
duration: 2000,
35+
isClosable: true,
36+
});
37+
} else {
38+
toast({
39+
title: "Could not import.",
40+
status: "error",
41+
duration: 2000,
42+
isClosable: true,
43+
});
44+
}
45+
onClose();
46+
return result;
47+
};
48+
49+
return (
50+
<>
51+
<Tooltip label="Import a state string that was previously exported. This will open a modal, paste in the string to load the state.">
52+
<Button size="md" onClick={onOpen} colorScheme="teal">
53+
Import
54+
</Button>
55+
</Tooltip>
56+
57+
<Modal
58+
initialFocusRef={initialRef}
59+
finalFocusRef={finalRef}
60+
isOpen={isOpen}
61+
onClose={onClose}
62+
>
63+
<ModalOverlay />
64+
<ModalContent>
65+
<ModalHeader>Import state string</ModalHeader>
66+
<ModalCloseButton />
67+
<ModalBody pb={6}>
68+
<FormControl>
69+
<Textarea
70+
value={value}
71+
onChange={(e) => setValue(e.target.value)}
72+
ref={initialRef}
73+
placeholder="Paste the state string here..."
74+
/>
75+
</FormControl>
76+
</ModalBody>
77+
78+
<ModalFooter>
79+
<Button onClick={onImport} colorScheme="blue" mr={3}>
80+
Import
81+
</Button>
82+
<Button onClick={onClose}>Cancel</Button>
83+
</ModalFooter>
84+
</ModalContent>
85+
</Modal>
86+
</>
87+
);
88+
}
89+
90+
export default ControlConfigImport;

0 commit comments

Comments
 (0)