2026-06-19 20:09:42 +00:00
|
|
|
import { useEffect, useMemo, useState } from 'react'
|
2026-02-24 20:44:16 +00:00
|
|
|
import { DataGrid } from 'devextreme-react'
|
2026-06-19 20:09:42 +00:00
|
|
|
import {
|
|
|
|
|
Column,
|
|
|
|
|
Paging,
|
|
|
|
|
Scrolling,
|
|
|
|
|
SearchPanel,
|
|
|
|
|
Export,
|
|
|
|
|
Selection,
|
|
|
|
|
} from 'devextreme-react/data-grid'
|
|
|
|
|
import { Button, Checkbox, DatePicker, Dialog } from '@/components/ui'
|
2026-02-24 20:44:16 +00:00
|
|
|
import type { SqlQueryExecutionResultDto } from '@/proxy/sql-query-manager/models'
|
|
|
|
|
import { useLocalization } from '@/utils/hooks/useLocalization'
|
|
|
|
|
import { FaTimesCircle } from 'react-icons/fa'
|
2026-06-19 20:09:42 +00:00
|
|
|
import DateTimepicker from '@/components/ui/DatePicker/DateTimepicker'
|
|
|
|
|
import dayjs from 'dayjs'
|
|
|
|
|
import { sqlObjectManagerService } from '@/services/sql-query-manager.service'
|
2026-08-20 12:01:10 +00:00
|
|
|
import Input from '@/components/ui/Input'
|
2026-06-19 20:09:42 +00:00
|
|
|
|
|
|
|
|
type ResultColumn = { name: string; dataType?: string; isNullable?: boolean }
|
2026-02-24 20:44:16 +00:00
|
|
|
|
|
|
|
|
interface SqlResultsGridProps {
|
|
|
|
|
result: SqlQueryExecutionResultDto
|
2026-06-19 20:09:42 +00:00
|
|
|
queryText?: string
|
|
|
|
|
dataSourceCode?: string | null
|
|
|
|
|
isPostgreSql?: boolean
|
|
|
|
|
onExecuteMutation?: (queryText: string) => Promise<void>
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const getEditableTableName = (queryText?: string) => {
|
|
|
|
|
if (!queryText || /\b(join|union|intersect|except)\b/i.test(queryText)) return null
|
|
|
|
|
|
|
|
|
|
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, "''")}'`
|
2026-02-24 20:44:16 +00:00
|
|
|
}
|
|
|
|
|
|
2026-06-19 20:09:42 +00:00
|
|
|
const SqlResultsGrid = ({
|
|
|
|
|
result,
|
|
|
|
|
queryText,
|
|
|
|
|
dataSourceCode,
|
|
|
|
|
isPostgreSql = false,
|
|
|
|
|
onExecuteMutation,
|
|
|
|
|
}: SqlResultsGridProps) => {
|
2026-02-24 20:44:16 +00:00
|
|
|
const { translate } = useLocalization()
|
2026-06-19 20:09:42 +00:00
|
|
|
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[]>([])
|
2026-02-24 20:44:16 +00:00
|
|
|
|
2026-06-19 20:09:42 +00:00
|
|
|
const resultColumns = useMemo<ResultColumn[]>(() => {
|
2026-02-24 20:44:16 +00:00
|
|
|
if (result.metadata?.columns && Array.isArray(result.metadata.columns)) {
|
|
|
|
|
return result.metadata.columns
|
|
|
|
|
}
|
|
|
|
|
if (result.data && result.data.length > 0) {
|
|
|
|
|
const firstRow = result.data[0]
|
2026-06-19 20:09:42 +00:00
|
|
|
return Object.keys(firstRow).map((name) => ({
|
|
|
|
|
name,
|
|
|
|
|
dataType: typeof firstRow[name],
|
2026-02-24 20:44:16 +00:00
|
|
|
isNullable: true,
|
|
|
|
|
}))
|
|
|
|
|
}
|
|
|
|
|
return []
|
|
|
|
|
}, [result])
|
|
|
|
|
|
2026-06-19 20:09:42 +00:00
|
|
|
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('')
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-06 09:44:26 +00:00
|
|
|
const normalizeBoolean = (value: unknown): boolean => {
|
|
|
|
|
return value === true || value === 'true' || value === 1 || value === '1'
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-19 20:09:42 +00:00
|
|
|
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});`,
|
|
|
|
|
)
|
|
|
|
|
}
|
2026-02-24 20:44:16 +00:00
|
|
|
|
|
|
|
|
if (!result.success) {
|
|
|
|
|
return (
|
2026-06-19 20:09:42 +00:00
|
|
|
<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" />
|
2026-02-24 20:44:16 +00:00
|
|
|
<div>
|
|
|
|
|
<div className="font-semibold text-red-700 dark:text-red-400">
|
|
|
|
|
{translate('::App.Platform.Error')}
|
|
|
|
|
</div>
|
|
|
|
|
<div className="text-sm text-red-600 dark:text-red-500">
|
|
|
|
|
{result.error || result.message}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
2026-06-19 20:09:42 +00:00
|
|
|
<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',
|
2026-08-14 14:03:34 +00:00
|
|
|
text: translate('::App.Platform.Add'),
|
2026-06-19 20:09:42 +00:00
|
|
|
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((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>
|
2026-02-24 20:44:16 +00:00
|
|
|
</div>
|
2026-06-19 20:09:42 +00:00
|
|
|
</>
|
2026-02-24 20:44:16 +00:00
|
|
|
) : (
|
2026-06-19 20:09:42 +00:00
|
|
|
<div className="flex flex-1 items-center justify-center text-gray-500">
|
2026-02-24 20:44:16 +00:00
|
|
|
<div className="text-center">
|
2026-06-19 20:09:42 +00:00
|
|
|
<div className="mb-2 text-lg">{translate('::App.Platform.NoResults')}</div>
|
2026-02-24 20:44:16 +00:00
|
|
|
<div className="text-sm">
|
|
|
|
|
{result.rowsAffected > 0
|
|
|
|
|
? translate('::App.Platform.RowCount', { count: result.rowsAffected })
|
|
|
|
|
: translate('::App.Platform.NoResultsReturned')}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2026-06-19 20:09:42 +00:00
|
|
|
|
|
|
|
|
<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 }))
|
|
|
|
|
}
|
|
|
|
|
>
|
2026-07-06 09:44:26 +00:00
|
|
|
{normalizeBoolean(value) ? 'True' : 'False'}
|
2026-06-19 20:09:42 +00:00
|
|
|
</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') : '',
|
|
|
|
|
}))
|
|
|
|
|
}
|
|
|
|
|
/>
|
|
|
|
|
)
|
|
|
|
|
) : (
|
2026-08-20 12:01:10 +00:00
|
|
|
<Input
|
|
|
|
|
unstyle
|
2026-06-19 20:09:42 +00:00
|
|
|
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}>
|
2026-08-14 14:03:34 +00:00
|
|
|
{translate('::App.Platform.Cancel')}
|
2026-06-19 20:09:42 +00:00
|
|
|
</Button>
|
|
|
|
|
<Button size="sm" variant="solid" loading={isSaving} onClick={saveRecord}>
|
2026-08-14 14:03:34 +00:00
|
|
|
{translate('::App.Platform.Save')}
|
2026-06-19 20:09:42 +00:00
|
|
|
</Button>
|
|
|
|
|
</Dialog.Footer>
|
|
|
|
|
</Dialog>
|
2026-02-24 20:44:16 +00:00
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default SqlResultsGrid
|