From 84b9f6510787bd82f3797728fd675f755b4caa2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sedat=20=C3=96zt=C3=BCrk?= Date: Wed, 27 May 2026 15:36:46 +0300 Subject: [PATCH] Sql Query Manager -> Move to Host Data --- .../ISqlObjectManagerAppService.cs | 7 +- .../SqlDataFileDto.cs | 3 + .../SqlExecutionDto.cs | 7 + .../SqlObjectManagerAppService.cs | 134 +++++++- .../Seeds/LanguagesData.json | 36 ++- ui/src/services/sql-query-manager.service.ts | 21 +- ui/src/views/developerKit/SqlQueryManager.tsx | 288 ++++++++++++++++-- 7 files changed, 449 insertions(+), 47 deletions(-) diff --git a/api/modules/Sozsoft.SqlQueryManager/Sozsoft.SqlQueryManager.Application.Contracts/ISqlObjectManagerAppService.cs b/api/modules/Sozsoft.SqlQueryManager/Sozsoft.SqlQueryManager.Application.Contracts/ISqlObjectManagerAppService.cs index f9652ee..98bb408 100644 --- a/api/modules/Sozsoft.SqlQueryManager/Sozsoft.SqlQueryManager.Application.Contracts/ISqlObjectManagerAppService.cs +++ b/api/modules/Sozsoft.SqlQueryManager/Sozsoft.SqlQueryManager.Application.Contracts/ISqlObjectManagerAppService.cs @@ -44,5 +44,10 @@ public interface ISqlObjectManagerAppService : IApplicationService /// /// Lists .sql files currently available under DbMigrator Seeds/SqlData. /// - Task> GetSqlDataFilesAsync(); + Task> GetSqlDataFilesAsync(string dataDirectoryName = "SqlData", string relativePath = ""); + + /// + /// Moves a SQL seed file between the selected data directory root and HostData. + /// + Task MoveSqlDataFileAsync(MoveSqlDataFileDto input); } diff --git a/api/modules/Sozsoft.SqlQueryManager/Sozsoft.SqlQueryManager.Application.Contracts/SqlDataFileDto.cs b/api/modules/Sozsoft.SqlQueryManager/Sozsoft.SqlQueryManager.Application.Contracts/SqlDataFileDto.cs index 9aec930..3e64ead 100644 --- a/api/modules/Sozsoft.SqlQueryManager/Sozsoft.SqlQueryManager.Application.Contracts/SqlDataFileDto.cs +++ b/api/modules/Sozsoft.SqlQueryManager/Sozsoft.SqlQueryManager.Application.Contracts/SqlDataFileDto.cs @@ -5,5 +5,8 @@ namespace Sozsoft.SqlQueryManager.Application.Contracts; public class SqlDataFileDto { public string FileName { get; set; } = string.Empty; + public string Name { get; set; } = string.Empty; + public string RelativePath { get; set; } = string.Empty; + public bool IsDirectory { get; set; } public DateTime CreatedAt { get; set; } } diff --git a/api/modules/Sozsoft.SqlQueryManager/Sozsoft.SqlQueryManager.Application.Contracts/SqlExecutionDto.cs b/api/modules/Sozsoft.SqlQueryManager/Sozsoft.SqlQueryManager.Application.Contracts/SqlExecutionDto.cs index 83f4417..f7c22d1 100644 --- a/api/modules/Sozsoft.SqlQueryManager/Sozsoft.SqlQueryManager.Application.Contracts/SqlExecutionDto.cs +++ b/api/modules/Sozsoft.SqlQueryManager/Sozsoft.SqlQueryManager.Application.Contracts/SqlExecutionDto.cs @@ -46,3 +46,10 @@ public class DeleteSqlDataFilesDto /// public List FileNames { get; set; } = new(); } + +public class MoveSqlDataFileDto +{ + public string DataDirectoryName { get; set; } = string.Empty; + public string SourceRelativePath { get; set; } = string.Empty; + public string TargetRelativePath { get; set; } = string.Empty; +} diff --git a/api/modules/Sozsoft.SqlQueryManager/Sozsoft.SqlQueryManager.Application/SqlObjectManagerAppService.cs b/api/modules/Sozsoft.SqlQueryManager/Sozsoft.SqlQueryManager.Application/SqlObjectManagerAppService.cs index 4f6c545..f59868c 100644 --- a/api/modules/Sozsoft.SqlQueryManager/Sozsoft.SqlQueryManager.Application/SqlObjectManagerAppService.cs +++ b/api/modules/Sozsoft.SqlQueryManager/Sozsoft.SqlQueryManager.Application/SqlObjectManagerAppService.cs @@ -955,27 +955,48 @@ FROM ( } [HttpGet("api/app/sql-object-manager/sql-data-files")] - public Task> GetSqlDataFilesAsync() + public Task> GetSqlDataFilesAsync( + [FromQuery] string dataDirectoryName = "SqlData", + [FromQuery] string relativePath = "") { ValidateTenantAccess(); try { - var outputPath = ResolveSqlDataOutputPath(); + var rootPath = ResolveSqlDataOutputPath(dataDirectoryName); + var outputPath = ResolveSqlDataChildPath(rootPath, relativePath); if (!Directory.Exists(outputPath)) return Task.FromResult(new List()); + var directories = Directory.GetDirectories(outputPath, "*", SearchOption.TopDirectoryOnly) + .Where(d => string.Equals(Path.GetFileName(d), "HostData", StringComparison.OrdinalIgnoreCase)) + .Select(d => new SqlDataFileDto + { + FileName = Path.GetFileName(d)!, + Name = Path.GetFileName(d)!, + RelativePath = BuildSqlDataRelativePath(relativePath, Path.GetFileName(d)!), + IsDirectory = true, + CreatedAt = Directory.GetCreationTime(d) + }); + var files = Directory.GetFiles(outputPath, "*.sql", SearchOption.TopDirectoryOnly) .Select(f => new SqlDataFileDto { FileName = Path.GetFileName(f)!, + Name = Path.GetFileName(f)!, + RelativePath = BuildSqlDataRelativePath(relativePath, Path.GetFileName(f)!), + IsDirectory = false, CreatedAt = File.GetCreationTime(f) }) - .Where(x => !string.IsNullOrWhiteSpace(x.FileName)) - .OrderBy(x => x.FileName, StringComparer.OrdinalIgnoreCase) + .Where(x => !string.IsNullOrWhiteSpace(x.Name)); + + var entries = directories + .Concat(files) + .OrderByDescending(x => x.IsDirectory) + .ThenBy(x => x.Name, StringComparer.OrdinalIgnoreCase) .ToList(); - return Task.FromResult(files); + return Task.FromResult(entries); } catch (Exception ex) { @@ -984,24 +1005,121 @@ FROM ( } } + [HttpPost("api/app/sql-object-manager/move-sql-data-file")] + public Task MoveSqlDataFileAsync(MoveSqlDataFileDto input) + { + ValidateTenantAccess(); + + if (input == null) + throw new Volo.Abp.UserFriendlyException("Invalid move request."); + + var rootPath = ResolveSqlDataOutputPath(input.DataDirectoryName); + var sourcePath = ResolveSqlDataChildPath(rootPath, input.SourceRelativePath); + var targetPath = ResolveSqlDataChildPath(rootPath, input.TargetRelativePath); + + if (!File.Exists(sourcePath)) + throw new Volo.Abp.UserFriendlyException("Source file was not found."); + + if (!string.Equals(Path.GetExtension(sourcePath), ".sql", StringComparison.OrdinalIgnoreCase) || + !string.Equals(Path.GetExtension(targetPath), ".sql", StringComparison.OrdinalIgnoreCase)) + { + throw new Volo.Abp.UserFriendlyException("Only .sql files can be moved."); + } + + Directory.CreateDirectory(Path.GetDirectoryName(targetPath)!); + + if (File.Exists(targetPath)) + throw new Volo.Abp.UserFriendlyException("A file with the same name already exists in the target folder."); + + File.Move(sourcePath, targetPath); + _logger.LogInformation("SQL seed file moved from {SourcePath} to {TargetPath}", sourcePath, targetPath); + + return Task.CompletedTask; + } + private string ResolveSqlDataOutputPath() + { + return ResolveSqlDataOutputPath("SqlData"); + } + + private string ResolveSqlDataOutputPath(string dataDirectoryName) { const string dbMigratorName = "Sozsoft.Platform.DbMigrator"; + var safeDirectoryName = NormalizeSqlDataDirectoryName(dataDirectoryName); var dir = new DirectoryInfo(_hostEnvironment.ContentRootPath); while (dir != null) { var candidate = Path.Combine(dir.FullName, "src", dbMigratorName, "Seeds"); if (Directory.Exists(candidate)) - return Path.Combine(candidate, "SqlData"); + return Path.Combine(candidate, safeDirectoryName); candidate = Path.Combine(dir.FullName, dbMigratorName, "Seeds"); if (Directory.Exists(candidate)) - return Path.Combine(candidate, "SqlData"); + return Path.Combine(candidate, safeDirectoryName); dir = dir.Parent; } - return Path.Combine(_hostEnvironment.ContentRootPath, "Seeds", "SqlData"); + return Path.Combine(_hostEnvironment.ContentRootPath, "Seeds", safeDirectoryName); + } + + private static string NormalizeSqlDataDirectoryName(string dataDirectoryName) + { + return string.Equals(dataDirectoryName, "PostgresData", StringComparison.OrdinalIgnoreCase) + ? "PostgresData" + : "SqlData"; + } + + private static string ResolveSqlDataChildPath(string rootPath, string relativePath) + { + var normalized = NormalizeSqlDataRelativePath(relativePath); + var fullPath = Path.GetFullPath(Path.Combine(rootPath, normalized)); + var fullRoot = Path.GetFullPath(rootPath); + var fullRootWithSeparator = fullRoot.EndsWith(Path.DirectorySeparatorChar) + ? fullRoot + : fullRoot + Path.DirectorySeparatorChar; + + if (!string.Equals(fullPath, fullRoot, StringComparison.OrdinalIgnoreCase) && + !fullPath.StartsWith(fullRootWithSeparator, StringComparison.OrdinalIgnoreCase)) + { + throw new Volo.Abp.UserFriendlyException("Invalid path."); + } + + return fullPath; + } + + private static string NormalizeSqlDataRelativePath(string relativePath) + { + if (string.IsNullOrWhiteSpace(relativePath)) + return string.Empty; + + var normalized = relativePath.Replace('\\', '/').Trim('/'); + var parts = normalized.Split('/', StringSplitOptions.RemoveEmptyEntries); + + if (parts.Length == 0) + return string.Empty; + + if (parts.Any(p => p == "." || p == ".." || p.IndexOfAny(Path.GetInvalidFileNameChars()) >= 0)) + throw new Volo.Abp.UserFriendlyException("Invalid path."); + + var isRootFile = parts.Length == 1 && parts[0].EndsWith(".sql", StringComparison.OrdinalIgnoreCase); + var isHostDataFolder = parts.Length == 1 && string.Equals(parts[0], "HostData", StringComparison.OrdinalIgnoreCase); + var isHostDataFile = parts.Length == 2 && + string.Equals(parts[0], "HostData", StringComparison.OrdinalIgnoreCase) && + parts[1].EndsWith(".sql", StringComparison.OrdinalIgnoreCase); + + if (!isRootFile && !isHostDataFolder && !isHostDataFile) + throw new Volo.Abp.UserFriendlyException("Invalid path."); + + return Path.Combine(parts); + } + + private static string BuildSqlDataRelativePath(string parentRelativePath, string name) + { + if (string.IsNullOrWhiteSpace(parentRelativePath)) + return name; + + return $"{parentRelativePath.Trim('/', '\\')}/{name}"; } } diff --git a/api/src/Sozsoft.Platform.DbMigrator/Seeds/LanguagesData.json b/api/src/Sozsoft.Platform.DbMigrator/Seeds/LanguagesData.json index a5e5a69..95680ab 100644 --- a/api/src/Sozsoft.Platform.DbMigrator/Seeds/LanguagesData.json +++ b/api/src/Sozsoft.Platform.DbMigrator/Seeds/LanguagesData.json @@ -17782,15 +17782,45 @@ }, { "resourceName": "Platform", + "key": "App.SqlQueryManager.MoveFiles", + "en": "Move to Host Folder", + "tr": "Host Klasörüne Taşı" + }, + { + "resourceName": "Platform", + "key": "App.SqlQueryManager.MoveToHostData", + "en": "Move to Host Data", + "tr": "Host Klasörüne Taşı" + }, + { + "resourceName": "Platform", + "key": "App.SqlQueryManager.MoveOut", + "en": "Move Out", + "tr": "Dışarı Taşı" + }, + { + "resourceName": "Platform", + "key": "App.SqlQueryManager.NoIndexesDefined", + "en": "No indexes defined", + "tr": "Dizin tanımlanmamış" + }, + { + "resourceName": "Platform", "key": "App.SqlQueryManager.NoIndexesDefined", "en": "No indexes defined", "tr": "Dizin tanımlanmamış" }, { "resourceName": "Platform", - "key": "App.SqlQueryManager.IndexKey", - "en": "Index Key", - "tr": "Dizin Anahtarı" + "key": "App.Platform.OperationCompleted", + "en": "Operation completed successfully", + "tr": "İşlem başarıyla tamamlandı" + }, + { + "resourceName": "Platform", + "key": "App.SqlQueryManager.NoSqlDataFiles", + "en": "No SQL data files found", + "tr": "SQL veri dosyası bulunamadı" }, { "resourceName": "Platform", diff --git a/ui/src/services/sql-query-manager.service.ts b/ui/src/services/sql-query-manager.service.ts index e05ddf4..e0c6c37 100644 --- a/ui/src/services/sql-query-manager.service.ts +++ b/ui/src/services/sql-query-manager.service.ts @@ -79,11 +79,28 @@ export class SqlObjectManagerService { { apiName: this.apiName, ...config }, ) - getSqlDataFiles = (config?: Partial) => - apiService.fetchData<{ fileName: string; createdAt: string }[], void>( + getSqlDataFiles = (dataDirectoryName = 'SqlData', relativePath = '', config?: Partial) => + apiService.fetchData< + { fileName: string; name: string; relativePath: string; isDirectory: boolean; createdAt: string }[], + void + >( { method: 'GET', url: '/api/app/sql-object-manager/sql-data-files', + params: { dataDirectoryName, relativePath }, + }, + { apiName: this.apiName, ...config }, + ) + + moveSqlDataFile = ( + input: { dataDirectoryName: string; sourceRelativePath: string; targetRelativePath: string }, + config?: Partial, + ) => + apiService.fetchData( + { + method: 'POST', + url: '/api/app/sql-object-manager/move-sql-data-file', + data: input, }, { apiName: this.apiName, ...config }, ) diff --git a/ui/src/views/developerKit/SqlQueryManager.tsx b/ui/src/views/developerKit/SqlQueryManager.tsx index 700031c..955bbb9 100644 --- a/ui/src/views/developerKit/SqlQueryManager.tsx +++ b/ui/src/views/developerKit/SqlQueryManager.tsx @@ -1,5 +1,6 @@ import { useState, useCallback, useEffect, useRef } from 'react' -import { Button, Dialog, Notification, toast } from '@/components/ui' +import type { Dispatch, SetStateAction } from 'react' +import { Button, Checkbox, Dialog, Notification, toast } from '@/components/ui' import Container from '@/components/shared/Container' import ConfirmDialog from '@/components/shared/ConfirmDialog' import { getDataSources } from '@/services/data-source.service' @@ -7,7 +8,15 @@ import type { DataSourceDto } from '@/proxy/data-source' import { DataSourceTypeEnum } from '@/proxy/form/models' import type { SqlQueryExecutionResultDto } from '@/proxy/sql-query-manager/models' import { sqlObjectManagerService } from '@/services/sql-query-manager.service' -import { FaDatabase, FaPlay, FaFileAlt, FaCopy, FaExclamationTriangle } from 'react-icons/fa' +import { + FaDatabase, + FaPlay, + FaFileAlt, + FaCopy, + FaExclamationTriangle, + FaArrowLeft, + FaArrowRight, +} from 'react-icons/fa' import { FaCheckCircle } from 'react-icons/fa' import { useLocalization } from '@/utils/hooks/useLocalization' import SqlObjectExplorer, { type SqlExplorerSelectedObject } from './SqlObjectExplorer' @@ -20,7 +29,7 @@ import { useStoreState } from '@/store/store' import { APP_NAME } from '@/constants/app.constant' import { UiEvalService } from '@/services/UiEvalService' import { FcAcceptDatabase } from 'react-icons/fc' -import { FaFolderOpen } from "react-icons/fa" +import { FaFolderOpen } from 'react-icons/fa' interface SqlManagerState { dataSources: DataSourceDto[] @@ -42,6 +51,14 @@ interface SqlCopyResultItem { message: string } +interface SqlDataExplorerEntry { + fileName: string + name: string + relativePath: string + isDirectory: boolean + createdAt: string +} + const SqlQueryManager = () => { const { translate } = useLocalization() const editorRef = useRef(null) @@ -82,7 +99,11 @@ const SqlQueryManager = () => { const [sqlScriptForCopy, setSqlScriptForCopy] = useState('') const [showSqlDataFilesDialog, setShowSqlDataFilesDialog] = useState(false) const [isLoadingSqlDataFiles, setIsLoadingSqlDataFiles] = useState(false) - const [sqlDataFiles, setSqlDataFiles] = useState<{ fileName: string; createdAt: string }[]>([]) + const [isMovingSqlDataFile, setIsMovingSqlDataFile] = useState(false) + const [sqlDataRootFiles, setSqlDataRootFiles] = useState([]) + const [sqlDataHostFiles, setSqlDataHostFiles] = useState([]) + const [selectedRootSqlDataFiles, setSelectedRootSqlDataFiles] = useState([]) + const [selectedHostSqlDataFiles, setSelectedHostSqlDataFiles] = useState([]) useEffect(() => { loadDataSources() @@ -150,6 +171,7 @@ const SqlQueryManager = () => { (item) => item.code === state.selectedDataSource, )?.dataSourceType const isPostgreSql = selectedDataSourceType === DataSourceTypeEnum.Postgresql + const sqlDataDirectoryName = isPostgreSql ? 'PostgresData' : 'SqlData' const buildTableScriptQuery = (schemaName: string, tableName: string) => { const fullName = getSafeFullName(schemaName, tableName) @@ -990,15 +1012,24 @@ GO`, const copyErrorCount = copyResults.filter((x) => x.status === 'error').length const copySkippedCount = copyResults.filter((x) => x.status === 'skipped').length - const handleOpenSqlDataFilesDialog = async () => { - setShowSqlDataFilesDialog(true) + const loadSqlDataFiles = async () => { setIsLoadingSqlDataFiles(true) try { - const response = await sqlObjectManagerService.getSqlDataFiles() - setSqlDataFiles(response.data || []) + const [rootResponse, hostResponse] = await Promise.all([ + sqlObjectManagerService.getSqlDataFiles(sqlDataDirectoryName, ''), + sqlObjectManagerService.getSqlDataFiles(sqlDataDirectoryName, 'HostData'), + ]) + + setSqlDataRootFiles(normalizeSqlDataEntries(rootResponse.data || [], '')) + setSqlDataHostFiles(normalizeSqlDataEntries(hostResponse.data || [], 'HostData')) + setSelectedRootSqlDataFiles([]) + setSelectedHostSqlDataFiles([]) } catch (error: any) { - setSqlDataFiles([]) + setSqlDataRootFiles([]) + setSqlDataHostFiles([]) + setSelectedRootSqlDataFiles([]) + setSelectedHostSqlDataFiles([]) toast.push( {error.response?.data?.error?.message || @@ -1012,6 +1043,148 @@ GO`, } } + const handleOpenSqlDataFilesDialog = async () => { + setShowSqlDataFilesDialog(true) + await loadSqlDataFiles() + } + + const normalizeSqlDataEntries = ( + files: SqlDataExplorerEntry[], + parentRelativePath: '' | 'HostData', + ) => + files + .filter((file) => !file.isDirectory) + .map((file) => { + const fileName = file.name || file.fileName || file.relativePath?.split('/').pop() || '' + const relativePath = + file.relativePath || (parentRelativePath ? `${parentRelativePath}/${fileName}` : fileName) + + return { + ...file, + fileName, + name: fileName, + relativePath, + } + }) + .filter((file) => Boolean(file.name && file.relativePath)) + + const toggleSqlDataFileSelection = ( + setSelectedFiles: Dispatch>, + relativePath: string, + ) => { + setSelectedFiles((current) => + current.includes(relativePath) + ? current.filter((item) => item !== relativePath) + : [...current, relativePath], + ) + } + + const handleMoveSqlDataFiles = async (direction: 'toHostData' | 'toRoot') => { + const selectedRelativePaths = + direction === 'toHostData' ? selectedRootSqlDataFiles : selectedHostSqlDataFiles + + if (selectedRelativePaths.length === 0) { + return + } + + setIsMovingSqlDataFile(true) + + try { + for (const sourceRelativePath of selectedRelativePaths) { + const name = sourceRelativePath.split('/').pop() || sourceRelativePath + await sqlObjectManagerService.moveSqlDataFile({ + dataDirectoryName: sqlDataDirectoryName, + sourceRelativePath, + targetRelativePath: direction === 'toHostData' ? `HostData/${name}` : name, + }) + } + + await loadSqlDataFiles() + toast.push( + + {translate('::App.Platform.OperationCompleted') || 'Dosya tasindi.'} + , + { placement: 'top-center' }, + ) + } catch (error: any) { + toast.push( + + {error.response?.data?.error?.message || 'Dosya tasinamadi.'} + , + { placement: 'top-center' }, + ) + } finally { + setIsMovingSqlDataFile(false) + } + } + + const renderSqlDataPane = ( + title: string, + files: SqlDataExplorerEntry[], + selectedFiles: string[], + setSelectedFiles: Dispatch>, + ) => ( +
+
+
+
{title}
+ + {selectedFiles.length}/{files.length} + +
+ 0 && selectedFiles.length === files.length} + disabled={files.length === 0 || isMovingSqlDataFile} + onChange={(event) => + setSelectedFiles(event ? files.map((file) => file.relativePath) : []) + } + > + {translate('::FileManager.SelectAll')} + +
+
+ {files.length === 0 ? ( +

+ {translate('::App.SqlQueryManager.NoSqlDataFiles') || 'Dosya bulunamadi.'} +

+ ) : ( +
    + {files.map((file) => { + const selected = selectedFiles.includes(file.relativePath) + + return ( +
  • + +
  • + ) + })} +
+ )} +
+
+ ) + return ( } onClick={handleOpenSqlDataFilesDialog} className="shadow-sm px-2 py-1" - title={translate('::App.SqlQueryManager.SqlDataFiles') || 'Show SqlData files'} + title={translate('::App.SqlQueryManager.MoveFiles')} > - {translate('::App.SqlQueryManager.SqlDataFiles') || 'SqlData Files'} + {translate('::App.SqlQueryManager.MoveFiles')} @@ -1219,7 +1392,8 @@ GO`, }} >

- {translate('::App.DbMigrate.ConfirmMessage') || 'Are you sure you want to start the database migration process?'} + {translate('::App.DbMigrate.ConfirmMessage') || + 'Are you sure you want to start the database migration process?'}

@@ -1227,36 +1401,79 @@ GO`, isOpen={showSqlDataFilesDialog} onClose={() => setShowSqlDataFilesDialog(false)} onRequestClose={() => setShowSqlDataFilesDialog(false)} + width={1050} contentClassName="max-h-[90vh] overflow-hidden" > -
-
- {translate('::App.SqlQueryManager.SqlDataFiles') || 'SqlData Files'} -
+ +
+
+
{translate('::App.SqlQueryManager.MoveFiles')}
+
+
{isLoadingSqlDataFiles ? (

{translate('::App.Platform.Loading') || 'Loading...'}

- ) : sqlDataFiles.length === 0 ? ( -

- {translate('::App.SqlQueryManager.NoSqlDataFiles') || 'SqlData klasorunde .sql dosyasi bulunamadi.'} -

) : ( -
-
    - {sqlDataFiles.map((file) => ( -
  • - {file.fileName} - - {new Date(file.createdAt).toLocaleString()} - -
  • - ))} -
+
+ {renderSqlDataPane( + sqlDataDirectoryName, + sqlDataRootFiles, + selectedRootSqlDataFiles, + setSelectedRootSqlDataFiles, + )} + +
+
+ + {renderSqlDataPane( + 'HostData', + sqlDataHostFiles, + selectedHostSqlDataFiles, + setSelectedHostSqlDataFiles, + )}
)} -
+
+ + + + + {/* Template Confirmation Dialog */} @@ -1273,7 +1490,12 @@ GO`, -