mirror of
https://github.com/supabase/supabase.git
synced 2026-07-04 05:44:39 +08:00
304 lines
8.6 KiB
TypeScript
304 lines
8.6 KiB
TypeScript
import { FC, useState } from 'react'
|
|
import { find, isEmpty, isUndefined } from 'lodash'
|
|
import { Query, Dictionary } from 'components/grid'
|
|
import { Modal } from '@supabase/ui'
|
|
import {
|
|
PostgresRelationship,
|
|
PostgresTable,
|
|
PostgresColumn,
|
|
PostgresType,
|
|
} from '@supabase/postgres-meta'
|
|
|
|
import { useStore } from 'hooks'
|
|
import { RowEditor, ColumnEditor, TableEditor } from '.'
|
|
import { ImportContent } from './TableEditor/TableEditor.types'
|
|
import { ColumnField, CreateColumnPayload, UpdateColumnPayload } from './SidePanelEditor.types'
|
|
import ConfirmationModal from 'components/ui/ConfirmationModal'
|
|
|
|
interface Props {
|
|
selectedSchema: string
|
|
selectedTable?: PostgresTable
|
|
selectedRowToEdit?: Dictionary<any>
|
|
selectedColumnToEdit?: PostgresColumn
|
|
selectedTableToEdit?: PostgresTable
|
|
sidePanelKey?: 'row' | 'column' | 'table'
|
|
isDuplicating?: boolean
|
|
closePanel: () => void
|
|
onRowCreated?: (row: Dictionary<any>) => void
|
|
onRowUpdated?: (row: Dictionary<any>, idx: number) => void
|
|
|
|
// Because the panel is shared between grid editor and database pages
|
|
// Both require different responses upon success of these events
|
|
onTableCreated?: (table: PostgresTable) => void
|
|
onColumnSaved?: () => void
|
|
}
|
|
|
|
const SidePanelEditor: FC<Props> = ({
|
|
selectedSchema,
|
|
selectedTable,
|
|
selectedRowToEdit,
|
|
selectedColumnToEdit,
|
|
selectedTableToEdit,
|
|
sidePanelKey,
|
|
isDuplicating = false,
|
|
closePanel,
|
|
onRowCreated = () => {},
|
|
onRowUpdated = () => {},
|
|
onTableCreated = () => {},
|
|
onColumnSaved = () => {},
|
|
}) => {
|
|
const { meta, ui } = useStore()
|
|
|
|
const [isEdited, setIsEdited] = useState<boolean>(false)
|
|
const [isClosingPanel, setIsClosingPanel] = useState<boolean>(false)
|
|
|
|
const tables = meta.tables.list()
|
|
const enumTypes = meta.types.list(
|
|
(type: PostgresType) => !meta.excludedSchemas.includes(type.schema)
|
|
)
|
|
|
|
const saveRow = async (
|
|
payload: any,
|
|
isNewRecord: boolean,
|
|
configuration: { identifiers: any; rowIdx: number },
|
|
onComplete: Function
|
|
) => {
|
|
let saveRowError = false
|
|
if (isNewRecord) {
|
|
const insertQuery = new Query()
|
|
.from(selectedTable!.name, selectedTable!.schema)
|
|
.insert([payload], { returning: true })
|
|
.toSql()
|
|
|
|
const res: any = await meta.query(insertQuery)
|
|
if (res.error) {
|
|
saveRowError = true
|
|
ui.setNotification({ category: 'error', message: res.error?.message })
|
|
} else {
|
|
onRowCreated(res[0])
|
|
}
|
|
} else {
|
|
const hasChanges = !isEmpty(payload)
|
|
if (hasChanges) {
|
|
if (selectedTable!.primary_keys.length > 0) {
|
|
const updateQuery = new Query()
|
|
.from(selectedTable!.name, selectedTable!.schema)
|
|
.update(payload, { returning: true })
|
|
.match(configuration.identifiers)
|
|
.toSql()
|
|
|
|
const res: any = await meta.query(updateQuery)
|
|
if (res.error) {
|
|
saveRowError = true
|
|
ui.setNotification({ category: 'error', message: res.error?.message })
|
|
} else {
|
|
onRowUpdated(res[0], configuration.rowIdx)
|
|
}
|
|
} else {
|
|
saveRowError = true
|
|
ui.setNotification({
|
|
category: 'error',
|
|
message:
|
|
"We can't make changes to this table because there is no primary key. Please create a primary key and try again.",
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
onComplete()
|
|
if (!saveRowError) {
|
|
setIsEdited(false)
|
|
closePanel()
|
|
}
|
|
}
|
|
|
|
const saveColumn = async (
|
|
payload: CreateColumnPayload | UpdateColumnPayload,
|
|
foreignKey: Partial<PostgresRelationship> | undefined,
|
|
isNewRecord: boolean,
|
|
configuration: { columnId?: string },
|
|
resolve: any
|
|
) => {
|
|
const response = isNewRecord
|
|
? await meta.createColumn(payload as CreateColumnPayload, foreignKey)
|
|
: await meta.updateColumn(
|
|
configuration.columnId as string,
|
|
payload as UpdateColumnPayload,
|
|
selectedTable as PostgresTable,
|
|
foreignKey
|
|
)
|
|
|
|
if (response?.error) {
|
|
ui.setNotification({ category: 'error', message: response.error.message })
|
|
} else {
|
|
await meta.tables.loadById(selectedTable!.id)
|
|
onColumnSaved()
|
|
setIsEdited(false)
|
|
closePanel()
|
|
}
|
|
|
|
resolve()
|
|
}
|
|
|
|
const saveTable = async (
|
|
payload: any,
|
|
columns: ColumnField[],
|
|
isNewRecord: boolean,
|
|
configuration: {
|
|
tableId?: number
|
|
importContent?: ImportContent
|
|
isRLSEnabled: boolean
|
|
isRealtimeEnabled: boolean
|
|
isDuplicateRows: boolean
|
|
},
|
|
resolve: any
|
|
) => {
|
|
let toastId
|
|
let saveTableError = false
|
|
const { tableId, importContent, isRLSEnabled, isRealtimeEnabled, isDuplicateRows } =
|
|
configuration
|
|
|
|
try {
|
|
if (isDuplicating) {
|
|
const duplicateTable = find(tables, { id: tableId }) as PostgresTable
|
|
toastId = ui.setNotification({
|
|
category: 'loading',
|
|
message: `Duplicating table: ${duplicateTable.name}...`,
|
|
})
|
|
const table: any = await meta.duplicateTable(payload, {
|
|
isRLSEnabled,
|
|
isRealtimeEnabled,
|
|
isDuplicateRows,
|
|
duplicateTable,
|
|
})
|
|
ui.setNotification({
|
|
id: toastId,
|
|
category: 'success',
|
|
message: `Table ${duplicateTable.name} has been successfully duplicated into ${table.name}!`,
|
|
})
|
|
onTableCreated(table)
|
|
} else if (isNewRecord) {
|
|
toastId = ui.setNotification({
|
|
category: 'loading',
|
|
message: `Creating new table: ${payload.name}...`,
|
|
})
|
|
|
|
const table = await meta.createTable(
|
|
toastId,
|
|
payload,
|
|
columns,
|
|
isRLSEnabled,
|
|
isRealtimeEnabled,
|
|
importContent
|
|
)
|
|
ui.setNotification({
|
|
id: toastId,
|
|
category: 'success',
|
|
message: `Table ${table.name} is good to go!`,
|
|
})
|
|
onTableCreated(table)
|
|
} else if (selectedTableToEdit) {
|
|
toastId = ui.setNotification({
|
|
category: 'loading',
|
|
message: `Updating table: ${selectedTableToEdit?.name}...`,
|
|
})
|
|
const { table, hasError }: any = await meta.updateTable(
|
|
toastId,
|
|
selectedTableToEdit,
|
|
payload,
|
|
columns,
|
|
isRealtimeEnabled
|
|
)
|
|
if (hasError) {
|
|
ui.setNotification({
|
|
id: toastId,
|
|
category: 'info',
|
|
message: `Table ${table.name} has been updated, but there were some errors`,
|
|
})
|
|
} else {
|
|
ui.setNotification({
|
|
id: toastId,
|
|
category: 'success',
|
|
message: `Successfully updated ${table.name}!`,
|
|
})
|
|
}
|
|
}
|
|
} catch (error: any) {
|
|
saveTableError = true
|
|
ui.setNotification({ id: toastId, category: 'error', message: error.message })
|
|
}
|
|
|
|
if (!saveTableError) {
|
|
setIsEdited(false)
|
|
closePanel()
|
|
}
|
|
|
|
resolve()
|
|
}
|
|
|
|
const onClosePanel = () => {
|
|
if (isEdited) {
|
|
setIsClosingPanel(true)
|
|
} else {
|
|
closePanel()
|
|
}
|
|
}
|
|
|
|
return (
|
|
<>
|
|
{!isUndefined(selectedTable) && (
|
|
<RowEditor
|
|
row={selectedRowToEdit}
|
|
selectedTable={selectedTable}
|
|
visible={sidePanelKey === 'row'}
|
|
closePanel={onClosePanel}
|
|
saveChanges={saveRow}
|
|
updateEditorDirty={() => setIsEdited(true)}
|
|
/>
|
|
)}
|
|
{!isUndefined(selectedTable) && (
|
|
<ColumnEditor
|
|
enumTypes={enumTypes}
|
|
tables={tables}
|
|
column={selectedColumnToEdit}
|
|
selectedTable={selectedTable}
|
|
visible={sidePanelKey === 'column'}
|
|
closePanel={onClosePanel}
|
|
saveChanges={saveColumn}
|
|
updateEditorDirty={() => setIsEdited(true)}
|
|
/>
|
|
)}
|
|
<TableEditor
|
|
table={selectedTableToEdit}
|
|
selectedSchema={selectedSchema}
|
|
isDuplicating={isDuplicating}
|
|
visible={sidePanelKey === 'table'}
|
|
closePanel={onClosePanel}
|
|
saveChanges={saveTable}
|
|
updateEditorDirty={() => setIsEdited(true)}
|
|
/>
|
|
<ConfirmationModal
|
|
visible={isClosingPanel}
|
|
header="Confirm to close"
|
|
buttonLabel="Confirm"
|
|
onSelectCancel={() => setIsClosingPanel(false)}
|
|
onSelectConfirm={() => {
|
|
setIsClosingPanel(false)
|
|
setIsEdited(false)
|
|
closePanel()
|
|
}}
|
|
children={
|
|
<Modal.Content>
|
|
<p className="text-scale-1100 py-4 text-sm">
|
|
There are unsaved changes. Are you sure you want to close the panel? Your changes will
|
|
be lost.
|
|
</p>
|
|
</Modal.Content>
|
|
}
|
|
/>
|
|
</>
|
|
)
|
|
}
|
|
|
|
export default SidePanelEditor
|