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 AllowDetail { get; set; }
public bool ConfirmDelete { 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 LanguageTextMenuEn { get; set; }
public string LanguageTextMenuTr { get; set; } public string LanguageTextMenuTr { get; set; }
public string LanguageTextTitleEn { get; set; } public string LanguageTextTitleEn { get; set; }

View file

@ -1,10 +1,12 @@
using System.Data; using System.Data;
using Sozsoft.Platform.Enums;
namespace Sozsoft.Platform.ListForms; namespace Sozsoft.Platform.ListForms;
public class WizardColumnItemInputDto public class WizardColumnItemInputDto
{ {
public string DataField { get; set; } public string DataField { get; set; }
public string CaptionName { get; set; }
public string EditorType { get; set; } public string EditorType { get; set; }
public string EditorOptions { get; set; } public string EditorOptions { get; set; }
public string EditorScript { get; set; } public string EditorScript { get; set; }
@ -13,4 +15,8 @@ public class WizardColumnItemInputDto
public DbType DbSourceType { get; set; } = DbType.String; public DbType DbSourceType { get; set; } = DbType.String;
public string TurkishCaption { get; set; } public string TurkishCaption { get; set; }
public string EnglishCaption { 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;
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;
@ -16,6 +17,7 @@ using static Sozsoft.Platform.PlatformConsts;
using System.Data; using System.Data;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Sozsoft.Languages; using Sozsoft.Languages;
using Sozsoft.Platform.DynamicData;
namespace Sozsoft.Platform.ListForms; namespace Sozsoft.Platform.ListForms;
@ -30,7 +32,8 @@ public class ListFormWizardAppService(
IRepository<Menu, Guid> repoMenu, IRepository<Menu, Guid> repoMenu,
IPermissionGrantRepository permissionGrantRepository, IPermissionGrantRepository permissionGrantRepository,
IConfiguration configuration, IConfiguration configuration,
LanguageTextAppService languageTextAppService LanguageTextAppService languageTextAppService,
IDynamicDataManager dynamicDataManager
) : PlatformAppService(), IListFormWizardAppService ) : PlatformAppService(), IListFormWizardAppService
{ {
private readonly IRepository<ListForm, Guid> repoListForm = repoListForm; private readonly IRepository<ListForm, Guid> repoListForm = repoListForm;
@ -44,6 +47,7 @@ public class ListFormWizardAppService(
private readonly IPermissionGrantRepository permissionGrantRepository = permissionGrantRepository; private readonly IPermissionGrantRepository permissionGrantRepository = permissionGrantRepository;
private readonly IConfiguration _configuration = configuration; private readonly IConfiguration _configuration = configuration;
private readonly LanguageTextAppService _languageTextAppService = languageTextAppService; private readonly LanguageTextAppService _languageTextAppService = languageTextAppService;
private readonly IDynamicDataManager _dynamicDataManager = dynamicDataManager;
private readonly string cultureNameDefault = PlatformConsts.DefaultLanguage; private readonly string cultureNameDefault = PlatformConsts.DefaultLanguage;
[UnitOfWork] [UnitOfWork]
@ -194,6 +198,10 @@ public class ListFormWizardAppService(
await repoListFormField.DeleteManyAsync(existingListFormFields, autoSave: true); 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 var listForm = await repoListForm.InsertAsync(new ListForm
{ {
ListFormType = ListFormTypeEnum.List, ListFormType = ListFormTypeEnum.List,
@ -201,7 +209,7 @@ public class ListFormWizardAppService(
ExportJson = WizardConsts.DefaultExportJson, ExportJson = WizardConsts.DefaultExportJson,
IsSubForm = false, IsSubForm = false,
ShowNote = true, 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, CultureName = LanguageCodes.En,
ListFormCode = input.ListFormCode, ListFormCode = input.ListFormCode,
Name = nameLangKey, Name = nameLangKey,
@ -215,7 +223,7 @@ public class ListFormWizardAppService(
SelectCommand = input.SelectCommand, SelectCommand = input.SelectCommand,
KeyFieldName = input.KeyFieldName, KeyFieldName = input.KeyFieldName,
KeyFieldDbSourceType = input.KeyFieldDbSourceType, KeyFieldDbSourceType = input.KeyFieldDbSourceType,
DefaultFilter = WizardConsts.DefaultFilterJson, DefaultFilter = isDeleted ? WizardConsts.DefaultFilterJson : null,
SortMode = GridOptions.SortModeSingle, SortMode = GridOptions.SortModeSingle,
FilterRowJson = WizardConsts.DefaultFilterRowJson, FilterRowJson = WizardConsts.DefaultFilterRowJson,
HeaderFilterJson = WizardConsts.DefaultHeaderFilterJson, HeaderFilterJson = WizardConsts.DefaultHeaderFilterJson,
@ -224,9 +232,9 @@ public class ListFormWizardAppService(
SelectionJson = WizardConsts.DefaultSelectionSingleJson, SelectionJson = WizardConsts.DefaultSelectionSingleJson,
ColumnOptionJson = WizardConsts.DefaultColumnOptionJson(), ColumnOptionJson = WizardConsts.DefaultColumnOptionJson(),
PermissionJson = WizardConsts.DefaultPermissionJson(code), PermissionJson = WizardConsts.DefaultPermissionJson(code),
DeleteCommand = WizardConsts.DefaultDeleteCommand(input.SelectCommand), DeleteCommand = isDeleted ? WizardConsts.DefaultDeleteCommand(input.SelectCommand) : null,
DeleteFieldsDefaultValueJson = WizardConsts.DefaultDeleteFieldsDefaultValueJson(input.KeyFieldDbSourceType), DeleteFieldsDefaultValueJson = isDeleted ? WizardConsts.DefaultDeleteFieldsDefaultValueJson(input.KeyFieldDbSourceType) : WizardConsts.DefaultFieldsJsonOnlyId(input.KeyFieldDbSourceType),
InsertFieldsDefaultValueJson = WizardConsts.DefaultInsertFieldsDefaultValueJson(input.KeyFieldDbSourceType), InsertFieldsDefaultValueJson = isCreated ? WizardConsts.DefaultInsertFieldsDefaultValueJson(input.KeyFieldDbSourceType) : WizardConsts.DefaultFieldsJsonOnlyId(input.KeyFieldDbSourceType),
PagerOptionJson = WizardConsts.DefaultPagerOptionJson, PagerOptionJson = WizardConsts.DefaultPagerOptionJson,
EditingOptionJson = WizardConsts.DefaultEditingOptionJson(titleLangKey, 600, 500, input.AllowDeleting, input.AllowAdding, input.AllowUpdating, input.ConfirmDelete, false, input.AllowDetail), 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, 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 // ListFormField - each item in each group becomes a visible field record
var fieldOrder = 0; var fieldOrder = 0;
var captionName = string.Empty;
foreach (var group in input.Groups) foreach (var group in input.Groups)
{ {
foreach (var item in group.Items) foreach (var item in group.Items)
{ {
fieldOrder++; fieldOrder++;
captionName = $"App.Listform.ListformField.{item.DataField}";
await repoListFormField.InsertAsync(new ListFormField await repoListFormField.InsertAsync(new ListFormField
{ {
ListFormCode = input.ListFormCode, ListFormCode = input.ListFormCode,
FieldName = item.DataField, FieldName = item.DataField,
CaptionName = captionName, CaptionName = item.CaptionName,
Visible = item.DataField != input.KeyFieldName, Visible = item.DataField != input.KeyFieldName,
IsActive = true, IsActive = true,
AllowSearch = true, AllowSearch = true,
@ -256,9 +262,10 @@ public class ListFormWizardAppService(
ColumnCustomizationJson = WizardConsts.DefaultColumnCustomizationJson, ColumnCustomizationJson = WizardConsts.DefaultColumnCustomizationJson,
ColumnFilterJson = WizardConsts.DefaultColumnFilteringJson, ColumnFilterJson = WizardConsts.DefaultColumnFilteringJson,
PivotSettingsJson = WizardConsts.DefaultPivotSettingsJson, 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) private async Task<LanguageKey> CreateLangKey(string key, string textEn, string textTr)
{ {
var res = PlatformConsts.AppName; var res = PlatformConsts.AppName;
var keyQueryable = await repoLangKey.GetQueryableAsync(); var langKey = await repoLangKey.FirstOrDefaultAsync(a => a.ResourceName == res && a.Key == key)
var langKey = await AsyncExecuter.FirstOrDefaultAsync(keyQueryable.Where(a => a.ResourceName == res && a.Key == key)) ?? await repoLangKey.InsertAsync(new LanguageKey { ResourceName = res, Key = key }, autoSave: true);
?? await repoLangKey.InsertAsync(new LanguageKey { ResourceName = res, Key = key }, autoSave: false);
var textQueryable = await repoLangText.GetQueryableAsync(); var existingTexts = await repoLangText.GetListAsync(a => a.ResourceName == res && a.Key == langKey.Key);
var existingTexts = await AsyncExecuter.ToListAsync(
textQueryable.Where(a => a.ResourceName == res && a.Key == langKey.Key)
);
var langTextEn = existingTexts.FirstOrDefault(a => a.CultureName == cultureNameDefault) var existingEn = existingTexts.FirstOrDefault(a => a.CultureName == cultureNameDefault);
?? await repoLangText.InsertAsync(new LanguageText { ResourceName = res, Key = langKey.Key, CultureName = cultureNameDefault, Value = textEn }, autoSave: false); 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) var existingTr = existingTexts.FirstOrDefault(a => a.CultureName == LanguageCodes.Tr);
?? await repoLangText.InsertAsync(new LanguageText { ResourceName = res, Key = langKey.Key, CultureName = LanguageCodes.Tr, Value = textTr }, autoSave: false); 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; return langKey;
} }

View file

@ -16280,6 +16280,42 @@
"en": "Columns load after selecting a Select Command", "en": "Columns load after selecting a Select Command",
"tr": "Select Command seçince sütunlar yüklenir" "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", "resourceName": "Platform",
"key": "ListForms.Wizard.Step3.TurkishCaption", "key": "ListForms.Wizard.Step3.TurkishCaption",
@ -16292,6 +16328,12 @@
"en": "English Caption", "en": "English Caption",
"tr": "İngilizce Başlık" "tr": "İngilizce Başlık"
}, },
{
"resourceName": "Platform",
"key": "ListForms.Wizard.Step3.EditorType",
"en": "Editor Type",
"tr": "Editör Türü"
},
{ {
"resourceName": "Platform", "resourceName": "Platform",
"key": "ListForms.Wizard.Step3.EditorOptions", "key": "ListForms.Wizard.Step3.EditorOptions",

View file

@ -78,14 +78,14 @@ public static class WizardConsts
Margin = 10 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, Grid = Grid,
Pivot = true, Pivot = Pivot,
Chart = true, Chart = Chart,
Tree = true, Tree = Tree,
Gantt = true, Gantt = Gantt,
Scheduler = true, Scheduler = Scheduler,
DefaultLayout = DefaultLayout, DefaultLayout = DefaultLayout,
}); });
@ -153,6 +153,14 @@ public static class WizardConsts
IsPivot = true 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) public static string DefaultDeleteCommand(string tableName)
{ {
return $"UPDATE \"{tableName}\" SET \"DeleterId\"=@DeleterId, \"DeletionTime\"=CURRENT_TIMESTAMP, \"IsDeleted\"='true' WHERE \"Id\"=@Id"; 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 public static readonly string DefaultPagerOptionJson = JsonSerializer.Serialize(new
{ {
Visible = true, Visible = true,

View file

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

View file

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

View file

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

View file

@ -3,7 +3,12 @@ import { ListFormWizardDto } from '@/proxy/admin/list-form/models'
import { 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, sqlDataTypeToDbType } from './edit/options' import {
dbSourceTypeOptions,
listFormDefaultLayoutOptions,
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' import { FaArrowLeft, FaArrowRight } from 'react-icons/fa'
@ -395,6 +400,121 @@ const WizardStep2 = ({
</div> </div>
</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"> <div className="grid grid-cols-1 md:grid-cols-2 gap-x-6">
<FormItem <FormItem
label={translate('::ListForms.Wizard.Step2.TitleTextEnglish')} label={translate('::ListForms.Wizard.Step2.TitleTextEnglish')}
@ -474,14 +594,14 @@ const WizardStep2 = ({
selectCommandColumns.length > 0 ? ( selectCommandColumns.length > 0 ? (
<div className="flex items-center gap-2 ml-3"> <div className="flex items-center gap-2 ml-3">
<Button <Button
variant='solid' variant="solid"
onClick={() => onToggleAllColumns(true)} onClick={() => onToggleAllColumns(true)}
className="text-xs px-2 py-0.5 rounded bg-indigo-500 text-white hover:bg-indigo-600" 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ç'} {translate('::ListForms.Wizard.Step2.SelectAll') || 'Tümünü Seç'}
</Button> </Button>
<Button <Button
variant='default' variant="default"
onClick={() => onToggleAllColumns(false)} 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" 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 { Button, Dialog } from '@/components/ui'
import { useLocalization } from '@/utils/hooks/useLocalization' import { useLocalization } from '@/utils/hooks/useLocalization'
import { columnEditorTypeListOptions } from '@/views/admin/listForm/edit/options' import {
import type { DatabaseColumnDto } from '@/proxy/sql-query-manager/models' 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 { import {
DndContext, DndContext,
DragEndEvent, DragEndEvent,
@ -16,7 +20,16 @@ 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 { 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 ──────────────────────────────────────────────────────────────────── // ─── Types ────────────────────────────────────────────────────────────────────
export interface WizardGroupItem { export interface WizardGroupItem {
@ -29,6 +42,11 @@ export interface WizardGroupItem {
isRequired: boolean isRequired: boolean
turkishCaption?: string turkishCaption?: string
englishCaption?: string englishCaption?: string
captionName?: string
lookupDataSourceType?: UiLookupDataSourceTypeEnum
valueExpr?: string
displayExpr?: string
lookupQuery?: string
} }
export interface WizardGroup { export interface WizardGroup {
@ -43,6 +61,9 @@ export interface WizardStep3Props {
selectCommandColumns: DatabaseColumnDto[] selectCommandColumns: DatabaseColumnDto[]
groups: WizardGroup[] groups: WizardGroup[]
onGroupsChange: (groups: WizardGroup[]) => void onGroupsChange: (groups: WizardGroup[]) => void
dbObjects: SqlObjectExplorerDto | null
isLoadingDbObjects: boolean
dsCode: string
translate: (key: string) => string translate: (key: string) => string
onBack: () => void onBack: () => void
onNext: () => void onNext: () => void
@ -73,16 +94,15 @@ function inferEditorType(sqlType: string): string {
} }
const formatLabel = (text: string) => { const formatLabel = (text: string) => {
return text return (
text
// CamelCase → kelimelere ayır // CamelCase → kelimelere ayır
.split(/(?=[A-Z])/) .split(/(?=[A-Z])/)
.filter(Boolean) .filter(Boolean)
.map(word => .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
word.charAt(0).toUpperCase() + .join(' ')
word.slice(1).toLowerCase()
) )
.join(" "); }
};
function newGroupItem(colName: string, meta?: DatabaseColumnDto): WizardGroupItem { function newGroupItem(colName: string, meta?: DatabaseColumnDto): WizardGroupItem {
const sqlType = meta?.dataType ?? '' const sqlType = meta?.dataType ?? ''
@ -96,6 +116,11 @@ function newGroupItem(colName: string, meta?: DatabaseColumnDto): WizardGroupIte
isRequired: meta?.isNullable === false, isRequired: meta?.isNullable === false,
turkishCaption: formatLabel(colName), turkishCaption: formatLabel(colName),
englishCaption: 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 { interface SortableItemProps {
item: WizardGroupItem item: WizardGroupItem
groupColCount: number groupColCount: number
dbObjects: SqlObjectExplorerDto | null
dsCode: string
onTurkishCaptionChange: (val: string) => void onTurkishCaptionChange: (val: string) => void
onEnglishCaptionChange: (val: string) => void onEnglishCaptionChange: (val: string) => void
onEditorTypeChange: (val: string) => void onEditorTypeChange: (val: string) => void
onEditorOptionsChange: (val: string) => void onEditorOptionsChange: (val: string) => void
onEditorScriptChange: (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 onColSpanChange: (val: number) => void
onRequiredChange: (val: boolean) => void onRequiredChange: (val: boolean) => void
onRemove: () => void onRemove: () => void
@ -153,6 +185,8 @@ interface SortableItemProps {
function SortableItem({ function SortableItem({
item, item,
groupColCount, groupColCount,
dbObjects,
dsCode,
onTurkishCaptionChange, onTurkishCaptionChange,
onEnglishCaptionChange, onEnglishCaptionChange,
onEditorTypeChange, onEditorTypeChange,
@ -160,9 +194,24 @@ function SortableItem({
onEditorScriptChange, onEditorScriptChange,
onColSpanChange, onColSpanChange,
onRequiredChange, onRequiredChange,
onCaptionNameChange,
onLookupDataSourceTypeChange,
onDisplayExprChange,
onValueExprChange,
onLookupQueryChange,
onRemove, onRemove,
}: SortableItemProps) { }: SortableItemProps) {
const { translate } = useLocalization() 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({ const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
id: `${ITM_PREFIX}${item.id}`, id: `${ITM_PREFIX}${item.id}`,
}) })
@ -206,7 +255,46 @@ function SortableItem({
</button> </button>
</div> </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 */} {/* 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 <select
value={item.editorType} value={item.editorType}
onChange={(e) => onEditorTypeChange(e.target.value)} onChange={(e) => onEditorTypeChange(e.target.value)}
@ -218,30 +306,240 @@ function SortableItem({
</option> </option>
))} ))}
</select> </select>
</div>
{/* Turkish Caption */} <div className="flex flex-row flex-wrap gap-1.5">
<div className="flex flex-col gap-0.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.TurkishCaption')}</span> <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 <input
value={item.turkishCaption} value={item.valueExpr}
onChange={(e) => onTurkishCaptionChange(e.target.value)} 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" 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>
{/* English Caption */} <div className="flex flex-col gap-0.5 min-w-[100px] flex-1">
<div className="flex flex-col gap-0.5"> <span className="text-[10px] text-gray-400 font-medium">
<span className="text-[10px] text-gray-400 font-medium">{translate('::ListForms.Wizard.Step3.EnglishCaption')}</span> {translate('::ListForms.Wizard.Step3.LookupDisplayExpression')}
</span>
<input <input
value={item.englishCaption} value={item.displayExpr}
onChange={(e) => onEnglishCaptionChange(e.target.value)} 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" 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>
</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 */} {/* Editor Options */}
<div className="flex flex-col gap-0.5"> <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 <input
value={item.editorOptions} value={item.editorOptions}
onChange={(e) => onEditorOptionsChange(e.target.value)} onChange={(e) => onEditorOptionsChange(e.target.value)}
@ -252,7 +550,9 @@ function SortableItem({
{/* Editor Script */} {/* Editor Script */}
<div className="flex flex-col gap-0.5"> <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 <input
value={item.editorScript} value={item.editorScript}
onChange={(e) => onEditorScriptChange(e.target.value)} onChange={(e) => onEditorScriptChange(e.target.value)}
@ -264,7 +564,9 @@ function SortableItem({
{/* Bottom row: ColSpan + Required */} {/* Bottom row: ColSpan + Required */}
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="flex items-center gap-1"> <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 <select
value={item.colSpan} value={item.colSpan}
onChange={(e) => onColSpanChange(Number(e.target.value))} onChange={(e) => onColSpanChange(Number(e.target.value))}
@ -284,7 +586,9 @@ function SortableItem({
onChange={(e) => onRequiredChange(e.target.checked)} onChange={(e) => onRequiredChange(e.target.checked)}
className="w-3 h-3 accent-red-500" 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> </label>
</div> </div>
</div> </div>
@ -295,6 +599,8 @@ interface GroupCardProps {
group: WizardGroup group: WizardGroup
isOver: boolean isOver: boolean
hasAvailable: boolean hasAvailable: boolean
dbObjects: SqlObjectExplorerDto | null
dsCode: string
onCaptionChange: (val: string) => void onCaptionChange: (val: string) => void
onColCountChange: (val: number) => void onColCountChange: (val: number) => void
onItemChange: (itemId: string, patch: Partial<WizardGroupItem>) => void onItemChange: (itemId: string, patch: Partial<WizardGroupItem>) => void
@ -307,6 +613,8 @@ function GroupCard({
group, group,
isOver, isOver,
hasAvailable, hasAvailable,
dbObjects,
dsCode,
onCaptionChange, onCaptionChange,
onColCountChange, onColCountChange,
onItemChange, onItemChange,
@ -328,17 +636,19 @@ function GroupCard({
}`} }`}
> >
{/* Group Header */} {/* 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 <input
type="text" type="text"
value={group.caption} value={group.caption}
onChange={(e) => onCaptionChange(e.target.value)} onChange={(e) => onCaptionChange(e.target.value)}
placeholder="Group caption…" 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 */} {/* ColCount */}
<div className="flex items-center gap-1 shrink-0"> <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) => ( {[1, 2, 3, 4].map((n) => (
<button <button
key={n} key={n}
@ -359,7 +669,10 @@ function GroupCard({
type="button" type="button"
onClick={onAddAll} 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" 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]" /> <FaArrowRight className="text-[9px]" />
{translate('::ListForms.Wizard.Step3.AddAll') || 'Tümünü Ekle'} {translate('::ListForms.Wizard.Step3.AddAll') || 'Tümünü Ekle'}
@ -400,6 +713,9 @@ function GroupCard({
key={item.id} key={item.id}
item={item} item={item}
groupColCount={group.colCount} groupColCount={group.colCount}
dbObjects={dbObjects}
dsCode={dsCode}
onCaptionNameChange={(val) => onItemChange(item.id, { captionName: val })}
onTurkishCaptionChange={(val) => onItemChange(item.id, { turkishCaption: val })} onTurkishCaptionChange={(val) => onItemChange(item.id, { turkishCaption: val })}
onEnglishCaptionChange={(val) => onItemChange(item.id, { englishCaption: val })} onEnglishCaptionChange={(val) => onItemChange(item.id, { englishCaption: val })}
onEditorTypeChange={(val) => onItemChange(item.id, { editorType: val })} onEditorTypeChange={(val) => onItemChange(item.id, { editorType: val })}
@ -407,6 +723,12 @@ function GroupCard({
onEditorScriptChange={(val) => onItemChange(item.id, { editorScript: val })} onEditorScriptChange={(val) => onItemChange(item.id, { editorScript: val })}
onColSpanChange={(val) => onItemChange(item.id, { colSpan: val })} onColSpanChange={(val) => onItemChange(item.id, { colSpan: val })}
onRequiredChange={(val) => onItemChange(item.id, { isRequired: 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)} onRemove={() => onRemoveItem(item.id)}
/> />
))} ))}
@ -423,6 +745,9 @@ const WizardStep3 = ({
selectCommandColumns, selectCommandColumns,
groups, groups,
onGroupsChange, onGroupsChange,
dbObjects,
isLoadingDbObjects: _isLoadingDbObjects,
dsCode,
translate, translate,
onBack, onBack,
onNext, onNext,
@ -662,7 +987,8 @@ const WizardStep3 = ({
const validationMsg = hasNoGroups const validationMsg = hasNoGroups
? translate('::ListForms.Wizard.Step3.AtLeastOneGroup') || 'En az bir grup eklemelisiniz.' ? translate('::ListForms.Wizard.Step3.AtLeastOneGroup') || 'En az bir grup eklemelisiniz.'
: hasEmptyGroup : 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 ( return (
@ -673,10 +999,10 @@ const WizardStep3 = ({
onDragEnd={onDragEnd} onDragEnd={onDragEnd}
> >
<div className="pb-20"> <div className="pb-20">
<div className="flex gap-4"> <div className="flex flex-col lg:flex-row gap-4">
{/* ── Left: Available Columns ─────────────────────────────────── */} {/* ── Left: Available Columns ─────────────────────────────────── */}
<div className="w-72 shrink-0"> <div className="w-full lg:w-64 lg:shrink-0">
<div className="sticky top-4"> <div className="lg:sticky lg:top-4">
<div className="flex items-center justify-between mb-2"> <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"> <span className="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wide">
{translate('::App.Listform.ListformField.Column')} {translate('::App.Listform.ListformField.Column')}
@ -685,10 +1011,11 @@ const WizardStep3 = ({
{availableColumns.length}/{selectedColumns.size} {availableColumns.length}/{selectedColumns.size}
</span> </span>
</div> </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 ? ( {availableColumns.length === 0 ? (
<div className="text-xs text-gray-300 dark:text-gray-600 py-4 text-center select-none"> <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'} {translate('::ListForms.Wizard.Step3.AllColumnsAdded') ||
'Tüm sütunlar gruplara eklendi'}
</div> </div>
) : ( ) : (
availableColumns.map((col) => <AvailableColumnChip key={col} colName={col} />) availableColumns.map((col) => <AvailableColumnChip key={col} colName={col} />)
@ -701,7 +1028,8 @@ const WizardStep3 = ({
<div className="flex-1 flex flex-col gap-3"> <div className="flex-1 flex flex-col gap-3">
{groups.length === 0 && ( {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"> <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> </div>
)} )}
@ -711,6 +1039,8 @@ const WizardStep3 = ({
group={group} group={group}
isOver={overGroupId === group.id} isOver={overGroupId === group.id}
hasAvailable={availableColumns.length > 0} hasAvailable={availableColumns.length > 0}
dbObjects={dbObjects}
dsCode={dsCode}
onCaptionChange={(val) => updateGroup(group.id, { caption: val })} onCaptionChange={(val) => updateGroup(group.id, { caption: val })}
onColCountChange={(val) => updateGroup(group.id, { colCount: val })} onColCountChange={(val) => updateGroup(group.id, { colCount: val })}
onItemChange={(itemId, patch) => updateItem(group.id, itemId, patch)} onItemChange={(itemId, patch) => updateItem(group.id, itemId, patch)}
@ -793,19 +1123,34 @@ 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-0 h-16 flex items-center"> <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 items-center gap-3 w-full"> <div className="flex flex-wrap items-center gap-2 w-full">
<Button size='sm' variant="default" type="button" icon={<FaArrowLeft />} onClick={onBack}> <Button size="sm" variant="default" type="button" icon={<FaArrowLeft />} onClick={onBack}>
{translate('::Back') || 'Back'} {translate('::Back') || 'Back'}
</Button> </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'} {translate('::Helper Codes') || 'Helper Codes'}
</Button> </Button>
<div className="flex-1 flex items-center justify-end gap-3"> <div className="flex-1 flex items-center justify-end gap-3">
{!canProceed && ( {!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'} {translate('::Next') || 'Next'}
</Button> </Button>
</div> </div>

View file

@ -1,9 +1,14 @@
import { Container } from '@/components/shared' import { Container } from '@/components/shared'
import { Button, Checkbox, FormContainer, FormItem, Input, Select } from '@/components/ui' import { Button, Checkbox, FormContainer, FormItem, Input, Select } from '@/components/ui'
import { ColumnFormatEditDto, ListFormFieldEditTabs } from '@/proxy/admin/list-form-field/models' 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 { SelectBoxOption } from '@/types/shared'
import { useLocalization } from '@/utils/hooks/useLocalization' import { useLocalization } from '@/utils/hooks/useLocalization'
import { useStoreState } from '@/store/store'
import { Field, FieldProps, Form, Formik } from 'formik' 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 { number, object, string } from 'yup'
import { cascadeFilterOperator, columnLookupDataSourceTypeListOptions } from '../options' import { cascadeFilterOperator, columnLookupDataSourceTypeListOptions } from '../options'
import { FormFieldEditProps } from './FormFields' import { FormFieldEditProps } from './FormFields'
@ -26,6 +31,177 @@ const schema = object().shape({
.required(), .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) { function getNormalizedInitialValues(initialValues: ColumnFormatEditDto) {
return { return {
...initialValues, ...initialValues,
@ -52,6 +228,22 @@ function FormFieldTabLookup({
initialValues: ColumnFormatEditDto initialValues: ColumnFormatEditDto
} & FormFieldEditProps) { } & FormFieldEditProps) {
const { translate } = useLocalization() 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 ( return (
<Container className="grid xl:grid-cols-2"> <Container className="grid xl:grid-cols-2">
@ -94,17 +286,42 @@ function FormFieldTabLookup({
<FormItem <FormItem
label={translate('::ListForms.ListFormFieldEdit.LookupLookupQuery')} 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} invalid={errors.lookupDto?.lookupQuery && touched.lookupDto?.lookupQuery}
errorMessage={errors.lookupDto?.lookupQuery} errorMessage={errors.lookupDto?.lookupQuery}
> >
<Field <Field name="lookupDto.lookupQuery">
type="text" {({ field, form }: FieldProps<string>) => (
<>
<Input
{...field}
textArea
autoComplete="off" autoComplete="off"
name="lookupDto.lookupQuery"
placeholder={translate('::ListForms.ListFormFieldEdit.LookupLookupQuery')} 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>
<FormItem <FormItem

View file

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