feat(destinations): enhance additionalFlags handling in destination settings

- Refactored the `additionalFlags` field to use a structured object format, allowing for better validation and management of flag values.
- Replaced the textarea input with a dynamic list of input fields, enabling users to add or remove flags easily.
- Updated form handling to accommodate the new structure, ensuring proper data mapping during form submission.
This commit is contained in:
Mauricio Siu
2026-03-29 08:43:16 -06:00
parent 4bbb2ece49
commit 82a908a865

View File

@@ -1,7 +1,7 @@
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { PenBoxIcon, PlusIcon } from "lucide-react";
import { PenBoxIcon, PlusIcon, Trash2 } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { useFieldArray, useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { AlertBlock } from "@/components/shared/alert-block";
@@ -24,7 +24,6 @@ import {
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import {
Select,
SelectContent,
@@ -47,7 +46,9 @@ const addDestination = z.object({
region: z.string(),
endpoint: z.string().min(1, "Endpoint is required"),
serverId: z.string().optional(),
additionalFlags: z.array(z.string()).optional(),
additionalFlags: z
.array(z.object({ value: z.string().min(1, "Flag cannot be empty") }))
.optional(),
});
type AddDestination = z.infer<typeof addDestination>;
@@ -95,6 +96,12 @@ export const HandleDestinations = ({ destinationId }: Props) => {
},
resolver: zodResolver(addDestination),
});
const { fields, append, remove } = useFieldArray({
control: form.control,
name: "additionalFlags",
});
useEffect(() => {
if (destination) {
form.reset({
@@ -105,7 +112,8 @@ export const HandleDestinations = ({ destinationId }: Props) => {
bucket: destination.bucket,
region: destination.region,
endpoint: destination.endpoint,
additionalFlags: destination.additionalFlags ?? [],
additionalFlags:
destination.additionalFlags?.map((f) => ({ value: f })) ?? [],
});
} else {
form.reset();
@@ -122,7 +130,8 @@ export const HandleDestinations = ({ destinationId }: Props) => {
region: data.region,
secretAccessKey: data.secretAccessKey,
destinationId: destinationId || "",
additionalFlags: data.additionalFlags ?? [],
additionalFlags:
data.additionalFlags?.map((f) => f.value) ?? [],
})
.then(async () => {
toast.success(`Destination ${destinationId ? "Updated" : "Created"}`);
@@ -184,7 +193,8 @@ export const HandleDestinations = ({ destinationId }: Props) => {
region,
secretAccessKey: secretKey,
serverId,
additionalFlags: form.getValues("additionalFlags") ?? [],
additionalFlags:
form.getValues("additionalFlags")?.map((f) => f.value) ?? [],
})
.then(() => {
toast.success("Connection Success");
@@ -364,33 +374,48 @@ export const HandleDestinations = ({ destinationId }: Props) => {
</FormItem>
)}
/>
<FormField
control={form.control}
name="additionalFlags"
render={({ field }) => (
<FormItem>
<FormLabel>Additional Flags (Optional)</FormLabel>
<FormControl>
<Textarea
placeholder={
"--s3-sign-accept-encoding=false\n--s3-chunk-size=50M"
}
className="h-20"
value={(field.value || []).join("\n")}
onChange={(e) =>
field.onChange(
e.target.value
.split("\n")
.map((f) => f.trim())
.filter((f) => f.length > 0),
)
}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="flex flex-col gap-2">
<div className="flex items-center justify-between">
<FormLabel>Additional Flags (Optional)</FormLabel>
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => append({ value: "" })}
>
<PlusIcon className="size-4" />
Add Flag
</Button>
</div>
{fields.map((field, index) => (
<FormField
key={field.id}
control={form.control}
name={`additionalFlags.${index}.value`}
render={({ field }) => (
<FormItem>
<div className="flex items-center gap-2">
<FormControl>
<Input
placeholder="--s3-sign-accept-encoding=false"
{...field}
/>
</FormControl>
<Button
type="button"
variant="ghost"
size="icon"
onClick={() => remove(index)}
>
<Trash2 className="size-4 text-muted-foreground" />
</Button>
</div>
<FormMessage />
</FormItem>
)}
/>
))}
</div>
</form>
<DialogFooter