Sql Query Manager Add, Edit ve Delete butonları
This commit is contained in:
parent
947d21ecb7
commit
99e6f91b2a
3 changed files with 524 additions and 76 deletions
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue