diff --git a/packages/zudoku/src/lib/components/MultiSelect.tsx b/packages/zudoku/src/lib/components/MultiSelect.tsx new file mode 100644 index 00000000..ce8ebc47 --- /dev/null +++ b/packages/zudoku/src/lib/components/MultiSelect.tsx @@ -0,0 +1,131 @@ +import * as SelectPrimitive from "@radix-ui/react-select"; +import { Check, ChevronDown } from "lucide-react"; +import * as React from "react"; +import { cn } from "../util/cn.js"; + +import { + SelectContent, + SelectGroup, + SelectScrollDownButton, + SelectScrollUpButton, +} from "./Select.js"; + +type MultiSelectProps = { + options: string[]; + value?: string; // this value will only be used to clear the selected items + onValueChange?: (value: string[]) => void; + placeholder?: string; + className?: string; +}; + +const MultiSelectTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + selectedItems: string[]; + placeholder?: string; + } +>( + ( + { className, selectedItems, placeholder = "Select options", ...props }, + ref, + ) => ( + + + {selectedItems.length > 0 ? selectedItems.join(", ") : placeholder} + + + + + + ), +); +MultiSelectTrigger.displayName = SelectPrimitive.Trigger.displayName; + +const MultiSelectItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + value: string; + selectedItems: string[]; + } +>(({ className, children, value, selectedItems, ...props }, ref) => ( + + + {selectedItems.includes(value) && } + + {children} + +)); +MultiSelectItem.displayName = SelectPrimitive.Item.displayName; + +const MultiSelect: React.FC = ({ + options, + value, + onValueChange, + placeholder, + className, +}) => { + const [selectedItems, setSelectedItems] = React.useState([]); + + React.useEffect(() => { + if (value === "") setSelectedItems([]); + }, [value]); + + const handleValueChange = (newValue: string) => { + if (newValue === "") return; // Prevent adding empty string + + const updatedValues = selectedItems.includes(newValue) + ? selectedItems.filter((item) => item !== newValue) // Remove if selected + : [...selectedItems, newValue]; // Add if not selected + + setSelectedItems(updatedValues); + if (onValueChange) { + onValueChange(updatedValues); + } + }; + + return ( + + + + + + + {options.map((option) => ( + + {option} + + ))} + + + + + + ); +}; + +export default MultiSelect; diff --git a/packages/zudoku/src/lib/components/Select.tsx b/packages/zudoku/src/lib/components/Select.tsx index 01031f77..0202c58f 100644 --- a/packages/zudoku/src/lib/components/Select.tsx +++ b/packages/zudoku/src/lib/components/Select.tsx @@ -11,8 +11,10 @@ const SelectValue = SelectPrimitive.Value; const SelectTrigger = React.forwardRef< React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, children, ...props }, ref) => ( + React.ComponentPropsWithoutRef & { + placeholder?: string; + } +>(({ className, children, placeholder, ...props }, ref) => ( + {!props.value && placeholder && ( + {placeholder} + )} {children} diff --git a/packages/zudoku/src/lib/plugins/openapi/PlaygroundDialogWrapper.tsx b/packages/zudoku/src/lib/plugins/openapi/PlaygroundDialogWrapper.tsx index 21305b79..0d1d0f16 100644 --- a/packages/zudoku/src/lib/plugins/openapi/PlaygroundDialogWrapper.tsx +++ b/packages/zudoku/src/lib/plugins/openapi/PlaygroundDialogWrapper.tsx @@ -23,6 +23,8 @@ export const PlaygroundDialogWrapper = ({ name: p.name, defaultActive: p.required ?? false, isRequired: p.required ?? false, + enum: p.schema?.type == "array" ? p.schema?.items?.enum : p.schema?.enum, + type: p.schema?.type ?? "string", })); const pathParams = operation.parameters ?.filter((p) => p.in === "path") diff --git a/packages/zudoku/src/lib/plugins/openapi/playground/Playground.tsx b/packages/zudoku/src/lib/plugins/openapi/playground/Playground.tsx index 21ab9501..2dd73ebe 100644 --- a/packages/zudoku/src/lib/plugins/openapi/playground/Playground.tsx +++ b/packages/zudoku/src/lib/plugins/openapi/playground/Playground.tsx @@ -46,6 +46,8 @@ export type QueryParam = { defaultValue?: string; defaultActive?: boolean; isRequired?: boolean; + enum?: string[]; + type?: string; }; export type PathParam = { name: string; @@ -55,7 +57,12 @@ export type PathParam = { export type PlaygroundForm = { body: string; - queryParams: Array<{ name: string; value: string; active: boolean }>; + queryParams: Array<{ + name: string; + value: string | string[]; + active: boolean; + enum?: string[]; + }>; pathParams: Array<{ name: string; value: string }>; headers: Array<{ name: string; value: string }>; identity?: string; @@ -92,6 +99,7 @@ export const Playground = ({ name: param.name, value: param.defaultValue ?? "", active: param.defaultActive ?? false, + enum: param.enum ?? [], })), pathParams: pathParams.map((param) => ({ name: param.name, @@ -198,13 +206,23 @@ export const Playground = ({ const urlQueryParams = formState.queryParams .filter((p) => p.active) - .map((p, i, arr) => ( - - {p.name}={encodeURIComponent(p.value).replaceAll("%20", "+")} - {i < arr.length - 1 && "&"} - - - )); + .flatMap((p, i, arr) => + Array.isArray(p.value) ? ( + p.value.map((v, vi) => ( + + {p.name}={encodeURIComponent(v)} + {(vi < p.value.length - 1 || i < arr.length - 1) && "&"} + + + )) + ) : ( + + {p.name}={encodeURIComponent(p.value)} + {i < arr.length - 1 && "&"} + + + ), + ); const serverSelect = (
diff --git a/packages/zudoku/src/lib/plugins/openapi/playground/QueryParams.tsx b/packages/zudoku/src/lib/plugins/openapi/playground/QueryParams.tsx index a2949212..4007c748 100644 --- a/packages/zudoku/src/lib/plugins/openapi/playground/QueryParams.tsx +++ b/packages/zudoku/src/lib/plugins/openapi/playground/QueryParams.tsx @@ -5,6 +5,14 @@ import { useFieldArray, useFormContext, } from "react-hook-form"; +import MultiSelect from "../../../components/MultiSelect.js"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "../../../components/Select.js"; import { Button } from "../../../ui/Button.js"; import { Input } from "../../../ui/Input.js"; import { cn } from "../../../util/cn.js"; @@ -82,19 +90,67 @@ export const QueryParams = ({
( - { - field.onChange(e.target.value); - if (e.target.value.length > 0) { - form.setValue(`queryParams.${i}.active`, true); - } - }} - placeholder="Enter value" - className="w-full border-0 shadow-none text-xs font-mono" - /> - )} + render={({ field }) => { + const hasEnum = + queryParams[i].enum && queryParams[i].enum.length > 0; + const typeOfField = queryParams[i].type; + return hasEnum ? ( + typeOfField === "array" ? ( + { + if ( + Array.isArray(value) && + value.length === 0 + ) { + field.onChange(""); + } else { + field.onChange(value); + } + }} + placeholder="Choose options" + value={ + Array.isArray(field.value) + ? field.value.join(",") + : field.value + } + /> + ) : ( + + ) + ) : ( + { + field.onChange(e.target.value); + if (e.target.value.length > 0) { + form.setValue(`queryParams.${i}.active`, true); + } + }} + placeholder="Enter value" + className="w-full border-0 shadow-none text-xs font-mono" + /> + ); + }} name={`queryParams.${i}.value`} /> { if (!param.value) { return; } - url.searchParams.set(param.name, param.value); + + if (Array.isArray(param.value)) { + // If the parameter is an array then create multiple query params with the same name by comma separating the values + param.value.forEach((value) => { + url.searchParams.append(param.name, value); + }); + } else { + url.searchParams.set(param.name, param.value); + } }); return ( diff --git a/packages/zudoku/src/lib/plugins/openapi/playground/createUrl.ts b/packages/zudoku/src/lib/plugins/openapi/playground/createUrl.ts index 009c284f..87c5a5ce 100644 --- a/packages/zudoku/src/lib/plugins/openapi/playground/createUrl.ts +++ b/packages/zudoku/src/lib/plugins/openapi/playground/createUrl.ts @@ -18,7 +18,14 @@ export const createUrl = (host: string, path: string, data: PlaygroundForm) => { data.queryParams .filter((param) => param.active) .forEach((param) => { - url.searchParams.set(param.name, param.value); + if (Array.isArray(param.value)) { + // If the parameter is an array then create multiple query params with the same name by comma separating the values (repeating query param name) + param.value.forEach((value) => { + url.searchParams.append(param.name, value); + }); + } else { + url.searchParams.set(param.name, param.value); + } }); return url;