Files
supabase/apps/studio/components/interfaces/Database/Wrappers/WrapperTableEditor.tsx
2024-03-04 20:48:22 +08:00

313 lines
9.3 KiB
TypeScript

import { useEffect, useState } from 'react'
import ActionBar from 'components/interfaces/TableGridEditor/SidePanelEditor/ActionBar'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import ShimmeringLoader from 'components/ui/ShimmeringLoader'
import { useSchemasQuery } from 'data/database/schemas-query'
import { Form, IconDatabase, IconPlus, Input, Listbox, Modal, SidePanel } from 'ui'
import WrapperDynamicColumns from './WrapperDynamicColumns'
import type { Table, TableOption } from './Wrappers.types'
import { makeValidateRequired } from './Wrappers.utils'
export type WrapperTableEditorProps = {
visible: boolean
onCancel: () => void
onSave: (values: any) => void
tables: Table[]
initialData: any
}
type OnSubmitFn = (values: any, { resetForm }: { resetForm: () => void }) => void
const WrapperTableEditor = ({
visible,
onCancel,
onSave,
tables,
initialData,
}: WrapperTableEditorProps) => {
const [selectedTableIndex, setSelectedTableIndex] = useState<string>('')
useEffect(() => {
if (initialData) {
setSelectedTableIndex(String(initialData.index))
}
}, [initialData])
const selectedTable = selectedTableIndex === '' ? undefined : tables[parseInt(selectedTableIndex)]
const handleCancel = () => {
setSelectedTableIndex('')
onCancel()
}
const onSubmit: OnSubmitFn = (values, { resetForm }) => {
onSave({
...values,
index: parseInt(selectedTableIndex),
schema_name: values.schema === 'custom' ? values.schema_name : values.schema,
is_new_schema: values.schema === 'custom',
})
resetForm()
setSelectedTableIndex('')
}
return (
<SidePanel
key="WrapperTableEditor"
size="medium"
visible={visible}
onCancel={handleCancel}
header={<span>Edit foreign table</span>}
customFooter={
<ActionBar
backButtonLabel="Cancel"
applyButtonLabel="Save"
formId="wrapper-table-editor-form"
closePanel={handleCancel}
/>
}
>
<SidePanel.Content>
<div className="my-4 space-y-6">
<Listbox
size="small"
label="Select a target the table will point to"
value={selectedTableIndex}
onChange={(value) => setSelectedTableIndex(value)}
>
<Listbox.Option key="empty" value="" label="---">
---
</Listbox.Option>
{tables.map((table, i) => {
return (
<Listbox.Option
className="group"
key={String(i)}
value={String(i)}
label={table.label}
>
<div className="space-y-1">
<p>{table.label}</p>
<p className="opacity-50">{table.description}</p>
</div>
</Listbox.Option>
)
})}
</Listbox>
{selectedTable && (
<TableForm table={selectedTable} onSubmit={onSubmit} initialData={initialData} />
)}
</div>
</SidePanel.Content>
</SidePanel>
)
}
export default WrapperTableEditor
const Option = ({ option }: { option: TableOption }) => {
if (option.type === 'select') {
return (
<Listbox
key={option.name}
id={option.name}
name={option.name}
label={option.label}
defaultValue={option.defaultValue ?? ''}
>
{[
...(!option.required
? [
<Listbox.Option key="empty" value="" label="---">
---
</Listbox.Option>,
]
: []),
...option.options.map((subOption) => (
<Listbox.Option
key={subOption.value}
id={option.name + subOption.value}
value={subOption.value}
label={subOption.label}
>
{subOption.label}
</Listbox.Option>
)),
]}
</Listbox>
)
}
return (
<Input
key={option.name}
id={option.name}
name={option.name}
label={option.label}
placeholder={option.placeholder ?? ''}
defaultValue={option.defaultValue ?? ''}
/>
)
}
const TableForm = ({
table,
onSubmit,
initialData,
}: {
table: Table
onSubmit: OnSubmitFn
initialData: any
}) => {
const { project } = useProjectContext()
const {
data: schemas,
isLoading,
isSuccess,
} = 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 initialValues = initialData ?? {
table_name: '',
columns: table.availableColumns ?? [],
...Object.fromEntries(table.options.map((option) => [option.name, option.defaultValue ?? ''])),
schema: 'public',
schema_name: '',
}
const validate = makeValidateRequired([
...table.options,
{ name: 'table_name', required: true },
{ name: 'columns', required: true },
...(table.availableColumns ? [] : [{ name: 'columns.name', required: true }]),
])
return (
<Form
id="wrapper-table-editor-form"
initialValues={initialValues}
validate={validate}
onSubmit={onSubmit}
enableReinitialize={true}
>
{({ errors, values, setFieldValue }: any) => {
return (
<div className="space-y-4">
{isLoading && <ShimmeringLoader className="py-4" />}
{isSuccess && (
<Listbox size="small" name="schema" label="Select a schema for the foreign table">
<Listbox.Option
key="custom"
id="custom"
label={`Create a new schema`}
value="custom"
addOnBefore={() => <IconPlus size={16} strokeWidth={1.5} />}
>
Create a new schema
</Listbox.Option>
<Modal.Separator />
{(schemas ?? [])?.map((schema) => {
return (
<Listbox.Option
key={schema.id}
id={schema.name}
label={schema.name}
value={schema.name}
addOnBefore={() => <IconDatabase size={16} strokeWidth={1.5} />}
>
{schema.name}
</Listbox.Option>
)
})}
</Listbox>
)}
{values.schema === 'custom' && (
<Input id="schema_name" name="schema_name" label="Schema name" />
)}
<Input
id="table_name"
name="table_name"
label="Table name"
descriptionText="You can query from this table after the wrapper is enabled."
/>
{requiredOptions.map((option) => (
<Option key={option.name} option={option} />
))}
<div className="form-group">
<label className="!w-full">
{table.availableColumns
? 'Select the columns to be added to your table'
: 'Add columns to your table'}
</label>
<div className="flex flex-wrap gap-2 w-full">
{table.availableColumns ? (
table.availableColumns.map((column) => {
const isSelected = Boolean(
values.columns.find((col: any) => col.name === column.name)
)
return (
<div
key={column.name}
className={[
'px-2 py-1 rounded cursor-pointer transition',
`${isSelected ? 'bg-brand-300' : 'bg-surface-300 hover:bg-selection'}`,
].join(' ')}
onClick={() => {
if (isSelected) {
setFieldValue(
'columns',
values.columns.filter((col: any) => col.name !== column.name)
)
} else {
setFieldValue('columns', values.columns.concat([column]))
}
}}
>
<p className="text-sm">{column.name}</p>
</div>
)
})
) : (
<WrapperDynamicColumns
initialColumns={values.columns}
onChange={(columns) => {
setFieldValue('columns', columns)
}}
errors={errors}
/>
)}
</div>
{errors.columns && (
<span className="text-red-900 text-sm mt-2">{errors.columns}</span>
)}
</div>
{optionalOptions.map((option) => (
<Option key={option.name} option={option} />
))}
</div>
)
}}
</Form>
)
}