Skip to content

Commit 547eb29

Browse files
committed
feat: JSON Formatter tool addition
1 parent 7e42ce1 commit 547eb29

File tree

16 files changed

+563
-18
lines changed

16 files changed

+563
-18
lines changed

README.md

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -54,30 +54,26 @@ Devbox is a lightweight, cross-platform desktop application built with Tauri (Ru
5454
- [x] Backslash Escape/Unescape
5555
- [x] DNS Lookup Tool
5656
- [x] Certificate Decoder X.509
57+
- [x] JSON Formatter
5758
- [ ] Diff tools
5859
- [ ] CSS Playground
59-
- [ ] Scratch Pad (code)
6060
- [ ] Color Testing
6161
- [ ] Quick Type
6262
- [ ] Password Generator
6363
- [ ] Stateless password
64-
- [ ] QR Code Generator
6564
- [ ] Lorem Ipsum
6665
- [ ] Harmonies
6766
- [ ] Faker
6867
- [ ] HTML Formatter
6968
- [ ] CSS Formatter
7069
- [ ] JS/TS Formatter
71-
- [ ] JSON Formatter
7270
- [ ] SQL Formatter
7371
- [ ] HTML Preview
74-
- [ ] PDF View
7572
- [ ] Base64 Text (encode/decode)
7673
- [ ] Color Utils
7774
- [ ] JSON <> YAML
7875
- [ ] Hashing Text
7976
- [ ] Hasing Files
80-
- [ ] Notes
8177
- [ ] Timezone
8278
- [ ] WebSocket Client
8379
- [ ] Mock API Server / Webhook test

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
"graphql": "^16.11.0",
5454
"graphql-ws": "^6.0.6",
5555
"jose": "^5.2.3",
56+
"json-edit-react": "^1.28.2",
5657
"mermaid": "^10.8.0",
5758
"monaco-themes": "^0.4.4",
5859
"nanoid": "^5.0.9",

src-tauri/tauri.conf.json

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@
77
"digestAlgorithm": "sha256",
88
"timestampUrl": ""
99
},
10-
"icon": ["icons/icon.png", "icons/icon.icns"],
10+
"icon": [
11+
"icons/icon.png",
12+
"icons/icon.icns"
13+
],
1114
"resources": [],
1215
"externalBin": [],
1316
"copyright": "",
@@ -34,7 +37,7 @@
3437
"frontendDist": "../dist",
3538
"beforeDevCommand": "yarn dev",
3639
"beforeBuildCommand": "yarn build",
37-
"devUrl": "http://localhost:3000"
40+
"devUrl": "http://localhost:3001"
3841
},
3942
"productName": "Devbox",
4043
"mainBinaryName": "devbox",
@@ -43,17 +46,23 @@
4346
"plugins": {
4447
"updater": {
4548
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEE1RDRCNkNFQTVBRERBMTQKUldRVTJxMmx6cmJVcFR3UWtxdnZWdVlCL3RRVUFwOE9ReDB3cDc3VGd4NjVJOGtzQnJZZDlUU24K",
46-
"endpoints": ["https://github.com/smithg09/devbox/releases/latest/download/latest.json"]
49+
"endpoints": [
50+
"https://github.com/smithg09/devbox/releases/latest/download/latest.json"
51+
]
4752
}
4853
},
4954
"app": {
5055
"macOSPrivateApi": true,
5156
"security": {
5257
"assetProtocol": {
5358
"enable": true,
54-
"scope": ["http://asset.localhost"]
59+
"scope": [
60+
"http://asset.localhost"
61+
]
5562
},
56-
"dangerousDisableAssetCspModification": ["style-src"],
63+
"dangerousDisableAssetCspModification": [
64+
"style-src"
65+
],
5766
"csp": {
5867
"connect-src": [
5968
"ipc:",
@@ -78,11 +87,21 @@
7887
"tauri:",
7988
"asset:"
8089
],
81-
"worker-src": ["'self'", "blob:", "https://unpkg.com"],
82-
"script-src": ["'self'", "'unsafe-inline'"],
83-
"style-src": ["'self'", "'unsafe-inline'"]
90+
"worker-src": [
91+
"'self'",
92+
"blob:",
93+
"https://unpkg.com"
94+
],
95+
"script-src": [
96+
"'self'",
97+
"'unsafe-inline'"
98+
],
99+
"style-src": [
100+
"'self'",
101+
"'unsafe-inline'"
102+
]
84103
}
85104
},
86105
"windows": []
87106
}
88-
}
107+
}

src/Components/AppRoutes.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ const componentMap = {
2626
"url-parser": loadable(() => import("@/Features/url/UrlParser")) as React.ComponentType,
2727
"url-encoder": loadable(() => import("@/Features/url/UrlEncoder")) as React.ComponentType,
2828
"certificate-decoder": loadable(() => import("../Features/x509/X509")) as React.ComponentType,
29+
"json-formatter": loadable(() => import("../Features/json/JsonFormatter")) as React.ComponentType,
2930
};
3031
// Dynamically create lazy-loaded components
3132
const routes = tools

src/Components/CopyButton.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
import { Button, CopyButton as DefaultCopyButton, MantineSize, Tooltip } from "@mantine/core";
1+
import {
2+
Button,
3+
ButtonProps,
4+
CopyButton as DefaultCopyButton,
5+
MantineSize,
6+
Tooltip,
7+
} from "@mantine/core";
28
import {} from "@tauri-apps/api";
39
import * as clipboard from "@tauri-apps/plugin-clipboard-manager";
410
import { BsCheck2, BsCopy } from "react-icons/bs";
@@ -9,14 +15,15 @@ type CopyProps = {
915
size?: MantineSize;
1016
fullWidth?: boolean;
1117
variant?: "filled" | "light" | "subtle";
12-
};
18+
} & ButtonProps;
1319

1420
export function CopyButton({
1521
value,
1622
label,
1723
size,
1824
fullWidth = true,
1925
variant = "filled",
26+
...rest
2027
}: CopyProps) {
2128
return (
2229
<DefaultCopyButton value={value.toString()} timeout={2400}>
@@ -31,6 +38,7 @@ export function CopyButton({
3138
copy();
3239
clipboard.writeText(value.toString());
3340
}}
41+
{...rest}
3442
>
3543
{copied ? "Copied" : label}
3644
</Button>
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import { CopyButton } from "@/Components/CopyButton";
2+
import { Alert, Box, Group, Stack, Tabs, Text } from "@mantine/core";
3+
import { useRef, useState } from "react";
4+
import {
5+
ImperativePanelHandle,
6+
Panel,
7+
PanelGroup,
8+
PanelResizeHandle,
9+
} from "react-resizable-panels";
10+
import { JsonInput } from "./components/Editors/JsonInput";
11+
import styles from "./components/styles.module.css";
12+
import { Toolbar } from "./components/Toolbar";
13+
import { JsonText } from "./components/Viewers/JsonText";
14+
import { JsonTree } from "./components/Viewers/JsonTree";
15+
import { useJsonFormatter } from "./hooks/useJsonFormatter";
16+
17+
const SAMPLE_JSON = `{
18+
"name": "DevBox",
19+
"version": "1.0.0",
20+
"features": [
21+
{ "id": 1, "name": "JSON Formatter", "enabled": true },
22+
{ "id": 2, "name": "Regex Tester", "enabled": true }
23+
],
24+
"meta": { "createdAt": "2024-01-01T00:00:00Z" }
25+
}`;
26+
27+
export default function JsonFormatter() {
28+
const {
29+
state: { rawInput, parsed, error, autoFormat },
30+
setRawInput,
31+
setAutoFormat,
32+
outputText,
33+
handleFormat,
34+
handleMinify,
35+
} = useJsonFormatter(SAMPLE_JSON);
36+
const [collapsedDepth, setCollapsedDepth] = useState<number | boolean>(2);
37+
const [activeTab, setActiveTab] = useState<string>("tree");
38+
const leftPanelRef = useRef<ImperativePanelHandle>(null);
39+
40+
const onExpandAll = () => setCollapsedDepth(false);
41+
const onCollapseAll = () => setCollapsedDepth(1);
42+
43+
return (
44+
<Stack className={`overflow-padding ${styles.containerStack}`} gap="sm">
45+
<Toolbar
46+
autoFormat={autoFormat}
47+
setAutoFormat={setAutoFormat}
48+
onFormat={() => {
49+
if (handleFormat()) setActiveTab("tree");
50+
}}
51+
onMinify={() => {
52+
if (handleMinify()) setActiveTab("text");
53+
}}
54+
onExpandAll={onExpandAll}
55+
onCollapseAll={onCollapseAll}
56+
collapsedDepth={collapsedDepth}
57+
setCollapsedDepth={v => setCollapsedDepth(v)}
58+
/>
59+
60+
{error && (
61+
<Alert color="red" title="Invalid JSON" variant="light">
62+
<Text fz="xs">
63+
{error.line && error.column
64+
? `Error at line ${error.line}, column ${error.column}: ${error.message}`
65+
: error.message}
66+
</Text>
67+
</Alert>
68+
)}
69+
70+
<Group className={styles.groupMain} grow gap={12} align="stretch">
71+
<PanelGroup direction="horizontal">
72+
<Panel ref={leftPanelRef}>
73+
<Box className={styles.panelBox}>
74+
<Stack gap={6} h="100%" style={{ flex: 1, minHeight: 0 }}>
75+
<JsonInput value={rawInput} onChange={setRawInput} />
76+
</Stack>
77+
</Box>
78+
</Panel>
79+
<PanelResizeHandle
80+
className={styles.resizeHandle}
81+
onDoubleClick={() => {
82+
leftPanelRef.current?.resize(50);
83+
}}
84+
>
85+
<Box className={styles.resizeHandler} />
86+
</PanelResizeHandle>
87+
<Panel>
88+
<Box className={styles.panelBox}>
89+
<Tabs
90+
value={activeTab}
91+
onChange={value => setActiveTab(value || "tree")}
92+
className={styles.tabsRoot}
93+
bg="#191919"
94+
>
95+
<Tabs.List>
96+
<Tabs.Tab value="tree">Tree</Tabs.Tab>
97+
<Tabs.Tab value="text">Text</Tabs.Tab>
98+
<CopyButton
99+
value={outputText}
100+
variant="subtle"
101+
size="xs"
102+
fullWidth={false}
103+
label="Copy"
104+
m={6}
105+
ml="auto"
106+
/>
107+
</Tabs.List>
108+
<Tabs.Panel value="tree" className={styles.panelTree}>
109+
<JsonTree
110+
value={parsed}
111+
collapsedDepth={collapsedDepth}
112+
onChange={next => {
113+
try {
114+
setRawInput(JSON.stringify(next, null, 2));
115+
} catch (e) {
116+
// ignore stringify failure
117+
}
118+
}}
119+
/>
120+
</Tabs.Panel>
121+
<Tabs.Panel value="text" className={styles.panelText}>
122+
<JsonText value={outputText} />
123+
</Tabs.Panel>
124+
</Tabs>
125+
</Box>
126+
</Panel>
127+
</PanelGroup>
128+
</Group>
129+
</Stack>
130+
);
131+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { MonacoEditor } from "@/Components/Monaco/Editor";
2+
import { Box } from "@mantine/core";
3+
import styles from "../styles.module.css";
4+
5+
type Props = {
6+
value: string;
7+
onChange: (v: string) => void;
8+
};
9+
10+
export function JsonInput({ value, onChange }: Props) {
11+
return (
12+
<>
13+
<Box className={styles.inputBox}>
14+
<MonacoEditor
15+
language="json"
16+
value={value}
17+
setValue={v => onChange(v ?? "")}
18+
height="100%"
19+
onEditorMounted={editor => {
20+
setTimeout(() => editor.layout(), 0);
21+
}}
22+
/>
23+
</Box>
24+
</>
25+
);
26+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { ActionIcon, Button, Group, NumberInput, Switch, Text, Tooltip } from "@mantine/core";
2+
import { BsArrowsExpand, BsBraces, BsCodeSlash, BsScissors } from "react-icons/bs";
3+
4+
type Props = {
5+
autoFormat: boolean;
6+
setAutoFormat: (v: boolean) => void;
7+
onFormat: () => void;
8+
onMinify: () => void;
9+
onExpandAll: () => void;
10+
onCollapseAll: () => void;
11+
collapsedDepth: number | boolean;
12+
setCollapsedDepth: (v: number) => void;
13+
};
14+
15+
export function Toolbar({
16+
autoFormat,
17+
setAutoFormat,
18+
onFormat,
19+
onMinify,
20+
onExpandAll,
21+
onCollapseAll,
22+
collapsedDepth,
23+
setCollapsedDepth,
24+
}: Props) {
25+
return (
26+
<Group justify="space-between" wrap="nowrap">
27+
<Group gap="xs" wrap="nowrap">
28+
<Button
29+
size="xs"
30+
variant="light"
31+
leftSection={<BsCodeSlash size={12} />}
32+
onClick={onFormat}
33+
>
34+
Format
35+
</Button>
36+
<Button size="xs" variant="light" leftSection={<BsScissors size={12} />} onClick={onMinify}>
37+
Minify
38+
</Button>
39+
<Switch
40+
size="xs"
41+
checked={autoFormat}
42+
onChange={e => setAutoFormat(e.currentTarget.checked)}
43+
label="Auto-format on paste"
44+
/>
45+
</Group>
46+
<Group gap="xs" wrap="nowrap">
47+
<Group gap={6} wrap="nowrap">
48+
<Text size="xs" c="dimmed">
49+
Depth
50+
</Text>
51+
<NumberInput
52+
size="xs"
53+
min={0}
54+
max={8}
55+
value={typeof collapsedDepth === "number" ? collapsedDepth : 0}
56+
onChange={val =>
57+
setCollapsedDepth(typeof val === "number" && !Number.isNaN(val) ? val : 0)
58+
}
59+
w={50}
60+
/>
61+
<Tooltip label="Expand all">
62+
<ActionIcon size="md" variant="subtle" onClick={onExpandAll}>
63+
<BsArrowsExpand size={16} />
64+
</ActionIcon>
65+
</Tooltip>
66+
<Tooltip label="Collapse all">
67+
<ActionIcon size="md" variant="subtle" onClick={onCollapseAll}>
68+
<BsBraces size={18} />
69+
</ActionIcon>
70+
</Tooltip>
71+
</Group>
72+
</Group>
73+
</Group>
74+
);
75+
}

0 commit comments

Comments
 (0)