Wizard Problemleri Giderildi

This commit is contained in:
Sedat Öztürk 2026-07-17 23:23:43 +03:00
parent 319afbcb13
commit ab6a33c340
23 changed files with 1320 additions and 343 deletions

View file

@ -41,6 +41,7 @@ public class ListFormWizardDto
public string PermissionGroupName { get; set; } public string PermissionGroupName { get; set; }
public string MenuParentCode { get; set; } public string MenuParentCode { get; set; }
public string MenuParentShortName { get; set; }
public string MenuParentIcon { get; set; } public string MenuParentIcon { get; set; }
public string MenuIcon { get; set; } public string MenuIcon { get; set; }
public string DataSourceCode { get; set; } public string DataSourceCode { get; set; }

View file

@ -180,11 +180,18 @@ public class ListFormWizardAppService(
Code = input.MenuParentCode, Code = input.MenuParentCode,
DisplayName = input.MenuParentCode, DisplayName = input.MenuParentCode,
IsDisabled = false, IsDisabled = false,
ShortName = input.MenuParentShortName?.Trim(),
Icon = menuParentIcon, Icon = menuParentIcon,
Order = maxRootOrder + 1, Order = maxRootOrder + 1,
}, autoSave: false); }, autoSave: false);
inserted.MenuCodes.Add(input.MenuParentCode); inserted.MenuCodes.Add(input.MenuParentCode);
} }
else if (string.IsNullOrWhiteSpace(menuParent.ShortName) &&
!string.IsNullOrWhiteSpace(input.MenuParentShortName))
{
menuParent.ShortName = input.MenuParentShortName.Trim();
await repoMenu.UpdateAsync(menuParent, autoSave: false);
}
//Menu //Menu
var maxChildOrder = menuQueryable.Where(a => a.ParentCode == menuParent.Code).Select(a => (int?)a.Order).Max() ?? 0; var maxChildOrder = menuQueryable.Where(a => a.ParentCode == menuParent.Code).Select(a => (int?)a.Order).Max() ?? 0;
@ -301,8 +308,8 @@ public class ListFormWizardAppService(
ColumnOptionJson = WizardConsts.DefaultColumnOptionJson(), ColumnOptionJson = WizardConsts.DefaultColumnOptionJson(),
PermissionJson = WizardConsts.DefaultPermissionJson(code), PermissionJson = WizardConsts.DefaultPermissionJson(code),
DeleteCommand = isDeleted ? WizardConsts.DefaultDeleteCommand(input.SelectCommand) : null, DeleteCommand = isDeleted ? WizardConsts.DefaultDeleteCommand(input.SelectCommand) : null,
DeleteFieldsDefaultValueJson = isDeleted ? WizardConsts.DefaultDeleteFieldsDefaultValueJson(input.KeyFieldDbSourceType) : WizardConsts.DefaultFieldsJsonOnlyId(input.KeyFieldDbSourceType), DeleteFieldsDefaultValueJson = isDeleted ? WizardConsts.DefaultDeleteFieldsDefaultValueJson(input.KeyFieldDbSourceType) : WizardConsts.DefaultDeleteFieldsJsonOnlyId(input.KeyFieldDbSourceType),
InsertFieldsDefaultValueJson = isCreated ? WizardConsts.DefaultInsertFieldsDefaultValueJson(input.KeyFieldDbSourceType) : WizardConsts.DefaultFieldsJsonOnlyId(input.KeyFieldDbSourceType), InsertFieldsDefaultValueJson = isCreated ? WizardConsts.DefaultInsertFieldsDefaultValueJson(input.KeyFieldDbSourceType) : WizardConsts.DefaultInsertFieldsJsonOnlyId(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,
@ -382,9 +389,11 @@ public class ListFormWizardAppService(
Width = 0, Width = 0,
ListOrderNo = fieldOrder, ListOrderNo = fieldOrder,
EditGroupOrderNo = item.IncludeInEditingForm ? editGroupOrder : null, EditGroupOrderNo = item.IncludeInEditingForm ? editGroupOrder : null,
EditOrderNo = item.IncludeInEditingForm && item.FieldName != input.KeyFieldName ? editOrder : null, EditOrderNo = item.IncludeInEditingForm && item.FieldName != input.KeyFieldName
EditorType2 = item.IncludeInEditingForm ? item.EditorType : null, ? editOrder
ColSpan = item.IncludeInEditingForm ? item.ColSpan : null, : null,
EditorType2 = item.EditorType,
ColSpan = item.ColSpan,
EditorOptions = string.IsNullOrWhiteSpace(item.EditorOptions) ? null : item.EditorOptions, EditorOptions = string.IsNullOrWhiteSpace(item.EditorOptions) ? null : item.EditorOptions,
EditorScript = string.IsNullOrWhiteSpace(item.EditorScript) ? null : item.EditorScript, EditorScript = string.IsNullOrWhiteSpace(item.EditorScript) ? null : item.EditorScript,
SourceDbType = item.DbSourceType, SourceDbType = item.DbSourceType,

View file

@ -15,6 +15,7 @@ using Volo.Abp.PermissionManagement;
using Volo.Abp.TenantManagement; using Volo.Abp.TenantManagement;
using static Sozsoft.Platform.Data.Seeds.SeedConsts; using static Sozsoft.Platform.Data.Seeds.SeedConsts;
using Sozsoft.Languages; using Sozsoft.Languages;
using Sozsoft.Languages.Languages;
using Sozsoft.Platform.ListForms; using Sozsoft.Platform.ListForms;
namespace Sozsoft.Platform.Menus; namespace Sozsoft.Platform.Menus;
@ -131,6 +132,30 @@ public class MenuAppService : CrudAppService<
.ToList(); .ToList();
entityDtos = await base.MapToGetListOutputDtosAsync(entities); entityDtos = await base.MapToGetListOutputDtosAsync(entities);
var languageKeys = entityDtos
.Select(item => string.IsNullOrWhiteSpace(item.DisplayName) ? item.Code : item.DisplayName)
.Where(key => !string.IsNullOrWhiteSpace(key))
.Distinct()
.ToList();
var languageTexts = await _repositoryText.GetListAsync(text =>
text.ResourceName == PlatformConsts.AppName &&
languageKeys.Contains(text.Key) &&
(text.CultureName == PlatformConsts.DefaultLanguage ||
text.CultureName == LanguageCodes.Tr));
foreach (var menu in entityDtos)
{
var languageKey = string.IsNullOrWhiteSpace(menu.DisplayName)
? menu.Code
: menu.DisplayName;
menu.MenuTextEn = languageTexts.FirstOrDefault(text =>
text.Key == languageKey &&
text.CultureName == PlatformConsts.DefaultLanguage)?.Value;
menu.MenuTextTr = languageTexts.FirstOrDefault(text =>
text.Key == languageKey &&
text.CultureName == LanguageCodes.Tr)?.Value;
}
} }
return new PagedResultDto<MenuDto>( return new PagedResultDto<MenuDto>(

View file

@ -19467,7 +19467,7 @@
{ {
"resourceName": "Platform", "resourceName": "Platform",
"key": "ListForms.ListFormFieldEdit.TodoStatusOrderPlaceholder", "key": "ListForms.ListFormFieldEdit.TodoStatusOrderPlaceholder",
"en": "Inbox, Today, This Week, This Month, Later, Done", "en": "Backlog, Ready to Start, In Progress, Code Review, Testing, Staging",
"tr": "Gelen Kutusu, Bugün, Bu Hafta, Bu Ay, Daha Sonra, Tamamlandı" "tr": "Gelen Kutusu, Bugün, Bu Hafta, Bu Ay, Daha Sonra, Tamamlandı"
}, },
{ {
@ -19476,6 +19476,18 @@
"en": "Allow cards to be dragged between columns", "en": "Allow cards to be dragged between columns",
"tr": "Kartları kolonlar arasında sürüklemeye izin ver" "tr": "Kartları kolonlar arasında sürüklemeye izin ver"
}, },
{
"resourceName": "Platform",
"key": "ListForms.ListFormFieldEdit.TodoRequiredFields",
"en": "Title field and column / status field are required.",
"tr": "Başlık alanı ve kolon / durum alanı zorunludur."
},
{
"resourceName": "Platform",
"key": "ListForms.ListFormFieldEdit.TodoDisabledInListSettings",
"en": "Todo view is disabled in the list settings. You can skip this step or enable the Todo view in the previous steps.",
"tr": "Todo görünümü liste ayarlarında kapalı. Bu adımı atlayabilir veya önceki adımlardan Todo görünümünü etkinleştirebilirsiniz."
},
{ {
"resourceName": "Platform", "resourceName": "Platform",
"key": "ListForms.TodoBoard.Uncategorized", "key": "ListForms.TodoBoard.Uncategorized",
@ -19743,13 +19755,13 @@
{ {
"resourceName": "Platform", "resourceName": "Platform",
"key": "ListForms.TodoBoard.NewColumn", "key": "ListForms.TodoBoard.NewColumn",
"en": "New Kanban column", "en": "New Kanban",
"tr": "Yeni kanban" "tr": "Yeni kanban"
}, },
{ {
"resourceName": "Platform", "resourceName": "Platform",
"key": "ListForms.TodoBoard.AddNewColumn", "key": "ListForms.TodoBoard.AddNewColumn",
"en": "Add a new Kanban column", "en": "Add a new Kanban",
"tr": "Yeni kanban ekle" "tr": "Yeni kanban ekle"
}, },
{ {

View file

@ -0,0 +1,18 @@
IF OBJECT_ID(N'[dbo].[Pro_T_Approval]', 'U') IS NULL
BEGIN
CREATE TABLE [dbo].[Pro_T_Approval]
(
[Id] uniqueidentifier NOT NULL DEFAULT NEWID(),
[ApprovalUserName] nvarchar(256) NULL,
[ApprovalStatus] nvarchar(50) NULL,
[ApprovalDate] datetime NULL,
[ApprovalDescription] nvarchar(200) NULL,
[TenantId] uniqueidentifier NULL,
[Title] nvarchar(100) NULL,
CONSTRAINT [PK_Pro_T_Approval] PRIMARY KEY NONCLUSTERED
(
[Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]
END
GO

View file

@ -0,0 +1,22 @@
IF OBJECT_ID(N'[dbo].[Pro_T_Todo]', 'U') IS NULL
BEGIN
CREATE TABLE [dbo].[Pro_T_Todo]
(
[Id] uniqueidentifier NOT NULL DEFAULT NEWID(),
[Title] nvarchar(300) NOT NULL,
[Status] nvarchar(50) NOT NULL DEFAULT N'Inbox',
[Description] nvarchar(MAX) NULL,
[DueDate] datetime2 NULL,
[Tags] nvarchar(500) NULL,
[Assignees] nvarchar(500) NULL,
[Priority] nvarchar(50) NULL,
[Completed] bit NOT NULL DEFAULT 0,
[SortOrder] int NOT NULL DEFAULT 0,
[TenantId] uniqueidentifier NULL,
CONSTRAINT [PK_Table] PRIMARY KEY NONCLUSTERED
(
[Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]
END
GO

View file

@ -0,0 +1,281 @@
{
"Wizard": {
"WizardName": "Approval",
"ListFormCode": "App.Wizard.Approval",
"MenuCode": "App.Wizard.Approval",
"IsTenant": true,
"IsBranch": false,
"IsOrganizationUnit": false,
"AllowAdding": true,
"AllowUpdating": true,
"AllowDeleting": true,
"AllowDetail": false,
"ConfirmDelete": true,
"DefaultLayout": "grid",
"Grid": true,
"Card": true,
"Pivot": true,
"Tree": true,
"Chart": true,
"Gantt": true,
"Scheduler": true,
"Todo": false,
"LanguageTextMenuEn": "Approval",
"LanguageTextMenuTr": "Onaylama",
"LanguageTextTitleEn": "Approval",
"LanguageTextTitleTr": "Onaylama",
"LanguageTextDescEn": "Approval",
"LanguageTextDescTr": "Onaylama",
"LanguageTextMenuParentEn": "Project",
"LanguageTextMenuParentTr": "Project",
"PermissionGroupName": "App.Wizard.Project",
"MenuParentCode": "App.Wizard.Project",
"MenuParentShortName": "Pro",
"MenuParentIcon": "FcVideoProjector",
"MenuIcon": "FcApproval",
"DataSourceCode": "Default",
"DataSourceConnectionString": "",
"SelectCommandType": 1,
"SelectCommand": "Pro_T_Approval",
"KeyFieldName": "Id",
"KeyFieldDbSourceType": 9,
"TreeKeyExpr": "",
"TreeParentIdExpr": "",
"TreeAutoExpandAll": false,
"GanttKeyExpr": "",
"GanttParentIdExpr": "",
"GanttAutoExpandAll": false,
"GanttTitleExpr": "",
"GanttStartExpr": "",
"GanttEndExpr": "",
"GanttProgressExpr": "",
"SchedulerTextExpr": "",
"SchedulerStartDateExpr": "",
"SchedulerEndDateExpr": "",
"TodoTitleExpr": "",
"TodoStatusExpr": "",
"TodoDescriptionExpr": "",
"TodoDueDateExpr": "",
"TodoTagExpr": "",
"TodoAssigneeExpr": "",
"TodoPriorityExpr": "",
"TodoCompletedExpr": "",
"TodoOrderExpr": "",
"TodoStatusOrder": "Backlog, Ready to Start, In Progress, Code Review, Testing, Staging",
"TodoAllowDragging": true,
"ApprovalUserFieldName": "ApprovalUserName",
"ApprovalDateFieldName": "ApprovalDate",
"ApprovalStatusFieldName": "ApprovalStatus",
"ApprovalDescriptionFieldName": "ApprovalDescription",
"ApprovalIsFilterUserName": false,
"Criteria": [
{
"ListFormCode": "App.Wizard.Approval",
"Kind": "Start",
"Title": "\u0130\u015F Ak\u0131\u015F\u0131 Ba\u015Flat1",
"CompareColumn": "Price",
"CompareOperator": "\u003E",
"CompareValue": 5000,
"Approver": "",
"NextOnStart": "App.Wizard.Approval-N002",
"NextOnTrue": "",
"NextOnFalse": "",
"NextOnApprove": "",
"NextOnReject": "",
"PositionX": 46,
"PositionY": 46,
"CompareOutcomes": [],
"Id": "App.Wizard.Approval-N001"
},
{
"ListFormCode": "App.Wizard.Approval",
"Kind": "Approval",
"Title": "Onay1",
"CompareColumn": "Price",
"CompareOperator": "\u003E",
"CompareValue": 5000,
"Approver": "system@sozsoft.com",
"NextOnStart": "",
"NextOnTrue": "",
"NextOnFalse": "",
"NextOnApprove": "App.Wizard.Approval-N003",
"NextOnReject": "App.Wizard.Approval-N004",
"PositionX": 337,
"PositionY": 39,
"CompareOutcomes": [],
"Id": "App.Wizard.Approval-N002"
},
{
"ListFormCode": "App.Wizard.Approval",
"Kind": "Inform",
"Title": "Bilgilendirme1",
"CompareColumn": "Price",
"CompareOperator": "\u003E",
"CompareValue": 5000,
"Approver": "system@sozsoft.com",
"NextOnStart": "App.Wizard.Approval-N004",
"NextOnTrue": "",
"NextOnFalse": "",
"NextOnApprove": "",
"NextOnReject": "",
"PositionX": 538,
"PositionY": 324,
"CompareOutcomes": [],
"Id": "App.Wizard.Approval-N003"
},
{
"ListFormCode": "App.Wizard.Approval",
"Kind": "End",
"Title": "\u0130\u015F Ak\u0131\u015F\u0131 Bitir1",
"CompareColumn": "Price",
"CompareOperator": "\u003E",
"CompareValue": 5000,
"Approver": "",
"NextOnStart": "",
"NextOnTrue": "",
"NextOnFalse": "",
"NextOnApprove": "",
"NextOnReject": "",
"PositionX": 852,
"PositionY": 26,
"CompareOutcomes": [],
"Id": "App.Wizard.Approval-N004"
}
],
"Groups": [
{
"Caption": "",
"ColCount": 1,
"Items": [
{
"FieldName": "Id",
"CaptionName": "App.Listform.ListformField.Id",
"EditorType": "dxTextBox",
"EditorOptions": "",
"EditorScript": "",
"ColSpan": 1,
"IsRequired": true,
"IncludeInEditingForm": true,
"DbSourceType": 9,
"TurkishCaption": "Id",
"EnglishCaption": "Id",
"LookupDataSourceType": 1,
"ValueExpr": "Key",
"DisplayExpr": "Name",
"LookupQuery": ""
},
{
"FieldName": "ApprovalUserName",
"CaptionName": "App.Listform.ListformField.ApprovalUserName",
"EditorType": "dxTextBox",
"EditorOptions": "",
"EditorScript": "",
"ColSpan": 1,
"IsRequired": false,
"IncludeInEditingForm": false,
"DbSourceType": 16,
"TurkishCaption": "Approval User Name",
"EnglishCaption": "Approval User Name",
"LookupDataSourceType": 1,
"ValueExpr": "Key",
"DisplayExpr": "Name",
"LookupQuery": ""
},
{
"FieldName": "ApprovalStatus",
"CaptionName": "App.Listform.ListformField.ApprovalStatus",
"EditorType": "dxTextBox",
"EditorOptions": "",
"EditorScript": "",
"ColSpan": 1,
"IsRequired": false,
"IncludeInEditingForm": false,
"DbSourceType": 16,
"TurkishCaption": "Approval Status",
"EnglishCaption": "Approval Status",
"LookupDataSourceType": 1,
"ValueExpr": "Key",
"DisplayExpr": "Name",
"LookupQuery": ""
},
{
"FieldName": "ApprovalDate",
"CaptionName": "App.Listform.ListformField.ApprovalDate",
"EditorType": "dxDateBox",
"EditorOptions": "",
"EditorScript": "",
"ColSpan": 1,
"IsRequired": false,
"IncludeInEditingForm": false,
"DbSourceType": 6,
"TurkishCaption": "Approval Date",
"EnglishCaption": "Approval Date",
"LookupDataSourceType": 1,
"ValueExpr": "Key",
"DisplayExpr": "Name",
"LookupQuery": ""
},
{
"FieldName": "ApprovalDescription",
"CaptionName": "App.Listform.ListformField.ApprovalDescription",
"EditorType": "dxTextBox",
"EditorOptions": "",
"EditorScript": "",
"ColSpan": 1,
"IsRequired": false,
"IncludeInEditingForm": false,
"DbSourceType": 16,
"TurkishCaption": "Approval Description",
"EnglishCaption": "Approval Description",
"LookupDataSourceType": 1,
"ValueExpr": "Key",
"DisplayExpr": "Name",
"LookupQuery": ""
},
{
"FieldName": "Title",
"CaptionName": "App.Listform.ListformField.Title",
"EditorType": "dxTextBox",
"EditorOptions": "",
"EditorScript": "",
"ColSpan": 1,
"IsRequired": false,
"IncludeInEditingForm": true,
"DbSourceType": 16,
"TurkishCaption": "Title",
"EnglishCaption": "Title",
"LookupDataSourceType": 1,
"ValueExpr": "Key",
"DisplayExpr": "Name",
"LookupQuery": ""
}
]
}
],
"SubForms": [],
"Widgets": []
},
"IsDeletedField": false,
"IsCreatedField": false,
"InsertedRecords": {
"LanguageKeys": [
"App.Wizard.Approval",
"App.Wizard.Approval.Title",
"App.Wizard.Approval.Desc"
],
"PermissionGroupNames": [],
"PermissionNames": [
"App.Wizard.Approval",
"App.Wizard.Approval.Create",
"App.Wizard.Approval.Update",
"App.Wizard.Approval.Delete",
"App.Wizard.Approval.Export",
"App.Wizard.Approval.Import",
"App.Wizard.Approval.Note"
],
"MenuCodes": [
"App.Wizard.Approval"
],
"DataSourceCodes": []
}
}

View file

@ -0,0 +1,281 @@
{
"Wizard": {
"WizardName": "Todo",
"ListFormCode": "App.Wizard.Todo",
"MenuCode": "App.Wizard.Todo",
"IsTenant": true,
"IsBranch": false,
"IsOrganizationUnit": false,
"AllowAdding": true,
"AllowUpdating": true,
"AllowDeleting": true,
"AllowDetail": false,
"ConfirmDelete": true,
"DefaultLayout": "todo",
"Grid": true,
"Card": true,
"Pivot": true,
"Tree": true,
"Chart": true,
"Gantt": true,
"Scheduler": true,
"Todo": true,
"LanguageTextMenuEn": "Todo",
"LanguageTextMenuTr": "\u0130\u015Fler",
"LanguageTextTitleEn": "Todo",
"LanguageTextTitleTr": "\u0130\u015Fler",
"LanguageTextDescEn": "Todo",
"LanguageTextDescTr": "\u0130\u015Fler",
"LanguageTextMenuParentEn": "Project",
"LanguageTextMenuParentTr": "Project",
"PermissionGroupName": "App.Wizard.Project",
"MenuParentCode": "App.Wizard.Project",
"MenuParentShortName": "Pro",
"MenuParentIcon": "FcVideoProjector",
"MenuIcon": "FcTodoList",
"DataSourceCode": "Default",
"DataSourceConnectionString": "",
"SelectCommandType": 1,
"SelectCommand": "Pro_T_Todo",
"KeyFieldName": "Id",
"KeyFieldDbSourceType": 9,
"TreeKeyExpr": "",
"TreeParentIdExpr": "",
"TreeAutoExpandAll": false,
"GanttKeyExpr": "",
"GanttParentIdExpr": "",
"GanttAutoExpandAll": false,
"GanttTitleExpr": "",
"GanttStartExpr": "",
"GanttEndExpr": "",
"GanttProgressExpr": "",
"SchedulerTextExpr": "",
"SchedulerStartDateExpr": "",
"SchedulerEndDateExpr": "",
"TodoTitleExpr": "Title",
"TodoStatusExpr": "Status",
"TodoDescriptionExpr": "Description",
"TodoDueDateExpr": "DueDate",
"TodoTagExpr": "Tags",
"TodoAssigneeExpr": "Assignees",
"TodoPriorityExpr": "Priority",
"TodoCompletedExpr": "Completed",
"TodoOrderExpr": "SortOrder",
"TodoStatusOrder": "Backlog, Ready to Start, In Progress, Code Review, Testing, Staging",
"TodoAllowDragging": true,
"ApprovalUserFieldName": "",
"ApprovalDateFieldName": "",
"ApprovalStatusFieldName": "",
"ApprovalDescriptionFieldName": "",
"ApprovalIsFilterUserName": false,
"Criteria": [],
"Groups": [
{
"Caption": "",
"ColCount": 1,
"Items": [
{
"FieldName": "Id",
"CaptionName": "App.Listform.ListformField.Id",
"EditorType": "dxTextBox",
"EditorOptions": "",
"EditorScript": "",
"ColSpan": 1,
"IsRequired": true,
"IncludeInEditingForm": true,
"DbSourceType": 9,
"TurkishCaption": "Id",
"EnglishCaption": "Id",
"LookupDataSourceType": 1,
"ValueExpr": "Key",
"DisplayExpr": "Name",
"LookupQuery": ""
},
{
"FieldName": "Title",
"CaptionName": "App.Listform.ListformField.Title",
"EditorType": "dxTextBox",
"EditorOptions": "",
"EditorScript": "",
"ColSpan": 1,
"IsRequired": true,
"IncludeInEditingForm": true,
"DbSourceType": 16,
"TurkishCaption": "Title",
"EnglishCaption": "Title",
"LookupDataSourceType": 1,
"ValueExpr": "Key",
"DisplayExpr": "Name",
"LookupQuery": ""
},
{
"FieldName": "Status",
"CaptionName": "App.Listform.ListformField.Status",
"EditorType": "dxTextBox",
"EditorOptions": "",
"EditorScript": "",
"ColSpan": 1,
"IsRequired": true,
"IncludeInEditingForm": false,
"DbSourceType": 16,
"TurkishCaption": "Status",
"EnglishCaption": "Status",
"LookupDataSourceType": 1,
"ValueExpr": "Key",
"DisplayExpr": "Name",
"LookupQuery": ""
},
{
"FieldName": "Description",
"CaptionName": "App.Listform.ListformField.Description",
"EditorType": "dxHtmlEditor",
"EditorOptions": "{\u0022mediaResizing\u0022:{\u0022enabled\u0022:true},\u0022imageUpload\u0022:{\u0022tabs\u0022:[\u0022file\u0022,\u0022url\u0022],\u0022fileUploadMode\u0022:\u0022base64\u0022},\u0022toolbar\u0022:{\u0022multiline\u0022:true,\u0022items\u0022:[{\u0022name\u0022:\u0022undo\u0022},{\u0022name\u0022:\u0022redo\u0022},{\u0022name\u0022:\u0022separator\u0022},{\u0022name\u0022:\u0022size\u0022,\u0022acceptedValues\u0022:[\u00228pt\u0022,\u002210pt\u0022,\u002212pt\u0022,\u002214pt\u0022,\u002218pt\u0022,\u002224pt\u0022,\u002236pt\u0022],\u0022options\u0022:{\u0022inputAttr\u0022:{\u0022aria-label\u0022:\u0022Font size\u0022}}},{\u0022name\u0022:\u0022font\u0022,\u0022acceptedValues\u0022:[\u0022Arial\u0022,\u0022Courier New\u0022,\u0022Georgia\u0022,\u0022Impact\u0022,\u0022Lucida Console\u0022,\u0022Tahoma\u0022,\u0022Times New Roman\u0022,\u0022Verdana\u0022],\u0022options\u0022:{\u0022inputAttr\u0022:{\u0022aria-label\u0022:\u0022Font family\u0022}}},{\u0022name\u0022:\u0022separator\u0022},{\u0022name\u0022:\u0022bold\u0022},{\u0022name\u0022:\u0022italic\u0022},{\u0022name\u0022:\u0022strike\u0022},{\u0022name\u0022:\u0022underline\u0022},{\u0022name\u0022:\u0022separator\u0022},{\u0022name\u0022:\u0022alignLeft\u0022},{\u0022name\u0022:\u0022alignCenter\u0022},{\u0022name\u0022:\u0022alignRight\u0022},{\u0022name\u0022:\u0022alignJustify\u0022},{\u0022name\u0022:\u0022separator\u0022},{\u0022name\u0022:\u0022orderedList\u0022},{\u0022name\u0022:\u0022bulletList\u0022},{\u0022name\u0022:\u0022separator\u0022},{\u0022name\u0022:\u0022header\u0022,\u0022acceptedValues\u0022:[false,1,2,3,4,5]},{\u0022name\u0022:\u0022separator\u0022},{\u0022name\u0022:\u0022color\u0022},{\u0022name\u0022:\u0022background\u0022},{\u0022name\u0022:\u0022separator\u0022},{\u0022name\u0022:\u0022link\u0022},{\u0022name\u0022:\u0022image\u0022},{\u0022name\u0022:\u0022separator\u0022},{\u0022name\u0022:\u0022clear\u0022},{\u0022name\u0022:\u0022codeBlock\u0022},{\u0022name\u0022:\u0022blockquote\u0022},{\u0022name\u0022:\u0022separator\u0022},{\u0022name\u0022:\u0022insertTable\u0022},{\u0022name\u0022:\u0022deleteTable\u0022},{\u0022name\u0022:\u0022insertRowAbove\u0022},{\u0022name\u0022:\u0022insertRowBelow\u0022},{\u0022name\u0022:\u0022deleteRow\u0022},{\u0022name\u0022:\u0022insertColumnLeft\u0022},{\u0022name\u0022:\u0022insertColumnRight\u0022},{\u0022name\u0022:\u0022deleteColumn\u0022},{\u0022name\u0022:\u0022cellProperties\u0022},{\u0022name\u0022:\u0022tableProperties\u0022}]}}",
"EditorScript": "",
"ColSpan": 1,
"IsRequired": false,
"IncludeInEditingForm": false,
"DbSourceType": 16,
"TurkishCaption": "Description",
"EnglishCaption": "Description",
"LookupDataSourceType": 1,
"ValueExpr": "Key",
"DisplayExpr": "Name",
"LookupQuery": ""
},
{
"FieldName": "DueDate",
"CaptionName": "App.Listform.ListformField.DueDate",
"EditorType": "dxDateBox",
"EditorOptions": "",
"EditorScript": "",
"ColSpan": 1,
"IsRequired": false,
"IncludeInEditingForm": false,
"DbSourceType": 6,
"TurkishCaption": "Due Date",
"EnglishCaption": "Due Date",
"LookupDataSourceType": 1,
"ValueExpr": "Key",
"DisplayExpr": "Name",
"LookupQuery": ""
},
{
"FieldName": "Tags",
"CaptionName": "App.Listform.ListformField.Tags",
"EditorType": "dxTextBox",
"EditorOptions": "",
"EditorScript": "",
"ColSpan": 1,
"IsRequired": false,
"IncludeInEditingForm": false,
"DbSourceType": 16,
"TurkishCaption": "Tags",
"EnglishCaption": "Tags",
"LookupDataSourceType": 1,
"ValueExpr": "Key",
"DisplayExpr": "Name",
"LookupQuery": ""
},
{
"FieldName": "Assignees",
"CaptionName": "App.Listform.ListformField.Assignees",
"EditorType": "dxTextBox",
"EditorOptions": "",
"EditorScript": "",
"ColSpan": 1,
"IsRequired": false,
"IncludeInEditingForm": false,
"DbSourceType": 16,
"TurkishCaption": "Assignees",
"EnglishCaption": "Assignees",
"LookupDataSourceType": 1,
"ValueExpr": "Key",
"DisplayExpr": "Name",
"LookupQuery": ""
},
{
"FieldName": "Priority",
"CaptionName": "App.Listform.ListformField.Priority",
"EditorType": "dxTextBox",
"EditorOptions": "",
"EditorScript": "",
"ColSpan": 1,
"IsRequired": false,
"IncludeInEditingForm": false,
"DbSourceType": 16,
"TurkishCaption": "Priority",
"EnglishCaption": "Priority",
"LookupDataSourceType": 1,
"ValueExpr": "Key",
"DisplayExpr": "Name",
"LookupQuery": ""
},
{
"FieldName": "Completed",
"CaptionName": "App.Listform.ListformField.Completed",
"EditorType": "dxCheckBox",
"EditorOptions": "",
"EditorScript": "",
"ColSpan": 1,
"IsRequired": true,
"IncludeInEditingForm": false,
"DbSourceType": 3,
"TurkishCaption": "Completed",
"EnglishCaption": "Completed",
"LookupDataSourceType": 1,
"ValueExpr": "Key",
"DisplayExpr": "Name",
"LookupQuery": ""
},
{
"FieldName": "SortOrder",
"CaptionName": "App.Listform.ListformField.SortOrder",
"EditorType": "dxNumberBox",
"EditorOptions": "",
"EditorScript": "",
"ColSpan": 1,
"IsRequired": true,
"IncludeInEditingForm": false,
"DbSourceType": 11,
"TurkishCaption": "Sort Order",
"EnglishCaption": "Sort Order",
"LookupDataSourceType": 1,
"ValueExpr": "Key",
"DisplayExpr": "Name",
"LookupQuery": ""
}
]
}
],
"SubForms": [],
"Widgets": []
},
"IsDeletedField": false,
"IsCreatedField": false,
"InsertedRecords": {
"LanguageKeys": [
"App.Wizard.Todo",
"App.Wizard.Todo.Title",
"App.Wizard.Todo.Desc",
"App.Listform.ListformField.Assignees",
"App.Listform.ListformField.Completed",
"App.Listform.ListformField.SortOrder"
],
"PermissionGroupNames": [
"App.Wizard.Project"
],
"PermissionNames": [
"App.Wizard.Todo",
"App.Wizard.Todo.Create",
"App.Wizard.Todo.Update",
"App.Wizard.Todo.Delete",
"App.Wizard.Todo.Export",
"App.Wizard.Todo.Import",
"App.Wizard.Todo.Note"
],
"MenuCodes": [
"App.Wizard.Todo"
],
"DataSourceCodes": []
}
}

View file

@ -107,6 +107,8 @@ public class WizardDataSeeder : IDataSeedContributor, ITransientDependency
var fileName = Path.GetFileName(filePath); var fileName = Path.GetFileName(filePath);
await BackfillMenuParentShortNameAsync(seedFile.Wizard);
// Zaten seeded mi kontrol et (ListForm var mı?) // Zaten seeded mi kontrol et (ListForm var mı?)
if (await _repoListForm.AnyAsync(a => a.ListFormCode == seedFile.Wizard.ListFormCode)) if (await _repoListForm.AnyAsync(a => a.ListFormCode == seedFile.Wizard.ListFormCode))
{ {
@ -253,6 +255,7 @@ public class WizardDataSeeder : IDataSeedContributor, ITransientDependency
Code = input.MenuParentCode, Code = input.MenuParentCode,
DisplayName = input.MenuParentCode, DisplayName = input.MenuParentCode,
IsDisabled = false, IsDisabled = false,
ShortName = input.MenuParentShortName,
Icon = menuParentIcon, Icon = menuParentIcon,
Order = maxRootOrder + 1, Order = maxRootOrder + 1,
}, autoSave: true); }, autoSave: true);
@ -410,10 +413,10 @@ public class WizardDataSeeder : IDataSeedContributor, ITransientDependency
DeleteCommand = isDeleted ? WizardConsts.DefaultDeleteCommand(input.SelectCommand) : null, DeleteCommand = isDeleted ? WizardConsts.DefaultDeleteCommand(input.SelectCommand) : null,
DeleteFieldsDefaultValueJson = isDeleted DeleteFieldsDefaultValueJson = isDeleted
? WizardConsts.DefaultDeleteFieldsDefaultValueJson(input.KeyFieldDbSourceType) ? WizardConsts.DefaultDeleteFieldsDefaultValueJson(input.KeyFieldDbSourceType)
: WizardConsts.DefaultFieldsJsonOnlyId(input.KeyFieldDbSourceType), : WizardConsts.DefaultDeleteFieldsJsonOnlyId(input.KeyFieldDbSourceType),
InsertFieldsDefaultValueJson = isCreated InsertFieldsDefaultValueJson = isCreated
? WizardConsts.DefaultInsertFieldsDefaultValueJson(input.KeyFieldDbSourceType) ? WizardConsts.DefaultInsertFieldsDefaultValueJson(input.KeyFieldDbSourceType)
: WizardConsts.DefaultFieldsJsonOnlyId(input.KeyFieldDbSourceType), : WizardConsts.DefaultInsertFieldsJsonOnlyId(input.KeyFieldDbSourceType),
PagerOptionJson = WizardConsts.DefaultPagerOptionJson, PagerOptionJson = WizardConsts.DefaultPagerOptionJson,
EditingOptionJson = WizardConsts.DefaultEditingOptionJson(titleLangKey, formWidth, formHeight, input.AllowDeleting, input.AllowAdding, input.AllowUpdating, input.ConfirmDelete, false, input.AllowDetail), EditingOptionJson = WizardConsts.DefaultEditingOptionJson(titleLangKey, formWidth, formHeight, 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,
@ -465,9 +468,11 @@ public class WizardDataSeeder : IDataSeedContributor, ITransientDependency
AllowSearch = true, AllowSearch = true,
ListOrderNo = fieldOrder, ListOrderNo = fieldOrder,
EditGroupOrderNo = item.IncludeInEditingForm ? editGroupOrder : null, EditGroupOrderNo = item.IncludeInEditingForm ? editGroupOrder : null,
EditOrderNo = item.IncludeInEditingForm && item.FieldName != input.KeyFieldName ? editOrder : null, EditOrderNo = item.IncludeInEditingForm && item.FieldName != input.KeyFieldName
EditorType2 = item.IncludeInEditingForm ? item.EditorType : null, ? editOrder
ColSpan = item.IncludeInEditingForm ? item.ColSpan : null, : null,
EditorType2 = item.EditorType,
ColSpan = item.ColSpan,
EditorOptions = string.IsNullOrWhiteSpace(item.EditorOptions) ? null : item.EditorOptions, EditorOptions = string.IsNullOrWhiteSpace(item.EditorOptions) ? null : item.EditorOptions,
EditorScript = string.IsNullOrWhiteSpace(item.EditorScript) ? null : item.EditorScript, EditorScript = string.IsNullOrWhiteSpace(item.EditorScript) ? null : item.EditorScript,
SourceDbType = item.DbSourceType, SourceDbType = item.DbSourceType,
@ -517,6 +522,24 @@ public class WizardDataSeeder : IDataSeedContributor, ITransientDependency
} }
} }
private async Task BackfillMenuParentShortNameAsync(ListFormWizardDto input)
{
if (string.IsNullOrWhiteSpace(input.MenuParentCode) ||
string.IsNullOrWhiteSpace(input.MenuParentShortName))
return;
var menuParent = await _repoMenu.FirstOrDefaultAsync(a => a.Code == input.MenuParentCode);
if (menuParent == null || !string.IsNullOrWhiteSpace(menuParent.ShortName))
return;
menuParent.ShortName = input.MenuParentShortName.Trim();
await _repoMenu.UpdateAsync(menuParent, autoSave: true);
_logger.LogInformation(
"Menu parent '{MenuParentCode}' ShortName backfilled as '{ShortName}'.",
input.MenuParentCode,
menuParent.ShortName);
}
private static bool HasWorkflow(WorkflowDto workflow, List<ListFormWorkflowCriteriaDto> criteria) private static bool HasWorkflow(WorkflowDto workflow, List<ListFormWorkflowCriteriaDto> criteria)
{ {
return workflow != null && ( return workflow != null && (

View file

@ -198,6 +198,14 @@ public static class WizardConsts
}); });
} }
public static string DefaultInsertFieldsJsonOnlyId(DbType dbType = DbType.Guid)
{
return JsonSerializer.Serialize(new[]
{
new { FieldName = "Id", FieldDbType = dbType, Value = "@NEWID", CustomValueType = FieldCustomValueTypeEnum.CustomKey }
});
}
public static string DefaultDeleteFieldsDefaultValueJson(DbType dbType = DbType.Guid) public static string DefaultDeleteFieldsDefaultValueJson(DbType dbType = DbType.Guid)
{ {
return JsonSerializer.Serialize(new[] return JsonSerializer.Serialize(new[]
@ -207,11 +215,11 @@ public static class WizardConsts
}); });
} }
public static string DefaultFieldsJsonOnlyId(DbType dbType = DbType.Guid) public static string DefaultDeleteFieldsJsonOnlyId(DbType dbType = DbType.Guid)
{ {
return JsonSerializer.Serialize(new[] return JsonSerializer.Serialize(new[]
{ {
new { FieldName = "Id", FieldDbType = dbType, Value = "@NEWID", CustomValueType = FieldCustomValueTypeEnum.CustomKey } new { FieldName = "Id", FieldDbType = dbType, Value = "@ID", CustomValueType = FieldCustomValueTypeEnum.CustomKey }
}); });
} }

View file

@ -170,7 +170,8 @@ public class QueryManager : PlatformDomainService, IQueryManager
// oncelik Command alanindadir, dolu ise silme islemi buradaki sorguya yonlendirilir // oncelik Command alanindadir, dolu ise silme islemi buradaki sorguya yonlendirilir
if (!string.IsNullOrEmpty(command)) if (!string.IsNullOrEmpty(command))
{ {
sql = NormalizeCollectionParameterCommand(command, parameters, dataSourceType); // sql = NormalizeCollectionParameterCommand(command, parameters, dataSourceType);
sql = command;
} }
else else
{ {
@ -188,12 +189,6 @@ public class QueryManager : PlatformDomainService, IQueryManager
} }
else if (op == OperationEnum.Update) else if (op == OperationEnum.Update)
{ {
var where = dataSourceType switch
{
DataSourceTypeEnum.Mssql => $"\"{listForm.KeyFieldName}\" IN @{listForm.KeyFieldName}",
DataSourceTypeEnum.Postgresql => $"\"{listForm.KeyFieldName}\" = ANY(@{listForm.KeyFieldName})",
_ => string.Empty,
};
var updateFields = parameters.Select(a => $"\"{a.Key}\" = @{a.Key}").ToList(); var updateFields = parameters.Select(a => $"\"{a.Key}\" = @{a.Key}").ToList();
var val = QueryHelper.GetFormattedValue(listForm.KeyFieldDbSourceType, keys); var val = QueryHelper.GetFormattedValue(listForm.KeyFieldDbSourceType, keys);
if (!parameters.ContainsKey(listForm.KeyFieldName)) if (!parameters.ContainsKey(listForm.KeyFieldName))
@ -202,6 +197,18 @@ public class QueryManager : PlatformDomainService, IQueryManager
listForm.KeyFieldName, listForm.KeyFieldName,
val.GetType().IsArray ? val : new object[] { val }); val.GetType().IsArray ? val : new object[] { val });
} }
var where = BuildWhereCondition(
listForm.KeyFieldName,
parameters[listForm.KeyFieldName],
dataSourceType);
if (parameters.TryGetValue("TenantId", out var tenantId)
&& !string.Equals(listForm.KeyFieldName, "TenantId", StringComparison.OrdinalIgnoreCase))
{
where += $" AND {BuildWhereCondition("TenantId", tenantId, dataSourceType)}";
}
sql = $"UPDATE \"{listForm.SelectCommand}\" SET {string.Join(',', updateFields)} WHERE {where}"; sql = $"UPDATE \"{listForm.SelectCommand}\" SET {string.Join(',', updateFields)} WHERE {where}";
} }
else if (op == OperationEnum.Delete) else if (op == OperationEnum.Delete)
@ -212,12 +219,7 @@ public class QueryManager : PlatformDomainService, IQueryManager
{ {
where = string.Join( where = string.Join(
" AND ", " AND ",
parameters.Select(a => dataSourceType switch parameters.Select(a => BuildWhereCondition(a.Key, a.Value, dataSourceType)).ToList());
{
DataSourceTypeEnum.Mssql => $"\"{a.Key}\" IN @{a.Key}",
DataSourceTypeEnum.Postgresql => $"\"{a.Key}\" = ANY(@{a.Key})",
_ => "1 = 0",
}).ToList());
} }
else else
{ {
@ -240,77 +242,101 @@ public class QueryManager : PlatformDomainService, IQueryManager
return sql; return sql;
} }
private static string NormalizeCollectionParameterCommand( private static bool IsCollectionValue(object value)
string command,
Dictionary<string, object> parameters,
DataSourceTypeEnum dataSourceType)
{
if (string.IsNullOrWhiteSpace(command) || parameters == null || parameters.Count == 0)
{
return command;
}
var sql = command;
foreach (var parameter in parameters)
{
if (!IsCollectionParameter(parameter.Value))
{
continue;
}
var escapedParameterName = Regex.Escape(parameter.Key);
sql = dataSourceType switch
{
DataSourceTypeEnum.Mssql => NormalizeMssqlCollectionParameter(sql, escapedParameterName, parameter.Key),
DataSourceTypeEnum.Postgresql => NormalizePostgresqlCollectionParameter(sql, escapedParameterName, parameter.Key),
_ => sql
};
}
return sql;
}
private static bool IsCollectionParameter(object value)
{ {
return value is System.Collections.IEnumerable return value is System.Collections.IEnumerable
&& value is not string && value is not string
&& value is not byte[]; && value is not byte[];
} }
private static string NormalizeMssqlCollectionParameter( private static string BuildWhereCondition(
string sql, string fieldName,
string escapedParameterName, object value,
string parameterName) DataSourceTypeEnum dataSourceType)
{ {
sql = Regex.Replace( return dataSourceType switch
sql,
$@"IN\s*\(\s*@{escapedParameterName}\s*\)",
$"IN @{parameterName}",
RegexOptions.IgnoreCase);
return Regex.Replace(
sql,
$@"(?<column>(""[^""]+""|\[[^\]]+\]|`[^`]+`|\w+))\s*=\s*@{escapedParameterName}\b",
match => $"{match.Groups["column"].Value} IN @{parameterName}",
RegexOptions.IgnoreCase);
}
private static string NormalizePostgresqlCollectionParameter(
string sql,
string escapedParameterName,
string parameterName)
{ {
sql = Regex.Replace( DataSourceTypeEnum.Mssql when value is null or DBNull => $"\"{fieldName}\" IS NULL",
sql, DataSourceTypeEnum.Postgresql when value is null or DBNull => $"\"{fieldName}\" IS NULL",
$@"IN\s*\(\s*@{escapedParameterName}\s*\)", DataSourceTypeEnum.Mssql when IsCollectionValue(value) => $"\"{fieldName}\" IN @{fieldName}",
$"= ANY(@{parameterName})", DataSourceTypeEnum.Postgresql when IsCollectionValue(value) => $"\"{fieldName}\" = ANY(@{fieldName})",
RegexOptions.IgnoreCase); DataSourceTypeEnum.Mssql => $"\"{fieldName}\" = @{fieldName}",
DataSourceTypeEnum.Postgresql => $"\"{fieldName}\" = @{fieldName}",
return Regex.Replace( _ => "1 = 0",
sql, };
$@"(?<column>(""[^""]+""|\[[^\]]+\]|`[^`]+`|\w+))\s*=\s*@{escapedParameterName}\b", }
match => $"{match.Groups["column"].Value} = ANY(@{parameterName})",
RegexOptions.IgnoreCase); // private static string NormalizeCollectionParameterCommand(
} // string command,
// Dictionary<string, object> parameters,
// DataSourceTypeEnum dataSourceType)
// {
// if (string.IsNullOrWhiteSpace(command) || parameters == null || parameters.Count == 0)
// {
// return command;
// }
// var sql = command;
// foreach (var parameter in parameters)
// {
// if (!IsCollectionParameter(parameter.Value))
// {
// continue;
// }
// var escapedParameterName = Regex.Escape(parameter.Key);
// sql = dataSourceType switch
// {
// DataSourceTypeEnum.Mssql => NormalizeMssqlCollectionParameter(sql, escapedParameterName, parameter.Key),
// DataSourceTypeEnum.Postgresql => NormalizePostgresqlCollectionParameter(sql, escapedParameterName, parameter.Key),
// _ => sql
// };
// }
// return sql;
// }
// private static bool IsCollectionParameter(object value)
// {
// return value is System.Collections.IEnumerable
// && value is not string
// && value is not byte[];
// }
// private static string NormalizeMssqlCollectionParameter(
// string sql,
// string escapedParameterName,
// string parameterName)
// {
// sql = Regex.Replace(
// sql,
// $@"IN\s*\(\s*@{escapedParameterName}\s*\)",
// $"IN @{parameterName}",
// RegexOptions.IgnoreCase);
// return Regex.Replace(
// sql,
// $@"(?<column>(""[^""]+""|\[[^\]]+\]|`[^`]+`|\w+))\s*=\s*@{escapedParameterName}\b",
// match => $"{match.Groups["column"].Value} IN @{parameterName}",
// RegexOptions.IgnoreCase);
// }
// private static string NormalizePostgresqlCollectionParameter(
// string sql,
// string escapedParameterName,
// string parameterName)
// {
// sql = Regex.Replace(
// sql,
// $@"IN\s*\(\s*@{escapedParameterName}\s*\)",
// $"= ANY(@{parameterName})",
// RegexOptions.IgnoreCase);
// return Regex.Replace(
// sql,
// $@"(?<column>(""[^""]+""|\[[^\]]+\]|`[^`]+`|\w+))\s*=\s*@{escapedParameterName}\b",
// match => $"{match.Groups["column"].Value} = ANY(@{parameterName})",
// RegexOptions.IgnoreCase);
// }
} }

View file

@ -1,6 +1,17 @@
{ {
"commit": "1d05d24", "commit": "8146adf",
"releases": [ "releases": [
{
"version": "1.1.08",
"buildDate": "2026-07-16",
"commit": "24e92fcb82eef0ee26ca5d65e08c5404f132a09e",
"changeLog": [
"- CardView komponenti geliştirildi.",
"- Todo / Kanban komponenti geliştirildi.",
"- Licence information",
"- Check New Versiyon komponenti."
]
},
{ {
"version": "1.1.07", "version": "1.1.07",
"buildDate": "2026-07-13", "buildDate": "2026-07-13",

View file

@ -7,7 +7,7 @@ import {
import { ListViewLayoutType } from '@/views/admin/listForm/edit/types' import { ListViewLayoutType } from '@/views/admin/listForm/edit/types'
export interface ListFormWizardColumnItemDto { export interface ListFormWizardColumnItemDto {
dataField: string fieldName: string
editorType: string editorType: string
editorOptions: string editorOptions: string
editorScript: string editorScript: string
@ -56,6 +56,7 @@ export interface ListFormWizardDto {
languageTextMenuParentTr: string languageTextMenuParentTr: string
permissionGroupName: string permissionGroupName: string
menuParentCode: string menuParentCode: string
menuParentShortName?: string
menuParentIcon?: string menuParentIcon?: string
menuIcon: string menuIcon: string
dataSourceCode: string dataSourceCode: string
@ -129,7 +130,7 @@ export interface WizardInsertedRecordsDto {
} }
export interface WizardSeedFileItemDto { export interface WizardSeedFileItemDto {
dataField: string fieldName: string
editorType: string editorType: string
editorOptions: string editorOptions: string
editorScript: string editorScript: string

View file

@ -64,7 +64,7 @@ const initialValues: ListFormWizardDto = {
chart: true, chart: true,
gantt: true, gantt: true,
scheduler: true, scheduler: true,
todo: true, todo: false,
todoTitleExpr: '', todoTitleExpr: '',
todoStatusExpr: '', todoStatusExpr: '',
todoDescriptionExpr: '', todoDescriptionExpr: '',
@ -74,7 +74,7 @@ const initialValues: ListFormWizardDto = {
todoPriorityExpr: '', todoPriorityExpr: '',
todoCompletedExpr: '', todoCompletedExpr: '',
todoOrderExpr: '', todoOrderExpr: '',
todoStatusOrder: 'Inbox, Today, This Week, This Month, Later, Done', todoStatusOrder: 'Backlog, Ready to Start, In Progress, Code Review, Testing, Staging',
todoAllowDragging: true, todoAllowDragging: true,
languageTextMenuEn: '', languageTextMenuEn: '',
languageTextMenuTr: '', languageTextMenuTr: '',
@ -86,6 +86,7 @@ const initialValues: ListFormWizardDto = {
languageTextMenuParentTr: '', languageTextMenuParentTr: '',
permissionGroupName: '', permissionGroupName: '',
menuParentCode: '', menuParentCode: '',
menuParentShortName: '',
menuParentIcon: '', menuParentIcon: '',
menuIcon: '', menuIcon: '',
dataSourceCode: '', dataSourceCode: '',
@ -146,7 +147,26 @@ const step2ValidationSchema = Yup.object().shape({
confirmDelete: Yup.boolean(), confirmDelete: Yup.boolean(),
}) })
const listFormValidationSchema = step1ValidationSchema.concat(step2ValidationSchema) const todoValidationSchema = Yup.object().shape({
todoTitleExpr: Yup.string()
.trim()
.when('todo', {
is: true,
then: (schema) => schema.required(),
otherwise: (schema) => schema.notRequired(),
}),
todoStatusExpr: Yup.string()
.trim()
.when('todo', {
is: true,
then: (schema) => schema.required(),
otherwise: (schema) => schema.notRequired(),
}),
})
const listFormValidationSchema = step1ValidationSchema
.concat(step2ValidationSchema)
.concat(todoValidationSchema)
// ─── Wizard ─────────────────────────────────────────────────────────────────── // ─── Wizard ───────────────────────────────────────────────────────────────────
@ -266,7 +286,7 @@ const Wizard = () => {
const removeTenantGroupItems = (groups: WizardGroup[]) => const removeTenantGroupItems = (groups: WizardGroup[]) =>
groups.map((group) => ({ groups.map((group) => ({
...group, ...group,
items: group.items.filter((item) => !isTenantColumn(item.dataField)), items: group.items.filter((item) => !isTenantColumn(item.fieldName)),
})) }))
const loadColumns = async (dsCode: string, schema: string, name: string) => { const loadColumns = async (dsCode: string, schema: string, name: string) => {
@ -433,7 +453,9 @@ const Wizard = () => {
todoPriorityExpr: w.todoPriorityExpr ?? '', todoPriorityExpr: w.todoPriorityExpr ?? '',
todoCompletedExpr: w.todoCompletedExpr ?? '', todoCompletedExpr: w.todoCompletedExpr ?? '',
todoOrderExpr: w.todoOrderExpr ?? '', todoOrderExpr: w.todoOrderExpr ?? '',
todoStatusOrder: w.todoStatusOrder ?? 'Inbox, Today, This Week, This Month, Later, Done', todoStatusOrder:
w.todoStatusOrder ??
'Backlog, Ready to Start, In Progress, Code Review, Testing, Staging',
todoAllowDragging: w.todoAllowDragging ?? true, todoAllowDragging: w.todoAllowDragging ?? true,
languageTextMenuEn: w.languageTextMenuEn ?? '', languageTextMenuEn: w.languageTextMenuEn ?? '',
languageTextMenuTr: w.languageTextMenuTr ?? '', languageTextMenuTr: w.languageTextMenuTr ?? '',
@ -445,6 +467,7 @@ const Wizard = () => {
languageTextMenuParentTr: w.languageTextMenuParentTr ?? '', languageTextMenuParentTr: w.languageTextMenuParentTr ?? '',
permissionGroupName: w.permissionGroupName ?? '', permissionGroupName: w.permissionGroupName ?? '',
menuParentCode: w.menuParentCode ?? '', menuParentCode: w.menuParentCode ?? '',
menuParentShortName: w.menuParentShortName ?? '',
menuParentIcon: w.menuParentIcon ?? '', menuParentIcon: w.menuParentIcon ?? '',
menuIcon: w.menuIcon ?? '', menuIcon: w.menuIcon ?? '',
dataSourceCode: w.dataSourceCode ?? '', dataSourceCode: w.dataSourceCode ?? '',
@ -497,19 +520,19 @@ const Wizard = () => {
caption: g.caption ?? '', caption: g.caption ?? '',
colCount: g.colCount ?? 2, colCount: g.colCount ?? 2,
items: (g.items ?? []).map( items: (g.items ?? []).map(
(it: any, ii: number) => (it: WizardGroupItem, ii: number) =>
({ ({
id: `${it.dataField}_edit_${ii}_${Date.now()}`, id: `${it.fieldName}_edit_${ii}_${Date.now()}`,
dataField: it.dataField ?? '', fieldName: it.fieldName ?? '',
editorType: it.editorType ?? 'dxTextBox', editorType: it.editorType ?? 'dxTextBox',
editorOptions: it.editorOptions ?? '', editorOptions: it.editorOptions ?? '',
editorScript: it.editorScript ?? '', editorScript: it.editorScript ?? '',
colSpan: it.colSpan ?? 1, colSpan: it.colSpan ?? 1,
isRequired: it.isRequired ?? false, isRequired: it.isRequired ?? false,
includeInEditingForm: it.includeInEditingForm ?? true, includeInEditingForm: it.includeInEditingForm ?? true,
turkishCaption: it.turkishCaption ?? it.dataField, turkishCaption: it.turkishCaption ?? it.fieldName,
englishCaption: it.englishCaption ?? it.dataField, englishCaption: it.englishCaption ?? it.fieldName,
captionName: it.captionName ?? `App.Listform.ListformField.${it.dataField}`, captionName: it.captionName ?? `App.Listform.ListformField.${it.fieldName}`,
lookupDataSourceType: lookupDataSourceType:
(it.lookupDataSourceType as UiLookupDataSourceTypeEnum) ?? (it.lookupDataSourceType as UiLookupDataSourceTypeEnum) ??
UiLookupDataSourceTypeEnum.StaticData, UiLookupDataSourceTypeEnum.StaticData,
@ -522,7 +545,7 @@ const Wizard = () => {
setEditingGroups(restoredGroups) setEditingGroups(restoredGroups)
// Restore selectedColumns from all group items // Restore selectedColumns from all group items
const allFields = new Set(restoredGroups.flatMap((g) => g.items.map((i) => i.dataField))) const allFields = new Set(restoredGroups.flatMap((g) => g.items.map((i) => i.fieldName)))
setSelectedColumns(allFields) setSelectedColumns(allFields)
} }
@ -638,8 +661,16 @@ const Wizard = () => {
formikRef.current?.setFieldValue('menuParentCode', code) formikRef.current?.setFieldValue('menuParentCode', code)
const selectedMenu = rawMenuItems.find((item) => item.code === code) const selectedMenu = rawMenuItems.find((item) => item.code === code)
formikRef.current?.setFieldValue('menuParentIcon', selectedMenu?.icon ?? '') formikRef.current?.setFieldValue('menuParentIcon', selectedMenu?.icon ?? '')
if (!code) return formikRef.current?.setFieldValue('languageTextMenuParentEn', selectedMenu?.menuTextEn ?? '')
applyPermissionGroupFromRoot(findRootCode(rawMenuItems, code)) formikRef.current?.setFieldValue('languageTextMenuParentTr', selectedMenu?.menuTextTr ?? '')
if (!code) {
formikRef.current?.setFieldValue('menuParentShortName', '')
return
}
const rootCode = findRootCode(rawMenuItems, code)
const rootMenu = rawMenuItems.find((item) => item.code === rootCode)
formikRef.current?.setFieldValue('menuParentShortName', rootMenu?.shortName ?? '')
applyPermissionGroupFromRoot(rootCode)
} }
const handleMenuCreated = async (menu: { const handleMenuCreated = async (menu: {
@ -648,6 +679,7 @@ const Wizard = () => {
menuTextEn: string menuTextEn: string
menuTextTr: string menuTextTr: string
icon?: string icon?: string
shortName?: string
}) => { }) => {
formikRef.current?.setFieldValue('menuParentCode', menu.code) formikRef.current?.setFieldValue('menuParentCode', menu.code)
formikRef.current?.setFieldValue('menuParentIcon', menu.icon ?? '') formikRef.current?.setFieldValue('menuParentIcon', menu.icon ?? '')
@ -655,6 +687,11 @@ const Wizard = () => {
formikRef.current?.setFieldValue('languageTextMenuParentTr', menu.menuTextTr) formikRef.current?.setFieldValue('languageTextMenuParentTr', menu.menuTextTr)
const rootCode = menu.parentCode ? findRootCode(rawMenuItems, menu.parentCode) : menu.code const rootCode = menu.parentCode ? findRootCode(rawMenuItems, menu.parentCode) : menu.code
const existingRoot = rawMenuItems.find((item) => item.code === rootCode)
formikRef.current?.setFieldValue(
'menuParentShortName',
menu.parentCode ? (existingRoot?.shortName ?? '') : (menu.shortName ?? ''),
)
applyPermissionGroupFromRoot(rootCode) applyPermissionGroupFromRoot(rootCode)
await getMenuList() await getMenuList()
@ -726,10 +763,10 @@ const Wizard = () => {
caption: g.caption, caption: g.caption,
colCount: g.colCount, colCount: g.colCount,
items: g.items.map((item) => { items: g.items.map((item) => {
const col = selectCommandColumns.find((c) => c.columnName === item.dataField) const col = selectCommandColumns.find((c) => c.columnName === item.fieldName)
return { return {
dataField: item.dataField, fieldName: item.fieldName,
editorType: item.editorType, editorType: item.editorType,
editorOptions: item.editorOptions ?? '', editorOptions: item.editorOptions ?? '',
editorScript: item.editorScript ?? '', editorScript: item.editorScript ?? '',
@ -836,6 +873,7 @@ const Wizard = () => {
onMenuParentChange={handleMenuParentChange} onMenuParentChange={handleMenuParentChange}
onClearMenuParent={() => { onClearMenuParent={() => {
formikRef.current?.setFieldValue('menuParentCode', '') formikRef.current?.setFieldValue('menuParentCode', '')
formikRef.current?.setFieldValue('menuParentShortName', '')
formikRef.current?.setFieldValue('menuParentIcon', '') formikRef.current?.setFieldValue('menuParentIcon', '')
}} }}
onMenuCreated={handleMenuCreated} onMenuCreated={handleMenuCreated}

View file

@ -26,6 +26,7 @@ import { ListFormWizardDto } from '@/proxy/admin/wizard/models'
export interface MenuTreeNode { export interface MenuTreeNode {
code: string code: string
displayName: string displayName: string
shortName?: string
icon?: string icon?: string
url?: string url?: string
order?: number order?: number
@ -41,6 +42,7 @@ export function buildMenuTree(items: MenuItem[]): MenuTreeNode[] {
map.set(item.code!, { map.set(item.code!, {
code: item.code!, code: item.code!,
displayName: item.displayName ?? item.code!, displayName: item.displayName ?? item.code!,
shortName: item.shortName ?? undefined,
icon: item.icon ?? undefined, icon: item.icon ?? undefined,
url: item.url ?? undefined, url: item.url ?? undefined,
order: item.order ?? undefined, order: item.order ?? undefined,

View file

@ -1,4 +1,5 @@
import { Button, Dialog } from '@/components/ui' import { Button, Dialog } from '@/components/ui'
import type { SelectBoxOption } from '@/types/shared'
import { useLocalization } from '@/utils/hooks/useLocalization' import { useLocalization } from '@/utils/hooks/useLocalization'
import { import {
columnEditorTypeListOptions, columnEditorTypeListOptions,
@ -28,14 +29,17 @@ import {
FaTrash, FaTrash,
FaArrowRight, FaArrowRight,
FaCode, FaCode,
FaSlidersH,
} from 'react-icons/fa' } from 'react-icons/fa'
import { UiLookupDataSourceTypeEnum } from '@/proxy/form/models' import { UiLookupDataSourceTypeEnum } from '@/proxy/form/models'
import EditorOptionsBuilderDialog from '../edit/json-row-operations/EditorOptionsBuilderDialog'
import EditorScriptBuilderDialog from '../edit/json-row-operations/EditorScriptBuilderDialog'
import { buildLookupQuery } from '../lookupQuery' import { buildLookupQuery } from '../lookupQuery'
// ─── Types ──────────────────────────────────────────────────────────────────── // ─── Types ────────────────────────────────────────────────────────────────────
export interface WizardGroupItem { export interface WizardGroupItem {
id: string id: string
dataField: string fieldName: string
editorType: string editorType: string
editorOptions: string editorOptions: string
editorScript: string editorScript: string
@ -109,7 +113,7 @@ function newGroupItem(colName: string, meta?: DatabaseColumnDto): WizardGroupIte
const sqlType = meta?.dataType ?? '' const sqlType = meta?.dataType ?? ''
return { return {
id: `${colName}_${Date.now()}`, id: `${colName}_${Date.now()}`,
dataField: colName, fieldName: colName,
editorType: inferEditorType(sqlType), editorType: inferEditorType(sqlType),
editorOptions: '', editorOptions: '',
editorScript: '', editorScript: '',
@ -169,6 +173,7 @@ interface SortableItemProps {
groupColCount: number groupColCount: number
dbObjects: SqlObjectExplorerDto | null dbObjects: SqlObjectExplorerDto | null
dsCode: string dsCode: string
editorScriptFields: SelectBoxOption[]
onTurkishCaptionChange: (val: string) => void onTurkishCaptionChange: (val: string) => void
onEnglishCaptionChange: (val: string) => void onEnglishCaptionChange: (val: string) => void
onEditorTypeChange: (val: string) => void onEditorTypeChange: (val: string) => void
@ -190,6 +195,7 @@ function SortableItem({
groupColCount, groupColCount,
dbObjects, dbObjects,
dsCode, dsCode,
editorScriptFields,
onTurkishCaptionChange, onTurkishCaptionChange,
onEnglishCaptionChange, onEnglishCaptionChange,
onEditorTypeChange, onEditorTypeChange,
@ -207,6 +213,8 @@ function SortableItem({
}: SortableItemProps) { }: SortableItemProps) {
const { translate } = useLocalization() const { translate } = useLocalization()
const [isTablePickerOpen, setIsTablePickerOpen] = useState(false) const [isTablePickerOpen, setIsTablePickerOpen] = useState(false)
const [isEditorOptionsDialogOpen, setIsEditorOptionsDialogOpen] = useState(false)
const [isEditorScriptDialogOpen, setIsEditorScriptDialogOpen] = useState(false)
const [tableSearch, setTableSearch] = useState('') const [tableSearch, setTableSearch] = useState('')
const [pickerStep, setPickerStep] = useState<'table' | 'columns'>('table') const [pickerStep, setPickerStep] = useState<'table' | 'columns'>('table')
const [pickerTable, setPickerTable] = useState<{ schemaName: string; tableName: string } | null>( const [pickerTable, setPickerTable] = useState<{ schemaName: string; tableName: string } | null>(
@ -248,7 +256,7 @@ function SortableItem({
tabIndex={-1} tabIndex={-1}
/> />
<span className="flex-1 text-xs font-semibold text-indigo-600 dark:text-indigo-400 truncate"> <span className="flex-1 text-xs font-semibold text-indigo-600 dark:text-indigo-400 truncate">
{item.dataField} {item.fieldName}
</span> </span>
<Button <Button
type="button" type="button"
@ -564,12 +572,23 @@ function SortableItem({
<span className="text-[10px] text-gray-400 font-medium"> <span className="text-[10px] text-gray-400 font-medium">
{translate('::ListForms.Wizard.Step3.EditorOptions')} {translate('::ListForms.Wizard.Step3.EditorOptions')}
</span> </span>
<div className="flex items-center gap-1">
<input <input
value={item.editorOptions} value={item.editorOptions}
onChange={(e) => onEditorOptionsChange(e.target.value)} onChange={(e) => onEditorOptionsChange(e.target.value)}
placeholder='{"readOnly": false}' placeholder='{"readOnly": false}'
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="min-w-0 flex-1 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"
/> />
<Button
shape="circle"
variant="plain"
type="button"
size="sm"
title="Build editor options"
icon={<FaSlidersH />}
onClick={() => setIsEditorOptionsDialogOpen(true)}
/>
</div>
</div> </div>
{/* Editor Script */} {/* Editor Script */}
@ -577,13 +596,40 @@ function SortableItem({
<span className="text-[10px] text-gray-400 font-medium"> <span className="text-[10px] text-gray-400 font-medium">
{translate('::ListForms.Wizard.Step3.EditorScript')} {translate('::ListForms.Wizard.Step3.EditorScript')}
</span> </span>
<div className="flex items-center gap-1">
<input <input
value={item.editorScript} value={item.editorScript}
onChange={(e) => onEditorScriptChange(e.target.value)} onChange={(e) => onEditorScriptChange(e.target.value)}
placeholder="(e) => { /* e.component */ }" placeholder="(e) => { /* e.component */ }"
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="min-w-0 flex-1 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"
/>
<Button
shape="circle"
variant="plain"
type="button"
size="sm"
title="Build editor script"
icon={<FaCode />}
onClick={() => setIsEditorScriptDialogOpen(true)}
/> />
</div> </div>
</div>
<EditorOptionsBuilderDialog
isOpen={isEditorOptionsDialogOpen}
value={item.editorOptions}
editorType={item.editorType}
onClose={() => setIsEditorOptionsDialogOpen(false)}
onApply={onEditorOptionsChange}
/>
<EditorScriptBuilderDialog
isOpen={isEditorScriptDialogOpen}
value={item.editorScript}
currentField={item.fieldName}
fields={editorScriptFields}
onClose={() => setIsEditorScriptDialogOpen(false)}
onApply={onEditorScriptChange}
/>
{/* Bottom row: ColSpan + Editing Form + Required */} {/* Bottom row: ColSpan + Editing Form + Required */}
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
@ -639,6 +685,7 @@ interface GroupCardProps {
hasAvailable: boolean hasAvailable: boolean
dbObjects: SqlObjectExplorerDto | null dbObjects: SqlObjectExplorerDto | null
dsCode: string dsCode: string
editorScriptFields: SelectBoxOption[]
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
@ -653,6 +700,7 @@ function GroupCard({
hasAvailable, hasAvailable,
dbObjects, dbObjects,
dsCode, dsCode,
editorScriptFields,
onCaptionChange, onCaptionChange,
onColCountChange, onColCountChange,
onItemChange, onItemChange,
@ -759,6 +807,7 @@ function GroupCard({
groupColCount={group.colCount} groupColCount={group.colCount}
dbObjects={dbObjects} dbObjects={dbObjects}
dsCode={dsCode} dsCode={dsCode}
editorScriptFields={editorScriptFields}
onCaptionNameChange={(val) => onItemChange(item.id, { captionName: val })} 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 })}
@ -809,7 +858,7 @@ const WizardStep3 = ({
// 1. Remove items that are no longer in selectedColumns // 1. Remove items that are no longer in selectedColumns
const cleaned = groups.map((g) => ({ const cleaned = groups.map((g) => ({
...g, ...g,
items: g.items.filter((i) => selectedColumns.has(i.dataField)), items: g.items.filter((i) => selectedColumns.has(i.fieldName)),
})) }))
// 2. Ensure at least one empty group exists for the user to drag into // 2. Ensure at least one empty group exists for the user to drag into
@ -835,8 +884,14 @@ const WizardStep3 = ({
}, [selectedColumns]) }, [selectedColumns])
// ── Available columns = selected but NOT yet placed ─────────────────────── // ── Available columns = selected but NOT yet placed ───────────────────────
const placedColumns = new Set(groups.flatMap((g) => g.items.map((i) => i.dataField))) const placedColumns = new Set(groups.flatMap((g) => g.items.map((i) => i.fieldName)))
const availableColumns = [...selectedColumns].filter((c) => !placedColumns.has(c)) const availableColumns = [...selectedColumns].filter((c) => !placedColumns.has(c))
const editorScriptFields: SelectBoxOption[] = groups.flatMap((group) =>
group.items.map((item) => ({
value: item.fieldName,
label: item.captionName || item.fieldName,
})),
)
// ── Helpers ─────────────────────────────────────────────────────────────── // ── Helpers ───────────────────────────────────────────────────────────────
const colMeta = (name: string) => selectCommandColumns.find((c) => c.columnName === name) const colMeta = (name: string) => selectCommandColumns.find((c) => c.columnName === name)
@ -878,7 +933,7 @@ const WizardStep3 = ({
} }
const addAllToGroup = (groupId: string) => { const addAllToGroup = (groupId: string) => {
const placed = new Set(groups.flatMap((g) => g.items.map((i) => i.dataField))) const placed = new Set(groups.flatMap((g) => g.items.map((i) => i.fieldName)))
const toAdd = [...selectedColumns].filter((c) => !placed.has(c)) const toAdd = [...selectedColumns].filter((c) => !placed.has(c))
if (toAdd.length === 0) return if (toAdd.length === 0) return
onGroupsChange( onGroupsChange(
@ -1018,7 +1073,7 @@ const WizardStep3 = ({
<div className="flex items-center gap-2 px-3 py-2 rounded-lg border border-indigo-400 bg-indigo-50 text-sm shadow-lg cursor-grabbing"> <div className="flex items-center gap-2 px-3 py-2 rounded-lg border border-indigo-400 bg-indigo-50 text-sm shadow-lg cursor-grabbing">
<FaGripVertical className="text-indigo-300 text-xs" /> <FaGripVertical className="text-indigo-300 text-xs" />
<span className="text-xs font-semibold text-indigo-600 bg-indigo-100 px-2 py-0.5 rounded"> <span className="text-xs font-semibold text-indigo-600 bg-indigo-100 px-2 py-0.5 rounded">
{item.dataField} {item.fieldName}
</span> </span>
<span className="text-xs text-gray-500">{item.editorType}</span> <span className="text-xs text-gray-500">{item.editorType}</span>
</div> </div>
@ -1087,6 +1142,7 @@ const WizardStep3 = ({
hasAvailable={availableColumns.length > 0} hasAvailable={availableColumns.length > 0}
dbObjects={dbObjects} dbObjects={dbObjects}
dsCode={dsCode} dsCode={dsCode}
editorScriptFields={editorScriptFields}
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)}

View file

@ -37,6 +37,8 @@ type PendingLink = {
outcome: string outcome: string
} | null } | null
type RequiredWorkflowField = 'approvalUserFieldName' | 'approvalStatusFieldName'
const toDesignerCriteria = (items: ListFormWorkflowCriteriaDto[]): WorkflowCriteriaDto[] => const toDesignerCriteria = (items: ListFormWorkflowCriteriaDto[]): WorkflowCriteriaDto[] =>
items.map((item) => ({ items.map((item) => ({
...item, ...item,
@ -67,6 +69,9 @@ function WizardStep6({
const [dragPreview, setDragPreview] = useState<any>(null) const [dragPreview, setDragPreview] = useState<any>(null)
const [canvasZoom, setCanvasZoom] = useState(1) const [canvasZoom, setCanvasZoom] = useState(1)
const [designerTab, setDesignerTab] = useState('flow') const [designerTab, setDesignerTab] = useState('flow')
const [workflowErrors, setWorkflowErrors] = useState<
Partial<Record<RequiredWorkflowField, string>>
>({})
const canvasRef = useRef<HTMLDivElement | null>(null) const canvasRef = useRef<HTMLDivElement | null>(null)
const currentCriteria = useMemo(() => toDesignerCriteria(criteria), [criteria]) const currentCriteria = useMemo(() => toDesignerCriteria(criteria), [criteria])
@ -91,6 +96,10 @@ function WizardStep6({
setCriteriaForm(selected ? toCriteriaForm(selected) : emptyCriteria('Start', listFormCode)) setCriteriaForm(selected ? toCriteriaForm(selected) : emptyCriteria('Start', listFormCode))
}, [currentCriteria, listFormCode, selectedId]) }, [currentCriteria, listFormCode, selectedId])
useEffect(() => {
if (criteria.length === 0) setWorkflowErrors({})
}, [criteria.length])
const updateCriteria = (next: WorkflowCriteriaDto[]) => onCriteriaChange(toWizardCriteria(next)) const updateCriteria = (next: WorkflowCriteriaDto[]) => onCriteriaChange(toWizardCriteria(next))
const saveCriteria = (event: FormEvent<HTMLFormElement>) => { const saveCriteria = (event: FormEvent<HTMLFormElement>) => {
@ -250,6 +259,26 @@ function WizardStep6({
]) ])
} }
const continueToTodo = () => {
if (criteria.length === 0) {
setWorkflowErrors({})
onNext()
return
}
const requiredMessage = translate('::ListForms.Wizard.Required')
const nextErrors: Partial<Record<RequiredWorkflowField, string>> = {}
if (!workflow.approvalUserFieldName?.trim()) {
nextErrors.approvalUserFieldName = requiredMessage
}
if (!workflow.approvalStatusFieldName?.trim()) {
nextErrors.approvalStatusFieldName = requiredMessage
}
setWorkflowErrors(nextErrors)
if (Object.keys(nextErrors).length === 0) onNext()
}
return ( return (
<div className="flex h-[calc(100vh-250px)] min-h-[500px] flex-col overflow-hidden"> <div className="flex h-[calc(100vh-250px)] min-h-[500px] flex-col overflow-hidden">
<div className="flex-1 overflow-y-auto pr-1"> <div className="flex-1 overflow-y-auto pr-1">
@ -280,8 +309,19 @@ function WizardStep6({
'approvalDescriptionFieldName', 'approvalDescriptionFieldName',
'::ListForms.ListFormEdit.Workflow.ApprovalDescriptionFieldName', '::ListForms.ListFormEdit.Workflow.ApprovalDescriptionFieldName',
], ],
].map(([name, label]) => ( ].map(([name, label]) => {
<FormItem key={name} label={translate(label)}> const requiredForCriteria =
criteria.length > 0 &&
(name === 'approvalUserFieldName' || name === 'approvalStatusFieldName')
const error = workflowErrors[name as RequiredWorkflowField]
return (
<FormItem
key={name}
errorMessage={error}
invalid={Boolean(error)}
label={`${translate(label)}${requiredForCriteria ? ' *' : ''}`}
>
<Field name={name}> <Field name={name}>
{({ field, form }: FieldProps) => ( {({ field, form }: FieldProps) => (
<Select <Select
@ -294,6 +334,10 @@ function WizardStep6({
)} )}
onChange={(option: any) => { onChange={(option: any) => {
form.setFieldValue(field.name, option?.value ?? '') form.setFieldValue(field.name, option?.value ?? '')
setWorkflowErrors((current) => ({
...current,
[field.name]: undefined,
}))
onWorkflowChange({ onWorkflowChange({
...values, ...values,
[field.name]: option?.value ?? '', [field.name]: option?.value ?? '',
@ -304,9 +348,12 @@ function WizardStep6({
)} )}
</Field> </Field>
</FormItem> </FormItem>
))} )
})}
<FormItem <FormItem
label={translate('::ListForms.ListFormEdit.Workflow.ApprovalIsFilterUserName')} label={translate(
'::ListForms.ListFormEdit.Workflow.ApprovalIsFilterUserName',
)}
> >
<Field name="approvalIsFilterUserName"> <Field name="approvalIsFilterUserName">
{({ field, form }: FieldProps<boolean>) => ( {({ field, form }: FieldProps<boolean>) => (
@ -402,7 +449,7 @@ function WizardStep6({
variant="solid" variant="solid"
type="button" type="button"
icon={<FaArrowRight />} icon={<FaArrowRight />}
onClick={onNext} onClick={continueToTodo}
> >
{translate('::Next') || 'Next'} {translate('::Next') || 'Next'}
</Button> </Button>

View file

@ -4,7 +4,7 @@ import type { DatabaseColumnDto } from '@/proxy/sql-query-manager/models'
import type { SelectBoxOption } from '@/types/shared' import type { SelectBoxOption } from '@/types/shared'
import { Field, type FieldProps, useFormikContext } from 'formik' import { Field, type FieldProps, useFormikContext } from 'formik'
import { useMemo, useState } from 'react' import { useMemo, useState } from 'react'
import { FaArrowLeft, FaArrowRight, FaColumns } from 'react-icons/fa' import { FaArrowLeft, FaArrowRight } from 'react-icons/fa'
interface WizardStep7Props { interface WizardStep7Props {
selectedColumns: Set<string> selectedColumns: Set<string>
@ -25,18 +25,41 @@ const mappings: Array<{
| 'todoPriorityExpr' | 'todoPriorityExpr'
| 'todoCompletedExpr' | 'todoCompletedExpr'
| 'todoOrderExpr' | 'todoOrderExpr'
label: string translationKey: string
required?: boolean required?: boolean
}> = [ }> = [
{ name: 'todoTitleExpr', label: 'Başlık alanı', required: true }, {
{ name: 'todoStatusExpr', label: 'Kanban / durum alanı', required: true }, name: 'todoTitleExpr',
{ name: 'todoDescriptionExpr', label: 'Açıklama alanı' }, translationKey: '::ListForms.ListFormFieldEdit.TodoTitleExpr',
{ name: 'todoDueDateExpr', label: 'Termin tarihi alanı' }, required: true,
{ name: 'todoTagExpr', label: 'Etiket alanı' }, },
{ name: 'todoAssigneeExpr', label: 'Atanan kişi alanı' }, {
{ name: 'todoPriorityExpr', label: 'Öncelik alanı' }, name: 'todoStatusExpr',
{ name: 'todoCompletedExpr', label: 'Tamamlandı alanı' }, translationKey: '::ListForms.ListFormFieldEdit.TodoStatusExpr',
{ name: 'todoOrderExpr', label: 'Kart sıra alanı' }, required: true,
},
{
name: 'todoDescriptionExpr',
translationKey: '::ListForms.ListFormFieldEdit.TodoDescriptionExpr',
},
{
name: 'todoDueDateExpr',
translationKey: '::ListForms.ListFormFieldEdit.TodoDueDateExpr',
},
{ name: 'todoTagExpr', translationKey: '::ListForms.ListFormFieldEdit.TodoTagExpr' },
{
name: 'todoAssigneeExpr',
translationKey: '::ListForms.ListFormFieldEdit.TodoAssigneeExpr',
},
{
name: 'todoPriorityExpr',
translationKey: '::ListForms.ListFormFieldEdit.TodoPriorityExpr',
},
{
name: 'todoCompletedExpr',
translationKey: '::ListForms.ListFormFieldEdit.TodoCompletedExpr',
},
{ name: 'todoOrderExpr', translationKey: '::ListForms.ListFormFieldEdit.TodoOrderExpr' },
] ]
const WizardStep7 = ({ const WizardStep7 = ({
@ -46,7 +69,8 @@ const WizardStep7 = ({
onBack, onBack,
onNext, onNext,
}: WizardStep7Props) => { }: WizardStep7Props) => {
const { values, setFieldValue } = useFormikContext<ListFormWizardDto>() const { errors, touched, values, setFieldTouched, setFieldValue, validateForm } =
useFormikContext<ListFormWizardDto>()
const [validationError, setValidationError] = useState('') const [validationError, setValidationError] = useState('')
const fieldOptions = useMemo<SelectBoxOption[]>( const fieldOptions = useMemo<SelectBoxOption[]>(
() => () =>
@ -56,9 +80,15 @@ const WizardStep7 = ({
[selectCommandColumns, selectedColumns], [selectCommandColumns, selectedColumns],
) )
const continueToDeploy = () => { const continueToDeploy = async () => {
if (values.todo && (!values.todoTitleExpr || !values.todoStatusExpr)) { await Promise.all([
setValidationError('Başlık alanı ve Kanban / durum alanı zorunludur.') setFieldTouched('todoTitleExpr', true, false),
setFieldTouched('todoStatusExpr', true, false),
])
const validationErrors = await validateForm()
if (validationErrors.todoTitleExpr || validationErrors.todoStatusExpr) {
setValidationError(translate('::ListForms.ListFormFieldEdit.TodoRequiredFields'))
return return
} }
setValidationError('') setValidationError('')
@ -67,34 +97,23 @@ const WizardStep7 = ({
return ( return (
<div className="pb-20"> <div className="pb-20">
<div className="mb-5 rounded-xl border border-indigo-200 bg-indigo-50/60 p-4 dark:border-indigo-900 dark:bg-indigo-950/20">
<div className="flex items-center gap-2 font-semibold text-indigo-700 dark:text-indigo-300">
<FaColumns /> Todo / Kanban ayrıntıları
</div>
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
Liste alanlarını Todo kartının başlık, durum ve diğer özellikleriyle eşleştirin.
</p>
</div>
{!values.todo ? (
<div className="rounded-xl border border-amber-200 bg-amber-50 p-4 text-sm text-amber-700 dark:border-amber-900 dark:bg-amber-950/30 dark:text-amber-200">
Todo görünümü liste ayarlarında kapalı. Bu adımı atlayabilir veya önceki adımlardan Todo
görünümünü etkinleştirebilirsiniz.
</div>
) : (
<>
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3"> <div className="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3">
{mappings.map((mapping) => ( {mappings.map((mapping) => {
const isRequired = Boolean(mapping.required && values.todo)
return (
<FormItem <FormItem
key={mapping.name} key={mapping.name}
label={`${mapping.label}${mapping.required ? ' *' : ''}`} label={`${translate(mapping.translationKey)}${isRequired ? ' *' : ''}`}
invalid={Boolean(touched[mapping.name] && errors[mapping.name])}
errorMessage={touched[mapping.name] ? errors[mapping.name] : undefined}
> >
<Field name={mapping.name}> <Field name={mapping.name}>
{({ field, form }: FieldProps) => ( {({ field, form }: FieldProps) => (
<Select <Select
field={field} field={field}
form={form} form={form}
isClearable={!mapping.required} isClearable={!isRequired}
options={fieldOptions} options={fieldOptions}
value={fieldOptions.find((option) => option.value === values[mapping.name])} value={fieldOptions.find((option) => option.value === values[mapping.name])}
onChange={(option) => { onChange={(option) => {
@ -105,18 +124,19 @@ const WizardStep7 = ({
)} )}
</Field> </Field>
</FormItem> </FormItem>
))} )
})}
</div> </div>
<div className="mt-5 grid grid-cols-1 gap-4 md:grid-cols-2"> <div className="mt-5 grid grid-cols-1 gap-4 md:grid-cols-2">
<FormItem label="Durum sırası (virgülle ayırın)"> <FormItem label={translate('::ListForms.ListFormFieldEdit.TodoStatusOrder')}>
<Field <Field
name="todoStatusOrder" name="todoStatusOrder"
component={Input} component={Input}
placeholder="Inbox, Today, This Week, This Month, Later, Done" placeholder={translate('::ListForms.ListFormFieldEdit.TodoStatusOrderPlaceholder')}
/> />
</FormItem> </FormItem>
<FormItem label="Kartları kolonlar arasında sürüklemeye izin ver"> <FormItem label={translate('::ListForms.ListFormFieldEdit.TodoAllowDragging')}>
<Field name="todoAllowDragging"> <Field name="todoAllowDragging">
{({ field }: FieldProps<boolean>) => ( {({ field }: FieldProps<boolean>) => (
<Checkbox <Checkbox
@ -127,15 +147,13 @@ const WizardStep7 = ({
</Field> </Field>
</FormItem> </FormItem>
</div> </div>
</>
)}
{validationError && <p className="mt-4 text-sm text-red-500">{validationError}</p>} {validationError && <p className="mt-4 text-sm text-red-500">{validationError}</p>}
<div className="fixed bottom-0 left-0 right-0 z-10 flex h-16 items-center border-t border-gray-200 bg-white px-6 dark:border-gray-700 dark:bg-gray-900"> <div className="fixed bottom-0 left-0 right-0 z-10 flex h-16 items-center border-t border-gray-200 bg-white px-6 dark:border-gray-700 dark:bg-gray-900">
<div className="flex w-full items-center justify-between gap-3"> <div className="flex w-full items-center justify-between gap-3">
<Button size="sm" variant="default" type="button" icon={<FaArrowLeft />} onClick={onBack}> <Button size="sm" variant="default" type="button" icon={<FaArrowLeft />} onClick={onBack}>
{translate('::Back') || 'Geri'} {translate('::Back')}
</Button> </Button>
<Button <Button
size="sm" size="sm"
@ -144,7 +162,7 @@ const WizardStep7 = ({
icon={<FaArrowRight />} icon={<FaArrowRight />}
onClick={continueToDeploy} onClick={continueToDeploy}
> >
{translate('::Next') || 'Deploy adımına geç'} {translate('::Next')}
</Button> </Button>
</div> </div>
</div> </div>

View file

@ -218,7 +218,9 @@ const WizardStep8 = ({
...prev, ...prev,
{ {
id: 999, id: 999,
label: `Hata: ${err?.message ?? 'Bilinmeyen hata'}`, label: `${translate('::ListForms.Wizard.Step4.Error')}: ${
err?.message ?? translate('::App.Platform.Unknown')
}`,
status: 'error', status: 'error',
}, },
]) ])
@ -231,10 +233,10 @@ const WizardStep8 = ({
const totalFields = groups.reduce((acc, g) => acc + g.items.length, 0) const totalFields = groups.reduce((acc, g) => acc + g.items.length, 0)
const editingFormFields = groups.flatMap((g) => const editingFormFields = groups.flatMap((g) =>
g.items g.items
.filter((item) => item.includeInEditingForm && item.dataField !== values.keyFieldName) .filter((item) => item.includeInEditingForm && item.fieldName !== values.keyFieldName)
.map((item) => ({ ...item, groupCaption: g.caption })), .map((item) => ({ ...item, groupCaption: g.caption })),
) )
const groupedFieldNames = new Set(groups.flatMap((g) => g.items.map((item) => item.dataField))) const groupedFieldNames = new Set(groups.flatMap((g) => g.items.map((item) => item.fieldName)))
const ungroupedSelectedColumns = [...selectedColumns].filter((col) => !groupedFieldNames.has(col)) const ungroupedSelectedColumns = [...selectedColumns].filter((col) => !groupedFieldNames.has(col))
const hasWorkflowFields = Boolean( const hasWorkflowFields = Boolean(
workflow.approvalUserFieldName || workflow.approvalUserFieldName ||
@ -387,8 +389,8 @@ const WizardStep8 = ({
</span> </span>
) : ( ) : (
g.items.map((item) => { g.items.map((item) => {
const meta = selectCommandColumns.find((c) => c.columnName === item.dataField) const meta = selectCommandColumns.find((c) => c.columnName === item.fieldName)
const isKeyField = item.dataField === values.keyFieldName const isKeyField = item.fieldName === values.keyFieldName
const isPopupField = item.includeInEditingForm && !isKeyField const isPopupField = item.includeInEditingForm && !isKeyField
return ( return (
<div <div
@ -397,7 +399,7 @@ const WizardStep8 = ({
> >
<div className="flex min-w-0 items-center gap-2"> <div className="flex min-w-0 items-center gap-2">
<span className="text-xs font-medium text-indigo-600 dark:text-indigo-400 truncate"> <span className="text-xs font-medium text-indigo-600 dark:text-indigo-400 truncate">
{item.dataField} {item.fieldName}
</span> </span>
{meta?.dataType && ( {meta?.dataType && (
<span className="text-[10px] text-gray-400 truncate"> <span className="text-[10px] text-gray-400 truncate">
@ -574,17 +576,52 @@ const WizardStep8 = ({
{values.todo && ( {values.todo && (
<Section title={translate('::ListForms.ListFormEdit.TabTodo') || 'Todo / Kanban'}> <Section title={translate('::ListForms.ListFormEdit.TabTodo') || 'Todo / Kanban'}>
<div className="grid grid-cols-2 gap-x-4"> <div className="grid grid-cols-2 gap-x-4">
<Row label="Başlık alanı" value={values.todoTitleExpr} /> <Row
<Row label="Kanban / durum alanı" value={values.todoStatusExpr} /> label={translate('::ListForms.ListFormFieldEdit.TodoTitleExpr')}
<Row label="Açıklama alanı" value={values.todoDescriptionExpr} /> value={values.todoTitleExpr}
<Row label="Termin tarihi alanı" value={values.todoDueDateExpr} /> />
<Row label="Etiket alanı" value={values.todoTagExpr} /> <Row
<Row label="Atanan kişi alanı" value={values.todoAssigneeExpr} /> label={translate('::ListForms.ListFormFieldEdit.TodoStatusExpr')}
<Row label="Öncelik alanı" value={values.todoPriorityExpr} /> value={values.todoStatusExpr}
<Row label="Tamamlandı alanı" value={values.todoCompletedExpr} /> />
<Row label="Kart sıra alanı" value={values.todoOrderExpr} /> <Row
<Row label="Durum sırası" value={values.todoStatusOrder} /> label={translate('::ListForms.ListFormFieldEdit.TodoDescriptionExpr')}
<Row label="Sürükle-bırak" value={values.todoAllowDragging ? 'Aktif' : 'Pasif'} /> value={values.todoDescriptionExpr}
/>
<Row
label={translate('::ListForms.ListFormFieldEdit.TodoDueDateExpr')}
value={values.todoDueDateExpr}
/>
<Row
label={translate('::ListForms.ListFormFieldEdit.TodoTagExpr')}
value={values.todoTagExpr}
/>
<Row
label={translate('::ListForms.ListFormFieldEdit.TodoAssigneeExpr')}
value={values.todoAssigneeExpr}
/>
<Row
label={translate('::ListForms.ListFormFieldEdit.TodoPriorityExpr')}
value={values.todoPriorityExpr}
/>
<Row
label={translate('::ListForms.ListFormFieldEdit.TodoCompletedExpr')}
value={values.todoCompletedExpr}
/>
<Row
label={translate('::ListForms.ListFormFieldEdit.TodoOrderExpr')}
value={values.todoOrderExpr}
/>
<Row
label={translate('::ListForms.ListFormFieldEdit.TodoStatusOrder')}
value={values.todoStatusOrder}
/>
<Row
label={translate('::ListForms.ListFormFieldEdit.TodoAllowDragging')}
value={translate(
values.todoAllowDragging ? '::App.Status.Active' : '::App.Status.Inactive',
)}
/>
</div> </div>
</Section> </Section>
)} )}
@ -643,12 +680,12 @@ const WizardStep8 = ({
</span> </span>
{isDone && ( {isDone && (
<span className="text-xs text-emerald-500 font-semibold flex items-center gap-1"> <span className="text-xs text-emerald-500 font-semibold flex items-center gap-1">
<FaCheckCircle /> {translate('::ListForms.Wizard.Step4.Success') || 'Başarılı'} <FaCheckCircle /> {translate('::ListForms.Wizard.Step4.Success')}
</span> </span>
)} )}
{hasError && ( {hasError && (
<span className="text-xs text-red-500 font-semibold flex items-center gap-1"> <span className="text-xs text-red-500 font-semibold flex items-center gap-1">
<FaExclamationCircle /> {translate('::ListForms.Wizard.Step4.Error') || 'Hata'} <FaExclamationCircle /> {translate('::ListForms.Wizard.Step4.Error')}
</span> </span>
)} )}
</div> </div>
@ -658,7 +695,7 @@ const WizardStep8 = ({
<div className="flex flex-col items-center justify-center h-full gap-3 py-10 select-none"> <div className="flex flex-col items-center justify-center h-full gap-3 py-10 select-none">
<FaRocket className="text-gray-700 text-3xl" /> <FaRocket className="text-gray-700 text-3xl" />
<span className="text-xs text-gray-600 italic text-center"> <span className="text-xs text-gray-600 italic text-center">
{translate('::ListForms.Wizard.Step4.AllInfoReady') || 'Tüm bilgiler hazır.'} {translate('::ListForms.Wizard.Step4.AllInfoReady')}
<br /> <br />
{translate('::ListForms.Wizard.Step4.DeployStartHint')} {translate('::ListForms.Wizard.Step4.DeployStartHint')}
</span> </span>

View file

@ -90,6 +90,47 @@ interface TableIndex {
// ─── Constants ──────────────────────────────────────────────────────────────── // ─── Constants ────────────────────────────────────────────────────────────────
const REL_TYPES: { value: RelationshipType; label: string; desc: string }[] = [
{ value: 'OneToMany', label: '1 → N', desc: 'Bire-çok' },
{ value: 'OneToOne', label: '1 → 1', desc: 'Bire-bir' },
]
const CASCADE_OPTIONS: { value: CascadeBehavior; label: string }[] = [
{ value: 'NoAction', label: 'No Action' },
{ value: 'Cascade', label: 'Cascade' },
{ value: 'SetNull', label: 'Set Null' },
{ value: 'Restrict', label: 'Restrict' },
]
const EMPTY_FK: Omit<SqlTableRelation, 'id'> = {
relationshipType: 'OneToMany',
fkColumnName: '',
referencedTable: '',
referencedColumn: 'Id',
cascadeDelete: 'NoAction',
cascadeUpdate: 'Cascade',
isRequired: false,
description: '',
}
const EMPTY_INDEX: Omit<TableIndex, 'id'> = {
indexName: '',
indexType: 'Index',
isClustered: false,
columns: [],
description: '',
}
const INDEX_TYPES: { value: IndexType; label: string; desc: string }[] = [
{
value: 'PrimaryKey',
label: 'Primary Key',
desc: 'App.SqlQueryManager.IndexType_PrimaryKey_Desc',
},
{ value: 'UniqueKey', label: 'Unique Key', desc: 'App.SqlQueryManager.IndexType_UniqueKey_Desc' },
{ value: 'Index', label: 'Index', desc: 'App.SqlQueryManager.IndexType_Index_Desc' },
]
const DATA_TYPES: { value: SqlDataType; label: string }[] = [ const DATA_TYPES: { value: SqlDataType; label: string }[] = [
{ value: 'nvarchar', label: 'String (nvarchar)' }, { value: 'nvarchar', label: 'String (nvarchar)' },
{ value: 'nvarchar(MAX)', label: 'Text (nvarchar MAX)' }, { value: 'nvarchar(MAX)', label: 'Text (nvarchar MAX)' },
@ -180,47 +221,6 @@ const FULL_AUDIT_COLUMNS: ColumnDefinition[] = [
}, },
] ]
const REL_TYPES: { value: RelationshipType; label: string; desc: string }[] = [
{ value: 'OneToMany', label: '1 → N', desc: 'Bire-çok' },
{ value: 'OneToOne', label: '1 → 1', desc: 'Bire-bir' },
]
const CASCADE_OPTIONS: { value: CascadeBehavior; label: string }[] = [
{ value: 'NoAction', label: 'No Action' },
{ value: 'Cascade', label: 'Cascade' },
{ value: 'SetNull', label: 'Set Null' },
{ value: 'Restrict', label: 'Restrict' },
]
const EMPTY_FK: Omit<SqlTableRelation, 'id'> = {
relationshipType: 'OneToMany',
fkColumnName: '',
referencedTable: '',
referencedColumn: 'Id',
cascadeDelete: 'NoAction',
cascadeUpdate: 'Cascade',
isRequired: false,
description: '',
}
const EMPTY_INDEX: Omit<TableIndex, 'id'> = {
indexName: '',
indexType: 'Index',
isClustered: false,
columns: [],
description: '',
}
const INDEX_TYPES: { value: IndexType; label: string; desc: string }[] = [
{
value: 'PrimaryKey',
label: 'Primary Key',
desc: 'App.SqlQueryManager.IndexType_PrimaryKey_Desc',
},
{ value: 'UniqueKey', label: 'Unique Key', desc: 'App.SqlQueryManager.IndexType_UniqueKey_Desc' },
{ value: 'Index', label: 'Index', desc: 'App.SqlQueryManager.IndexType_Index_Desc' },
]
const TENANT_COLUMN: ColumnDefinition = { const TENANT_COLUMN: ColumnDefinition = {
id: '__TenantId', id: '__TenantId',
columnName: 'TenantId', columnName: 'TenantId',
@ -232,6 +232,15 @@ const TENANT_COLUMN: ColumnDefinition = {
} }
const WORKFLOW_COLUMNS: ColumnDefinition[] = [ const WORKFLOW_COLUMNS: ColumnDefinition[] = [
{
id: '__WorkflowId',
columnName: 'Id',
dataType: 'uniqueidentifier',
maxLength: '',
isNullable: false,
defaultValue: 'NEWID()',
description: 'Workflow primary key',
},
{ {
id: '__ApprovalUserId', id: '__ApprovalUserId',
columnName: 'ApprovalUserName', columnName: 'ApprovalUserName',
@ -1324,6 +1333,26 @@ const SqlTableDesignerDialog = ({
const clearAllColumns = () => setColumns([createEmptyColumn()]) const clearAllColumns = () => setColumns([createEmptyColumn()])
const addAutomaticPrimaryKey = (preset: string, description: string) => {
const tableName = settings.tableName || initialTableData?.tableName || 'Table'
setIndexes((prev) => {
if (prev.some((index) => index.indexType === 'PrimaryKey')) return prev
return [
...prev,
{
id: `auto-${preset}-${crypto.randomUUID()}`,
indexName: `PK_${tableName}`,
indexType: 'PrimaryKey',
isClustered: false,
columns: [{ columnName: 'Id', order: 'ASC' }],
description,
},
]
})
}
const addFullAuditedColumns = () => { const addFullAuditedColumns = () => {
const existingNames = new Set(columns.map((c) => c.columnName.trim().toLowerCase())) const existingNames = new Set(columns.map((c) => c.columnName.trim().toLowerCase()))
const toAdd = FULL_AUDIT_COLUMNS.filter((c) => !existingNames.has(c.columnName.toLowerCase())) const toAdd = FULL_AUDIT_COLUMNS.filter((c) => !existingNames.has(c.columnName.toLowerCase()))
@ -1332,24 +1361,7 @@ const SqlTableDesignerDialog = ({
return [...nonEmpty, ...toAdd.map((c) => ({ ...c })), createEmptyColumn()] return [...nonEmpty, ...toAdd.map((c) => ({ ...c })), createEmptyColumn()]
}) })
// FullAudited ile Id eklendiğinde PK tanımı Index/Key listesine otomatik düşsün. addAutomaticPrimaryKey('full-audit', 'Full audited primary key (auto)')
setIndexes((prev) => {
const hasPk = prev.some((ix) => ix.indexType === 'PrimaryKey')
if (hasPk) return prev
const tableName = settings.tableName || initialTableData?.tableName || 'Table'
return [
...prev,
{
id: `auto-pk-${crypto.randomUUID()}`,
indexName: `PK_${tableName}`,
indexType: 'PrimaryKey',
isClustered: false,
columns: [{ columnName: 'Id', order: 'ASC' }],
description: 'Primary key (auto)',
},
]
})
} }
const addMultiTenantColumns = () => { const addMultiTenantColumns = () => {
@ -1372,6 +1384,8 @@ const SqlTableDesignerDialog = ({
return [...nonEmpty, ...toAdd.map((c) => ({ ...c })), createEmptyColumn()] return [...nonEmpty, ...toAdd.map((c) => ({ ...c })), createEmptyColumn()]
}) })
} }
addAutomaticPrimaryKey('workflow', 'Workflow primary key (auto)')
} }
const addTodoColumns = () => { const addTodoColumns = () => {
@ -1385,22 +1399,7 @@ const SqlTableDesignerDialog = ({
}) })
} }
setIndexes((prev) => { addAutomaticPrimaryKey('todo', 'Todo primary key (auto)')
if (prev.some((index) => index.indexType === 'PrimaryKey')) return prev
const tableName = settings.tableName || initialTableData?.tableName || 'Table'
return [
...prev,
{
id: `auto-todo-pk-${crypto.randomUUID()}`,
indexName: `PK_${tableName}`,
indexType: 'PrimaryKey',
isClustered: false,
columns: [{ columnName: 'Id', order: 'ASC' }],
description: 'Todo primary key (auto)',
},
]
})
} }
const importColumnsFromRememberedCreateTable = async () => { const importColumnsFromRememberedCreateTable = async () => {
@ -1511,11 +1510,10 @@ const SqlTableDesignerDialog = ({
const syncAutoPkName = (newTableName: string) => { const syncAutoPkName = (newTableName: string) => {
if (!newTableName) return if (!newTableName) return
setIndexes((prev) => setIndexes((prev) =>
prev.map((ix) => prev.map((ix) => {
ix.indexType === 'PrimaryKey' && ix.id.startsWith('auto-pk') if (!ix.id.startsWith('auto-') || ix.indexType !== 'PrimaryKey') return ix
? { ...ix, indexName: `PK_${newTableName}` } return { ...ix, indexName: `PK_${newTableName}` }
: ix, }),
),
) )
} }
@ -1862,12 +1860,12 @@ const SqlTableDesignerDialog = ({
<> <>
<div className="flex items-center justify-between py-2"> <div className="flex items-center justify-between py-2">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Button size="xs" variant="solid" color="blue-600" onClick={addFullAuditedColumns}>
{translate('::App.SqlQueryManager.AddFullAuditedColumns')}
</Button>
<Button size="xs" variant="solid" color="green-600" onClick={addMultiTenantColumns}> <Button size="xs" variant="solid" color="green-600" onClick={addMultiTenantColumns}>
{translate('::App.SqlQueryManager.AddMultiTenantColumns')} {translate('::App.SqlQueryManager.AddMultiTenantColumns')}
</Button> </Button>
<Button size="xs" variant="solid" color="blue-600" onClick={addFullAuditedColumns}>
{translate('::App.SqlQueryManager.AddFullAuditedColumns')}
</Button>
<Button size="xs" variant="solid" color="indigo-600" onClick={addWorkflowColumns}> <Button size="xs" variant="solid" color="indigo-600" onClick={addWorkflowColumns}>
{translate('::App.SqlQueryManager.AddWorkflowColumns')} {translate('::App.SqlQueryManager.AddWorkflowColumns')}
</Button> </Button>

View file

@ -99,6 +99,7 @@ const toCardColumn = (
const isImageColumn = const isImageColumn =
typeof column.cellTemplate === 'function' && typeof column.cellTemplate === 'function' &&
Object.prototype.hasOwnProperty.call(column.extras ?? {}, 'imageUploadOptions') Object.prototype.hasOwnProperty.call(column.extras ?? {}, 'imageUploadOptions')
const isHtmlColumn = column.colData?.editorType2 === 'dxHtmlEditor'
return Object.fromEntries( return Object.fromEntries(
Object.entries({ Object.entries({
@ -144,6 +145,13 @@ const toCardColumn = (
}) })
return container return container
} }
: isHtmlColumn
? (templateOptions: any) => {
const templateData = templateOptions?.model ?? templateOptions
const container = document.createElement('div')
container.innerHTML = String(templateData.field?.value ?? '')
return container
}
: undefined, : undefined,
format: column.format, format: column.format,
headerFilter: column.headerFilter, headerFilter: column.headerFilter,
@ -1239,7 +1247,6 @@ const CardView = (props: CardViewProps) => {
/> />
<FilterPanel <FilterPanel
visible={gridDto.gridOptions.filterPanelDto.visible} visible={gridDto.gridOptions.filterPanelDto.visible}
filterEnabled={gridDto.gridOptions.filterPanelDto.filterEnabled}
texts={gridDto.gridOptions.filterPanelDto.texts} texts={gridDto.gridOptions.filterPanelDto.texts}
/> />
<HeaderFilter <HeaderFilter

View file

@ -2,8 +2,17 @@ import type { GridDto, TodoOptionDto } from '@/proxy/form/models'
import type { IdentityUserDto } from '@/proxy/admin/models' import type { IdentityUserDto } from '@/proxy/admin/models'
import type { DataGridRef } from 'devextreme-react/data-grid' import type { DataGridRef } from 'devextreme-react/data-grid'
import type CustomStore from 'devextreme/data/custom_store' import type CustomStore from 'devextreme/data/custom_store'
import { HtmlEditor, ImageUpload, Item, MediaResizing, Toolbar } from 'devextreme-react/html-editor'
import TagBox from 'devextreme-react/tag-box' import TagBox from 'devextreme-react/tag-box'
import { AVATAR_URL } from '@/constants/app.constant' import { AVATAR_URL } from '@/constants/app.constant'
import {
fontFamilyOptions,
fontSizeOptions,
fontValues,
headerOptions,
headerValues,
sizeValues,
} from '@/proxy/reports/data'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import relativeTime from 'dayjs/plugin/relativeTime' import relativeTime from 'dayjs/plugin/relativeTime'
import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from 'react' import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from 'react'
@ -60,6 +69,16 @@ interface UserOption {
const fieldValue = (row: TodoRow, field?: string) => (field ? row[field] : undefined) const fieldValue = (row: TodoRow, field?: string) => (field ? row[field] : undefined)
const htmlToPlainText = (value: unknown) => {
const html = String(value ?? '')
if (!html) return ''
if (typeof document === 'undefined') return html.replace(/<[^>]*>/g, ' ')
const container = document.createElement('div')
container.innerHTML = html
return (container.textContent ?? '').replace(/\s+/g, ' ').trim()
}
const textList = (value: unknown) => const textList = (value: unknown) =>
Array.isArray(value) Array.isArray(value)
? value.map(String) ? value.map(String)
@ -682,18 +701,51 @@ const TodoBoard = ({ listFormCode, searchParams, gridDto }: TodoBoardProps) => {
<label className={labelClass}> <label className={labelClass}>
{translate('::ListForms.TodoBoard.Description')} {translate('::ListForms.TodoBoard.Description')}
</label> </label>
<textarea <HtmlEditor
className={`${inputClass} min-h-36 resize-y`} height={220}
disabled={!canEdit}
placeholder={translate('::ListForms.TodoBoard.DescriptionPlaceholder')} placeholder={translate('::ListForms.TodoBoard.DescriptionPlaceholder')}
readOnly={!canEdit}
value={String(draft[options.descriptionExpr] ?? '')} value={String(draft[options.descriptionExpr] ?? '')}
onChange={(event) => onValueChanged={(event) =>
setDraft((current) => ({ setDraft((current) => ({
...current, ...current,
[options.descriptionExpr!]: event.target.value, [options.descriptionExpr!]: event.value,
})) }))
} }
/> >
<MediaResizing enabled={true} />
<ImageUpload fileUploadMode="base64" />
<Toolbar multiline>
<Item name="undo" />
<Item name="redo" />
<Item name="separator" />
<Item name="size" acceptedValues={sizeValues} options={fontSizeOptions} />
<Item name="font" acceptedValues={fontValues} options={fontFamilyOptions} />
<Item name="separator" />
<Item name="bold" />
<Item name="italic" />
<Item name="underline" />
<Item name="strike" />
<Item name="separator" />
<Item name="orderedList" />
<Item name="bulletList" />
<Item name="separator" />
<Item name="header" acceptedValues={headerValues} options={headerOptions} />
<Item name="separator" />
<Item name="color" />
<Item name="background" />
<Item name="separator" />
<Item name="alignLeft" />
<Item name="alignCenter" />
<Item name="alignRight" />
<Item name="alignJustify" />
<Item name="separator" />
<Item name="link" />
<Item name="image" />
<Item name="separator" />
<Item name="clear" />
</Toolbar>
</HtmlEditor>
</div> </div>
)} )}
<div className="grid gap-4 sm:grid-cols-2"> <div className="grid gap-4 sm:grid-cols-2">
@ -972,9 +1024,9 @@ const TodoBoard = ({ listFormCode, searchParams, gridDto }: TodoBoardProps) => {
</form> </form>
) : ( ) : (
<> <>
<h3 className="min-w-0 truncate font-semibold" title={status}> <h4 className="min-w-0 truncate font-sm" title={status}>
{status} {status}
</h3> </h4>
<button <button
type="button" type="button"
className="rounded p-1 text-gray-400 opacity-0 transition hover:bg-gray-200 hover:text-blue-600 group-hover:opacity-100 dark:hover:bg-gray-700" className="rounded p-1 text-gray-400 opacity-0 transition hover:bg-gray-200 hover:text-blue-600 group-hover:opacity-100 dark:hover:bg-gray-700"
@ -1152,7 +1204,7 @@ const TodoBoard = ({ listFormCode, searchParams, gridDto }: TodoBoardProps) => {
</div> </div>
{fieldValue(row, options.descriptionExpr) && ( {fieldValue(row, options.descriptionExpr) && (
<p className="mt-1 line-clamp-2 text-xs leading-5 text-gray-500 dark:text-gray-400"> <p className="mt-1 line-clamp-2 text-xs leading-5 text-gray-500 dark:text-gray-400">
{String(fieldValue(row, options.descriptionExpr))} {htmlToPlainText(fieldValue(row, options.descriptionExpr))}
</p> </p>
)} )}
{due && ( {due && (

View file

@ -922,6 +922,10 @@ const useListFormColumns = ({
column.allowEditing = colData?.allowEditing || colData?.allowAdding column.allowEditing = colData?.allowEditing || colData?.allowAdding
if (colData.editorType2 === 'dxHtmlEditor') {
column.encodeHtml = false
}
// #region lookup ayarlari // #region lookup ayarlari
if (colData.lookupDto?.dataSourceType) { if (colData.lookupDto?.dataSourceType) {
// UiColumnEditorTemplateTypeEnum : None:0, Table:1, TagBox:2 // UiColumnEditorTemplateTypeEnum : None:0, Table:1, TagBox:2