WizardStep güncellemeleri

This commit is contained in:
Sedat Öztürk 2026-02-28 01:59:50 +03:00
parent f2652dbb44
commit 4b2fceb404
15 changed files with 833 additions and 242 deletions

View file

@ -1,6 +1,9 @@
namespace Sozsoft.Platform.ListForms; using System.Threading.Tasks;
namespace Sozsoft.Platform.ListForms;
public interface IListFormWizardAppService public interface IListFormWizardAppService
{ {
Task Create(WizardCreateInputDto input);
} }

View file

@ -0,0 +1,10 @@
using System.Collections.Generic;
namespace Sozsoft.Platform.ListForms;
public class WizardColumnGroupInputDto
{
public string Caption { get; set; }
public int ColCount { get; set; } = 2;
public List<WizardColumnItemInputDto> Items { get; set; } = new();
}

View file

@ -0,0 +1,14 @@
using System.Data;
namespace Sozsoft.Platform.ListForms;
public class WizardColumnItemInputDto
{
public string DataField { get; set; }
public string EditorType { get; set; }
public string EditorOptions { get; set; }
public string EditorScript { get; set; }
public int ColSpan { get; set; } = 1;
public bool IsRequired { get; set; }
public DbType DbSourceType { get; set; } = DbType.String;
}

View file

@ -1,4 +1,5 @@
using Sozsoft.Platform.Enums; using Sozsoft.Platform.Enums;
using System.Collections.Generic;
using System.Data; using System.Data;
namespace Sozsoft.Platform.ListForms; namespace Sozsoft.Platform.ListForms;
@ -27,5 +28,7 @@ public class WizardCreateInputDto
public string SelectCommand { get; set; } public string SelectCommand { get; set; }
public string KeyFieldName { get; set; } public string KeyFieldName { get; set; }
public DbType KeyFieldDbSourceType { get; set; } public DbType KeyFieldDbSourceType { get; set; }
public List<WizardColumnGroupInputDto> Groups { get; set; } = new();
} }

View file

@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text.Json; using System.Text.Json;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -12,11 +13,15 @@ using Volo.Abp.Identity;
using Volo.Abp.MultiTenancy; using Volo.Abp.MultiTenancy;
using Volo.Abp.PermissionManagement; using Volo.Abp.PermissionManagement;
using Volo.Abp.Uow; using Volo.Abp.Uow;
using static Sozsoft.Platform.PlatformConsts;
using System.Data;
using Sozsoft.Platform.Data.Seeds;
namespace Sozsoft.Platform.ListForms; namespace Sozsoft.Platform.ListForms;
public class ListFormWizardAppService( public class ListFormWizardAppService(
IRepository<ListForm, Guid> repoListForm, IRepository<ListForm, Guid> repoListForm,
IRepository<ListFormField, Guid> repoListFormField,
IRepository<DataSource, Guid> repoDataSource, IRepository<DataSource, Guid> repoDataSource,
IRepository<LanguageKey, Guid> repoLangKey, IRepository<LanguageKey, Guid> repoLangKey,
IRepository<LanguageText, Guid> repoLangText, IRepository<LanguageText, Guid> repoLangText,
@ -30,6 +35,7 @@ public class ListFormWizardAppService(
) : PlatformAppService(), IListFormWizardAppService ) : PlatformAppService(), IListFormWizardAppService
{ {
private readonly IRepository<ListForm, Guid> repoListForm = repoListForm; private readonly IRepository<ListForm, Guid> repoListForm = repoListForm;
private readonly IRepository<ListFormField, Guid> repoListFormField = repoListFormField;
private readonly IRepository<DataSource, Guid> repoDataSource = repoDataSource; private readonly IRepository<DataSource, Guid> repoDataSource = repoDataSource;
private readonly IRepository<LanguageKey, Guid> repoLangKey = repoLangKey; private readonly IRepository<LanguageKey, Guid> repoLangKey = repoLangKey;
private readonly IRepository<LanguageText, Guid> repoLangText = repoLangText; private readonly IRepository<LanguageText, Guid> repoLangText = repoLangText;
@ -84,6 +90,12 @@ public class ListFormWizardAppService(
var permExport = existingPerms.FirstOrDefault(a => a.Name == PlatformConsts.Wizard.PermExport(listFormCode)) ?? var permExport = existingPerms.FirstOrDefault(a => a.Name == PlatformConsts.Wizard.PermExport(listFormCode)) ??
await repoPerm.InsertAsync(new PermissionDefinitionRecord(Guid.NewGuid(), groupName, PlatformConsts.Wizard.PermExport(listFormCode), permRead.Name, PlatformConsts.Wizard.LangKeyExport, true, MultiTenancySides.Both), autoSave: false); await repoPerm.InsertAsync(new PermissionDefinitionRecord(Guid.NewGuid(), groupName, PlatformConsts.Wizard.PermExport(listFormCode), permRead.Name, PlatformConsts.Wizard.LangKeyExport, true, MultiTenancySides.Both), autoSave: false);
var permImport = existingPerms.FirstOrDefault(a => a.Name == PlatformConsts.Wizard.PermImport(listFormCode)) ??
await repoPerm.InsertAsync(new PermissionDefinitionRecord(Guid.NewGuid(), groupName, PlatformConsts.Wizard.PermImport(listFormCode), permRead.Name, PlatformConsts.Wizard.LangKeyImport, true, MultiTenancySides.Both), autoSave: false);
var PermNote = existingPerms.FirstOrDefault(a => a.Name == PlatformConsts.Wizard.PermNote(listFormCode)) ??
await repoPerm.InsertAsync(new PermissionDefinitionRecord(Guid.NewGuid(), groupName, PlatformConsts.Wizard.PermNote(listFormCode), permRead.Name, PlatformConsts.Wizard.LangKeyNote, true, MultiTenancySides.Both), autoSave: false);
// Permission Grants - Bulk Insert // Permission Grants - Bulk Insert
var adminUserName = PlatformConsts.AbpIdentity.User.AdminEmailDefaultValue; var adminUserName = PlatformConsts.AbpIdentity.User.AdminEmailDefaultValue;
var adminUser = await userRepository.FindByNormalizedUserNameAsync(lookupNormalizer.NormalizeName(adminUserName)); var adminUser = await userRepository.FindByNormalizedUserNameAsync(lookupNormalizer.NormalizeName(adminUserName));
@ -128,7 +140,7 @@ public class ListFormWizardAppService(
RequiredPermissionName = permRead.Name RequiredPermissionName = permRead.Name
}, autoSave: false); }, autoSave: false);
//Data Source //DataSource kodu ile iligli kod blogu
var dataSourceQueryable = await repoDataSource.GetQueryableAsync(); var dataSourceQueryable = await repoDataSource.GetQueryableAsync();
var dataSource = await AsyncExecuter.FirstOrDefaultAsync(dataSourceQueryable.Where(a => a.Code == input.DataSourceCode)); var dataSource = await AsyncExecuter.FirstOrDefaultAsync(dataSourceQueryable.Where(a => a.Code == input.DataSourceCode));
if (dataSource is null) if (dataSource is null)
@ -141,27 +153,88 @@ public class ListFormWizardAppService(
}, autoSave: false); }, autoSave: false);
} }
// Build EditingFormJson from wizard groups
var editingFormDtos = input.Groups
.Select((g, gi) => new EditingFormDto
{
Order = gi + 1,
Caption = g.Caption,
ColCount = g.ColCount,
ColSpan = g.ColCount,
ItemType = "group",
Items = g.Items
.Select((it, ii) => new EditingFormItemDto
{
Order = ii + 1,
DataField = it.DataField,
EditorType2 = it.EditorType,
ColSpan = it.ColSpan,
EditorOptions = string.IsNullOrWhiteSpace(it.EditorOptions) ? null : it.EditorOptions,
EditorScript = string.IsNullOrWhiteSpace(it.EditorScript) ? null : it.EditorScript,
IsRequired = it.IsRequired,
})
.ToArray()
})
.ToList();
//ListForm //ListForm
var listForm = await repoListForm.InsertAsync(new ListForm var listForm = await repoListForm.InsertAsync(new ListForm
{ {
ListFormType = ListFormTypeEnum.List,
PageSize = 10,
ExportJson = Wizard.DefaultExportJson,
IsSubForm = false,
ShowNote = true,
LayoutJson = Wizard.DefaultLayoutJson(),
CultureName = LanguageCodes.En,
ListFormCode = listFormCode, ListFormCode = listFormCode,
DataSourceCode = input.DataSourceCode,
Name = nameLangKey, Name = nameLangKey,
Title = titleLangKey, Title = titleLangKey,
CultureName = PlatformConsts.DefaultLanguage, DataSourceCode = input.DataSourceCode,
Description = PlatformConsts.Wizard.WizardKeyDesc(listFormCode), IsTenant = false,
IsBranch = false,
IsOrganizationUnit = false,
Description = Wizard.WizardKeyDesc(listFormCode),
SelectCommandType = input.SelectCommandType, SelectCommandType = input.SelectCommandType,
SelectCommand = input.SelectCommand, SelectCommand = input.SelectCommand,
KeyFieldName = input.KeyFieldName, KeyFieldName = input.KeyFieldName,
KeyFieldDbSourceType = input.KeyFieldDbSourceType, KeyFieldDbSourceType = input.KeyFieldDbSourceType,
PermissionJson = JsonSerializer.Serialize(new PermissionCrudDto DefaultFilter = Wizard.DefaultFilterJson,
{ SortMode = GridOptions.SortModeSingle,
C = permCreate.Name, FilterRowJson = Wizard.DefaultFilterRowJson,
R = permRead.Name, HeaderFilterJson = Wizard.DefaultHeaderFilterJson,
U = permUpdate.Name, SearchPanelJson = Wizard.DefaultSearchPanelJson,
D = permDelete.Name GroupPanelJson = JsonSerializer.Serialize(new { Visible = false }),
}), SelectionJson = Wizard.DefaultSelectionSingleJson,
ColumnOptionJson = Wizard.DefaultColumnOptionJson(),
PermissionJson = Wizard.DefaultPermissionJson(listFormCode),
DeleteCommand = Wizard.DefaultDeleteCommand(nameof(TableNameEnum.Country)),
DeleteFieldsDefaultValueJson = Wizard.DefaultDeleteFieldsDefaultValueJson(),
PagerOptionJson = Wizard.DefaultPagerOptionJson,
EditingOptionJson = Wizard.DefaultEditingOptionJson(listFormCode, 600, 500, true, true, true, true, false),
EditingFormJson = editingFormDtos.Count > 0 ? JsonSerializer.Serialize(editingFormDtos) : null,
}, autoSave: true); }, autoSave: true);
// ListFormField - each item in each group becomes a visible field record
var fieldOrder = 0;
foreach (var group in input.Groups)
{
foreach (var item in group.Items)
{
fieldOrder++;
await repoListFormField.InsertAsync(new ListFormField
{
ListFormCode = listFormCode,
FieldName = item.DataField,
CaptionName = item.DataField,
Visible = true,
IsActive = true,
ListOrderNo = fieldOrder,
SourceDbType = item.DbSourceType,
CultureName = PlatformConsts.DefaultLanguage,
}, autoSave: false);
}
}
} }
private async Task<LanguageKey> CreateLangKey(string key, string textEn, string textTr) private async Task<LanguageKey> CreateLangKey(string key, string textEn, string textTr)

View file

@ -1,4 +1,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Data;
using System.Text.Json;
using Sozsoft.Languages.Languages; using Sozsoft.Languages.Languages;
using Sozsoft.Platform.Enums; using Sozsoft.Platform.Enums;
using Volo.Abp.Reflection; using Volo.Abp.Reflection;
@ -414,14 +416,122 @@ public static class PlatformConsts
public static string PermUpdate(string code) => $"{WizardKey(code)}.Update"; public static string PermUpdate(string code) => $"{WizardKey(code)}.Update";
public static string PermDelete(string code) => $"{WizardKey(code)}.Delete"; public static string PermDelete(string code) => $"{WizardKey(code)}.Delete";
public static string PermExport(string code) => $"{WizardKey(code)}.Export"; public static string PermExport(string code) => $"{WizardKey(code)}.Export";
public static string PermImport(string code) => $"{WizardKey(code)}.Import";
public static string PermNote(string code) => $"{WizardKey(code)}.Note";
public static string LangKeyCreate => $"{Prefix.App}.Create"; public static string LangKeyCreate => $"{Prefix.App}.Create";
public static string LangKeyUpdate => $"{Prefix.App}.Update"; public static string LangKeyUpdate => $"{Prefix.App}.Update";
public static string LangKeyDelete => $"{Prefix.App}.Delete"; public static string LangKeyDelete => $"{Prefix.App}.Delete";
public static string LangKeyExport => $"{Prefix.App}.Export"; public static string LangKeyExport => $"{Prefix.App}.Export";
public static string LangKeyImport => $"{Prefix.App}.Import";
public static string LangKeyNote => $"{Prefix.App}.Note";
public static string MenuUrl(string code) => $"/list/{code}"; public static string MenuUrl(string code) => $"/admin/list/{code}";
public static string MenuIcon => "FcList"; public static string MenuIcon => "FcList";
public static readonly string DefaultExportJson = JsonSerializer.Serialize(new
{
Enabled = true,
AllowExportSelectedData = false,
PrintingEnabled = true,
BackgroundColor = "#FFFFFF",
Margin = 10
});
public static string DefaultLayoutJson(string DefaultLayout = "grid") => JsonSerializer.Serialize(new
{
Grid = true,
Pivot = true,
Chart = true,
Tree = true,
Gantt = true,
Scheduler = true,
DefaultLayout = DefaultLayout,
});
public static readonly string DefaultFilterJson = "\"IsDeleted\" = 'false'";
public static readonly string DefaultFilterRowJson = JsonSerializer.Serialize(new { Visible = true });
public static readonly string DefaultHeaderFilterJson = JsonSerializer.Serialize(new { Visible = true });
public static readonly string DefaultSearchPanelJson = JsonSerializer.Serialize(new { Visible = true });
public static readonly string DefaultGroupPanelJson = JsonSerializer.Serialize(new { Visible = true });
public static readonly string DefaultSelectionSingleJson = JsonSerializer.Serialize(new
{
Mode = GridOptions.SelectionModeNone,
AllowSelectAll = false
});
public static string DefaultColumnOptionJson(bool FocusedRowEnabled = true) => JsonSerializer.Serialize(new
{
ColumnFixingEnabled = true,
ColumnAutoWidth = true,
ColumnChooserEnabled = true,
AllowColumnResizing = true,
AllowColumnReordering = true,
ColumnResizingMode = "widget",
FocusedRowEnabled = FocusedRowEnabled,
});
public static string DefaultPermissionJson(string permissionName)
{
return JsonSerializer.Serialize(new
{
C = permissionName + ".Create",
R = permissionName,
U = permissionName + ".Update",
D = permissionName + ".Delete",
E = permissionName + ".Export",
I = permissionName + ".Import",
N = permissionName + ".Note",
});
}
public static string DefaultDeleteCommand(string tableName)
{
return $"UPDATE \"{TableNameResolver.GetFullTableName(tableName)}\" SET \"DeleterId\"=@DeleterId, \"DeletionTime\"=CURRENT_TIMESTAMP, \"IsDeleted\"='true' WHERE \"Id\"=@Id";
}
public static string DefaultDeleteFieldsDefaultValueJson(DbType dbType = DbType.Guid)
{
return JsonSerializer.Serialize(new[]
{
new { FieldName = "DeleterId", FieldDbType = DbType.Guid.ToString(), Value = "@USERID", CustomValueType = FieldCustomValueTypeEnum.CustomKey },
new { FieldName = "Id", FieldDbType = dbType.ToString(), Value = "@ID", CustomValueType = FieldCustomValueTypeEnum.CustomKey }
});
}
public static readonly string DefaultPagerOptionJson = JsonSerializer.Serialize(new
{
Visible = true,
AllowedPageSizes = "10,20,50,100",
ShowPageSizeSelector = true,
ShowNavigationButtons = true,
ShowInfo = false,
InfoText = "Page {0} of {1} ({2} items)",
DisplayMode = GridColumnOptions.PagerDisplayModeAdaptive,
ScrollingMode = GridColumnOptions.ScrollingModeStandard,
LoadPanelEnabled = "auto",
LoadPanelText = "Loading..."
});
public static string DefaultEditingOptionJson(
string Title,
int Width,
int Height,
bool AllowDeleting,
bool AllowAdding,
bool AllowEditing,
bool ConfirmDelete,
bool SendOnlyChangedFormValuesUpdate,
bool AllowDetail = false) => JsonSerializer.Serialize(new
{
Popup = new { Title = Title, Width = Width, Height = Height },
AllowDeleting = AllowDeleting,
AllowAdding = AllowAdding,
AllowEditing = AllowEditing,
ConfirmDelete = ConfirmDelete,
SendOnlyChangedFormValuesUpdate = SendOnlyChangedFormValuesUpdate,
AllowDetail = AllowDetail
});
} }
public static class AppErrorCodes public static class AppErrorCodes

View file

@ -15,6 +15,22 @@ import {
ChartValueAxisDto, ChartValueAxisDto,
} from '../charts/models' } from '../charts/models'
export interface ListFormWizardColumnItemDto {
dataField: string
editorType: string
editorOptions: string
editorScript: string
colSpan: number
isRequired: boolean
dbSourceType: number
}
export interface ListFormWizardColumnGroupDto {
caption: string
colCount: number
items: ListFormWizardColumnItemDto[]
}
export interface ListFormWizardDto { export interface ListFormWizardDto {
listFormCode: string listFormCode: string
menuCode: string menuCode: string
@ -35,6 +51,7 @@ export interface ListFormWizardDto {
selectCommand: string selectCommand: string
keyFieldName: string keyFieldName: string
keyFieldDbSourceType: number keyFieldDbSourceType: number
groups?: ListFormWizardColumnGroupDto[]
} }
export interface ListFormJsonRowDto { export interface ListFormJsonRowDto {

View file

@ -23,10 +23,11 @@ import WizardStep1, {
filterNonLinkNodes, filterNonLinkNodes,
findRootCode, findRootCode,
} from './WizardStep1' } from './WizardStep1'
import WizardStep2, { sqlDataTypeToDbType } from './WizardStep2' import WizardStep2 from './WizardStep2'
import WizardStep3, { WizardGroup } from './WizardStep3' import WizardStep3, { WizardGroup } from './WizardStep3'
import WizardStep4 from './WizardStep4' import WizardStep4 from './WizardStep4'
import { Container } from '@/components/shared' import { Container } from '@/components/shared'
import { sqlDataTypeToDbType } from './edit/options'
// ─── Formik initial values & validation ────────────────────────────────────── // ─── Formik initial values & validation ──────────────────────────────────────
const initialValues: ListFormWizardDto = { const initialValues: ListFormWizardDto = {
@ -296,7 +297,25 @@ const Wizard = () => {
const handleDeploy = async () => { const handleDeploy = async () => {
if (!formikRef.current) throw new Error('Form bulunamadı') if (!formikRef.current) throw new Error('Form bulunamadı')
const values = formikRef.current.values const values = formikRef.current.values
await postListFormWizard({ ...values }) await postListFormWizard({
...values,
groups: editingGroups.map((g) => ({
caption: g.caption,
colCount: g.colCount,
items: g.items.map((item) => {
const col = selectCommandColumns.find((c) => c.columnName === item.dataField)
return {
dataField: item.dataField,
editorType: item.editorType,
editorOptions: item.editorOptions ?? '',
editorScript: item.editorScript ?? '',
colSpan: item.colSpan,
isRequired: item.isRequired,
dbSourceType: col ? sqlDataTypeToDbType(col.dataType) : 12, // 12 = DbType.String
}
}),
})),
})
toast.push( toast.push(
<Notification type="success" duration={2000}> <Notification type="success" duration={2000}>
{translate('::ListForms.FormBilgileriKaydedildi')} {translate('::ListForms.FormBilgileriKaydedildi')}

View file

@ -9,6 +9,7 @@ import { Field, FieldProps, FormikErrors, FormikTouched } from 'formik'
import { useEffect, useRef, useState } from 'react' import { useEffect, useRef, useState } from 'react'
import CreatableSelect from 'react-select/creatable' import CreatableSelect from 'react-select/creatable'
import { import {
FaArrowRight,
FaChevronDown, FaChevronDown,
FaChevronRight, FaChevronRight,
FaEdit, FaEdit,
@ -670,6 +671,15 @@ const WizardStep1 = ({
const [menuDialogParentCode, setMenuDialogParentCode] = useState('') const [menuDialogParentCode, setMenuDialogParentCode] = useState('')
const [menuDialogInitialOrder, setMenuDialogInitialOrder] = useState(999) const [menuDialogInitialOrder, setMenuDialogInitialOrder] = useState(999)
const step1Missing = [
!wizardName && 'Wizard Adı',
!values.menuParentCode && 'Menu Parent',
!values.permissionGroupName && 'İzin Grubu',
!values.languageTextMenuEn && 'Menü (EN)',
!values.languageTextMenuTr && 'Menü (TR)',
].filter(Boolean) as string[]
const step1CanGo = step1Missing.length === 0
return ( return (
<div className="pb-20"> <div className="pb-20">
{/* Wizard Name */} {/* Wizard Name */}
@ -848,9 +858,16 @@ const WizardStep1 = ({
</div> </div>
{/* ─── Fixed Footer ─────────────────────────────── */} {/* ─── Fixed Footer ─────────────────────────────── */}
<div className="fixed bottom-0 left-0 right-0 z-10 bg-white dark:bg-gray-900 border-t border-gray-200 dark:border-gray-700 px-6 py-3"> <div className="fixed bottom-0 left-0 right-0 z-10 bg-white dark:bg-gray-900 border-t border-gray-200 dark:border-gray-700 px-6 py-0 h-16 flex items-center">
<div className="max-w-sm mx-auto"> <div className="flex items-center gap-3 w-full">
<Button block variant="solid" type="button" onClick={onNext}> <div className="flex-1 flex items-center gap-2">
{!step1CanGo && (
<span className="text-xs text-amber-600 dark:text-amber-400 font-medium">
Zorunlu: {step1Missing.join(', ')}
</span>
)}
</div>
<Button variant="solid" type="button" icon={<FaArrowRight />} disabled={!step1CanGo} onClick={onNext}>
{translate('::Next') || 'Next'} {translate('::Next') || 'Next'}
</Button> </Button>
</div> </div>

View file

@ -1,37 +1,12 @@
import { Button, FormItem, Input, Select } from '@/components/ui' import { Button, FormItem, Input, Select } from '@/components/ui'
import { ListFormWizardDto } from '@/proxy/admin/list-form/models' import { ListFormWizardDto } from '@/proxy/admin/list-form/models'
import { DbTypeEnum, SelectCommandTypeEnum } from '@/proxy/form/models' import { SelectCommandTypeEnum } from '@/proxy/form/models'
import type { DatabaseColumnDto, SqlObjectExplorerDto } from '@/proxy/sql-query-manager/models' import type { DatabaseColumnDto, SqlObjectExplorerDto } from '@/proxy/sql-query-manager/models'
import { SelectBoxOption } from '@/types/shared' import { SelectBoxOption } from '@/types/shared'
import { dbSourceTypeOptions, selectCommandTypeOptions } from './edit/options' import { dbSourceTypeOptions, selectCommandTypeOptions, sqlDataTypeToDbType } from './edit/options'
import { Field, FieldProps, FormikErrors, FormikTouched } from 'formik' import { Field, FieldProps, FormikErrors, FormikTouched } from 'formik'
import CreatableSelect from 'react-select/creatable' import CreatableSelect from 'react-select/creatable'
import { FaArrowLeft, FaArrowRight } from 'react-icons/fa'
// ─── SQL dataType → DbTypeEnum mapper ────────────────────────────────────────
export function sqlDataTypeToDbType(sqlType: string): DbTypeEnum {
const t = sqlType
.toLowerCase()
.replace(/\s*\(.*\)/, '')
.trim()
if (['int', 'integer', 'int32'].includes(t)) return DbTypeEnum.Int32
if (['bigint', 'int64'].includes(t)) return DbTypeEnum.Int64
if (['smallint', 'int16'].includes(t)) return DbTypeEnum.Int16
if (['tinyint', 'byte'].includes(t)) return DbTypeEnum.Byte
if (['bit', 'boolean', 'bool'].includes(t)) return DbTypeEnum.Boolean
if (['float', 'real', 'double', 'double precision'].includes(t)) return DbTypeEnum.Double
if (['decimal', 'numeric', 'money', 'smallmoney'].includes(t)) return DbTypeEnum.Decimal
if (['uniqueidentifier'].includes(t)) return DbTypeEnum.Guid
if (['datetime2', 'smalldatetime', 'datetime'].includes(t)) return DbTypeEnum.DateTime
if (['date'].includes(t)) return DbTypeEnum.Date
if (['time'].includes(t)) return DbTypeEnum.Time
if (['datetimeoffset'].includes(t)) return DbTypeEnum.DateTimeOffset
if (['nvarchar', 'varchar', 'char', 'nchar', 'text', 'ntext', 'string'].includes(t))
return DbTypeEnum.String
if (['xml'].includes(t)) return DbTypeEnum.Xml
if (['binary', 'varbinary', 'image'].includes(t)) return DbTypeEnum.Binary
return DbTypeEnum.String
}
// ─── Props ──────────────────────────────────────────────────────────────────── // ─── Props ────────────────────────────────────────────────────────────────────
@ -86,6 +61,15 @@ const WizardStep2 = ({
onBack, onBack,
onNext, onNext,
}: WizardStep2Props) => { }: WizardStep2Props) => {
const step2Missing = [
!values.listFormCode && 'ListForm Code',
!values.dataSourceCode && 'Veri Kaynağı',
!values.selectCommand && 'Select Command',
!values.keyFieldName && 'Key Field',
selectedColumns.size === 0 && 'Sütun seçimi',
].filter(Boolean) as string[]
const step2CanGo = step2Missing.length === 0
return ( return (
<div className="pb-20"> <div className="pb-20">
{/* ListForm Code + Data Source */} {/* ListForm Code + Data Source */}
@ -445,14 +429,21 @@ const WizardStep2 = ({
</FormItem> </FormItem>
{/* ─── Fixed Footer ─────────────────────────────── */} {/* ─── Fixed Footer ─────────────────────────────── */}
<div className="fixed bottom-0 left-0 right-0 z-10 bg-white dark:bg-gray-900 border-t border-gray-200 dark:border-gray-700 px-6 py-3"> <div className="fixed bottom-0 left-0 right-0 z-10 bg-white dark:bg-gray-900 border-t border-gray-200 dark:border-gray-700 px-6 py-0 h-16 flex items-center">
<div className="max-w-sm mx-auto flex gap-3"> <div className="flex items-center gap-3 w-full">
<Button block variant="default" type="button" onClick={onBack}> <Button variant="default" type="button" icon={<FaArrowLeft />} onClick={onBack}>
{translate('::Back') || 'Back'} {translate('::Back') || 'Back'}
</Button> </Button>
<Button block variant="solid" type="button" onClick={onNext}> <div className="flex-1 flex items-center justify-end gap-3">
{translate('::Next') || 'Next'} {!step2CanGo && (
</Button> <span className="text-xs text-amber-600 dark:text-amber-400 font-medium">
Zorunlu: {step2Missing.join(', ')}
</span>
)}
<Button variant="solid" type="button" icon={<FaArrowRight />} disabled={!step2CanGo} onClick={onNext}>
{translate('::Next') || 'Next'}
</Button>
</div>
</div> </div>
</div> </div>
</div> </div>

View file

@ -15,7 +15,7 @@ import {
import { SortableContext, arrayMove, useSortable, rectSortingStrategy } from '@dnd-kit/sortable' import { SortableContext, arrayMove, useSortable, rectSortingStrategy } from '@dnd-kit/sortable'
import { CSS } from '@dnd-kit/utilities' import { CSS } from '@dnd-kit/utilities'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { FaGripVertical, FaPlus, FaTimes, FaTrash, FaArrowRight, FaCode } from 'react-icons/fa' import { FaArrowLeft, FaGripVertical, FaPlus, FaTimes, FaTrash, FaArrowRight, FaCode } from 'react-icons/fa'
// ─── Types ──────────────────────────────────────────────────────────────────── // ─── Types ────────────────────────────────────────────────────────────────────
export interface WizardGroupItem { export interface WizardGroupItem {
@ -612,6 +612,15 @@ const WizardStep3 = ({
return null return null
} }
const hasNoGroups = groups.length === 0
const hasEmptyGroup = groups.some((g) => g.items.length === 0)
const canProceed = !hasNoGroups && !hasEmptyGroup
const validationMsg = hasNoGroups
? 'En az bir grup eklemelisiniz.'
: hasEmptyGroup
? 'Her gruba en az bir sütun eklemelisiniz.'
: ''
return ( return (
<DndContext <DndContext
sensors={sensors} sensors={sensors}
@ -740,23 +749,22 @@ const WizardStep3 = ({
</Dialog> </Dialog>
{/* ── Fixed Footer ─────────────────────────────────────────────────── */} {/* ── Fixed Footer ─────────────────────────────────────────────────── */}
<div className="fixed bottom-0 left-0 right-0 z-10 bg-white dark:bg-gray-900 border-t border-gray-200 dark:border-gray-700 px-6 py-3"> <div className="fixed bottom-0 left-0 right-0 z-10 bg-white dark:bg-gray-900 border-t border-gray-200 dark:border-gray-700 px-6 py-0 h-16 flex items-center">
<div className="max-w-sm mx-auto flex gap-3"> <div className="flex items-center gap-3 w-full">
<Button <Button variant="default" type="button" icon={<FaCode />} onClick={() => setIsHelperOpen(true)}>
block
variant="default"
type="button"
onClick={() => setIsHelperOpen(true)}
title="Helper Codes"
>
{translate('::Helper Codes') || 'Helper Codes'} {translate('::Helper Codes') || 'Helper Codes'}
</Button> </Button>
<Button block variant="default" type="button" onClick={onBack}> <Button variant="default" type="button" icon={<FaArrowLeft />} onClick={onBack}>
{translate('::Back') || 'Back'} {translate('::Back') || 'Back'}
</Button> </Button>
<Button block variant="solid" type="button" onClick={onNext}> <div className="flex-1 flex items-center justify-end gap-3">
{translate('::Next') || 'Next'} {!canProceed && (
</Button> <span className="text-xs text-amber-600 dark:text-amber-400 font-medium"> {validationMsg}</span>
)}
<Button variant="solid" type="button" icon={<FaArrowRight />} disabled={!canProceed} onClick={onNext}>
{translate('::Next') || 'Next'}
</Button>
</div>
</div> </div>
</div> </div>
</DndContext> </DndContext>

View file

@ -4,6 +4,7 @@ import type { DatabaseColumnDto } from '@/proxy/sql-query-manager/models'
import type { WizardGroup } from './WizardStep3' import type { WizardGroup } from './WizardStep3'
import { useState } from 'react' import { useState } from 'react'
import { import {
FaArrowLeft,
FaCheckCircle, FaCheckCircle,
FaChevronDown, FaChevronDown,
FaChevronRight, FaChevronRight,
@ -12,6 +13,7 @@ import {
FaRocket, FaRocket,
FaSpinner, FaSpinner,
} from 'react-icons/fa' } from 'react-icons/fa'
import { selectCommandTypeOptions } from './edit/options'
// ─── Types ──────────────────────────────────────────────────────────────────── // ─── Types ────────────────────────────────────────────────────────────────────
@ -37,15 +39,30 @@ interface LogEntry {
// ─── Deploy log steps ───────────────────────────────────────────────────────── // ─── Deploy log steps ─────────────────────────────────────────────────────────
function buildLogSteps(values: ListFormWizardDto, groups: WizardGroup[]): Omit<LogEntry, 'status'>[] { function buildLogSteps(
values: ListFormWizardDto,
groups: WizardGroup[],
): Omit<LogEntry, 'status'>[] {
const totalFields = groups.reduce((acc, g) => acc + g.items.length, 0) const totalFields = groups.reduce((acc, g) => acc + g.items.length, 0)
return [ return [
{ id: 1, label: 'Konfigürasyon doğrulanıyor…' }, { id: 1, label: 'Konfigürasyon doğrulanıyor…' },
{ id: 2, label: `Menü oluşturuluyor: ${values.menuCode}`, detail: `Parent: ${values.menuParentCode}` }, {
{ id: 3, label: 'Dil metinleri kaydediliyor', detail: `EN: ${values.languageTextMenuEn} / TR: ${values.languageTextMenuTr}` }, id: 2,
label: `Menü oluşturuluyor: ${values.menuCode}`,
detail: `Parent: ${values.menuParentCode}`,
},
{
id: 3,
label: 'Dil metinleri kaydediliyor',
detail: `EN: ${values.languageTextMenuEn} / TR: ${values.languageTextMenuTr}`,
},
{ id: 4, label: `İzin grubu yapılandırılıyor: ${values.permissionGroupName}` }, { id: 4, label: `İzin grubu yapılandırılıyor: ${values.permissionGroupName}` },
{ id: 5, label: `Veri kaynağı bağlanıyor: ${values.dataSourceCode}` }, { id: 5, label: `Veri kaynağı bağlanıyor: ${values.dataSourceCode}` },
{ id: 6, label: `ListForm oluşturuluyor: ${values.listFormCode}`, detail: `Key: ${values.keyFieldName}` }, {
id: 6,
label: `ListForm oluşturuluyor: ${values.listFormCode}`,
detail: `Key: ${values.keyFieldName}`,
},
{ id: 7, label: `Form grupları kaydediliyor (${groups.length} grup, ${totalFields} alan)` }, { id: 7, label: `Form grupları kaydediliyor (${groups.length} grup, ${totalFields} alan)` },
{ id: 8, label: 'Sunucuya deploy ediliyor…' }, { id: 8, label: 'Sunucuya deploy ediliyor…' },
{ id: 9, label: 'Tamamlandı ✓' }, { id: 9, label: 'Tamamlandı ✓' },
@ -101,13 +118,14 @@ function Row({ label, value }: { label: string; value?: string | number }) {
return ( return (
<div className="flex gap-2 py-1 border-b border-gray-100 dark:border-gray-800 last:border-0"> <div className="flex gap-2 py-1 border-b border-gray-100 dark:border-gray-800 last:border-0">
<span className="text-xs text-gray-400 w-40 shrink-0">{label}</span> <span className="text-xs text-gray-400 w-40 shrink-0">{label}</span>
<span className="text-xs text-gray-700 dark:text-gray-200 font-medium break-all">{value}</span> <span className="text-xs text-gray-700 dark:text-gray-200 font-medium break-all">
{value}
</span>
</div> </div>
) )
} }
// ─── WizardStep4 ────────────────────────────────────────────────────────────── // ─── WizardStep4 ──────────────────────────────────────────────────────────────
const WizardStep4 = ({ const WizardStep4 = ({
values, values,
wizardName, wizardName,
@ -179,42 +197,43 @@ const WizardStep4 = ({
const totalFields = groups.reduce((acc, g) => acc + g.items.length, 0) const totalFields = groups.reduce((acc, g) => acc + g.items.length, 0)
return ( return (
<div className="flex gap-6 pb-24"> <div className="grid grid-cols-[3fr_2fr] gap-5 pb-24 items-start">
{/* ── Left: Summary ────────────────────────────────────────────── */} {/* ── Left: Summary ──────────────────────────────────────────── */}
<div className="flex-1 flex flex-col gap-3 overflow-y-auto max-h-[calc(100vh-220px)]"> <div className="flex flex-col gap-3">
<h6 className="text-sm font-bold text-gray-600 dark:text-gray-300 uppercase tracking-wide mb-1"> <div className="grid grid-cols-2 gap-3 items-start">
Özet <Section title="Menü Bilgileri">
</h6> <Row label="Wizard Adı" value={wizardName} />
<Row label="Menu Code" value={values.menuCode} />
<Row label="Menu Parent" value={values.menuParentCode} />
<Row label="İzin Grubu" value={values.permissionGroupName} />
<Row label="İkon" value={values.menuIcon} />
<Row label="Menü (TR)" value={values.languageTextMenuTr} />
<Row label="Menü (EN)" value={values.languageTextMenuEn} />
<Row label="Menü Parent (TR)" value={values.languageTextMenuParentTr} />
<Row label="Menü Parent (EN)" value={values.languageTextMenuParentEn} />
</Section>
{/* Step 1 Summary */} <Section title="ListForm Ayarları">
<Section title="Menü Bilgileri"> <Row label="ListForm Code" value={values.listFormCode} />
<Row label="Wizard Adı" value={wizardName} /> <Row label="Başlık (TR)" value={values.languageTextTitleTr} />
<Row label="Menu Code" value={values.menuCode} /> <Row label="Başlık (EN)" value={values.languageTextTitleEn} />
<Row label="Menu Parent" value={values.menuParentCode} /> <Row label="Açıklama (TR)" value={values.languageTextDescTr} />
<Row label="İzin Grubu" value={values.permissionGroupName} /> <Row label="Açıklama (EN)" value={values.languageTextDescEn} />
<Row label="İkon" value={values.menuIcon} /> <Row label="Veri Kaynağı" value={values.dataSourceCode} />
<Row label="Menü (TR)" value={values.languageTextMenuTr} /> <Row label="Connection String" value={values.dataSourceConnectionString} />
<Row label="Menü (EN)" value={values.languageTextMenuEn} /> <Row
<Row label="Menü Parent (TR)" value={values.languageTextMenuParentTr} /> label="Komut Tipi"
<Row label="Menü Parent (EN)" value={values.languageTextMenuParentEn} /> value={
</Section> selectCommandTypeOptions.find((o) => o.value === values.selectCommandType)?.label ||
values.selectCommandType
}
/>
<Row label="Select Command" value={values.selectCommand} />
<Row label="Key Field" value={values.keyFieldName} />
<Row label="Key Field Tipi" value={String(values.keyFieldDbSourceType)} />
</Section>
</div>
{/* Step 2 Summary */}
<Section title="ListForm Ayarları">
<Row label="ListForm Code" value={values.listFormCode} />
<Row label="Başlık (TR)" value={values.languageTextTitleTr} />
<Row label="Başlık (EN)" value={values.languageTextTitleEn} />
<Row label="Açıklama (TR)" value={values.languageTextDescTr} />
<Row label="Açıklama (EN)" value={values.languageTextDescEn} />
<Row label="Veri Kaynağı" value={values.dataSourceCode} />
<Row label="Connection String" value={values.dataSourceConnectionString} />
<Row label="Komut Tipi" value={values.selectCommandType} />
<Row label="Select Command" value={values.selectCommand} />
<Row label="Key Field" value={values.keyFieldName} />
<Row label="Key Field Tipi" value={String(values.keyFieldDbSourceType)} />
</Section>
{/* Columns */}
<Section title="Seçili Sütunlar" badge={selectedColumns.size}> <Section title="Seçili Sütunlar" badge={selectedColumns.size}>
<div className="flex flex-wrap gap-1.5"> <div className="flex flex-wrap gap-1.5">
{[...selectedColumns].map((col) => { {[...selectedColumns].map((col) => {
@ -226,7 +245,7 @@ const WizardStep4 = ({
> >
{col} {col}
{meta?.dataType && ( {meta?.dataType && (
<span className="text-[10px] text-indigo-400">{meta.dataType}</span> <span className="text-[10px] text-indigo-400 opacity-70">{meta.dataType}</span>
)} )}
</span> </span>
) )
@ -234,11 +253,15 @@ const WizardStep4 = ({
</div> </div>
</Section> </Section>
{/* Step 3 Groups */} <Section title="Form Grupları" badge={groups.length}>
<Section title="Form Grupları" badge={groups.length} defaultOpen={true}>
<div className="flex flex-col gap-3"> <div className="flex flex-col gap-3">
{groups.map((g) => ( {groups.map((g) => (
<Section key={g.id} title={g.caption || '(Grup)'} badge={`${g.items.length} alan · ${g.colCount} sütun`} defaultOpen={false}> <Section
key={g.id}
title={g.caption || '(Grup)'}
badge={`${g.items.length} alan · ${g.colCount} sütun`}
defaultOpen={false}
>
<div className="flex flex-col gap-0.5"> <div className="flex flex-col gap-0.5">
{g.items.length === 0 ? ( {g.items.length === 0 ? (
<span className="text-xs text-gray-300 italic">Alan yok</span> <span className="text-xs text-gray-300 italic">Alan yok</span>
@ -254,7 +277,7 @@ const WizardStep4 = ({
<span className="text-[10px] text-gray-400 bg-gray-100 dark:bg-gray-800 px-1.5 py-0.5 rounded"> <span className="text-[10px] text-gray-400 bg-gray-100 dark:bg-gray-800 px-1.5 py-0.5 rounded">
{item.editorType} {item.editorType}
</span> </span>
<span className="text-[10px] text-gray-400 ml-auto"> <span className="text-[10px] text-gray-400 ml-auto shrink-0">
span:{item.colSpan} span:{item.colSpan}
{item.isRequired && ( {item.isRequired && (
<span className="ml-1 text-red-400 font-semibold">*</span> <span className="ml-1 text-red-400 font-semibold">*</span>
@ -270,9 +293,9 @@ const WizardStep4 = ({
</Section> </Section>
</div> </div>
{/* ── Right: Deploy ────────────────────────────────────────────── */} {/* ── Right: Deploy ──────────────────────────────────────────── */}
<div className="w-96 shrink-0 flex flex-col gap-4"> <div className="sticky top-4 flex flex-col gap-3 max-h-[calc(100vh-200px)]">
{/* Stats bar */} {/* Stats */}
<div className="grid grid-cols-3 gap-2"> <div className="grid grid-cols-3 gap-2">
{[ {[
{ label: 'Grup', value: groups.length }, { label: 'Grup', value: groups.length },
@ -281,18 +304,23 @@ const WizardStep4 = ({
].map((s) => ( ].map((s) => (
<div <div
key={s.label} key={s.label}
className="rounded-xl border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800 p-3 text-center" className="rounded-xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-3 text-center shadow-sm"
> >
<div className="text-xl font-bold text-indigo-600 dark:text-indigo-400">{s.value}</div> <div className="text-2xl font-bold text-indigo-600 dark:text-indigo-400">
{s.value}
</div>
<div className="text-xs text-gray-400 mt-0.5">{s.label}</div> <div className="text-xs text-gray-400 mt-0.5">{s.label}</div>
</div> </div>
))} ))}
</div> </div>
{/* Log panel */} {/* Log panel — grows to fill remaining height */}
<div className="rounded-xl border border-gray-200 dark:border-gray-700 overflow-hidden flex flex-col flex-1"> <div className="rounded-xl border border-gray-200 dark:border-gray-700 overflow-hidden flex flex-col flex-1 min-h-0">
<div className="px-4 py-2.5 bg-gray-50 dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between"> <div className="px-4 py-2.5 bg-gray-50 dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between shrink-0">
<span className="text-sm font-semibold text-gray-700 dark:text-gray-200">Deploy Log</span> <span className="text-sm font-semibold text-gray-700 dark:text-gray-200 flex items-center gap-2">
<FaRocket className="text-indigo-400 text-xs" />
Deploy Log
</span>
{isDone && ( {isDone && (
<span className="text-xs text-emerald-500 font-semibold flex items-center gap-1"> <span className="text-xs text-emerald-500 font-semibold flex items-center gap-1">
<FaCheckCircle /> Başarılı <FaCheckCircle /> Başarılı
@ -305,26 +333,33 @@ const WizardStep4 = ({
)} )}
</div> </div>
<div className="flex-1 min-h-[280px] max-h-[calc(100vh-380px)] overflow-y-auto p-3 bg-gray-950 dark:bg-black font-mono"> <div className="flex-1 overflow-y-auto p-4 bg-[#0d1117] dark:bg-black font-mono min-h-[360px]">
{logs.length === 0 ? ( {logs.length === 0 ? (
<div className="text-xs text-gray-500 italic py-4 text-center select-none"> <div className="flex flex-col items-center justify-center h-full gap-3 py-10 select-none">
Deploy başlatmak için butona tıklayın <FaRocket className="text-gray-700 text-3xl" />
<span className="text-xs text-gray-600 italic text-center">
Tüm bilgiler hazır.
<br />
Deploy başlatmak için butona tıklayın.
</span>
</div> </div>
) : ( ) : (
<div className="flex flex-col gap-1.5"> <div className="flex flex-col gap-2">
{logs.map((log) => ( {logs.map((log) => (
<div key={log.id} className="flex items-start gap-2"> <div key={log.id} className="flex items-start gap-2.5">
<LogIcon status={log.status} /> <span className="mt-0.5 shrink-0">
<LogIcon status={log.status} />
</span>
<div> <div>
<span <span
className={`text-xs ${ className={`text-xs leading-relaxed ${
log.status === 'success' log.status === 'success'
? 'text-emerald-400' ? 'text-emerald-400'
: log.status === 'error' : log.status === 'error'
? 'text-red-400' ? 'text-red-400'
: log.status === 'running' : log.status === 'running'
? 'text-indigo-300' ? 'text-yellow-300'
: 'text-gray-500' : 'text-gray-600'
}`} }`}
> >
{log.label} {log.label}
@ -335,44 +370,35 @@ const WizardStep4 = ({
</div> </div>
</div> </div>
))} ))}
{isDone && (
<div className="mt-4 rounded-lg border border-emerald-800 bg-emerald-950/40 px-4 py-2.5 text-xs text-emerald-400 text-center font-semibold">
🎉 ListForm başarıyla oluşturuldu ve deploy edildi!
</div>
)}
</div> </div>
)} )}
</div> </div>
</div> </div>
{isDone && (
<div className="rounded-xl border border-emerald-200 dark:border-emerald-800 bg-emerald-50 dark:bg-emerald-900/20 p-4 text-sm text-emerald-700 dark:text-emerald-300 text-center font-medium">
🎉 ListForm başarıyla oluşturuldu ve deploy edildi!
</div>
)}
</div> </div>
{/* ── Fixed Footer ─────────────────────────────────────────────── */} {/* ── Fixed Footer ─────────────────────────────────────────────── */}
<div className="fixed bottom-0 left-0 right-0 z-10 bg-white dark:bg-gray-900 border-t border-gray-200 dark:border-gray-700 px-6 py-3"> <div className="fixed bottom-0 left-0 right-0 z-10 bg-white dark:bg-gray-900 border-t border-gray-200 dark:border-gray-700 px-6 py-0 h-16 flex items-center">
<div className="max-w-5xl mx-auto flex gap-3"> <div className="flex items-center gap-3 w-full">
<Button <Button variant="default" type="button" icon={<FaArrowLeft />} onClick={onBack} disabled={isDeploying}>
variant="default"
type="button"
onClick={onBack}
disabled={isDeploying}
>
{translate('::Back') || 'Back'} {translate('::Back') || 'Back'}
</Button> </Button>
<Button <div className="flex-1 flex items-center justify-end">
block <Button
variant="solid" variant="solid"
type="button" type="button"
icon={<FaRocket />} icon={<FaRocket />}
loading={isDeploying} loading={isDeploying}
disabled={isDeploying || isDone} disabled={isDeploying || isDone}
onClick={runDeploy} onClick={runDeploy}
> >
{isDeploying {isDeploying ? 'Deploy ediliyor…' : isDone ? '✓ Tamamlandı' : 'Deploy & Kaydet'}
? 'Deploy ediliyor…' </Button>
: isDone </div>
? 'Tamamlandı'
: translate('::Save') || 'Deploy & Save'}
</Button>
</div> </div>
</div> </div>
</div> </div>

View file

@ -8,9 +8,12 @@ import { Field, FieldProps, Form, Formik } from 'formik'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import * as Yup from 'yup' import * as Yup from 'yup'
import { FormEditProps } from './FormEdit' import { FormEditProps } from './FormEdit'
import { dbSourceTypeOptions, selectCommandTypeOptions } from './options' import { dbSourceTypeOptions, selectCommandTypeOptions, sqlDataTypeToDbType } from './options'
import { DataSourceTypeEnum, SelectCommandTypeEnum } from '@/proxy/form/models' import { DataSourceTypeEnum, SelectCommandTypeEnum } from '@/proxy/form/models'
import { getDataSources } from '@/services/data-source.service' import { getDataSources } from '@/services/data-source.service'
import { sqlObjectManagerService } from '@/services/sql-query-manager.service'
import type { DatabaseColumnDto, SqlObjectExplorerDto } from '@/proxy/sql-query-manager/models'
import CreatableSelect from 'react-select/creatable'
const schema = Yup.object().shape({ const schema = Yup.object().shape({
isOrganizationUnit: Yup.bool(), isOrganizationUnit: Yup.bool(),
@ -25,6 +28,10 @@ const schema = Yup.object().shape({
}) })
function FormTabDatabaseDataSource(props: FormEditProps) { function FormTabDatabaseDataSource(props: FormEditProps) {
const initialValues = useStoreState((s) => s.admin.lists.values)
const { translate } = useLocalization()
// ── Data Source List ────────────────────────────────────────────────────────
const [dataSourceList, setDataSourceList] = useState<SelectBoxOption[]>([]) const [dataSourceList, setDataSourceList] = useState<SelectBoxOption[]>([])
const getDataSourceList = async () => { const getDataSourceList = async () => {
const response = await getDataSources() const response = await getDataSources()
@ -41,11 +48,74 @@ function FormTabDatabaseDataSource(props: FormEditProps) {
getDataSourceList() getDataSourceList()
}, []) }, [])
const initialValues = useStoreState((s) => s.admin.lists.values) // ── DB Objects ──────────────────────────────────────────────────────────────
const [dbObjects, setDbObjects] = useState<SqlObjectExplorerDto | null>(null)
const [isLoadingDbObjects, setIsLoadingDbObjects] = useState(false)
// Mevcut kayıttaki dataSourceCode ile başlat
const [currentDataSource, setCurrentDataSource] = useState(initialValues?.dataSourceCode ?? '')
const loadDbObjects = async (dsCode: string) => {
if (!dsCode) {
setDbObjects(null)
return
}
setIsLoadingDbObjects(true)
try {
const res = await sqlObjectManagerService.getAllObjects(dsCode)
setDbObjects(res.data)
} catch {
setDbObjects(null)
} finally {
setIsLoadingDbObjects(false)
}
}
useEffect(() => {
loadDbObjects(currentDataSource)
}, [currentDataSource])
// ── Columns ─────────────────────────────────────────────────────────────────
const [selectCommandColumns, setSelectCommandColumns] = useState<DatabaseColumnDto[]>([])
const [isLoadingColumns, setIsLoadingColumns] = useState(false)
const loadColumns = async (dsCode: string, schema: string, name: string) => {
if (!dsCode || !name) {
setSelectCommandColumns([])
return
}
setIsLoadingColumns(true)
try {
const res = await sqlObjectManagerService.getTableColumns(dsCode, schema, name)
setSelectCommandColumns(res.data ?? [])
} catch {
setSelectCommandColumns([])
} finally {
setIsLoadingColumns(false)
}
}
// Mevcut selectCommand'a göre sütunları otomatik yükle (dbObjects hazır olunca)
useEffect(() => {
const dsCode = initialValues?.dataSourceCode
const cmd = initialValues?.selectCommand
if (!dbObjects || !dsCode || !cmd) return
// Daha önce yüklenmiş sütun varsa tekrar yükleme
if (selectCommandColumns.length > 0) return
const table = dbObjects.tables.find((t) => t.tableName === cmd)
if (table) { loadColumns(dsCode, table.schemaName, table.tableName); return }
const view = dbObjects.views.find((v) => v.viewName === cmd)
if (view) { loadColumns(dsCode, view.schemaName, view.viewName); return }
const fn = dbObjects.functions.find((f) => f.functionName === cmd)
if (fn) { loadColumns(dsCode, fn.schemaName, fn.functionName); return }
const sp = dbObjects.storedProcedures.find((p) => p.procedureName === cmd)
if (sp) { loadColumns(dsCode, sp.schemaName, sp.procedureName); return }
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [dbObjects])
if (!initialValues) { if (!initialValues) {
return null return null
} }
const { translate } = useLocalization()
return ( return (
<Container> <Container>
@ -116,34 +186,17 @@ function FormTabDatabaseDataSource(props: FormEditProps) {
value={dataSourceList?.filter( value={dataSourceList?.filter(
(option) => option.value === values.dataSourceCode, (option) => option.value === values.dataSourceCode,
)} )}
onChange={(option) => form.setFieldValue(field.name, option?.value)} onChange={(option) => {
/> const val = option?.value ?? ''
)} form.setFieldValue(field.name, val)
</Field> setCurrentDataSource(val)
</FormItem> // reset dependent fields
<FormItem form.setFieldValue('selectCommand', '')
label={translate('::ListForms.ListFormEdit.DatabaseDataSourceSelectCommandType')} form.setFieldValue('selectCommandType', undefined)
invalid={errors.selectCommandType && touched.selectCommandType} form.setFieldValue('keyFieldName', '')
errorMessage={errors.selectCommandType} form.setFieldValue('keyFieldDbSourceType', undefined)
> setSelectCommandColumns([])
<Field }}
type="text"
autoComplete="off"
name="selectCommandType"
placeholder={translate(
'::ListForms.ListFormEdit.DatabaseDataSourceSelectCommandType',
)}
>
{({ field, form }: FieldProps<SelectCommandTypeEnum>) => (
<Select
field={field}
form={form}
isClearable={true}
options={selectCommandTypeOptions}
value={selectCommandTypeOptions?.filter(
(option: any) => option.value === values.selectCommandType,
)}
onChange={(option) => form.setFieldValue(field.name, option?.value)}
/> />
)} )}
</Field> </Field>
@ -152,16 +205,111 @@ function FormTabDatabaseDataSource(props: FormEditProps) {
label={translate('::ListForms.ListFormEdit.DatabaseDataSourceSelectCommand')} label={translate('::ListForms.ListFormEdit.DatabaseDataSourceSelectCommand')}
invalid={errors.selectCommand && touched.selectCommand} invalid={errors.selectCommand && touched.selectCommand}
errorMessage={errors.selectCommand} errorMessage={errors.selectCommand}
extra={
values.selectCommandType != null ? (
<span className="ml-2 text-xs px-2 py-0.5 rounded bg-indigo-100 text-indigo-600 dark:bg-indigo-900/40 dark:text-indigo-300">
{selectCommandTypeOptions.find(
(o: any) => o.value === values.selectCommandType,
)?.label ?? String(values.selectCommandType)}
</span>
) : null
}
> >
<Field <Field type="text" autoComplete="off" name="selectCommand">
type="text" {({ field, form }: FieldProps<string>) => {
autoComplete="off" const grouped = dbObjects
name="selectCommand" ? [
placeholder={translate( {
'::ListForms.ListFormEdit.DatabaseDataSourceSelectCommand', label: 'Tables',
)} options: dbObjects.tables.map((t) => ({
component={Input} label: t.tableName,
/> value: t.tableName,
__type: SelectCommandTypeEnum.Table,
__schema: t.schemaName,
__rawName: t.tableName,
})),
},
{
label: 'Views',
options: dbObjects.views.map((v) => ({
label: v.viewName,
value: v.viewName,
__type: SelectCommandTypeEnum.View,
__schema: v.schemaName,
__rawName: v.viewName,
})),
},
{
label: 'Functions',
options: dbObjects.functions.map((f) => ({
label: f.functionName,
value: f.functionName,
__type: SelectCommandTypeEnum.TableValuedFunction,
__schema: f.schemaName,
__rawName: f.functionName,
})),
},
{
label: 'Stored Procedures',
options: dbObjects.storedProcedures.map((p) => ({
label: p.procedureName,
value: p.procedureName,
__type: SelectCommandTypeEnum.StoredProcedure,
__schema: p.schemaName,
__rawName: p.procedureName,
})),
},
]
: []
return (
<Select
componentAs={CreatableSelect}
field={field}
form={form}
isClearable
isLoading={isLoadingDbObjects}
options={grouped}
placeholder={
isLoadingDbObjects
? translate('::Loading')
: translate(
'::ListForms.ListFormEdit.DatabaseDataSourceSelectCommand',
)
}
value={field.value ? { label: field.value, value: field.value } : null}
onChange={(option: any) => {
if (!option) {
form.setFieldValue(field.name, '')
form.setFieldValue('selectCommandType', undefined)
form.setFieldValue('keyFieldName', '')
form.setFieldValue('keyFieldDbSourceType', undefined)
setSelectCommandColumns([])
return
}
form.setFieldValue(field.name, option.value)
const type = option.__isNew__
? SelectCommandTypeEnum.Query
: (option.__type ?? SelectCommandTypeEnum.Query)
form.setFieldValue('selectCommandType', type)
form.setFieldValue('keyFieldName', '')
form.setFieldValue('keyFieldDbSourceType', undefined)
if (!option.__isNew__ && option.__schema != null && option.__rawName) {
loadColumns(values.dataSourceCode ?? '', option.__schema, option.__rawName)
} else {
setSelectCommandColumns([])
}
}}
onCreateOption={(inputValue: string) => {
form.setFieldValue(field.name, inputValue)
form.setFieldValue('selectCommandType', SelectCommandTypeEnum.Query)
form.setFieldValue('keyFieldName', '')
form.setFieldValue('keyFieldDbSourceType', undefined)
setSelectCommandColumns([])
}}
/>
)
}}
</Field>
</FormItem> </FormItem>
<FormItem <FormItem
label={translate('::ListForms.ListFormEdit.DatabaseDataSourceTableName')} label={translate('::ListForms.ListFormEdit.DatabaseDataSourceTableName')}
@ -180,42 +328,59 @@ function FormTabDatabaseDataSource(props: FormEditProps) {
label={translate('::ListForms.ListFormEdit.DatabaseDataSourceKeyFieldName')} label={translate('::ListForms.ListFormEdit.DatabaseDataSourceKeyFieldName')}
invalid={errors.keyFieldName && touched.keyFieldName} invalid={errors.keyFieldName && touched.keyFieldName}
errorMessage={errors.keyFieldName} errorMessage={errors.keyFieldName}
extra={
values.keyFieldName && values.keyFieldDbSourceType != null ? (
<span className="ml-2 text-xs px-2 py-0.5 rounded bg-green-100 text-green-700 dark:bg-green-900/40 dark:text-green-300">
{dbSourceTypeOptions.find(
(o: any) => o.value === values.keyFieldDbSourceType,
)?.label ?? String(values.keyFieldDbSourceType)}
</span>
) : selectCommandColumns.length === 0 && !isLoadingColumns ? (
<span className="text-xs ml-2 text-gray-400">
{translate('::ListForms.ListFormEdit.DatabaseDataSourceSelectCommand') +
' seçince sütunlar yüklenir'}
</span>
) : null
}
> >
<Field <Field type="text" autoComplete="off" name="keyFieldName">
type="text" {({ field, form }: FieldProps<string>) => (
autoComplete="off"
name="keyFieldName"
placeholder={translate(
'::ListForms.ListFormEdit.DatabaseDataSourceKeyFieldName',
)}
component={Input}
/>
</FormItem>
<FormItem
label={translate(
'::ListForms.ListFormEdit.DatabaseDataSourceKeyFieldDbSourceType',
)}
// invalid={errors.keyFieldDbSourceType && touched.keyFieldDbSourceType}
// errorMessage={errors.keyFieldDbSourceType}
>
<Field
type="text"
autoComplete="off"
name="keyFieldDbSourceType"
placeholder={translate(
'::ListForms.ListFormEdit.DatabaseDataSourceKeyFieldDbSourceType',
)}
>
{({ field, form }: FieldProps<any>) => (
<Select <Select
componentAs={CreatableSelect}
field={field} field={field}
form={form} form={form}
isClearable={true} isClearable
options={dbSourceTypeOptions} isLoading={isLoadingColumns}
value={dbSourceTypeOptions?.filter( placeholder={
(option: any) => option.value === values.keyFieldDbSourceType, isLoadingColumns
)} ? translate('::Loading')
onChange={(option) => form.setFieldValue(field.name, option?.value)} : translate(
'::ListForms.ListFormEdit.DatabaseDataSourceKeyFieldName',
)
}
options={selectCommandColumns.map((c) => ({
label: `${c.columnName} (${c.dataType})`,
value: c.columnName,
__dataType: c.dataType,
}))}
value={field.value ? { label: field.value, value: field.value } : null}
onChange={(option: any) => {
if (!option) {
form.setFieldValue(field.name, '')
form.setFieldValue('keyFieldDbSourceType', undefined)
return
}
form.setFieldValue(field.name, option.value)
if (!option.__isNew__ && option.__dataType) {
form.setFieldValue(
'keyFieldDbSourceType',
sqlDataTypeToDbType(option.__dataType),
)
}
}}
onCreateOption={(inputValue: string) => {
form.setFieldValue(field.name, inputValue)
}}
/> />
)} )}
</Field> </Field>

View file

@ -36,11 +36,13 @@ import setNull from '@/utils/setNull'
import classNames from 'classnames' import classNames from 'classnames'
import { Field, FieldProps, Form, Formik, FormikHelpers } from 'formik' import { Field, FieldProps, Form, Formik, FormikHelpers } from 'formik'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { FaFileMedical, FaCopy, FaEyeSlash, FaMinus, FaTimes } from 'react-icons/fa' import { FaFileMedical, FaCopy, FaEyeSlash, FaMinus, FaTimes, FaTable } from 'react-icons/fa'
import { number, object, string } from 'yup' import { number, object, string } from 'yup'
import FormFieldEdit from './FormFieldEdit' import FormFieldEdit from './FormFieldEdit'
import { dbSourceTypeOptions } from '../options' import { dbSourceTypeOptions, sqlDataTypeToDbType } from '../options'
import { IdentityRoleDto, IdentityUserDto } from '@/proxy/admin/models' import { IdentityRoleDto, IdentityUserDto } from '@/proxy/admin/models'
import { sqlObjectManagerService } from '@/services/sql-query-manager.service'
import type { DatabaseColumnDto } from '@/proxy/sql-query-manager/models'
export interface FormFieldEditProps { export interface FormFieldEditProps {
onSubmit: ( onSubmit: (
@ -87,6 +89,103 @@ function FormFields({
const [fieldList, setFieldList] = useState<SelectBoxOption[]>([]) const [fieldList, setFieldList] = useState<SelectBoxOption[]>([])
const cultureName = useStoreState((state) => state.locale.currentLang) const cultureName = useStoreState((state) => state.locale.currentLang)
// ── All DB Columns (for "Tüm Sütunları Ekle") ───────────────────────────────
const listFormValues = useStoreState((s) => s.admin.lists.values)
const [allDbColumns, setAllDbColumns] = useState<DatabaseColumnDto[]>([])
const [isAddingAllColumns, setIsAddingAllColumns] = useState(false)
useEffect(() => {
const dsCode = listFormValues?.dataSourceCode
const cmd = listFormValues?.selectCommand
if (!dsCode || !cmd) {
setAllDbColumns([])
return
}
const load = async () => {
try {
const objRes = await sqlObjectManagerService.getAllObjects(dsCode)
const dbObjects = objRes.data
if (!dbObjects) return
let schema = ''
let name = ''
const table = dbObjects.tables.find((t) => t.tableName === cmd)
if (table) {
schema = table.schemaName
name = table.tableName
} else {
const view = dbObjects.views.find((v) => v.viewName === cmd)
if (view) {
schema = view.schemaName
name = view.viewName
} else {
const fn = dbObjects.functions.find((f) => f.functionName === cmd)
if (fn) {
schema = fn.schemaName
name = fn.functionName
} else {
const sp = dbObjects.storedProcedures.find((p) => p.procedureName === cmd)
if (sp) {
schema = sp.schemaName
name = sp.procedureName
}
}
}
}
if (!name) {
setAllDbColumns([])
return
}
const colRes = await sqlObjectManagerService.getTableColumns(dsCode, schema, name)
setAllDbColumns(colRes.data ?? [])
} catch {
setAllDbColumns([])
}
}
load()
}, [listFormValues?.dataSourceCode, listFormValues?.selectCommand])
const addAllColumns = async () => {
if (!allDbColumns.length || !listFormCode) return
const existingNames = new Set((fields ?? []).map((f) => f.fieldName))
const newCols = allDbColumns.filter((c) => !existingNames.has(c.columnName))
if (newCols.length === 0) {
toast.push(
<Notification type="info" duration={2000}>
Tüm sütunlar zaten eklenmiş.
</Notification>,
{ placement: 'top-end' },
)
return
}
setIsAddingAllColumns(true)
try {
for (const col of newCols) {
await postListFormField({
listFormCode,
fieldName: col.columnName,
sourceDbType: sqlDataTypeToDbType(col.dataType) as any,
cultureName,
})
}
toast.push(
<Notification type="success" duration={2000}>
{newCols.length} sütun eklendi.
</Notification>,
{ placement: 'top-end' },
)
await getFields()
} catch (error: any) {
toast.push(
<Notification type="danger">
Hata: <code>{error.toString()}</code>
</Notification>,
{ placement: 'top-end' },
)
} finally {
setIsAddingAllColumns(false)
}
}
const NewField: Partial<ColumnFormatEditDto> = { const NewField: Partial<ColumnFormatEditDto> = {
fieldName: '', fieldName: '',
cultureName, cultureName,
@ -324,6 +423,18 @@ function FormFields({
setIsCopyField(true) setIsCopyField(true)
}} }}
/> />
{allDbColumns.length > 0 && (
<Button
shape="circle"
variant="plain"
type="button"
size="xs"
title="Tüm Sütunları Ekle"
icon={<FaTable />}
loading={isAddingAllColumns}
onClick={addAllColumns}
/>
)}
</div> </div>
</Th> </Th>
<Th>Field</Th> <Th>Field</Th>

View file

@ -433,6 +433,30 @@ export const firstDayOfWeekOptions = [
{ value: 6, label: 'Saturday' }, { value: 6, label: 'Saturday' },
] ]
export const sqlDataTypeToDbType = (sqlType: string): DbTypeEnum => {
const t = sqlType
.toLowerCase()
.replace(/\s*\(.*\)/, '')
.trim()
if (['int', 'integer', 'int32'].includes(t)) return DbTypeEnum.Int32
if (['bigint', 'int64'].includes(t)) return DbTypeEnum.Int64
if (['smallint', 'int16'].includes(t)) return DbTypeEnum.Int16
if (['tinyint', 'byte'].includes(t)) return DbTypeEnum.Byte
if (['bit', 'boolean', 'bool'].includes(t)) return DbTypeEnum.Boolean
if (['float', 'real', 'double', 'double precision'].includes(t)) return DbTypeEnum.Double
if (['decimal', 'numeric', 'money', 'smallmoney'].includes(t)) return DbTypeEnum.Decimal
if (['uniqueidentifier'].includes(t)) return DbTypeEnum.Guid
if (['datetime2', 'smalldatetime', 'datetime'].includes(t)) return DbTypeEnum.DateTime
if (['date'].includes(t)) return DbTypeEnum.Date
if (['time'].includes(t)) return DbTypeEnum.Time
if (['datetimeoffset'].includes(t)) return DbTypeEnum.DateTimeOffset
if (['nvarchar', 'varchar', 'char', 'nchar', 'text', 'ntext', 'string'].includes(t))
return DbTypeEnum.String
if (['xml'].includes(t)) return DbTypeEnum.Xml
if (['binary', 'varbinary', 'image'].includes(t)) return DbTypeEnum.Binary
return DbTypeEnum.String
}
export const dbSourceTypeOptions = enumToList(DbTypeEnum) export const dbSourceTypeOptions = enumToList(DbTypeEnum)
export const dataSourceTypeOptions = enumToList(DataSourceTypeEnum) export const dataSourceTypeOptions = enumToList(DataSourceTypeEnum)
export const selectCommandTypeOptions = enumToList(SelectCommandTypeEnum) export const selectCommandTypeOptions = enumToList(SelectCommandTypeEnum)