-
- {table.schema_name}.{table.table_name}
-
-
- Columns:{' '}
- {table.columns.map((column: any) => column.name).join(', ')}
+
+
+
+
+
+
+
+ {wrapperMeta.label} Configuration
+
+
+
+
+ {wrapperMeta.server.options
+ .filter((option) => !option.hidden)
+ .map((option) => (
+
+
+
+ ))}
+
+
+
+
+
+
+ Data target
+
+
+
+ (
+
+
+
+
+
+
+
+ Create foreign tables to query data from {wrapperMeta.label}.
-
- }
- onClick={() => {
- setIsEditingTable(true)
- setSelectedTableToEdit({ ...table, tableIndex: i })
- }}
- />
- }
- onClick={() => {
- setNewTables((prev) => prev.filter((_, j) => j !== i))
- }}
- />
+
+ {wrapperMeta.tables.length === 0 ? (
+
+
+
+ This wrapper doesn't support using foreign tables.
+
+
+ ) : null}
+
+
+
+
+
+ Create all foreign tables from {wrapperMeta.label} in a
+ specified schema.
+
- ))}
+ {wrapperMeta.canTargetSchema ? (
+ hasRequiredVersionForeignSchema ? null : (
+
+
+
+ This feature requires the{' '}
+ wrappers extension to be
+ of minimum version of 0.5.0.
+
+
+ )
+ ) : (
+
+
+
+ This wrapper doesn't support using a foreign schema.
+
+
+ )}
+
+
+
+
+ )}
+ />
+
+
+ {mode === 'tables' && (
+
+
+
+ Foreign Tables
+
+ You can query your data from these foreign tables after the wrapper is
+ created
+
+
+
+
+ {tablesField.map((t, tableIndex) => {
+ // FIXME: make inference work
+ const table = t as unknown as FormattedWrapperTable
+ return (
+
+
+
+ {table.schema_name}.{table.table_name}
+
+
+ Columns: {table.columns.map((column: any) => column.name).join(', ')}
+
- )}
- {newTables.length > 0 && (
-
-
+
+ }
+ onClick={() => {
+ setSelectedTableToEdit(table)
+ }}
+ />
+ }
+ onClick={() => {
+ removeTable(tableIndex)
+ }}
+ />
- )}
- {newTables.length === 0 && formErrors.tables && (
-
{formErrors.tables}
- )}
-
-
- )}
- {selectedMode === 'schema' && (
-
- Foreign Schema
-
- You can query your data from the foreign tables in the specified schema
- after the wrapper is created.
-
-
- }
- >
-
- {wrapperMeta.sourceSchemaOption &&
- !wrapperMeta.sourceSchemaOption?.readOnly && (
- // Hide the field if the source schema is read-only
-
-
-
- {wrapperMeta.sourceSchemaOption.description}
-
-
- )}
-
-
-
- A new schema will be created. For security purposes, the wrapper tables
- from the foreign schema cannot be created within an existing schema.
-
-
-
- )}
-
+ )
+ })}
-
-
-
-
- >
- )
- }}
-
+
+
+
+ {tablesField.length === 0 && errors.tables && (
+
+ {errors.tables.message?.toString()}
+
+ )}
+
+
+ )}
+ {mode === 'schema' && (
+
+
+
+ Foreign Schema
+
+ You can query your data from the foreign tables in the specified schema
+ after the wrapper is created.
+
+
+
+
+ {wrapperMeta.sourceSchemaOption &&
+ !wrapperMeta.sourceSchemaOption?.readOnly && (
+ // Hide the field if the source schema is read-only
+
+ )}
+
+
+
+
+
+ )}
+
+
+
+
+
+
+
{
setSelectedTableToEdit(undefined)
- setIsEditingTable(false)
}}
onSave={onUpdateTable}
initialData={selectedTableToEdit}
diff --git a/apps/studio/components/interfaces/Integrations/Wrappers/EditWrapperSheet.tsx b/apps/studio/components/interfaces/Integrations/Wrappers/EditWrapperSheet.tsx
index f9bb04d32d1..91da61224fc 100644
--- a/apps/studio/components/interfaces/Integrations/Wrappers/EditWrapperSheet.tsx
+++ b/apps/studio/components/interfaces/Integrations/Wrappers/EditWrapperSheet.tsx
@@ -1,10 +1,34 @@
+import { zodResolver } from '@hookform/resolvers/zod'
import { useQueryClient } from '@tanstack/react-query'
-import { compact, isEmpty, mapValues } from 'lodash'
+import { compact } from 'lodash'
import { Edit, Trash } from 'lucide-react'
-import { useCallback, useEffect, useRef, useState } from 'react'
+import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
+import { SubmitHandler, useFieldArray, useForm, useWatch } from 'react-hook-form'
import { toast } from 'sonner'
-import { Button, Form, Input, SheetFooter, SheetHeader, SheetTitle } from 'ui'
+import {
+ Button,
+ Card,
+ CardContent,
+ Form_Shadcn_,
+ FormControl_Shadcn_,
+ FormField_Shadcn_,
+ Input_Shadcn_,
+ SheetFooter,
+ SheetHeader,
+ SheetSection,
+ SheetTitle,
+} from 'ui'
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
+import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
+import {
+ PageSection,
+ PageSectionContent,
+ PageSectionDescription,
+ PageSectionMeta,
+ PageSectionSummary,
+ PageSectionTitle,
+} from 'ui-patterns/PageSection'
+import * as z from 'zod'
import InputField from './InputField'
import { WrapperMeta } from './Wrappers.types'
@@ -12,15 +36,11 @@ import {
convertKVStringArrayToJson,
FormattedWrapperTable,
formatWrapperTables,
- makeValidateRequired,
+ getEditionFormSchema,
+ NewTable,
} from './Wrappers.utils'
import WrapperTableEditor from './WrapperTableEditor'
import { DiscardChangesConfirmationDialog } from '@/components/ui-patterns/Dialogs/DiscardChangesConfirmationDialog'
-import {
- FormSection,
- FormSectionContent,
- FormSectionLabel,
-} from '@/components/ui/Forms/FormSection'
import { invalidateSchemasQuery } from '@/data/database/schemas-query'
import { useFDWUpdateMutation } from '@/data/fdw/fdw-update-mutation'
import { FDW } from '@/data/fdw/fdws-query'
@@ -52,319 +72,286 @@ export const EditWrapperSheet = ({
const { mutate: updateFDW, isPending: isSaving } = useFDWUpdateMutation({
onSuccess: () => {
toast.success(`Successfully updated ${wrapperMeta?.label} foreign data wrapper`)
- setWrapperTables([])
- const hasNewSchema = wrapperTables.some((table) => table.is_new_schema)
+ const { tables } = getValues()
+ const hasNewSchema = (tables as Record[]).some((table) => table.is_new_schema)
if (hasNewSchema) invalidateSchemasQuery(queryClient, project?.ref)
},
})
- const [wrapperTables, setWrapperTables] = useState(() =>
- formatWrapperTables(wrapper, wrapperMeta)
+ const initialValues: Record = useMemo(
+ () => ({
+ wrapper_name: wrapper?.name,
+ server_name: wrapper?.server_name,
+ ...convertKVStringArrayToJson(wrapper?.server_options ?? []),
+ tables: formatWrapperTables(wrapper, wrapperMeta),
+ }),
+ [wrapper, wrapperMeta]
)
- const [isEditingTable, setIsEditingTable] = useState(false)
+
+ const formSchema = getEditionFormSchema(wrapperMeta)
+ type FormSchema = z.infer
+ const form = useForm({
+ defaultValues: initialValues,
+ resolver: zodResolver(formSchema),
+ })
+
+ const { getValues, reset, resetField, setError } = form
+ const { errors, isDirty, isSubmitting } = form.formState
+
+ const {
+ fields: tablesField,
+ append: appendTable,
+ remove: removeTable,
+ update: updateTable,
+ } = useFieldArray({
+ control: form.control,
+ name: 'tables',
+ })
+
const [selectedTableToEdit, setSelectedTableToEdit] = useState(
undefined
)
- const [formErrors, setFormErrors] = useState<{ [k: string]: string }>({})
const [isUpdateConfirmationOpen, setIsUpdateConfirmationOpen] = useState(false)
- const [pendingFormState, setPendingFormState] = useState | null>(null)
- const hasChangesRef = useRef(false)
-
- const initialValues = {
- wrapper_name: wrapper?.name,
- server_name: wrapper?.server_name,
- ...convertKVStringArrayToJson(wrapper?.server_options ?? []),
- }
const onUpdateTable = (values: FormattedWrapperTable) => {
- setWrapperTables((prev) => {
- // if the new values have tableIndex, we are editing an existing table
- if (values.tableIndex !== undefined) {
- const tableIndex = values.tableIndex
- const wrapperTables = [...prev]
- delete values.tableIndex
- wrapperTables[tableIndex] = values
- return wrapperTables
- }
- return [...prev, values]
- })
- setIsEditingTable(false)
+ if (values.index !== undefined) {
+ updateTable(values.index, values)
+ } else {
+ appendTable(values)
+ }
setSelectedTableToEdit(undefined)
}
- const onSubmit = async (values: Record) => {
- const validate = makeValidateRequired(wrapperMeta.server.options)
- const errors = validate(values)
-
- const { wrapper_name } = values
- if (wrapper_name.length === 0) errors.name = 'Please provide a name for your wrapper'
- if (!wrapperMeta.canTargetSchema && wrapperTables.length === 0)
- errors.tables = 'Please add at least one table'
- if (!isEmpty(errors)) {
- setFormErrors(errors)
+ const onSubmit: SubmitHandler = async (values) => {
+ const { tables } = values
+ if (tables.length === 0) {
+ setError('tables', {
+ type: 'validate',
+ message: 'Please provide at least one table.',
+ })
return
}
-
- setFormErrors({})
- setPendingFormState({ ...values, server_name: `${wrapper_name}_server` })
setIsUpdateConfirmationOpen(true)
}
- const checkIsDirty = useCallback(() => hasChangesRef.current, [])
-
const { confirmOnClose, modalProps } = useConfirmOnClose({
- checkIsDirty,
+ checkIsDirty: () => isDirty,
onClose,
})
useEffect(() => {
if (!isClosing) return
- if (checkIsDirty()) {
+ if (isDirty) {
confirmOnClose()
} else {
onClose()
}
setIsClosing(false)
- }, [checkIsDirty, confirmOnClose, isClosing, onClose, setIsClosing])
+ }, [isDirty, confirmOnClose, isClosing, onClose, setIsClosing])
+
+ const wrapper_name = useWatch({ name: 'wrapper_name', control: form.control })
+
+ const [isLoadingSecrets, setIsLoadingSecrets] = useState(false)
+ useEffect(() => {
+ const encryptedOptions = wrapperMeta.server.options.filter((option) => option.encrypted)
+
+ const encryptedIdsToFetch = compact(
+ encryptedOptions.map((option) => {
+ const value = initialValues[option.name]
+ return value ?? null
+ })
+ ).filter((x) => UUID_REGEX.test(x))
+ // [Joshen] ^ Validate UUID to filter out already decrypted values
+
+ const fetchEncryptedValues = async (ids: string[]) => {
+ try {
+ setIsLoadingSecrets(true)
+ // If the secrets haven't loaded, escape and run the effect again when they're loaded
+ const decryptedValues = await getDecryptedValues({
+ projectRef: project?.ref,
+ connectionString: project?.connectionString,
+ ids: ids,
+ })
+
+ encryptedOptions.forEach((option) => {
+ const encryptedId = initialValues[option.name]
+
+ resetField(option.name, { defaultValue: decryptedValues[encryptedId] })
+ })
+ } catch (error) {
+ toast.error('Failed to fetch encrypted values')
+ } finally {
+ setIsLoadingSecrets(false)
+ }
+ }
+
+ if (encryptedIdsToFetch.length > 0) {
+ fetchEncryptedValues(encryptedIdsToFetch)
+ }
+ }, [initialValues, wrapperMeta, resetField, project?.ref, project?.connectionString])
return (
<>
- )
- } else {
- return (
-
- {option.label}
- {option.urlHelper !== undefined && (
-
-
-
+ labelOptional={!option.required ? 'Optional' : undefined}
+ description={option.description}
+ >
+
+ {loading ? (
+
+ Fetching value from Vault...
+
+ ) : option.isTextArea ? (
+
+ ) : option.secureEntry ? (
+
+ ) : (
+
)}
-
- }
- labelOptional={option.required ? undefined : 'Optional'}
- defaultValue={option.defaultValue ?? ''}
- error={error}
- value={loading ? 'Fetching value from Vault...' : undefined}
- type={!option.secureEntry || loading ? 'text' : showHidden ? 'text' : 'password'}
- disabled={loading || option.readOnly}
- actions={
- loading ? (
-
- } />
-
- ) : option.secureEntry ? (
-
- : }
- onClick={() => setShowHidden(!showHidden)}
- />
-
- ) : null
- }
- />
- )
- }
+
+
+ )}
+ />
+ )
}
export default InputField
diff --git a/apps/studio/components/interfaces/Integrations/Wrappers/WrapperDynamicColumns.tsx b/apps/studio/components/interfaces/Integrations/Wrappers/WrapperDynamicColumns.tsx
deleted file mode 100644
index a3c6868bd88..00000000000
--- a/apps/studio/components/interfaces/Integrations/Wrappers/WrapperDynamicColumns.tsx
+++ /dev/null
@@ -1,165 +0,0 @@
-import { X } from 'lucide-react'
-import { useEffect, useReducer } from 'react'
-import { Button, Input } from 'ui'
-
-import ColumnType from '@/components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/ColumnType'
-import useLatest from '@/hooks/misc/useLatest'
-
-export type SimpleColumn = {
- id: number
- name: string
- type: string
-}
-
-export type WrapperDynamicColumnsProps = {
- initialColumns?: Pick[]
- onChange?: (columns: SimpleColumn[]) => void
- errors?: any
-}
-
-const DEFAULT_INITIAL_COLUMNS: WrapperDynamicColumnsProps['initialColumns'] = [
- { name: '', type: 'text' },
-]
-
-type State = {
- columns: {
- [key: number]: SimpleColumn
- }
- nextId: number
-}
-
-type Action =
- | {
- type: 'ADD_COLUMN'
- }
- | {
- type: 'REMOVE_COLUMN'
- payload: {
- id: number
- }
- }
- | {
- type: 'UPDATE_COLUMN'
- payload: {
- id: number
- key: keyof SimpleColumn
- value: string
- }
- }
-
-const WrapperDynamicColumns = ({
- initialColumns = DEFAULT_INITIAL_COLUMNS,
- onChange,
- errors = {},
-}: WrapperDynamicColumnsProps) => {
- const [state, dispatch] = useReducer(
- (state: State, action: Action) => {
- switch (action.type) {
- case 'ADD_COLUMN':
- return {
- ...state,
- columns: {
- ...state.columns,
- [state.nextId]: { id: state.nextId, name: '', type: 'text' },
- },
- nextId: state.nextId + 1,
- }
- case 'REMOVE_COLUMN':
- return {
- ...state,
- columns: Object.fromEntries(
- Object.entries(state.columns).filter(([key]) => Number(key) !== action.payload.id)
- ),
- }
- case 'UPDATE_COLUMN':
- return {
- ...state,
- columns: {
- ...state.columns,
- [action.payload.id]: {
- ...state.columns[action.payload.id],
- [action.payload.key]: action.payload.value,
- },
- },
- }
- default:
- return state
- }
- },
- {
- columns: Object.fromEntries(
- initialColumns.map((column, index) => [index, { ...column, id: index }])
- ),
- nextId: initialColumns.length,
- }
- )
-
- const onChangeRef = useLatest(onChange)
- useEffect(() => {
- onChangeRef.current?.(getColumns(state.columns))
- }, [state.columns])
-
- const onAddColumn = () => {
- dispatch({ type: 'ADD_COLUMN' })
- }
-
- const onRemoveColumn = (id: number) => {
- dispatch({ type: 'REMOVE_COLUMN', payload: { id } })
- }
-
- const onUpdateValue = (id: number, key: keyof SimpleColumn, value: string) => {
- dispatch({ type: 'UPDATE_COLUMN', payload: { id, key, value } })
- }
-
- const columns = getColumns(state.columns)
-
- return (
-
-
- {columns.map((column, idx) => (
-
-
-
onUpdateValue(column.id, 'name', e.target.value)}
- />
-
-
- onUpdateValue(column.id, 'type', value)}
- className="[&_label]:!p-0"
- />
-
-
-
}
- onClick={() => onRemoveColumn(column.id)}
- className="self-end -translate-y-1.5 px-1.5"
- />
-
-
- {errors[`columns.${idx}`] && (
-
{errors[`columns.${idx}`]}
- )}
-
- ))}
-
-
-
-
- )
-}
-
-export default WrapperDynamicColumns
-
-function getColumns(columns: State['columns']) {
- return Object.values(columns).sort((a, b) => a.id - b.id)
-}
diff --git a/apps/studio/components/interfaces/Integrations/Wrappers/WrapperTable.tsx b/apps/studio/components/interfaces/Integrations/Wrappers/WrapperTable.tsx
index 13d0cb57743..0cadcef8c83 100644
--- a/apps/studio/components/interfaces/Integrations/Wrappers/WrapperTable.tsx
+++ b/apps/studio/components/interfaces/Integrations/Wrappers/WrapperTable.tsx
@@ -35,7 +35,7 @@ export const WrapperTable = ({ isLatest = false }: WrapperTableProps) => {
const [isClosingEditWrapper, setIsClosingEditWrapper] = useState(false)
- const { data, isSuccess } = useFDWsQuery({
+ const { data, isError } = useFDWsQuery({
projectRef: ref,
connectionString: project?.connectionString,
})
@@ -52,11 +52,11 @@ export const WrapperTable = ({ isLatest = false }: WrapperTableProps) => {
const selectedWrapperToEdit = wrappers.find((w) => w.id.toString() === selectedWrapperIdToEdit)
useEffect(() => {
- if (isSuccess && !!selectedWrapperIdToEdit && !selectedWrapperToEdit) {
+ if (isError && !!selectedWrapperIdToEdit && !selectedWrapperToEdit) {
toast('Wrapper not found')
setSelectedWrapperToEdit(null)
}
- }, [isSuccess, selectedWrapperIdToEdit, selectedWrapperToEdit, setSelectedWrapperToEdit])
+ }, [isError, selectedWrapperIdToEdit, selectedWrapperToEdit, setSelectedWrapperToEdit])
if (!integration || integration.type !== 'wrapper') {
return (
diff --git a/apps/studio/components/interfaces/Integrations/Wrappers/WrapperTableEditor.tsx b/apps/studio/components/interfaces/Integrations/Wrappers/WrapperTableEditor.tsx
index d58521f31e0..447a606e4c8 100644
--- a/apps/studio/components/interfaces/Integrations/Wrappers/WrapperTableEditor.tsx
+++ b/apps/studio/components/interfaces/Integrations/Wrappers/WrapperTableEditor.tsx
@@ -1,5 +1,14 @@
-import { Check, ChevronsUpDown, Database, Plus } from 'lucide-react'
-import { useEffect, useId, useState } from 'react'
+import { zodResolver } from '@hookform/resolvers/zod'
+import { Check, ChevronsUpDown, XIcon } from 'lucide-react'
+import { useEffect, useId, useMemo, useState } from 'react'
+import {
+ Control,
+ FieldValues,
+ SubmitHandler,
+ useFieldArray,
+ useForm,
+ useWatch,
+} from 'react-hook-form'
import {
Button,
cn,
@@ -9,22 +18,37 @@ import {
CommandInput_Shadcn_,
CommandItem_Shadcn_,
CommandList_Shadcn_,
- Form,
- Input,
+ Form_Shadcn_,
+ FormControl_Shadcn_,
+ FormField_Shadcn_,
+ Input_Shadcn_,
Label_Shadcn_,
- Listbox,
- Modal,
Popover_Shadcn_,
PopoverContent_Shadcn_,
PopoverTrigger_Shadcn_,
ScrollArea,
+ Select_Shadcn_,
+ SelectContent_Shadcn_,
+ SelectItem_Shadcn_,
+ SelectSeparator_Shadcn_,
+ SelectTrigger_Shadcn_,
+ SelectValue_Shadcn_,
SidePanel,
} from 'ui'
+import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
+import {
+ MultiSelector,
+ MultiSelectorContent,
+ MultiSelectorItem,
+ MultiSelectorList,
+ MultiSelectorTrigger,
+} from 'ui-patterns/multi-select'
import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
+import * as z from 'zod'
-import WrapperDynamicColumns from './WrapperDynamicColumns'
-import type { Table, TableOption } from './Wrappers.types'
-import { makeValidateRequired } from './Wrappers.utils'
+import { ColumnType } from './ColumnType'
+import type { AvailableColumn, Table, TableOption } from './Wrappers.types'
+import { getTableFormSchema } from './Wrappers.utils'
import { ActionBar } from '@/components/interfaces/TableGridEditor/SidePanelEditor/ActionBar'
import { useSchemasQuery } from '@/data/database/schemas-query'
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
@@ -38,8 +62,6 @@ export type WrapperTableEditorProps = {
initialData: any
}
-type OnSubmitFn = (values: any, { resetForm }: { resetForm: () => void }) => void
-
const WrapperTableEditor = ({
visible,
onCancel,
@@ -52,7 +74,7 @@ const WrapperTableEditor = ({
const [selectedTableIndex, setSelectedTableIndex] = useState('')
useEffect(() => {
- if (initialData) {
+ if (initialData && Object.keys(initialData).length > 0) {
setSelectedTableIndex(String(initialData.index))
}
}, [initialData])
@@ -64,14 +86,13 @@ const WrapperTableEditor = ({
onCancel()
}
- const onSubmit: OnSubmitFn = (values, { resetForm }) => {
+ const onSubmit: SubmitHandler = (values) => {
onSave({
...values,
index: parseInt(selectedTableIndex),
schema_name: values.schema === 'custom' ? values.schema_name : values.schema,
is_new_schema: values.schema === 'custom',
})
- resetForm()
setSelectedTableIndex('')
}
@@ -164,48 +185,48 @@ const WrapperTableEditor = ({
export default WrapperTableEditor
-const Option = ({ option }: { option: TableOption }) => {
+const Option = ({ option, control }: { option: TableOption; control: Control }) => {
if (option.type === 'select') {
return (
-
- {[
- ...(!option.required
- ? [
-
- ---
- ,
- ]
- : []),
- ...option.options.map((subOption) => (
-
- {subOption.label}
-
- )),
- ]}
-
+ defaultValue={option.defaultValue}
+ render={({ field }) => (
+
+
+
+
+
+
+
+
+ {option.options.map((subOption) => (
+
+ {subOption.label}
+
+ ))}
+
+
+
+
+ )}
+ />
)
}
return (
- (
+
+
+
+
+
+ )}
/>
)
}
@@ -216,153 +237,285 @@ const TableForm = ({
initialData,
}: {
table: Table
- onSubmit: OnSubmitFn
+ onSubmit: SubmitHandler
initialData: any
}) => {
const { data: project } = useSelectedProjectQuery()
- const {
- data: schemas,
- isPending: isLoading,
- isSuccess,
- } = useSchemasQuery({
+ const { data: schemas, isPending: isLoading } = useSchemasQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
})
- const requiredOptions =
- table.options.filter((option) => option.editable && option.required && !option.defaultValue) ??
- []
- const optionalOptions =
- table.options.filter(
- (option) => option.editable && (!option.required || option.defaultValue)
- ) ?? []
+ const requiredOptions: TableOption[] = []
+ const optionalOptions: TableOption[] = []
+ const nonEditableOptions: TableOption[] = []
- const initialValues = initialData ?? {
- table_name: '',
- columns: table.availableColumns ?? [],
- ...Object.fromEntries(table.options.map((option) => [option.name, option.defaultValue ?? ''])),
- schema: 'public',
- schema_name: '',
+ table.options.forEach((option) => {
+ if (option.editable) {
+ if (option.required && !option.defaultValue) {
+ requiredOptions.push(option)
+ return
+ }
+ optionalOptions.push(option)
+ return
+ }
+ nonEditableOptions.push(option)
+ })
+
+ const defaultValues = useMemo(() => {
+ if (initialData && Object.keys(initialData).length > 0) {
+ const { schema } = initialData
+ const existingSchema = schemas?.find((s) => s.name === schema)
+
+ return {
+ schema_name: existingSchema ? '' : schema,
+ schema: existingSchema ? existingSchema.name : 'custom',
+ ...Object.fromEntries(
+ table.options.map((option) => [option.name, option.defaultValue ?? ''])
+ ),
+ ...initialData,
+ }
+ }
+ return {
+ table_name: '',
+ columns: table.availableColumns ?? [],
+ schema: 'public',
+ ...Object.fromEntries(
+ table.options.map((option) => [option.name, option.defaultValue ?? ''])
+ ),
+ }
+ }, [initialData, table, schemas])
+
+ const formSchema = getTableFormSchema(table)
+ type FormSchema = z.infer
+
+ const form = useForm({
+ defaultValues,
+ resolver: zodResolver(formSchema),
+ shouldUnregister: true,
+ })
+
+ const {
+ fields: columnFields,
+ append: appendColumn,
+ replace: replaceColumns,
+ remove: removeColumn,
+ } = useFieldArray({
+ control: form.control,
+ name: 'columns',
+ })
+
+ const { reset } = form
+ useEffect(() => {
+ reset(defaultValues)
+ // Workaround bug in react-hook-form
+ replaceColumns(defaultValues.columns ?? [])
+ }, [reset, replaceColumns, defaultValues])
+
+ const handleSubmit: SubmitHandler = (values) => {
+ const { schema_name, schema, ...valuesWithoutSchema } = values
+ onSubmit({
+ ...valuesWithoutSchema,
+ // Ensure all options are accounted for.
+ ...Object.fromEntries(
+ table.options.map((option) => [
+ option.name,
+ values[option.name] ?? option.defaultValue ?? '',
+ ])
+ ),
+ schema,
+ schema_name: schema === 'custom' ? schema_name : schema,
+ is_new_schema: schema === 'custom',
+ })
+ reset()
}
- const validate = makeValidateRequired([
- ...table.options,
- { name: 'table_name', required: true },
- { name: 'columns', required: true },
- ...(table.availableColumns ? [] : [{ name: 'columns.name', required: true }]),
- ])
+ const { errors } = form.formState
+ const schema = useWatch({ name: 'schema', control: form.control })
return (
-