diff --git a/dashboard/e2e/database/create-table.test.ts b/dashboard/e2e/database/create-table.test.ts index 8209ca61b..6631cc9ba 100644 --- a/dashboard/e2e/database/create-table.test.ts +++ b/dashboard/e2e/database/create-table.test.ts @@ -102,7 +102,7 @@ test('should create a table with nullable columns', async ({ page.getByRole('link', { name: tableName, exact: true }), ).toBeVisible(); await page - .locator(`li:has-text("${tableName}") #table-management-menu button`) + .locator(`li:has-text("${tableName}") #table-management-menu-${tableName}`) .click(); await page.getByText('Edit Table').click(); await expect(page.locator('h2:has-text("Edit Table")')).toBeVisible(); @@ -143,7 +143,7 @@ test('should create a table with an identity column', async ({ page.getByRole('link', { name: tableName, exact: true }), ).toBeVisible(); await page - .locator(`li:has-text("${tableName}") #table-management-menu button`) + .locator(`li:has-text("${tableName}") #table-management-menu-${tableName}`) .click(); await page.getByText('Edit Table').click(); await expect(page.locator('h2:has-text("Edit Table")')).toBeVisible(); @@ -267,7 +267,7 @@ test('should be able to create a table with a composite key', async ({ ).toBeVisible(); await page - .locator(`li:has-text("${tableName}") #table-management-menu button`) + .locator(`li:has-text("${tableName}") #table-management-menu-${tableName}`) .click(); await page.getByText('Edit Table').click(); await expect(page.locator('div[data-testid="id"]')).toBeVisible(); diff --git a/dashboard/e2e/database/permissions-table.test.ts b/dashboard/e2e/database/permissions-table.test.ts index 87a98598e..81d6951b2 100644 --- a/dashboard/e2e/database/permissions-table.test.ts +++ b/dashboard/e2e/database/permissions-table.test.ts @@ -41,7 +41,7 @@ test('should create a table with role permissions to select row', async ({ // Press three horizontal dots more options button next to the table name await page - .locator(`li:has-text("${tableName}") #table-management-menu button`) + .locator(`li:has-text("${tableName}") #table-management-menu-${tableName}`) .click(); await page.getByRole('menuitem', { name: /edit permissions/i }).click(); @@ -89,7 +89,7 @@ test('should create a table with role permissions and a custom check to select r // Press three horizontal dots more options button next to the table name await page - .locator(`li:has-text("${tableName}") #table-management-menu button`) + .locator(`li:has-text("${tableName}") #table-management-menu-${tableName}`) .click(); await page.getByRole('menuitem', { name: /edit permissions/i }).click(); @@ -114,7 +114,7 @@ test('should create a table with role permissions and a custom check to select r await page.getByText('Select variable...', { exact: true }).click(); - const variableSelector = await page.locator('input[role="combobox"]'); + const variableSelector = page.locator('input[role="combobox"]'); await variableSelector.fill('X-Hasura-User-Id'); diff --git a/dashboard/react-table-config.d.ts b/dashboard/react-table-config.d.ts index 7403b86fd..112ffa598 100644 --- a/dashboard/react-table-config.d.ts +++ b/dashboard/react-table-config.d.ts @@ -64,7 +64,6 @@ declare module 'react-table' { export interface Cell< D extends Record = Record, - V = any, > extends UseGroupByCellProps, UseRowStateCellProps {} diff --git a/dashboard/src/components/common/TimePicker/TimePickerInput.tsx b/dashboard/src/components/common/TimePicker/TimePickerInput.tsx index 3438f3667..0263460dc 100644 --- a/dashboard/src/components/common/TimePicker/TimePickerInput.tsx +++ b/dashboard/src/components/common/TimePicker/TimePickerInput.tsx @@ -123,7 +123,7 @@ const TimePickerInput = React.forwardRef< id={id || picker} name={name || picker} className={cn( - 'w-[48px] text-center font-mono text-base tabular-nums focus:bg-accent focus:text-accent-foreground [&::-webkit-inner-spin-button]:appearance-none', + 'w-[48px] text-center font-mono text-base tabular-nums focus:bg-accent-background focus:text-accent-foreground [&::-webkit-inner-spin-button]:appearance-none', className, )} value={value || calculatedValue} diff --git a/dashboard/src/components/form/FormActivityIndicator/FormActivityIndicator.tsx b/dashboard/src/components/form/FormActivityIndicator/FormActivityIndicator.tsx index 324fe2259..a5a1ad714 100644 --- a/dashboard/src/components/form/FormActivityIndicator/FormActivityIndicator.tsx +++ b/dashboard/src/components/form/FormActivityIndicator/FormActivityIndicator.tsx @@ -1,26 +1,25 @@ -import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator'; -import type { BoxProps } from '@/components/ui/v2/Box'; -import { Box } from '@/components/ui/v2/Box'; -import { twMerge } from 'tailwind-merge'; +import { Spinner } from '@/components/ui/v3/spinner'; +import { cn } from '@/lib/utils'; -export interface FormActivityIndicatorProps extends BoxProps {} +export interface FormActivityIndicatorProps { + className?: string; +} export default function FormActivityIndicator({ className, ...props }: FormActivityIndicatorProps) { return ( - - - + + Loading form... + + ); } diff --git a/dashboard/src/components/form/FormInput/FormInput.tsx b/dashboard/src/components/form/FormInput/FormInput.tsx index 1f39f102b..5efe6dc2c 100644 --- a/dashboard/src/components/form/FormInput/FormInput.tsx +++ b/dashboard/src/components/form/FormInput/FormInput.tsx @@ -1,12 +1,16 @@ import { FormControl, + FormDescription, FormField, FormItem, FormLabel, FormMessage, } from '@/components/ui/v3/form'; import { Input } from '@/components/ui/v3/input'; +import { cn } from '@/lib/utils'; +import { type ForwardedRef, forwardRef, type ReactNode } from 'react'; import type { Control, FieldPath, FieldValues } from 'react-hook-form'; +import { mergeRefs } from 'react-merge-refs'; const inputClasses = '!bg-transparent aria-[invalid=true]:border-red-500 aria-[invalid=true]:focus:border-red-500 aria-[invalid=true]:focus:ring-red-500'; @@ -17,43 +21,82 @@ interface FormInputProps< > { control: Control; name: TName; - label: string; + label: ReactNode; placeholder?: string; className?: string; type?: string; + inline?: boolean; + helperText?: string | null; } -function FormInput< +function InnerFormInput< TFieldValues extends FieldValues = FieldValues, TName extends FieldPath = FieldPath, ->({ - control, - name, - label, - placeholder, - className = '', - type = 'text', -}: FormInputProps) { +>( + { + control, + name, + label, + placeholder, + className = '', + type = 'text', + inline, + helperText, + }: FormInputProps, + ref: ForwardedRef, +) { return ( ( - - {label} - - - - + + + {label} + +
+ + + + {!!helperText && ( + + {helperText} + + )} + +
)} /> ); } +const FormInput = forwardRef(InnerFormInput) as < + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath, +>( + props: FormInputProps & { + ref?: ForwardedRef; + }, +) => ReturnType; + export default FormInput; diff --git a/dashboard/src/components/form/FormSelect/FormSelect.tsx b/dashboard/src/components/form/FormSelect/FormSelect.tsx new file mode 100644 index 000000000..9da4242bf --- /dev/null +++ b/dashboard/src/components/form/FormSelect/FormSelect.tsx @@ -0,0 +1,91 @@ +import { + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@/components/ui/v3/form'; +import { + Select, + SelectContent, + SelectTrigger, + SelectValue, +} from '@/components/ui/v3/select'; +import { cn } from '@/lib/utils'; +import type { PropsWithChildren, ReactNode } from 'react'; +import type { Control, FieldPath, FieldValues } from 'react-hook-form'; + +interface FormSelectProps< + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath, +> { + control: Control; + name: TName; + label: ReactNode; + placeholder?: string; + className?: string; + inline?: boolean; + helperText?: string | null; +} + +function FormSelect< + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath, +>({ + control, + name, + label, + placeholder, + className = '', + inline, + helperText, + children, +}: PropsWithChildren>) { + return ( + { + const { onChange, ...selectProps } = field; + return ( + + + {label} + +
+ + {!!helperText && ( + + {helperText} + + )} + +
+
+ ); + }} + /> + ); +} + +export default FormSelect; diff --git a/dashboard/src/components/form/FormSelect/index.ts b/dashboard/src/components/form/FormSelect/index.ts new file mode 100644 index 000000000..cfedc469a --- /dev/null +++ b/dashboard/src/components/form/FormSelect/index.ts @@ -0,0 +1 @@ +export { default as FormSelect } from './FormSelect'; diff --git a/dashboard/src/components/form/FormTextarea/FormTextarea.tsx b/dashboard/src/components/form/FormTextarea/FormTextarea.tsx new file mode 100644 index 000000000..bd93c0952 --- /dev/null +++ b/dashboard/src/components/form/FormTextarea/FormTextarea.tsx @@ -0,0 +1,98 @@ +import { + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@/components/ui/v3/form'; +import { Textarea } from '@/components/ui/v3/textarea'; +import { cn } from '@/lib/utils'; +import { forwardRef, type ForwardedRef, type ReactNode } from 'react'; +import type { Control, FieldPath, FieldValues } from 'react-hook-form'; +import { mergeRefs } from 'react-merge-refs'; + +const inputClasses = + '!bg-transparent aria-[invalid=true]:border-red-500 aria-[invalid=true]:focus:border-red-500 aria-[invalid=true]:focus:ring-red-500'; + +interface FormTextareaProps< + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath, +> { + control: Control; + name: TName; + label: ReactNode; + placeholder?: string; + className?: string; + inline?: boolean; + helperText?: string | null; +} + +function InnerFormTextarea< + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath, +>( + { + control, + name, + label, + placeholder, + className = '', + inline, + helperText, + }: FormTextareaProps, + ref: ForwardedRef, +) { + return ( + ( + + + {label} + +
+ +