Skip to content

Commit 091ede2

Browse files
committed
Feat: Copy the jsonjoy-builder source code to the project
1 parent 6c3b4e2 commit 091ede2

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+7124
-215
lines changed

web/package-lock.json

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

web/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@
6161
"@uiw/react-markdown-preview": "^5.1.3",
6262
"@xyflow/react": "^12.3.6",
6363
"ahooks": "^3.7.10",
64+
"ajv": "^8.17.1",
65+
"ajv-formats": "^3.0.1",
6466
"antd": "^5.12.7",
6567
"axios": "^1.12.0",
6668
"class-variance-authority": "^0.7.0",
Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
import { Badge } from '@/components/ui/badge';
2+
import { Button } from '@/components/ui/button';
3+
import {
4+
Dialog,
5+
DialogContent,
6+
DialogDescription,
7+
DialogFooter,
8+
DialogHeader,
9+
DialogTitle,
10+
} from '@/components/ui/dialog';
11+
import { Input } from '@/components/ui/input';
12+
import {
13+
Tooltip,
14+
TooltipContent,
15+
TooltipProvider,
16+
TooltipTrigger,
17+
} from '@/components/ui/tooltip';
18+
import { CirclePlus, HelpCircle, Info } from 'lucide-react';
19+
import { useId, useState, type FC, type FormEvent } from 'react';
20+
import { useTranslation } from '../../hooks/use-translation';
21+
import type { NewField, SchemaType } from '../../types/jsonSchema';
22+
import SchemaTypeSelector from './SchemaTypeSelector';
23+
24+
interface AddFieldButtonProps {
25+
onAddField: (field: NewField) => void;
26+
variant?: 'primary' | 'secondary';
27+
}
28+
29+
const AddFieldButton: FC<AddFieldButtonProps> = ({
30+
onAddField,
31+
variant = 'primary',
32+
}) => {
33+
const [dialogOpen, setDialogOpen] = useState(false);
34+
const [fieldName, setFieldName] = useState('');
35+
const [fieldType, setFieldType] = useState<SchemaType>('string');
36+
const [fieldDesc, setFieldDesc] = useState('');
37+
const [fieldRequired, setFieldRequired] = useState(false);
38+
const fieldNameId = useId();
39+
const fieldDescId = useId();
40+
const fieldRequiredId = useId();
41+
const fieldTypeId = useId();
42+
43+
const t = useTranslation();
44+
45+
const handleSubmit = (e: FormEvent) => {
46+
e.preventDefault();
47+
if (!fieldName.trim()) return;
48+
49+
onAddField({
50+
name: fieldName,
51+
type: fieldType,
52+
description: fieldDesc,
53+
required: fieldRequired,
54+
});
55+
56+
setFieldName('');
57+
setFieldType('string');
58+
setFieldDesc('');
59+
setFieldRequired(false);
60+
setDialogOpen(false);
61+
};
62+
63+
return (
64+
<>
65+
<Button
66+
type="button"
67+
onClick={() => setDialogOpen(true)}
68+
variant={variant === 'primary' ? 'default' : 'outline'}
69+
size="sm"
70+
className="flex items-center gap-1.5 group"
71+
>
72+
<CirclePlus
73+
size={16}
74+
className="group-hover:scale-110 transition-transform"
75+
/>
76+
<span>{t.fieldAddNewButton}</span>
77+
</Button>
78+
79+
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
80+
<DialogContent className="md:max-w-[1200px] max-h-[85vh] w-[95vw] p-4 sm:p-6 jsonjoy">
81+
<DialogHeader className="mb-4">
82+
<DialogTitle className="text-xl flex flex-wrap items-center gap-2">
83+
{t.fieldAddNewLabel}
84+
<Badge variant="secondary" className="text-xs">
85+
{t.fieldAddNewBadge}
86+
</Badge>
87+
</DialogTitle>
88+
<DialogDescription className="text-sm">
89+
{t.fieldAddNewDescription}
90+
</DialogDescription>
91+
</DialogHeader>
92+
93+
<form onSubmit={handleSubmit} className="space-y-6">
94+
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
95+
<div className="space-y-4 min-w-[280px]">
96+
<div>
97+
<div className="flex flex-wrap items-center gap-2 mb-1.5">
98+
<label
99+
htmlFor={fieldNameId}
100+
className="text-sm font-medium"
101+
>
102+
{t.fieldNameLabel}
103+
</label>
104+
<TooltipProvider>
105+
<Tooltip>
106+
<TooltipTrigger asChild>
107+
<Info className="h-4 w-4 text-muted-foreground shrink-0" />
108+
</TooltipTrigger>
109+
<TooltipContent className="max-w-[90vw]">
110+
<p>{t.fieldNameTooltip}</p>
111+
</TooltipContent>
112+
</Tooltip>
113+
</TooltipProvider>
114+
</div>
115+
<Input
116+
id={fieldNameId}
117+
value={fieldName}
118+
onChange={(e) => setFieldName(e.target.value)}
119+
placeholder={t.fieldNamePlaceholder}
120+
className="font-mono text-sm w-full"
121+
required
122+
/>
123+
</div>
124+
125+
<div>
126+
<div className="flex flex-wrap items-center gap-2 mb-1.5">
127+
<label
128+
htmlFor={fieldDescId}
129+
className="text-sm font-medium"
130+
>
131+
{t.fieldDescription}
132+
</label>
133+
<TooltipProvider>
134+
<Tooltip>
135+
<TooltipTrigger asChild>
136+
<Info className="h-4 w-4 text-muted-foreground shrink-0" />
137+
</TooltipTrigger>
138+
<TooltipContent className="max-w-[90vw]">
139+
<p>{t.fieldDescriptionTooltip}</p>
140+
</TooltipContent>
141+
</Tooltip>
142+
</TooltipProvider>
143+
</div>
144+
<Input
145+
id={fieldDescId}
146+
value={fieldDesc}
147+
onChange={(e) => setFieldDesc(e.target.value)}
148+
placeholder={t.fieldDescriptionPlaceholder}
149+
className="text-sm w-full"
150+
/>
151+
</div>
152+
153+
<div className="flex items-center gap-3 p-3 rounded-lg border bg-muted/50">
154+
<input
155+
type="checkbox"
156+
id={fieldRequiredId}
157+
checked={fieldRequired}
158+
onChange={(e) => setFieldRequired(e.target.checked)}
159+
className="rounded border-gray-300 shrink-0"
160+
/>
161+
<label htmlFor={fieldRequiredId} className="text-sm">
162+
{t.fieldRequiredLabel}
163+
</label>
164+
</div>
165+
</div>
166+
167+
<div className="space-y-4 min-w-[280px]">
168+
<div>
169+
<div className="flex flex-wrap items-center gap-2 mb-1.5">
170+
<label
171+
htmlFor={fieldTypeId}
172+
className="text-sm font-medium"
173+
>
174+
{t.fieldType}
175+
</label>
176+
<TooltipProvider>
177+
<Tooltip>
178+
<TooltipTrigger asChild>
179+
<HelpCircle className="h-4 w-4 text-muted-foreground shrink-0" />
180+
</TooltipTrigger>
181+
<TooltipContent
182+
side="left"
183+
className="w-72 max-w-[90vw]"
184+
>
185+
<div className="grid grid-cols-2 gap-x-4 gap-y-1 text-xs">
186+
<div>{t.fieldTypeTooltipString}</div>
187+
<div>{t.fieldTypeTooltipNumber}</div>
188+
<div>{t.fieldTypeTooltipBoolean}</div>
189+
<div>{t.fieldTypeTooltipObject}</div>
190+
<div className="col-span-2">
191+
{t.fieldTypeTooltipArray}
192+
</div>
193+
</div>
194+
</TooltipContent>
195+
</Tooltip>
196+
</TooltipProvider>
197+
</div>
198+
<SchemaTypeSelector
199+
id={fieldTypeId}
200+
value={fieldType}
201+
onChange={setFieldType}
202+
/>
203+
</div>
204+
205+
<div className="rounded-lg border bg-muted/50 p-3 hidden md:block">
206+
<p className="text-xs font-medium mb-2">
207+
{t.fieldTypeExample}
208+
</p>
209+
<code className="text-sm bg-background/80 p-2 rounded block overflow-x-auto">
210+
{fieldType === 'string' && '"example"'}
211+
{fieldType === 'number' && '42'}
212+
{fieldType === 'boolean' && 'true'}
213+
{fieldType === 'object' && '{ "key": "value" }'}
214+
{fieldType === 'array' && '["item1", "item2"]'}
215+
</code>
216+
</div>
217+
</div>
218+
</div>
219+
220+
<DialogFooter className="mt-6 gap-2 flex-wrap">
221+
<Button
222+
type="button"
223+
variant="outline"
224+
size="sm"
225+
onClick={() => setDialogOpen(false)}
226+
>
227+
{t.fieldAddNewCancel}
228+
</Button>
229+
<Button type="submit" size="sm">
230+
{t.fieldAddNewConfirm}
231+
</Button>
232+
</DialogFooter>
233+
</form>
234+
</DialogContent>
235+
</Dialog>
236+
</>
237+
);
238+
};
239+
240+
export default AddFieldButton;

0 commit comments

Comments
 (0)