import { useEffect, useMemo, useState } from 'react' import { DataGrid } from 'devextreme-react' 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' import Input from '@/components/ui/Input' type ResultColumn = { name: string; dataType?: string; isNullable?: boolean } interface SqlResultsGridProps { result: SqlQueryExecutionResultDto queryText?: string dataSourceCode?: string | null isPostgreSql?: boolean onExecuteMutation?: (queryText: string) => Promise } 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, "''")}'` } const SqlResultsGrid = ({ result, queryText, dataSourceCode, isPostgreSql = false, onExecuteMutation, }: SqlResultsGridProps) => { const { translate } = useLocalization() const [selectedRows, setSelectedRows] = useState[]>([]) const [focusedRow, setFocusedRow] = useState | null>(null) const [focusedRowKey, setFocusedRowKey] = useState(null) const [editingRow, setEditingRow] = useState | null>(null) const [isAdding, setIsAdding] = useState(false) const [formValues, setFormValues] = useState>({}) const [isSaving, setIsSaving] = useState(false) const [validationError, setValidationError] = useState('') const [tableColumns, setTableColumns] = useState([]) const resultColumns = useMemo(() => { 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] return Object.keys(firstRow).map((name) => ({ name, dataType: typeof firstRow[name], isNullable: true, })) } return [] }, [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 = {} columns.filter(isBooleanColumn).forEach((column) => { initialValues[column.name] = false }) if (primaryKey) initialValues[primaryKey.name] = createNewId() setFormValues(initialValues) setValidationError('') } const normalizeBoolean = (value: unknown): boolean => { return value === true || value === 'true' || value === 1 || value === '1' } 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 (
{translate('::App.Platform.Error')}
{result.error || result.message}
) } return (
{dataSource.length > 0 || columns.length > 0 ? ( <>
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('::App.Platform.Add'), 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, ] } }} > {columns.map((column, index) => ( ))}
) : (
{translate('::App.Platform.NoResults')}
{result.rowsAffected > 0 ? translate('::App.Platform.RowCount', { count: result.rowsAffected }) : translate('::App.Platform.NoResultsReturned')}
)} {isAdding ? 'Add New Record' : 'Edit Record'} {columns.map((column) => { const isPrimaryKey = column.name === primaryKey?.name const required = column.isNullable === false const value = formValues[column.name] return ( ) })} {validationError && ( {validationError} )}
) } export default SqlResultsGrid