feat: add new UI component for multi option inputs (#20533)
* add multi option input support * fix import * add multi-key-field-support
This commit is contained in:
@@ -1027,4 +1027,4 @@ select:focus {
|
||||
|
||||
.prose :where(a:not([data-card])):not(:where([class~="not-prose"] *)) {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,11 @@
|
||||
import { RootProvider } from "fumadocs-ui/provider";
|
||||
import type { Metadata } from "next";
|
||||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
import { Inter } from "next/font/google";
|
||||
|
||||
import "./global.css";
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
const geistMono = Geist_Mono({
|
||||
variable: "--font-geist-mono",
|
||||
const inter = Inter({
|
||||
variable: "--font-inter",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
@@ -26,7 +21,14 @@ export default function RootLayout({
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>
|
||||
<head>
|
||||
<style>{`
|
||||
:root {
|
||||
--font-inter: ${inter.style.fontFamily.replace(/\'/g, "")};
|
||||
}
|
||||
`}</style>
|
||||
</head>
|
||||
<body className={`${inter.variable} antialiased`}>
|
||||
<RootProvider>{children}</RootProvider>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
---
|
||||
title: MultiOptionInput
|
||||
---
|
||||
import { MultiOptionInput } from "@calcom/ui/components/form"
|
||||
import { RenderComponentWithSnippet } from "@/app/components/render"
|
||||
import { Row } from "@/app/components/row"
|
||||
import { StatesExample } from "./multiInputField.states"
|
||||
import { PasteExample } from "./multiInputField.paste"
|
||||
import { CustomizationExample } from "./multiInputField.customization"
|
||||
|
||||
## Overview
|
||||
|
||||
The MultiOptionInput component is a flexible form input that allows users to manage a list of options with features like:
|
||||
- Adding and removing options
|
||||
- Reordering options with move up/down buttons
|
||||
- Pasting multiple options with configurable delimiters
|
||||
- Minimum options requirement
|
||||
- Customizable placeholders and labels
|
||||
- Key-value pair mode for environment variables, configuration settings, etc.
|
||||
|
||||
## States
|
||||
|
||||
MultiOptionInput in different states: default, disabled, and with minimum options.
|
||||
|
||||
<StatesExample/>
|
||||
|
||||
## Paste Support
|
||||
|
||||
Demonstrates the paste functionality with different delimiters. Users can paste comma-separated values, newline-separated values, or use custom delimiters.
|
||||
|
||||
In key-value mode, the component can also parse key-value pairs from pasted text. For example:
|
||||
- `KEY=value`
|
||||
- `NODE_ENV:production`
|
||||
|
||||
<PasteExample/>
|
||||
|
||||
## Customization
|
||||
|
||||
Examples of different customization options like placeholders, move buttons, remove buttons, and key-value pairs.
|
||||
|
||||
<CustomizationExample/>
|
||||
|
||||
## Usage
|
||||
|
||||
### Standard Options
|
||||
|
||||
```tsx
|
||||
import { MultiOptionInput } from "@calcom/ui/components/form/inputs/MultiOptionInput";
|
||||
import { useForm, FormProvider } from "react-hook-form";
|
||||
|
||||
type FormValues = {
|
||||
options: Array<{ label: string; id: string }>;
|
||||
};
|
||||
|
||||
const MyForm = () => {
|
||||
const methods = useForm<FormValues>();
|
||||
|
||||
return (
|
||||
<FormProvider {...methods}>
|
||||
<form>
|
||||
<MultiOptionInput<FormValues>
|
||||
fieldArrayName="options"
|
||||
optionPlaceholders={["Option 1", "Option 2"]}
|
||||
defaultNumberOfOptions={2}
|
||||
pasteDelimiters={["\n", ","]} // Optional
|
||||
showMoveButtons={true} // Optional
|
||||
minOptions={1} // Optional
|
||||
addOptionLabel="Add another option" // Optional
|
||||
/>
|
||||
</form>
|
||||
</FormProvider>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### Key-Value Pairs
|
||||
|
||||
```tsx
|
||||
import { MultiOptionInput } from "@calcom/ui/components/form/inputs/MultiOptionInput";
|
||||
import { useForm, FormProvider } from "react-hook-form";
|
||||
|
||||
type FormValues = {
|
||||
envVars: Array<{ label: string; value: string; id: string }>;
|
||||
};
|
||||
|
||||
const EnvVarsForm = () => {
|
||||
const methods = useForm<FormValues>();
|
||||
|
||||
return (
|
||||
<FormProvider {...methods}>
|
||||
<form>
|
||||
<MultiOptionInput<FormValues>
|
||||
fieldArrayName="envVars"
|
||||
keyValueMode={true}
|
||||
keyLabel="Environment Variable"
|
||||
valueLabel="Value"
|
||||
optionPlaceholders={["NODE_ENV", "PORT"]}
|
||||
valuePlaceholders={["production", "3000"]}
|
||||
keyValueDelimiters={[":", "="]} // Optional, defaults to [":", "="]
|
||||
defaultNumberOfOptions={2}
|
||||
/>
|
||||
</form>
|
||||
</FormProvider>
|
||||
);
|
||||
};
|
||||
```
|
||||
@@ -0,0 +1,71 @@
|
||||
"use client";
|
||||
|
||||
import { RenderComponentWithSnippet } from "@/app/components/render";
|
||||
import { useForm, FormProvider } from "react-hook-form";
|
||||
|
||||
import { MultiOptionInput } from "@calcom/ui/components/form";
|
||||
|
||||
type FormValues = {
|
||||
customPlaceholders: Array<{ label: string; id: string }>;
|
||||
noMoveButtons: Array<{ label: string; id: string }>;
|
||||
customLabel: Array<{ label: string; id: string }>;
|
||||
keyValuePairs: Array<{ label: string; value: string; id: string }>;
|
||||
};
|
||||
|
||||
export const CustomizationExample: React.FC = () => {
|
||||
const methods = useForm<FormValues>();
|
||||
|
||||
return (
|
||||
<RenderComponentWithSnippet>
|
||||
<FormProvider {...methods}>
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-4">
|
||||
<div className="flex flex-col space-y-2">
|
||||
<h3 className="text-emphasis text-sm">Key-Value Pairs</h3>
|
||||
<p className="text-subtle text-xs">Allows inputting keys and values</p>
|
||||
<MultiOptionInput<FormValues>
|
||||
fieldArrayName="keyValuePairs"
|
||||
keyValueMode
|
||||
keyLabel="Environment Variable"
|
||||
valueLabel="Value"
|
||||
optionPlaceholders={["NODE_ENV", "PORT", "DATABASE_URL"]}
|
||||
valuePlaceholders={["production", "3000", "postgres://..."]}
|
||||
defaultNumberOfOptions={3}
|
||||
keyValueDelimiters={[":", "="]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col space-y-2">
|
||||
<h3 className="text-emphasis text-sm">Custom Placeholders</h3>
|
||||
<MultiOptionInput<FormValues>
|
||||
fieldArrayName="customPlaceholders"
|
||||
optionPlaceholders={["Enter your name", "Enter your email", "Enter your phone"]}
|
||||
defaultNumberOfOptions={3}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col space-y-2">
|
||||
<h3 className="text-emphasis text-sm">Without Move Buttons</h3>
|
||||
<MultiOptionInput<FormValues>
|
||||
fieldArrayName="noMoveButtons"
|
||||
optionPlaceholders={["Static option 1", "Static option 2"]}
|
||||
defaultNumberOfOptions={2}
|
||||
showMoveButtons={false}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col space-y-2">
|
||||
<h3 className="text-emphasis text-sm">Custom Add Button Label</h3>
|
||||
<MultiOptionInput<FormValues>
|
||||
fieldArrayName="customLabel"
|
||||
optionPlaceholders={["Social media link"]}
|
||||
defaultNumberOfOptions={1}
|
||||
addOptionLabel="Add another social media link"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</FormProvider>
|
||||
</RenderComponentWithSnippet>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,76 @@
|
||||
"use client";
|
||||
|
||||
import { RenderComponentWithSnippet } from "@/app/components/render";
|
||||
import { useForm, FormProvider } from "react-hook-form";
|
||||
|
||||
import { MultiOptionInput } from "@calcom/ui/components/form";
|
||||
|
||||
type FormValues = {
|
||||
newlineOptions: Array<{ label: string; id: string }>;
|
||||
commaOptions: Array<{ label: string; id: string }>;
|
||||
customOptions: Array<{ label: string; id: string }>;
|
||||
keyValueOptions: Array<{ label: string; value: string; id: string }>;
|
||||
};
|
||||
|
||||
export const PasteExample: React.FC = () => {
|
||||
const methods = useForm<FormValues>();
|
||||
|
||||
return (
|
||||
<RenderComponentWithSnippet>
|
||||
<FormProvider {...methods}>
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-4">
|
||||
<div className="flex flex-col space-y-2">
|
||||
<h3 className="text-emphasis text-sm">Default Delimiters (Newline and Comma)</h3>
|
||||
<p className="text-subtle text-xs">
|
||||
Try pasting: “Option 1, Option 2” or multiple lines
|
||||
</p>
|
||||
<MultiOptionInput<FormValues>
|
||||
fieldArrayName="newlineOptions"
|
||||
optionPlaceholders={["Paste here..."]}
|
||||
defaultNumberOfOptions={1}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col space-y-2">
|
||||
<h3 className="text-emphasis text-sm">Comma Only Delimiter</h3>
|
||||
<p className="text-subtle text-xs">Try pasting: “First, Second, Third”</p>
|
||||
<MultiOptionInput<FormValues>
|
||||
fieldArrayName="commaOptions"
|
||||
optionPlaceholders={["Paste here..."]}
|
||||
defaultNumberOfOptions={1}
|
||||
pasteDelimiters={[","]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col space-y-2">
|
||||
<h3 className="text-emphasis text-sm">Key-Value Pair Paste Support</h3>
|
||||
<p className="text-subtle text-xs">
|
||||
Try pasting: “NODE_ENV=production” or “KEY1:value1, KEY2:value2”
|
||||
</p>
|
||||
<MultiOptionInput<FormValues>
|
||||
fieldArrayName="keyValueOptions"
|
||||
keyValueMode
|
||||
optionPlaceholders={["Key..."]}
|
||||
valuePlaceholders={["Value..."]}
|
||||
defaultNumberOfOptions={1}
|
||||
keyValueDelimiters={[":", "="]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col space-y-2">
|
||||
<h3 className="text-emphasis text-sm">Custom Delimiters (Semicolon and Pipe)</h3>
|
||||
<p className="text-subtle text-xs">Try pasting: “One;Two|Three”</p>
|
||||
<MultiOptionInput<FormValues>
|
||||
fieldArrayName="customOptions"
|
||||
optionPlaceholders={["Paste here..."]}
|
||||
defaultNumberOfOptions={1}
|
||||
pasteDelimiters={[";", "|"]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</FormProvider>
|
||||
</RenderComponentWithSnippet>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
"use client";
|
||||
|
||||
import { RenderComponentWithSnippet } from "@/app/components/render";
|
||||
import { useForm, FormProvider } from "react-hook-form";
|
||||
|
||||
import { MultiOptionInput } from "@calcom/ui/components/form";
|
||||
|
||||
type FormValues = {
|
||||
defaultOptions: Array<{ label: string; id: string }>;
|
||||
disabledOptions: Array<{ label: string; id: string }>;
|
||||
minOptions: Array<{ label: string; id: string }>;
|
||||
};
|
||||
|
||||
export const StatesExample: React.FC = () => {
|
||||
const methods = useForm<FormValues>();
|
||||
|
||||
return (
|
||||
<RenderComponentWithSnippet>
|
||||
<FormProvider {...methods}>
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-4">
|
||||
<div className="flex flex-col space-y-2">
|
||||
<h3 className="text-emphasis text-sm">Default State</h3>
|
||||
<MultiOptionInput<FormValues>
|
||||
fieldArrayName="defaultOptions"
|
||||
optionPlaceholders={["First option", "Second option", "Third option"]}
|
||||
defaultNumberOfOptions={3}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col space-y-2">
|
||||
<h3 className="text-emphasis text-sm">Disabled State</h3>
|
||||
<MultiOptionInput<FormValues>
|
||||
fieldArrayName="disabledOptions"
|
||||
optionPlaceholders={["Disabled option 1", "Disabled option 2"]}
|
||||
defaultNumberOfOptions={2}
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col space-y-2">
|
||||
<h3 className="text-emphasis text-sm">Minimum Options (2)</h3>
|
||||
<MultiOptionInput<FormValues>
|
||||
fieldArrayName="minOptions"
|
||||
optionPlaceholders={["Required option 1", "Required option 2", "Optional option"]}
|
||||
defaultNumberOfOptions={3}
|
||||
minOptions={2}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</FormProvider>
|
||||
</RenderComponentWithSnippet>
|
||||
);
|
||||
};
|
||||
@@ -14,6 +14,8 @@ export {
|
||||
FilterSearchField,
|
||||
} from "./inputs/Input";
|
||||
|
||||
export { MultiOptionInput } from "./inputs/MultiOptionInput";
|
||||
|
||||
export { InputFieldWithSelect } from "./inputs/InputFieldWithSelect";
|
||||
export type { InputFieldProps, InputProps } from "./inputs/types";
|
||||
export { InputField, Input, TextField, inputStyles } from "./inputs/TextField";
|
||||
|
||||
@@ -0,0 +1,312 @@
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import {
|
||||
useFieldArray,
|
||||
useFormContext,
|
||||
type FieldValues,
|
||||
type Path,
|
||||
type PathValue,
|
||||
type ArrayPath,
|
||||
} from "react-hook-form";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
import classNames from "@calcom/ui/classNames";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
import { TextField } from "@calcom/ui/components/form";
|
||||
import { Icon } from "@calcom/ui/components/icon";
|
||||
|
||||
export interface Option {
|
||||
label: string;
|
||||
value?: string;
|
||||
id?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
interface MultiOptionInputProps<TFieldValues extends FieldValues> {
|
||||
/**
|
||||
* The field array name in the form schema
|
||||
* @example "fields.0.options" or "questions"
|
||||
*/
|
||||
fieldArrayName: ArrayPath<TFieldValues>;
|
||||
/**
|
||||
* Whether to display as key-value pairs
|
||||
* @default false
|
||||
*/
|
||||
keyValueMode?: boolean;
|
||||
/**
|
||||
* Label for the key field in key-value mode
|
||||
* @default "Key"
|
||||
*/
|
||||
keyLabel?: string;
|
||||
/**
|
||||
* Label for the value field in key-value mode
|
||||
* @default "Value"
|
||||
*/
|
||||
valueLabel?: string;
|
||||
/**
|
||||
* Custom regex pattern to validate keys
|
||||
*/
|
||||
keyPattern?: string;
|
||||
addOptionLabel?: string;
|
||||
optionPlaceholders?: string[];
|
||||
/**
|
||||
* Placeholders for value fields in key-value mode
|
||||
*/
|
||||
valuePlaceholders?: string[];
|
||||
defaultNumberOfOptions?: number;
|
||||
/**
|
||||
* Delimiters to split pasted text on. Defaults to ["\n", ","]
|
||||
* For key-value pairs, will try to split on ":" or "=" between key and value
|
||||
*/
|
||||
pasteDelimiters?: string[];
|
||||
/**
|
||||
* Delimiters to split key-value pairs on. Defaults to [":", "="]
|
||||
*/
|
||||
keyValueDelimiters?: string[];
|
||||
/**
|
||||
* Whether to show move up/down buttons. Defaults to true
|
||||
*/
|
||||
showMoveButtons?: boolean;
|
||||
/**
|
||||
* Whether to show remove button. Defaults to true when there is more than 1 option
|
||||
*/
|
||||
showRemoveButton?: boolean;
|
||||
/**
|
||||
* Minimum number of options required. Defaults to 1
|
||||
*/
|
||||
minOptions?: number;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export const MultiOptionInput = <TFieldValues extends FieldValues>({
|
||||
fieldArrayName,
|
||||
keyValueMode = false,
|
||||
keyLabel = "Key",
|
||||
valueLabel = "Value",
|
||||
keyPattern,
|
||||
addOptionLabel = "Add an option",
|
||||
optionPlaceholders = ["Option 1", "Option 2", "Option 3", "Option 4"],
|
||||
valuePlaceholders = ["Value 1", "Value 2", "Value 3", "Value 4"],
|
||||
defaultNumberOfOptions = 4,
|
||||
pasteDelimiters = ["\n", ","],
|
||||
keyValueDelimiters = [":", "="],
|
||||
showMoveButtons = true,
|
||||
showRemoveButton: _showRemoveButton,
|
||||
minOptions = 1,
|
||||
disabled = false,
|
||||
}: MultiOptionInputProps<TFieldValues>) => {
|
||||
const [animationRef] = useAutoAnimate<HTMLUListElement>();
|
||||
const { control } = useFormContext<TFieldValues>();
|
||||
|
||||
const {
|
||||
fields: options,
|
||||
append: appendOption,
|
||||
remove: removeOption,
|
||||
move: moveOption,
|
||||
replace: replaceOptions,
|
||||
} = useFieldArray({
|
||||
control,
|
||||
name: fieldArrayName,
|
||||
});
|
||||
|
||||
// Initialize with default options if none exist
|
||||
if (options.length === 0) {
|
||||
const defaultOptions = Array(defaultNumberOfOptions)
|
||||
.fill(0)
|
||||
.map(() => ({ label: "", value: "", id: uuidv4() }));
|
||||
replaceOptions(defaultOptions as PathValue<TFieldValues, Path<TFieldValues>>);
|
||||
}
|
||||
|
||||
const showRemoveButton = _showRemoveButton ?? options.length > minOptions;
|
||||
|
||||
const handlePasteInOptionAtIndex = (event: React.ClipboardEvent, optionIndex: number) => {
|
||||
const paste = event.clipboardData.getData("text");
|
||||
// Split on any of the delimiters
|
||||
const delimiterRegex = new RegExp(`[${pasteDelimiters.join("")}]+`);
|
||||
const keyValueRegex = new RegExp(
|
||||
`([^${keyValueDelimiters.join("")}]+)([${keyValueDelimiters.join("")}])(.+)`
|
||||
);
|
||||
|
||||
const optionsBeingPasted = paste
|
||||
.split(delimiterRegex)
|
||||
.map((optionText) => optionText.trim())
|
||||
.filter((optionText) => optionText)
|
||||
.map((optionText) => {
|
||||
// If in key-value mode, try to parse as key-value
|
||||
if (keyValueMode) {
|
||||
const match = optionText.match(keyValueRegex);
|
||||
if (match) {
|
||||
return {
|
||||
label: match[1].trim(),
|
||||
value: match[3].trim(),
|
||||
id: uuidv4(),
|
||||
};
|
||||
}
|
||||
return { label: optionText, value: "", id: uuidv4() };
|
||||
}
|
||||
return { label: optionText, id: uuidv4() };
|
||||
});
|
||||
|
||||
if (optionsBeingPasted.length === 1) {
|
||||
// If there is only one option, let the default paste behavior handle it
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't allow pasting that value, as we would update the options through state update
|
||||
event.preventDefault();
|
||||
|
||||
// Replace the current option with the first pasted option
|
||||
const updatedOptions = [...options] as PathValue<TFieldValues, Path<TFieldValues>>[];
|
||||
updatedOptions[optionIndex] = optionsBeingPasted[0] as PathValue<TFieldValues, Path<TFieldValues>>;
|
||||
|
||||
// Insert the rest of the options after the current option
|
||||
updatedOptions.splice(
|
||||
optionIndex + 1,
|
||||
0,
|
||||
...(optionsBeingPasted.slice(1) as PathValue<TFieldValues, Path<TFieldValues>>[])
|
||||
);
|
||||
replaceOptions(updatedOptions as PathValue<TFieldValues, Path<TFieldValues>>);
|
||||
};
|
||||
|
||||
const addOption = () => {
|
||||
appendOption({ label: "", value: "", id: uuidv4() } as PathValue<TFieldValues, Path<TFieldValues>>);
|
||||
};
|
||||
|
||||
const handleRemoveOption = (index: number) => {
|
||||
if (options.length <= minOptions) return;
|
||||
removeOption(index);
|
||||
};
|
||||
|
||||
const moveUp = (index: number) => {
|
||||
if (index === 0) return;
|
||||
moveOption(index, index - 1);
|
||||
};
|
||||
|
||||
const moveDown = (index: number) => {
|
||||
if (index === options.length - 1) return;
|
||||
moveOption(index, index + 1);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
{keyValueMode && (
|
||||
<div className="mb-2 flex items-center px-2">
|
||||
<div className="flex-grow">
|
||||
<span className="text-subtle text-xs font-medium">{keyLabel}</span>
|
||||
</div>
|
||||
<div className="flex-grow">
|
||||
<span className="text-subtle text-xs font-medium">{valueLabel}</span>
|
||||
</div>
|
||||
{/* Space for buttons */}
|
||||
<div className="w-12" />
|
||||
</div>
|
||||
)}
|
||||
<ul ref={animationRef}>
|
||||
{options.map((option, index) => (
|
||||
<li
|
||||
key={option.id || `option-${index}`}
|
||||
className="group mt-2 flex items-center gap-2"
|
||||
onPaste={(event) => handlePasteInOptionAtIndex(event, index)}>
|
||||
{keyValueMode ? (
|
||||
// Key-value pair mode
|
||||
<div className="flex w-full gap-2">
|
||||
<div className="flex-grow">
|
||||
<TextField
|
||||
disabled={disabled}
|
||||
labelSrOnly
|
||||
placeholder={optionPlaceholders[index] ?? "Key"}
|
||||
pattern={keyPattern}
|
||||
type="text"
|
||||
required
|
||||
addOnClassname="bg-transparent border-0"
|
||||
{...control.register(`${fieldArrayName}.${index}.label` as Path<TFieldValues>)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-grow">
|
||||
<TextField
|
||||
disabled={disabled}
|
||||
labelSrOnly
|
||||
placeholder={valuePlaceholders[index] ?? "Value"}
|
||||
type="text"
|
||||
required
|
||||
addOnClassname="bg-transparent border-0"
|
||||
{...control.register(`${fieldArrayName}.${index}.value` as Path<TFieldValues>)}
|
||||
addOnSuffix={
|
||||
showRemoveButton ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleRemoveOption(index)}
|
||||
aria-label="Remove option"
|
||||
disabled={disabled}>
|
||||
<Icon name="x" className="h-4 w-4" />
|
||||
</button>
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
// Standard mode
|
||||
<div className="flex-grow">
|
||||
<TextField
|
||||
disabled={disabled}
|
||||
labelSrOnly
|
||||
placeholder={optionPlaceholders[index] ?? "New Option"}
|
||||
type="text"
|
||||
required
|
||||
addOnClassname="bg-transparent border-0"
|
||||
{...control.register(`${fieldArrayName}.${index}.label` as Path<TFieldValues>)}
|
||||
addOnSuffix={
|
||||
showRemoveButton ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleRemoveOption(index)}
|
||||
aria-label="Remove option"
|
||||
disabled={disabled}>
|
||||
<Icon name="x" className="h-4 w-4" />
|
||||
</button>
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{showMoveButtons && (
|
||||
<div className="flex flex-col">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => moveUp(index)}
|
||||
disabled={index === 0 || disabled}
|
||||
className={classNames(
|
||||
"bg-default text-muted hover:text-emphasis invisible flex h-6 w-6 items-center justify-center rounded-t-md border p-1 transition-all hover:border-transparent hover:shadow group-hover:visible",
|
||||
index === 0 ? "cursor-not-allowed opacity-30" : "cursor-pointer"
|
||||
)}>
|
||||
<Icon name="arrow-up" className="h-3 w-3" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => moveDown(index)}
|
||||
disabled={index === options.length - 1 || disabled}
|
||||
className={classNames(
|
||||
"bg-default text-muted hover:text-emphasis invisible -mt-px flex h-6 w-6 items-center justify-center rounded-b-md border p-1 transition-all hover:border-transparent hover:shadow group-hover:visible",
|
||||
index === options.length - 1 ? "cursor-not-allowed opacity-30" : "cursor-pointer"
|
||||
)}>
|
||||
<Icon name="arrow-down" className="h-3 w-3" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div className={classNames("flex")}>
|
||||
<Button
|
||||
type="button"
|
||||
color="secondary"
|
||||
StartIcon="plus"
|
||||
onClick={addOption}
|
||||
className="mt-4 border-none"
|
||||
disabled={disabled}>
|
||||
{addOptionLabel}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -82,6 +82,7 @@
|
||||
"@storybook/react": "^7.6.3",
|
||||
"@tanstack/react-query": "^5.17.15",
|
||||
"@tanstack/react-table": "^8.20.6",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"@wojtekmaj/react-daterange-picker": "^3.3.1",
|
||||
"class-variance-authority": "^0.4.0",
|
||||
"cmdk": "^0.2.0",
|
||||
@@ -96,7 +97,8 @@
|
||||
"react-hook-form": "^7.43.3",
|
||||
"react-inlinesvg": "^4.1.3",
|
||||
"react-select": "^5.7.0",
|
||||
"tailwind-merge": "^1.13.2"
|
||||
"tailwind-merge": "^1.13.2",
|
||||
"uuid": "^11.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@calcom/config": "*",
|
||||
|
||||
@@ -3723,6 +3723,7 @@ __metadata:
|
||||
"@testing-library/user-event": ^14.6.1
|
||||
"@types/react": 18.0.26
|
||||
"@types/react-dom": ^18.0.9
|
||||
"@types/uuid": ^10.0.0
|
||||
"@wojtekmaj/react-daterange-picker": ^3.3.1
|
||||
class-variance-authority: ^0.4.0
|
||||
cmdk: ^0.2.0
|
||||
@@ -3744,6 +3745,7 @@ __metadata:
|
||||
react-select: ^5.7.0
|
||||
tailwind-merge: ^1.13.2
|
||||
typescript: ^5.8.2
|
||||
uuid: ^11.1.0
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
@@ -17604,6 +17606,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/uuid@npm:^10.0.0":
|
||||
version: 10.0.0
|
||||
resolution: "@types/uuid@npm:10.0.0"
|
||||
checksum: e3958f8b0fe551c86c14431f5940c3470127293280830684154b91dc7eb3514aeb79fe3216968833cf79d4d1c67f580f054b5be2cd562bebf4f728913e73e944
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/uuid@npm:^9.0.1":
|
||||
version: 9.0.8
|
||||
resolution: "@types/uuid@npm:9.0.8"
|
||||
@@ -45694,7 +45703,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"uuid@npm:^11.0.5":
|
||||
"uuid@npm:^11.0.5, uuid@npm:^11.1.0":
|
||||
version: 11.1.0
|
||||
resolution: "uuid@npm:11.1.0"
|
||||
bin:
|
||||
|
||||
Reference in New Issue
Block a user