Sql Query Manager Add, Edit ve Delete butonları

This commit is contained in:
Sedat Öztürk 2026-06-19 23:09:42 +03:00
parent 947d21ecb7
commit 99e6f91b2a
3 changed files with 524 additions and 76 deletions

View file

@ -282,6 +282,29 @@ div.dialog-after-open > div.dialog-content.maximized {
margin-left: 2px !important;
}
.sql-results-grid .dx-toolbar-items-container {
display: flex !important;
}
.sql-results-grid .dx-toolbar-before {
width: auto !important;
max-width: none !important;
flex: 1 1 auto !important;
display: flex !important;
justify-content: flex-start !important;
}
.sql-results-grid .dx-toolbar-before .dx-toolbar-item {
margin-right: 4px !important;
}
.sql-results-grid .dx-toolbar-after {
width: auto !important;
max-width: none !important;
margin-left: auto !important;
text-align: right !important;
}
.dx-datagrid-rowsview .dx-datagrid-group-row-container {
background-color: transparent !important;
}

View file

@ -42,6 +42,7 @@ interface SqlManagerState {
isDirty: boolean
tableColumns: any | null
refreshTrigger: number
lastExecutedQuery: string
}
interface SqlCopyResultItem {
@ -75,6 +76,7 @@ const SqlQueryManager = () => {
showProperties: false,
isDirty: false,
tableColumns: null,
lastExecutedQuery: '',
})
const [showTemplateConfirmDialog, setShowTemplateConfirmDialog] = useState(false)
@ -138,6 +140,7 @@ const SqlQueryManager = () => {
selectedDataSource: dataSource.code ?? null,
editorContent: '',
executionResult: null,
lastExecutedQuery: '',
isDirty: false,
}))
}, [])
@ -639,6 +642,7 @@ GO`,
editorContent: '',
executionResult: null,
tableColumns: null,
lastExecutedQuery: '',
isDirty: false,
}))
}, [])
@ -671,7 +675,13 @@ GO`,
// Seçili metni koru
const savedSelection = editorRef.current?.preserveSelection()
setState((prev) => ({ ...prev, isExecuting: true, executionResult: null, tableColumns: null }))
setState((prev) => ({
...prev,
isExecuting: true,
executionResult: null,
tableColumns: null,
lastExecutedQuery: queryToExecute,
}))
try {
const result = await sqlObjectManagerService.executeQuery({
@ -1120,6 +1130,34 @@ GO`,
}
}
const handleResultsMutation = async (mutationQuery: string) => {
if (!state.selectedDataSource || !state.lastExecutedQuery) return
try {
const mutationResult = await sqlObjectManagerService.executeQuery({
queryText: mutationQuery,
dataSourceCode: state.selectedDataSource,
})
if (!mutationResult.data.success) {
throw new Error(mutationResult.data.error || mutationResult.data.message)
}
const refreshedResult = await sqlObjectManagerService.executeQuery({
queryText: state.lastExecutedQuery,
dataSourceCode: state.selectedDataSource,
})
setState((prev) => ({ ...prev, executionResult: refreshedResult.data }))
} catch (error: any) {
toast.push(
<Notification type="danger" title={translate('::App.Platform.Error')}>
{error.response?.data?.error?.message || error.message || 'Record operation failed.'}
</Notification>,
{ placement: 'top-center' },
)
throw error
}
}
const handlePreviewSqlDataFile = async (file: SqlDataExplorerEntry) => {
if (!file.relativePath || isPreviewingSqlDataFile) {
return
@ -1240,7 +1278,10 @@ GO`,
)
return (
<Container className="flex flex-col overflow-hidden" style={{ height: 'calc(100vh - 130px)' }}>
<Container
className="flex flex-col overflow-x-hidden overflow-y-auto lg:overflow-hidden"
style={{ height: 'calc(100vh - 130px)' }}
>
<Helmet
titleTemplate={`%s | ${APP_NAME}`}
title={translate('::' + 'App.SqlQueryManager')}
@ -1320,7 +1361,7 @@ GO`,
icon={<FaPlay />}
onClick={handleExecute}
loading={state.isExecuting}
disabled={!state.selectedDataSource}
disabled={!state.selectedDataSource || !state.editorContent.trim()}
className="shadow-sm px-2 py-1"
>
{translate('::App.Platform.Execute')}
@ -1333,7 +1374,7 @@ GO`,
{/* Main Content Area */}
<div className="flex-1 flex min-h-0 flex-col gap-3 lg:flex-row lg:gap-4">
{/* Left Panel - Object Explorer */}
<div className="w-full lg:w-1/3 flex-shrink-0 flex flex-col h-[35vh] min-h-[260px] max-h-[420px] lg:h-auto lg:min-h-0 lg:max-h-none bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-600 shadow">
<div className="w-full lg:w-1/3 flex-shrink-0 flex flex-col h-[220px] min-h-[220px] max-h-[260px] sm:h-[260px] sm:min-h-[260px] sm:max-h-[300px] lg:h-auto lg:min-h-0 lg:max-h-none bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-600 shadow">
<div className="border-b px-4 py-2 bg-gray-50 dark:bg-gray-800 flex-shrink-0 rounded-t-lg">
<h6 className="font-semibold text-sm">
{translate('::App.Platform.ObjectExplorer')}
@ -1355,7 +1396,9 @@ GO`,
</div>
{/* Center Panel - Editor and Results */}
<div className="flex-1 flex flex-col min-h-0">
<div
className={`flex flex-1 flex-col min-h-0 ${state.executionResult || state.tableColumns ? 'min-h-[620px] lg:min-h-0' : ''}`}
>
{state.executionResult || state.tableColumns ? (
<Splitter direction="vertical" initialSize={250} minSize={150} maxSize={1200}>
<div className="border rounded-lg shadow-sm bg-white dark:bg-gray-800 flex flex-col h-full">
@ -1405,7 +1448,13 @@ GO`,
</div>
</div>
<div className="flex-1 overflow-hidden p-2">
<SqlResultsGrid result={(state.executionResult || state.tableColumns)!} />
<SqlResultsGrid
result={(state.executionResult || state.tableColumns)!}
queryText={state.lastExecutedQuery}
dataSourceCode={state.selectedDataSource}
isPostgreSql={isPostgreSql}
onExecuteMutation={handleResultsMutation}
/>
</div>
</div>
</Splitter>
@ -1461,7 +1510,7 @@ GO`,
<Dialog.Body className="flex max-h-[90vh] min-h-[420px] flex-col gap-2">
<div className="mb-4 shrink-0">
<div>
<h5>{translate('::App.SqlQueryManager.MoveFiles')}</h5>
<h5>{translate('::App.SqlQueryManager.MoveFiles')}</h5>
</div>
</div>

View file

@ -1,45 +1,272 @@
import { useMemo } from 'react'
import { useEffect, useMemo, useState } from 'react'
import { DataGrid } from 'devextreme-react'
import { Column, Paging, Scrolling, SearchPanel, Export, Selection } from 'devextreme-react/data-grid'
import {
Column,
Paging,
Scrolling,
SearchPanel,
Export,
Selection,
} from 'devextreme-react/data-grid'
import { Button, Checkbox, DatePicker, Dialog } from '@/components/ui'
import type { SqlQueryExecutionResultDto } from '@/proxy/sql-query-manager/models'
import { useLocalization } from '@/utils/hooks/useLocalization'
import { FaTimesCircle } from 'react-icons/fa'
import DateTimepicker from '@/components/ui/DatePicker/DateTimepicker'
import dayjs from 'dayjs'
import { sqlObjectManagerService } from '@/services/sql-query-manager.service'
type ResultColumn = { name: string; dataType?: string; isNullable?: boolean }
interface SqlResultsGridProps {
result: SqlQueryExecutionResultDto
queryText?: string
dataSourceCode?: string | null
isPostgreSql?: boolean
onExecuteMutation?: (queryText: string) => Promise<void>
}
const SqlResultsGrid = ({ result }: SqlResultsGridProps) => {
const { translate } = useLocalization()
const getEditableTableName = (queryText?: string) => {
if (!queryText || /\b(join|union|intersect|except)\b/i.test(queryText)) return null
const columns = useMemo(() => {
// Get columns from metadata if available
const match = queryText.match(
/\bfrom\s+((?:\[[^\]]+\]|"[^"]+"|[\w$]+)(?:\s*\.\s*(?:\[[^\]]+\]|"[^"]+"|[\w$]+)){0,2})/i,
)
return match?.[1]?.replace(/\s+/g, '') || null
}
const getTableParts = (fullName: string, isPostgreSql: boolean) => {
const parts = fullName
.match(/\[[^\]]+\]|"[^"]+"|[\w$]+/g)
?.map((part) => part.replace(/^\[|\]$|^"|"$/g, ''))
if (!parts?.length) return null
return {
schemaName: parts.length > 1 ? parts[parts.length - 2] : isPostgreSql ? 'public' : 'dbo',
tableName: parts[parts.length - 1],
}
}
const quoteIdentifier = (name: string, isPostgreSql: boolean) =>
isPostgreSql ? `"${name.replace(/"/g, '""')}"` : `[${name.replace(/]/g, ']]')}]`
const isBooleanColumn = (column: ResultColumn) =>
/bit|bool/.test(column.dataType?.toLowerCase() || '')
const isNumberColumn = (column: ResultColumn) =>
/int|decimal|numeric|float|double|money|real/.test(column.dataType?.toLowerCase() || '')
const isDateColumn = (column: ResultColumn) =>
/date|time|timestamp/.test(column.dataType?.toLowerCase() || '') || /date|time/i.test(column.name)
const isDateTimeColumn = (column: ResultColumn) =>
/time|datetime|datetimeoffset|timestamp/.test(column.dataType?.toLowerCase() || '') ||
/time$/i.test(column.name)
const createNewId = () => {
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
return crypto.randomUUID()
}
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (character) => {
const random = Math.floor(Math.random() * 16)
const value = character === 'x' ? random : (random & 0x3) | 0x8
return value.toString(16)
})
}
const toSqlLiteral = (value: unknown, column: ResultColumn) => {
if (value === null || value === undefined || value === '') return 'NULL'
const dataType = column.dataType?.toLowerCase() || ''
if (dataType.includes('bit') || dataType.includes('bool')) {
return value === true || value === 'true' || value === 1 || value === '1' ? '1' : '0'
}
if (/int|decimal|numeric|float|double|money|real/.test(dataType)) {
const numberValue = Number(value)
if (Number.isFinite(numberValue)) return String(numberValue)
}
return `'${String(value).replace(/'/g, "''")}'`
}
const SqlResultsGrid = ({
result,
queryText,
dataSourceCode,
isPostgreSql = false,
onExecuteMutation,
}: SqlResultsGridProps) => {
const { translate } = useLocalization()
const [selectedRows, setSelectedRows] = useState<Record<string, unknown>[]>([])
const [focusedRow, setFocusedRow] = useState<Record<string, unknown> | null>(null)
const [focusedRowKey, setFocusedRowKey] = useState<string | number | null>(null)
const [editingRow, setEditingRow] = useState<Record<string, unknown> | null>(null)
const [isAdding, setIsAdding] = useState(false)
const [formValues, setFormValues] = useState<Record<string, string | boolean>>({})
const [isSaving, setIsSaving] = useState(false)
const [validationError, setValidationError] = useState('')
const [tableColumns, setTableColumns] = useState<ResultColumn[]>([])
const resultColumns = useMemo<ResultColumn[]>(() => {
if (result.metadata?.columns && Array.isArray(result.metadata.columns)) {
return result.metadata.columns
}
// Otherwise infer from data
if (result.data && result.data.length > 0) {
const firstRow = result.data[0]
return Object.keys(firstRow).map((key) => ({
name: key,
dataType: typeof firstRow[key],
return Object.keys(firstRow).map((name) => ({
name,
dataType: typeof firstRow[name],
isNullable: true,
}))
}
return []
}, [result])
const dataSource = useMemo(() => {
return result.data || []
}, [result])
const dataSource = useMemo(() => result.data || [], [result])
const tableName = useMemo(() => getEditableTableName(queryText), [queryText])
const tableParts = useMemo(
() => (tableName ? getTableParts(tableName, isPostgreSql) : null),
[isPostgreSql, tableName],
)
const columns = resultColumns.length > 0 ? resultColumns : tableColumns
const primaryKey = useMemo(
() => columns.find((column) => column.name.toLowerCase() === 'id'),
[columns],
)
const canModify = Boolean(tableName && primaryKey && onExecuteMutation)
useEffect(() => {
setTableColumns([])
if (resultColumns.length > 0 || !dataSourceCode || !tableParts) return
let isActive = true
sqlObjectManagerService
.getTableColumns(dataSourceCode, tableParts.schemaName, tableParts.tableName)
.then((response) => {
if (!isActive) return
setTableColumns(
response.data.map((column) => ({
name: column.columnName,
dataType: column.dataType,
isNullable: column.isNullable,
})),
)
})
.catch(() => {
if (isActive) setTableColumns([])
})
return () => {
isActive = false
}
}, [dataSourceCode, resultColumns.length, tableParts])
useEffect(() => {
const firstRow = dataSource[0]
const key = primaryKey && firstRow ? firstRow[primaryKey.name] : null
setFocusedRow(firstRow || null)
setFocusedRowKey(typeof key === 'string' || typeof key === 'number' ? key : null)
}, [dataSource, primaryKey])
const openAddDialog = () => {
setEditingRow(null)
setIsAdding(true)
const initialValues: Record<string, string | boolean> = {}
columns.filter(isBooleanColumn).forEach((column) => {
initialValues[column.name] = false
})
if (primaryKey) initialValues[primaryKey.name] = createNewId()
setFormValues(initialValues)
setValidationError('')
}
const openEditDialog = () => {
const row = focusedRow
if (!row) return
setEditingRow(row)
setIsAdding(false)
setFormValues(
Object.fromEntries(
columns.map((column) => [
column.name,
isBooleanColumn(column) ? Boolean(row[column.name]) : String(row[column.name] ?? ''),
]),
),
)
setValidationError('')
}
const closeDialog = () => {
if (isSaving) return
setEditingRow(null)
setIsAdding(false)
setFormValues({})
setValidationError('')
}
const executeMutation = async (sql: string) => {
if (!onExecuteMutation) return
setIsSaving(true)
try {
await onExecuteMutation(sql)
setSelectedRows([])
setFocusedRow(null)
setEditingRow(null)
setIsAdding(false)
setFormValues({})
} catch {
// The parent reports the execution error; keep the dialog open for correction.
} finally {
setIsSaving(false)
}
}
const saveRecord = async () => {
if (!tableName) return
const requiredColumns = columns.filter(
(column) => column.isNullable === false && (isAdding || column.name !== primaryKey?.name),
)
const missingRequiredColumn = requiredColumns.find((column) => {
const value = formValues[column.name]
return value === undefined || value === null || value === ''
})
if (missingRequiredColumn) {
setValidationError(`${missingRequiredColumn.name} is required.`)
return
}
if (isAdding) {
const populatedColumns = columns.filter(
(column) => formValues[column.name] !== '' && formValues[column.name] !== undefined,
)
if (populatedColumns.length === 0) return
const sql = `INSERT INTO ${tableName} (${populatedColumns.map((column) => quoteIdentifier(column.name, isPostgreSql)).join(', ')}) VALUES (${populatedColumns.map((column) => toSqlLiteral(formValues[column.name], column)).join(', ')});`
await executeMutation(sql)
return
}
if (!editingRow || !primaryKey) return
const editableColumns = columns.filter((column) => column.name !== primaryKey.name)
const sql = `UPDATE ${tableName} SET ${editableColumns.map((column) => `${quoteIdentifier(column.name, isPostgreSql)} = ${toSqlLiteral(formValues[column.name], column)}`).join(', ')} WHERE ${quoteIdentifier(primaryKey.name, isPostgreSql)} = ${toSqlLiteral(editingRow[primaryKey.name], primaryKey)};`
await executeMutation(sql)
}
const deleteSelectedRows = async () => {
if (!tableName || !primaryKey || selectedRows.length === 0) return
if (!window.confirm(`Delete ${selectedRows.length} selected record(s)?`)) return
const values = selectedRows
.map((row) => toSqlLiteral(row[primaryKey.name], primaryKey))
.join(', ')
await executeMutation(
`DELETE FROM ${tableName} WHERE ${quoteIdentifier(primaryKey.name, isPostgreSql)} IN (${values});`,
)
}
if (!result.success) {
return (
<div className="h-full flex flex-col">
<div className="flex items-center gap-2 mb-4 p-4 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded">
<FaTimesCircle className="text-red-500 dark:text-red-400 text-xl" />
<div className="flex h-full flex-col">
<div className="mb-4 flex items-center gap-2 rounded border border-red-200 bg-red-50 p-4 dark:border-red-800 dark:bg-red-900/20">
<FaTimesCircle className="text-xl text-red-500 dark:text-red-400" />
<div>
<div className="font-semibold text-red-700 dark:text-red-400">
{translate('::App.Platform.Error')}
@ -54,61 +281,116 @@ const SqlResultsGrid = ({ result }: SqlResultsGridProps) => {
}
return (
<div className="h-full flex flex-col">
{dataSource.length > 0 ? (
<div className="flex-1 overflow-hidden relative">
<div className="absolute inset-0">
<DataGrid
dataSource={dataSource}
showBorders={true}
showRowLines={true}
showColumnLines={true}
rowAlternationEnabled={true}
columnAutoWidth={true}
wordWrapEnabled={false}
allowColumnReordering={true}
allowColumnResizing={true}
columnResizingMode="widget"
height="100%"
>
<Scrolling mode="virtual" rowRenderingMode="virtual" />
<Paging enabled={true} pageSize={50} />
<SearchPanel visible={true} width={240} placeholder={translate('::App.Platform.Search')} />
<Export enabled={true} allowExportSelectedData={true} />
<Selection mode="multiple" showCheckBoxesMode="always" />
<div className="flex h-full flex-col">
{dataSource.length > 0 || columns.length > 0 ? (
<>
<div className="relative flex-1 overflow-hidden">
<div className="absolute inset-0">
<DataGrid
dataSource={dataSource}
elementAttr={{ class: 'sql-results-grid' }}
showBorders={true}
showRowLines={true}
showColumnLines={true}
rowAlternationEnabled={true}
columnAutoWidth={true}
wordWrapEnabled={false}
allowColumnReordering={true}
allowColumnResizing={true}
columnResizingMode="widget"
height="100%"
keyExpr={primaryKey?.name}
focusedRowEnabled={Boolean(primaryKey)}
focusedRowKey={focusedRowKey}
onSelectionChanged={(event) => setSelectedRows(event.selectedRowsData)}
onFocusedRowChanged={(event) => {
setFocusedRow(event.row?.data || null)
setFocusedRowKey(
typeof event.row?.key === 'string' || typeof event.row?.key === 'number'
? event.row.key
: null,
)
}}
onToolbarPreparing={(event) => {
const items = event.toolbarOptions.items || []
items.forEach((item) => {
if (item.name === 'searchPanel' || item.name === 'exportButton') {
item.location = 'after'
}
})
if (canModify) {
event.toolbarOptions.items = [
{
location: 'before',
widget: 'dxButton',
options: {
icon: 'add',
text: translate('::ListForms.ListForm.AddNewRecord'),
onClick: openAddDialog,
},
},
{
location: 'before',
widget: 'dxButton',
options: {
icon: 'edit',
text: translate('::App.Platform.Edit'),
disabled: !focusedRow,
onClick: openEditDialog,
},
},
{
location: 'before',
widget: 'dxButton',
options: {
icon: 'trash',
text: `Delete Selected (${selectedRows.length})`,
disabled: selectedRows.length === 0,
onClick: deleteSelectedRows,
},
},
...items,
]
}
}}
>
<Scrolling mode="virtual" rowRenderingMode="virtual" />
<Paging enabled={true} pageSize={50} />
<SearchPanel
visible={true}
width={240}
placeholder={translate('::App.Platform.Search')}
/>
<Export enabled={true} allowExportSelectedData={true} />
<Selection mode="multiple" showCheckBoxesMode="always" />
{columns.map((col, index) => (
<Column
key={col.name || index}
dataField={col.name}
caption={col.name}
dataType={
col.dataType?.toLowerCase().includes('int') ||
col.dataType?.toLowerCase().includes('decimal') ||
col.dataType?.toLowerCase().includes('numeric') ||
col.dataType?.toLowerCase().includes('float') ||
col.dataType?.toLowerCase().includes('money')
? 'number'
: col.dataType?.toLowerCase().includes('date') ||
col.dataType?.toLowerCase().includes('time')
? 'date'
: col.dataType?.toLowerCase().includes('bit') ||
col.dataType?.toLowerCase().includes('boolean')
? 'boolean'
: 'string'
}
allowSorting={true}
allowFiltering={true}
allowHeaderFiltering={true}
/>
))}
</DataGrid>
{columns.map((column, index) => (
<Column
key={column.name || index}
dataField={column.name}
caption={column.name}
dataType={
/int|decimal|numeric|float|money/.test(column.dataType?.toLowerCase() || '')
? 'number'
: /date|time/.test(column.dataType?.toLowerCase() || '')
? 'date'
: /bit|boolean/.test(column.dataType?.toLowerCase() || '')
? 'boolean'
: 'string'
}
allowSorting={true}
allowFiltering={true}
allowHeaderFiltering={true}
/>
))}
</DataGrid>
</div>
</div>
</div>
</>
) : (
<div className="flex-1 flex items-center justify-center text-gray-500">
<div className="flex flex-1 items-center justify-center text-gray-500">
<div className="text-center">
<div className="text-lg mb-2">{translate('::App.Platform.NoResults')}</div>
<div className="mb-2 text-lg">{translate('::App.Platform.NoResults')}</div>
<div className="text-sm">
{result.rowsAffected > 0
? translate('::App.Platform.RowCount', { count: result.rowsAffected })
@ -117,6 +399,100 @@ const SqlResultsGrid = ({ result }: SqlResultsGridProps) => {
</div>
</div>
)}
<Dialog
isOpen={isAdding || editingRow !== null}
onClose={closeDialog}
onRequestClose={closeDialog}
width={720}
>
<Dialog.Header>{isAdding ? 'Add New Record' : 'Edit Record'}</Dialog.Header>
<Dialog.Body className="mb-3 grid max-h-[65vh] grid-cols-1 gap-3 overflow-y-auto sm:grid-cols-2">
{columns.map((column) => {
const isPrimaryKey = column.name === primaryKey?.name
const required = column.isNullable === false
const value = formValues[column.name]
return (
<label key={column.name} className="flex min-w-0 flex-col gap-1 text-sm font-medium">
<span>
{column.name}
{required && <span className="ml-1 text-red-500">*</span>}
</span>
{isBooleanColumn(column) ? (
<Checkbox
checked={Boolean(value)}
disabled={!isAdding && isPrimaryKey}
onChange={(checked) =>
setFormValues((current) => ({ ...current, [column.name]: checked }))
}
>
{Boolean(value) ? 'True' : 'False'}
</Checkbox>
) : isDateColumn(column) ? (
isDateTimeColumn(column) ? (
<DateTimepicker
value={
typeof value === 'string' && dayjs(value).isValid()
? dayjs(value).toDate()
: null
}
disabled={!isAdding && isPrimaryKey}
inputFormat="YYYY-MM-DD HH:mm:ss"
onChange={(date) =>
setFormValues((current) => ({
...current,
[column.name]: date ? dayjs(date).format('YYYY-MM-DDTHH:mm:ss') : '',
}))
}
/>
) : (
<DatePicker
value={
typeof value === 'string' && dayjs(value).isValid()
? dayjs(value).toDate()
: null
}
disabled={!isAdding && isPrimaryKey}
inputFormat="YYYY-MM-DD"
onChange={(date) =>
setFormValues((current) => ({
...current,
[column.name]: date ? dayjs(date).format('YYYY-MM-DD') : '',
}))
}
/>
)
) : (
<input
type={isNumberColumn(column) ? 'number' : 'text'}
value={typeof value === 'boolean' ? String(value) : value || ''}
required={required}
disabled={!isAdding && isPrimaryKey}
className="h-9 rounded-md border border-gray-300 bg-white px-3 text-sm outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500 disabled:cursor-not-allowed disabled:bg-gray-100 dark:border-gray-600 dark:bg-gray-800 dark:disabled:bg-gray-700"
onChange={(event) =>
setFormValues((current) => ({
...current,
[column.name]: event.target.value,
}))
}
/>
)}
</label>
)
})}
</Dialog.Body>
<Dialog.Footer className="flex justify-end gap-2 border-t pt-3">
{validationError && (
<span className="mr-auto text-sm text-red-500">{validationError}</span>
)}
<Button size="sm" onClick={closeDialog} disabled={isSaving}>
Cancel
</Button>
<Button size="sm" variant="solid" loading={isSaving} onClick={saveRecord}>
Save
</Button>
</Dialog.Footer>
</Dialog>
</div>
)
}