WizardManager düzenlemesi

This commit is contained in:
Sedat Öztürk 2026-07-28 21:36:37 +03:00
parent 3e50a28176
commit a196e84890
15 changed files with 1685 additions and 1465 deletions

View file

@ -52,45 +52,11 @@ public class ListFormWizardDto
public string KeyFieldName { get; set; } public string KeyFieldName { get; set; }
public DbType KeyFieldDbSourceType { get; set; } public DbType KeyFieldDbSourceType { get; set; }
// Tree options (required when DefaultLayout = "tree") public TreeOptionDto TreeOptionDto { get; set; } = new();
public string TreeKeyExpr { get; set; } public GanttOptionDto GanttOptionDto { get; set; } = new();
public string TreeParentIdExpr { get; set; } public SchedulerOptionDto SchedulerOptionDto { get; set; } = new();
public bool TreeAutoExpandAll { get; set; } public TodoOptionDto TodoOptionDto { get; set; } = new();
public WorkflowDto WorkflowDto { get; set; } = new();
// Gantt options (required when DefaultLayout = "gantt")
public string GanttKeyExpr { get; set; }
public string GanttParentIdExpr { get; set; }
public bool GanttAutoExpandAll { get; set; }
public string GanttTitleExpr { get; set; }
public string GanttStartExpr { get; set; }
public string GanttEndExpr { get; set; }
public string GanttProgressExpr { get; set; }
// Scheduler options (required when DefaultLayout = "scheduler")
public string SchedulerTextExpr { get; set; }
public string SchedulerStartDateExpr { get; set; }
public string SchedulerEndDateExpr { get; set; }
// Todo options (required when DefaultLayout = "todo")
public string TodoTitleExpr { get; set; }
public string TodoStatusExpr { get; set; }
public string TodoDescriptionExpr { get; set; }
public string TodoDueDateExpr { get; set; }
public string TodoTagExpr { get; set; }
public string TodoAssigneeExpr { get; set; }
public string TodoPriorityExpr { get; set; }
public string TodoCompletedExpr { get; set; }
public string TodoOrderExpr { get; set; }
public string TodoStatusOrder { get; set; }
public bool TodoAllowDragging { get; set; } = true;
// Workflow options (required when DefaultLayout = "workflow")
public string ApprovalUserFieldName { get; set; }
public string ApprovalDateFieldName { get; set; }
public string ApprovalStatusFieldName { get; set; }
public string ApprovalDescriptionFieldName { get; set; }
public bool ApprovalIsFilterUserName { get; set; }
public List<ListFormWorkflowCriteriaDto> Criteria { get; set; } = [];
public List<WizardColumnGroupInputDto> Groups { get; set; } = new(); public List<WizardColumnGroupInputDto> Groups { get; set; } = new();
public List<SubFormDto> SubForms { get; set; } = new(); public List<SubFormDto> SubForms { get; set; } = new();

View file

@ -264,18 +264,11 @@ public class ListFormWizardAppService(
var tableColumns = await GetTableColumnNamesAsync(input.DataSourceCode, input.SelectCommandType, input.SelectCommand); var tableColumns = await GetTableColumnNamesAsync(input.DataSourceCode, input.SelectCommandType, input.SelectCommand);
var isDeleted = tableColumns.Contains("IsDeleted"); var isDeleted = tableColumns.Contains("IsDeleted");
var isCreated = tableColumns.Contains("CreatorId"); var isCreated = tableColumns.Contains("CreatorId");
input.Criteria ??= []; input.WorkflowDto ??= new WorkflowDto();
EnsureUniqueWorkflowCriteriaTitles(input.Criteria); input.WorkflowDto.Criteria ??= [];
NormalizeWorkflowCriteriaIds(listFormCode, input.Criteria); EnsureUniqueWorkflowCriteriaTitles(input.WorkflowDto.Criteria);
var workflow = new WorkflowDto NormalizeWorkflowCriteriaIds(listFormCode, input.WorkflowDto.Criteria);
{ var workflow = input.WorkflowDto;
ApprovalUserFieldName = input.ApprovalUserFieldName,
ApprovalIsFilterUserName = input.ApprovalIsFilterUserName,
ApprovalDateFieldName = input.ApprovalDateFieldName,
ApprovalStatusFieldName = input.ApprovalStatusFieldName,
ApprovalDescriptionFieldName = input.ApprovalDescriptionFieldName,
Criteria = input.Criteria,
};
await repoListForm.InsertAsync(new ListForm await repoListForm.InsertAsync(new ListForm
{ {
@ -283,7 +276,7 @@ public class ListFormWizardAppService(
PageSize = 10, PageSize = 10,
ExportJson = WizardConsts.DefaultExportJson, ExportJson = WizardConsts.DefaultExportJson,
IsSubForm = false, IsSubForm = false,
ShowNote = input.SubForms.Count > 0 || input.Criteria.Count > 0, ShowNote = input.SubForms.Count > 0 || workflow.Criteria.Count > 0,
LayoutJson = WizardConsts.DefaultLayoutJson(input.DefaultLayout, input.Grid, input.Card, input.Pivot, input.Tree, input.Chart, input.Gantt, input.Scheduler, input.Todo), LayoutJson = WizardConsts.DefaultLayoutJson(input.DefaultLayout, input.Grid, input.Card, input.Pivot, input.Tree, input.Chart, input.Gantt, input.Scheduler, input.Todo),
CultureName = LanguageCodes.En, CultureName = LanguageCodes.En,
ListFormCode = input.ListFormCode, ListFormCode = input.ListFormCode,
@ -304,7 +297,7 @@ public class ListFormWizardAppService(
HeaderFilterJson = WizardConsts.DefaultHeaderFilterJson(), HeaderFilterJson = WizardConsts.DefaultHeaderFilterJson(),
SearchPanelJson = WizardConsts.DefaultSearchPanelJson(), SearchPanelJson = WizardConsts.DefaultSearchPanelJson(),
GroupPanelJson = WizardConsts.DefaultGroupPanelJson(), GroupPanelJson = WizardConsts.DefaultGroupPanelJson(),
SelectionJson = WizardConsts.DefaultSelectionSingleJson(input.Criteria.Count > 0 ? PlatformConsts.GridOptions.SelectionModeSingle : PlatformConsts.GridOptions.SelectionModeNone), SelectionJson = WizardConsts.DefaultSelectionSingleJson(workflow.Criteria.Count > 0 ? PlatformConsts.GridOptions.SelectionModeSingle : PlatformConsts.GridOptions.SelectionModeNone),
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,
@ -315,52 +308,23 @@ public class ListFormWizardAppService(
EditingFormJson = editingFormDtos.Count > 0 ? JsonSerializer.Serialize(editingFormDtos) : null, EditingFormJson = editingFormDtos.Count > 0 ? JsonSerializer.Serialize(editingFormDtos) : null,
SubFormsJson = input.SubForms.Count > 0 ? JsonSerializer.Serialize(input.SubForms) : null, SubFormsJson = input.SubForms.Count > 0 ? JsonSerializer.Serialize(input.SubForms) : null,
WidgetsJson = input.Widgets.Count > 0 ? JsonSerializer.Serialize(input.Widgets) : null, WidgetsJson = input.Widgets.Count > 0 ? JsonSerializer.Serialize(input.Widgets) : null,
WorkflowJson = HasWorkflow(workflow, input.Criteria) ? JsonSerializer.Serialize(workflow) : null, WorkflowJson = HasWorkflow(workflow) ? JsonSerializer.Serialize(workflow) : null,
TreeOptionJson = (input.Tree || input.DefaultLayout == "tree") && !string.IsNullOrEmpty(input.TreeParentIdExpr) TreeOptionJson = (input.Tree || input.DefaultLayout == "tree") &&
? JsonSerializer.Serialize(new TreeOptionDto !string.IsNullOrEmpty(input.TreeOptionDto?.ParentIdExpr)
{ ? JsonSerializer.Serialize(input.TreeOptionDto)
KeyExpr = input.TreeKeyExpr,
ParentIdExpr = input.TreeParentIdExpr,
AutoExpandAll = input.TreeAutoExpandAll,
})
: null, : null,
GanttOptionJson = (input.Gantt || input.DefaultLayout == "gantt") && !string.IsNullOrEmpty(input.GanttParentIdExpr) GanttOptionJson = (input.Gantt || input.DefaultLayout == "gantt") &&
? JsonSerializer.Serialize(new GanttOptionDto !string.IsNullOrEmpty(input.GanttOptionDto?.ParentIdExpr)
{ ? JsonSerializer.Serialize(input.GanttOptionDto)
KeyExpr = input.GanttKeyExpr,
ParentIdExpr = input.GanttParentIdExpr,
AutoExpandAll = input.GanttAutoExpandAll,
TitleExpr = input.GanttTitleExpr,
StartExpr = input.GanttStartExpr,
EndExpr = input.GanttEndExpr,
ProgressExpr = input.GanttProgressExpr,
})
: null, : null,
SchedulerOptionJson = (input.Scheduler || input.DefaultLayout == "scheduler") && !string.IsNullOrEmpty(input.SchedulerTextExpr) SchedulerOptionJson = (input.Scheduler || input.DefaultLayout == "scheduler") &&
? JsonSerializer.Serialize(new SchedulerOptionDto !string.IsNullOrEmpty(input.SchedulerOptionDto?.TextExpr)
{ ? JsonSerializer.Serialize(input.SchedulerOptionDto)
TextExpr = input.SchedulerTextExpr,
StartDateExpr = input.SchedulerStartDateExpr,
EndDateExpr = input.SchedulerEndDateExpr,
})
: null, : null,
TodoOptionJson = (input.Todo || input.DefaultLayout == "todo") && TodoOptionJson = (input.Todo || input.DefaultLayout == "todo") &&
!string.IsNullOrEmpty(input.TodoTitleExpr) && !string.IsNullOrEmpty(input.TodoOptionDto?.TitleExpr) &&
!string.IsNullOrEmpty(input.TodoStatusExpr) !string.IsNullOrEmpty(input.TodoOptionDto?.StatusExpr)
? JsonSerializer.Serialize(new TodoOptionDto ? JsonSerializer.Serialize(input.TodoOptionDto)
{
TitleExpr = input.TodoTitleExpr,
StatusExpr = input.TodoStatusExpr,
DescriptionExpr = input.TodoDescriptionExpr,
DueDateExpr = input.TodoDueDateExpr,
TagExpr = input.TodoTagExpr,
AssigneeExpr = input.TodoAssigneeExpr,
PriorityExpr = input.TodoPriorityExpr,
CompletedExpr = input.TodoCompletedExpr,
OrderExpr = input.TodoOrderExpr,
StatusOrder = input.TodoStatusOrder,
AllowDragging = input.TodoAllowDragging,
})
: null, : null,
}, autoSave: true); }, autoSave: true);
@ -409,7 +373,7 @@ public class ListFormWizardAppService(
} }
} }
foreach (var criteria in input.Criteria) foreach (var criteria in workflow.Criteria)
{ {
if (string.IsNullOrWhiteSpace(criteria.Id)) if (string.IsNullOrWhiteSpace(criteria.Id))
{ {
@ -443,12 +407,12 @@ public class ListFormWizardAppService(
await SaveWizardSeedFileAsync(input, isDeleted, isCreated, inserted); await SaveWizardSeedFileAsync(input, isDeleted, isCreated, inserted);
} }
private static bool HasWorkflow(WorkflowDto workflow, List<ListFormWorkflowCriteriaDto> criteria) private static bool HasWorkflow(WorkflowDto workflow)
{ {
return workflow != null && ( return workflow != null && (
!string.IsNullOrWhiteSpace(workflow.ApprovalUserFieldName) || !string.IsNullOrWhiteSpace(workflow.ApprovalUserFieldName) ||
!string.IsNullOrWhiteSpace(workflow.ApprovalStatusFieldName) || !string.IsNullOrWhiteSpace(workflow.ApprovalStatusFieldName) ||
criteria.Count > 0 workflow.Criteria.Count > 0
); );
} }

View file

@ -15,10 +15,10 @@
"Grid": true, "Grid": true,
"Card": true, "Card": true,
"Pivot": true, "Pivot": true,
"Tree": true,
"Chart": true, "Chart": true,
"Gantt": true, "Tree": false,
"Scheduler": true, "Gantt": false,
"Scheduler": false,
"Todo": true, "Todo": true,
"LanguageTextMenuEn": "Todo", "LanguageTextMenuEn": "Todo",
"LanguageTextMenuTr": "\u0130\u015Fler", "LanguageTextMenuTr": "\u0130\u015Fler",
@ -39,36 +39,30 @@
"SelectCommand": "Pro_T_Todo", "SelectCommand": "Pro_T_Todo",
"KeyFieldName": "Id", "KeyFieldName": "Id",
"KeyFieldDbSourceType": 9, "KeyFieldDbSourceType": 9,
"TreeKeyExpr": "", "TreeOptionDto": {},
"TreeParentIdExpr": "", "GanttOptionDto": {},
"TreeAutoExpandAll": false, "SchedulerOptionDto": {},
"GanttKeyExpr": "", "TodoOptionDto": {
"GanttParentIdExpr": "", "TitleExpr": "Title",
"GanttAutoExpandAll": false, "StatusExpr": "Status",
"GanttTitleExpr": "", "DescriptionExpr": "Description",
"GanttStartExpr": "", "DueDateExpr": "DueDate",
"GanttEndExpr": "", "TagExpr": "Tags",
"GanttProgressExpr": "", "AssigneeExpr": "Assignees",
"SchedulerTextExpr": "", "PriorityExpr": "Priority",
"SchedulerStartDateExpr": "", "CompletedExpr": "Completed",
"SchedulerEndDateExpr": "", "OrderExpr": "SortOrder",
"TodoTitleExpr": "Title", "StatusOrder": "Backlog, Ready to Start, In Progress, Code Review, Testing, Staging",
"TodoStatusExpr": "Status", "AllowDragging": true
"TodoDescriptionExpr": "Description", },
"TodoDueDateExpr": "DueDate", "WorkflowDto": {
"TodoTagExpr": "Tags", "ApprovalUserFieldName": "",
"TodoAssigneeExpr": "Assignees", "ApprovalDateFieldName": "",
"TodoPriorityExpr": "Priority", "ApprovalStatusFieldName": "",
"TodoCompletedExpr": "Completed", "ApprovalDescriptionFieldName": "",
"TodoOrderExpr": "SortOrder", "ApprovalIsFilterUserName": false,
"TodoStatusOrder": "Backlog, Ready to Start, In Progress, Code Review, Testing, Staging", "Criteria": []
"TodoAllowDragging": true, },
"ApprovalUserFieldName": "",
"ApprovalDateFieldName": "",
"ApprovalStatusFieldName": "",
"ApprovalDescriptionFieldName": "",
"ApprovalIsFilterUserName": false,
"Criteria": [],
"Groups": [ "Groups": [
{ {
"Caption": "", "Caption": "",

View file

@ -15,10 +15,10 @@
"Grid": true, "Grid": true,
"Card": true, "Card": true,
"Pivot": true, "Pivot": true,
"Tree": true,
"Chart": true, "Chart": true,
"Gantt": true, "Tree": true,
"Scheduler": true, "Gantt": false,
"Scheduler": false,
"Todo": false, "Todo": false,
"LanguageTextMenuEn": "Approval", "LanguageTextMenuEn": "Approval",
"LanguageTextMenuTr": "Onaylama", "LanguageTextMenuTr": "Onaylama",
@ -39,36 +39,24 @@
"SelectCommand": "Pro_T_Approval", "SelectCommand": "Pro_T_Approval",
"KeyFieldName": "Id", "KeyFieldName": "Id",
"KeyFieldDbSourceType": 9, "KeyFieldDbSourceType": 9,
"TreeKeyExpr": "Title", "TreeOptionDto": {
"TreeParentIdExpr": "ParentTitle", "KeyExpr": "Title",
"TreeAutoExpandAll": true, "ParentIdExpr": "ParentTitle",
"GanttKeyExpr": "", "AutoExpandAll": true
"GanttParentIdExpr": "", },
"GanttAutoExpandAll": false, "GanttOptionDto": {},
"GanttTitleExpr": "", "SchedulerOptionDto": {},
"GanttStartExpr": "", "TodoOptionDto": {
"GanttEndExpr": "", "StatusOrder": "Backlog, Ready to Start, In Progress, Code Review, Testing, Staging",
"GanttProgressExpr": "", "AllowDragging": true
"SchedulerTextExpr": "", },
"SchedulerStartDateExpr": "", "WorkflowDto": {
"SchedulerEndDateExpr": "", "ApprovalUserFieldName": "ApprovalUserName",
"TodoTitleExpr": "", "ApprovalDateFieldName": "ApprovalDate",
"TodoStatusExpr": "", "ApprovalStatusFieldName": "ApprovalStatus",
"TodoDescriptionExpr": "", "ApprovalDescriptionFieldName": "ApprovalDescription",
"TodoDueDateExpr": "", "ApprovalIsFilterUserName": false,
"TodoTagExpr": "", "Criteria": [
"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", "ListFormCode": "App.Wizard.Approval",
"Kind": "Start", "Kind": "Start",
@ -141,7 +129,8 @@
"CompareOutcomes": [], "CompareOutcomes": [],
"Id": "App.Wizard.Approval-N004" "Id": "App.Wizard.Approval-N004"
} }
], ]
},
"Groups": [ "Groups": [
{ {
"Caption": "", "Caption": "",
@ -296,4 +285,4 @@
], ],
"DataSourceCodes": [] "DataSourceCodes": []
} }
} }

View file

@ -15,10 +15,10 @@
"Grid": true, "Grid": true,
"Card": true, "Card": true,
"Pivot": true, "Pivot": true,
"Tree": true,
"Chart": true, "Chart": true,
"Tree": false,
"Gantt": true, "Gantt": true,
"Scheduler": true, "Scheduler": false,
"Todo": false, "Todo": false,
"LanguageTextMenuEn": "Gantt", "LanguageTextMenuEn": "Gantt",
"LanguageTextMenuTr": "Gantt", "LanguageTextMenuTr": "Gantt",
@ -39,36 +39,29 @@
"SelectCommand": "Pro_D_Gantt", "SelectCommand": "Pro_D_Gantt",
"KeyFieldName": "Id", "KeyFieldName": "Id",
"KeyFieldDbSourceType": 9, "KeyFieldDbSourceType": 9,
"TreeKeyExpr": "", "TreeOptionDto": {},
"TreeParentIdExpr": "", "GanttOptionDto": {
"TreeAutoExpandAll": false, "KeyExpr": "Id",
"GanttKeyExpr": "Id", "ParentIdExpr": "ParentId",
"GanttParentIdExpr": "ParentId", "AutoExpandAll": true,
"GanttAutoExpandAll": true, "TitleExpr": "Title",
"GanttTitleExpr": "Title", "StartExpr": "StartDate",
"GanttStartExpr": "StartDate", "EndExpr": "EndDate",
"GanttEndExpr": "EndDate", "ProgressExpr": "Progress"
"GanttProgressExpr": "Progress", },
"SchedulerTextExpr": "", "SchedulerOptionDto": {},
"SchedulerStartDateExpr": "", "TodoOptionDto": {
"SchedulerEndDateExpr": "", "StatusOrder": "Backlog, Ready to Start, In Progress, Code Review, Testing, Staging",
"TodoTitleExpr": "", "AllowDragging": true
"TodoStatusExpr": "", },
"TodoDescriptionExpr": "", "WorkflowDto": {
"TodoDueDateExpr": "", "ApprovalUserFieldName": "",
"TodoTagExpr": "", "ApprovalDateFieldName": "",
"TodoAssigneeExpr": "", "ApprovalStatusFieldName": "",
"TodoPriorityExpr": "", "ApprovalDescriptionFieldName": "",
"TodoCompletedExpr": "", "ApprovalIsFilterUserName": false,
"TodoOrderExpr": "", "Criteria": []
"TodoStatusOrder": "Backlog, Ready to Start, In Progress, Code Review, Testing, Staging", },
"TodoAllowDragging": true,
"ApprovalUserFieldName": "",
"ApprovalDateFieldName": "",
"ApprovalStatusFieldName": "",
"ApprovalDescriptionFieldName": "",
"ApprovalIsFilterUserName": false,
"Criteria": [],
"Groups": [ "Groups": [
{ {
"Caption": "", "Caption": "",
@ -205,4 +198,4 @@
], ],
"DataSourceCodes": [] "DataSourceCodes": []
} }
} }

View file

@ -134,17 +134,10 @@ public class WizardDataSeeder : IDataSeedContributor, ITransientDependency
var isCreated = seedFile.IsCreatedField; var isCreated = seedFile.IsCreatedField;
input.SubForms ??= new List<SubFormDto>(); input.SubForms ??= new List<SubFormDto>();
input.Widgets ??= new List<WidgetEditDto>(); input.Widgets ??= new List<WidgetEditDto>();
input.Criteria ??= []; input.WorkflowDto ??= new WorkflowDto();
EnsureUniqueWorkflowCriteriaTitles(input.Criteria); input.WorkflowDto.Criteria ??= [];
var workflow = new WorkflowDto EnsureUniqueWorkflowCriteriaTitles(input.WorkflowDto.Criteria);
{ var workflow = input.WorkflowDto;
ApprovalUserFieldName = input.ApprovalUserFieldName,
ApprovalIsFilterUserName = input.ApprovalIsFilterUserName,
ApprovalDateFieldName = input.ApprovalDateFieldName,
ApprovalStatusFieldName = input.ApprovalStatusFieldName,
ApprovalDescriptionFieldName = input.ApprovalDescriptionFieldName,
Criteria = input.Criteria,
};
var wizardName = input.WizardName.Trim(); var wizardName = input.WizardName.Trim();
var code = string.IsNullOrWhiteSpace(input.MenuCode) var code = string.IsNullOrWhiteSpace(input.MenuCode)
@ -153,7 +146,7 @@ public class WizardDataSeeder : IDataSeedContributor, ITransientDependency
var listFormCode = string.IsNullOrWhiteSpace(input.ListFormCode) var listFormCode = string.IsNullOrWhiteSpace(input.ListFormCode)
? code ? code
: input.ListFormCode.Trim(); : input.ListFormCode.Trim();
NormalizeWorkflowCriteriaIds(listFormCode, input.Criteria); NormalizeWorkflowCriteriaIds(listFormCode, workflow.Criteria);
var titleLangKey = $"{listFormCode}.Title"; var titleLangKey = $"{listFormCode}.Title";
var nameLangKey = code; var nameLangKey = code;
var descLangKey = $"{listFormCode}.Desc"; var descLangKey = $"{listFormCode}.Desc";
@ -386,7 +379,7 @@ public class WizardDataSeeder : IDataSeedContributor, ITransientDependency
PageSize = 10, PageSize = 10,
ExportJson = WizardConsts.DefaultExportJson, ExportJson = WizardConsts.DefaultExportJson,
IsSubForm = false, IsSubForm = false,
ShowNote = input.SubForms.Count > 0 || input.Criteria.Count > 0, ShowNote = input.SubForms.Count > 0 || workflow.Criteria.Count > 0,
LayoutJson = WizardConsts.DefaultLayoutJson(input.DefaultLayout, input.Grid, input.Card, input.Pivot, input.Tree, input.Chart, input.Gantt, input.Scheduler, input.Todo), LayoutJson = WizardConsts.DefaultLayoutJson(input.DefaultLayout, input.Grid, input.Card, input.Pivot, input.Tree, input.Chart, input.Gantt, input.Scheduler, input.Todo),
CultureName = LanguageCodes.En, CultureName = LanguageCodes.En,
ListFormCode = input.ListFormCode, ListFormCode = input.ListFormCode,
@ -407,7 +400,7 @@ public class WizardDataSeeder : IDataSeedContributor, ITransientDependency
HeaderFilterJson = WizardConsts.DefaultHeaderFilterJson(), HeaderFilterJson = WizardConsts.DefaultHeaderFilterJson(),
SearchPanelJson = WizardConsts.DefaultSearchPanelJson(), SearchPanelJson = WizardConsts.DefaultSearchPanelJson(),
GroupPanelJson = WizardConsts.DefaultGroupPanelJson(), GroupPanelJson = WizardConsts.DefaultGroupPanelJson(),
SelectionJson = WizardConsts.DefaultSelectionSingleJson(input.Criteria.Count > 0 ? GridOptions.SelectionModeSingle : GridOptions.SelectionModeNone), SelectionJson = WizardConsts.DefaultSelectionSingleJson(workflow.Criteria.Count > 0 ? GridOptions.SelectionModeSingle : GridOptions.SelectionModeNone),
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,
@ -422,24 +415,23 @@ public class WizardDataSeeder : IDataSeedContributor, ITransientDependency
EditingFormJson = editingFormDtos.Count > 0 ? JsonSerializer.Serialize(editingFormDtos) : null, EditingFormJson = editingFormDtos.Count > 0 ? JsonSerializer.Serialize(editingFormDtos) : null,
SubFormsJson = input.SubForms.Count > 0 ? JsonSerializer.Serialize(input.SubForms) : null, SubFormsJson = input.SubForms.Count > 0 ? JsonSerializer.Serialize(input.SubForms) : null,
WidgetsJson = input.Widgets.Count > 0 ? JsonSerializer.Serialize(input.Widgets) : null, WidgetsJson = input.Widgets.Count > 0 ? JsonSerializer.Serialize(input.Widgets) : null,
WorkflowJson = HasWorkflow(workflow, input.Criteria) ? JsonSerializer.Serialize(workflow) : null, WorkflowJson = HasWorkflow(workflow) ? JsonSerializer.Serialize(workflow) : null,
TreeOptionJson = (input.Tree || input.DefaultLayout == "tree") &&
!string.IsNullOrEmpty(input.TreeOptionDto?.ParentIdExpr)
? JsonSerializer.Serialize(input.TreeOptionDto)
: null,
GanttOptionJson = (input.Gantt || input.DefaultLayout == "gantt") &&
!string.IsNullOrEmpty(input.GanttOptionDto?.ParentIdExpr)
? JsonSerializer.Serialize(input.GanttOptionDto)
: null,
SchedulerOptionJson = (input.Scheduler || input.DefaultLayout == "scheduler") &&
!string.IsNullOrEmpty(input.SchedulerOptionDto?.TextExpr)
? JsonSerializer.Serialize(input.SchedulerOptionDto)
: null,
TodoOptionJson = (input.Todo || input.DefaultLayout == "todo") && TodoOptionJson = (input.Todo || input.DefaultLayout == "todo") &&
!string.IsNullOrEmpty(input.TodoTitleExpr) && !string.IsNullOrEmpty(input.TodoOptionDto?.TitleExpr) &&
!string.IsNullOrEmpty(input.TodoStatusExpr) !string.IsNullOrEmpty(input.TodoOptionDto?.StatusExpr)
? JsonSerializer.Serialize(new TodoOptionDto ? JsonSerializer.Serialize(input.TodoOptionDto)
{
TitleExpr = input.TodoTitleExpr,
StatusExpr = input.TodoStatusExpr,
DescriptionExpr = input.TodoDescriptionExpr,
DueDateExpr = input.TodoDueDateExpr,
TagExpr = input.TodoTagExpr,
AssigneeExpr = input.TodoAssigneeExpr,
PriorityExpr = input.TodoPriorityExpr,
CompletedExpr = input.TodoCompletedExpr,
OrderExpr = input.TodoOrderExpr,
StatusOrder = input.TodoStatusOrder,
AllowDragging = input.TodoAllowDragging,
})
: null, : null,
}, autoSave: true); }, autoSave: true);
@ -493,7 +485,7 @@ public class WizardDataSeeder : IDataSeedContributor, ITransientDependency
} }
} }
foreach (var criteria in input.Criteria) foreach (var criteria in workflow.Criteria)
{ {
if (string.IsNullOrWhiteSpace(criteria.Id)) if (string.IsNullOrWhiteSpace(criteria.Id))
{ {
@ -540,14 +532,14 @@ public class WizardDataSeeder : IDataSeedContributor, ITransientDependency
menuParent.ShortName); menuParent.ShortName);
} }
private static bool HasWorkflow(WorkflowDto workflow, List<ListFormWorkflowCriteriaDto> criteria) private static bool HasWorkflow(WorkflowDto workflow)
{ {
return workflow != null && ( return workflow != null && (
!string.IsNullOrWhiteSpace(workflow.ApprovalUserFieldName) || !string.IsNullOrWhiteSpace(workflow.ApprovalUserFieldName) ||
!string.IsNullOrWhiteSpace(workflow.ApprovalDateFieldName) || !string.IsNullOrWhiteSpace(workflow.ApprovalDateFieldName) ||
!string.IsNullOrWhiteSpace(workflow.ApprovalStatusFieldName) || !string.IsNullOrWhiteSpace(workflow.ApprovalStatusFieldName) ||
!string.IsNullOrWhiteSpace(workflow.ApprovalDescriptionFieldName) || !string.IsNullOrWhiteSpace(workflow.ApprovalDescriptionFieldName) ||
(criteria?.Count ?? 0) > 0 workflow.Criteria.Count > 0
); );
} }

View file

@ -1,7 +1,11 @@
import { import {
ListFormWorkflowCriteriaDto, GanttOptionDto,
SchedulerOptionDto,
SubFormDto, SubFormDto,
TodoOptionDto,
TreeOptionDto,
WidgetEditDto, WidgetEditDto,
WorkflowDto,
SelectCommandTypeEnum, SelectCommandTypeEnum,
} from '@/proxy/form/models' } from '@/proxy/form/models'
import { ListViewLayoutType } from '@/views/admin/listForm/edit/types' import { ListViewLayoutType } from '@/views/admin/listForm/edit/types'
@ -66,45 +70,11 @@ export interface ListFormWizardDto {
keyFieldName: string keyFieldName: string
keyFieldDbSourceType: number keyFieldDbSourceType: number
// Tree options (required when defaultLayout = 'tree') treeOptionDto: TreeOptionDto
treeKeyExpr?: string ganttOptionDto: GanttOptionDto
treeParentIdExpr?: string schedulerOptionDto: SchedulerOptionDto
treeAutoExpandAll?: boolean todoOptionDto: TodoOptionDto
workflowDto: WorkflowDto
// Gantt options (required when defaultLayout = 'gantt')
ganttKeyExpr?: string
ganttParentIdExpr?: string
ganttAutoExpandAll?: boolean
ganttTitleExpr?: string
ganttStartExpr?: string
ganttEndExpr?: string
ganttProgressExpr?: string
// Scheduler options (required when defaultLayout = 'scheduler')
schedulerTextExpr?: string
schedulerStartDateExpr?: string
schedulerEndDateExpr?: string
// Todo options (required when defaultLayout = 'todo')
todoTitleExpr: string
todoStatusExpr: string
todoDescriptionExpr: string
todoDueDateExpr: string
todoTagExpr: string
todoAssigneeExpr: string
todoPriorityExpr: string
todoCompletedExpr: string
todoOrderExpr: string
todoStatusOrder: string
todoAllowDragging: boolean
// Workflow options (required when defaultLayout = 'workflow')
approvalUserFieldName: string
approvalDateFieldName: string
approvalStatusFieldName: string
approvalDescriptionFieldName: string
approvalIsFilterUserName: boolean
criteria: ListFormWorkflowCriteriaDto[]
groups?: ListFormWizardColumnGroupDto[] groups?: ListFormWizardColumnGroupDto[]
subForms?: SubFormDto[] subForms?: SubFormDto[]

View file

@ -31,6 +31,7 @@ export const ROUTES_ENUM = {
menuManager: '/admin/menuManager', menuManager: '/admin/menuManager',
listFormManagement: { listFormManagement: {
wizard: '/admin/listform/wizard', wizard: '/admin/listform/wizard',
wizardManager: '/admin/listform/wizardManager',
edit: '/admin/listform/edit/:listFormCode', edit: '/admin/listform/edit/:listFormCode',
}, },
forumManagement: '/admin/forumManagement', forumManagement: '/admin/forumManagement',

View file

@ -8,6 +8,7 @@ import { useEffect, useRef, useState } from 'react'
import { Helmet } from 'react-helmet' import { Helmet } from 'react-helmet'
import { useLocation, useNavigate } from 'react-router-dom' import { useLocation, useNavigate } from 'react-router-dom'
import * as Yup from 'yup' import * as Yup from 'yup'
import { FaFolderOpen } from 'react-icons/fa'
import { getMenus } from '@/services/menu.service' import { getMenus } from '@/services/menu.service'
import { getPermissions } from '@/services/identity.service' import { getPermissions } from '@/services/identity.service'
import { getDataSources } from '@/services/data-source.service' import { getDataSources } from '@/services/data-source.service'
@ -25,6 +26,9 @@ import WizardStep2 from './WizardStep2'
import WizardStep3, { WizardGroup, WizardGroupItem } from './WizardStep3' import WizardStep3, { WizardGroup, WizardGroupItem } from './WizardStep3'
import WizardStep7 from './WizardStep7' import WizardStep7 from './WizardStep7'
import WizardStep8 from './WizardStep8' import WizardStep8 from './WizardStep8'
import WizardStep9 from './WizardStep9'
import WizardStep10 from './WizardStep10'
import WizardStep11 from './WizardStep11'
import WizardStep4 from './WizardStep4' import WizardStep4 from './WizardStep4'
import WizardStep5 from './WizardStep5' import WizardStep5 from './WizardStep5'
import WizardStep6 from './WizardStep6' import WizardStep6 from './WizardStep6'
@ -60,22 +64,81 @@ const initialValues: ListFormWizardDto = {
grid: true, grid: true,
card: true, card: true,
pivot: true, pivot: true,
tree: true,
chart: true, chart: true,
gantt: true, tree: false,
scheduler: true, gantt: false,
scheduler: false,
todo: false, todo: false,
todoTitleExpr: '', treeOptionDto: {
todoStatusExpr: '', keyExpr: '',
todoDescriptionExpr: '', parentIdExpr: '',
todoDueDateExpr: '', hasItemsExpr: '',
todoTagExpr: '', rootValue: '',
todoAssigneeExpr: '', autoExpandAll: false,
todoPriorityExpr: '', recursiveSelection: false,
todoCompletedExpr: '', },
todoOrderExpr: '', ganttOptionDto: {
todoStatusOrder: 'Backlog, Ready to Start, In Progress, Code Review, Testing, Staging', keyExpr: '',
todoAllowDragging: true, parentIdExpr: '',
hasItemsExpr: '',
rootValue: '',
autoExpandAll: false,
recursiveSelection: false,
titleExpr: '',
startExpr: '',
endExpr: '',
progressExpr: '',
scaleType: 'weeks',
allowEditing: false,
allowTaskAdding: false,
allowTaskUpdating: false,
allowTaskDeleting: false,
allowDependencyAdding: false,
allowDependencyDeleting: false,
allowResourceAdding: false,
allowResourceDeleting: false,
},
schedulerOptionDto: {
textExpr: '',
startDateExpr: '',
endDateExpr: '',
allDayExpr: '',
recurrenceRuleExpr: '',
recurrenceExceptionExpr: '',
startDayHour: 8,
endDayHour: 18,
defaultView: 'week',
showAllDayPanel: true,
cellDuration: 30,
firstDayOfWeek: 1,
crossScrollingEnabled: false,
allowResizing: false,
allowDragging: false,
allowAdding: false,
allowEditing: false,
allowDeleting: false,
},
todoOptionDto: {
titleExpr: '',
statusExpr: '',
descriptionExpr: '',
dueDateExpr: '',
tagExpr: '',
assigneeExpr: '',
priorityExpr: '',
completedExpr: '',
orderExpr: '',
statusOrder: 'Backlog, Ready to Start, In Progress, Code Review, Testing, Staging',
allowDragging: true,
},
workflowDto: {
approvalUserFieldName: '',
approvalDateFieldName: '',
approvalStatusFieldName: '',
approvalDescriptionFieldName: '',
approvalIsFilterUserName: false,
criteria: [],
},
languageTextMenuEn: '', languageTextMenuEn: '',
languageTextMenuTr: '', languageTextMenuTr: '',
languageTextTitleEn: '', languageTextTitleEn: '',
@ -95,27 +158,8 @@ const initialValues: ListFormWizardDto = {
selectCommand: '', selectCommand: '',
keyFieldName: '', keyFieldName: '',
keyFieldDbSourceType: DbTypeEnum.Int32, keyFieldDbSourceType: DbTypeEnum.Int32,
treeKeyExpr: '',
treeParentIdExpr: '',
treeAutoExpandAll: false,
ganttKeyExpr: '',
ganttParentIdExpr: '',
ganttAutoExpandAll: false,
ganttTitleExpr: '',
ganttStartExpr: '',
ganttEndExpr: '',
ganttProgressExpr: '',
schedulerTextExpr: '',
schedulerStartDateExpr: '',
schedulerEndDateExpr: '',
subForms: [], subForms: [],
widgets: [], widgets: [],
approvalUserFieldName: '',
approvalDateFieldName: '',
approvalStatusFieldName: '',
approvalDescriptionFieldName: '',
approvalIsFilterUserName: false,
criteria: [],
} }
const step1ValidationSchema = Yup.object().shape({ const step1ValidationSchema = Yup.object().shape({
@ -147,26 +191,7 @@ const step2ValidationSchema = Yup.object().shape({
confirmDelete: Yup.boolean(), confirmDelete: Yup.boolean(),
}) })
const todoValidationSchema = Yup.object().shape({ const listFormValidationSchema = step1ValidationSchema.concat(step2ValidationSchema)
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 ───────────────────────────────────────────────────────────────────
@ -444,19 +469,11 @@ const Wizard = () => {
gantt: w.gantt ?? true, gantt: w.gantt ?? true,
scheduler: w.scheduler ?? true, scheduler: w.scheduler ?? true,
todo: w.todo ?? true, todo: w.todo ?? true,
todoTitleExpr: w.todoTitleExpr ?? '', treeOptionDto: w.treeOptionDto ?? initialValues.treeOptionDto,
todoStatusExpr: w.todoStatusExpr ?? '', ganttOptionDto: w.ganttOptionDto ?? initialValues.ganttOptionDto,
todoDescriptionExpr: w.todoDescriptionExpr ?? '', schedulerOptionDto: w.schedulerOptionDto ?? initialValues.schedulerOptionDto,
todoDueDateExpr: w.todoDueDateExpr ?? '', todoOptionDto: w.todoOptionDto ?? initialValues.todoOptionDto,
todoTagExpr: w.todoTagExpr ?? '', workflowDto: w.workflowDto ?? initialValues.workflowDto,
todoAssigneeExpr: w.todoAssigneeExpr ?? '',
todoPriorityExpr: w.todoPriorityExpr ?? '',
todoCompletedExpr: w.todoCompletedExpr ?? '',
todoOrderExpr: w.todoOrderExpr ?? '',
todoStatusOrder:
w.todoStatusOrder ??
'Backlog, Ready to Start, In Progress, Code Review, Testing, Staging',
todoAllowDragging: w.todoAllowDragging ?? true,
languageTextMenuEn: w.languageTextMenuEn ?? '', languageTextMenuEn: w.languageTextMenuEn ?? '',
languageTextMenuTr: w.languageTextMenuTr ?? '', languageTextMenuTr: w.languageTextMenuTr ?? '',
languageTextTitleEn: w.languageTextTitleEn ?? '', languageTextTitleEn: w.languageTextTitleEn ?? '',
@ -476,27 +493,8 @@ const Wizard = () => {
selectCommand: w.selectCommand ?? '', selectCommand: w.selectCommand ?? '',
keyFieldName: w.keyFieldName ?? '', keyFieldName: w.keyFieldName ?? '',
keyFieldDbSourceType: w.keyFieldDbSourceType ?? DbTypeEnum.Int32, keyFieldDbSourceType: w.keyFieldDbSourceType ?? DbTypeEnum.Int32,
treeKeyExpr: w.treeKeyExpr ?? '',
treeParentIdExpr: w.treeParentIdExpr ?? '',
treeAutoExpandAll: w.treeAutoExpandAll ?? false,
ganttKeyExpr: w.ganttKeyExpr ?? '',
ganttParentIdExpr: w.ganttParentIdExpr ?? '',
ganttAutoExpandAll: w.ganttAutoExpandAll ?? false,
ganttTitleExpr: w.ganttTitleExpr ?? '',
ganttStartExpr: w.ganttStartExpr ?? '',
ganttEndExpr: w.ganttEndExpr ?? '',
ganttProgressExpr: w.ganttProgressExpr ?? '',
schedulerTextExpr: w.schedulerTextExpr ?? '',
schedulerStartDateExpr: w.schedulerStartDateExpr ?? '',
schedulerEndDateExpr: w.schedulerEndDateExpr ?? '',
subForms: w.subForms ?? [], subForms: w.subForms ?? [],
widgets: w.widgets ?? [], widgets: w.widgets ?? [],
approvalUserFieldName: w.approvalUserFieldName ?? '',
approvalDateFieldName: w.approvalDateFieldName ?? '',
approvalStatusFieldName: w.approvalStatusFieldName ?? '',
approvalDescriptionFieldName: w.approvalDescriptionFieldName ?? '',
approvalIsFilterUserName: w.approvalIsFilterUserName ?? false,
criteria: w.criteria ?? [],
}) })
// Queue column load to run once dbObjects is available // Queue column load to run once dbObjects is available
@ -551,15 +549,8 @@ const Wizard = () => {
setSubForms(w.subForms ?? []) setSubForms(w.subForms ?? [])
setWidgets(w.widgets ?? []) setWidgets(w.widgets ?? [])
setWorkflow({ setWorkflow(w.workflowDto ?? initialValues.workflowDto)
approvalUserFieldName: w.approvalUserFieldName ?? '', setCriteria(w.workflowDto?.criteria ?? [])
approvalDateFieldName: w.approvalDateFieldName ?? '',
approvalStatusFieldName: w.approvalStatusFieldName ?? '',
approvalDescriptionFieldName: w.approvalDescriptionFieldName ?? '',
approvalIsFilterUserName: w.approvalIsFilterUserName ?? false,
criteria: w.criteria ?? [],
})
setCriteria(w.criteria ?? [])
} catch { } catch {
toast.push( toast.push(
<Notification type="danger"> <Notification type="danger">
@ -786,12 +777,10 @@ const Wizard = () => {
})), })),
subForms, subForms,
widgets, widgets,
approvalUserFieldName: workflow.approvalUserFieldName, workflowDto: {
approvalDateFieldName: workflow.approvalDateFieldName, ...workflow,
approvalStatusFieldName: workflow.approvalStatusFieldName, criteria,
approvalDescriptionFieldName: workflow.approvalDescriptionFieldName, },
approvalIsFilterUserName: workflow.approvalIsFilterUserName,
criteria,
}) })
// ✅ sonra config çek // ✅ sonra config çek
@ -822,26 +811,6 @@ const Wizard = () => {
defaultTitle={APP_NAME} defaultTitle={APP_NAME}
/> />
<div className="mb-6 mt-2">
<Steps
current={currentStep}
className="flex flex-row flex-wrap !justify-start gap-y-2 lg:flex-nowrap lg:!justify-between"
>
<Steps.Item title={translate('::ListForms.Wizard.MenuInfo') || 'Menu Info'} />
<Steps.Item
title={translate('::ListForms.Wizard.ListFormSettings') || 'List Form Settings'}
/>
<Steps.Item
title={translate('::ListForms.Wizard.ListFormFields') || 'List Form Fields'}
/>
<Steps.Item title={translate('::ListForms.ListFormEdit.SubForms') || 'Sub Forms'} />
<Steps.Item title={translate('::ListForms.ListFormEdit.TabWidgets') || 'Widgets'} />
<Steps.Item title={translate('::ListForms.ListFormEdit.TabWorkflow') || 'Workflow'} />
<Steps.Item title={translate('::ListForms.ListFormEdit.TabTodo') || 'Todo / Kanban'} />
<Steps.Item title={translate('::App.Platform.Deploy') || 'Deploy'} />
</Steps>
</div>
{isLoadingEditData && ( {isLoadingEditData && (
<p className="text-xs text-gray-400 text-center py-4 animate-pulse"> <p className="text-xs text-gray-400 text-center py-4 animate-pulse">
{translate('::App.Loading') || 'Loading...'} {translate('::App.Loading') || 'Loading...'}
@ -856,150 +825,268 @@ const Wizard = () => {
setSubmitting(false) setSubmitting(false)
}} }}
> >
{({ touched, errors, values }) => ( {({ touched, errors, values }) => {
<Form onKeyDown={preventEnterSubmit}> const activeSteps = [
<FormContainer size={currentStep >= 2 ? undefined : 'sm'}> { id: 0, title: translate('::ListForms.Wizard.MenuInfo') || 'Menu Info' },
{/* ─── Step 1: Basic Info ─────────────────────────────── */} {
{currentStep === 0 && ( id: 1,
<WizardStep1 title: translate('::ListForms.Wizard.ListFormSettings') || 'List Form Settings',
values={values} },
errors={errors} {
touched={touched} id: 2,
wizardName={values.wizardName} title: translate('::ListForms.Wizard.ListFormFields') || 'List Form Fields',
onWizardNameChange={handleWizardNameChange} },
rawMenuItems={rawMenuItems} {
menuTree={menuTree} id: 3,
isLoadingMenu={isLoadingMenu} title: translate('::ListForms.ListFormEdit.SubForms') || 'Sub Forms',
onMenuParentChange={handleMenuParentChange} },
onClearMenuParent={() => { {
formikRef.current?.setFieldValue('menuParentCode', '') id: 4,
formikRef.current?.setFieldValue('menuParentShortName', '') title: translate('::ListForms.ListFormEdit.TabWidgets') || 'Widgets',
formikRef.current?.setFieldValue('menuParentIcon', '') },
}} {
onMenuCreated={handleMenuCreated} id: 5,
onReloadMenu={getMenuList} title: translate('::ListForms.ListFormEdit.TabWorkflow') || 'Workflow',
permissionGroupList={permissionGroupList} },
isLoadingPermissionGroup={isLoadingPermissionGroup} ...(values.todo
onNext={handleNext} ? [
translate={translate} {
/> id: 6,
)} title: translate('::ListForms.ListFormEdit.TabTodo') || 'Todo / Kanban',
},
]
: []),
...(values.tree
? [
{
id: 7,
title: translate('::ListForms.ListFormEdit.TabTree') || 'Tree',
},
]
: []),
...(values.gantt
? [
{
id: 8,
title: translate('::ListForms.ListFormEdit.TabGantt') || 'Gantt',
},
]
: []),
...(values.scheduler
? [
{
id: 9,
title: translate('::ListForms.ListFormEdit.TabScheduler') || 'Scheduler',
},
]
: []),
{ id: 10, title: translate('::App.Platform.Deploy') || 'Deploy' },
]
{/* ─── Step 2: Data Settings ───────────────────────────── */} const goToAdjacentStep = (direction: -1 | 1) => {
{currentStep === 1 && ( const currentIndex = activeSteps.findIndex((step) => step.id === currentStep)
<WizardStep2 const targetStep = activeSteps[currentIndex + direction]
values={values} if (targetStep) {
errors={errors} setCurrentStep(targetStep.id)
touched={touched} }
isLoadingDataSource={isLoadingDataSource} }
dataSourceList={dataSourceList}
isDataSourceNew={isDataSourceNew}
onDataSourceSelect={setCurrentDataSource}
onDataSourceNewChange={setIsDataSourceNew}
dbObjects={dbObjects}
isLoadingDbObjects={isLoadingDbObjects}
onDbObjectsRefresh={loadDbObjects}
selectCommandColumns={selectCommandColumns}
isLoadingColumns={isLoadingColumns}
selectedColumns={selectedColumns}
onLoadColumns={loadColumns}
onClearColumns={() => {
setSelectCommandColumns([])
setSelectedColumns(new Set())
}}
onToggleColumn={toggleColumn}
onToggleAllColumns={(all) => toggleAllColumns(all, values.isTenant)}
onTenantChange={handleTenantChange}
translate={translate}
onBack={handleBack}
onNext={handleNext2}
/>
)}
{/* ─── Step 3: List Form Fields ───────────────────────────── */} return (
{currentStep === 2 && ( <>
<WizardStep3 <div className="mb-6 mt-2">
selectedColumns={selectedColumns} <Steps
selectCommandColumns={selectCommandColumns} current={activeSteps.findIndex((step) => step.id === currentStep) + 1}
groups={editingGroups} className="flex flex-row flex-wrap !justify-start gap-y-2 lg:flex-nowrap lg:!justify-between"
onGroupsChange={setEditingGroups} >
dbObjects={dbObjects} <Steps.Item
dsCode={currentDataSource} customIcon={<FaFolderOpen />}
translate={translate} status="pending"
onBack={() => setCurrentStep(1)} title={translate('::App.Listforms.WizardManager') || 'Wizard Manager'}
onNext={() => setCurrentStep(3)} onStepChange={() =>
/> navigate(ROUTES_ENUM.protected.saas.listFormManagement.wizardManager)
)} }
/>
{activeSteps.map((step) => (
<Steps.Item key={step.id} title={step.title} />
))}
</Steps>
</div>
{/* ─── Step 4: Sub Forms ───────────────────────────── */} <Form onKeyDown={preventEnterSubmit}>
{currentStep === 3 && ( <FormContainer size={currentStep >= 2 ? undefined : 'sm'}>
<WizardStep4 {/* ─── Step 1: Basic Info ─────────────────────────────── */}
subForms={subForms} {currentStep === 0 && (
selectCommandColumns={selectCommandColumns} <WizardStep1
translate={translate} values={values}
onChange={setSubForms} errors={errors}
onBack={() => setCurrentStep(2)} touched={touched}
onNext={() => setCurrentStep(4)} wizardName={values.wizardName}
/> onWizardNameChange={handleWizardNameChange}
)} rawMenuItems={rawMenuItems}
menuTree={menuTree}
isLoadingMenu={isLoadingMenu}
onMenuParentChange={handleMenuParentChange}
onClearMenuParent={() => {
formikRef.current?.setFieldValue('menuParentCode', '')
formikRef.current?.setFieldValue('menuParentShortName', '')
formikRef.current?.setFieldValue('menuParentIcon', '')
}}
onMenuCreated={handleMenuCreated}
onReloadMenu={getMenuList}
permissionGroupList={permissionGroupList}
isLoadingPermissionGroup={isLoadingPermissionGroup}
onNext={handleNext}
translate={translate}
/>
)}
{/* ─── Step 5: Widgets ───────────────────────────── */} {/* ─── Step 2: Data Settings ───────────────────────────── */}
{currentStep === 4 && ( {currentStep === 1 && (
<WizardStep5 <WizardStep2
widgets={widgets} values={values}
translate={translate} errors={errors}
onChange={setWidgets} touched={touched}
onBack={() => setCurrentStep(3)} isLoadingDataSource={isLoadingDataSource}
onNext={() => setCurrentStep(5)} dataSourceList={dataSourceList}
/> isDataSourceNew={isDataSourceNew}
)} onDataSourceSelect={setCurrentDataSource}
onDataSourceNewChange={setIsDataSourceNew}
dbObjects={dbObjects}
isLoadingDbObjects={isLoadingDbObjects}
onDbObjectsRefresh={loadDbObjects}
selectCommandColumns={selectCommandColumns}
isLoadingColumns={isLoadingColumns}
selectedColumns={selectedColumns}
onLoadColumns={loadColumns}
onClearColumns={() => {
setSelectCommandColumns([])
setSelectedColumns(new Set())
}}
onToggleColumn={toggleColumn}
onToggleAllColumns={(all) => toggleAllColumns(all, values.isTenant)}
onTenantChange={handleTenantChange}
translate={translate}
onBack={handleBack}
onNext={handleNext2}
/>
)}
{/* ─── Step 6: Workflow ───────────────────────────── */} {/* ─── Step 3: List Form Fields ───────────────────────────── */}
{currentStep === 5 && ( {currentStep === 2 && (
<WizardStep6 <WizardStep3
listFormCode={values.listFormCode} selectedColumns={selectedColumns}
workflow={workflow} selectCommandColumns={selectCommandColumns}
criteria={criteria} groups={editingGroups}
selectCommandColumns={selectCommandColumns} onGroupsChange={setEditingGroups}
translate={translate} dbObjects={dbObjects}
onWorkflowChange={setWorkflow} dsCode={currentDataSource}
onCriteriaChange={setCriteria} translate={translate}
onBack={() => setCurrentStep(4)} onBack={() => setCurrentStep(1)}
onNext={() => setCurrentStep(6)} onNext={() => setCurrentStep(3)}
/> />
)} )}
{/* ─── Step 7: Todo / Kanban ────────────────────── */} {/* ─── Step 4: Sub Forms ───────────────────────────── */}
{currentStep === 6 && ( {currentStep === 3 && (
<WizardStep7 <WizardStep4
selectedColumns={selectedColumns} subForms={subForms}
selectCommandColumns={selectCommandColumns} selectCommandColumns={selectCommandColumns}
translate={translate} translate={translate}
onBack={() => setCurrentStep(5)} onChange={setSubForms}
onNext={() => setCurrentStep(7)} onBack={() => setCurrentStep(2)}
/> onNext={() => setCurrentStep(4)}
)} />
)}
{/* ─── Step 8: Deploy ───────────────────────────── */} {/* ─── Step 5: Widgets ───────────────────────────── */}
{currentStep === 7 && ( {currentStep === 4 && (
<WizardStep8 <WizardStep5
values={values} widgets={widgets}
wizardName={values.wizardName} translate={translate}
selectedColumns={selectedColumns} onChange={setWidgets}
selectCommandColumns={selectCommandColumns} onBack={() => setCurrentStep(3)}
groups={editingGroups} onNext={() => setCurrentStep(5)}
subForms={subForms} />
widgets={widgets} )}
workflow={workflow}
criteria={criteria} {/* ─── Step 6: Workflow ───────────────────────────── */}
translate={translate} {currentStep === 5 && (
onBack={() => setCurrentStep(6)} <WizardStep6
onSubmit={handleDeploy} listFormCode={values.listFormCode}
/> workflow={workflow}
)} criteria={criteria}
</FormContainer> selectCommandColumns={selectCommandColumns}
</Form> translate={translate}
)} onWorkflowChange={setWorkflow}
onCriteriaChange={setCriteria}
onBack={() => setCurrentStep(4)}
onNext={() => goToAdjacentStep(1)}
/>
)}
{/* ─── Step 7: Todo / Kanban ────────────────────── */}
{currentStep === 6 && values.todo && (
<WizardStep7
selectedColumns={selectedColumns}
selectCommandColumns={selectCommandColumns}
translate={translate}
onBack={() => goToAdjacentStep(-1)}
onNext={() => goToAdjacentStep(1)}
/>
)}
{/* ─── Step 8: Tree ─────────────────────────────── */}
{currentStep === 7 && values.tree && (
<WizardStep8
selectCommandColumns={selectCommandColumns}
translate={translate}
onBack={() => goToAdjacentStep(-1)}
onNext={() => goToAdjacentStep(1)}
/>
)}
{/* ─── Step 9: Gantt ────────────────────────────── */}
{currentStep === 8 && values.gantt && (
<WizardStep9
selectCommandColumns={selectCommandColumns}
translate={translate}
onBack={() => goToAdjacentStep(-1)}
onNext={() => goToAdjacentStep(1)}
/>
)}
{/* ─── Step 10: Scheduler ───────────────────────── */}
{currentStep === 9 && values.scheduler && (
<WizardStep10
selectCommandColumns={selectCommandColumns}
translate={translate}
onBack={() => goToAdjacentStep(-1)}
onNext={() => goToAdjacentStep(1)}
/>
)}
{/* ─── Step 11: Deploy ──────────────────────────── */}
{currentStep === 10 && (
<WizardStep11
values={values}
wizardName={values.wizardName}
selectedColumns={selectedColumns}
selectCommandColumns={selectCommandColumns}
groups={editingGroups}
subForms={subForms}
widgets={widgets}
workflow={workflow}
criteria={criteria}
translate={translate}
onBack={() => goToAdjacentStep(-1)}
onSubmit={handleDeploy}
/>
)}
</FormContainer>
</Form>
</>
)
}}
</Formik> </Formik>
</Container> </Container>
) )

View file

@ -0,0 +1,163 @@
import { Button, Card, Checkbox, FormItem, Input, Select } from '@/components/ui'
import type { ListFormWizardDto } from '@/proxy/admin/wizard/models'
import type { DatabaseColumnDto } from '@/proxy/sql-query-manager/models'
import type { SelectBoxOption } from '@/types/shared'
import { Field, type FieldProps, useFormikContext } from 'formik'
import { useMemo } from 'react'
import { FaArrowLeft, FaArrowRight } from 'react-icons/fa'
import { firstDayOfWeekOptions, schedulerViewOptions } from '../edit/options'
interface WizardStep10Props {
selectCommandColumns: DatabaseColumnDto[]
translate: (key: string) => string
onBack: () => void
onNext: () => void
}
const WizardStep10 = ({ selectCommandColumns, translate, onBack, onNext }: WizardStep10Props) => {
const { values } = useFormikContext<ListFormWizardDto>()
const fieldOptions = useMemo<SelectBoxOption[]>(
() =>
selectCommandColumns.map((column) => ({
value: column.columnName,
label: column.columnName,
})),
[selectCommandColumns],
)
const option = values.schedulerOptionDto
const fieldMappings = [
['textExpr', '::ListForms.SchedulerOptions.TextExpr'],
['startDateExpr', '::ListForms.SchedulerOptions.StartDateExpr'],
['endDateExpr', '::ListForms.SchedulerOptions.EndDateExpr'],
['allDayExpr', '::ListForms.SchedulerOptions.AllDayExpr'],
['recurrenceRuleExpr', '::ListForms.SchedulerOptions.RecurrenceRuleExpr'],
['recurrenceExceptionExpr', '::ListForms.SchedulerOptions.RecurrenceExceptionExpr'],
] as const
const interactionFields = [
['showAllDayPanel', '::ListForms.SchedulerOptions.ShowAllDayPanel'],
['crossScrollingEnabled', '::ListForms.SchedulerOptions.CrossScrollingEnabled'],
['allowResizing', '::ListForms.SchedulerOptions.AllowResizing'],
['allowDragging', '::ListForms.SchedulerOptions.AllowDragging'],
['allowAdding', '::ListForms.SchedulerOptions.AllowAdding'],
['allowEditing', '::ListForms.ListFormEdit.AllowEditing'],
['allowDeleting', '::ListForms.SchedulerOptions.AllowDeleting'],
] as const
return (
<div className="pb-20">
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-4">
<Card header={translate('::ListForms.SchedulerOptions.BasicSettings')}>
{fieldMappings.slice(0, 4).map(([name, translationKey]) => (
<FormItem key={name} label={translate(translationKey)}>
<Field name={`schedulerOptionDto.${name}`}>
{({ field, form }: FieldProps) => (
<Select
field={field}
form={form}
isClearable
options={fieldOptions}
value={fieldOptions.find((fieldOption) => fieldOption.value === option[name])}
onChange={(selected) => form.setFieldValue(field.name, selected?.value ?? '')}
/>
)}
</Field>
</FormItem>
))}
</Card>
<Card header={translate('::ListForms.SchedulerOptions.ViewSettings')}>
<FormItem label={translate('::ListForms.SchedulerOptions.DefaultView')}>
<Field name="schedulerOptionDto.defaultView">
{({ field, form }: FieldProps) => (
<Select
field={field}
form={form}
options={schedulerViewOptions}
value={schedulerViewOptions.find(
(viewOption) => viewOption.value === option.defaultView,
)}
onChange={(selected) => form.setFieldValue(field.name, selected?.value ?? 'week')}
/>
)}
</Field>
</FormItem>
<FormItem label={translate('::ListForms.SchedulerOptions.FirstDayOfWeek')}>
<Field name="schedulerOptionDto.firstDayOfWeek">
{({ field, form }: FieldProps) => (
<Select
field={field}
form={form}
options={firstDayOfWeekOptions}
value={firstDayOfWeekOptions.find(
(dayOption) => dayOption.value === option.firstDayOfWeek,
)}
onChange={(selected) => form.setFieldValue(field.name, selected?.value ?? 1)}
/>
)}
</Field>
</FormItem>
{[
['startDayHour', 0, 23],
['endDayHour', 0, 24],
['cellDuration', 5, undefined],
].map(([name, min, max]) => (
<FormItem
key={name}
label={translate(
`::ListForms.SchedulerOptions.${String(name).charAt(0).toUpperCase()}${String(name).slice(1)}`,
)}
>
<Field
type="number"
name={`schedulerOptionDto.${name}`}
component={Input}
min={min}
max={max}
/>
</FormItem>
))}
</Card>
<Card header={translate('::ListForms.SchedulerOptions.RecurrenceSettings')}>
{fieldMappings.slice(4).map(([name, translationKey]) => (
<FormItem key={name} label={translate(translationKey)}>
<Field name={`schedulerOptionDto.${name}`}>
{({ field, form }: FieldProps) => (
<Select
field={field}
form={form}
isClearable
options={fieldOptions}
value={fieldOptions.find((fieldOption) => fieldOption.value === option[name])}
onChange={(selected) => form.setFieldValue(field.name, selected?.value ?? '')}
/>
)}
</Field>
</FormItem>
))}
</Card>
<Card header={translate('::ListForms.SchedulerOptions.InteractionSettings')}>
{interactionFields.map(([name, translationKey]) => (
<FormItem key={name} label={translate(translationKey)}>
<Field name={`schedulerOptionDto.${name}`} component={Checkbox} />
</FormItem>
))}
</Card>
</div>
<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">
<Button type="button" size="sm" icon={<FaArrowLeft />} onClick={onBack}>
{translate('::Back')}
</Button>
<Button type="button" size="sm" variant="solid" icon={<FaArrowRight />} onClick={onNext}>
{translate('::Next')}
</Button>
</div>
</div>
</div>
)
}
export default WizardStep10

View file

@ -0,0 +1,779 @@
import { Button } from '@/components/ui'
import type { DatabaseColumnDto } from '@/proxy/sql-query-manager/models'
import React, { useState } from 'react'
import {
FaArrowLeft,
FaCheckCircle,
FaChevronDown,
FaChevronRight,
FaCircle,
FaExclamationCircle,
FaRocket,
FaSpinner,
} from 'react-icons/fa'
import { WizardGroup } from './WizardStep3'
import { dbSourceTypeOptions, selectCommandTypeOptions } from '../edit/options'
import { ListFormWizardDto } from '@/proxy/admin/wizard/models'
import {
ListFormWorkflowCriteriaDto,
SubFormDto,
WidgetEditDto,
WorkflowDto,
} from '@/proxy/form/models'
import { formatWorkflowApprovers } from '@/utils/workflow/workflowHelpers'
// ─── Types ────────────────────────────────────────────────────────────────────
export interface WizardStep11Props {
values: ListFormWizardDto
wizardName: string
selectedColumns: Set<string>
selectCommandColumns: DatabaseColumnDto[]
groups: WizardGroup[]
subForms: SubFormDto[]
widgets: WidgetEditDto[]
workflow: WorkflowDto
criteria: ListFormWorkflowCriteriaDto[]
translate: (key: string) => string
onBack: () => void
onSubmit: () => Promise<void>
}
type LogStatus = 'pending' | 'running' | 'success' | 'error'
interface LogEntry {
id: number
label: string
status: LogStatus
detail?: string
}
// ─── Deploy log steps ─────────────────────────────────────────────────────────
function buildLogSteps(
values: ListFormWizardDto,
groups: WizardGroup[],
translate: (key: string, params?: Record<string, string | number>) => string,
): Omit<LogEntry, 'status'>[] {
const totalFields = groups.reduce((acc, g) => acc + g.items.length, 0)
return [
{ id: 1, label: translate('::ListForms.Wizard.Step4.Log.ValidatingConfig') },
{
id: 2,
label: translate('::ListForms.Wizard.Step4.Log.CreatingMenu', { 0: values.menuCode }),
detail: `Parent: ${values.menuParentCode}`,
},
{
id: 3,
label: translate('::ListForms.Wizard.Step4.Log.SavingLanguageTexts'),
detail: `EN: ${values.languageTextMenuEn} / TR: ${values.languageTextMenuTr}`,
},
{
id: 4,
label: translate('::ListForms.Wizard.Step4.Log.ConfiguringPermission', {
0: values.permissionGroupName,
}),
},
{
id: 5,
label: translate('::ListForms.Wizard.Step4.Log.ConnectingDataSource', {
0: values.dataSourceCode,
}),
},
{
id: 6,
label: translate('::ListForms.Wizard.Step4.Log.CreatingListForm', { 0: values.listFormCode }),
detail: `Key: ${values.keyFieldName}`,
},
{
id: 7,
label: translate('::ListForms.Wizard.Step4.Log.SavingFormGroups', {
0: groups.length,
1: totalFields,
}),
},
{ id: 8, label: translate('::ListForms.Wizard.Step4.Log.Deploying') },
{ id: 9, label: translate('::ListForms.Wizard.Step4.Log.Completed') },
]
}
// ─── Mini-components ──────────────────────────────────────────────────────────
function LogIcon({ status }: { status: LogStatus }) {
if (status === 'running') return <FaSpinner className="text-indigo-500 animate-spin shrink-0" />
if (status === 'success') return <FaCheckCircle className="text-emerald-500 shrink-0" />
if (status === 'error') return <FaExclamationCircle className="text-red-500 shrink-0" />
return <FaCircle className="text-gray-300 dark:text-gray-600 shrink-0 text-[8px] mt-1" />
}
interface SectionProps {
title: string
badge?: string | number
children: React.ReactNode
defaultOpen?: boolean
}
function Section({ title, badge, children, defaultOpen = true }: SectionProps) {
const [open, setOpen] = useState(defaultOpen)
return (
<div className="rounded-xl border border-gray-200 dark:border-gray-700 overflow-hidden">
<Button
type="button"
variant="plain"
shape="none"
className="w-full !h-auto !items-center !justify-between !rounded-none !bg-gray-50 !px-4 !py-2.5 transition-colors hover:!bg-gray-100 dark:!bg-gray-800 dark:hover:!bg-gray-700"
onClick={() => setOpen((v) => !v)}
>
<div className="flex items-center gap-2">
{open ? (
<FaChevronDown className="text-gray-400 text-xs" />
) : (
<FaChevronRight className="text-gray-400 text-xs" />
)}
<span className="text-sm font-semibold text-gray-700 dark:text-gray-200">{title}</span>
</div>
{badge !== undefined && (
<span className="text-xs bg-indigo-100 dark:bg-indigo-900/40 text-indigo-600 dark:text-indigo-400 px-2 py-0.5 rounded-full font-medium">
{badge}
</span>
)}
</Button>
{open && <div className="px-4 py-3 bg-white dark:bg-gray-900">{children}</div>}
</div>
)
}
function Row({ label, value }: { label: string; value?: string | number }) {
if (!value && value !== 0) return null
return (
<div className="flex gap-2 py-1 border-b border-gray-100 dark:border-gray-800 last:border-0">
<span className="text-xs text-gray-400 w-40 shrink-0">{label}</span>
<span className="text-xs text-gray-700 dark:text-gray-200 font-medium break-all">
{value}
</span>
</div>
)
}
// ─── WizardStep11 ─────────────────────────────────────────────────────────────
const WizardStep11 = ({
values,
wizardName,
selectedColumns,
selectCommandColumns,
groups,
subForms,
widgets,
workflow,
criteria,
translate,
onBack,
onSubmit,
}: WizardStep11Props) => {
const [logs, setLogs] = useState<LogEntry[]>([])
const [isDeploying, setIsDeploying] = useState(false)
const [isDone, setIsDone] = useState(false)
const [hasError, setHasError] = useState(false)
const steps = buildLogSteps(values, groups, translate)
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms))
const runDeploy = async () => {
if (isDeploying) return
setIsDeploying(true)
setHasError(false)
setIsDone(false)
// Initialize all as pending
setLogs(steps.map((s) => ({ ...s, status: 'pending' })))
const setStatus = (id: number, status: LogStatus) =>
setLogs((prev) => prev.map((l) => (l.id === id ? { ...l, status } : l)))
// Steps 1-7: pre-deploy simulation (fast)
for (let i = 0; i < steps.length - 2; i++) {
const step = steps[i]
setStatus(step.id, 'running')
await sleep(300 + Math.random() * 200)
setStatus(step.id, 'success')
}
// Step 8: actual API call
const deployStep = steps[steps.length - 2]
setStatus(deployStep.id, 'running')
try {
await onSubmit()
setStatus(deployStep.id, 'success')
await sleep(200)
// Step 9: done
const doneStep = steps[steps.length - 1]
setStatus(doneStep.id, 'running')
await sleep(300)
setStatus(doneStep.id, 'success')
setIsDone(true)
} catch (err: any) {
setStatus(deployStep.id, 'error')
setLogs((prev) => [
...prev,
{
id: 999,
label: `${translate('::ListForms.Wizard.Step4.Error')}: ${
err?.message ?? translate('::App.Platform.Unknown')
}`,
status: 'error',
},
])
setHasError(true)
} finally {
setIsDeploying(false)
}
}
const totalFields = groups.reduce((acc, g) => acc + g.items.length, 0)
const editingFormFields = groups.flatMap((g) =>
g.items
.filter((item) => item.includeInEditingForm && item.fieldName !== values.keyFieldName)
.map((item) => ({ ...item, groupCaption: g.caption })),
)
const groupedFieldNames = new Set(groups.flatMap((g) => g.items.map((item) => item.fieldName)))
const ungroupedSelectedColumns = [...selectedColumns].filter((col) => !groupedFieldNames.has(col))
const hasWorkflowFields = Boolean(
workflow.approvalUserFieldName ||
workflow.approvalDateFieldName ||
workflow.approvalStatusFieldName ||
workflow.approvalDescriptionFieldName,
)
const workflowItems = criteria.length > 0 ? criteria : (workflow.criteria ?? [])
return (
<div className="grid grid-cols-[3fr_2fr] gap-5 pb-24 items-start">
{/* ── Left: Summary ──────────────────────────────────────────── */}
<div className="flex flex-col gap-3">
<div className="grid grid-cols-2 gap-3 items-start">
<Section title={translate('::ListForms.Wizard.Step4.MenuInfo')}>
<Row label={translate('::ListForms.Wizard.Step4.WizardName')} value={wizardName} />
<Row label={translate('::ListForms.Wizard.Step4.MenuCode')} value={values.menuCode} />
<Row
label={translate('::ListForms.Wizard.Step4.MenuParent')}
value={values.menuParentCode}
/>
<Row
label={translate('::ListForms.Wizard.Step4.PermissionGroup')}
value={values.permissionGroupName}
/>
<Row label={translate('::App.Listform.ListformField.Icon')} value={values.menuIcon} />
<Row
label={translate('::ListForms.Wizard.Step4.MenuTr')}
value={values.languageTextMenuTr}
/>
<Row
label={translate('::ListForms.Wizard.Step4.MenuEn')}
value={values.languageTextMenuEn}
/>
<Row
label={translate('::ListForms.Wizard.Step4.MenuParentTr')}
value={values.languageTextMenuParentTr}
/>
<Row
label={translate('::ListForms.Wizard.Step4.MenuParentEn')}
value={values.languageTextMenuParentEn}
/>
</Section>
<Section title={translate('::ListForms.Wizard.Step4.ListFormSettings')}>
<Row
label={translate('::ListForms.Wizard.Step4.ListFormCode')}
value={values.listFormCode}
/>
<Row
label={translate('::ListForms.Wizard.Step4.TitleTr')}
value={values.languageTextTitleTr}
/>
<Row
label={translate('::ListForms.Wizard.Step4.TitleEn')}
value={values.languageTextTitleEn}
/>
<Row
label={translate('::ListForms.Wizard.Step4.DescTr')}
value={values.languageTextDescTr}
/>
<Row
label={translate('::ListForms.Wizard.Step4.DescEn')}
value={values.languageTextDescEn}
/>
<Row
label={translate('::ListForms.Wizard.Step4.DataSource')}
value={values.dataSourceCode}
/>
<Row
label={translate('::App.Listform.ListformField.ConnectionString')}
value={values.dataSourceConnectionString}
/>
<Row
label={translate('::ListForms.Wizard.Step4.SelectCommand')}
value={`${values.selectCommand} (${
selectCommandTypeOptions.find((o: any) => o.value === values.selectCommandType)
?.label ?? String(values.selectCommandType)
})`}
/>
<Row
label={translate('::ListForms.Wizard.Step4.KeyField')}
value={`${values.keyFieldName} (${
dbSourceTypeOptions.find((o: any) => o.value === values.keyFieldDbSourceType)
?.label ?? String(values.keyFieldDbSourceType)
})`}
/>
</Section>
</div>
<Section
title={
translate('::ListForms.Wizard.Step4.ColumnsAndFormLayout') || 'Columns & Form Layout'
}
badge={`${selectedColumns.size} ${translate('::App.Listform.ListformField.Column')} / ${editingFormFields.length} ${translate('::ListForms.Wizard.Step4.EditingForm') || 'Popup Form'}`}
>
<div className="mb-3 grid grid-cols-3 gap-2">
{[
{
label: translate('::ListForms.Wizard.Step4.SelectedColumns'),
value: selectedColumns.size,
className: 'text-indigo-600 dark:text-indigo-400',
},
{
label: translate('::ListForms.Wizard.Step4.EditingForm') || 'Popup Form',
value: editingFormFields.length,
className: 'text-emerald-600 dark:text-emerald-400',
},
{
label: translate('::ListForms.Wizard.Step4.FormGroups'),
value: groups.length,
className: 'text-gray-700 dark:text-gray-200',
},
].map((item) => (
<div
key={item.label}
className="rounded-lg border border-gray-100 dark:border-gray-800 bg-gray-50 dark:bg-gray-800 px-3 py-2"
>
<div className={`text-lg font-semibold leading-none ${item.className}`}>
{item.value}
</div>
<div className="mt-1 text-[11px] text-gray-400 truncate">{item.label}</div>
</div>
))}
</div>
<div className="flex flex-col gap-2">
{groups.map((g) => (
<div
key={g.id}
className="overflow-hidden rounded-lg border border-gray-100 dark:border-gray-800"
>
<div className="flex items-center justify-between gap-3 bg-gray-50 dark:bg-gray-800 px-3 py-2">
<span className="text-xs font-semibold text-gray-700 dark:text-gray-200 truncate">
{g.caption || `(${translate('::ListForms.Wizard.Step4.StatGroup')})`}
</span>
<div className="flex items-center gap-1.5 text-[10px] text-gray-400 shrink-0">
<span className="rounded bg-white dark:bg-gray-900 px-1.5 py-0.5">
{g.items.length} {translate('::ListForms.Wizard.Step4.StatField')}
</span>
<span className="rounded bg-white dark:bg-gray-900 px-1.5 py-0.5">
{g.colCount} {translate('::App.Listform.ListformField.Column')}
</span>
</div>
</div>
<div className="divide-y divide-gray-100 dark:divide-gray-800">
{g.items.length === 0 ? (
<span className="block px-3 py-2 text-xs text-gray-300 italic">
{translate('::ListForms.Wizard.Step4.NoFields') || 'Alan yok'}
</span>
) : (
g.items.map((item) => {
const meta = selectCommandColumns.find((c) => c.columnName === item.fieldName)
const isKeyField = item.fieldName === values.keyFieldName
const isPopupField = item.includeInEditingForm && !isKeyField
return (
<div
key={item.id}
className="grid grid-cols-[minmax(150px,1fr)_auto] items-center gap-3 px-3 py-1.5"
>
<div className="flex min-w-0 items-center gap-2">
<span className="text-xs font-medium text-indigo-600 dark:text-indigo-400 truncate">
{item.fieldName}
</span>
{meta?.dataType && (
<span className="text-[10px] text-gray-400 truncate">
{meta.dataType}
</span>
)}
</div>
<div className="flex flex-wrap justify-end gap-1">
<span className="rounded bg-indigo-50 dark:bg-indigo-900/30 px-1.5 py-0.5 text-[10px] text-indigo-600 dark:text-indigo-300">
{translate('::ListForms.Wizard.Step4.SelectedColumns') || 'List'}
</span>
{isPopupField && (
<span className="rounded bg-emerald-50 dark:bg-emerald-900/30 px-1.5 py-0.5 text-[10px] text-emerald-600 dark:text-emerald-300">
{translate('::ListForms.Wizard.Step4.EditingForm') || 'Popup Form'}
</span>
)}
{isKeyField && (
<span className="rounded bg-amber-50 dark:bg-amber-900/30 px-1.5 py-0.5 text-[10px] text-amber-600 dark:text-amber-300">
{translate('::ListForms.Wizard.Step4.KeyField')}
</span>
)}
<span className="rounded bg-gray-100 dark:bg-gray-800 px-1.5 py-0.5 text-[10px] text-gray-400">
{item.editorType}
</span>
<span className="rounded bg-gray-100 dark:bg-gray-800 px-1.5 py-0.5 text-[10px] text-gray-400">
span-{item.colSpan}
{item.isRequired && <span className="ml-1 text-red-400">*</span>}
</span>
</div>
</div>
)
})
)}
</div>
</div>
))}
{ungroupedSelectedColumns.length > 0 && (
<div className="rounded-lg border border-amber-100 dark:border-amber-900/40 bg-amber-50/50 dark:bg-amber-900/10 px-3 py-2">
<div className="mb-1.5 text-xs font-semibold text-amber-700 dark:text-amber-300">
{translate('::ListForms.Wizard.Step4.UngroupedColumns') || 'Ungrouped columns'}
</div>
<div className="flex flex-wrap gap-1.5">
{ungroupedSelectedColumns.map((col) => {
const meta = selectCommandColumns.find((c) => c.columnName === col)
return (
<span
key={col}
className="inline-flex items-center gap-1 rounded-full border border-amber-200 dark:border-amber-800 bg-white dark:bg-gray-900 px-2 py-0.5 text-xs text-amber-700 dark:text-amber-300"
>
{col}
{meta?.dataType && (
<span className="text-[10px] text-amber-500 opacity-70">
{meta.dataType}
</span>
)}
</span>
)
})}
</div>
</div>
)}
</div>
</Section>
{subForms.length > 0 && (
<Section
title={translate('::ListForms.ListFormEdit.SubForms') || 'Sub Forms'}
badge={subForms.length}
>
<div className="flex flex-col gap-3">
{subForms.map((subForm, index) => (
<div
key={`${subForm.code}-${index}`}
className="rounded-lg border border-gray-100 dark:border-gray-800 px-3 py-2"
>
<Row label="Tab Title" value={subForm.tabTitle} />
<Row label="List Form Code" value={subForm.code} />
<Row label="Tab Type" value={subForm.tabType} />
<Row label="Refresh" value={subForm.isRefresh ? 'Active' : 'Inactive'} />
{(subForm.relation || []).length > 0 && (
<div className="pt-2">
<span className="text-xs text-gray-400">Relations</span>
<div className="mt-1 flex flex-wrap gap-1.5">
{subForm.relation.map((relation, relationIndex) => (
<span
key={`${relation.parentFieldName}-${relation.childFieldName}-${relationIndex}`}
className="rounded bg-gray-100 dark:bg-gray-800 px-2 py-0.5 text-xs text-gray-600 dark:text-gray-300"
>
{relation.parentFieldName} : {relation.childFieldName}
</span>
))}
</div>
</div>
)}
</div>
))}
</div>
</Section>
)}
{widgets.length > 0 && (
<Section
title={translate('::ListForms.ListFormEdit.TabWidgets') || 'Widgets'}
badge={widgets.length}
>
<div className="flex flex-col gap-3">
{widgets.map((widget, index) => (
<div
key={`${widget.title}-${widget.value}-${index}`}
className="rounded-lg border border-gray-100 dark:border-gray-800 px-3 py-2"
>
<Row label="Title Field" value={widget.title} />
<Row label="Value Field" value={widget.value} />
<Row label="Color Field" value={widget.color} />
<Row label="Icon Field" value={widget.icon} />
<Row label="Column Gap" value={widget.colGap} />
<Row label="Column Span" value={widget.colSpan} />
<Row label="Class Name" value={widget.className} />
<Row label="Value Class Name" value={widget.valueClassName} />
<Row label="Status" value={widget.isActive ? 'Active' : 'Inactive'} />
<Row label="Sql Query" value={widget.sqlQuery} />
</div>
))}
</div>
</Section>
)}
{(hasWorkflowFields || workflowItems.length > 0) && (
<Section
title={translate('::ListForms.ListFormEdit.TabWorkflow') || 'Workflow'}
badge={workflowItems.length}
>
<div className="grid grid-cols-2 gap-3">
<div>
<Row label="Approval User" value={workflow.approvalUserFieldName} />
<Row label="Approval Date" value={workflow.approvalDateFieldName} />
<Row label="Approval Status" value={workflow.approvalStatusFieldName} />
<Row label="Approval Description" value={workflow.approvalDescriptionFieldName} />
<Row
label="Is Filter User Name?"
value={workflow.approvalIsFilterUserName === true ? 'Yes' : 'No'}
/>
</div>
{workflowItems.length > 0 && (
<div className="flex flex-col gap-2">
{workflowItems.map((criteria, index) => (
<div
key={criteria.id || index}
className="rounded bg-gray-50 dark:bg-gray-800 px-3 py-2"
>
<div className="text-xs font-semibold text-gray-700 dark:text-gray-200">
{criteria.title || criteria.kind || `Criteria ${index + 1}`}
</div>
{criteria.kind === 'Compare' && (
<div className="mt-1 text-[11px] text-gray-500 dark:text-gray-400">
{criteria.compareColumn} {criteria.compareOperator}{' '}
{criteria.compareValue}
</div>
)}
{(criteria.kind === 'Approval' || criteria.kind === 'Inform') &&
criteria.approver && (
<div className="text-[11px] text-gray-500 dark:text-gray-400">
Approver: {formatWorkflowApprovers(criteria.approver)}
</div>
)}
</div>
))}
</div>
)}
</div>
</Section>
)}
{values.todo && (
<Section title={translate('::ListForms.ListFormEdit.TabTodo') || 'Todo / Kanban'}>
<div className="grid grid-cols-2 gap-x-4">
<Row
label={translate('::ListForms.ListFormFieldEdit.TodoTitleExpr')}
value={values.todoOptionDto.titleExpr}
/>
<Row
label={translate('::ListForms.ListFormFieldEdit.TodoStatusExpr')}
value={values.todoOptionDto.statusExpr}
/>
<Row
label={translate('::ListForms.ListFormFieldEdit.TodoDescriptionExpr')}
value={values.todoOptionDto.descriptionExpr}
/>
<Row
label={translate('::ListForms.ListFormFieldEdit.TodoDueDateExpr')}
value={values.todoOptionDto.dueDateExpr}
/>
<Row
label={translate('::ListForms.ListFormFieldEdit.TodoTagExpr')}
value={values.todoOptionDto.tagExpr}
/>
<Row
label={translate('::ListForms.ListFormFieldEdit.TodoAssigneeExpr')}
value={values.todoOptionDto.assigneeExpr}
/>
<Row
label={translate('::ListForms.ListFormFieldEdit.TodoPriorityExpr')}
value={values.todoOptionDto.priorityExpr}
/>
<Row
label={translate('::ListForms.ListFormFieldEdit.TodoCompletedExpr')}
value={values.todoOptionDto.completedExpr}
/>
<Row
label={translate('::ListForms.ListFormFieldEdit.TodoOrderExpr')}
value={values.todoOptionDto.orderExpr}
/>
<Row
label={translate('::ListForms.ListFormFieldEdit.TodoStatusOrder')}
value={values.todoOptionDto.statusOrder}
/>
<Row
label={translate('::ListForms.ListFormFieldEdit.TodoAllowDragging')}
value={translate(
values.todoOptionDto.allowDragging
? '::App.Status.Active'
: '::App.Status.Inactive',
)}
/>
</div>
</Section>
)}
</div>
{/* ── Right: Deploy ──────────────────────────────────────────── */}
<div className="sticky top-4 flex flex-col gap-3 max-h-[calc(100vh-200px)]">
{/* Stats */}
<div className="grid grid-cols-8 gap-2">
{[
{ label: translate('::ListForms.Wizard.Step4.StatGroup'), value: groups.length },
{ label: translate('::ListForms.Wizard.Step4.StatField'), value: totalFields },
{
label: translate('::ListForms.Wizard.Step4.EditingForm') || 'Popup Form',
value: editingFormFields.length,
},
{
label: translate('::App.Listform.ListformField.Column'),
value: selectedColumns.size,
},
{
label: translate('::ListForms.ListFormEdit.SubForms') || 'Sub Forms',
value: subForms.length,
},
{
label: translate('::ListForms.ListFormEdit.TabWidgets') || 'Widgets',
value: widgets.length,
},
{
label: translate('::ListForms.ListFormEdit.TabWorkflow') || 'Workflow',
value: workflowItems.length,
},
{
label: translate('::ListForms.ListFormEdit.TabTodo') || 'Todo',
value: values.todo ? 1 : 0,
},
].map((s) => (
<div
key={s.label}
className="rounded-xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-3 text-center shadow-sm"
>
<div className="text-2xl font-bold text-indigo-600 dark:text-indigo-400">
{s.value}
</div>
<div className="text-xs text-gray-400 mt-0.5">{s.label}</div>
</div>
))}
</div>
{/* Log panel — grows to fill remaining height */}
<div className="rounded-xl border border-gray-200 dark:border-gray-700 overflow-hidden flex flex-col flex-1 min-h-0">
<div className="px-4 py-2.5 bg-gray-50 dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between shrink-0">
<span className="text-sm font-semibold text-gray-700 dark:text-gray-200 flex items-center gap-2">
<FaRocket className="text-indigo-400 text-xs" />
{translate('::ListForms.Wizard.Step4.DeployLog') || 'Deploy Log'}
</span>
{isDone && (
<span className="text-xs text-emerald-500 font-semibold flex items-center gap-1">
<FaCheckCircle /> {translate('::ListForms.Wizard.Step4.Success')}
</span>
)}
{hasError && (
<span className="text-xs text-red-500 font-semibold flex items-center gap-1">
<FaExclamationCircle /> {translate('::ListForms.Wizard.Step4.Error')}
</span>
)}
</div>
<div className="flex-1 overflow-y-auto p-4 bg-[#0d1117] dark:bg-black font-mono min-h-[360px]">
{logs.length === 0 ? (
<div className="flex flex-col items-center justify-center h-full gap-3 py-10 select-none">
<FaRocket className="text-gray-700 text-3xl" />
<span className="text-xs text-gray-600 italic text-center">
{translate('::ListForms.Wizard.Step4.AllInfoReady')}
<br />
{translate('::ListForms.Wizard.Step4.DeployStartHint')}
</span>
</div>
) : (
<div className="flex flex-col gap-2">
{logs.map((log) => (
<div key={log.id} className="flex items-start gap-2.5">
<span className="mt-0.5 shrink-0">
<LogIcon status={log.status} />
</span>
<div>
<span
className={`text-xs leading-relaxed ${
log.status === 'success'
? 'text-emerald-400'
: log.status === 'error'
? 'text-red-400'
: log.status === 'running'
? 'text-yellow-300'
: 'text-gray-600'
}`}
>
{log.label}
</span>
{log.detail && (
<div className="text-[10px] text-gray-600 mt-0.5">{log.detail}</div>
)}
</div>
</div>
))}
{isDone && (
<div className="mt-4 rounded-lg border border-emerald-800 bg-emerald-950/40 px-4 py-2.5 text-xs text-emerald-400 text-center font-semibold">
🎉 {translate('::ListForms.Wizard.Step4.DeploySuccess')}
</div>
)}
</div>
)}
</div>
</div>
</div>
{/* ── Fixed Footer ─────────────────────────────────────────────── */}
<div className="fixed bottom-0 left-0 right-0 z-10 bg-white dark:bg-gray-900 border-t border-gray-200 dark:border-gray-700 px-6 py-0 h-16 flex items-center">
<div className="flex items-center gap-3 w-full">
<Button
size="sm"
variant="default"
type="button"
icon={<FaArrowLeft />}
disabled={isDeploying}
onClick={onBack}
>
{translate('::Back') || 'Back'}
</Button>
<div className="flex-1 flex items-center justify-end">
<Button
size="sm"
variant="solid"
type="button"
icon={<FaRocket />}
loading={isDeploying}
disabled={isDeploying || isDone}
onClick={runDeploy}
>
{isDeploying
? translate('::ListForms.Wizard.Step4.Log.Deploying')
: isDone
? `${translate('::ListForms.Wizard.Step4.Log.Completed')}`
: translate('::ListForms.Wizard.Step4.DeployAndSave')}
</Button>
</div>
</div>
</div>
</div>
)
}
export default WizardStep11

View file

@ -6,7 +6,12 @@ import { Field, FieldProps, FormikErrors, FormikTouched, useFormikContext } from
import { useState } from 'react' import { useState } from 'react'
import CreatableSelect from 'react-select/creatable' import CreatableSelect from 'react-select/creatable'
import { FaArrowLeft, FaArrowRight, FaPlus, FaTable } from 'react-icons/fa' import { FaArrowLeft, FaArrowRight, FaPlus, FaTable } from 'react-icons/fa'
import { dbSourceTypeOptions, listFormDefaultLayoutOptions, selectCommandTypeOptions, sqlDataTypeToDbType } from '../edit/options' import {
dbSourceTypeOptions,
listFormDefaultLayoutOptions,
selectCommandTypeOptions,
sqlDataTypeToDbType,
} from '../edit/options'
import { ListFormWizardDto } from '@/proxy/admin/wizard/models' import { ListFormWizardDto } from '@/proxy/admin/wizard/models'
import SqlTableDesignerDialog from '@/views/developerKit/SqlTableDesignerDialog' import SqlTableDesignerDialog from '@/views/developerKit/SqlTableDesignerDialog'
@ -265,44 +270,44 @@ const WizardStep2 = ({
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<Select <Select
field={field} field={field}
form={form} form={form}
isClearable isClearable
isLoading={isLoadingDbObjects} isLoading={isLoadingDbObjects}
options={grouped} options={grouped}
placeholder={ placeholder={
isLoadingDbObjects isLoadingDbObjects
? translate('::App.Loading') ? translate('::App.Loading')
: translate('::ListForms.Wizard.Step2.SelectCommand') || : translate('::ListForms.Wizard.Step2.SelectCommand') ||
'Tablo/View/SP seç veya SQL yaz…' 'Tablo/View/SP seç veya SQL yaz…'
} }
value={field.value ? { label: field.value, value: field.value } : null} value={field.value ? { label: field.value, value: field.value } : null}
onChange={(option: any) => { onChange={(option: any) => {
if (!option) { if (!option) {
form.setFieldValue(field.name, '') form.setFieldValue(field.name, '')
onClearColumns() onClearColumns()
return return
} }
form.setFieldValue(field.name, option.value) form.setFieldValue(field.name, option.value)
const type = option.__isNew__ const type = option.__isNew__
? SelectCommandTypeEnum.Query ? SelectCommandTypeEnum.Query
: (option.__type ?? SelectCommandTypeEnum.Query) : (option.__type ?? SelectCommandTypeEnum.Query)
form.setFieldValue('selectCommandType', type) form.setFieldValue('selectCommandType', type)
form.setFieldValue('keyFieldName', '') form.setFieldValue('keyFieldName', '')
form.setFieldTouched('keyFieldName', false) form.setFieldTouched('keyFieldName', false)
if (!option.__isNew__ && option.__schema != null && option.__rawName) { if (!option.__isNew__ && option.__schema != null && option.__rawName) {
onLoadColumns(values.dataSourceCode, option.__schema, option.__rawName) onLoadColumns(values.dataSourceCode, option.__schema, option.__rawName)
} else { } else {
onClearColumns() onClearColumns()
} }
}} }}
onCreateOption={(inputValue: string) => { onCreateOption={(inputValue: string) => {
form.setFieldValue(field.name, inputValue) form.setFieldValue(field.name, inputValue)
form.setFieldValue('selectCommandType', SelectCommandTypeEnum.Query) form.setFieldValue('selectCommandType', SelectCommandTypeEnum.Query)
form.setFieldValue('keyFieldName', '') form.setFieldValue('keyFieldName', '')
form.setFieldTouched('keyFieldName', false) form.setFieldTouched('keyFieldName', false)
onClearColumns() onClearColumns()
}} }}
/> />
</div> </div>
<Button <Button
@ -473,7 +478,7 @@ const WizardStep2 = ({
</div> </div>
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-6"> <div className="grid grid-cols-1 gap-x-6">
<div className="flex flex-wrap gap-6"> <div className="flex flex-wrap gap-6">
<FormItem <FormItem
label={translate('::ListForms.ListFormEdit.DetailsLayoutDto.DefaultLayout')} label={translate('::ListForms.ListFormEdit.DetailsLayoutDto.DefaultLayout')}
@ -501,7 +506,7 @@ const WizardStep2 = ({
</Field> </Field>
</FormItem> </FormItem>
</div> </div>
<div className="flex flex-wrap gap-6"> <div className="grid grid-cols-8 gap-x-4">
<FormItem <FormItem
label={translate('::ListForms.ListFormEdit.DetailsLayoutDto.GridLayout')} label={translate('::ListForms.ListFormEdit.DetailsLayoutDto.GridLayout')}
invalid={errors?.grid && touched?.grid} invalid={errors?.grid && touched?.grid}
@ -616,116 +621,6 @@ const WizardStep2 = ({
</div> </div>
</div> </div>
{/* ── Tree options (full-width, shown when defaultLayout = 'tree') ─── */}
{values.defaultLayout === 'tree' && (
<div className="flex flex-wrap gap-6 mt-2 p-4 rounded-lg border border-indigo-200 dark:border-indigo-800 bg-indigo-50 dark:bg-indigo-900/20">
<h6 className="w-full text-sm font-semibold text-indigo-700 dark:text-indigo-300 mb-1">
{translate('::ListForms.ListFormEdit.DetailsLayoutDto.TreeLayout') || 'Tree Ayarları'}
</h6>
<FormItem
label={translate('::App.Listform.ListformField.KeyFieldName')}
className="min-w-[180px]"
>
<Field name="treeKeyExpr">
{({ field, form }: FieldProps) => (
<Select
field={field}
form={form}
isClearable
options={selectCommandColumns.map((c) => ({ label: c.columnName, value: c.columnName }))}
value={values.treeKeyExpr ? { label: values.treeKeyExpr, value: values.treeKeyExpr } : null}
onChange={(opt: any) => form.setFieldValue(field.name, opt?.value ?? '')}
/>
)}
</Field>
</FormItem>
<FormItem
label={translate('::ListForms.ListFormEdit.ParentIdExpr')}
className="min-w-[180px]"
>
<Field name="treeParentIdExpr">
{({ field, form }: FieldProps) => (
<Select
field={field}
form={form}
isClearable
options={selectCommandColumns.map((c) => ({ label: c.columnName, value: c.columnName }))}
value={values.treeParentIdExpr ? { label: values.treeParentIdExpr, value: values.treeParentIdExpr } : null}
onChange={(opt: any) => form.setFieldValue(field.name, opt?.value ?? '')}
/>
)}
</Field>
</FormItem>
<FormItem label={translate('::ListForms.ListFormEdit.AutoExpandAll')}>
<Field name="treeAutoExpandAll" component={Checkbox} />
</FormItem>
</div>
)}
{/* ── Gantt options (full-width, shown when defaultLayout = 'gantt') ─── */}
{values.defaultLayout === 'gantt' && (
<div className="flex flex-wrap gap-6 mt-2 p-4 rounded-lg border border-amber-200 dark:border-amber-800 bg-amber-50 dark:bg-amber-900/20">
<h6 className="w-full text-sm font-semibold text-amber-700 dark:text-amber-300 mb-1">
{translate('::ListForms.ListFormEdit.DetailsLayoutDto.GanttLayout') || 'Gantt Ayarları'}
</h6>
{[
{ name: 'ganttKeyExpr', label: translate('::App.Listform.ListformField.KeyFieldName') },
{ name: 'ganttParentIdExpr', label: translate('::ListForms.ListFormEdit.ParentIdExpr') },
{ name: 'ganttTitleExpr', label: translate('::ListForms.ListFormEdit.TitleExpr') },
{ name: 'ganttStartExpr', label: translate('::ListForms.ListFormEdit.StartExpr') },
{ name: 'ganttEndExpr', label: translate('::ListForms.ListFormEdit.EndExpr') },
{ name: 'ganttProgressExpr', label: translate('::ListForms.ListFormEdit.ProgressExpr') },
].map(({ name, label }) => (
<FormItem key={name} label={label} className="min-w-[180px]">
<Field name={name}>
{({ field, form }: FieldProps) => (
<Select
field={field}
form={form}
isClearable
options={selectCommandColumns.map((c) => ({ label: c.columnName, value: c.columnName }))}
value={(values as any)[name] ? { label: (values as any)[name], value: (values as any)[name] } : null}
onChange={(opt: any) => form.setFieldValue(field.name, opt?.value ?? '')}
/>
)}
</Field>
</FormItem>
))}
<FormItem label={translate('::ListForms.ListFormEdit.AutoExpandAll')}>
<Field name="ganttAutoExpandAll" component={Checkbox} />
</FormItem>
</div>
)}
{/* ── Scheduler options (full-width, shown when defaultLayout = 'scheduler') ─── */}
{values.defaultLayout === 'scheduler' && (
<div className="flex flex-wrap gap-6 mt-2 p-4 rounded-lg border border-teal-200 dark:border-teal-800 bg-teal-50 dark:bg-teal-900/20">
<h6 className="w-full text-sm font-semibold text-teal-700 dark:text-teal-300 mb-1">
{translate('::ListForms.ListFormEdit.DetailsLayoutDto.SchedulerLayout') || 'Scheduler Ayarları'}
</h6>
{[
{ name: 'schedulerTextExpr', label: translate('::ListForms.SchedulerOptions.TextExpr') },
{ name: 'schedulerStartDateExpr', label: translate('::ListForms.SchedulerOptions.StartDateExpr') },
{ name: 'schedulerEndDateExpr', label: translate('::ListForms.SchedulerOptions.EndDateExpr') },
].map(({ name, label }) => (
<FormItem key={name} label={label} className="min-w-[180px]">
<Field name={name}>
{({ field, form }: FieldProps) => (
<Select
field={field}
form={form}
isClearable
options={selectCommandColumns.map((c) => ({ label: c.columnName, value: c.columnName }))}
value={(values as any)[name] ? { label: (values as any)[name], value: (values as any)[name] } : null}
onChange={(opt: any) => form.setFieldValue(field.name, opt?.value ?? '')}
/>
)}
</Field>
</FormItem>
))}
</div>
)}
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-6"> <div className="grid grid-cols-1 md:grid-cols-2 gap-x-6">
<FormItem <FormItem
label={translate('::ListForms.Wizard.Step2.TitleTextEnglish')} label={translate('::ListForms.Wizard.Step2.TitleTextEnglish')}

View file

@ -1,5 +1,6 @@
import { Button, Checkbox, FormItem, Input, Select } from '@/components/ui' import { Button, Checkbox, FormItem, Input, Select } from '@/components/ui'
import type { ListFormWizardDto } from '@/proxy/admin/wizard/models' import type { ListFormWizardDto } from '@/proxy/admin/wizard/models'
import type { TodoOptionDto } from '@/proxy/form/models'
import type { DatabaseColumnDto } from '@/proxy/sql-query-manager/models' 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'
@ -15,51 +16,42 @@ interface WizardStep7Props {
} }
const mappings: Array<{ const mappings: Array<{
name: name: keyof TodoOptionDto
| 'todoTitleExpr'
| 'todoStatusExpr'
| 'todoDescriptionExpr'
| 'todoDueDateExpr'
| 'todoTagExpr'
| 'todoAssigneeExpr'
| 'todoPriorityExpr'
| 'todoCompletedExpr'
| 'todoOrderExpr'
translationKey: string translationKey: string
required?: boolean required?: boolean
}> = [ }> = [
{ {
name: 'todoTitleExpr', name: 'titleExpr',
translationKey: '::ListForms.ListFormFieldEdit.TodoTitleExpr', translationKey: '::ListForms.ListFormFieldEdit.TodoTitleExpr',
required: true, required: true,
}, },
{ {
name: 'todoStatusExpr', name: 'statusExpr',
translationKey: '::ListForms.ListFormFieldEdit.TodoStatusExpr', translationKey: '::ListForms.ListFormFieldEdit.TodoStatusExpr',
required: true, required: true,
}, },
{ {
name: 'todoDescriptionExpr', name: 'descriptionExpr',
translationKey: '::ListForms.ListFormFieldEdit.TodoDescriptionExpr', translationKey: '::ListForms.ListFormFieldEdit.TodoDescriptionExpr',
}, },
{ {
name: 'todoDueDateExpr', name: 'dueDateExpr',
translationKey: '::ListForms.ListFormFieldEdit.TodoDueDateExpr', translationKey: '::ListForms.ListFormFieldEdit.TodoDueDateExpr',
}, },
{ name: 'todoTagExpr', translationKey: '::ListForms.ListFormFieldEdit.TodoTagExpr' }, { name: 'tagExpr', translationKey: '::ListForms.ListFormFieldEdit.TodoTagExpr' },
{ {
name: 'todoAssigneeExpr', name: 'assigneeExpr',
translationKey: '::ListForms.ListFormFieldEdit.TodoAssigneeExpr', translationKey: '::ListForms.ListFormFieldEdit.TodoAssigneeExpr',
}, },
{ {
name: 'todoPriorityExpr', name: 'priorityExpr',
translationKey: '::ListForms.ListFormFieldEdit.TodoPriorityExpr', translationKey: '::ListForms.ListFormFieldEdit.TodoPriorityExpr',
}, },
{ {
name: 'todoCompletedExpr', name: 'completedExpr',
translationKey: '::ListForms.ListFormFieldEdit.TodoCompletedExpr', translationKey: '::ListForms.ListFormFieldEdit.TodoCompletedExpr',
}, },
{ name: 'todoOrderExpr', translationKey: '::ListForms.ListFormFieldEdit.TodoOrderExpr' }, { name: 'orderExpr', translationKey: '::ListForms.ListFormFieldEdit.TodoOrderExpr' },
] ]
const WizardStep7 = ({ const WizardStep7 = ({
@ -69,8 +61,7 @@ const WizardStep7 = ({
onBack, onBack,
onNext, onNext,
}: WizardStep7Props) => { }: WizardStep7Props) => {
const { errors, touched, values, setFieldTouched, setFieldValue, validateForm } = const { values, setFieldValue } = useFormikContext<ListFormWizardDto>()
useFormikContext<ListFormWizardDto>()
const [validationError, setValidationError] = useState('') const [validationError, setValidationError] = useState('')
const fieldOptions = useMemo<SelectBoxOption[]>( const fieldOptions = useMemo<SelectBoxOption[]>(
() => () =>
@ -80,14 +71,11 @@ const WizardStep7 = ({
[selectCommandColumns, selectedColumns], [selectCommandColumns, selectedColumns],
) )
const continueToDeploy = async () => { const continueToDeploy = () => {
await Promise.all([ if (
setFieldTouched('todoTitleExpr', true, false), values.todo &&
setFieldTouched('todoStatusExpr', true, false), (!values.todoOptionDto.titleExpr?.trim() || !values.todoOptionDto.statusExpr?.trim())
]) ) {
const validationErrors = await validateForm()
if (validationErrors.todoTitleExpr || validationErrors.todoStatusExpr) {
setValidationError(translate('::ListForms.ListFormFieldEdit.TodoRequiredFields')) setValidationError(translate('::ListForms.ListFormFieldEdit.TodoRequiredFields'))
return return
} }
@ -105,17 +93,19 @@ const WizardStep7 = ({
<FormItem <FormItem
key={mapping.name} key={mapping.name}
label={`${translate(mapping.translationKey)}${isRequired ? ' *' : ''}`} label={`${translate(mapping.translationKey)}${isRequired ? ' *' : ''}`}
invalid={Boolean(touched[mapping.name] && errors[mapping.name])} invalid={Boolean(validationError && mapping.required)}
errorMessage={touched[mapping.name] ? errors[mapping.name] : undefined} errorMessage={mapping.required ? validationError : undefined}
> >
<Field name={mapping.name}> <Field name={`todoOptionDto.${mapping.name}`}>
{({ field, form }: FieldProps) => ( {({ field, form }: FieldProps) => (
<Select <Select
field={field} field={field}
form={form} form={form}
isClearable={!isRequired} isClearable={!isRequired}
options={fieldOptions} options={fieldOptions}
value={fieldOptions.find((option) => option.value === values[mapping.name])} value={fieldOptions.find(
(option) => option.value === values.todoOptionDto[mapping.name],
)}
onChange={(option) => { onChange={(option) => {
form.setFieldValue(field.name, option?.value ?? '') form.setFieldValue(field.name, option?.value ?? '')
setValidationError('') setValidationError('')
@ -131,13 +121,13 @@ const WizardStep7 = ({
<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={translate('::ListForms.ListFormFieldEdit.TodoStatusOrder')}> <FormItem label={translate('::ListForms.ListFormFieldEdit.TodoStatusOrder')}>
<Field <Field
name="todoStatusOrder" name="todoOptionDto.statusOrder"
component={Input} component={Input}
placeholder={translate('::ListForms.ListFormFieldEdit.TodoStatusOrderPlaceholder')} placeholder={translate('::ListForms.ListFormFieldEdit.TodoStatusOrderPlaceholder')}
/> />
</FormItem> </FormItem>
<FormItem label={translate('::ListForms.ListFormFieldEdit.TodoAllowDragging')}> <FormItem label={translate('::ListForms.ListFormFieldEdit.TodoAllowDragging')}>
<Field name="todoAllowDragging"> <Field name="todoOptionDto.allowDragging">
{({ field }: FieldProps<boolean>) => ( {({ field }: FieldProps<boolean>) => (
<Checkbox <Checkbox
checked={Boolean(field.value)} checked={Boolean(field.value)}

View file

@ -1,773 +1,84 @@
import { Button } from '@/components/ui' import { Button, Checkbox, FormItem, Input, Select } from '@/components/ui'
import type { ListFormWizardDto } from '@/proxy/admin/wizard/models'
import type { DatabaseColumnDto } from '@/proxy/sql-query-manager/models' import type { DatabaseColumnDto } from '@/proxy/sql-query-manager/models'
import React, { useState } from 'react' import type { SelectBoxOption } from '@/types/shared'
import { import { Field, type FieldProps, useFormikContext } from 'formik'
FaArrowLeft, import { useMemo } from 'react'
FaCheckCircle, import { FaArrowLeft, FaArrowRight } from 'react-icons/fa'
FaChevronDown,
FaChevronRight,
FaCircle,
FaExclamationCircle,
FaRocket,
FaSpinner,
} from 'react-icons/fa'
import { WizardGroup } from './WizardStep3'
import { dbSourceTypeOptions, selectCommandTypeOptions } from '../edit/options'
import { ListFormWizardDto } from '@/proxy/admin/wizard/models'
import {
ListFormWorkflowCriteriaDto,
SubFormDto,
WidgetEditDto,
WorkflowDto,
} from '@/proxy/form/models'
import { formatWorkflowApprovers } from '@/utils/workflow/workflowHelpers'
// ─── Types ──────────────────────────────────────────────────────────────────── interface WizardStep8Props {
export interface WizardStep8Props {
values: ListFormWizardDto
wizardName: string
selectedColumns: Set<string>
selectCommandColumns: DatabaseColumnDto[] selectCommandColumns: DatabaseColumnDto[]
groups: WizardGroup[]
subForms: SubFormDto[]
widgets: WidgetEditDto[]
workflow: WorkflowDto
criteria: ListFormWorkflowCriteriaDto[]
translate: (key: string) => string translate: (key: string) => string
onBack: () => void onBack: () => void
onSubmit: () => Promise<void> onNext: () => void
} }
type LogStatus = 'pending' | 'running' | 'success' | 'error' const WizardStep8 = ({ selectCommandColumns, translate, onBack, onNext }: WizardStep8Props) => {
const { values } = useFormikContext<ListFormWizardDto>()
const fieldOptions = useMemo<SelectBoxOption[]>(
() =>
selectCommandColumns.map((column) => ({
value: column.columnName,
label: column.columnName,
})),
[selectCommandColumns],
)
interface LogEntry { const selectFields = [
id: number
label: string
status: LogStatus
detail?: string
}
// ─── Deploy log steps ─────────────────────────────────────────────────────────
function buildLogSteps(
values: ListFormWizardDto,
groups: WizardGroup[],
translate: (key: string, params?: Record<string, string | number>) => string,
): Omit<LogEntry, 'status'>[] {
const totalFields = groups.reduce((acc, g) => acc + g.items.length, 0)
return [
{ id: 1, label: translate('::ListForms.Wizard.Step4.Log.ValidatingConfig') },
{ {
id: 2, name: 'treeOptionDto.keyExpr',
label: translate('::ListForms.Wizard.Step4.Log.CreatingMenu', { 0: values.menuCode }), value: values.treeOptionDto.keyExpr,
detail: `Parent: ${values.menuParentCode}`, label: translate('::App.Listform.ListformField.KeyFieldName'),
}, },
{ {
id: 3, name: 'treeOptionDto.parentIdExpr',
label: translate('::ListForms.Wizard.Step4.Log.SavingLanguageTexts'), value: values.treeOptionDto.parentIdExpr,
detail: `EN: ${values.languageTextMenuEn} / TR: ${values.languageTextMenuTr}`, label: translate('::ListForms.ListFormEdit.ParentIdExpr'),
}, },
{
id: 4,
label: translate('::ListForms.Wizard.Step4.Log.ConfiguringPermission', {
0: values.permissionGroupName,
}),
},
{
id: 5,
label: translate('::ListForms.Wizard.Step4.Log.ConnectingDataSource', {
0: values.dataSourceCode,
}),
},
{
id: 6,
label: translate('::ListForms.Wizard.Step4.Log.CreatingListForm', { 0: values.listFormCode }),
detail: `Key: ${values.keyFieldName}`,
},
{
id: 7,
label: translate('::ListForms.Wizard.Step4.Log.SavingFormGroups', {
0: groups.length,
1: totalFields,
}),
},
{ id: 8, label: translate('::ListForms.Wizard.Step4.Log.Deploying') },
{ id: 9, label: translate('::ListForms.Wizard.Step4.Log.Completed') },
] ]
}
// ─── Mini-components ──────────────────────────────────────────────────────────
function LogIcon({ status }: { status: LogStatus }) {
if (status === 'running') return <FaSpinner className="text-indigo-500 animate-spin shrink-0" />
if (status === 'success') return <FaCheckCircle className="text-emerald-500 shrink-0" />
if (status === 'error') return <FaExclamationCircle className="text-red-500 shrink-0" />
return <FaCircle className="text-gray-300 dark:text-gray-600 shrink-0 text-[8px] mt-1" />
}
interface SectionProps {
title: string
badge?: string | number
children: React.ReactNode
defaultOpen?: boolean
}
function Section({ title, badge, children, defaultOpen = true }: SectionProps) {
const [open, setOpen] = useState(defaultOpen)
return (
<div className="rounded-xl border border-gray-200 dark:border-gray-700 overflow-hidden">
<Button
type="button"
variant="plain"
shape="none"
className="w-full !h-auto !items-center !justify-between !rounded-none !bg-gray-50 !px-4 !py-2.5 transition-colors hover:!bg-gray-100 dark:!bg-gray-800 dark:hover:!bg-gray-700"
onClick={() => setOpen((v) => !v)}
>
<div className="flex items-center gap-2">
{open ? (
<FaChevronDown className="text-gray-400 text-xs" />
) : (
<FaChevronRight className="text-gray-400 text-xs" />
)}
<span className="text-sm font-semibold text-gray-700 dark:text-gray-200">{title}</span>
</div>
{badge !== undefined && (
<span className="text-xs bg-indigo-100 dark:bg-indigo-900/40 text-indigo-600 dark:text-indigo-400 px-2 py-0.5 rounded-full font-medium">
{badge}
</span>
)}
</Button>
{open && <div className="px-4 py-3 bg-white dark:bg-gray-900">{children}</div>}
</div>
)
}
function Row({ label, value }: { label: string; value?: string | number }) {
if (!value && value !== 0) return null
return (
<div className="flex gap-2 py-1 border-b border-gray-100 dark:border-gray-800 last:border-0">
<span className="text-xs text-gray-400 w-40 shrink-0">{label}</span>
<span className="text-xs text-gray-700 dark:text-gray-200 font-medium break-all">
{value}
</span>
</div>
)
}
// ─── WizardStep8 ──────────────────────────────────────────────────────────────
const WizardStep8 = ({
values,
wizardName,
selectedColumns,
selectCommandColumns,
groups,
subForms,
widgets,
workflow,
criteria,
translate,
onBack,
onSubmit,
}: WizardStep8Props) => {
const [logs, setLogs] = useState<LogEntry[]>([])
const [isDeploying, setIsDeploying] = useState(false)
const [isDone, setIsDone] = useState(false)
const [hasError, setHasError] = useState(false)
const steps = buildLogSteps(values, groups, translate)
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms))
const runDeploy = async () => {
if (isDeploying) return
setIsDeploying(true)
setHasError(false)
setIsDone(false)
// Initialize all as pending
setLogs(steps.map((s) => ({ ...s, status: 'pending' })))
const setStatus = (id: number, status: LogStatus) =>
setLogs((prev) => prev.map((l) => (l.id === id ? { ...l, status } : l)))
// Steps 1-7: pre-deploy simulation (fast)
for (let i = 0; i < steps.length - 2; i++) {
const step = steps[i]
setStatus(step.id, 'running')
await sleep(300 + Math.random() * 200)
setStatus(step.id, 'success')
}
// Step 8: actual API call
const deployStep = steps[steps.length - 2]
setStatus(deployStep.id, 'running')
try {
await onSubmit()
setStatus(deployStep.id, 'success')
await sleep(200)
// Step 9: done
const doneStep = steps[steps.length - 1]
setStatus(doneStep.id, 'running')
await sleep(300)
setStatus(doneStep.id, 'success')
setIsDone(true)
} catch (err: any) {
setStatus(deployStep.id, 'error')
setLogs((prev) => [
...prev,
{
id: 999,
label: `${translate('::ListForms.Wizard.Step4.Error')}: ${
err?.message ?? translate('::App.Platform.Unknown')
}`,
status: 'error',
},
])
setHasError(true)
} finally {
setIsDeploying(false)
}
}
const totalFields = groups.reduce((acc, g) => acc + g.items.length, 0)
const editingFormFields = groups.flatMap((g) =>
g.items
.filter((item) => item.includeInEditingForm && item.fieldName !== values.keyFieldName)
.map((item) => ({ ...item, groupCaption: g.caption })),
)
const groupedFieldNames = new Set(groups.flatMap((g) => g.items.map((item) => item.fieldName)))
const ungroupedSelectedColumns = [...selectedColumns].filter((col) => !groupedFieldNames.has(col))
const hasWorkflowFields = Boolean(
workflow.approvalUserFieldName ||
workflow.approvalDateFieldName ||
workflow.approvalStatusFieldName ||
workflow.approvalDescriptionFieldName,
)
const workflowItems = criteria.length > 0 ? criteria : (workflow.criteria ?? [])
return ( return (
<div className="grid grid-cols-[3fr_2fr] gap-5 pb-24 items-start"> <div className="pb-20">
{/* ── Left: Summary ──────────────────────────────────────────── */} <div className="grid grid-cols-1 gap-x-6 md:grid-cols-2">
<div className="flex flex-col gap-3"> {selectFields.map((item) => (
<div className="grid grid-cols-2 gap-3 items-start"> <FormItem key={item.name} label={item.label}>
<Section title={translate('::ListForms.Wizard.Step4.MenuInfo')}> <Field name={item.name}>
<Row label={translate('::ListForms.Wizard.Step4.WizardName')} value={wizardName} /> {({ field, form }: FieldProps) => (
<Row label={translate('::ListForms.Wizard.Step4.MenuCode')} value={values.menuCode} /> <Select
<Row field={field}
label={translate('::ListForms.Wizard.Step4.MenuParent')} form={form}
value={values.menuParentCode} isClearable
/> options={fieldOptions}
<Row value={fieldOptions.find((option) => option.value === item.value)}
label={translate('::ListForms.Wizard.Step4.PermissionGroup')} onChange={(option) => form.setFieldValue(field.name, option?.value ?? '')}
value={values.permissionGroupName}
/>
<Row label={translate('::App.Listform.ListformField.Icon')} value={values.menuIcon} />
<Row
label={translate('::ListForms.Wizard.Step4.MenuTr')}
value={values.languageTextMenuTr}
/>
<Row
label={translate('::ListForms.Wizard.Step4.MenuEn')}
value={values.languageTextMenuEn}
/>
<Row
label={translate('::ListForms.Wizard.Step4.MenuParentTr')}
value={values.languageTextMenuParentTr}
/>
<Row
label={translate('::ListForms.Wizard.Step4.MenuParentEn')}
value={values.languageTextMenuParentEn}
/>
</Section>
<Section title={translate('::ListForms.Wizard.Step4.ListFormSettings')}>
<Row
label={translate('::ListForms.Wizard.Step4.ListFormCode')}
value={values.listFormCode}
/>
<Row
label={translate('::ListForms.Wizard.Step4.TitleTr')}
value={values.languageTextTitleTr}
/>
<Row
label={translate('::ListForms.Wizard.Step4.TitleEn')}
value={values.languageTextTitleEn}
/>
<Row
label={translate('::ListForms.Wizard.Step4.DescTr')}
value={values.languageTextDescTr}
/>
<Row
label={translate('::ListForms.Wizard.Step4.DescEn')}
value={values.languageTextDescEn}
/>
<Row
label={translate('::ListForms.Wizard.Step4.DataSource')}
value={values.dataSourceCode}
/>
<Row
label={translate('::App.Listform.ListformField.ConnectionString')}
value={values.dataSourceConnectionString}
/>
<Row
label={translate('::ListForms.Wizard.Step4.SelectCommand')}
value={`${values.selectCommand} (${
selectCommandTypeOptions.find((o: any) => o.value === values.selectCommandType)
?.label ?? String(values.selectCommandType)
})`}
/>
<Row
label={translate('::ListForms.Wizard.Step4.KeyField')}
value={`${values.keyFieldName} (${
dbSourceTypeOptions.find((o: any) => o.value === values.keyFieldDbSourceType)
?.label ?? String(values.keyFieldDbSourceType)
})`}
/>
</Section>
</div>
<Section
title={
translate('::ListForms.Wizard.Step4.ColumnsAndFormLayout') || 'Columns & Form Layout'
}
badge={`${selectedColumns.size} ${translate('::App.Listform.ListformField.Column')} / ${editingFormFields.length} ${translate('::ListForms.Wizard.Step4.EditingForm') || 'Popup Form'}`}
>
<div className="mb-3 grid grid-cols-3 gap-2">
{[
{
label: translate('::ListForms.Wizard.Step4.SelectedColumns'),
value: selectedColumns.size,
className: 'text-indigo-600 dark:text-indigo-400',
},
{
label: translate('::ListForms.Wizard.Step4.EditingForm') || 'Popup Form',
value: editingFormFields.length,
className: 'text-emerald-600 dark:text-emerald-400',
},
{
label: translate('::ListForms.Wizard.Step4.FormGroups'),
value: groups.length,
className: 'text-gray-700 dark:text-gray-200',
},
].map((item) => (
<div
key={item.label}
className="rounded-lg border border-gray-100 dark:border-gray-800 bg-gray-50 dark:bg-gray-800 px-3 py-2"
>
<div className={`text-lg font-semibold leading-none ${item.className}`}>
{item.value}
</div>
<div className="mt-1 text-[11px] text-gray-400 truncate">{item.label}</div>
</div>
))}
</div>
<div className="flex flex-col gap-2">
{groups.map((g) => (
<div
key={g.id}
className="overflow-hidden rounded-lg border border-gray-100 dark:border-gray-800"
>
<div className="flex items-center justify-between gap-3 bg-gray-50 dark:bg-gray-800 px-3 py-2">
<span className="text-xs font-semibold text-gray-700 dark:text-gray-200 truncate">
{g.caption || `(${translate('::ListForms.Wizard.Step4.StatGroup')})`}
</span>
<div className="flex items-center gap-1.5 text-[10px] text-gray-400 shrink-0">
<span className="rounded bg-white dark:bg-gray-900 px-1.5 py-0.5">
{g.items.length} {translate('::ListForms.Wizard.Step4.StatField')}
</span>
<span className="rounded bg-white dark:bg-gray-900 px-1.5 py-0.5">
{g.colCount} {translate('::App.Listform.ListformField.Column')}
</span>
</div>
</div>
<div className="divide-y divide-gray-100 dark:divide-gray-800">
{g.items.length === 0 ? (
<span className="block px-3 py-2 text-xs text-gray-300 italic">
{translate('::ListForms.Wizard.Step4.NoFields') || 'Alan yok'}
</span>
) : (
g.items.map((item) => {
const meta = selectCommandColumns.find((c) => c.columnName === item.fieldName)
const isKeyField = item.fieldName === values.keyFieldName
const isPopupField = item.includeInEditingForm && !isKeyField
return (
<div
key={item.id}
className="grid grid-cols-[minmax(150px,1fr)_auto] items-center gap-3 px-3 py-1.5"
>
<div className="flex min-w-0 items-center gap-2">
<span className="text-xs font-medium text-indigo-600 dark:text-indigo-400 truncate">
{item.fieldName}
</span>
{meta?.dataType && (
<span className="text-[10px] text-gray-400 truncate">
{meta.dataType}
</span>
)}
</div>
<div className="flex flex-wrap justify-end gap-1">
<span className="rounded bg-indigo-50 dark:bg-indigo-900/30 px-1.5 py-0.5 text-[10px] text-indigo-600 dark:text-indigo-300">
{translate('::ListForms.Wizard.Step4.SelectedColumns') || 'List'}
</span>
{isPopupField && (
<span className="rounded bg-emerald-50 dark:bg-emerald-900/30 px-1.5 py-0.5 text-[10px] text-emerald-600 dark:text-emerald-300">
{translate('::ListForms.Wizard.Step4.EditingForm') || 'Popup Form'}
</span>
)}
{isKeyField && (
<span className="rounded bg-amber-50 dark:bg-amber-900/30 px-1.5 py-0.5 text-[10px] text-amber-600 dark:text-amber-300">
{translate('::ListForms.Wizard.Step4.KeyField')}
</span>
)}
<span className="rounded bg-gray-100 dark:bg-gray-800 px-1.5 py-0.5 text-[10px] text-gray-400">
{item.editorType}
</span>
<span className="rounded bg-gray-100 dark:bg-gray-800 px-1.5 py-0.5 text-[10px] text-gray-400">
span-{item.colSpan}
{item.isRequired && <span className="ml-1 text-red-400">*</span>}
</span>
</div>
</div>
)
})
)}
</div>
</div>
))}
{ungroupedSelectedColumns.length > 0 && (
<div className="rounded-lg border border-amber-100 dark:border-amber-900/40 bg-amber-50/50 dark:bg-amber-900/10 px-3 py-2">
<div className="mb-1.5 text-xs font-semibold text-amber-700 dark:text-amber-300">
{translate('::ListForms.Wizard.Step4.UngroupedColumns') || 'Ungrouped columns'}
</div>
<div className="flex flex-wrap gap-1.5">
{ungroupedSelectedColumns.map((col) => {
const meta = selectCommandColumns.find((c) => c.columnName === col)
return (
<span
key={col}
className="inline-flex items-center gap-1 rounded-full border border-amber-200 dark:border-amber-800 bg-white dark:bg-gray-900 px-2 py-0.5 text-xs text-amber-700 dark:text-amber-300"
>
{col}
{meta?.dataType && (
<span className="text-[10px] text-amber-500 opacity-70">
{meta.dataType}
</span>
)}
</span>
)
})}
</div>
</div>
)}
</div>
</Section>
{subForms.length > 0 && (
<Section
title={translate('::ListForms.ListFormEdit.SubForms') || 'Sub Forms'}
badge={subForms.length}
>
<div className="flex flex-col gap-3">
{subForms.map((subForm, index) => (
<div
key={`${subForm.code}-${index}`}
className="rounded-lg border border-gray-100 dark:border-gray-800 px-3 py-2"
>
<Row label="Tab Title" value={subForm.tabTitle} />
<Row label="List Form Code" value={subForm.code} />
<Row label="Tab Type" value={subForm.tabType} />
<Row label="Refresh" value={subForm.isRefresh ? 'Active' : 'Inactive'} />
{(subForm.relation || []).length > 0 && (
<div className="pt-2">
<span className="text-xs text-gray-400">Relations</span>
<div className="mt-1 flex flex-wrap gap-1.5">
{subForm.relation.map((relation, relationIndex) => (
<span
key={`${relation.parentFieldName}-${relation.childFieldName}-${relationIndex}`}
className="rounded bg-gray-100 dark:bg-gray-800 px-2 py-0.5 text-xs text-gray-600 dark:text-gray-300"
>
{relation.parentFieldName} : {relation.childFieldName}
</span>
))}
</div>
</div>
)}
</div>
))}
</div>
</Section>
)}
{widgets.length > 0 && (
<Section
title={translate('::ListForms.ListFormEdit.TabWidgets') || 'Widgets'}
badge={widgets.length}
>
<div className="flex flex-col gap-3">
{widgets.map((widget, index) => (
<div
key={`${widget.title}-${widget.value}-${index}`}
className="rounded-lg border border-gray-100 dark:border-gray-800 px-3 py-2"
>
<Row label="Title Field" value={widget.title} />
<Row label="Value Field" value={widget.value} />
<Row label="Color Field" value={widget.color} />
<Row label="Icon Field" value={widget.icon} />
<Row label="Column Gap" value={widget.colGap} />
<Row label="Column Span" value={widget.colSpan} />
<Row label="Class Name" value={widget.className} />
<Row label="Value Class Name" value={widget.valueClassName} />
<Row label="Status" value={widget.isActive ? 'Active' : 'Inactive'} />
<Row label="Sql Query" value={widget.sqlQuery} />
</div>
))}
</div>
</Section>
)}
{(hasWorkflowFields || workflowItems.length > 0) && (
<Section
title={translate('::ListForms.ListFormEdit.TabWorkflow') || 'Workflow'}
badge={workflowItems.length}
>
<div className="grid grid-cols-2 gap-3">
<div>
<Row label="Approval User" value={workflow.approvalUserFieldName} />
<Row label="Approval Date" value={workflow.approvalDateFieldName} />
<Row label="Approval Status" value={workflow.approvalStatusFieldName} />
<Row label="Approval Description" value={workflow.approvalDescriptionFieldName} />
<Row
label="Is Filter User Name?"
value={workflow.approvalIsFilterUserName === true ? 'Yes' : 'No'}
/> />
</div>
{workflowItems.length > 0 && (
<div className="flex flex-col gap-2">
{workflowItems.map((criteria, index) => (
<div
key={criteria.id || index}
className="rounded bg-gray-50 dark:bg-gray-800 px-3 py-2"
>
<div className="text-xs font-semibold text-gray-700 dark:text-gray-200">
{criteria.title || criteria.kind || `Criteria ${index + 1}`}
</div>
{criteria.kind === 'Compare' && (
<div className="mt-1 text-[11px] text-gray-500 dark:text-gray-400">
{criteria.compareColumn} {criteria.compareOperator}{' '}
{criteria.compareValue}
</div>
)}
{(criteria.kind === 'Approval' || criteria.kind === 'Inform') &&
criteria.approver && (
<div className="text-[11px] text-gray-500 dark:text-gray-400">
Approver: {formatWorkflowApprovers(criteria.approver)}
</div>
)}
</div>
))}
</div>
)} )}
</div> </Field>
</Section> </FormItem>
)} ))}
{values.todo && ( <FormItem label={translate('::ListForms.ListFormEdit.HasItemsExpr')}>
<Section title={translate('::ListForms.ListFormEdit.TabTodo') || 'Todo / Kanban'}> <Field name="treeOptionDto.hasItemsExpr" component={Input} />
<div className="grid grid-cols-2 gap-x-4"> </FormItem>
<Row <FormItem label={translate('::ListForms.ListFormEdit.RootValue')}>
label={translate('::ListForms.ListFormFieldEdit.TodoTitleExpr')} <Field name="treeOptionDto.rootValue" component={Input} />
value={values.todoTitleExpr} </FormItem>
/> <FormItem label={translate('::ListForms.ListFormEdit.AutoExpandAll')}>
<Row <Field name="treeOptionDto.autoExpandAll" component={Checkbox} />
label={translate('::ListForms.ListFormFieldEdit.TodoStatusExpr')} </FormItem>
value={values.todoStatusExpr} <FormItem label={translate('::ListForms.ListFormEdit.RecursiveSelection')}>
/> <Field name="treeOptionDto.recursiveSelection" component={Checkbox} />
<Row </FormItem>
label={translate('::ListForms.ListFormFieldEdit.TodoDescriptionExpr')}
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>
</Section>
)}
</div> </div>
{/* ── Right: Deploy ──────────────────────────────────────────── */} <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="sticky top-4 flex flex-col gap-3 max-h-[calc(100vh-200px)]"> <div className="flex w-full items-center justify-between gap-3">
{/* Stats */} <Button type="button" size="sm" icon={<FaArrowLeft />} onClick={onBack}>
<div className="grid grid-cols-8 gap-2"> {translate('::Back')}
{[ </Button>
{ label: translate('::ListForms.Wizard.Step4.StatGroup'), value: groups.length }, <Button type="button" size="sm" variant="solid" icon={<FaArrowRight />} onClick={onNext}>
{ label: translate('::ListForms.Wizard.Step4.StatField'), value: totalFields }, {translate('::Next')}
{
label: translate('::ListForms.Wizard.Step4.EditingForm') || 'Popup Form',
value: editingFormFields.length,
},
{
label: translate('::App.Listform.ListformField.Column'),
value: selectedColumns.size,
},
{
label: translate('::ListForms.ListFormEdit.SubForms') || 'Sub Forms',
value: subForms.length,
},
{
label: translate('::ListForms.ListFormEdit.TabWidgets') || 'Widgets',
value: widgets.length,
},
{
label: translate('::ListForms.ListFormEdit.TabWorkflow') || 'Workflow',
value: workflowItems.length,
},
{
label: translate('::ListForms.ListFormEdit.TabTodo') || 'Todo',
value: values.todo ? 1 : 0,
},
].map((s) => (
<div
key={s.label}
className="rounded-xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-3 text-center shadow-sm"
>
<div className="text-2xl font-bold text-indigo-600 dark:text-indigo-400">
{s.value}
</div>
<div className="text-xs text-gray-400 mt-0.5">{s.label}</div>
</div>
))}
</div>
{/* Log panel — grows to fill remaining height */}
<div className="rounded-xl border border-gray-200 dark:border-gray-700 overflow-hidden flex flex-col flex-1 min-h-0">
<div className="px-4 py-2.5 bg-gray-50 dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between shrink-0">
<span className="text-sm font-semibold text-gray-700 dark:text-gray-200 flex items-center gap-2">
<FaRocket className="text-indigo-400 text-xs" />
{translate('::ListForms.Wizard.Step4.DeployLog') || 'Deploy Log'}
</span>
{isDone && (
<span className="text-xs text-emerald-500 font-semibold flex items-center gap-1">
<FaCheckCircle /> {translate('::ListForms.Wizard.Step4.Success')}
</span>
)}
{hasError && (
<span className="text-xs text-red-500 font-semibold flex items-center gap-1">
<FaExclamationCircle /> {translate('::ListForms.Wizard.Step4.Error')}
</span>
)}
</div>
<div className="flex-1 overflow-y-auto p-4 bg-[#0d1117] dark:bg-black font-mono min-h-[360px]">
{logs.length === 0 ? (
<div className="flex flex-col items-center justify-center h-full gap-3 py-10 select-none">
<FaRocket className="text-gray-700 text-3xl" />
<span className="text-xs text-gray-600 italic text-center">
{translate('::ListForms.Wizard.Step4.AllInfoReady')}
<br />
{translate('::ListForms.Wizard.Step4.DeployStartHint')}
</span>
</div>
) : (
<div className="flex flex-col gap-2">
{logs.map((log) => (
<div key={log.id} className="flex items-start gap-2.5">
<span className="mt-0.5 shrink-0">
<LogIcon status={log.status} />
</span>
<div>
<span
className={`text-xs leading-relaxed ${
log.status === 'success'
? 'text-emerald-400'
: log.status === 'error'
? 'text-red-400'
: log.status === 'running'
? 'text-yellow-300'
: 'text-gray-600'
}`}
>
{log.label}
</span>
{log.detail && (
<div className="text-[10px] text-gray-600 mt-0.5">{log.detail}</div>
)}
</div>
</div>
))}
{isDone && (
<div className="mt-4 rounded-lg border border-emerald-800 bg-emerald-950/40 px-4 py-2.5 text-xs text-emerald-400 text-center font-semibold">
🎉 {translate('::ListForms.Wizard.Step4.DeploySuccess')}
</div>
)}
</div>
)}
</div>
</div>
</div>
{/* ── Fixed Footer ─────────────────────────────────────────────── */}
<div className="fixed bottom-0 left-0 right-0 z-10 bg-white dark:bg-gray-900 border-t border-gray-200 dark:border-gray-700 px-6 py-0 h-16 flex items-center">
<div className="flex items-center gap-3 w-full">
<Button
size="sm"
variant="default"
type="button"
icon={<FaArrowLeft />}
disabled={isDeploying}
onClick={onBack}
>
{translate('::Back') || 'Back'}
</Button> </Button>
<div className="flex-1 flex items-center justify-end">
<Button
size="sm"
variant="solid"
type="button"
icon={<FaRocket />}
loading={isDeploying}
disabled={isDeploying || isDone}
onClick={runDeploy}
>
{isDeploying
? translate('::ListForms.Wizard.Step4.Log.Deploying')
: isDone
? `${translate('::ListForms.Wizard.Step4.Log.Completed')}`
: translate('::ListForms.Wizard.Step4.DeployAndSave')}
</Button>
</div>
</div> </div>
</div> </div>
</div> </div>

View file

@ -0,0 +1,126 @@
import { Button, Card, Checkbox, FormItem, Input, Select } from '@/components/ui'
import type { ListFormWizardDto } from '@/proxy/admin/wizard/models'
import type { DatabaseColumnDto } from '@/proxy/sql-query-manager/models'
import type { SelectBoxOption } from '@/types/shared'
import { Field, type FieldProps, useFormikContext } from 'formik'
import { useMemo } from 'react'
import { FaArrowLeft, FaArrowRight } from 'react-icons/fa'
import { gantScaleOptions } from '../edit/options'
interface WizardStep9Props {
selectCommandColumns: DatabaseColumnDto[]
translate: (key: string) => string
onBack: () => void
onNext: () => void
}
const WizardStep9 = ({ selectCommandColumns, translate, onBack, onNext }: WizardStep9Props) => {
const { values } = useFormikContext<ListFormWizardDto>()
const fieldOptions = useMemo<SelectBoxOption[]>(
() =>
selectCommandColumns.map((column) => ({
value: column.columnName,
label: column.columnName,
})),
[selectCommandColumns],
)
const option = values.ganttOptionDto
const selectFields = [
['keyExpr', translate('::App.Listform.ListformField.KeyFieldName')],
['parentIdExpr', translate('::ListForms.ListFormEdit.ParentIdExpr')],
['titleExpr', translate('::ListForms.ListFormEdit.TitleExpr')],
['startExpr', translate('::ListForms.ListFormEdit.StartExpr')],
['endExpr', translate('::ListForms.ListFormEdit.EndExpr')],
['progressExpr', translate('::ListForms.ListFormEdit.ProgressExpr')],
] as const
const permissionFields = [
['allowEditing', '::ListForms.ListFormEdit.AllowEditing'],
['allowTaskAdding', '::ListForms.ListFormEdit.AllowTaskAdding'],
['allowTaskUpdating', '::ListForms.ListFormEdit.AllowTaskUpdating'],
['allowTaskDeleting', '::ListForms.ListFormEdit.AllowTaskDeleting'],
['allowDependencyAdding', '::ListForms.ListFormEdit.AllowDependencyAdding'],
['allowDependencyDeleting', '::ListForms.ListFormEdit.AllowDependencyDeleting'],
['allowResourceAdding', '::ListForms.ListFormEdit.AllowResourceAdding'],
['allowResourceDeleting', '::ListForms.ListFormEdit.AllowResourceDeleting'],
] as const
return (
<div className="pb-20">
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
<Card header={translate('::ListForms.SchedulerOptions.BasicSettings')}>
<div className="grid grid-cols-1 gap-x-5 md:grid-cols-2">
{selectFields.map(([name, label]) => (
<FormItem key={name} label={label}>
<Field name={`ganttOptionDto.${name}`}>
{({ field, form }: FieldProps) => (
<Select
field={field}
form={form}
isClearable
options={fieldOptions}
value={fieldOptions.find((fieldOption) => fieldOption.value === option[name])}
onChange={(selected) => form.setFieldValue(field.name, selected?.value ?? '')}
/>
)}
</Field>
</FormItem>
))}
<FormItem label={translate('::ListForms.ListFormEdit.HasItemsExpr')}>
<Field name="ganttOptionDto.hasItemsExpr" component={Input} />
</FormItem>
<FormItem label={translate('::ListForms.ListFormEdit.RootValue')}>
<Field name="ganttOptionDto.rootValue" component={Input} />
</FormItem>
<FormItem label={translate('::ListForms.ListFormEdit.ScaleType')}>
<Field name="ganttOptionDto.scaleType">
{({ field, form }: FieldProps) => (
<Select
field={field}
form={form}
isClearable
options={gantScaleOptions}
value={gantScaleOptions.find(
(scaleOption) => scaleOption.value === option.scaleType,
)}
onChange={(selected) =>
form.setFieldValue(field.name, selected?.value ?? 'weeks')
}
/>
)}
</Field>
</FormItem>
<FormItem label={translate('::ListForms.ListFormEdit.AutoExpandAll')}>
<Field name="ganttOptionDto.autoExpandAll" component={Checkbox} />
</FormItem>
<FormItem label={translate('::ListForms.ListFormEdit.RecursiveSelection')}>
<Field name="ganttOptionDto.recursiveSelection" component={Checkbox} />
</FormItem>
</div>
</Card>
<Card header={translate('::ListForms.ListFormEdit.AllowEditing')}>
<div className="grid grid-cols-1 gap-x-5 md:grid-cols-2">
{permissionFields.map(([name, translationKey]) => (
<FormItem key={name} label={translate(translationKey)}>
<Field name={`ganttOptionDto.${name}`} component={Checkbox} />
</FormItem>
))}
</div>
</Card>
</div>
<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">
<Button type="button" size="sm" icon={<FaArrowLeft />} onClick={onBack}>
{translate('::Back')}
</Button>
<Button type="button" size="sm" variant="solid" icon={<FaArrowRight />} onClick={onNext}>
{translate('::Next')}
</Button>
</div>
</div>
</div>
)
}
export default WizardStep9