Skip to content

Commit f460fb0

Browse files
committed
feat: Dialog for managing radio and checkbox options
1 parent b893a09 commit f460fb0

6 files changed

Lines changed: 244 additions & 125 deletions

File tree

‎src/components/form-builder/form-components/form-checkbox-group.tsx‎

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ export function FormCheckboxGroup(
2626
form: UseFormReturn<FieldValues, undefined>,
2727
field: ControllerRenderProps
2828
) {
29+
const oneOptionHasLabelDescription = component.options?.some((option) => option.labelDescription);
30+
2931
return (
3032
<div className={cn("flex flex-col gap-2")}>
3133
{component.options?.map((option) => (
@@ -35,7 +37,7 @@ export function FormCheckboxGroup(
3537
control={form.control}
3638
render={({ field: OptionField }) => {
3739
return (
38-
<FormItem key={option.value} className="flex items-start gap-3">
40+
<FormItem key={option.value} className="flex items-start gap-2">
3941
<FormControl>
4042
<Checkbox
4143
checked={OptionField.value?.includes(option.value)}
@@ -53,9 +55,16 @@ export function FormCheckboxGroup(
5355
}}
5456
/>
5557
</FormControl>
56-
<FormLabel className="text-sm leading-tight font-normal">
57-
{option.label}
58-
</FormLabel>
58+
<div className="grid gap-2 leading-none">
59+
<FormLabel className={cn("text-sm leading-tight font-normal", oneOptionHasLabelDescription && "font-medium")}>
60+
{option.label}
61+
</FormLabel>
62+
{option.labelDescription && (
63+
<p className="text-sm text-muted-foreground">
64+
{option.labelDescription}
65+
</p>
66+
)}
67+
</div>
5968
</FormItem>
6069
);
6170
}}
@@ -71,6 +80,8 @@ type ReactCode = {
7180
};
7281

7382
export function getReactCode(component: FormComponentModel): ReactCode {
83+
const oneOptionHasLabelDescription = component.options?.some((option) => option.labelDescription);
84+
7485
return {
7586
code: `
7687
<div className="flex flex-col gap-2">
@@ -100,9 +111,12 @@ export function getReactCode(component: FormComponentModel): ReactCode {
100111
}}
101112
/>
102113
</FormControl>
103-
<FormLabel className="text-sm leading-tight font-normal">
104-
${option.label}
105-
</FormLabel>
114+
<div className="grid gap-2 leading-none">
115+
<FormLabel className="${oneOptionHasLabelDescription ? "font-medium" : "font-normal"}">
116+
${option.label}
117+
</FormLabel>
118+
${option.labelDescription ? `<p className="text-sm text-muted-foreground">${escapeHtml(option.labelDescription)}</p>` : ""}
119+
</div>
106120
</FormItem>
107121
);
108122
}}

‎src/components/form-builder/form-components/form-radio.tsx‎

Lines changed: 47 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,22 @@ import { LabelGroup } from "../sidebar/groups/label-group";
88
import { InputGroup } from "../sidebar/groups/input-group";
99
import { OptionsGroup } from "../sidebar/groups/options-group";
1010
import { cn, escapeHtml } from "@/lib/utils";
11-
import { ControllerRenderProps, FieldValues, UseFormReturn } from "react-hook-form";
11+
import {
12+
ControllerRenderProps,
13+
FieldValues,
14+
UseFormReturn,
15+
} from "react-hook-form";
1216
import { ValidationGroup } from "../sidebar/groups/validation-group";
1317
import { FormLabel } from "@/components/ui/form";
1418

15-
export function FormRadio(component: FormComponentModel, form: UseFormReturn<FieldValues, undefined>, field: ControllerRenderProps) {
19+
export function FormRadio(
20+
component: FormComponentModel,
21+
form: UseFormReturn<FieldValues, undefined>,
22+
field: ControllerRenderProps
23+
) {
24+
25+
const oneOptionHasLabelDescription = component.options?.some((option) => option.labelDescription);
26+
1627
return (
1728
<RadioGroup
1829
key={component.id}
@@ -22,14 +33,24 @@ export function FormRadio(component: FormComponentModel, form: UseFormReturn<Fie
2233
onValueChange={field.onChange}
2334
>
2435
{component.options?.map((option) => (
25-
<div key={option.value} className="flex items-center space-x-2">
36+
<div key={option.value} className="flex items-start space-x-2">
2637
<RadioGroupItem
2738
value={option.value}
2839
id={`${component.getField("attributes.id")}-${option.value}`}
2940
/>
30-
<FormLabel htmlFor={`${component.getField("attributes.id")}-${option.value}`} className="font-normal">
31-
{option.label}
32-
</FormLabel>
41+
<div className="grid gap-2 leading-none">
42+
<FormLabel
43+
htmlFor={`${component.getField("attributes.id")}-${option.value}`}
44+
className={cn("font-normal", oneOptionHasLabelDescription && "font-medium")}
45+
>
46+
{option.label}
47+
</FormLabel>
48+
{option.labelDescription && (
49+
<p className="text-sm text-muted-foreground">
50+
{option.labelDescription}
51+
</p>
52+
)}
53+
</div>
3354
</div>
3455
))}
3556
</RadioGroup>
@@ -42,6 +63,8 @@ type ReactCode = {
4263
};
4364

4465
export function getReactCode(component: FormComponentModel): ReactCode {
66+
const oneOptionHasLabelDescription = component.options?.some((option) => option.labelDescription);
67+
4568
return {
4669
code: `
4770
<RadioGroup
@@ -51,20 +74,26 @@ export function getReactCode(component: FormComponentModel): ReactCode {
5174
{...field}
5275
onValueChange={field.onChange}
5376
>
54-
${component.options?.map((option) => `
77+
${component.options
78+
?.map(
79+
(option) => `
5580
<div key="${escapeHtml(option.value)}" className="flex items-center space-x-2">
5681
<RadioGroupItem value="${escapeHtml(option.value)}" id="${escapeHtml(component.getField("attributes.id"))}-${escapeHtml(option.value)}" />
57-
<FormLabel htmlFor="${escapeHtml(component.getField("attributes.id"))}-${escapeHtml(option.value)}">
58-
${escapeHtml(option.label)}
59-
</FormLabel>
82+
<div className="grid gap-2 leading-none">
83+
<FormLabel htmlFor="${escapeHtml(component.getField("attributes.id"))}-${escapeHtml(option.value)}" className="${oneOptionHasLabelDescription ? "font-medium" : "font-normal"}">
84+
${escapeHtml(option.label)}
85+
</FormLabel>
86+
${option.labelDescription ? `<p className="text-sm text-muted-foreground">${escapeHtml(option.labelDescription)}</p>` : ""}
87+
</div>
6088
</div>
61-
`).join("\n")}
89+
`
90+
)
91+
.join("\n")}
6292
</RadioGroup>
6393
`,
6494
dependencies: {
65-
"@/components/ui/radio-group": ["RadioGroup", "RadioGroupItem"],
95+
"@/components/ui/radio-group": ["RadioGroup", "RadioGroupItem"],
6696
"@/components/ui/form": ["FormLabel"],
67-
6897
},
6998
};
7099
}
@@ -73,7 +102,11 @@ export const RadioDesignProperties: DesignPropertiesViews = {
73102
base: null,
74103
grid: <GridGroup />,
75104
html: <HtmlGroup />,
76-
label: <LabelGroup whitelist={["label", "labelPosition", "labelAlign", "showLabel"]} />,
105+
label: (
106+
<LabelGroup
107+
whitelist={["label", "labelPosition", "labelAlign", "showLabel"]}
108+
/>
109+
),
77110
input: <InputGroup whitelist={["placeholder", "description", "value"]} />,
78111
options: <OptionsGroup />,
79112
button: null,
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
import {
2+
Dialog,
3+
DialogContent,
4+
DialogFooter,
5+
DialogHeader,
6+
DialogTitle,
7+
DialogTrigger,
8+
} from "@/components/ui/dialog";
9+
import { Input } from "@/components/ui/input";
10+
import { Checkbox } from "@/components/ui/checkbox";
11+
import { Button } from "@/components/ui/button";
12+
import {
13+
PlusIcon,
14+
Trash2,
15+
Pipette,
16+
Save,
17+
SaveIcon,
18+
ListPlusIcon,
19+
} from "lucide-react";
20+
import { useEffect, useState } from "react";
21+
import { FormComponentModelInput } from "@/types/FormComponent.types";
22+
23+
interface Option {
24+
label: string;
25+
labelDescription: string;
26+
value: string;
27+
checked: boolean;
28+
}
29+
30+
interface OptionsDialogProps {
31+
options: FormComponentModelInput["options"];
32+
onOptionsChange: (options: FormComponentModelInput["options"]) => void;
33+
showCheckbox?: boolean;
34+
}
35+
36+
export function OptionsDialog({
37+
options = [],
38+
onOptionsChange,
39+
showCheckbox = false,
40+
}: OptionsDialogProps) {
41+
const [open, setOpen] = useState(false);
42+
const [loadedOptions, setLoadedOptions] = useState<FormComponentModelInput["options"]>([]);
43+
44+
45+
useEffect(() => {
46+
setLoadedOptions(options);
47+
console.log("options", options);
48+
}, [options]);
49+
50+
const handleOptionChange = (
51+
index: number,
52+
field: keyof Option,
53+
value: any
54+
) => {
55+
const newOptions = loadedOptions ? [
56+
...loadedOptions.slice(0, index),
57+
{ ...loadedOptions[index], [field]: value },
58+
...loadedOptions.slice(index + 1),
59+
] : [];
60+
61+
setLoadedOptions(newOptions);
62+
};
63+
64+
const handleAddOption = () => {
65+
setLoadedOptions(loadedOptions ? [
66+
...loadedOptions,
67+
{
68+
label: `Option ${loadedOptions.length + 1}`,
69+
labelDescription: "",
70+
value: `option-${loadedOptions.length + 1}`,
71+
checked: false,
72+
},
73+
] : []);
74+
};
75+
76+
const handleDeleteOption = (index: number) => {
77+
setLoadedOptions(loadedOptions ? loadedOptions.filter((_, i) => i !== index) : []);
78+
};
79+
80+
const onSaveOptions = () => {
81+
onOptionsChange(loadedOptions);
82+
setOpen(false);
83+
};
84+
85+
return (
86+
<Dialog open={open} onOpenChange={setOpen}>
87+
<DialogTrigger asChild>
88+
<Button variant="outline" size="sm" className="w-full">
89+
<ListPlusIcon className="mr-2 h-4 w-4" />
90+
Manage Options
91+
</Button>
92+
</DialogTrigger>
93+
<DialogContent className="sm:max-w-3xl">
94+
<DialogHeader>
95+
<DialogTitle>Manage Options</DialogTitle>
96+
</DialogHeader>
97+
<div className="flex flex-col gap-4 py-4">
98+
<div className="grid grid-cols-12 text-sm text-gray-400 gap-4">
99+
{showCheckbox && <span className="col-span-1"></span>}
100+
<span className={`col-span-${showCheckbox ? 3 : 4}`}>Label</span>
101+
<span className="col-span-4">Label Description</span>
102+
<span className="col-span-3">Value</span>
103+
<span className="col-span-1"></span>
104+
</div>
105+
{options.map((option, index) => (
106+
<div key={index} className="grid grid-cols-12 gap-4 items-center justify-items-center">
107+
{showCheckbox && (
108+
<Checkbox
109+
id={`option-${index}-checked`}
110+
checked={option.checked}
111+
onCheckedChange={(checked) =>
112+
handleOptionChange(index, "checked", checked)
113+
}
114+
className="col-span-1"
115+
/>
116+
)}
117+
<Input
118+
defaultValue={option.label}
119+
onChange={(e) =>
120+
handleOptionChange(index, "label", e.target.value)
121+
}
122+
placeholder="Option label"
123+
className={`col-span-${showCheckbox ? 3 : 4}`}
124+
/>
125+
<Input
126+
defaultValue={option.labelDescription}
127+
onChange={(e) =>
128+
handleOptionChange(index, "labelDescription", e.target.value)
129+
}
130+
placeholder="Label description"
131+
className="col-span-4"
132+
/>
133+
<Input
134+
defaultValue={option.value}
135+
onChange={(e) =>
136+
handleOptionChange(index, "value", e.target.value)
137+
}
138+
placeholder="Option value"
139+
className="col-span-3"
140+
/>
141+
<Trash2
142+
onClick={() => handleDeleteOption(index)}
143+
className="h-4 w-4 text-muted-foreground hover:text-destructive cursor-pointer"
144+
/>
145+
</div>
146+
))}
147+
<hr className="w-full" />
148+
<Button
149+
variant="outline"
150+
className="w-fit"
151+
onClick={handleAddOption}
152+
>
153+
<PlusIcon className=" h-4 w-4" />
154+
Add Option
155+
</Button>
156+
157+
</div>
158+
<DialogFooter>
159+
<Button onClick={onSaveOptions} variant="default">
160+
<SaveIcon className="h-4 w-4" /> Save Options
161+
</Button>
162+
</DialogFooter>
163+
</DialogContent>
164+
</Dialog>
165+
);
166+
}

0 commit comments

Comments
 (0)