ListFormWizard

This commit is contained in:
Sedat Öztürk 2026-05-02 10:35:51 +03:00
parent 009c1a8416
commit 503c45282b
12 changed files with 932 additions and 109 deletions

View file

@ -20,6 +20,14 @@ public class ListFormWizardDto
public bool AllowDetail { get; set; }
public bool ConfirmDelete { get; set; }
public string DefaultLayout { get; set; }
public bool Grid { get; set; }
public bool Pivot { get; set; }
public bool Tree { get; set; }
public bool Chart { get; set; }
public bool Gantt { get; set; }
public bool Scheduler { get; set; }
public string LanguageTextMenuEn { get; set; }
public string LanguageTextMenuTr { get; set; }
public string LanguageTextTitleEn { get; set; }

View file

@ -1,10 +1,12 @@
using System.Data;
using Sozsoft.Platform.Enums;
namespace Sozsoft.Platform.ListForms;
public class WizardColumnItemInputDto
{
public string DataField { get; set; }
public string CaptionName { get; set; }
public string EditorType { get; set; }
public string EditorOptions { get; set; }
public string EditorScript { get; set; }
@ -13,4 +15,8 @@ public class WizardColumnItemInputDto
public DbType DbSourceType { get; set; } = DbType.String;
public string TurkishCaption { get; set; }
public string EnglishCaption { get; set; }
public UiLookupDataSourceTypeEnum LookupDataSourceType { get; set; }
public string ValueExpr { get; set; }
public string DisplayExpr { get; set; }
public string LookupQuery { get; set; }
}

View file

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
@ -16,6 +17,7 @@ using static Sozsoft.Platform.PlatformConsts;
using System.Data;
using Microsoft.Extensions.Configuration;
using Sozsoft.Languages;
using Sozsoft.Platform.DynamicData;
namespace Sozsoft.Platform.ListForms;
@ -30,7 +32,8 @@ public class ListFormWizardAppService(
IRepository<Menu, Guid> repoMenu,
IPermissionGrantRepository permissionGrantRepository,
IConfiguration configuration,
LanguageTextAppService languageTextAppService
LanguageTextAppService languageTextAppService,
IDynamicDataManager dynamicDataManager
) : PlatformAppService(), IListFormWizardAppService
{
private readonly IRepository<ListForm, Guid> repoListForm = repoListForm;
@ -44,6 +47,7 @@ public class ListFormWizardAppService(
private readonly IPermissionGrantRepository permissionGrantRepository = permissionGrantRepository;
private readonly IConfiguration _configuration = configuration;
private readonly LanguageTextAppService _languageTextAppService = languageTextAppService;
private readonly IDynamicDataManager _dynamicDataManager = dynamicDataManager;
private readonly string cultureNameDefault = PlatformConsts.DefaultLanguage;
[UnitOfWork]
@ -194,6 +198,10 @@ public class ListFormWizardAppService(
await repoListFormField.DeleteManyAsync(existingListFormFields, autoSave: true);
}
var tableColumns = await GetTableColumnNamesAsync(input.DataSourceCode, input.SelectCommandType, input.SelectCommand);
var isDeleted = tableColumns.Contains("IsDeleted");
var isCreated = tableColumns.Contains("CreatorId");
var listForm = await repoListForm.InsertAsync(new ListForm
{
ListFormType = ListFormTypeEnum.List,
@ -201,7 +209,7 @@ public class ListFormWizardAppService(
ExportJson = WizardConsts.DefaultExportJson,
IsSubForm = false,
ShowNote = true,
LayoutJson = WizardConsts.DefaultLayoutJson(),
LayoutJson = WizardConsts.DefaultLayoutJson(input.DefaultLayout, input.Grid, input.Pivot, input.Tree, input.Chart, input.Gantt, input.Scheduler),
CultureName = LanguageCodes.En,
ListFormCode = input.ListFormCode,
Name = nameLangKey,
@ -215,7 +223,7 @@ public class ListFormWizardAppService(
SelectCommand = input.SelectCommand,
KeyFieldName = input.KeyFieldName,
KeyFieldDbSourceType = input.KeyFieldDbSourceType,
DefaultFilter = WizardConsts.DefaultFilterJson,
DefaultFilter = isDeleted ? WizardConsts.DefaultFilterJson : null,
SortMode = GridOptions.SortModeSingle,
FilterRowJson = WizardConsts.DefaultFilterRowJson,
HeaderFilterJson = WizardConsts.DefaultHeaderFilterJson,
@ -224,9 +232,9 @@ public class ListFormWizardAppService(
SelectionJson = WizardConsts.DefaultSelectionSingleJson,
ColumnOptionJson = WizardConsts.DefaultColumnOptionJson(),
PermissionJson = WizardConsts.DefaultPermissionJson(code),
DeleteCommand = WizardConsts.DefaultDeleteCommand(input.SelectCommand),
DeleteFieldsDefaultValueJson = WizardConsts.DefaultDeleteFieldsDefaultValueJson(input.KeyFieldDbSourceType),
InsertFieldsDefaultValueJson = WizardConsts.DefaultInsertFieldsDefaultValueJson(input.KeyFieldDbSourceType),
DeleteCommand = isDeleted ? WizardConsts.DefaultDeleteCommand(input.SelectCommand) : null,
DeleteFieldsDefaultValueJson = isDeleted ? WizardConsts.DefaultDeleteFieldsDefaultValueJson(input.KeyFieldDbSourceType) : WizardConsts.DefaultFieldsJsonOnlyId(input.KeyFieldDbSourceType),
InsertFieldsDefaultValueJson = isCreated ? WizardConsts.DefaultInsertFieldsDefaultValueJson(input.KeyFieldDbSourceType) : WizardConsts.DefaultFieldsJsonOnlyId(input.KeyFieldDbSourceType),
PagerOptionJson = WizardConsts.DefaultPagerOptionJson,
EditingOptionJson = WizardConsts.DefaultEditingOptionJson(titleLangKey, 600, 500, input.AllowDeleting, input.AllowAdding, input.AllowUpdating, input.ConfirmDelete, false, input.AllowDetail),
EditingFormJson = editingFormDtos.Count > 0 ? JsonSerializer.Serialize(editingFormDtos) : null,
@ -234,18 +242,16 @@ public class ListFormWizardAppService(
// ListFormField - each item in each group becomes a visible field record
var fieldOrder = 0;
var captionName = string.Empty;
foreach (var group in input.Groups)
{
foreach (var item in group.Items)
{
fieldOrder++;
captionName = $"App.Listform.ListformField.{item.DataField}";
await repoListFormField.InsertAsync(new ListFormField
{
ListFormCode = input.ListFormCode,
FieldName = item.DataField,
CaptionName = captionName,
CaptionName = item.CaptionName,
Visible = item.DataField != input.KeyFieldName,
IsActive = true,
AllowSearch = true,
@ -256,9 +262,10 @@ public class ListFormWizardAppService(
ColumnCustomizationJson = WizardConsts.DefaultColumnCustomizationJson,
ColumnFilterJson = WizardConsts.DefaultColumnFilteringJson,
PivotSettingsJson = WizardConsts.DefaultPivotSettingsJson,
}, autoSave: false);
LookupJson = item.LookupQuery.Length > 0 ? WizardConsts.DefaultLookupJson(item.LookupDataSourceType, item.DisplayExpr, item.ValueExpr, item.LookupQuery) : null,
}, autoSave: true);
await CreateLangKey(captionName, item.EnglishCaption, item.TurkishCaption);
await CreateLangKey(item.CaptionName, item.EnglishCaption, item.TurkishCaption);
}
}
@ -267,24 +274,60 @@ public class ListFormWizardAppService(
}
private async Task<HashSet<string>> GetTableColumnNamesAsync(string dataSourceCode, SelectCommandTypeEnum commandType, string selectCommand)
{
if (commandType == SelectCommandTypeEnum.Query || commandType == SelectCommandTypeEnum.StoredProcedure)
return [];
var parts = selectCommand.Split('.');
var schemaName = parts.Length > 1 ? parts[0].Trim('"', '[', ']') : "dbo";
var tableName = parts[parts.Length - 1].Trim('"', '[', ']');
try
{
var (repo, connectionString, dbType) = await _dynamicDataManager.GetAsync(false, dataSourceCode);
var query = dbType == Sozsoft.Platform.Enums.DataSourceTypeEnum.Postgresql
? $"SELECT column_name FROM information_schema.columns WHERE table_schema = '{schemaName}' AND table_name = '{tableName}'"
: $"SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = '{schemaName}' AND TABLE_NAME = '{tableName}'";
var rows = await repo.QueryAsync(query, connectionString);
var columns = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var row in rows)
{
var dict = row as IDictionary<string, object>;
if (dict != null)
{
var key = dict.ContainsKey("COLUMN_NAME") ? "COLUMN_NAME" : "column_name";
if (dict.TryGetValue(key, out var col) && col != null)
columns.Add(col.ToString()!);
}
}
return columns;
}
catch
{
return [];
}
}
private async Task<LanguageKey> CreateLangKey(string key, string textEn, string textTr)
{
var res = PlatformConsts.AppName;
var keyQueryable = await repoLangKey.GetQueryableAsync();
var langKey = await AsyncExecuter.FirstOrDefaultAsync(keyQueryable.Where(a => a.ResourceName == res && a.Key == key))
?? await repoLangKey.InsertAsync(new LanguageKey { ResourceName = res, Key = key }, autoSave: false);
var langKey = await repoLangKey.FirstOrDefaultAsync(a => a.ResourceName == res && a.Key == key)
?? await repoLangKey.InsertAsync(new LanguageKey { ResourceName = res, Key = key }, autoSave: true);
var textQueryable = await repoLangText.GetQueryableAsync();
var existingTexts = await AsyncExecuter.ToListAsync(
textQueryable.Where(a => a.ResourceName == res && a.Key == langKey.Key)
);
var existingTexts = await repoLangText.GetListAsync(a => a.ResourceName == res && a.Key == langKey.Key);
var langTextEn = existingTexts.FirstOrDefault(a => a.CultureName == cultureNameDefault)
?? await repoLangText.InsertAsync(new LanguageText { ResourceName = res, Key = langKey.Key, CultureName = cultureNameDefault, Value = textEn }, autoSave: false);
var existingEn = existingTexts.FirstOrDefault(a => a.CultureName == cultureNameDefault);
if (existingEn != null) await repoLangText.DeleteAsync(existingEn, autoSave: true);
await repoLangText.InsertAsync(new LanguageText { ResourceName = res, Key = langKey.Key, CultureName = cultureNameDefault, Value = textEn }, autoSave: true);
var langTextTr = existingTexts.FirstOrDefault(a => a.CultureName == LanguageCodes.Tr)
?? await repoLangText.InsertAsync(new LanguageText { ResourceName = res, Key = langKey.Key, CultureName = LanguageCodes.Tr, Value = textTr }, autoSave: false);
var existingTr = existingTexts.FirstOrDefault(a => a.CultureName == LanguageCodes.Tr);
if (existingTr != null) await repoLangText.DeleteAsync(existingTr, autoSave: true);
await repoLangText.InsertAsync(new LanguageText { ResourceName = res, Key = langKey.Key, CultureName = LanguageCodes.Tr, Value = textTr }, autoSave: true);
return langKey;
}

View file

@ -16280,6 +16280,42 @@
"en": "Columns load after selecting a Select Command",
"tr": "Select Command seçince sütunlar yüklenir"
},
{
"resourceName": "Platform",
"key": "ListForms.Wizard.Step3.GenerateFromTable",
"en": "Generate From Table",
"tr": "Tablodan Üret"
},
{
"resourceName": "Platform",
"key": "ListForms.Wizard.Step3.LookupDataSourceType",
"en": "Lookup Data Source Type",
"tr": "Veri Kaynağı Türü"
},
{
"resourceName": "Platform",
"key": "ListForms.Wizard.Step3.LookupValueExpression",
"en": "Lookup Value Expression",
"tr": "Değer İfadesi"
},
{
"resourceName": "Platform",
"key": "ListForms.Wizard.Step3.LookupDisplayExpression",
"en": "Lookup Display Expression",
"tr": "Görüntü İfadesi"
},
{
"resourceName": "Platform",
"key": "ListForms.Wizard.Step3.LookupLookupQuery",
"en": "Lookup Query",
"tr": "Lookup Sorgusu"
},
{
"resourceName": "Platform",
"key": "ListForms.Wizard.Step3.LanguageKey",
"en": "Language Key",
"tr": "Dil Anahtarı"
},
{
"resourceName": "Platform",
"key": "ListForms.Wizard.Step3.TurkishCaption",
@ -16292,6 +16328,12 @@
"en": "English Caption",
"tr": "İngilizce Başlık"
},
{
"resourceName": "Platform",
"key": "ListForms.Wizard.Step3.EditorType",
"en": "Editor Type",
"tr": "Editör Türü"
},
{
"resourceName": "Platform",
"key": "ListForms.Wizard.Step3.EditorOptions",

View file

@ -78,14 +78,14 @@ public static class WizardConsts
Margin = 10
});
public static string DefaultLayoutJson(string DefaultLayout = "grid") => JsonSerializer.Serialize(new
public static string DefaultLayoutJson(string DefaultLayout = "grid", bool Grid = true, bool Pivot = true, bool Chart = true, bool Tree = true, bool Gantt = true, bool Scheduler = true) => JsonSerializer.Serialize(new
{
Grid = true,
Pivot = true,
Chart = true,
Tree = true,
Gantt = true,
Scheduler = true,
Grid = Grid,
Pivot = Pivot,
Chart = Chart,
Tree = Tree,
Gantt = Gantt,
Scheduler = Scheduler,
DefaultLayout = DefaultLayout,
});
@ -153,6 +153,14 @@ public static class WizardConsts
IsPivot = true
});
public static string DefaultLookupJson(UiLookupDataSourceTypeEnum dataSourceType, string displayExpr = "name", string valueExpr = "key", string lookupQuery = "") => JsonSerializer.Serialize(new
{
DataSourceType = dataSourceType,
DisplayExpr = displayExpr,
ValueExpr = valueExpr,
LookupQuery = lookupQuery,
});
public static string DefaultDeleteCommand(string tableName)
{
return $"UPDATE \"{tableName}\" SET \"DeleterId\"=@DeleterId, \"DeletionTime\"=CURRENT_TIMESTAMP, \"IsDeleted\"='true' WHERE \"Id\"=@Id";
@ -178,6 +186,14 @@ public static class WizardConsts
});
}
public static string DefaultFieldsJsonOnlyId(DbType dbType = DbType.Guid)
{
return JsonSerializer.Serialize(new[]
{
new { FieldName = "Id", FieldDbType = dbType, Value = "@NEWID", CustomValueType = FieldCustomValueTypeEnum.CustomKey }
});
}
public static readonly string DefaultPagerOptionJson = JsonSerializer.Serialize(new
{
Visible = true,

View file

@ -64,7 +64,7 @@ BEGIN
[DeletionTime] datetime2 NULL,
[DeleterId] uniqueidentifier NULL,
[TenantId] uniqueidentifier NULL,
[BranchId] uniqueidentifier NOT NULL,
[BranchId] uniqueidentifier NULL,
[Name] nvarchar(64) NOT NULL,
[ParentId] uniqueidentifier NULL,
CONSTRAINT [PK_Adm_T_Department] PRIMARY KEY CLUSTERED

View file

@ -43,6 +43,15 @@ export interface ListFormWizardDto {
allowDeleting: boolean
confirmDelete: boolean
allowDetail: boolean
defaultLayout: ListViewLayoutType
grid: boolean
pivot: boolean
tree: boolean
chart: boolean
gantt: boolean
scheduler: boolean
languageTextMenuEn: string
languageTextMenuTr: string
languageTextTitleEn: string

View file

@ -43,6 +43,13 @@ const initialValues: ListFormWizardDto = {
allowDeleting: true,
confirmDelete: true,
allowDetail: false,
defaultLayout: 'grid',
grid: true,
pivot: true,
tree: true,
chart: true,
gantt: true,
scheduler: true,
languageTextMenuEn: '',
languageTextMenuTr: '',
languageTextTitleEn: '',
@ -148,8 +155,7 @@ const Wizard = () => {
'deleterid',
])
const isAuditColumn = (columnName: string) =>
AUDIT_COLUMNS.has(columnName.toLowerCase())
const isAuditColumn = (columnName: string) => AUDIT_COLUMNS.has(columnName.toLowerCase())
const loadColumns = async (dsCode: string, schema: string, name: string) => {
if (!dsCode || !name) {
@ -268,7 +274,6 @@ const Wizard = () => {
.replace(/([A-Z])([A-Z][a-z])/g, '$1 $2')
.trim()
const handleWizardNameChange = (name: string) => {
const spacedLabel = toSpacedLabel(name)
const derived = deriveListFormCode(name)
@ -379,6 +384,11 @@ const Wizard = () => {
dbSourceType: col ? sqlDataTypeToDbType(col.dataType) : 12,
turkishCaption: item.turkishCaption,
englishCaption: item.englishCaption,
captionName: item.captionName,
lookupDataSourceType: item.lookupDataSourceType,
displayExpr: item.displayExpr,
valueExpr: item.valueExpr,
lookupQuery: item.lookupQuery,
}
}),
})),
@ -388,7 +398,9 @@ const Wizard = () => {
await getConfig(true)
// ✅ sonra navigate
navigate(ROUTES_ENUM.protected.admin.list.replace(':listFormCode', values.listFormCode), { replace: true })
navigate(ROUTES_ENUM.protected.admin.list.replace(':listFormCode', values.listFormCode), {
replace: true,
})
// ✅ en son kullanıcıya mesaj
toast.push(
@ -438,7 +450,10 @@ const Wizard = () => {
await getConfig(true)
// 🔴 3. Navigate
navigate(ROUTES_ENUM.protected.admin.list.replace(':listFormCode', values.listFormCode), { replace: true })
navigate(
ROUTES_ENUM.protected.admin.list.replace(':listFormCode', values.listFormCode),
{ replace: true },
)
// 🔴 4. Toast (istersen navigate öncesi de olabilir)
toast.push(
@ -518,6 +533,9 @@ const Wizard = () => {
selectCommandColumns={selectCommandColumns}
groups={editingGroups}
onGroupsChange={setEditingGroups}
dbObjects={dbObjects}
isLoadingDbObjects={isLoadingDbObjects}
dsCode={currentDataSource}
translate={translate}
onBack={() => setCurrentStep(1)}
onNext={() => setCurrentStep(3)}

View file

@ -3,7 +3,12 @@ import { ListFormWizardDto } from '@/proxy/admin/list-form/models'
import { SelectCommandTypeEnum } from '@/proxy/form/models'
import type { DatabaseColumnDto, SqlObjectExplorerDto } from '@/proxy/sql-query-manager/models'
import { SelectBoxOption } from '@/types/shared'
import { dbSourceTypeOptions, selectCommandTypeOptions, sqlDataTypeToDbType } from './edit/options'
import {
dbSourceTypeOptions,
listFormDefaultLayoutOptions,
selectCommandTypeOptions,
sqlDataTypeToDbType,
} from './edit/options'
import { Field, FieldProps, FormikErrors, FormikTouched } from 'formik'
import CreatableSelect from 'react-select/creatable'
import { FaArrowLeft, FaArrowRight } from 'react-icons/fa'
@ -395,6 +400,121 @@ const WizardStep2 = ({
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-6">
<div className="flex flex-wrap gap-6">
<FormItem
label={translate('::ListForms.ListFormEdit.DetailsLayoutDto.DefaultLayout')}
invalid={errors?.defaultLayout && touched?.defaultLayout}
errorMessage={errors?.defaultLayout}
>
<Field
type="text"
autoComplete="off"
name="defaultLayout"
placeholder={translate('::ListForms.ListFormEdit.DetailsLayoutDto.DefaultLayout')}
>
{({ field, form }: FieldProps<SelectBoxOption>) => (
<Select
field={field}
form={form}
isClearable={true}
options={listFormDefaultLayoutOptions}
value={listFormDefaultLayoutOptions?.filter(
(option) => option.value === values.defaultLayout,
)}
onChange={(option) => form.setFieldValue(field.name, option?.value)}
/>
)}
</Field>
</FormItem>
</div>
<div className="flex flex-wrap gap-6">
<FormItem
label={translate('::ListForms.ListFormEdit.DetailsLayoutDto.GridLayout')}
invalid={errors?.grid && touched?.grid}
errorMessage={errors?.grid}
>
<Field
className="w-20"
autoComplete="off"
name="grid"
placeholder={translate('::ListForms.ListFormEdit.DetailsLayoutDto.GridLayout')}
component={Checkbox}
/>
</FormItem>
<FormItem
label={translate('::ListForms.ListFormEdit.DetailsLayoutDto.PivotLayout')}
invalid={errors?.pivot && touched?.pivot}
errorMessage={errors?.pivot}
>
<Field
className="w-20"
autoComplete="off"
name="pivot"
placeholder={translate('::ListForms.ListFormEdit.DetailsLayoutDto.PivotLayout')}
component={Checkbox}
/>
</FormItem>
<FormItem
label={translate('::ListForms.ListFormEdit.DetailsLayoutDto.ChartLayout')}
invalid={errors?.chart && touched?.chart}
errorMessage={errors?.chart}
>
<Field
className="w-20"
autoComplete="off"
name="chart"
placeholder={translate('::ListForms.ListFormEdit.DetailsLayoutDto.ChartLayout')}
component={Checkbox}
/>
</FormItem>
<FormItem
label={translate('::ListForms.ListFormEdit.DetailsLayoutDto.TreeLayout')}
invalid={errors?.tree && touched?.tree}
errorMessage={errors?.tree}
>
<Field
className="w-20"
autoComplete="off"
name="tree"
placeholder={translate('::ListForms.ListFormEdit.DetailsLayoutDto.TreeLayout')}
component={Checkbox}
/>
</FormItem>
<FormItem
label={translate('::ListForms.ListFormEdit.DetailsLayoutDto.GanttLayout')}
invalid={errors?.gantt && touched?.gantt}
errorMessage={errors?.gantt}
>
<Field
className="w-20"
autoComplete="off"
name="gantt"
placeholder={translate('::ListForms.ListFormEdit.DetailsLayoutDto.GanttLayout')}
component={Checkbox}
/>
</FormItem>
<FormItem
label={translate('::ListForms.ListFormEdit.DetailsLayoutDto.SchedulerLayout')}
invalid={errors?.scheduler && touched?.scheduler}
errorMessage={errors?.scheduler}
>
<Field
className="w-20"
autoComplete="off"
name="scheduler"
placeholder={translate('::ListForms.ListFormEdit.DetailsLayoutDto.SchedulerLayout')}
component={Checkbox}
/>
</FormItem>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-6">
<FormItem
label={translate('::ListForms.Wizard.Step2.TitleTextEnglish')}
@ -474,14 +594,14 @@ const WizardStep2 = ({
selectCommandColumns.length > 0 ? (
<div className="flex items-center gap-2 ml-3">
<Button
variant='solid'
variant="solid"
onClick={() => onToggleAllColumns(true)}
className="text-xs px-2 py-0.5 rounded bg-indigo-500 text-white hover:bg-indigo-600"
>
{translate('::ListForms.Wizard.Step2.SelectAll') || 'Tümünü Seç'}
</Button>
<Button
variant='default'
variant="default"
onClick={() => onToggleAllColumns(false)}
className="text-xs px-2 py-0.5 rounded border border-gray-300 dark:border-gray-600 text-gray-500 hover:text-red-500 hover:border-red-400"
>

View file

@ -1,7 +1,11 @@
import { Button, Dialog } from '@/components/ui'
import { useLocalization } from '@/utils/hooks/useLocalization'
import { columnEditorTypeListOptions } from '@/views/admin/listForm/edit/options'
import type { DatabaseColumnDto } from '@/proxy/sql-query-manager/models'
import {
columnEditorTypeListOptions,
columnLookupDataSourceTypeListOptions,
} from '@/views/admin/listForm/edit/options'
import type { DatabaseColumnDto, SqlObjectExplorerDto } from '@/proxy/sql-query-manager/models'
import { sqlObjectManagerService } from '@/services/sql-query-manager.service'
import {
DndContext,
DragEndEvent,
@ -16,7 +20,16 @@ import {
import { SortableContext, arrayMove, useSortable, rectSortingStrategy } from '@dnd-kit/sortable'
import { CSS } from '@dnd-kit/utilities'
import { useEffect, useState } from 'react'
import { FaArrowLeft, FaGripVertical, FaPlus, FaTimes, FaTrash, FaArrowRight, FaCode } from 'react-icons/fa'
import {
FaArrowLeft,
FaGripVertical,
FaPlus,
FaTimes,
FaTrash,
FaArrowRight,
FaCode,
} from 'react-icons/fa'
import { UiLookupDataSourceTypeEnum } from '@/proxy/form/models'
// ─── Types ────────────────────────────────────────────────────────────────────
export interface WizardGroupItem {
@ -29,6 +42,11 @@ export interface WizardGroupItem {
isRequired: boolean
turkishCaption?: string
englishCaption?: string
captionName?: string
lookupDataSourceType?: UiLookupDataSourceTypeEnum
valueExpr?: string
displayExpr?: string
lookupQuery?: string
}
export interface WizardGroup {
@ -43,6 +61,9 @@ export interface WizardStep3Props {
selectCommandColumns: DatabaseColumnDto[]
groups: WizardGroup[]
onGroupsChange: (groups: WizardGroup[]) => void
dbObjects: SqlObjectExplorerDto | null
isLoadingDbObjects: boolean
dsCode: string
translate: (key: string) => string
onBack: () => void
onNext: () => void
@ -73,16 +94,15 @@ function inferEditorType(sqlType: string): string {
}
const formatLabel = (text: string) => {
return text
return (
text
// CamelCase → kelimelere ayır
.split(/(?=[A-Z])/)
.filter(Boolean)
.map(word =>
word.charAt(0).toUpperCase() +
word.slice(1).toLowerCase()
.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
.join(' ')
)
.join(" ");
};
}
function newGroupItem(colName: string, meta?: DatabaseColumnDto): WizardGroupItem {
const sqlType = meta?.dataType ?? ''
@ -96,6 +116,11 @@ function newGroupItem(colName: string, meta?: DatabaseColumnDto): WizardGroupIte
isRequired: meta?.isNullable === false,
turkishCaption: formatLabel(colName),
englishCaption: formatLabel(colName),
captionName: `App.Listform.ListformField.${colName}`,
lookupDataSourceType: UiLookupDataSourceTypeEnum.StaticData,
valueExpr: 'Key',
displayExpr: 'Name',
lookupQuery: '',
}
}
@ -140,11 +165,18 @@ function AvailableColumnChip({ colName }: { colName: string }) {
interface SortableItemProps {
item: WizardGroupItem
groupColCount: number
dbObjects: SqlObjectExplorerDto | null
dsCode: string
onTurkishCaptionChange: (val: string) => void
onEnglishCaptionChange: (val: string) => void
onEditorTypeChange: (val: string) => void
onEditorOptionsChange: (val: string) => void
onEditorScriptChange: (val: string) => void
onCaptionNameChange: (val: string) => void
onLookupDataSourceTypeChange: (val: UiLookupDataSourceTypeEnum) => void
onDisplayExprChange: (val: string) => void
onValueExprChange: (val: string) => void
onLookupQueryChange: (val: string) => void
onColSpanChange: (val: number) => void
onRequiredChange: (val: boolean) => void
onRemove: () => void
@ -153,6 +185,8 @@ interface SortableItemProps {
function SortableItem({
item,
groupColCount,
dbObjects,
dsCode,
onTurkishCaptionChange,
onEnglishCaptionChange,
onEditorTypeChange,
@ -160,9 +194,24 @@ function SortableItem({
onEditorScriptChange,
onColSpanChange,
onRequiredChange,
onCaptionNameChange,
onLookupDataSourceTypeChange,
onDisplayExprChange,
onValueExprChange,
onLookupQueryChange,
onRemove,
}: SortableItemProps) {
const { translate } = useLocalization()
const [isTablePickerOpen, setIsTablePickerOpen] = useState(false)
const [tableSearch, setTableSearch] = useState('')
const [pickerStep, setPickerStep] = useState<'table' | 'columns'>('table')
const [pickerTable, setPickerTable] = useState<{ schemaName: string; tableName: string } | null>(
null,
)
const [pickerColumns, setPickerColumns] = useState<DatabaseColumnDto[]>([])
const [isLoadingPickerColumns, setIsLoadingPickerColumns] = useState(false)
const [pickerKeyCol, setPickerKeyCol] = useState('')
const [pickerNameCol, setPickerNameCol] = useState('')
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
id: `${ITM_PREFIX}${item.id}`,
})
@ -206,7 +255,46 @@ function SortableItem({
</button>
</div>
{/* Language Key + Turkish Caption + English Caption */}
<div className="flex flex-row flex-wrap gap-1.5">
<div className="flex flex-col gap-0.5 min-w-[140px] w-1/2 flex-1">
<span className="text-[10px] text-gray-400 font-medium">
{translate('::ListForms.Wizard.Step3.LanguageKey')}
</span>
<input
value={item.captionName}
disabled
onChange={(e) => onCaptionNameChange(e.target.value)}
className="w-full text-xs px-1.5 py-1 rounded border border-gray-200 dark:border-gray-600 bg-gray-50 dark:bg-gray-700 text-gray-700 dark:text-gray-200 focus:outline-none focus:border-indigo-400 resize-none font-mono"
/>
</div>
<div className="flex flex-col gap-0.5 min-w-[100px] flex-1">
<span className="text-[10px] text-gray-400 font-medium">
{translate('::ListForms.Wizard.Step3.TurkishCaption')}
</span>
<input
value={item.turkishCaption}
onChange={(e) => onTurkishCaptionChange(e.target.value)}
className="w-full text-xs px-1.5 py-1 rounded border border-gray-200 dark:border-gray-600 bg-gray-50 dark:bg-gray-700 text-gray-700 dark:text-gray-200 focus:outline-none focus:border-indigo-400 resize-none font-mono"
/>
</div>
<div className="flex flex-col gap-0.5 min-w-[100px] flex-1">
<span className="text-[10px] text-gray-400 font-medium">
{translate('::ListForms.Wizard.Step3.EnglishCaption')}
</span>
<input
value={item.englishCaption}
onChange={(e) => onEnglishCaptionChange(e.target.value)}
className="w-full text-xs px-1.5 py-1 rounded border border-gray-200 dark:border-gray-600 bg-gray-50 dark:bg-gray-700 text-gray-700 dark:text-gray-200 focus:outline-none focus:border-indigo-400 resize-none font-mono"
/>
</div>
</div>
{/* Editor type select */}
<div className="flex flex-col gap-0.5">
<span className="text-[10px] text-gray-400 font-medium">
{translate('::ListForms.Wizard.Step3.EditorType')}
</span>
<select
value={item.editorType}
onChange={(e) => onEditorTypeChange(e.target.value)}
@ -218,30 +306,240 @@ function SortableItem({
</option>
))}
</select>
</div>
{/* Turkish Caption */}
<div className="flex flex-col gap-0.5">
<span className="text-[10px] text-gray-400 font-medium">{translate('::ListForms.Wizard.Step3.TurkishCaption')}</span>
<div className="flex flex-row flex-wrap gap-1.5">
<div className="flex flex-col gap-0.5 min-w-[140px] w-1/2 flex-1">
<span className="text-[10px] text-gray-400 font-medium">
{translate('::ListForms.Wizard.Step3.LookupDataSourceType')}
</span>
<select
value={item.lookupDataSourceType}
onChange={(e) =>
onLookupDataSourceTypeChange(e.target.value as unknown as UiLookupDataSourceTypeEnum)
}
className="w-full text-xs h-7 px-1.5 rounded border border-gray-200 dark:border-gray-600 bg-gray-50 dark:bg-gray-700 text-gray-700 dark:text-gray-200 focus:outline-none focus:border-indigo-400"
>
{columnLookupDataSourceTypeListOptions.map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
</div>
<div className="flex flex-col gap-0.5 min-w-[100px] flex-1">
<span className="text-[10px] text-gray-400 font-medium">
{translate('::ListForms.Wizard.Step3.LookupValueExpression')}
</span>
<input
value={item.turkishCaption}
onChange={(e) => onTurkishCaptionChange(e.target.value)}
value={item.valueExpr}
onChange={(e) => onValueExprChange(e.target.value)}
className="w-full text-xs px-1.5 py-1 rounded border border-gray-200 dark:border-gray-600 bg-gray-50 dark:bg-gray-700 text-gray-700 dark:text-gray-200 focus:outline-none focus:border-indigo-400 resize-none font-mono"
/>
</div>
{/* English Caption */}
<div className="flex flex-col gap-0.5">
<span className="text-[10px] text-gray-400 font-medium">{translate('::ListForms.Wizard.Step3.EnglishCaption')}</span>
<div className="flex flex-col gap-0.5 min-w-[100px] flex-1">
<span className="text-[10px] text-gray-400 font-medium">
{translate('::ListForms.Wizard.Step3.LookupDisplayExpression')}
</span>
<input
value={item.englishCaption}
onChange={(e) => onEnglishCaptionChange(e.target.value)}
value={item.displayExpr}
onChange={(e) => onDisplayExprChange(e.target.value)}
className="w-full text-xs px-1.5 py-1 rounded border border-gray-200 dark:border-gray-600 bg-gray-50 dark:bg-gray-700 text-gray-700 dark:text-gray-200 focus:outline-none focus:border-indigo-400 resize-none font-mono"
/>
</div>
</div>
{/* LookupQuery */}
<div className="flex flex-col gap-0.5">
<div className="flex items-center gap-1">
<span className="text-[10px] text-gray-400 font-medium flex-1">
{translate('::ListForms.Wizard.Step3.LookupLookupQuery')}
</span>
<button
type="button"
onClick={() => setIsTablePickerOpen(true)}
className="flex items-center gap-1 px-1.5 py-0.5 text-[10px] font-medium rounded border border-indigo-200 dark:border-indigo-700 bg-indigo-50 dark:bg-indigo-900/20 text-indigo-600 dark:text-indigo-400 hover:bg-indigo-100 dark:hover:bg-indigo-800/40 transition-colors shrink-0"
>
<FaPlus className="text-[8px]" />
{translate('::ListForms.Wizard.Step3.GenerateFromTable') || 'Tablodan Oluştur'}
</button>
</div>
<textarea
rows={2}
value={item.lookupQuery}
onChange={(e) => onLookupQueryChange(e.target.value)}
className="w-full text-xs px-1.5 py-1 rounded border border-gray-200 dark:border-gray-600 bg-gray-50 dark:bg-gray-700 text-gray-700 dark:text-gray-200 focus:outline-none focus:border-indigo-400 resize-none font-mono"
/>
</div>
{/* Table Picker Modal */}
{isTablePickerOpen && (
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-black/40"
onClick={() => setIsTablePickerOpen(false)}
>
<div
className="bg-white dark:bg-gray-900 rounded-xl shadow-2xl w-full max-w-md mx-4 flex flex-col max-h-[80vh]"
onClick={(e) => e.stopPropagation()}
>
{/* Header */}
<div className="flex items-center justify-between px-4 py-3 border-b border-gray-200 dark:border-gray-700">
<div className="flex items-center gap-2">
{pickerStep === 'columns' && (
<button
type="button"
onClick={() => setPickerStep('table')}
className="text-gray-400 hover:text-indigo-500 transition-colors"
>
<FaArrowLeft className="text-xs" />
</button>
)}
<span className="text-sm font-semibold text-gray-700 dark:text-gray-200">
{pickerStep === 'table'
? translate('::ListForms.Wizard.Step3.SelectTable') || 'Tablo Seç'
: (pickerTable?.tableName ?? '')}
</span>
</div>
<button
type="button"
onClick={() => setIsTablePickerOpen(false)}
className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-200"
>
<FaTimes />
</button>
</div>
{/* Step 1: Table list */}
{pickerStep === 'table' && (
<>
<div className="px-4 py-2 border-b border-gray-100 dark:border-gray-800">
<input
autoFocus
value={tableSearch}
onChange={(e) => setTableSearch(e.target.value)}
placeholder={translate('::Search') || 'Ara...'}
className="w-full text-xs px-2 py-1.5 rounded border border-gray-200 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 text-gray-700 dark:text-gray-200 focus:outline-none focus:border-indigo-400"
/>
</div>
<div className="overflow-y-auto flex-1 p-2">
{!dbObjects ? (
<div className="text-xs text-gray-400 text-center py-6">
{translate('::ListForms.Wizard.Step3.NoTablesAvailable') ||
'Tablo bulunamadı'}
</div>
) : (
dbObjects.tables
.filter((t) => t.tableName.toLowerCase().includes(tableSearch.toLowerCase()))
.map((t) => (
<button
key={t.fullName}
type="button"
onClick={async () => {
setPickerTable(t)
setPickerStep('columns')
setPickerKeyCol('')
setPickerNameCol('')
setPickerColumns([])
setIsLoadingPickerColumns(true)
try {
const res = await sqlObjectManagerService.getTableColumns(
dsCode,
t.schemaName,
t.tableName,
)
setPickerColumns(res.data ?? [])
} catch {
setPickerColumns([])
} finally {
setIsLoadingPickerColumns(false)
}
}}
className="w-full text-left text-xs px-3 py-2 rounded hover:bg-indigo-50 dark:hover:bg-indigo-900/30 text-gray-700 dark:text-gray-200 font-mono transition-colors"
>
<span className="text-gray-400 mr-1">{t.schemaName}.</span>
{t.tableName}
</button>
))
)}
</div>
</>
)}
{/* Step 2: Column selection */}
{pickerStep === 'columns' && (
<div className="flex flex-col gap-3 p-4">
{isLoadingPickerColumns ? (
<div className="text-xs text-gray-400 text-center py-6">
{translate('::Loading') || 'Yükleniyor...'}
</div>
) : (
<>
<div className="flex flex-col gap-1">
<label className="text-[10px] font-medium text-gray-500 dark:text-gray-400">
Key Sütunu
</label>
<select
value={pickerKeyCol}
onChange={(e) => setPickerKeyCol(e.target.value)}
className="w-full text-xs h-7 px-1.5 rounded border border-gray-200 dark:border-gray-600 bg-gray-50 dark:bg-gray-700 text-gray-700 dark:text-gray-200 focus:outline-none focus:border-indigo-400"
>
<option value="">-- Seçiniz --</option>
{pickerColumns.map((c) => (
<option key={c.columnName} value={c.columnName}>
{c.columnName}
</option>
))}
</select>
</div>
<div className="flex flex-col gap-1">
<label className="text-[10px] font-medium text-gray-500 dark:text-gray-400">
Name Sütunu
</label>
<select
value={pickerNameCol}
onChange={(e) => setPickerNameCol(e.target.value)}
className="w-full text-xs h-7 px-1.5 rounded border border-gray-200 dark:border-gray-600 bg-gray-50 dark:bg-gray-700 text-gray-700 dark:text-gray-200 focus:outline-none focus:border-indigo-400"
>
<option value="">-- Seçiniz --</option>
{pickerColumns.map((c) => (
<option key={c.columnName} value={c.columnName}>
{c.columnName}
</option>
))}
</select>
</div>
{pickerKeyCol && pickerNameCol && (
<div className="rounded bg-gray-50 dark:bg-gray-800 px-2 py-1.5 text-[10px] font-mono text-gray-500 dark:text-gray-400 break-all">
{`SELECT "${pickerKeyCol}" AS "Key", "${pickerNameCol}" AS "Name" FROM "${pickerTable?.tableName}" ORDER BY "${pickerNameCol}";`}
</div>
)}
<button
type="button"
disabled={!pickerKeyCol || !pickerNameCol}
onClick={() => {
const q = `SELECT "${pickerKeyCol}" AS "Key", "${pickerNameCol}" AS "Name" FROM "${pickerTable?.tableName}" ORDER BY "${pickerNameCol}";`
onLookupQueryChange(q)
setIsTablePickerOpen(false)
}}
className="mt-1 w-full py-1.5 text-xs font-semibold rounded bg-indigo-600 text-white hover:bg-indigo-700 disabled:opacity-40 disabled:cursor-not-allowed transition-colors"
>
Tamam
</button>
</>
)}
</div>
)}
</div>
</div>
)}
{/* Editor Options */}
<div className="flex flex-col gap-0.5">
<span className="text-[10px] text-gray-400 font-medium">{translate('::ListForms.Wizard.Step3.EditorOptions')}</span>
<span className="text-[10px] text-gray-400 font-medium">
{translate('::ListForms.Wizard.Step3.EditorOptions')}
</span>
<input
value={item.editorOptions}
onChange={(e) => onEditorOptionsChange(e.target.value)}
@ -252,7 +550,9 @@ function SortableItem({
{/* Editor Script */}
<div className="flex flex-col gap-0.5">
<span className="text-[10px] text-gray-400 font-medium">{translate('::ListForms.Wizard.Step3.EditorScript')}</span>
<span className="text-[10px] text-gray-400 font-medium">
{translate('::ListForms.Wizard.Step3.EditorScript')}
</span>
<input
value={item.editorScript}
onChange={(e) => onEditorScriptChange(e.target.value)}
@ -264,7 +564,9 @@ function SortableItem({
{/* Bottom row: ColSpan + Required */}
<div className="flex items-center gap-2">
<div className="flex items-center gap-1">
<span className="text-[10px] text-gray-400">{translate('::ListForms.Wizard.Step3.Span')}</span>
<span className="text-[10px] text-gray-400">
{translate('::ListForms.Wizard.Step3.Span')}
</span>
<select
value={item.colSpan}
onChange={(e) => onColSpanChange(Number(e.target.value))}
@ -284,7 +586,9 @@ function SortableItem({
onChange={(e) => onRequiredChange(e.target.checked)}
className="w-3 h-3 accent-red-500"
/>
<span className="text-[10px] text-gray-400">{translate('::ListForms.Wizard.Step3.Required') || 'Required'}</span>
<span className="text-[10px] text-gray-400">
{translate('::ListForms.Wizard.Step3.Required') || 'Required'}
</span>
</label>
</div>
</div>
@ -295,6 +599,8 @@ interface GroupCardProps {
group: WizardGroup
isOver: boolean
hasAvailable: boolean
dbObjects: SqlObjectExplorerDto | null
dsCode: string
onCaptionChange: (val: string) => void
onColCountChange: (val: number) => void
onItemChange: (itemId: string, patch: Partial<WizardGroupItem>) => void
@ -307,6 +613,8 @@ function GroupCard({
group,
isOver,
hasAvailable,
dbObjects,
dsCode,
onCaptionChange,
onColCountChange,
onItemChange,
@ -328,17 +636,19 @@ function GroupCard({
}`}
>
{/* Group Header */}
<div className="flex items-center gap-2 px-3 pt-3 pb-2">
<div className="flex flex-wrap items-center gap-2 px-3 pt-3 pb-2">
<input
type="text"
value={group.caption}
onChange={(e) => onCaptionChange(e.target.value)}
placeholder="Group caption…"
className="flex-1 text-sm font-semibold h-7 px-2 rounded border border-gray-200 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-800 dark:text-gray-100 focus:outline-none focus:border-indigo-400"
className="flex-1 min-w-[120px] text-sm font-semibold h-7 px-2 rounded border border-gray-200 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-800 dark:text-gray-100 focus:outline-none focus:border-indigo-400"
/>
{/* ColCount */}
<div className="flex items-center gap-1 shrink-0">
<span className="text-xs text-gray-400">{translate('::ListForms.Wizard.Step3.Cols') || 'Cols:'}</span>
<span className="text-xs text-gray-400">
{translate('::ListForms.Wizard.Step3.Cols') || 'Cols:'}
</span>
{[1, 2, 3, 4].map((n) => (
<button
key={n}
@ -359,7 +669,10 @@ function GroupCard({
type="button"
onClick={onAddAll}
className="flex items-center gap-1 h-6 px-2 text-[11px] font-medium rounded border border-indigo-200 dark:border-indigo-700 bg-indigo-50 dark:bg-indigo-900/20 text-indigo-600 dark:text-indigo-400 hover:bg-indigo-100 dark:hover:bg-indigo-900/40 transition-colors shrink-0"
title={translate('::ListForms.Wizard.Step3.AddAllToGroupTitle') || 'Tüm mevcut sütunları bu gruba ekle'}
title={
translate('::ListForms.Wizard.Step3.AddAllToGroupTitle') ||
'Tüm mevcut sütunları bu gruba ekle'
}
>
<FaArrowRight className="text-[9px]" />
{translate('::ListForms.Wizard.Step3.AddAll') || 'Tümünü Ekle'}
@ -400,6 +713,9 @@ function GroupCard({
key={item.id}
item={item}
groupColCount={group.colCount}
dbObjects={dbObjects}
dsCode={dsCode}
onCaptionNameChange={(val) => onItemChange(item.id, { captionName: val })}
onTurkishCaptionChange={(val) => onItemChange(item.id, { turkishCaption: val })}
onEnglishCaptionChange={(val) => onItemChange(item.id, { englishCaption: val })}
onEditorTypeChange={(val) => onItemChange(item.id, { editorType: val })}
@ -407,6 +723,12 @@ function GroupCard({
onEditorScriptChange={(val) => onItemChange(item.id, { editorScript: val })}
onColSpanChange={(val) => onItemChange(item.id, { colSpan: val })}
onRequiredChange={(val) => onItemChange(item.id, { isRequired: val })}
onLookupDataSourceTypeChange={(val: UiLookupDataSourceTypeEnum) =>
onItemChange(item.id, { lookupDataSourceType: val })
}
onDisplayExprChange={(val) => onItemChange(item.id, { displayExpr: val })}
onValueExprChange={(val) => onItemChange(item.id, { valueExpr: val })}
onLookupQueryChange={(val) => onItemChange(item.id, { lookupQuery: val })}
onRemove={() => onRemoveItem(item.id)}
/>
))}
@ -423,6 +745,9 @@ const WizardStep3 = ({
selectCommandColumns,
groups,
onGroupsChange,
dbObjects,
isLoadingDbObjects: _isLoadingDbObjects,
dsCode,
translate,
onBack,
onNext,
@ -662,7 +987,8 @@ const WizardStep3 = ({
const validationMsg = hasNoGroups
? translate('::ListForms.Wizard.Step3.AtLeastOneGroup') || 'En az bir grup eklemelisiniz.'
: hasEmptyGroup
? translate('::ListForms.Wizard.Step3.AtLeastOneColumn') || 'Her gruba en az bir sütun eklemelisiniz.'
? translate('::ListForms.Wizard.Step3.AtLeastOneColumn') ||
'Her gruba en az bir sütun eklemelisiniz.'
: ''
return (
@ -673,10 +999,10 @@ const WizardStep3 = ({
onDragEnd={onDragEnd}
>
<div className="pb-20">
<div className="flex gap-4">
<div className="flex flex-col lg:flex-row gap-4">
{/* ── Left: Available Columns ─────────────────────────────────── */}
<div className="w-72 shrink-0">
<div className="sticky top-4">
<div className="w-full lg:w-64 lg:shrink-0">
<div className="lg:sticky lg:top-4">
<div className="flex items-center justify-between mb-2">
<span className="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wide">
{translate('::App.Listform.ListformField.Column')}
@ -685,10 +1011,11 @@ const WizardStep3 = ({
{availableColumns.length}/{selectedColumns.size}
</span>
</div>
<div className="flex flex-col gap-1.5 max-h-[calc(100vh-280px)] overflow-y-auto pr-1">
<div className="flex flex-row flex-wrap lg:flex-col gap-1.5 max-h-40 lg:max-h-[calc(100vh-280px)] overflow-y-auto pr-1">
{availableColumns.length === 0 ? (
<div className="text-xs text-gray-300 dark:text-gray-600 py-4 text-center select-none">
{translate('::ListForms.Wizard.Step3.AllColumnsAdded') || 'Tüm sütunlar gruplara eklendi'}
<div className="text-xs text-gray-300 dark:text-gray-600 py-4 text-center select-none w-full">
{translate('::ListForms.Wizard.Step3.AllColumnsAdded') ||
'Tüm sütunlar gruplara eklendi'}
</div>
) : (
availableColumns.map((col) => <AvailableColumnChip key={col} colName={col} />)
@ -701,7 +1028,8 @@ const WizardStep3 = ({
<div className="flex-1 flex flex-col gap-3">
{groups.length === 0 && (
<div className="rounded-xl border-2 border-dashed border-gray-200 dark:border-gray-700 flex items-center justify-center h-36 text-sm text-gray-300 dark:text-gray-600 select-none">
{translate('::ListForms.Wizard.Step3.NoGroupsYet') || 'Henüz grup yok — aşağıdan grup ekleyin'}
{translate('::ListForms.Wizard.Step3.NoGroupsYet') ||
'Henüz grup yok — aşağıdan grup ekleyin'}
</div>
)}
@ -711,6 +1039,8 @@ const WizardStep3 = ({
group={group}
isOver={overGroupId === group.id}
hasAvailable={availableColumns.length > 0}
dbObjects={dbObjects}
dsCode={dsCode}
onCaptionChange={(val) => updateGroup(group.id, { caption: val })}
onColCountChange={(val) => updateGroup(group.id, { colCount: val })}
onItemChange={(itemId, patch) => updateItem(group.id, itemId, patch)}
@ -793,19 +1123,34 @@ const WizardStep3 = ({
</Dialog>
{/* ── 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-0 h-16 flex items-center">
<div className="flex items-center gap-3 w-full">
<Button size='sm' variant="default" type="button" icon={<FaArrowLeft />} onClick={onBack}>
<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-4 py-2 min-h-16 flex items-center">
<div className="flex flex-wrap items-center gap-2 w-full">
<Button size="sm" variant="default" type="button" icon={<FaArrowLeft />} onClick={onBack}>
{translate('::Back') || 'Back'}
</Button>
<Button size='sm' variant="default" type="button" icon={<FaCode />} onClick={() => setIsHelperOpen(true)}>
<Button
size="sm"
variant="default"
type="button"
icon={<FaCode />}
onClick={() => setIsHelperOpen(true)}
>
{translate('::Helper Codes') || 'Helper Codes'}
</Button>
<div className="flex-1 flex items-center justify-end gap-3">
{!canProceed && (
<span className="text-xs text-amber-600 dark:text-amber-400 font-medium"> {validationMsg}</span>
<span className="text-xs text-amber-600 dark:text-amber-400 font-medium">
{validationMsg}
</span>
)}
<Button size='sm' variant="solid" type="button" icon={<FaArrowRight />} disabled={!canProceed} onClick={onNext}>
<Button
size="sm"
variant="solid"
type="button"
icon={<FaArrowRight />}
disabled={!canProceed}
onClick={onNext}
>
{translate('::Next') || 'Next'}
</Button>
</div>

View file

@ -1,9 +1,14 @@
import { Container } from '@/components/shared'
import { Button, Checkbox, FormContainer, FormItem, Input, Select } from '@/components/ui'
import { ColumnFormatEditDto, ListFormFieldEditTabs } from '@/proxy/admin/list-form-field/models'
import type { DatabaseColumnDto, SqlObjectExplorerDto } from '@/proxy/sql-query-manager/models'
import { sqlObjectManagerService } from '@/services/sql-query-manager.service'
import { SelectBoxOption } from '@/types/shared'
import { useLocalization } from '@/utils/hooks/useLocalization'
import { useStoreState } from '@/store/store'
import { Field, FieldProps, Form, Formik } from 'formik'
import { useState } from 'react'
import { FaArrowLeft, FaPlus, FaTimes } from 'react-icons/fa'
import { number, object, string } from 'yup'
import { cascadeFilterOperator, columnLookupDataSourceTypeListOptions } from '../options'
import { FormFieldEditProps } from './FormFields'
@ -26,6 +31,177 @@ const schema = object().shape({
.required(),
})
// ─── Table Picker Modal ───────────────────────────────────────────────────────
function TablePickerModal({
dsCode,
dbObjects,
onSelect,
onClose,
}: {
dsCode: string
dbObjects: SqlObjectExplorerDto | null
onSelect: (query: string) => void
onClose: () => void
}) {
const { translate } = useLocalization()
const [step, setStep] = useState<'table' | 'columns'>('table')
const [tableSearch, setTableSearch] = useState('')
const [pickerTable, setPickerTable] = useState<{ schemaName: string; tableName: string } | null>(null)
const [pickerColumns, setPickerColumns] = useState<DatabaseColumnDto[]>([])
const [isLoadingColumns, setIsLoadingColumns] = useState(false)
const [keyCol, setKeyCol] = useState('')
const [nameCol, setNameCol] = useState('')
return (
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-black/40"
onClick={onClose}
>
<div
className="bg-white dark:bg-gray-900 rounded-xl shadow-2xl w-full max-w-md mx-4 flex flex-col max-h-[80vh]"
onClick={(e) => e.stopPropagation()}
>
{/* Header */}
<div className="flex items-center justify-between px-4 py-3 border-b border-gray-200 dark:border-gray-700">
<div className="flex items-center gap-2">
{step === 'columns' && (
<button
type="button"
onClick={() => setStep('table')}
className="text-gray-400 hover:text-indigo-500 transition-colors"
>
<FaArrowLeft className="text-xs" />
</button>
)}
<span className="text-sm font-semibold text-gray-700 dark:text-gray-200">
{step === 'table'
? translate('::ListForms.Wizard.Step3.SelectTable') || 'Tablo Seç'
: pickerTable?.tableName ?? ''}
</span>
</div>
<button
type="button"
onClick={onClose}
className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-200"
>
<FaTimes />
</button>
</div>
{/* Step 1: Table list */}
{step === 'table' && (
<>
<div className="px-4 py-2 border-b border-gray-100 dark:border-gray-800">
<input
autoFocus
value={tableSearch}
onChange={(e) => setTableSearch(e.target.value)}
placeholder={translate('::Search') || 'Ara...'}
className="w-full text-xs px-2 py-1.5 rounded border border-gray-200 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 text-gray-700 dark:text-gray-200 focus:outline-none focus:border-indigo-400"
/>
</div>
<div className="overflow-y-auto flex-1 p-2">
{!dbObjects ? (
<div className="text-xs text-gray-400 text-center py-6">
{translate('::ListForms.Wizard.Step3.NoTablesAvailable') || 'Tablo bulunamadı'}
</div>
) : (
dbObjects.tables
.filter((t) => t.tableName.toLowerCase().includes(tableSearch.toLowerCase()))
.map((t) => (
<button
key={t.fullName}
type="button"
onClick={async () => {
setPickerTable(t)
setStep('columns')
setKeyCol('')
setNameCol('')
setPickerColumns([])
setIsLoadingColumns(true)
try {
const res = await sqlObjectManagerService.getTableColumns(dsCode, t.schemaName, t.tableName)
setPickerColumns(res.data ?? [])
} catch {
setPickerColumns([])
} finally {
setIsLoadingColumns(false)
}
}}
className="w-full text-left text-xs px-3 py-2 rounded hover:bg-indigo-50 dark:hover:bg-indigo-900/30 text-gray-700 dark:text-gray-200 font-mono transition-colors"
>
<span className="text-gray-400 mr-1">{t.schemaName}.</span>
{t.tableName}
</button>
))
)}
</div>
</>
)}
{/* Step 2: Column selection */}
{step === 'columns' && (
<div className="flex flex-col gap-3 p-4">
{isLoadingColumns ? (
<div className="text-xs text-gray-400 text-center py-6">
{translate('::Loading') || 'Yükleniyor...'}
</div>
) : (
<>
<div className="flex flex-col gap-1">
<label className="text-[11px] font-medium text-gray-500 dark:text-gray-400">
Key Sütunu
</label>
<select
value={keyCol}
onChange={(e) => setKeyCol(e.target.value)}
className="w-full text-xs h-8 px-2 rounded border border-gray-200 dark:border-gray-600 bg-gray-50 dark:bg-gray-700 text-gray-700 dark:text-gray-200 focus:outline-none focus:border-indigo-400"
>
<option value="">-- Seçiniz --</option>
{pickerColumns.map((c) => (
<option key={c.columnName} value={c.columnName}>{c.columnName}</option>
))}
</select>
</div>
<div className="flex flex-col gap-1">
<label className="text-[11px] font-medium text-gray-500 dark:text-gray-400">
Name Sütunu
</label>
<select
value={nameCol}
onChange={(e) => setNameCol(e.target.value)}
className="w-full text-xs h-8 px-2 rounded border border-gray-200 dark:border-gray-600 bg-gray-50 dark:bg-gray-700 text-gray-700 dark:text-gray-200 focus:outline-none focus:border-indigo-400"
>
<option value="">-- Seçiniz --</option>
{pickerColumns.map((c) => (
<option key={c.columnName} value={c.columnName}>{c.columnName}</option>
))}
</select>
</div>
{keyCol && nameCol && (
<div className="rounded bg-gray-50 dark:bg-gray-800 px-2 py-1.5 text-[10px] font-mono text-gray-500 dark:text-gray-400 break-all">
{`SELECT "${keyCol}" AS "Key", "${nameCol}" AS "Name" FROM "${pickerTable?.tableName}" ORDER BY "${nameCol}";`}
</div>
)}
<button
type="button"
disabled={!keyCol || !nameCol}
onClick={() => {
onSelect(`SELECT "${keyCol}" AS "Key", "${nameCol}" AS "Name" FROM "${pickerTable?.tableName}" ORDER BY "${nameCol}";`)
}}
className="mt-1 w-full py-1.5 text-xs font-semibold rounded bg-indigo-600 text-white hover:bg-indigo-700 disabled:opacity-40 disabled:cursor-not-allowed transition-colors"
>
Tamam
</button>
</>
)}
</div>
)}
</div>
</div>
)
}
function getNormalizedInitialValues(initialValues: ColumnFormatEditDto) {
return {
...initialValues,
@ -52,6 +228,22 @@ function FormFieldTabLookup({
initialValues: ColumnFormatEditDto
} & FormFieldEditProps) {
const { translate } = useLocalization()
const listFormValues = useStoreState((s) => s.admin.lists.values)
const dsCode = listFormValues?.dataSourceCode ?? ''
const [dbObjects, setDbObjects] = useState<SqlObjectExplorerDto | null>(null)
const [isTablePickerOpen, setIsTablePickerOpen] = useState(false)
const openTablePicker = async () => {
if (dsCode && !dbObjects) {
try {
const res = await sqlObjectManagerService.getAllObjects(dsCode)
setDbObjects(res.data)
} catch {
setDbObjects(null)
}
}
setIsTablePickerOpen(true)
}
return (
<Container className="grid xl:grid-cols-2">
@ -94,17 +286,42 @@ function FormFieldTabLookup({
<FormItem
label={translate('::ListForms.ListFormFieldEdit.LookupLookupQuery')}
extra={
<button
type="button"
onClick={openTablePicker}
className="ml-2 flex items-center gap-1 px-1.5 py-0.5 text-[10px] font-medium rounded border border-indigo-200 dark:border-indigo-700 bg-indigo-50 dark:bg-indigo-900/20 text-indigo-600 dark:text-indigo-400 hover:bg-indigo-100 dark:hover:bg-indigo-800/40 transition-colors"
>
<FaPlus className="text-[8px]" />
{translate('::ListForms.Wizard.Step3.GenerateFromTable') || 'Tablodan Oluştur'}
</button>
}
invalid={errors.lookupDto?.lookupQuery && touched.lookupDto?.lookupQuery}
errorMessage={errors.lookupDto?.lookupQuery}
>
<Field
type="text"
<Field name="lookupDto.lookupQuery">
{({ field, form }: FieldProps<string>) => (
<>
<Input
{...field}
textArea
autoComplete="off"
name="lookupDto.lookupQuery"
placeholder={translate('::ListForms.ListFormFieldEdit.LookupLookupQuery')}
component={Input}
textArea={true}
/>
{isTablePickerOpen && (
<TablePickerModal
dsCode={dsCode}
dbObjects={dbObjects}
onSelect={(q) => {
form.setFieldValue(field.name, q)
setIsTablePickerOpen(false)
}}
onClose={() => setIsTablePickerOpen(false)}
/>
)}
</>
)}
</Field>
</FormItem>
<FormItem

View file

@ -167,7 +167,6 @@ export function MenuAddDialog({
}, [isOpen, initialParentCode, initialOrder])
const shortNameRequired = !form.parentCode.trim()
console.log(form.parentCode.length)
const handleSave = async () => {
if (!form.code.trim() || !form.menuTextEn.trim()) return
if (shortNameRequired && !form.shortName.trim()) return