ListFormWizard SubForms, Wizard, Workflow

This commit is contained in:
Sedat Öztürk 2026-05-27 22:09:05 +03:00
parent 96cd6dfd80
commit 231860e85a
15 changed files with 2509 additions and 962 deletions

View file

@ -68,5 +68,8 @@ public class ListFormWizardDto
public string SchedulerEndDateExpr { get; set; } public string SchedulerEndDateExpr { 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<WidgetEditDto> Widgets { get; set; } = new();
public WorkflowDto Workflow { get; set; } = new();
public List<ListFormWorkflowCriteriaDto> WorkflowCriteria { get; set; } = new();
} }

View file

@ -27,6 +27,7 @@ public class ListFormWizardAppService(
IRepository<ListForm, Guid> repoListForm, IRepository<ListForm, Guid> repoListForm,
IRepository<ListFormField, Guid> repoListFormField, IRepository<ListFormField, Guid> repoListFormField,
IRepository<DataSource, Guid> repoDataSource, IRepository<DataSource, Guid> repoDataSource,
IRepository<ListFormWorkflow, string> repoListFormWorkflow,
IRepository<LanguageKey, Guid> repoLangKey, IRepository<LanguageKey, Guid> repoLangKey,
IRepository<LanguageText, Guid> repoLangText, IRepository<LanguageText, Guid> repoLangText,
IRepository<PermissionDefinitionRecord, Guid> repoPerm, IRepository<PermissionDefinitionRecord, Guid> repoPerm,
@ -42,6 +43,7 @@ public class ListFormWizardAppService(
private readonly IRepository<ListForm, Guid> repoListForm = repoListForm; private readonly IRepository<ListForm, Guid> repoListForm = repoListForm;
private readonly IRepository<ListFormField, Guid> repoListFormField = repoListFormField; private readonly IRepository<ListFormField, Guid> repoListFormField = repoListFormField;
private readonly IRepository<DataSource, Guid> repoDataSource = repoDataSource; private readonly IRepository<DataSource, Guid> repoDataSource = repoDataSource;
private readonly IRepository<ListFormWorkflow, string> repoListFormWorkflow = repoListFormWorkflow;
private readonly IRepository<LanguageKey, Guid> repoLangKey = repoLangKey; private readonly IRepository<LanguageKey, Guid> repoLangKey = repoLangKey;
private readonly IRepository<LanguageText, Guid> repoLangText = repoLangText; private readonly IRepository<LanguageText, Guid> repoLangText = repoLangText;
private readonly IRepository<PermissionDefinitionRecord, Guid> repoPerm = repoPerm; private readonly IRepository<PermissionDefinitionRecord, Guid> repoPerm = repoPerm;
@ -244,9 +246,17 @@ public class ListFormWizardAppService(
await repoListFormField.DeleteManyAsync(existingListFormFields, autoSave: true); await repoListFormField.DeleteManyAsync(existingListFormFields, autoSave: true);
} }
var existingWorkflowCriteria = await repoListFormWorkflow.GetListAsync(a => a.ListFormCode == input.ListFormCode);
if (existingWorkflowCriteria.Count > 0)
{
await repoListFormWorkflow.DeleteManyAsync(existingWorkflowCriteria, autoSave: true);
}
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.Workflow ??= new WorkflowDto();
input.Workflow.Criteria = input.WorkflowCriteria;
var listForm = await repoListForm.InsertAsync(new ListForm var listForm = await repoListForm.InsertAsync(new ListForm
{ {
@ -254,7 +264,7 @@ public class ListFormWizardAppService(
PageSize = 10, PageSize = 10,
ExportJson = WizardConsts.DefaultExportJson, ExportJson = WizardConsts.DefaultExportJson,
IsSubForm = false, IsSubForm = false,
ShowNote = true, ShowNote = input.SubForms.Count > 0,
LayoutJson = WizardConsts.DefaultLayoutJson(input.DefaultLayout, input.Grid, input.Pivot, input.Tree, input.Chart, input.Gantt, input.Scheduler), LayoutJson = WizardConsts.DefaultLayoutJson(input.DefaultLayout, input.Grid, input.Pivot, input.Tree, input.Chart, input.Gantt, input.Scheduler),
CultureName = LanguageCodes.En, CultureName = LanguageCodes.En,
ListFormCode = input.ListFormCode, ListFormCode = input.ListFormCode,
@ -284,6 +294,9 @@ public class ListFormWizardAppService(
PagerOptionJson = WizardConsts.DefaultPagerOptionJson, PagerOptionJson = WizardConsts.DefaultPagerOptionJson,
EditingOptionJson = WizardConsts.DefaultEditingOptionJson(titleLangKey, 600, 500, input.AllowDeleting, input.AllowAdding, input.AllowUpdating, input.ConfirmDelete, false, input.AllowDetail), EditingOptionJson = WizardConsts.DefaultEditingOptionJson(titleLangKey, 600, 500, input.AllowDeleting, input.AllowAdding, input.AllowUpdating, input.ConfirmDelete, false, input.AllowDetail),
EditingFormJson = editingFormDtos.Count > 0 ? JsonSerializer.Serialize(editingFormDtos) : null, EditingFormJson = editingFormDtos.Count > 0 ? JsonSerializer.Serialize(editingFormDtos) : null,
SubFormsJson = input.SubForms.Count > 0 ? JsonSerializer.Serialize(input.SubForms) : null,
WidgetsJson = input.Widgets.Count > 0 ? JsonSerializer.Serialize(input.Widgets) : null,
WorkflowJson = HasWorkflow(input.Workflow, input.WorkflowCriteria) ? JsonSerializer.Serialize(input.Workflow) : null,
TreeOptionJson = (input.Tree || input.DefaultLayout == "tree") && !string.IsNullOrEmpty(input.TreeParentIdExpr) TreeOptionJson = (input.Tree || input.DefaultLayout == "tree") && !string.IsNullOrEmpty(input.TreeParentIdExpr)
? JsonSerializer.Serialize(new TreeOptionDto ? JsonSerializer.Serialize(new TreeOptionDto
{ {
@ -329,6 +342,7 @@ public class ListFormWizardAppService(
Visible = item.DataField != input.KeyFieldName, Visible = item.DataField != input.KeyFieldName,
IsActive = true, IsActive = true,
AllowSearch = true, AllowSearch = true,
Width = 0,
ListOrderNo = fieldOrder, ListOrderNo = fieldOrder,
SourceDbType = item.DbSourceType, SourceDbType = item.DbSourceType,
CultureName = PlatformConsts.DefaultLanguage, CultureName = PlatformConsts.DefaultLanguage,
@ -343,6 +357,33 @@ public class ListFormWizardAppService(
} }
} }
foreach (var criteria in input.WorkflowCriteria)
{
if (string.IsNullOrWhiteSpace(criteria.Id))
{
continue;
}
await repoListFormWorkflow.InsertAsync(new ListFormWorkflow(criteria.Id)
{
ListFormCode = input.ListFormCode,
Kind = criteria.Kind,
Title = criteria.Title,
CompareColumn = criteria.CompareColumn,
CompareOperator = criteria.CompareOperator,
CompareValue = criteria.CompareValue,
Approver = criteria.Approver,
NextOnStart = criteria.NextOnStart,
NextOnTrue = criteria.NextOnTrue,
NextOnFalse = criteria.NextOnFalse,
NextOnApprove = criteria.NextOnApprove,
NextOnReject = criteria.NextOnReject,
PositionX = criteria.PositionX,
PositionY = criteria.PositionY,
CompareOutcomesJson = JsonSerializer.Serialize(criteria.CompareOutcomes ?? []),
}, autoSave: true);
}
// Clear Redis Cache // Clear Redis Cache
await _languageTextAppService.ClearRedisCacheAsync(); await _languageTextAppService.ClearRedisCacheAsync();
@ -350,6 +391,17 @@ public class ListFormWizardAppService(
await SaveWizardSeedFileAsync(input, isDeleted, isCreated, inserted); await SaveWizardSeedFileAsync(input, isDeleted, isCreated, inserted);
} }
private static bool HasWorkflow(WorkflowDto workflow, List<ListFormWorkflowCriteriaDto> criteria)
{
return workflow != null && (
!string.IsNullOrWhiteSpace(workflow.ApprovalUserFieldName) ||
!string.IsNullOrWhiteSpace(workflow.ApprovalDateFieldName) ||
!string.IsNullOrWhiteSpace(workflow.ApprovalStatusFieldName) ||
!string.IsNullOrWhiteSpace(workflow.ApprovalDescriptionFieldName) ||
criteria.Count > 0
);
}
/// <summary> /// <summary>
/// Wizard konfigürasyonunu JSON dosyası olarak kaydeder. /// Wizard konfigürasyonunu JSON dosyası olarak kaydeder.
/// Önce ContentRootPath'ten yukarı çıkarak Sozsoft.Platform.DbMigrator/Seeds/WizardData dizinini arar. /// Önce ContentRootPath'ten yukarı çıkarak Sozsoft.Platform.DbMigrator/Seeds/WizardData dizinini arar.

View file

@ -17803,12 +17803,6 @@
"key": "App.SqlQueryManager.NoIndexesDefined", "key": "App.SqlQueryManager.NoIndexesDefined",
"en": "No indexes defined", "en": "No indexes defined",
"tr": "Dizin tanımlanmamış" "tr": "Dizin tanımlanmamış"
},
{
"resourceName": "Platform",
"key": "App.SqlQueryManager.NoIndexesDefined",
"en": "No indexes defined",
"tr": "Dizin tanımlanmamış"
}, },
{ {
"resourceName": "Platform", "resourceName": "Platform",

View file

@ -60,7 +60,7 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep
PageSize = 10, PageSize = 10,
ExportJson = DefaultExportJson, ExportJson = DefaultExportJson,
IsSubForm = false, IsSubForm = false,
ShowNote = true, ShowNote = false,
LayoutJson = DefaultLayoutJson(), LayoutJson = DefaultLayoutJson(),
CultureName = LanguageCodes.En, CultureName = LanguageCodes.En,
ListFormCode = listFormName, ListFormCode = listFormName,
@ -183,7 +183,7 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep
PageSize = 10, PageSize = 10,
ExportJson = DefaultExportJson, ExportJson = DefaultExportJson,
IsSubForm = false, IsSubForm = false,
ShowNote = true, ShowNote = false,
LayoutJson = DefaultLayoutJson("tree"), LayoutJson = DefaultLayoutJson("tree"),
CultureName = LanguageCodes.En, CultureName = LanguageCodes.En,
ListFormCode = listFormName, ListFormCode = listFormName,
@ -417,7 +417,7 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep
PageSize = 10, PageSize = 10,
ExportJson = DefaultExportJson, ExportJson = DefaultExportJson,
IsSubForm = false, IsSubForm = false,
ShowNote = true, ShowNote = false,
LayoutJson = DefaultLayoutJson(), LayoutJson = DefaultLayoutJson(),
CultureName = LanguageCodes.En, CultureName = LanguageCodes.En,
ListFormCode = listFormName, ListFormCode = listFormName,
@ -635,7 +635,7 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep
PageSize = 10, PageSize = 10,
ExportJson = DefaultExportJson, ExportJson = DefaultExportJson,
IsSubForm = false, IsSubForm = false,
ShowNote = true, ShowNote = false,
LayoutJson = DefaultLayoutJson(), LayoutJson = DefaultLayoutJson(),
CultureName = LanguageCodes.En, CultureName = LanguageCodes.En,
ListFormCode = listFormName, ListFormCode = listFormName,
@ -771,7 +771,7 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep
PageSize = 10, PageSize = 10,
ExportJson = DefaultExportJson, ExportJson = DefaultExportJson,
IsSubForm = false, IsSubForm = false,
ShowNote = true, ShowNote = false,
LayoutJson = DefaultLayoutJson(), LayoutJson = DefaultLayoutJson(),
CultureName = LanguageCodes.En, CultureName = LanguageCodes.En,
ListFormCode = listFormName, ListFormCode = listFormName,
@ -1171,7 +1171,7 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep
PageSize = 10, PageSize = 10,
ExportJson = DefaultExportJson, ExportJson = DefaultExportJson,
IsSubForm = false, IsSubForm = false,
ShowNote = true, ShowNote = false,
LayoutJson = DefaultLayoutJson(), LayoutJson = DefaultLayoutJson(),
CultureName = LanguageCodes.En, CultureName = LanguageCodes.En,
ListFormCode = listFormName, ListFormCode = listFormName,
@ -1370,7 +1370,7 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep
PageSize = 10, PageSize = 10,
ExportJson = DefaultExportJson, ExportJson = DefaultExportJson,
IsSubForm = false, IsSubForm = false,
ShowNote = true, ShowNote = false,
LayoutJson = DefaultLayoutJson(), LayoutJson = DefaultLayoutJson(),
CultureName = LanguageCodes.En, CultureName = LanguageCodes.En,
ListFormCode = listFormName, ListFormCode = listFormName,
@ -1498,7 +1498,7 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep
PageSize = 10, PageSize = 10,
ExportJson = DefaultExportJson, ExportJson = DefaultExportJson,
IsSubForm = false, IsSubForm = false,
ShowNote = true, ShowNote = false,
LayoutJson = DefaultLayoutJson(), LayoutJson = DefaultLayoutJson(),
CultureName = LanguageCodes.En, CultureName = LanguageCodes.En,
ListFormCode = listFormName, ListFormCode = listFormName,
@ -1695,7 +1695,7 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep
PageSize = 10, PageSize = 10,
ExportJson = DefaultExportJson, ExportJson = DefaultExportJson,
IsSubForm = false, IsSubForm = false,
ShowNote = true, ShowNote = false,
LayoutJson = DefaultLayoutJson(), LayoutJson = DefaultLayoutJson(),
CultureName = LanguageCodes.En, CultureName = LanguageCodes.En,
ListFormCode = listFormName, ListFormCode = listFormName,
@ -1789,7 +1789,7 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep
PageSize = 10, PageSize = 10,
ExportJson = DefaultExportJson, ExportJson = DefaultExportJson,
IsSubForm = false, IsSubForm = false,
ShowNote = true, ShowNote = false,
LayoutJson = DefaultLayoutJson(), LayoutJson = DefaultLayoutJson(),
CultureName = LanguageCodes.En, CultureName = LanguageCodes.En,
ListFormCode = listFormName, ListFormCode = listFormName,
@ -2046,7 +2046,7 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep
PageSize = 10, PageSize = 10,
ExportJson = DefaultExportJson, ExportJson = DefaultExportJson,
IsSubForm = false, IsSubForm = false,
ShowNote = true, ShowNote = false,
LayoutJson = DefaultLayoutJson(), LayoutJson = DefaultLayoutJson(),
CultureName = LanguageCodes.En, CultureName = LanguageCodes.En,
ListFormCode = listFormName, ListFormCode = listFormName,
@ -2190,7 +2190,7 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep
PageSize = 10, PageSize = 10,
ExportJson = DefaultExportJson, ExportJson = DefaultExportJson,
IsSubForm = false, IsSubForm = false,
ShowNote = true, ShowNote = false,
LayoutJson = DefaultLayoutJson(), LayoutJson = DefaultLayoutJson(),
CultureName = LanguageCodes.En, CultureName = LanguageCodes.En,
ListFormCode = listFormName, ListFormCode = listFormName,
@ -2383,7 +2383,7 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep
PageSize = 10, PageSize = 10,
ExportJson = DefaultExportJson, ExportJson = DefaultExportJson,
IsSubForm = false, IsSubForm = false,
ShowNote = true, ShowNote = false,
LayoutJson = DefaultLayoutJson("tree"), LayoutJson = DefaultLayoutJson("tree"),
CultureName = LanguageCodes.En, CultureName = LanguageCodes.En,
ListFormCode = listFormName, ListFormCode = listFormName,
@ -2499,7 +2499,7 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep
PageSize = 10, PageSize = 10,
ExportJson = DefaultExportJson, ExportJson = DefaultExportJson,
IsSubForm = false, IsSubForm = false,
ShowNote = true, ShowNote = false,
LayoutJson = DefaultLayoutJson("tree"), LayoutJson = DefaultLayoutJson("tree"),
CultureName = LanguageCodes.En, CultureName = LanguageCodes.En,
ListFormCode = listFormName, ListFormCode = listFormName,
@ -2643,7 +2643,7 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep
PageSize = 10, PageSize = 10,
ExportJson = DefaultExportJson, ExportJson = DefaultExportJson,
IsSubForm = false, IsSubForm = false,
ShowNote = true, ShowNote = false,
LayoutJson = DefaultLayoutJson(), LayoutJson = DefaultLayoutJson(),
CultureName = LanguageCodes.En, CultureName = LanguageCodes.En,
ListFormCode = listFormName, ListFormCode = listFormName,
@ -3268,7 +3268,7 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep
PageSize = 10, PageSize = 10,
ExportJson = DefaultExportJson, ExportJson = DefaultExportJson,
IsSubForm = true, IsSubForm = true,
ShowNote = true, ShowNote = false,
LayoutJson = DefaultLayoutJson(), LayoutJson = DefaultLayoutJson(),
CultureName = LanguageCodes.En, CultureName = LanguageCodes.En,
ListFormCode = listFormName, ListFormCode = listFormName,
@ -3548,7 +3548,7 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep
PageSize = 10, PageSize = 10,
ExportJson = DefaultExportJson, ExportJson = DefaultExportJson,
IsSubForm = true, IsSubForm = true,
ShowNote = true, ShowNote = false,
LayoutJson = DefaultLayoutJson(), LayoutJson = DefaultLayoutJson(),
CultureName = LanguageCodes.En, CultureName = LanguageCodes.En,
ListFormCode = listFormName, ListFormCode = listFormName,
@ -3856,7 +3856,7 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep
PageSize = 10, PageSize = 10,
ExportJson = DefaultExportJson, ExportJson = DefaultExportJson,
IsSubForm = false, IsSubForm = false,
ShowNote = true, ShowNote = false,
LayoutJson = DefaultLayoutJson(), LayoutJson = DefaultLayoutJson(),
CultureName = LanguageCodes.En, CultureName = LanguageCodes.En,
ListFormCode = listFormName, ListFormCode = listFormName,
@ -3961,7 +3961,7 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep
PageSize = 10, PageSize = 10,
ExportJson = DefaultExportJson, ExportJson = DefaultExportJson,
IsSubForm = false, IsSubForm = false,
ShowNote = true, ShowNote = false,
LayoutJson = DefaultLayoutJson(), LayoutJson = DefaultLayoutJson(),
CultureName = LanguageCodes.En, CultureName = LanguageCodes.En,
ListFormCode = listFormName, ListFormCode = listFormName,
@ -4049,7 +4049,7 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep
PageSize = 10, PageSize = 10,
ExportJson = DefaultExportJson, ExportJson = DefaultExportJson,
IsSubForm = false, IsSubForm = false,
ShowNote = true, ShowNote = false,
LayoutJson = DefaultLayoutJson(), LayoutJson = DefaultLayoutJson(),
CultureName = LanguageCodes.En, CultureName = LanguageCodes.En,
ListFormCode = listFormName, ListFormCode = listFormName,
@ -4444,7 +4444,7 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep
PageSize = 10, PageSize = 10,
ExportJson = DefaultExportJson, ExportJson = DefaultExportJson,
IsSubForm = false, IsSubForm = false,
ShowNote = true, ShowNote = false,
LayoutJson = DefaultLayoutJson(), LayoutJson = DefaultLayoutJson(),
CultureName = LanguageCodes.En, CultureName = LanguageCodes.En,
ListFormCode = listFormName, ListFormCode = listFormName,

View file

@ -560,7 +560,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
PageSize = 10, PageSize = 10,
ExportJson = DefaultExportJson, ExportJson = DefaultExportJson,
IsSubForm = true, IsSubForm = true,
ShowNote = true, ShowNote = false,
LayoutJson = DefaultLayoutJson(), LayoutJson = DefaultLayoutJson(),
CultureName = LanguageCodes.En, CultureName = LanguageCodes.En,
ListFormCode = listFormName, ListFormCode = listFormName,
@ -1018,7 +1018,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
PageSize = 10, PageSize = 10,
ExportJson = DefaultExportJson, ExportJson = DefaultExportJson,
IsSubForm = false, IsSubForm = false,
ShowNote = true, ShowNote = false,
LayoutJson = DefaultLayoutJson(), LayoutJson = DefaultLayoutJson(),
CultureName = LanguageCodes.En, CultureName = LanguageCodes.En,
ListFormCode = listFormName, ListFormCode = listFormName,
@ -1178,7 +1178,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
PageSize = 10, PageSize = 10,
ExportJson = DefaultExportJson, ExportJson = DefaultExportJson,
IsSubForm = false, IsSubForm = false,
ShowNote = true, ShowNote = false,
LayoutJson = DefaultLayoutJson(), LayoutJson = DefaultLayoutJson(),
CultureName = LanguageCodes.En, CultureName = LanguageCodes.En,
ListFormCode = listFormName, ListFormCode = listFormName,
@ -1323,7 +1323,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
PageSize = 10, PageSize = 10,
ExportJson = DefaultExportJson, ExportJson = DefaultExportJson,
IsSubForm = false, IsSubForm = false,
ShowNote = true, ShowNote = false,
LayoutJson = DefaultLayoutJson(), LayoutJson = DefaultLayoutJson(),
CultureName = LanguageCodes.En, CultureName = LanguageCodes.En,
ListFormCode = listFormName, ListFormCode = listFormName,
@ -1436,7 +1436,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
PageSize = 10, PageSize = 10,
ExportJson = DefaultExportJson, ExportJson = DefaultExportJson,
IsSubForm = false, IsSubForm = false,
ShowNote = true, ShowNote = false,
LayoutJson = DefaultLayoutJson(), LayoutJson = DefaultLayoutJson(),
CultureName = LanguageCodes.En, CultureName = LanguageCodes.En,
ListFormCode = listFormName, ListFormCode = listFormName,
@ -1581,7 +1581,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
PageSize = 10, PageSize = 10,
ExportJson = DefaultExportJson, ExportJson = DefaultExportJson,
IsSubForm = false, IsSubForm = false,
ShowNote = true, ShowNote = false,
LayoutJson = DefaultLayoutJson(), LayoutJson = DefaultLayoutJson(),
CultureName = LanguageCodes.En, CultureName = LanguageCodes.En,
ListFormCode = listFormName, ListFormCode = listFormName,
@ -1671,7 +1671,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
PageSize = 10, PageSize = 10,
ExportJson = DefaultExportJson, ExportJson = DefaultExportJson,
IsSubForm = false, IsSubForm = false,
ShowNote = true, ShowNote = false,
LayoutJson = DefaultLayoutJson(), LayoutJson = DefaultLayoutJson(),
CultureName = LanguageCodes.En, CultureName = LanguageCodes.En,
ListFormCode = listFormName, ListFormCode = listFormName,
@ -1867,7 +1867,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
PageSize = 10, PageSize = 10,
ExportJson = DefaultExportJson, ExportJson = DefaultExportJson,
IsSubForm = false, IsSubForm = false,
ShowNote = true, ShowNote = false,
LayoutJson = DefaultLayoutJson(), LayoutJson = DefaultLayoutJson(),
CultureName = LanguageCodes.En, CultureName = LanguageCodes.En,
ListFormCode = listFormName, ListFormCode = listFormName,
@ -2003,7 +2003,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
PageSize = 10, PageSize = 10,
ExportJson = DefaultExportJson, ExportJson = DefaultExportJson,
IsSubForm = false, IsSubForm = false,
ShowNote = true, ShowNote = false,
LayoutJson = DefaultLayoutJson(), LayoutJson = DefaultLayoutJson(),
CultureName = LanguageCodes.En, CultureName = LanguageCodes.En,
ListFormCode = listFormName, ListFormCode = listFormName,
@ -2274,7 +2274,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
PageSize = 10, PageSize = 10,
ExportJson = DefaultExportJson, ExportJson = DefaultExportJson,
IsSubForm = true, IsSubForm = true,
ShowNote = true, ShowNote = false,
LayoutJson = DefaultLayoutJson(), LayoutJson = DefaultLayoutJson(),
CultureName = LanguageCodes.En, CultureName = LanguageCodes.En,
ListFormCode = listFormName, ListFormCode = listFormName,
@ -2477,7 +2477,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
PageSize = 10, PageSize = 10,
ExportJson = DefaultExportJson, ExportJson = DefaultExportJson,
IsSubForm = false, IsSubForm = false,
ShowNote = true, ShowNote = false,
LayoutJson = DefaultLayoutJson(), LayoutJson = DefaultLayoutJson(),
CultureName = LanguageCodes.En, CultureName = LanguageCodes.En,
ListFormCode = listFormName, ListFormCode = listFormName,
@ -2894,7 +2894,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
PageSize = 10, PageSize = 10,
ExportJson = DefaultExportJson, ExportJson = DefaultExportJson,
IsSubForm = true, IsSubForm = true,
ShowNote = true, ShowNote = false,
LayoutJson = DefaultLayoutJson(), LayoutJson = DefaultLayoutJson(),
CultureName = LanguageCodes.En, CultureName = LanguageCodes.En,
ListFormCode = listFormName, ListFormCode = listFormName,
@ -3040,7 +3040,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
PageSize = 10, PageSize = 10,
ExportJson = DefaultExportJson, ExportJson = DefaultExportJson,
IsSubForm = true, IsSubForm = true,
ShowNote = true, ShowNote = false,
LayoutJson = DefaultLayoutJson(), LayoutJson = DefaultLayoutJson(),
CultureName = LanguageCodes.En, CultureName = LanguageCodes.En,
ListFormCode = listFormName, ListFormCode = listFormName,
@ -3147,7 +3147,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
PageSize = 10, PageSize = 10,
ExportJson = DefaultExportJson, ExportJson = DefaultExportJson,
IsSubForm = false, IsSubForm = false,
ShowNote = true, ShowNote = false,
LayoutJson = DefaultLayoutJson(), LayoutJson = DefaultLayoutJson(),
CultureName = LanguageCodes.En, CultureName = LanguageCodes.En,
ListFormCode = listFormName, ListFormCode = listFormName,
@ -3694,7 +3694,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
PageSize = 10, PageSize = 10,
ExportJson = DefaultExportJson, ExportJson = DefaultExportJson,
IsSubForm = true, IsSubForm = true,
ShowNote = true, ShowNote = false,
LayoutJson = DefaultLayoutJson(), LayoutJson = DefaultLayoutJson(),
CultureName = LanguageCodes.En, CultureName = LanguageCodes.En,
ListFormCode = listFormName, ListFormCode = listFormName,
@ -3868,7 +3868,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
PageSize = 10, PageSize = 10,
ExportJson = DefaultExportJson, ExportJson = DefaultExportJson,
IsSubForm = false, IsSubForm = false,
ShowNote = true, ShowNote = false,
LayoutJson = DefaultLayoutJson(), LayoutJson = DefaultLayoutJson(),
CultureName = LanguageCodes.En, CultureName = LanguageCodes.En,
ListFormCode = listFormName, ListFormCode = listFormName,
@ -4005,7 +4005,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
PageSize = 10, PageSize = 10,
ExportJson = DefaultExportJson, ExportJson = DefaultExportJson,
IsSubForm = false, IsSubForm = false,
ShowNote = true, ShowNote = false,
LayoutJson = DefaultLayoutJson(), LayoutJson = DefaultLayoutJson(),
CultureName = LanguageCodes.En, CultureName = LanguageCodes.En,
ListFormCode = listFormName, ListFormCode = listFormName,
@ -4496,7 +4496,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
PageSize = 10, PageSize = 10,
ExportJson = DefaultExportJson, ExportJson = DefaultExportJson,
IsSubForm = false, IsSubForm = false,
ShowNote = true, ShowNote = false,
LayoutJson = DefaultLayoutJson(), LayoutJson = DefaultLayoutJson(),
CultureName = LanguageCodes.En, CultureName = LanguageCodes.En,
ListFormCode = listFormName, ListFormCode = listFormName,
@ -4586,7 +4586,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
PageSize = 10, PageSize = 10,
ExportJson = DefaultExportJson, ExportJson = DefaultExportJson,
IsSubForm = false, IsSubForm = false,
ShowNote = true, ShowNote = false,
LayoutJson = DefaultLayoutJson(), LayoutJson = DefaultLayoutJson(),
CultureName = LanguageCodes.En, CultureName = LanguageCodes.En,
ListFormCode = listFormName, ListFormCode = listFormName,
@ -4828,7 +4828,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
PageSize = 10, PageSize = 10,
ExportJson = DefaultExportJson, ExportJson = DefaultExportJson,
IsSubForm = false, IsSubForm = false,
ShowNote = true, ShowNote = false,
LayoutJson = DefaultLayoutJson(), LayoutJson = DefaultLayoutJson(),
CultureName = LanguageCodes.En, CultureName = LanguageCodes.En,
ListFormCode = listFormName, ListFormCode = listFormName,
@ -5029,7 +5029,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
PageSize = 10, PageSize = 10,
ExportJson = DefaultExportJson, ExportJson = DefaultExportJson,
IsSubForm = false, IsSubForm = false,
ShowNote = true, ShowNote = false,
LayoutJson = DefaultLayoutJson(), LayoutJson = DefaultLayoutJson(),
CultureName = LanguageCodes.En, CultureName = LanguageCodes.En,
ListFormCode = listFormName, ListFormCode = listFormName,
@ -5267,7 +5267,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
PageSize = 10, PageSize = 10,
ExportJson = DefaultExportJson, ExportJson = DefaultExportJson,
IsSubForm = false, IsSubForm = false,
ShowNote = true, ShowNote = false,
LayoutJson = DefaultLayoutJson(), LayoutJson = DefaultLayoutJson(),
CultureName = LanguageCodes.En, CultureName = LanguageCodes.En,
ListFormCode = listFormName, ListFormCode = listFormName,
@ -5442,7 +5442,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
PageSize = 10, PageSize = 10,
ExportJson = DefaultExportJson, ExportJson = DefaultExportJson,
IsSubForm = false, IsSubForm = false,
ShowNote = true, ShowNote = false,
LayoutJson = DefaultLayoutJson(), LayoutJson = DefaultLayoutJson(),
CultureName = LanguageCodes.En, CultureName = LanguageCodes.En,
ListFormCode = listFormName, ListFormCode = listFormName,
@ -5624,7 +5624,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
PageSize = 10, PageSize = 10,
ExportJson = DefaultExportJson, ExportJson = DefaultExportJson,
IsSubForm = false, IsSubForm = false,
ShowNote = true, ShowNote = false,
LayoutJson = DefaultLayoutJson(), LayoutJson = DefaultLayoutJson(),
CultureName = LanguageCodes.En, CultureName = LanguageCodes.En,
ListFormCode = listFormName, ListFormCode = listFormName,
@ -5713,7 +5713,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
PageSize = 10, PageSize = 10,
ExportJson = DefaultExportJson, ExportJson = DefaultExportJson,
IsSubForm = false, IsSubForm = false,
ShowNote = true, ShowNote = false,
LayoutJson = DefaultLayoutJson("tree"), LayoutJson = DefaultLayoutJson("tree"),
CultureName = LanguageCodes.En, CultureName = LanguageCodes.En,
ListFormCode = listFormName, ListFormCode = listFormName,
@ -6029,7 +6029,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
PageSize = 10, PageSize = 10,
ExportJson = DefaultExportJson, ExportJson = DefaultExportJson,
IsSubForm = false, IsSubForm = false,
ShowNote = true, ShowNote = false,
LayoutJson = DefaultLayoutJson(), LayoutJson = DefaultLayoutJson(),
CultureName = LanguageCodes.En, CultureName = LanguageCodes.En,
ListFormCode = listFormName, ListFormCode = listFormName,
@ -6258,7 +6258,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
PageSize = 10, PageSize = 10,
ExportJson = DefaultExportJson, ExportJson = DefaultExportJson,
IsSubForm = false, IsSubForm = false,
ShowNote = true, ShowNote = false,
LayoutJson = DefaultLayoutJson(), LayoutJson = DefaultLayoutJson(),
CultureName = LanguageCodes.En, CultureName = LanguageCodes.En,
ListFormCode = listFormName, ListFormCode = listFormName,
@ -6493,7 +6493,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
PageSize = 10, PageSize = 10,
ExportJson = DefaultExportJson, ExportJson = DefaultExportJson,
IsSubForm = false, IsSubForm = false,
ShowNote = true, ShowNote = false,
LayoutJson = DefaultLayoutJson(), LayoutJson = DefaultLayoutJson(),
CultureName = LanguageCodes.En, CultureName = LanguageCodes.En,
ListFormCode = listFormName, ListFormCode = listFormName,
@ -6627,7 +6627,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
PageSize = 10, PageSize = 10,
ExportJson = DefaultExportJson, ExportJson = DefaultExportJson,
IsSubForm = false, IsSubForm = false,
ShowNote = true, ShowNote = false,
LayoutJson = DefaultLayoutJson(), LayoutJson = DefaultLayoutJson(),
CultureName = LanguageCodes.En, CultureName = LanguageCodes.En,
ListFormCode = listFormName, ListFormCode = listFormName,
@ -7291,7 +7291,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
PageSize = 10, PageSize = 10,
ExportJson = DefaultExportJson, ExportJson = DefaultExportJson,
IsSubForm = true, IsSubForm = true,
ShowNote = true, ShowNote = false,
LayoutJson = DefaultLayoutJson(), LayoutJson = DefaultLayoutJson(),
CultureName = LanguageCodes.En, CultureName = LanguageCodes.En,
ListFormCode = listFormName, ListFormCode = listFormName,
@ -7459,7 +7459,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
PageSize = 10, PageSize = 10,
ExportJson = DefaultExportJson, ExportJson = DefaultExportJson,
IsSubForm = false, IsSubForm = false,
ShowNote = true, ShowNote = false,
LayoutJson = DefaultLayoutJson(), LayoutJson = DefaultLayoutJson(),
CultureName = LanguageCodes.En, CultureName = LanguageCodes.En,
ListFormCode = listFormName, ListFormCode = listFormName,
@ -7660,7 +7660,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
PageSize = 10, PageSize = 10,
ExportJson = DefaultExportJson, ExportJson = DefaultExportJson,
IsSubForm = false, IsSubForm = false,
ShowNote = true, ShowNote = false,
LayoutJson = DefaultLayoutJson(), LayoutJson = DefaultLayoutJson(),
CultureName = LanguageCodes.En, CultureName = LanguageCodes.En,
ListFormCode = listFormName, ListFormCode = listFormName,
@ -8015,7 +8015,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
PageSize = 10, PageSize = 10,
ExportJson = DefaultExportJson, ExportJson = DefaultExportJson,
IsSubForm = false, IsSubForm = false,
ShowNote = true, ShowNote = false,
LayoutJson = DefaultLayoutJson(), LayoutJson = DefaultLayoutJson(),
CultureName = LanguageCodes.En, CultureName = LanguageCodes.En,
ListFormCode = listFormName, ListFormCode = listFormName,

View file

@ -82,6 +82,15 @@
"MultiTenancySide": 2, "MultiTenancySide": 2,
"MenuGroup": "Erp|Kurs" "MenuGroup": "Erp|Kurs"
}, },
{
"GroupName": "App.Saas",
"Name": "AbpTenantManagement.Tenants.Note",
"ParentName": "AbpTenantManagement.Tenants",
"DisplayName": "Note",
"IsEnabled": true,
"MultiTenancySide": 2,
"MenuGroup": "Erp|Kurs"
},
{ {
"GroupName": "App.Saas", "GroupName": "App.Saas",
"Name": "App.Branches", "Name": "App.Branches",
@ -1765,6 +1774,15 @@
"MultiTenancySide": 2, "MultiTenancySide": 2,
"MenuGroup": "Erp|Kurs" "MenuGroup": "Erp|Kurs"
}, },
{
"GroupName": "App.Saas",
"Name": "App.Orders.SalesOrders.Note",
"ParentName": "App.Orders.SalesOrders",
"DisplayName": "Note",
"IsEnabled": true,
"MultiTenancySide": 2,
"MenuGroup": "Erp|Kurs"
},
{ {
"GroupName": "App.Saas", "GroupName": "App.Saas",
"Name": "App.Orders.SalesOrderItem", "Name": "App.Orders.SalesOrderItem",
@ -3556,15 +3574,6 @@
"MultiTenancySide": 3, "MultiTenancySide": 3,
"MenuGroup": "Erp|Kurs" "MenuGroup": "Erp|Kurs"
}, },
{
"GroupName": "App.Administration",
"Name": "App.Intranet.Events.EventType.Note",
"ParentName": "App.Intranet.Events.EventType",
"DisplayName": "Note",
"IsEnabled": true,
"MultiTenancySide": 3,
"MenuGroup": "Erp|Kurs"
},
{ {
"GroupName": "App.Administration", "GroupName": "App.Administration",
"Name": "App.Intranet.Events.EventCategory", "Name": "App.Intranet.Events.EventCategory",
@ -3619,15 +3628,6 @@
"MultiTenancySide": 3, "MultiTenancySide": 3,
"MenuGroup": "Erp|Kurs" "MenuGroup": "Erp|Kurs"
}, },
{
"GroupName": "App.Administration",
"Name": "App.Intranet.Events.EventCategory.Note",
"ParentName": "App.Intranet.Events.EventCategory",
"DisplayName": "Note",
"IsEnabled": true,
"MultiTenancySide": 3,
"MenuGroup": "Erp|Kurs"
},
{ {
"GroupName": "App.Administration", "GroupName": "App.Administration",
"Name": "App.Intranet.Events.Event", "Name": "App.Intranet.Events.Event",
@ -3754,15 +3754,6 @@
"MultiTenancySide": 3, "MultiTenancySide": 3,
"MenuGroup": "Erp|Kurs" "MenuGroup": "Erp|Kurs"
}, },
{
"GroupName": "App.Administration",
"Name": "App.Intranet.Events.EventLike.Note",
"ParentName": "App.Intranet.Events.EventLike",
"DisplayName": "Note",
"IsEnabled": true,
"MultiTenancySide": 3,
"MenuGroup": "Erp|Kurs"
},
{ {
"GroupName": "App.Administration", "GroupName": "App.Administration",
"Name": "App.Intranet.Announcement", "Name": "App.Intranet.Announcement",
@ -3817,15 +3808,6 @@
"MultiTenancySide": 3, "MultiTenancySide": 3,
"MenuGroup": "Erp|Kurs" "MenuGroup": "Erp|Kurs"
}, },
{
"GroupName": "App.Administration",
"Name": "App.Intranet.Announcement.Note",
"ParentName": "App.Intranet.Announcement",
"DisplayName": "Note",
"IsEnabled": true,
"MultiTenancySide": 3,
"MenuGroup": "Erp|Kurs"
},
{ {
"GroupName": "App.Administration", "GroupName": "App.Administration",
"Name": "App.Intranet.Announcement.Widget", "Name": "App.Intranet.Announcement.Widget",
@ -3961,15 +3943,6 @@
"MultiTenancySide": 3, "MultiTenancySide": 3,
"MenuGroup": "Erp|Kurs" "MenuGroup": "Erp|Kurs"
}, },
{
"GroupName": "App.Administration",
"Name": "App.Intranet.SocialComment.Note",
"ParentName": "App.Intranet.SocialComment",
"DisplayName": "Note",
"IsEnabled": true,
"MultiTenancySide": 3,
"MenuGroup": "Erp|Kurs"
},
{ {
"GroupName": "App.Administration", "GroupName": "App.Administration",
"Name": "App.Intranet.Survey", "Name": "App.Intranet.Survey",
@ -4159,15 +4132,6 @@
"MultiTenancySide": 3, "MultiTenancySide": 3,
"MenuGroup": "Erp" "MenuGroup": "Erp"
}, },
{
"GroupName": "App.Administration",
"Name": "App.Intranet.SurveyQuestionOption.Note",
"ParentName": "App.Intranet.SurveyQuestionOption",
"DisplayName": "Note",
"IsEnabled": true,
"MultiTenancySide": 3,
"MenuGroup": "Erp"
},
{ {
"GroupName": "App.Administration", "GroupName": "App.Administration",
"Name": "App.Intranet.SurveyResponse", "Name": "App.Intranet.SurveyResponse",
@ -4285,15 +4249,6 @@
"MultiTenancySide": 3, "MultiTenancySide": 3,
"MenuGroup": "Erp" "MenuGroup": "Erp"
}, },
{
"GroupName": "App.Administration",
"Name": "App.Intranet.SurveyAnswer.Note",
"ParentName": "App.Intranet.SurveyAnswer",
"DisplayName": "Note",
"IsEnabled": true,
"MultiTenancySide": 3,
"MenuGroup": "Erp"
},
{ {
"GroupName": "App.Administration", "GroupName": "App.Administration",
"Name": "App.Videoroom", "Name": "App.Videoroom",

View file

@ -130,6 +130,11 @@ public class WizardDataSeeder : IDataSeedContributor, ITransientDependency
var input = seedFile.Wizard; var input = seedFile.Wizard;
var isDeleted = seedFile.IsDeletedField; var isDeleted = seedFile.IsDeletedField;
var isCreated = seedFile.IsCreatedField; var isCreated = seedFile.IsCreatedField;
input.SubForms ??= new List<SubFormDto>();
input.Widgets ??= new List<WidgetEditDto>();
input.Workflow ??= new WorkflowDto();
input.WorkflowCriteria ??= new List<ListFormWorkflowCriteriaDto>();
input.Workflow.Criteria = input.WorkflowCriteria;
var wizardName = input.WizardName.Trim(); var wizardName = input.WizardName.Trim();
var titleLangKey = WizardConsts.WizardKeyTitle(wizardName); var titleLangKey = WizardConsts.WizardKeyTitle(wizardName);
@ -306,7 +311,7 @@ public class WizardDataSeeder : IDataSeedContributor, ITransientDependency
PageSize = 10, PageSize = 10,
ExportJson = WizardConsts.DefaultExportJson, ExportJson = WizardConsts.DefaultExportJson,
IsSubForm = false, IsSubForm = false,
ShowNote = true, ShowNote = input.SubForms.Count > 0,
LayoutJson = WizardConsts.DefaultLayoutJson(input.DefaultLayout, input.Grid, input.Pivot, input.Tree, input.Chart, input.Gantt, input.Scheduler), LayoutJson = WizardConsts.DefaultLayoutJson(input.DefaultLayout, input.Grid, input.Pivot, input.Tree, input.Chart, input.Gantt, input.Scheduler),
CultureName = LanguageCodes.En, CultureName = LanguageCodes.En,
ListFormCode = input.ListFormCode, ListFormCode = input.ListFormCode,
@ -340,6 +345,9 @@ public class WizardDataSeeder : IDataSeedContributor, ITransientDependency
PagerOptionJson = WizardConsts.DefaultPagerOptionJson, PagerOptionJson = WizardConsts.DefaultPagerOptionJson,
EditingOptionJson = WizardConsts.DefaultEditingOptionJson(titleLangKey, 600, 500, input.AllowDeleting, input.AllowAdding, input.AllowUpdating, input.ConfirmDelete, false, input.AllowDetail), EditingOptionJson = WizardConsts.DefaultEditingOptionJson(titleLangKey, 600, 500, input.AllowDeleting, input.AllowAdding, input.AllowUpdating, input.ConfirmDelete, false, input.AllowDetail),
EditingFormJson = editingFormDtos.Count > 0 ? JsonSerializer.Serialize(editingFormDtos) : null, EditingFormJson = editingFormDtos.Count > 0 ? JsonSerializer.Serialize(editingFormDtos) : null,
SubFormsJson = input.SubForms.Count > 0 ? JsonSerializer.Serialize(input.SubForms) : null,
WidgetsJson = input.Widgets.Count > 0 ? JsonSerializer.Serialize(input.Widgets) : null,
WorkflowJson = HasWorkflow(input.Workflow, input.WorkflowCriteria) ? JsonSerializer.Serialize(input.Workflow) : null,
}, autoSave: true); }, autoSave: true);
// ListFormFields // ListFormFields
@ -375,6 +383,17 @@ public class WizardDataSeeder : IDataSeedContributor, ITransientDependency
} }
} }
private static bool HasWorkflow(WorkflowDto workflow, List<ListFormWorkflowCriteriaDto> criteria)
{
return workflow != null && (
!string.IsNullOrWhiteSpace(workflow.ApprovalUserFieldName) ||
!string.IsNullOrWhiteSpace(workflow.ApprovalDateFieldName) ||
!string.IsNullOrWhiteSpace(workflow.ApprovalStatusFieldName) ||
!string.IsNullOrWhiteSpace(workflow.ApprovalDescriptionFieldName) ||
(criteria?.Count ?? 0) > 0
);
}
private async Task CreateLangKeyAsync(string key, string textEn, string textTr) private async Task CreateLangKeyAsync(string key, string textEn, string textTr)
{ {
if (string.IsNullOrWhiteSpace(key)) return; if (string.IsNullOrWhiteSpace(key)) return;

View file

@ -1,5 +1,11 @@
import { SelectCommandTypeEnum } from "@/proxy/form/models" import { SelectCommandTypeEnum } from '@/proxy/form/models'
import { ListViewLayoutType } from "@/views/admin/listForm/edit/types" import {
ListFormWorkflowCriteriaDto,
SubFormDto,
WidgetEditDto,
WorkflowDto,
} from '@/proxy/form/models'
import { ListViewLayoutType } from '@/views/admin/listForm/edit/types'
export interface ListFormWizardColumnItemDto { export interface ListFormWizardColumnItemDto {
dataField: string dataField: string
@ -76,6 +82,10 @@ export interface ListFormWizardDto {
schedulerEndDateExpr?: string schedulerEndDateExpr?: string
groups?: ListFormWizardColumnGroupDto[] groups?: ListFormWizardColumnGroupDto[]
subForms?: SubFormDto[]
widgets?: WidgetEditDto[]
workflow?: WorkflowDto
workflowCriteria?: ListFormWorkflowCriteriaDto[]
} }
export interface WizardFileInfoDto { export interface WizardFileInfoDto {

View file

@ -18,7 +18,7 @@ const schema = object().shape({
d: string(), d: string(),
e: string(), e: string(),
i: string(), i: string(),
a: string(), n: string(),
}) })
function FormTabPermissions(props: FormEditProps) { function FormTabPermissions(props: FormEditProps) {
@ -210,31 +210,34 @@ function FormTabPermissions(props: FormEditProps) {
)} )}
</Field> </Field>
</FormItem> </FormItem>
<FormItem
label={translate('::ListForms.ListFormEdit.PermissionsNote')} {listFormValues.showNote && (
invalid={errors.permissionDto?.n && touched.permissionDto?.n} <FormItem
errorMessage={errors.permissionDto?.n} label={translate('::ListForms.ListFormEdit.PermissionsNote')}
> invalid={errors.permissionDto?.n && touched.permissionDto?.n}
<Field errorMessage={errors.permissionDto?.n}
type="text"
autoComplete="off"
name="permissionDto.n"
placeholder={translate('::ListForms.ListFormEdit.PermissionsNote')}
> >
{({ field, form }: FieldProps<SelectBoxOption>) => ( <Field
<Select type="text"
field={field} autoComplete="off"
form={form} name="permissionDto.n"
isClearable={true} placeholder={translate('::ListForms.ListFormEdit.PermissionsNote')}
options={permissions} >
value={permissions?.filter( {({ field, form }: FieldProps<SelectBoxOption>) => (
(option) => option.value === values.permissionDto.n, <Select
)} field={field}
onChange={(option) => form.setFieldValue(field.name, option?.value)} form={form}
/> isClearable={true}
)} options={permissions}
</Field> value={permissions?.filter(
</FormItem> (option) => option.value === values.permissionDto.n,
)}
onChange={(option) => form.setFieldValue(field.name, option?.value)}
/>
)}
</Field>
</FormItem>
)}
<Button block variant="solid" loading={isSubmitting} type="submit"> <Button block variant="solid" loading={isSubmitting} type="submit">
{isSubmitting ? translate('::SavingWithThreeDot') : translate('::Save')} {isSubmitting ? translate('::SavingWithThreeDot') : translate('::Save')}

View file

@ -63,7 +63,7 @@ function JsonRowOpDialogSubForm({
const childFieldOptions = childFields.map((field) => ({ const childFieldOptions = childFields.map((field) => ({
value: field.fieldName, value: field.fieldName,
label: field.fieldName, label: `${field.fieldName} (${field.dataType})`,
})) }))
const loadChildFields = async (listFormCode?: string) => { const loadChildFields = async (listFormCode?: string) => {
@ -159,329 +159,331 @@ function JsonRowOpDialogSubForm({
onRequestClose={handleClose} onRequestClose={handleClose}
> >
{(data.operation === 'create' || data.operation === 'update') && ( {(data.operation === 'create' || data.operation === 'update') && (
<Formik <Formik
initialValues={ initialValues={
data.subFormValues ?? { data.subFormValues ?? {
tabTitle: '', tabTitle: '',
tabType: 'List', tabType: 'List',
code: '', code: '',
isRefresh: false, isRefresh: false,
relation: [], relation: [],
}
} }
validationSchema={schema} }
onSubmit={async (values, { setSubmitting }) => { validationSchema={schema}
setSubmitting(true) onSubmit={async (values, { setSubmitting }) => {
try { setSubmitting(true)
const input: ListFormJsonRowDto = { try {
index: data.index, const input: ListFormJsonRowDto = {
fieldName: data.tabName, index: data.index,
itemSubForm: values as SubFormDto, fieldName: data.tabName,
} itemSubForm: values as SubFormDto,
if (data.index === -1) {
await postListFormJsonRow(data.id, input)
} else {
await putListFormJsonRow(data.id, input)
}
toast.push(
<Notification type="success">
{data.index === -1 ? 'Kayıt eklendi' : 'Kayıt güncellendi'}
</Notification>,
{ placement: 'top-end' },
)
handleClose()
} catch (error: any) {
toast.push(
<Notification type="danger">
Hata
<code>{error}</code>
</Notification>,
{ placement: 'top-end' },
)
} finally {
setSubmitting(false)
} }
}}
>
{({ touched, errors, values, isSubmitting }) => (
<Form className="flex flex-col h-full">
<Dialog.Body className="flex flex-col gap-2 overflow-hidden">
<h5 className="flex-shrink-0">{data.index === -1 ? 'Add' : 'Update'}</h5>
<div className="flex-1 overflow-y-auto p-2">
<div className="grid grid-cols-4 gap-4">
<FormItem
label="Tab Title"
invalid={errors.tabTitle && touched.tabTitle}
errorMessage={errors.tabTitle}
>
<Field
autoFocus={true}
type="text"
autoComplete="off"
name="tabTitle"
placeholder="Title"
component={Input}
/>
</FormItem>
<FormItem if (data.index === -1) {
label="Code" await postListFormJsonRow(data.id, input)
invalid={errors.code && touched.code} } else {
errorMessage={errors.code} await putListFormJsonRow(data.id, input)
> }
<Field type="text" autoComplete="off" name="code" placeholder="Code"> toast.push(
{({ field, form }: FieldProps<SelectBoxOption>) => ( <Notification type="success">
<Select {data.index === -1 ? 'Kayıt eklendi' : 'Kayıt güncellendi'}
field={field} </Notification>,
form={form} { placement: 'top-end' },
isClearable={true} )
isLoading={isLoadingListForms} handleClose()
options={listFormOptions} } catch (error: any) {
value={listFormOptions.find( toast.push(
(option) => option.value === values.code, <Notification type="danger">
)} Hata
onChange={(option) => { <code>{error}</code>
form.setFieldValue(field.name, option?.value ?? '') </Notification>,
form.setFieldValue('relation', []) { placement: 'top-end' },
loadChildFields(option?.value) )
} finally {
setSubmitting(false)
}
}}
>
{({ touched, errors, values, isSubmitting }) => (
<Form className="flex flex-col h-full">
<Dialog.Body className="flex flex-col gap-2 overflow-hidden">
<h5 className="flex-shrink-0">{data.index === -1 ? 'Add' : 'Update'}</h5>
<div className="flex-1 overflow-y-auto p-2">
<div className="grid grid-cols-4 gap-4">
<FormItem
label="Tab Title"
invalid={errors.tabTitle && touched.tabTitle}
errorMessage={errors.tabTitle}
>
<Field
autoFocus={true}
type="text"
autoComplete="off"
name="tabTitle"
placeholder="Title"
component={Input}
/>
</FormItem>
<FormItem
label="Code"
invalid={errors.code && touched.code}
errorMessage={errors.code}
>
<Field type="text" autoComplete="off" name="code" placeholder="Code">
{({ field, form }: FieldProps<SelectBoxOption>) => (
<Select
field={field}
form={form}
isClearable={true}
isLoading={isLoadingListForms}
options={listFormOptions}
value={listFormOptions.find((option) => option.value === values.code)}
onChange={(option) => {
form.setFieldValue(field.name, option?.value ?? '')
form.setFieldValue('relation', [])
loadChildFields(option?.value)
}}
/>
)}
</Field>
</FormItem>
<FormItem
label="Tab Type"
invalid={errors.tabType && touched.tabType}
errorMessage={errors.tabType}
>
<Field type="text" autoComplete="off" name="tabType" placeholder="Tab Type">
{({ field, form }: FieldProps<SelectBoxOption>) => (
<Select
field={field}
form={form}
isClearable={true}
options={tabTypeOptions}
value={tabTypeOptions?.filter(
(option: any) => option.value === values.tabType,
)}
onChange={(option) => form.setFieldValue(field.name, option?.value)}
/>
)}
</Field>
</FormItem>
<FormItem
label="isRefresh"
invalid={errors.isRefresh && touched.isRefresh}
errorMessage={errors.isRefresh}
>
<Field name="isRefresh" component={Checkbox} />
</FormItem>
</div>
<FormItem label="Relations">
<FieldArray
name="relation"
render={(arrayHelpers) => (
<div>
<div className="flex m-1 font-bold text-center">
<div className="w-4/12">Parent Field Name</div>
<div className="w-4/12">Child Field Name</div>
<div className="w-4/12">Db Type</div>
</div>
{values.relation && values.relation.length > 0 ? (
values.relation.map((item: SubFormRelationDto, index: any) => (
<div key={index} className="flex m-1">
<div className="w-4/12 ml-2">
<Field
type="text"
autoComplete="off"
name={`relation.${index}.parentFieldName`}
placeholder={translate(
'::ListForms.ListFormEdit.SubFormsRelation.ParentFieldName',
)}
>
{({ field, form }: FieldProps<SelectBoxOption>) => (
<Select
field={field}
form={form}
isClearable={true}
options={parentFieldOptions}
value={parentFieldOptions.find(
(option) => option.value === item?.parentFieldName,
)}
onChange={(option) => {
const selectedField = parentFields.find(
(parentField) =>
parentField.fieldName === option?.value,
)
form.setFieldValue(field.name, option?.value ?? '')
form.setFieldValue(
`relation.${index}.dbType`,
selectedField?.sourceDbType ?? '',
)
}}
/>
)}
</Field>
</div>
<div className="w-4/12 ml-2">
<Field
type="text"
autoComplete="off"
name={`relation.${index}.childFieldName`}
placeholder={translate(
'::ListForms.ListFormEdit.SubFormsRelation.ChildFieldName',
)}
>
{({ field, form }: FieldProps<SelectBoxOption>) => (
<Select
field={field}
form={form}
isClearable={true}
isLoading={isLoadingChildFields}
options={childFieldOptions}
value={childFieldOptions.find(
(option) => option.value === item?.childFieldName,
)}
onChange={(option) =>
form.setFieldValue(field.name, option?.value ?? '')
}
/>
)}
</Field>
</div>
<div className="w-3/12 ml-2">
<Field
type="text"
autoComplete="off"
name={`relation.${index}.dbType`}
placeholder={translate('::ListForms.ListFormEdit.FieldDbType')}
>
{({ field, form }: FieldProps<any>) => (
<Select
field={field}
form={form}
isClearable={true}
isDisabled={true}
options={dbSourceTypeOptions}
value={dbSourceTypeOptions?.find(
(option: any) => option.value === item?.dbType,
)}
onChange={(option) =>
form.setFieldValue(field.name, option?.value)
}
/>
)}
</Field>
</div>
<div className="w-1/12 ml-2">
<Button
shape="circle"
variant="plain"
type="button"
size="sm"
title="Add"
icon={<FaCalendarMinus />}
onClick={() => {
arrayHelpers.remove(index)
}}
/>
<Button
shape="circle"
variant="plain"
type="button"
size="sm"
title="Add"
icon={<FaCalendarPlus />}
onClick={() => {
arrayHelpers.insert(index + 1, {
parentFieldName: '',
childFieldName: '',
dbType: '',
})
}}
/>
</div>
</div>
))
) : (
<Button
shape="circle"
variant="plain"
type="button"
size="sm"
title="Add"
icon={<FaCalendarPlus />}
onClick={() => {
arrayHelpers.push({
parentFieldName: '',
childFieldName: '',
dbType: '',
})
}} }}
/> />
)} )}
</Field> </div>
</FormItem> )}
/>
<FormItem </FormItem>
label="Tab Type" </div>
invalid={errors.tabType && touched.tabType} </Dialog.Body>
errorMessage={errors.tabType} <Dialog.Footer className="flex justify-end gap-2 border-t pt-3 mt-1">
> <Button variant="plain" onClick={handleClose}>
<Field type="text" autoComplete="off" name="tabType" placeholder="Tab Type"> Cancel
{({ field, form }: FieldProps<SelectBoxOption>) => ( </Button>
<Select <Button variant="solid" loading={isSubmitting} type="submit">
field={field} {isSubmitting ? translate('::SavingWithThreeDot') : translate('::Save')}
form={form} </Button>
isClearable={true} </Dialog.Footer>
options={tabTypeOptions} </Form>
value={tabTypeOptions?.filter( )}
(option: any) => option.value === values.tabType, </Formik>
)}
onChange={(option) => form.setFieldValue(field.name, option?.value)}
/>
)}
</Field>
</FormItem>
<FormItem
label="isRefresh"
invalid={errors.isRefresh && touched.isRefresh}
errorMessage={errors.isRefresh}
>
<Field name="isRefresh" component={Checkbox} />
</FormItem>
</div>
<FormItem label="Relations">
<FieldArray
name="relation"
render={(arrayHelpers) => (
<div>
<div className="flex m-1 font-bold text-center">
<div className="w-4/12">Parent Field Name</div>
<div className="w-4/12">Child Field Name</div>
<div className="w-4/12">Db Type</div>
</div>
{values.relation && values.relation.length > 0 ? (
values.relation.map((item: SubFormRelationDto, index: any) => (
<div key={index} className="flex m-1">
<div className="w-4/12 ml-2">
<Field
type="text"
autoComplete="off"
name={`relation.${index}.parentFieldName`}
placeholder={translate('::ListForms.ListFormEdit.SubFormsRelation.ParentFieldName')}
>
{({ field, form }: FieldProps<SelectBoxOption>) => (
<Select
field={field}
form={form}
isClearable={true}
options={parentFieldOptions}
value={parentFieldOptions.find(
(option) => option.value === item?.parentFieldName,
)}
onChange={(option) => {
const selectedField = parentFields.find(
(parentField) => parentField.fieldName === option?.value,
)
form.setFieldValue(field.name, option?.value ?? '')
form.setFieldValue(
`relation.${index}.dbType`,
selectedField?.sourceDbType ?? '',
)
}}
/>
)}
</Field>
</div>
<div className="w-4/12 ml-2">
<Field
type="text"
autoComplete="off"
name={`relation.${index}.childFieldName`}
placeholder={translate('::ListForms.ListFormEdit.SubFormsRelation.ChildFieldName')}
>
{({ field, form }: FieldProps<SelectBoxOption>) => (
<Select
field={field}
form={form}
isClearable={true}
isLoading={isLoadingChildFields}
options={childFieldOptions}
value={childFieldOptions.find(
(option) => option.value === item?.childFieldName,
)}
onChange={(option) =>
form.setFieldValue(field.name, option?.value ?? '')
}
/>
)}
</Field>
</div>
<div className="w-3/12 ml-2">
<Field
type="text"
autoComplete="off"
name={`relation.${index}.dbType`}
placeholder={translate('::ListForms.ListFormEdit.FieldDbType')}
>
{({ field, form }: FieldProps<any>) => (
<Select
field={field}
form={form}
isClearable={true}
isDisabled={true}
options={dbSourceTypeOptions}
value={dbSourceTypeOptions?.find(
(option: any) =>
option.value === item?.dbType,
)}
onChange={(option) =>
form.setFieldValue(field.name, option?.value)
}
/>
)}
</Field>
</div>
<div className="w-1/12 ml-2">
<Button
shape="circle"
variant="plain"
type="button"
size="sm"
title="Add"
icon={<FaCalendarMinus />}
onClick={() => {
arrayHelpers.remove(index)
}}
/>
<Button
shape="circle"
variant="plain"
type="button"
size="sm"
title="Add"
icon={<FaCalendarPlus />}
onClick={() => {
arrayHelpers.insert(index + 1, {
parentFieldName: '',
childFieldName: '',
dbType: '',
})
}}
/>
</div>
</div>
))
) : (
<Button
shape="circle"
variant="plain"
type="button"
size="sm"
title="Add"
icon={<FaCalendarPlus />}
onClick={() => {
arrayHelpers.push({
parentFieldName: '',
childFieldName: '',
dbType: '',
})
}}
/>
)}
</div>
)}
/>
</FormItem>
</div>
</Dialog.Body>
<Dialog.Footer className="flex justify-end gap-2 border-t pt-3 mt-1">
<Button variant="plain" onClick={handleClose}>
Cancel
</Button>
<Button variant="solid" loading={isSubmitting} type="submit">
{isSubmitting ? translate('::SavingWithThreeDot') : translate('::Save')}
</Button>
</Dialog.Footer>
</Form>
)}
</Formik>
)} )}
{data.operation === 'delete' && ( {data.operation === 'delete' && (
<Formik <Formik
initialValues={data} initialValues={data}
onSubmit={async (values, { setSubmitting }) => { onSubmit={async (values, { setSubmitting }) => {
setSubmitting(true) setSubmitting(true)
try { try {
await deleteListFormJsonRow(data.id, data.tabName, values.index) await deleteListFormJsonRow(data.id, data.tabName, values.index)
toast.push(<Notification type="success">Kayıt silindi </Notification>) toast.push(<Notification type="success">Kayıt silindi </Notification>)
handleClose() handleClose()
} catch (error: any) { } catch (error: any) {
toast.push( toast.push(
<Notification type="danger"> <Notification type="danger">
Hata Hata
<code>{error}</code> <code>{error}</code>
</Notification>, </Notification>,
{ placement: 'top-end' }, { placement: 'top-end' },
) )
} finally { } finally {
setSubmitting(false) setSubmitting(false)
} }
// getListFormJsonRow() // getListFormJsonRow()
}} }}
> >
{({ isSubmitting }) => ( {({ isSubmitting }) => (
<Form className="flex flex-col h-full"> <Form className="flex flex-col h-full">
<Dialog.Body className="flex flex-col gap-2"> <Dialog.Body className="flex flex-col gap-2">
<h5>Delete</h5> <h5>Delete</h5>
<p>Silmek istediğinize emin misiniz?</p> <p>Silmek istediğinize emin misiniz?</p>
</Dialog.Body> </Dialog.Body>
<Dialog.Footer className="flex justify-end gap-2 border-t pt-3 mt-1"> <Dialog.Footer className="flex justify-end gap-2 border-t pt-3 mt-1">
<Button variant="plain" onClick={handleClose}> <Button variant="plain" onClick={handleClose}>
Cancel Cancel
</Button> </Button>
<Button variant="solid" loading={isSubmitting} type="submit"> <Button variant="solid" loading={isSubmitting} type="submit">
{isSubmitting ? 'Deleting' : 'Delete'} {isSubmitting ? 'Deleting' : 'Delete'}
</Button> </Button>
</Dialog.Footer> </Dialog.Footer>
</Form> </Form>
)} )}
</Formik> </Formik>
)} )}
</Dialog> </Dialog>
) )

View file

@ -23,13 +23,22 @@ import WizardStep1, {
} from './WizardStep1' } from './WizardStep1'
import WizardStep2 from './WizardStep2' import WizardStep2 from './WizardStep2'
import WizardStep3, { WizardGroup, WizardGroupItem } from './WizardStep3' import WizardStep3, { WizardGroup, WizardGroupItem } from './WizardStep3'
import WizardStep7 from './WizardStep7'
import WizardStep4 from './WizardStep4' import WizardStep4 from './WizardStep4'
import WizardStep5 from './WizardStep5'
import WizardStep6 from './WizardStep6'
import { Container } from '@/components/shared' import { Container } from '@/components/shared'
import { sqlDataTypeToDbType } from '../edit/options' import { sqlDataTypeToDbType } from '../edit/options'
import { useStoreActions } from '@/store/store' import { useStoreActions } from '@/store/store'
import { deleteWizardFile, getWizardFile, postListFormWizard } from '@/services/wizard.service' import { deleteWizardFile, getWizardFile, postListFormWizard } from '@/services/wizard.service'
import { UiLookupDataSourceTypeEnum } from '@/proxy/form/models' import { UiLookupDataSourceTypeEnum } from '@/proxy/form/models'
import { ListFormWizardDto } from '@/proxy/admin/wizard/models' import { ListFormWizardDto } from '@/proxy/admin/wizard/models'
import {
ListFormWorkflowCriteriaDto,
SubFormDto,
WidgetEditDto,
WorkflowDto,
} from '@/proxy/form/models'
// ─── Formik initial values & validation ────────────────────────────────────── // ─── Formik initial values & validation ──────────────────────────────────────
const initialValues: ListFormWizardDto = { const initialValues: ListFormWizardDto = {
@ -81,6 +90,16 @@ const initialValues: ListFormWizardDto = {
schedulerTextExpr: '', schedulerTextExpr: '',
schedulerStartDateExpr: '', schedulerStartDateExpr: '',
schedulerEndDateExpr: '', schedulerEndDateExpr: '',
subForms: [],
widgets: [],
workflow: {
approvalUserFieldName: '',
approvalDateFieldName: '',
approvalStatusFieldName: '',
approvalDescriptionFieldName: '',
criteria: [],
},
workflowCriteria: [],
} }
const step1ValidationSchema = Yup.object().shape({ const step1ValidationSchema = Yup.object().shape({
@ -136,7 +155,11 @@ const Wizard = () => {
const [isLoadingDbObjects, setIsLoadingDbObjects] = useState(false) const [isLoadingDbObjects, setIsLoadingDbObjects] = useState(false)
const [currentDataSource, setCurrentDataSource] = useState('') const [currentDataSource, setCurrentDataSource] = useState('')
// In edit mode, stores the selectCommand to load columns for once dbObjects is ready // In edit mode, stores the selectCommand to load columns for once dbObjects is ready
const pendingEditColumnsRef = useRef<{ dsCode: string; selectCommand: string; selectCommandType: SelectCommandTypeEnum } | null>(null) const pendingEditColumnsRef = useRef<{
dsCode: string
selectCommand: string
selectCommandType: SelectCommandTypeEnum
} | null>(null)
const loadDbObjects = async (dsCode: string) => { const loadDbObjects = async (dsCode: string) => {
if (!dsCode) { if (!dsCode) {
@ -194,6 +217,16 @@ const Wizard = () => {
// ── Editing Form Groups (Step 3) ── // ── Editing Form Groups (Step 3) ──
const [editingGroups, setEditingGroups] = useState<WizardGroup[]>([]) const [editingGroups, setEditingGroups] = useState<WizardGroup[]>([])
const [subForms, setSubForms] = useState<SubFormDto[]>([])
const [widgets, setWidgets] = useState<WidgetEditDto[]>([])
const [workflow, setWorkflow] = useState<WorkflowDto>({
approvalUserFieldName: '',
approvalDateFieldName: '',
approvalStatusFieldName: '',
approvalDescriptionFieldName: '',
criteria: [],
})
const [workflowCriteria, setWorkflowCriteria] = useState<ListFormWorkflowCriteriaDto[]>([])
// Audit columns that should not be selected by default // Audit columns that should not be selected by default
const AUDIT_COLUMNS = new Set([ const AUDIT_COLUMNS = new Set([
@ -378,6 +411,16 @@ const Wizard = () => {
schedulerTextExpr: w.schedulerTextExpr ?? '', schedulerTextExpr: w.schedulerTextExpr ?? '',
schedulerStartDateExpr: w.schedulerStartDateExpr ?? '', schedulerStartDateExpr: w.schedulerStartDateExpr ?? '',
schedulerEndDateExpr: w.schedulerEndDateExpr ?? '', schedulerEndDateExpr: w.schedulerEndDateExpr ?? '',
subForms: w.subForms ?? [],
widgets: w.widgets ?? [],
workflow: w.workflow ?? {
approvalUserFieldName: '',
approvalDateFieldName: '',
approvalStatusFieldName: '',
approvalDescriptionFieldName: '',
criteria: [],
},
workflowCriteria: w.workflowCriteria ?? w.workflow?.criteria ?? [],
}) })
// Queue column load to run once dbObjects is available // Queue column load to run once dbObjects is available
@ -428,6 +471,19 @@ const Wizard = () => {
const allFields = new Set(restoredGroups.flatMap((g) => g.items.map((i) => i.dataField))) const allFields = new Set(restoredGroups.flatMap((g) => g.items.map((i) => i.dataField)))
setSelectedColumns(allFields) setSelectedColumns(allFields)
} }
setSubForms(w.subForms ?? [])
setWidgets(w.widgets ?? [])
setWorkflow(
w.workflow ?? {
approvalUserFieldName: '',
approvalDateFieldName: '',
approvalStatusFieldName: '',
approvalDescriptionFieldName: '',
criteria: [],
},
)
setWorkflowCriteria(w.workflowCriteria ?? w.workflow?.criteria ?? [])
} catch { } catch {
toast.push( toast.push(
<Notification type="danger"> <Notification type="danger">
@ -582,6 +638,13 @@ const Wizard = () => {
} }
}), }),
})), })),
subForms,
widgets,
workflow: {
...workflow,
criteria: workflowCriteria,
},
workflowCriteria,
}) })
// ✅ sonra config çek // ✅ sonra config çek
@ -621,6 +684,9 @@ const Wizard = () => {
<Steps.Item <Steps.Item
title={translate('::ListForms.Wizard.ListFormFields') || 'List Form Fields'} 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('::App.Platform.Deploy') || 'Deploy'} /> <Steps.Item title={translate('::App.Platform.Deploy') || 'Deploy'} />
</Steps> </Steps>
</div> </div>
@ -635,128 +701,166 @@ const Wizard = () => {
innerRef={formikRef} innerRef={formikRef}
initialValues={{ ...initialValues }} initialValues={{ ...initialValues }}
validationSchema={listFormValidationSchema} validationSchema={listFormValidationSchema}
onSubmit={async (values, { setSubmitting }) => { onSubmit={async (values, { setSubmitting }) => {
setSubmitting(true) setSubmitting(true)
try { try {
// 🔴 1. Kaydet (bekle) // 🔴 1. Kaydet (bekle)
await postListFormWizard({ ...values }) await postListFormWizard({ ...values })
// 🔴 2. Config güncelle (bekle) // 🔴 2. Config güncelle (bekle)
await getConfig(true) await getConfig(true)
// 🔴 3. Navigate // 🔴 3. Navigate
navigate( navigate(
ROUTES_ENUM.protected.admin.list.replace(':listFormCode', values.listFormCode), ROUTES_ENUM.protected.admin.list.replace(':listFormCode', values.listFormCode),
{ replace: true }, { replace: true },
) )
// 🔴 4. Toast (istersen navigate öncesi de olabilir) // 🔴 4. Toast (istersen navigate öncesi de olabilir)
toast.push( toast.push(
<Notification type="success" duration={2000}> <Notification type="success" duration={2000}>
{translate('::ListForms.FormBilgileriKaydedildi')} {translate('::ListForms.FormBilgileriKaydedildi')}
</Notification>, </Notification>,
{ placement: 'top-end' }, { placement: 'top-end' },
) )
} catch (error: any) { } catch (error: any) {
toast.push(<Notification title={error.message} type="danger" />, { toast.push(<Notification title={error.message} type="danger" />, {
placement: 'top-end', placement: 'top-end',
}) })
} finally { } finally {
setSubmitting(false) setSubmitting(false)
} }
}} }}
> >
{({ touched, errors, isSubmitting, values }) => ( {({ touched, errors, isSubmitting, values }) => (
<Form> <Form>
<FormContainer <FormContainer size={currentStep >= 2 ? undefined : 'sm'}>
size={currentStep === 2 ? undefined : currentStep === 3 ? undefined : 'sm'} {/* ─── Step 1: Basic Info ─────────────────────────────── */}
> {currentStep === 0 && (
{/* ─── Step 1: Basic Info ─────────────────────────────── */} <WizardStep1
{currentStep === 0 && ( values={values}
<WizardStep1 errors={errors}
values={values} touched={touched}
errors={errors} wizardName={values.wizardName}
touched={touched} onWizardNameChange={handleWizardNameChange}
wizardName={values.wizardName} rawMenuItems={rawMenuItems}
onWizardNameChange={handleWizardNameChange} menuTree={menuTree}
rawMenuItems={rawMenuItems} isLoadingMenu={isLoadingMenu}
menuTree={menuTree} onMenuParentChange={handleMenuParentChange}
isLoadingMenu={isLoadingMenu} onClearMenuParent={() => formikRef.current?.setFieldValue('menuParentCode', '')}
onMenuParentChange={handleMenuParentChange} onReloadMenu={getMenuList}
onClearMenuParent={() => permissionGroupList={permissionGroupList}
formikRef.current?.setFieldValue('menuParentCode', '') isLoadingPermissionGroup={isLoadingPermissionGroup}
} onNext={handleNext}
onReloadMenu={getMenuList} translate={translate}
permissionGroupList={permissionGroupList} />
isLoadingPermissionGroup={isLoadingPermissionGroup} )}
onNext={handleNext}
translate={translate}
/>
)}
{/* ─── Step 2: Data Settings ───────────────────────────── */} {/* ─── Step 2: Data Settings ───────────────────────────── */}
{currentStep === 1 && ( {currentStep === 1 && (
<WizardStep2 <WizardStep2
values={values} values={values}
errors={errors} errors={errors}
touched={touched} touched={touched}
isLoadingDataSource={isLoadingDataSource} isLoadingDataSource={isLoadingDataSource}
dataSourceList={dataSourceList} dataSourceList={dataSourceList}
isDataSourceNew={isDataSourceNew} isDataSourceNew={isDataSourceNew}
onDataSourceSelect={setCurrentDataSource} onDataSourceSelect={setCurrentDataSource}
onDataSourceNewChange={setIsDataSourceNew} onDataSourceNewChange={setIsDataSourceNew}
dbObjects={dbObjects} dbObjects={dbObjects}
isLoadingDbObjects={isLoadingDbObjects} isLoadingDbObjects={isLoadingDbObjects}
selectCommandColumns={selectCommandColumns} selectCommandColumns={selectCommandColumns}
isLoadingColumns={isLoadingColumns} isLoadingColumns={isLoadingColumns}
selectedColumns={selectedColumns} selectedColumns={selectedColumns}
onLoadColumns={loadColumns} onLoadColumns={loadColumns}
onClearColumns={() => { onClearColumns={() => {
setSelectCommandColumns([]) setSelectCommandColumns([])
setSelectedColumns(new Set()) setSelectedColumns(new Set())
}} }}
onToggleColumn={toggleColumn} onToggleColumn={toggleColumn}
onToggleAllColumns={toggleAllColumns} onToggleAllColumns={toggleAllColumns}
translate={translate} translate={translate}
onBack={handleBack} onBack={handleBack}
onNext={handleNext2} onNext={handleNext2}
/> />
)} )}
{/* ─── Step 3: List Form Fields ───────────────────────────── */} {/* ─── Step 3: List Form Fields ───────────────────────────── */}
{currentStep === 2 && ( {currentStep === 2 && (
<WizardStep3 <WizardStep3
selectedColumns={selectedColumns} selectedColumns={selectedColumns}
selectCommandColumns={selectCommandColumns} selectCommandColumns={selectCommandColumns}
groups={editingGroups} groups={editingGroups}
onGroupsChange={setEditingGroups} onGroupsChange={setEditingGroups}
dbObjects={dbObjects} dbObjects={dbObjects}
isLoadingDbObjects={isLoadingDbObjects} isLoadingDbObjects={isLoadingDbObjects}
dsCode={currentDataSource} dsCode={currentDataSource}
translate={translate} translate={translate}
onBack={() => setCurrentStep(1)} onBack={() => setCurrentStep(1)}
onNext={() => setCurrentStep(3)} onNext={() => setCurrentStep(3)}
/> />
)} )}
{/* ─── Step 4: Deploy ───────────────────────────── */} {/* ─── Step 4: Sub Forms ───────────────────────────── */}
{currentStep === 3 && ( {currentStep === 3 && (
<WizardStep4 <WizardStep4
values={values} subForms={subForms}
wizardName={values.wizardName} selectCommandColumns={selectCommandColumns}
selectedColumns={selectedColumns} translate={translate}
selectCommandColumns={selectCommandColumns} onChange={setSubForms}
groups={editingGroups} onBack={() => setCurrentStep(2)}
translate={translate} onNext={() => setCurrentStep(4)}
onBack={() => setCurrentStep(2)} />
onSubmit={handleDeploy} )}
/>
)} {/* ─── Step 5: Widgets ───────────────────────────── */}
</FormContainer> {currentStep === 4 && (
</Form> <WizardStep5
)} widgets={widgets}
</Formik> translate={translate}
onChange={setWidgets}
onBack={() => setCurrentStep(3)}
onNext={() => setCurrentStep(5)}
/>
)}
{/* ─── Step 6: Workflow ───────────────────────────── */}
{currentStep === 5 && (
<WizardStep6
listFormCode={values.listFormCode}
workflow={workflow}
criteria={workflowCriteria}
selectCommandColumns={selectCommandColumns}
translate={translate}
onWorkflowChange={setWorkflow}
onCriteriaChange={setWorkflowCriteria}
onBack={() => setCurrentStep(4)}
onNext={() => setCurrentStep(6)}
/>
)}
{/* ─── Step 7: Deploy ───────────────────────────── */}
{currentStep === 6 && (
<WizardStep7
values={values}
wizardName={values.wizardName}
selectedColumns={selectedColumns}
selectCommandColumns={selectCommandColumns}
groups={editingGroups}
subForms={subForms}
widgets={widgets}
workflow={workflow}
workflowCriteria={workflowCriteria}
translate={translate}
onBack={() => setCurrentStep(5)}
onSubmit={handleDeploy}
/>
)}
</FormContainer>
</Form>
)}
</Formik>
</Container> </Container>
) )
} }

View file

@ -1,421 +1,475 @@
import { Button } from '@/components/ui' import { Button, Card, Checkbox, Dialog, FormItem, Input, Select, Table } from '@/components/ui'
import TBody from '@/components/ui/Table/TBody'
import THead from '@/components/ui/Table/THead'
import Td from '@/components/ui/Table/Td'
import Th from '@/components/ui/Table/Th'
import Tr from '@/components/ui/Table/Tr'
import type { ColumnFormatEditDto } from '@/proxy/admin/list-form-field/models'
import { DbTypeEnum, SubFormDto, SubFormRelationDto, SubFormTabTypeEnum } from '@/proxy/form/models'
import { getListFormFields } from '@/services/admin/list-form-field.service'
import { getListForms } from '@/services/admin/list-form.service'
import type { DatabaseColumnDto } from '@/proxy/sql-query-manager/models' import type { DatabaseColumnDto } from '@/proxy/sql-query-manager/models'
import { useState } from 'react' import type { SelectBoxOption } from '@/types/shared'
import { Field, FieldArray, FieldProps, Form, Formik } from 'formik'
import { useEffect, useState } from 'react'
import { import {
FaArrowLeft, FaArrowLeft,
FaCheckCircle, FaArrowRight,
FaChevronDown, FaCalendarMinus,
FaChevronRight, FaCalendarPlus,
FaCircle, FaEdit,
FaExclamationCircle, FaFileMedical,
FaRocket, FaTrash,
FaSpinner,
} from 'react-icons/fa' } from 'react-icons/fa'
import { WizardGroup } from './WizardStep3' import { object, string } from 'yup'
import { dbSourceTypeOptions, selectCommandTypeOptions } from '../edit/options' import { dbSourceTypeOptions, sqlDataTypeToDbType, tabTypeOptions } from '../edit/options'
import { ListFormWizardDto } from '@/proxy/admin/wizard/models'
// ─── Types ──────────────────────────────────────────────────────────────────── type Props = {
subForms: SubFormDto[]
export interface WizardStep4Props {
values: ListFormWizardDto
wizardName: string
selectedColumns: Set<string>
selectCommandColumns: DatabaseColumnDto[] selectCommandColumns: DatabaseColumnDto[]
groups: WizardGroup[]
translate: (key: string) => string translate: (key: string) => string
onChange: (subForms: SubFormDto[]) => void
onBack: () => void onBack: () => void
onSubmit: () => Promise<void> onNext: () => void
} }
type LogStatus = 'pending' | 'running' | 'success' | 'error' const schema = object().shape({
tabTitle: string().required(),
tabType: string().required(),
code: string().required(),
})
interface LogEntry { const emptySubForm: SubFormDto = {
id: number tabTitle: '',
label: string tabType: SubFormTabTypeEnum.List,
status: LogStatus code: '',
detail?: string isRefresh: false,
relation: [],
tabMode: 'edit',
searchParams: new URLSearchParams(),
} }
// ─── Deploy log steps ───────────────────────────────────────────────────────── function WizardStep4({
subForms,
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"
onClick={() => setOpen((v) => !v)}
className="w-full flex items-center justify-between px-4 py-2.5 bg-gray-50 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-750 transition-colors"
>
<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>
)
}
// ─── WizardStep4 ──────────────────────────────────────────────────────────────
const WizardStep4 = ({
values,
wizardName,
selectedColumns,
selectCommandColumns, selectCommandColumns,
groups,
translate, translate,
onChange,
onBack, onBack,
onSubmit, onNext,
}: WizardStep4Props) => { }: Props) {
const [logs, setLogs] = useState<LogEntry[]>([]) const [dialogIndex, setDialogIndex] = useState<number | null>(null)
const [isDeploying, setIsDeploying] = useState(false) const [deleteIndex, setDeleteIndex] = useState<number | null>(null)
const [isDone, setIsDone] = useState(false) const [listFormOptions, setListFormOptions] = useState<SelectBoxOption[]>([])
const [hasError, setHasError] = useState(false) const [childFields, setChildFields] = useState<ColumnFormatEditDto[]>([])
const [isLoadingListForms, setIsLoadingListForms] = useState(false)
const [isLoadingChildFields, setIsLoadingChildFields] = useState(false)
const steps = buildLogSteps(values, groups, translate) const parentFieldOptions = selectCommandColumns.map((column) => ({
value: column.columnName,
label: `${column.columnName} (${column.dataType})`,
}))
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)) const childFieldOptions = childFields
.filter((field) => field.fieldName)
.map((field) => ({
value: field.fieldName,
label: `${field.fieldName} (${field.dataType})`,
}))
const runDeploy = async () => { const loadChildFields = async (listFormCode?: string) => {
if (isDeploying) return if (!listFormCode) {
setIsDeploying(true) setChildFields([])
setHasError(false) return
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 setIsLoadingChildFields(true)
const deployStep = steps[steps.length - 2]
setStatus(deployStep.id, 'running')
try { try {
await onSubmit() const resp = await getListFormFields({
setStatus(deployStep.id, 'success') listFormCode,
await sleep(200) sorting: 'ListOrderNo',
// Step 9: done maxResultCount: 1000,
const doneStep = steps[steps.length - 1] })
setStatus(doneStep.id, 'running') setChildFields(resp.data?.items ?? [])
await sleep(300) } catch {
setStatus(doneStep.id, 'success') setChildFields([])
setIsDone(true)
} catch (err: any) {
setStatus(deployStep.id, 'error')
setLogs((prev) => [
...prev,
{
id: 999,
label: `Hata: ${err?.message ?? 'Bilinmeyen hata'}`,
status: 'error',
},
])
setHasError(true)
} finally { } finally {
setIsDeploying(false) setIsLoadingChildFields(false)
} }
} }
const totalFields = groups.reduce((acc, g) => acc + g.items.length, 0) useEffect(() => {
if (dialogIndex === null) return
const loadListForms = async () => {
setIsLoadingListForms(true)
try {
const resp = await getListForms({ sorting: 'ListFormCode', maxResultCount: 1000 })
setListFormOptions(
(resp.data?.items ?? [])
.filter((item) => item.listFormCode)
.map((item) => ({
value: item.listFormCode,
label: `${item.listFormCode}${item.title ? ` - ${translate(`::${item.title}`)}` : ''}`,
})),
)
} catch {
setListFormOptions([])
} finally {
setIsLoadingListForms(false)
}
}
loadListForms()
if (dialogIndex !== -1) {
loadChildFields(subForms[dialogIndex]?.code)
} else {
setChildFields([])
}
}, [dialogIndex])
const upsertSubForm = (values: SubFormDto) => {
const next = [...subForms]
if (dialogIndex === -1) next.push(values)
else if (dialogIndex !== null) next[dialogIndex] = values
onChange(next)
setDialogIndex(null)
}
const removeSubForm = () => {
if (deleteIndex === null) return
onChange(subForms.filter((_, index) => index !== deleteIndex))
setDeleteIndex(null)
}
return ( return (
<div className="grid grid-cols-[3fr_2fr] gap-5 pb-24 items-start"> <div className="flex h-[calc(100vh-250px)] min-h-[500px] flex-col overflow-hidden">
{/* ── Left: Summary ──────────────────────────────────────────── */} <div className="flex-1 overflow-y-auto pr-1">
<div className="flex flex-col gap-3"> <Card
<div className="grid grid-cols-2 gap-3 items-start"> bodyClass="p-0"
<Section title={translate('::ListForms.Wizard.Step4.MenuInfo')}> header={translate('::ListForms.ListFormEdit.SubForms')}
<Row label={translate('::ListForms.Wizard.Step4.WizardName')} value={wizardName} /> headerExtra={translate('::ListForms.ListFormEdit.SubFormsDescription')}
<Row label={translate('::ListForms.Wizard.Step4.MenuCode')} value={values.menuCode} /> >
<Row label={translate('::ListForms.Wizard.Step4.MenuParent')} value={values.menuParentCode} /> <Table compact>
<Row label={translate('::ListForms.Wizard.Step4.PermissionGroup')} value={values.permissionGroupName} /> <THead>
<Row label={translate('::App.Listform.ListformField.Icon')} value={values.menuIcon} /> <Tr>
<Row label={translate('::ListForms.Wizard.Step4.MenuTr')} value={values.languageTextMenuTr} /> <Th className="text-center min-w-[100px]">
<Row label={translate('::ListForms.Wizard.Step4.MenuEn')} value={values.languageTextMenuEn} /> <Button
<Row label={translate('::ListForms.Wizard.Step4.MenuParentTr')} value={values.languageTextMenuParentTr} /> shape="circle"
<Row label={translate('::ListForms.Wizard.Step4.MenuParentEn')} value={values.languageTextMenuParentEn} /> variant="plain"
</Section> type="button"
size="sm"
<Section title={translate('::ListForms.Wizard.Step4.ListFormSettings')}> title="Add"
<Row label={translate('::ListForms.Wizard.Step4.ListFormCode')} value={values.listFormCode} /> icon={<FaFileMedical />}
<Row label={translate('::ListForms.Wizard.Step4.TitleTr')} value={values.languageTextTitleTr} /> onClick={() => setDialogIndex(-1)}
<Row label={translate('::ListForms.Wizard.Step4.TitleEn')} value={values.languageTextTitleEn} /> />
<Row label={translate('::ListForms.Wizard.Step4.DescTr')} value={values.languageTextDescTr} /> </Th>
<Row label={translate('::ListForms.Wizard.Step4.DescEn')} value={values.languageTextDescEn} /> <Th>{translate('::ListForms.ListFormEdit.SubFormsTabTitle')}</Th>
<Row label={translate('::ListForms.Wizard.Step4.DataSource')} value={values.dataSourceCode} /> <Th>{translate('::ListForms.ListFormEdit.SubFormsTabType')}</Th>
<Row label={translate('::App.Listform.ListformField.ConnectionString')} value={values.dataSourceConnectionString} /> <Th>{translate('::App.Listform.ListformField.ListFormCode')}</Th>
<Row <Th>{translate('::ListForms.ListFormEdit.SubFormsIsRefresh')}</Th>
label={translate('::ListForms.Wizard.Step4.CommandType')} <Th>{translate('::ListForms.ListFormEdit.SubFormsRelation')}</Th>
value={ </Tr>
selectCommandTypeOptions.find((o) => o.value === values.selectCommandType)?.label || </THead>
values.selectCommandType <TBody>
} {subForms.map((row, index) => (
/> <Tr key={`${row.code}-${index}`}>
<Row label={translate('::ListForms.Wizard.Step4.SelectCommand')} value={values.selectCommand} /> <Td>
<Row label={translate('::ListForms.Wizard.Step4.KeyField')} value={values.keyFieldName} /> <div className="flex items-center gap-2">
<Row <Button
label={translate('::ListForms.Wizard.Step4.KeyFieldType')} shape="circle"
value={ variant="plain"
dbSourceTypeOptions.find((o: any) => o.value === values.keyFieldDbSourceType) type="button"
?.label ?? String(values.keyFieldDbSourceType) size="sm"
} title="Edit"
/> icon={<FaEdit />}
</Section> onClick={() => setDialogIndex(index)}
</div> />
<Button
<Section title={translate('::ListForms.Wizard.Step4.SelectedColumns')} badge={selectedColumns.size}> shape="circle"
<div className="flex flex-wrap gap-1.5"> variant="plain"
{[...selectedColumns].map((col) => { type="button"
const meta = selectCommandColumns.find((c) => c.columnName === col) size="sm"
return ( title="Delete"
<span icon={<FaTrash />}
key={col} onClick={() => setDeleteIndex(index)}
className="inline-flex items-center gap-1 px-2 py-0.5 text-xs rounded-full bg-indigo-50 dark:bg-indigo-900/30 text-indigo-700 dark:text-indigo-300 border border-indigo-200 dark:border-indigo-700" />
>
{col}
{meta?.dataType && (
<span className="text-[10px] text-indigo-400 opacity-70">{meta.dataType}</span>
)}
</span>
)
})}
</div>
</Section>
<Section title={translate('::ListForms.Wizard.Step4.FormGroups')} badge={groups.length}>
<div className="flex flex-col gap-3">
{groups.map((g) => (
<Section
key={g.id}
title={g.caption || `(${translate('::ListForms.Wizard.Step4.StatGroup')})`}
badge={`${g.items.length} ${translate('::ListForms.Wizard.Step4.StatField')} · ${g.colCount} ${translate('::App.Listform.ListformField.Column')}`}
defaultOpen={false}
>
<div className="grid grid-cols-2 gap-2">
{g.items.length === 0 ? (
<span className="text-xs text-gray-300 italic">{translate('::ListForms.Wizard.Step4.NoFields') || 'Alan yok'}</span>
) : (
g.items.map((item) => (
<div
key={item.id}
className="flex items-center gap-2 py-1 border-b border-gray-100 dark:border-gray-800 last:border-0"
>
<span className="text-xs font-medium text-indigo-600 dark:text-indigo-400 w-36 shrink-0 truncate">
{item.dataField}
</span>
<span className="text-[10px] text-gray-400 bg-gray-100 dark:bg-gray-800 px-1.5 py-0.5 rounded">
{item.editorType}
</span>
<span className="text-[10px] text-gray-400 mr-auto shrink-0 bg-gray-100 dark:bg-gray-800 px-1.5 py-0.5 rounded">
col-span-{item.colSpan}
{item.isRequired && (
<span className="ml-1 text-red-400 font-semibold">*</span>
)}
</span>
</div>
))
)}
</div>
</Section>
))}
</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-3 gap-2">
{[
{ label: translate('::ListForms.Wizard.Step4.StatGroup'), value: groups.length },
{ label: translate('::ListForms.Wizard.Step4.StatField'), value: totalFields },
{ label: translate('::App.Listform.ListformField.Column'), value: selectedColumns.size },
].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') || 'Başarılı'}
</span>
)}
{hasError && (
<span className="text-xs text-red-500 font-semibold flex items-center gap-1">
<FaExclamationCircle /> {translate('::ListForms.Wizard.Step4.Error') || 'Hata'}
</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') || 'Tüm bilgiler hazır.'}
<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>
</div> </Td>
))} <Td>{row.tabTitle}</Td>
{isDone && ( <Td>{row.tabType}</Td>
<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"> <Td>{row.code}</Td>
🎉 {translate('::ListForms.Wizard.Step4.DeploySuccess')} <Td>{row.isRefresh ? 'Active' : 'Inactive'}</Td>
</div> <Td>
)} {(row.relation || []).map((item) => (
</div> <div key={`${item.parentFieldName}-${item.childFieldName}`}>
)} {item.parentFieldName} : {item.childFieldName}
</div> </div>
</div> ))}
</Td>
</Tr>
))}
</TBody>
</Table>
</Card>
</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-4 py-2 min-h-16 flex items-center">
<div className="fixed bottom-0 left-0 right-0 z-10 bg-white dark:bg-gray-900 border-t border-gray-200 dark:border-gray-700 px-6 py-0 h-16 flex items-center"> <div className="flex flex-wrap items-center gap-2 w-full">
<div className="flex items-center gap-3 w-full"> <Button size="sm" variant="default" type="button" icon={<FaArrowLeft />} onClick={onBack}>
<Button
size='sm'
variant="default"
type="button"
icon={<FaArrowLeft />}
onClick={onBack}
disabled={isDeploying}
>
{translate('::Back') || 'Back'} {translate('::Back') || 'Back'}
</Button> </Button>
<div className="flex-1 flex items-center justify-end"> <div className="flex-1 flex items-center justify-end gap-3">
<Button <Button
size='sm' size="sm"
variant="solid" variant="solid"
type="button" type="button"
icon={<FaRocket />} icon={<FaArrowRight />}
loading={isDeploying} onClick={onNext}
disabled={isDeploying || isDone}
onClick={runDeploy}
> >
{isDeploying ? translate('::ListForms.Wizard.Step4.Log.Deploying') : isDone ? `${translate('::ListForms.Wizard.Step4.Log.Completed')}` : translate('::ListForms.Wizard.Step4.DeployAndSave')} {translate('::Next') || 'Next'}
</Button> </Button>
</div> </div>
</div> </div>
</div> </div>
<Dialog
width={900}
height="90vh"
isOpen={dialogIndex !== null}
preventScroll
onClose={() => setDialogIndex(null)}
onRequestClose={() => setDialogIndex(null)}
>
{dialogIndex !== null && (
<Formik
initialValues={dialogIndex === -1 ? emptySubForm : subForms[dialogIndex]}
validationSchema={schema}
onSubmit={upsertSubForm}
>
{({ values, errors, touched }) => (
<Form className="flex flex-col h-full">
<Dialog.Body className="flex flex-col gap-3 overflow-y-auto">
<h5>{dialogIndex === -1 ? 'Add' : 'Update'}</h5>
<div className="grid grid-cols-4 gap-4">
<FormItem
label="Tab Title"
invalid={errors.tabTitle && touched.tabTitle}
errorMessage={errors.tabTitle}
>
<Field name="tabTitle" component={Input} />
</FormItem>
<FormItem
label="Code"
invalid={errors.code && touched.code}
errorMessage={errors.code}
>
<Field name="code">
{({ field, form }: FieldProps<SelectBoxOption>) => (
<Select
field={field}
form={form}
isClearable
isLoading={isLoadingListForms}
options={listFormOptions}
value={listFormOptions.find((option) => option.value === values.code)}
onChange={(option) => {
form.setFieldValue(field.name, option?.value ?? '')
form.setFieldValue('relation', [])
loadChildFields(option?.value)
}}
/>
)}
</Field>
</FormItem>
<FormItem
label="Tab Type"
invalid={errors.tabType && touched.tabType}
errorMessage={errors.tabType}
>
<Field name="tabType">
{({ field, form }: FieldProps) => (
<Select
field={field}
form={form}
options={tabTypeOptions}
value={tabTypeOptions.find((option) => option.value === values.tabType)}
onChange={(option: any) =>
form.setFieldValue(field.name, option?.value)
}
/>
)}
</Field>
</FormItem>
<FormItem label="isRefresh">
<Field name="isRefresh" component={Checkbox} />
</FormItem>
</div>
<FormItem label="Relations">
<FieldArray
name="relation"
render={(arrayHelpers) => (
<div>
<div className="flex m-1 font-bold text-center">
<div className="w-4/12">Parent Field Name</div>
<div className="w-4/12">Child Field Name</div>
<div className="w-3/12">Db Type</div>
<div className="w-1/12" />
</div>
{values.relation && values.relation.length > 0 ? (
values.relation.map((item: SubFormRelationDto, index) => (
<div key={index} className="flex m-1">
<div className="w-4/12 ml-2">
<Field name={`relation.${index}.parentFieldName`}>
{({ field, form }: FieldProps<SelectBoxOption>) => (
<Select
field={field}
form={form}
isClearable
options={parentFieldOptions}
value={parentFieldOptions.find(
(option) => option.value === item.parentFieldName,
)}
onChange={(option) => {
const column = selectCommandColumns.find(
(c) => c.columnName === option?.value,
)
form.setFieldValue(field.name, option?.value ?? '')
form.setFieldValue(
`relation.${index}.dbType`,
column
? sqlDataTypeToDbType(column.dataType)
: DbTypeEnum.String,
)
}}
/>
)}
</Field>
</div>
<div className="w-4/12 ml-2">
<Field name={`relation.${index}.childFieldName`}>
{({ field, form }: FieldProps<SelectBoxOption>) => (
<Select
field={field}
form={form}
isClearable
isLoading={isLoadingChildFields}
options={childFieldOptions}
value={childFieldOptions.find(
(option) => option.value === item.childFieldName,
)}
onChange={(option) =>
form.setFieldValue(field.name, option?.value ?? '')
}
/>
)}
</Field>
</div>
<div className="w-3/12 ml-2">
<Field name={`relation.${index}.dbType`}>
{({ field, form }: FieldProps) => (
<Select
field={field}
form={form}
isClearable
isDisabled
options={dbSourceTypeOptions}
value={dbSourceTypeOptions.find(
(option: any) => option.value === item.dbType,
)}
onChange={(option: any) =>
form.setFieldValue(field.name, option?.value)
}
/>
)}
</Field>
</div>
<div className="w-1/12 ml-2 flex items-center">
<Button
shape="circle"
variant="plain"
type="button"
size="sm"
title="Remove"
icon={<FaCalendarMinus />}
onClick={() => arrayHelpers.remove(index)}
/>
<Button
shape="circle"
variant="plain"
type="button"
size="sm"
title="Add"
icon={<FaCalendarPlus />}
onClick={() => {
arrayHelpers.insert(index + 1, {
parentFieldName: '',
childFieldName: '',
dbType: DbTypeEnum.String,
})
}}
/>
</div>
</div>
))
) : (
<Button
shape="circle"
variant="plain"
type="button"
size="sm"
title="Add"
icon={<FaCalendarPlus />}
onClick={() => {
arrayHelpers.push({
parentFieldName: '',
childFieldName: '',
dbType: DbTypeEnum.String,
})
}}
/>
)}
</div>
)}
/>
</FormItem>
</Dialog.Body>
<Dialog.Footer className="flex justify-end gap-2">
<Button type="button" variant="plain" onClick={() => setDialogIndex(null)}>
Cancel
</Button>
<Button type="submit" variant="solid">
{translate('::Save')}
</Button>
</Dialog.Footer>
</Form>
)}
</Formik>
)}
</Dialog>
<Dialog
isOpen={deleteIndex !== null}
onClose={() => setDeleteIndex(null)}
onRequestClose={() => setDeleteIndex(null)}
>
<Dialog.Body>
<h5>Delete</h5>
<p>Silmek istediğinize emin misiniz?</p>
</Dialog.Body>
<Dialog.Footer className="flex justify-end gap-2">
<Button variant="plain" onClick={() => setDeleteIndex(null)}>
Cancel
</Button>
<Button variant="solid" onClick={removeSubForm}>
Delete
</Button>
</Dialog.Footer>
</Dialog>
</div> </div>
) )
} }

View file

@ -0,0 +1,355 @@
import {
Button,
Card,
Checkbox,
Dialog,
FormItem,
Input,
Select,
Table,
Tooltip,
} from '@/components/ui'
import TBody from '@/components/ui/Table/TBody'
import THead from '@/components/ui/Table/THead'
import Td from '@/components/ui/Table/Td'
import Th from '@/components/ui/Table/Th'
import Tr from '@/components/ui/Table/Tr'
import { WidgetEditDto } from '@/proxy/form/models'
import SqlEditor from '@/views/developerKit/SqlEditor'
import { Field, FieldProps, Form, Formik } from 'formik'
import { useState } from 'react'
import { FaArrowLeft, FaArrowRight, FaEdit, FaFileMedical, FaTrash } from 'react-icons/fa'
import { number, object, string } from 'yup'
import { colSpanOptions } from '../edit/options'
type Props = {
widgets: WidgetEditDto[]
translate: (key: string) => string
onChange: (widgets: WidgetEditDto[]) => void
onBack: () => void
onNext: () => void
}
const schema = object().shape({
colGap: number().required(),
colSpan: number().required(),
sqlQuery: string().required(),
title: string().required(),
value: string().required(),
})
const emptyWidget: WidgetEditDto = {
colGap: 3,
colSpan: 3,
sqlQuery:
"SELECT 'Total Records' as title, COUNT(*) as value, 'blue' as color, 'FaChartBar' as icon, 'Active records' as subTitle FROM YourTable",
className: 'mb-3',
valueClassName: 'text-3xl',
title: 'title',
value: 'value',
color: 'color',
icon: 'icon',
subTitle: 'subTitle',
onClick: '',
isActive: true,
}
function WizardStep5({ widgets, translate, onChange, onBack, onNext }: Props) {
const [dialogIndex, setDialogIndex] = useState<number | null>(null)
const [deleteIndex, setDeleteIndex] = useState<number | null>(null)
const upsertWidget = (values: WidgetEditDto) => {
const next = [...widgets]
if (dialogIndex === -1) next.push(values)
else if (dialogIndex !== null) next[dialogIndex] = values
onChange(next)
setDialogIndex(null)
}
const removeWidget = () => {
if (deleteIndex === null) return
onChange(widgets.filter((_, index) => index !== deleteIndex))
setDeleteIndex(null)
}
return (
<div className="flex h-[calc(100vh-250px)] min-h-[500px] flex-col overflow-hidden">
<div className="flex-1 overflow-y-auto pr-1">
<Card
bodyClass="p-0"
header={translate('::ListForms.ListFormEdit.TabWidgets')}
headerExtra={translate('::ListForms.ListFormEdit.WidgetsDescription')}
>
<Table compact>
<THead>
<Tr>
<Th className="text-center min-w-[100px]">
<Button
shape="circle"
variant="plain"
type="button"
size="sm"
title="Add"
icon={<FaFileMedical />}
onClick={() => setDialogIndex(-1)}
/>
</Th>
<Th>{translate('::ListForms.ListFormEdit.WidgetColGap')}</Th>
<Th>{translate('::ListForms.ListFormEdit.EditingFormColumnSpan')}</Th>
<Th>{translate('::ListForms.ListFormEdit.SqlQuery')}</Th>
<Th>{translate('::ListForms.ListFormEdit.WidgetClassName')}</Th>
<Th>{translate('::ListForms.ListFormEdit.WidgetValueClassName')}</Th>
<Th>{translate('::Abp.Identity.User.LockoutManagement.Status')}</Th>
</Tr>
</THead>
<TBody>
{widgets.map((row, index) => (
<Tr key={index}>
<Td>
<div className="flex items-center gap-2">
<Button
shape="circle"
variant="plain"
type="button"
size="sm"
title="Edit"
icon={<FaEdit />}
onClick={() => setDialogIndex(index)}
/>
<Button
shape="circle"
variant="plain"
type="button"
size="sm"
title="Delete"
icon={<FaTrash />}
onClick={() => setDeleteIndex(index)}
/>
</div>
</Td>
<Td>{row.colGap}</Td>
<Td>{row.colSpan}</Td>
<Td className="max-w-[360px] truncate">{row.sqlQuery}</Td>
<Td>{row.className}</Td>
<Td>{row.valueClassName}</Td>
<Td>{row.isActive ? 'Active' : 'Inactive'}</Td>
</Tr>
))}
</TBody>
</Table>
</Card>
</div>
<div className="fixed bottom-0 left-0 right-0 z-10 bg-white dark:bg-gray-900 border-t border-gray-200 dark:border-gray-700 px-4 py-2 min-h-16 flex items-center">
<div className="flex flex-wrap items-center gap-2 w-full">
<Button size="sm" variant="default" type="button" icon={<FaArrowLeft />} onClick={onBack}>
{translate('::Back') || 'Back'}
</Button>
<div className="flex-1 flex items-center justify-end gap-3">
<Button
size="sm"
variant="solid"
type="button"
icon={<FaArrowRight />}
onClick={onNext}
>
{translate('::Next') || 'Next'}
</Button>
</div>
</div>
</div>
<Dialog
width={900}
height="90vh"
isOpen={dialogIndex !== null}
preventScroll
onClose={() => setDialogIndex(null)}
onRequestClose={() => setDialogIndex(null)}
>
{dialogIndex !== null && (
<Formik
initialValues={dialogIndex === -1 ? emptyWidget : widgets[dialogIndex]}
validationSchema={schema}
onSubmit={upsertWidget}
>
{({ values, errors, touched, setFieldValue }) => (
<Form className="flex flex-col h-full">
<Dialog.Body className="flex flex-col gap-3 overflow-y-auto">
<h5>{dialogIndex === -1 ? 'Add' : 'Update'}</h5>
<div className="grid grid-cols-5 gap-3">
<FormItem
label="Column Gap"
invalid={errors.colGap && touched.colGap}
errorMessage={errors.colGap}
>
<Field type="number" name="colGap" component={Input} />
</FormItem>
<FormItem
label="Column Span"
invalid={errors.colSpan && touched.colSpan}
errorMessage={errors.colSpan}
>
<Field name="colSpan">
{({ field, form }: FieldProps) => (
<Select
field={field}
form={form}
options={colSpanOptions}
value={colSpanOptions.find(
(option: any) => option.value === values.colSpan,
)}
onChange={(option: any) =>
form.setFieldValue(field.name, option?.value)
}
/>
)}
</Field>
</FormItem>
<Tooltip
title={
<div className="text-xs">
<div className="font-semibold mb-1">Widget Container CSS Classes</div>
<div>Examples: mb-3, mt-2, p-4, rounded-lg, shadow-md</div>
</div>
}
>
<FormItem label="Class Name">
<Field name="className" component={Input} />
</FormItem>
</Tooltip>
<Tooltip
title={
<div className="text-xs">
<div className="font-semibold mb-1">Value Display CSS Classes</div>
<div>Examples: text-3xl, text-2xl, font-bold, text-sm</div>
</div>
}
>
<FormItem label="Value Class Name">
<Field name="valueClassName" component={Input} />
</FormItem>
</Tooltip>
<FormItem label="IsActive">
<Field name="isActive" component={Checkbox} />
</FormItem>
</div>
<Tooltip
title={
<div className="text-xs max-h-96 overflow-y-auto">
<div className="font-semibold mb-2">SQL Query Examples:</div>
<code className="block rounded bg-gray-800 p-2 text-xs text-white">
SELECT 'Aktif' as title, COUNT(Id) as value, 'blue' as color, 'FaChartBar'
as icon, 'Aktif kayitlar' as subTitle FROM YourTable
</code>
</div>
}
>
<span className="text-sm font-semibold text-gray-700 dark:text-gray-200">
Sql Query
</span>
</Tooltip>
<FormItem
invalid={errors.sqlQuery && touched.sqlQuery}
errorMessage={errors.sqlQuery}
>
<div className="h-[25vh] min-h-[180px] overflow-hidden rounded-lg border border-gray-200 dark:border-gray-700">
<SqlEditor
value={values.sqlQuery || ''}
onChange={(value) => setFieldValue('sqlQuery', value || '')}
/>
</div>
</FormItem>
<div className="grid grid-cols-5 gap-3">
<FormItem
label="Title Field"
invalid={errors.title && touched.title}
errorMessage={errors.title}
>
<Field name="title" component={Input} />
</FormItem>
<FormItem
label="Value Field"
invalid={errors.value && touched.value}
errorMessage={errors.value}
>
<Field name="value" component={Input} />
</FormItem>
<Tooltip
title={
<div className="grid grid-cols-2 gap-1 text-xs">
<div>blue</div>
<div>green</div>
<div>purple</div>
<div>gray</div>
<div>red</div>
<div>yellow</div>
<div>teal</div>
<div>orange</div>
</div>
}
>
<FormItem label="Color Field">
<Field name="color" component={Input} />
</FormItem>
</Tooltip>
<Tooltip
title={
<div className="text-xs">
<div className="font-semibold mb-1">Popular Font Awesome icons</div>
<div>FaChartBar, FaChartLine, FaUsers, FaShoppingCart, FaBell</div>
</div>
}
>
<FormItem label="Icon Field">
<Field name="icon" component={Input} />
</FormItem>
</Tooltip>
<FormItem label="Sub Title Field">
<Field name="subTitle" component={Input} />
</FormItem>
</div>
<FormItem label="On Click">
<Field name="onClick" component={Input} />
</FormItem>
</Dialog.Body>
<Dialog.Footer className="flex justify-end gap-2">
<Button type="button" variant="plain" onClick={() => setDialogIndex(null)}>
Cancel
</Button>
<Button type="submit" variant="solid">
{translate('::Save')}
</Button>
</Dialog.Footer>
</Form>
)}
</Formik>
)}
</Dialog>
<Dialog
isOpen={deleteIndex !== null}
onClose={() => setDeleteIndex(null)}
onRequestClose={() => setDeleteIndex(null)}
>
<Dialog.Body>
<h5>Delete</h5>
<p>Silmek istediğinize emin misiniz?</p>
</Dialog.Body>
<Dialog.Footer className="flex justify-end gap-2">
<Button variant="plain" onClick={() => setDeleteIndex(null)}>
Cancel
</Button>
<Button variant="solid" onClick={removeWidget}>
Delete
</Button>
</Dialog.Footer>
</Dialog>
</div>
)
}
export default WizardStep5

View file

@ -0,0 +1,368 @@
import { Button, Card, FormContainer, FormItem, Select } from '@/components/ui'
import type { DatabaseColumnDto } from '@/proxy/sql-query-manager/models'
import { ListFormWorkflowCriteriaDto, WorkflowDto } from '@/proxy/form/models'
import { getUsers } from '@/services/identity.service'
import type { WorkflowCriteriaDto } from '@/services/workflow.service'
import { SelectBoxOption } from '@/types/shared'
import {
buildFitLayout,
emptyCriteria,
normalizeCriteria,
toCriteriaForm,
type WorkflowCriteriaForm,
} from '@/utils/workflow/workflowHelpers'
import { Field, FieldProps, Form, Formik } from 'formik'
import { useEffect, useMemo, useRef, useState } from 'react'
import type { FormEvent } from 'react'
import { FaArrowLeft, FaArrowRight } from 'react-icons/fa'
import { WorkflowDesigner } from '../workflow/WorkflowDesigner'
type Props = {
listFormCode: string
workflow: WorkflowDto
criteria: ListFormWorkflowCriteriaDto[]
selectCommandColumns: DatabaseColumnDto[]
translate: (key: string) => string
onWorkflowChange: (workflow: WorkflowDto) => void
onCriteriaChange: (criteria: ListFormWorkflowCriteriaDto[]) => void
onBack: () => void
onNext: () => void
}
type PendingLink = {
sourceId: string
outcome: string
} | null
const toDesignerCriteria = (items: ListFormWorkflowCriteriaDto[]): WorkflowCriteriaDto[] =>
items.map((item) => ({
...item,
nodeId: item.id,
compareOutcomes: item.compareOutcomes || [],
}))
const toWizardCriteria = (items: WorkflowCriteriaDto[]): ListFormWorkflowCriteriaDto[] =>
items.map(({ nodeId: _nodeId, ...item }) => item)
const nextId = () => `WF${Date.now()}${Math.floor(Math.random() * 1000)}`
function WizardStep6({
listFormCode,
workflow,
criteria,
selectCommandColumns,
translate,
onWorkflowChange,
onCriteriaChange,
onBack,
onNext,
}: Props) {
const [userList, setUserList] = useState<SelectBoxOption[]>([])
const [selectedId, setSelectedId] = useState('')
const [pendingLink, setPendingLink] = useState<PendingLink>(null)
const [criteriaForm, setCriteriaForm] = useState<WorkflowCriteriaForm>(
emptyCriteria('Start', listFormCode),
)
const [dragPreview, setDragPreview] = useState<any>(null)
const [canvasZoom, setCanvasZoom] = useState(1)
const [designerTab, setDesignerTab] = useState('flow')
const canvasRef = useRef<HTMLDivElement | null>(null)
const currentCriteria = useMemo(() => toDesignerCriteria(criteria), [criteria])
const columnOptions: SelectBoxOption[] = selectCommandColumns.map((column) => ({
value: column.columnName,
label: `${column.columnName} (${column.dataType})`,
}))
useEffect(() => {
getUsers(0, 1000).then((response) => {
setUserList(
(response.data?.items ?? []).map((user: any) => ({
value: user.userName,
label: `${user.userName} (${user.name} ${user.surname})`,
})),
)
})
}, [])
useEffect(() => {
const selected = currentCriteria.find((item) => item.id === selectedId)
setCriteriaForm(selected ? toCriteriaForm(selected) : emptyCriteria('Start', listFormCode))
}, [currentCriteria, listFormCode, selectedId])
const updateCriteria = (next: WorkflowCriteriaDto[]) => onCriteriaChange(toWizardCriteria(next))
const saveCriteria = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault()
const normalized = normalizeCriteria({ ...criteriaForm, listFormCode })
const id = normalized.id || nextId()
const nextItem = { ...normalized, id, nodeId: id } as WorkflowCriteriaDto
const exists = currentCriteria.some((item) => item.id === id)
updateCriteria(
exists
? currentCriteria.map((item) => (item.id === id ? nextItem : item))
: [...currentCriteria, nextItem],
)
setSelectedId(id)
setDesignerTab('flow')
}
const addCriteria = (kind: string) => {
const id = nextId()
const nextItem = {
...normalizeCriteria(emptyCriteria(kind, listFormCode)),
id,
nodeId: id,
positionX: 80 + (currentCriteria.length % 5) * 230,
positionY: 220 + Math.floor(currentCriteria.length / 5) * 140,
} as WorkflowCriteriaDto
updateCriteria([...currentCriteria, nextItem])
setSelectedId(id)
}
const deleteCriteria = (criteriaId: string = selectedId) => {
if (!criteriaId) return
updateCriteria(
currentCriteria
.filter((item) => item.id !== criteriaId)
.map((item) => ({
...item,
nextOnStart: item.nextOnStart === criteriaId ? '' : item.nextOnStart,
nextOnTrue: item.nextOnTrue === criteriaId ? '' : item.nextOnTrue,
nextOnFalse: item.nextOnFalse === criteriaId ? '' : item.nextOnFalse,
nextOnApprove: item.nextOnApprove === criteriaId ? '' : item.nextOnApprove,
nextOnReject: item.nextOnReject === criteriaId ? '' : item.nextOnReject,
compareOutcomes: (item.compareOutcomes || []).map((outcome) => ({
...outcome,
targetId: outcome.targetId === criteriaId ? null : outcome.targetId,
})),
})),
)
setSelectedId('')
}
const disconnectLink = (sourceId: string, outcome: string) => {
updateCriteria(
currentCriteria.map((item) => {
if (item.id !== sourceId) return item
if (outcome.startsWith('compareOutcomes:')) {
const index = Number(outcome.split(':')[1])
const compareOutcomes = [...(item.compareOutcomes || [])]
if (compareOutcomes[index])
compareOutcomes[index] = { ...compareOutcomes[index], targetId: null }
return { ...item, compareOutcomes }
}
return { ...item, [outcome]: '' }
}),
)
setPendingLink(null)
}
const connectNodes = (sourceId: string, outcome: string, targetId: string) => {
if (sourceId === targetId) return
updateCriteria(
currentCriteria.map((item) => {
if (item.id !== sourceId) return item
if (outcome.startsWith('compareOutcomes:')) {
const index = Number(outcome.split(':')[1])
const compareOutcomes = [...(item.compareOutcomes || [])]
compareOutcomes[index] = { ...compareOutcomes[index], targetId }
return {
...item,
compareOutcomes,
nextOnTrue: index === 0 ? targetId : item.nextOnTrue,
nextOnFalse: index === 1 ? targetId : item.nextOnFalse,
}
}
return { ...item, [outcome]: targetId }
}),
)
setPendingLink(null)
}
const fitFlowLayout = () => {
const positions = buildFitLayout(currentCriteria)
updateCriteria(
currentCriteria.map((item) => {
const position = positions.get(item.id)
return position ? { ...item, positionX: position.x, positionY: position.y } : item
}),
)
setCanvasZoom(1)
}
const resetDemo = () => {
const startId = nextId()
const approvalId = nextId()
const endId = nextId()
updateCriteria([
{
...normalizeCriteria(emptyCriteria('Start', listFormCode)),
id: startId,
nodeId: startId,
nextOnStart: approvalId,
positionX: 72,
positionY: 160,
} as WorkflowCriteriaDto,
{
...normalizeCriteria(emptyCriteria('Approval', listFormCode)),
id: approvalId,
nodeId: approvalId,
nextOnApprove: endId,
positionX: 360,
positionY: 160,
} as WorkflowCriteriaDto,
{
...normalizeCriteria(emptyCriteria('End', listFormCode)),
id: endId,
nodeId: endId,
positionX: 650,
positionY: 160,
} as WorkflowCriteriaDto,
])
}
return (
<div className="flex h-[calc(100vh-250px)] min-h-[500px] flex-col overflow-hidden">
<div className="flex-1 overflow-y-auto pr-1">
<Formik
initialValues={workflow}
enableReinitialize
onSubmit={(values) => onWorkflowChange({ ...values, criteria })}
>
{({ values }) => (
<Form>
<Card className="mb-4" header={translate('::ListForms.ListFormEdit.TabWorkflow')}>
<FormContainer>
<div className="grid grid-cols-4 gap-4">
{[
[
'approvalUserFieldName',
'::ListForms.ListFormEdit.Workflow.ApprovalUserFieldName',
],
[
'approvalStatusFieldName',
'::ListForms.ListFormEdit.Workflow.ApprovalStatusFieldName',
],
[
'approvalDateFieldName',
'::ListForms.ListFormEdit.Workflow.ApprovalDateFieldName',
],
[
'approvalDescriptionFieldName',
'::ListForms.ListFormEdit.Workflow.ApprovalDescriptionFieldName',
],
].map(([name, label]) => (
<FormItem key={name} label={translate(label)}>
<Field name={name}>
{({ field, form }: FieldProps) => (
<Select
field={field}
form={form}
isClearable
options={columnOptions}
value={columnOptions.find(
(option) => option.value === (values as any)[name],
)}
onChange={(option: any) => {
form.setFieldValue(field.name, option?.value ?? '')
onWorkflowChange({
...values,
[field.name]: option?.value ?? '',
criteria,
})
}}
/>
)}
</Field>
</FormItem>
))}
</div>
</FormContainer>
</Card>
</Form>
)}
</Formik>
<WorkflowDesigner
busy={false}
canvasRef={canvasRef}
canvasZoom={canvasZoom}
criteriaForm={criteriaForm}
currentCriteria={currentCriteria}
designerTab={designerTab}
dragPreview={dragPreview}
pendingLink={pendingLink}
selectedCriteriaId={selectedId}
userList={userList}
selectCommandColumns={selectCommandColumns}
isLoadingSelectCommandColumns={false}
onAddCriteria={addCriteria}
onBeginLink={(sourceId, outcome) => {
setPendingLink({ sourceId, outcome })
setSelectedId(sourceId)
}}
onChangeCriteriaForm={setCriteriaForm}
onClearSelection={() => {
setPendingLink(null)
setSelectedId('')
}}
onConnect={connectNodes}
onDeleteCriteria={deleteCriteria}
onDeleteLink={disconnectLink}
onDragMove={(event: any) =>
setDragPreview(event ? { id: event.active.id, delta: event.delta } : null)
}
onFitLayout={fitFlowLayout}
onOpenDetails={(id) => {
setSelectedId(id)
setDesignerTab('criteria')
}}
onResetDemo={resetDemo}
onSaveCriteria={saveCriteria}
onSelectCriteria={setSelectedId}
onSetDesignerTab={setDesignerTab}
onUpdateNodePosition={(event: any) => {
setDragPreview(null)
updateCriteria(
currentCriteria.map((item) =>
item.id === event.active.id
? {
...item,
positionX: Math.max(12, Math.round(item.positionX + event.delta.x)),
positionY: Math.max(12, Math.round(item.positionY + event.delta.y)),
}
: item,
),
)
}}
onZoomIn={() => setCanvasZoom((value) => Math.min(1.6, Number((value + 0.1).toFixed(2))))}
onZoomOut={() =>
setCanvasZoom((value) => Math.max(0.6, Number((value - 0.1).toFixed(2))))
}
/>
</div>
<div className="fixed bottom-0 left-0 right-0 z-10 bg-white dark:bg-gray-900 border-t border-gray-200 dark:border-gray-700 px-4 py-2 min-h-16 flex items-center">
<div className="flex flex-wrap items-center gap-2 w-full">
<Button size="sm" variant="default" type="button" icon={<FaArrowLeft />} onClick={onBack}>
{translate('::Back') || 'Back'}
</Button>
<div className="flex-1 flex items-center justify-end gap-3">
<Button
size="sm"
variant="solid"
type="button"
icon={<FaArrowRight />}
onClick={onNext}
>
{translate('::Next') || 'Next'}
</Button>
</div>
</div>
</div>
</div>
)
}
export default WizardStep6

View file

@ -0,0 +1,628 @@
import { Button } from '@/components/ui'
import type { DatabaseColumnDto } from '@/proxy/sql-query-manager/models'
import { 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'
// ─── Types ────────────────────────────────────────────────────────────────────
export interface WizardStep7Props {
values: ListFormWizardDto
wizardName: string
selectedColumns: Set<string>
selectCommandColumns: DatabaseColumnDto[]
groups: WizardGroup[]
subForms: SubFormDto[]
widgets: WidgetEditDto[]
workflow: WorkflowDto
workflowCriteria: 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"
onClick={() => setOpen((v) => !v)}
className="w-full flex items-center justify-between px-4 py-2.5 bg-gray-50 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-750 transition-colors"
>
<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>
)
}
// ─── WizardStep7 ──────────────────────────────────────────────────────────────
const WizardStep7 = ({
values,
wizardName,
selectedColumns,
selectCommandColumns,
groups,
subForms,
widgets,
workflow,
workflowCriteria,
translate,
onBack,
onSubmit,
}: WizardStep7Props) => {
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: `Hata: ${err?.message ?? 'Bilinmeyen hata'}`,
status: 'error',
},
])
setHasError(true)
} finally {
setIsDeploying(false)
}
}
const totalFields = groups.reduce((acc, g) => acc + g.items.length, 0)
const hasWorkflowFields = Boolean(
workflow.approvalUserFieldName ||
workflow.approvalDateFieldName ||
workflow.approvalStatusFieldName ||
workflow.approvalDescriptionFieldName,
)
const workflowItems = workflowCriteria.length > 0 ? workflowCriteria : (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.CommandType')}
value={
selectCommandTypeOptions.find((o) => o.value === values.selectCommandType)?.label ||
values.selectCommandType
}
/>
<Row
label={translate('::ListForms.Wizard.Step4.SelectCommand')}
value={values.selectCommand}
/>
<Row
label={translate('::ListForms.Wizard.Step4.KeyField')}
value={values.keyFieldName}
/>
<Row
label={translate('::ListForms.Wizard.Step4.KeyFieldType')}
value={
dbSourceTypeOptions.find((o: any) => o.value === values.keyFieldDbSourceType)
?.label ?? String(values.keyFieldDbSourceType)
}
/>
</Section>
</div>
<Section
title={translate('::ListForms.Wizard.Step4.SelectedColumns')}
badge={selectedColumns.size}
>
<div className="flex flex-wrap gap-1.5">
{[...selectedColumns].map((col) => {
const meta = selectCommandColumns.find((c) => c.columnName === col)
return (
<span
key={col}
className="inline-flex items-center gap-1 px-2 py-0.5 text-xs rounded-full bg-indigo-50 dark:bg-indigo-900/30 text-indigo-700 dark:text-indigo-300 border border-indigo-200 dark:border-indigo-700"
>
{col}
{meta?.dataType && (
<span className="text-[10px] text-indigo-400 opacity-70">{meta.dataType}</span>
)}
</span>
)
})}
</div>
</Section>
<Section title={translate('::ListForms.Wizard.Step4.FormGroups')} badge={groups.length}>
<div className="flex flex-col gap-3">
{groups.map((g) => (
<Section
key={g.id}
title={g.caption || `(${translate('::ListForms.Wizard.Step4.StatGroup')})`}
badge={`${g.items.length} ${translate('::ListForms.Wizard.Step4.StatField')} · ${g.colCount} ${translate('::App.Listform.ListformField.Column')}`}
defaultOpen={false}
>
<div className="grid grid-cols-2 gap-2">
{g.items.length === 0 ? (
<span className="text-xs text-gray-300 italic">
{translate('::ListForms.Wizard.Step4.NoFields') || 'Alan yok'}
</span>
) : (
g.items.map((item) => (
<div
key={item.id}
className="flex items-center gap-2 py-1 border-b border-gray-100 dark:border-gray-800 last:border-0"
>
<span className="text-xs font-medium text-indigo-600 dark:text-indigo-400 w-36 shrink-0 truncate">
{item.dataField}
</span>
<span className="text-[10px] text-gray-400 bg-gray-100 dark:bg-gray-800 px-1.5 py-0.5 rounded">
{item.editorType}
</span>
<span className="text-[10px] text-gray-400 mr-auto shrink-0 bg-gray-100 dark:bg-gray-800 px-1.5 py-0.5 rounded">
col-span-{item.colSpan}
{item.isRequired && (
<span className="ml-1 text-red-400 font-semibold">*</span>
)}
</span>
</div>
))
)}
</div>
</Section>
))}
</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} />
</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>
<div className="mt-1 text-[11px] text-gray-500 dark:text-gray-400">
{criteria.compareColumn} {criteria.compareOperator} {criteria.compareValue}
</div>
<div className="text-[11px] text-gray-500 dark:text-gray-400">
Approver: {criteria.approver}
</div>
</div>
))}
</div>
)}
</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-3 gap-2">
{[
{ label: translate('::ListForms.Wizard.Step4.StatGroup'), value: groups.length },
{ label: translate('::ListForms.Wizard.Step4.StatField'), value: totalFields },
{
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,
},
].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') || 'Başarılı'}
</span>
)}
{hasError && (
<span className="text-xs text-red-500 font-semibold flex items-center gap-1">
<FaExclamationCircle /> {translate('::ListForms.Wizard.Step4.Error') || 'Hata'}
</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') || 'Tüm bilgiler hazır.'}
<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 />}
onClick={onBack}
disabled={isDeploying}
>
{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 WizardStep7