Todo komponenti

This commit is contained in:
Sedat ÖZTÜRK 2026-07-16 17:01:18 +03:00
parent 412ad236cc
commit 24e92fcb82
45 changed files with 3696 additions and 905 deletions

View file

@ -190,6 +190,19 @@ public class GridOptionsDto : AuditedEntityDto<Guid>
set { SchedulerOptionJson = JsonSerializer.Serialize(value); }
}
[JsonIgnore]
public string TodoOptionJson { get; set; }
public TodoOptionDto TodoOptionDto
{
get
{
if (!string.IsNullOrEmpty(TodoOptionJson))
return JsonSerializer.Deserialize<TodoOptionDto>(TodoOptionJson);
return new TodoOptionDto();
}
set { TodoOptionJson = JsonSerializer.Serialize(value); }
}
[JsonIgnore]
public string PagerOptionJson { get; set; }
public GridPagerOptionDto PagerOptionDto

View file

@ -9,6 +9,7 @@ public class LayoutDto
public bool Tree { get; set; } = true;
public bool Gantt { get; set; } = true;
public bool Scheduler { get; set; } = true;
public bool Todo { get; set; } = false;
public string DefaultLayout { get; set; } = "grid";
}

View file

@ -0,0 +1,20 @@
namespace Sozsoft.Platform.ListForms;
/// <summary>
/// Dinamik bir listeyi Todo/Kanban panosu olarak gostermek icin alan eslemeleri.
/// TitleExpr ve StatusExpr zorunlu, diger alanlar opsiyoneldir.
/// </summary>
public class TodoOptionDto
{
public string TitleExpr { get; set; }
public string StatusExpr { get; set; }
public string DescriptionExpr { get; set; }
public string DueDateExpr { get; set; }
public string TagExpr { get; set; }
public string AssigneeExpr { get; set; }
public string PriorityExpr { get; set; }
public string CompletedExpr { get; set; }
public string OrderExpr { get; set; }
public string StatusOrder { get; set; }
public bool AllowDragging { get; set; } = true;
}

View file

@ -5,10 +5,10 @@ namespace Sozsoft.Platform.ListForms;
public class WorkflowDto
{
public string ApprovalUserFieldName { get; set; }
public bool IsFilterUserName { get; set; }
public string ApprovalDateFieldName { get; set; }
public string ApprovalStatusFieldName { get; set; }
public string ApprovalDescriptionFieldName { get; set; }
public bool ApprovalIsFilterUserName { get; set; }
public List<ListFormWorkflowCriteriaDto> Criteria { get; set; } = [];
}

View file

@ -49,6 +49,7 @@ public class ListFormEditTabs
public const string TreeForm = "tree";
public const string GanttForm = "gantt";
public const string SchedulerForm = "scheduler";
public const string TodoForm = "todo";
public const string PagerForm = "pager";
public const string StateForm = "state";
public const string SubFormJsonRow = "subForm";

View file

@ -28,6 +28,7 @@ public class ListFormWizardDto
public bool Chart { get; set; }
public bool Gantt { get; set; }
public bool Scheduler { get; set; }
public bool Todo { get; set; }
public string LanguageTextMenuEn { get; set; }
public string LanguageTextMenuTr { get; set; }
@ -69,9 +70,28 @@ public class ListFormWizardDto
public string SchedulerStartDateExpr { get; set; }
public string SchedulerEndDateExpr { get; set; }
// Todo options (required when DefaultLayout = "todo")
public string TodoTitleExpr { get; set; }
public string TodoStatusExpr { get; set; }
public string TodoDescriptionExpr { get; set; }
public string TodoDueDateExpr { get; set; }
public string TodoTagExpr { get; set; }
public string TodoAssigneeExpr { get; set; }
public string TodoPriorityExpr { get; set; }
public string TodoCompletedExpr { get; set; }
public string TodoOrderExpr { get; set; }
public string TodoStatusOrder { get; set; }
public bool TodoAllowDragging { get; set; } = true;
// Workflow options (required when DefaultLayout = "workflow")
public string ApprovalUserFieldName { get; set; }
public string ApprovalDateFieldName { get; set; }
public string ApprovalStatusFieldName { get; set; }
public string ApprovalDescriptionFieldName { get; set; }
public bool ApprovalIsFilterUserName { get; set; }
public List<ListFormWorkflowCriteriaDto> Criteria { get; set; } = [];
public List<WizardColumnGroupInputDto> Groups { get; set; } = new();
public List<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

@ -166,6 +166,10 @@ public class ListFormsAppService : CrudAppService<
{
item.SchedulerOptionJson = JsonSerializer.Serialize(input.SchedulerOptionDto);
}
else if (input.EditType == ListFormEditTabs.TodoForm)
{
item.TodoOptionJson = JsonSerializer.Serialize(input.TodoOptionDto);
}
else if (input.EditType == ListFormEditTabs.PagerForm)
{
item.PageSize = input.PageSize;

View file

@ -269,10 +269,18 @@ public class ListFormWizardAppService(
var tableColumns = await GetTableColumnNamesAsync(input.DataSourceCode, input.SelectCommandType, input.SelectCommand);
var isDeleted = tableColumns.Contains("IsDeleted");
var isCreated = tableColumns.Contains("CreatorId");
input.Workflow ??= new WorkflowDto();
input.Workflow.Criteria = input.WorkflowCriteria;
EnsureUniqueWorkflowCriteriaTitles(input.WorkflowCriteria);
NormalizeWorkflowCriteriaIds(listFormCode, input.WorkflowCriteria);
input.Criteria ??= [];
EnsureUniqueWorkflowCriteriaTitles(input.Criteria);
NormalizeWorkflowCriteriaIds(listFormCode, input.Criteria);
var workflow = new WorkflowDto
{
ApprovalUserFieldName = input.ApprovalUserFieldName,
ApprovalIsFilterUserName = input.ApprovalIsFilterUserName,
ApprovalDateFieldName = input.ApprovalDateFieldName,
ApprovalStatusFieldName = input.ApprovalStatusFieldName,
ApprovalDescriptionFieldName = input.ApprovalDescriptionFieldName,
Criteria = input.Criteria,
};
await repoListForm.InsertAsync(new ListForm
{
@ -280,8 +288,8 @@ public class ListFormWizardAppService(
PageSize = 10,
ExportJson = WizardConsts.DefaultExportJson,
IsSubForm = false,
ShowNote = input.SubForms.Count > 0 || input.WorkflowCriteria.Count > 0,
LayoutJson = WizardConsts.DefaultLayoutJson(input.DefaultLayout, input.Grid, input.Card, input.Pivot, input.Tree, input.Chart, input.Gantt, input.Scheduler),
ShowNote = input.SubForms.Count > 0 || input.Criteria.Count > 0,
LayoutJson = WizardConsts.DefaultLayoutJson(input.DefaultLayout, input.Grid, input.Card, input.Pivot, input.Tree, input.Chart, input.Gantt, input.Scheduler, input.Todo),
CultureName = LanguageCodes.En,
ListFormCode = input.ListFormCode,
Name = nameLangKey,
@ -301,7 +309,7 @@ public class ListFormWizardAppService(
HeaderFilterJson = WizardConsts.DefaultHeaderFilterJson(),
SearchPanelJson = WizardConsts.DefaultSearchPanelJson(),
GroupPanelJson = WizardConsts.DefaultGroupPanelJson(),
SelectionJson = WizardConsts.DefaultSelectionSingleJson(input.WorkflowCriteria.Count > 0 ? PlatformConsts.GridOptions.SelectionModeSingle : PlatformConsts.GridOptions.SelectionModeNone),
SelectionJson = WizardConsts.DefaultSelectionSingleJson(input.Criteria.Count > 0 ? PlatformConsts.GridOptions.SelectionModeSingle : PlatformConsts.GridOptions.SelectionModeNone),
ColumnOptionJson = WizardConsts.DefaultColumnOptionJson(),
PermissionJson = WizardConsts.DefaultPermissionJson(code),
DeleteCommand = isDeleted ? WizardConsts.DefaultDeleteCommand(input.SelectCommand) : null,
@ -312,7 +320,7 @@ public class ListFormWizardAppService(
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,
WorkflowJson = HasWorkflow(workflow, input.Criteria) ? JsonSerializer.Serialize(workflow) : null,
TreeOptionJson = (input.Tree || input.DefaultLayout == "tree") && !string.IsNullOrEmpty(input.TreeParentIdExpr)
? JsonSerializer.Serialize(new TreeOptionDto
{
@ -341,6 +349,24 @@ public class ListFormWizardAppService(
EndDateExpr = input.SchedulerEndDateExpr,
})
: null,
TodoOptionJson = (input.Todo || input.DefaultLayout == "todo") &&
!string.IsNullOrEmpty(input.TodoTitleExpr) &&
!string.IsNullOrEmpty(input.TodoStatusExpr)
? JsonSerializer.Serialize(new TodoOptionDto
{
TitleExpr = input.TodoTitleExpr,
StatusExpr = input.TodoStatusExpr,
DescriptionExpr = input.TodoDescriptionExpr,
DueDateExpr = input.TodoDueDateExpr,
TagExpr = input.TodoTagExpr,
AssigneeExpr = input.TodoAssigneeExpr,
PriorityExpr = input.TodoPriorityExpr,
CompletedExpr = input.TodoCompletedExpr,
OrderExpr = input.TodoOrderExpr,
StatusOrder = input.TodoStatusOrder,
AllowDragging = input.TodoAllowDragging,
})
: null,
}, autoSave: true);
// ListFormField - each item in each group becomes a visible field record
@ -373,7 +399,7 @@ public class ListFormWizardAppService(
}
}
foreach (var criteria in input.WorkflowCriteria)
foreach (var criteria in input.Criteria)
{
if (string.IsNullOrWhiteSpace(criteria.Id))
{

View file

@ -9336,12 +9336,6 @@
"tr": "Seçili bölümün düzenlenebilir alanları burada gösterilir.",
"en": "Select a block on the page to edit its properties here."
},
{
"resourceName": "Platform",
"key": "Public.designer.save",
"tr": "Kaydet",
"en": "Save"
},
{
"resourceName": "Platform",
"key": "App.DeveloperKit.Component.Description",
@ -10170,12 +10164,6 @@
"en": "Save Entity",
"tr": "Varlığı Kaydet"
},
{
"resourceName": "Platform",
"key": "App.DeveloperKit.EntityEditor.Saving",
"en": "Saving...",
"tr": "Kaydediliyor..."
},
{
"resourceName": "Platform",
"key": "App.DeveloperKit.EntityEditor.BasicInfo",
@ -10308,18 +10296,6 @@
"en": "Inactive",
"tr": "Pasif"
},
{
"resourceName": "Platform",
"key": "App.DeveloperKit.ComponentEditor.Save",
"en": "Save",
"tr": "Kaydet"
},
{
"resourceName": "Platform",
"key": "App.DeveloperKit.ComponentEditor.Saving",
"en": "Saving...",
"tr": "Kaydediliyor..."
},
{
"resourceName": "Platform",
"key": "App.DeveloperKit.ComponentEditor.ValidationError.Title",
@ -11616,12 +11592,6 @@
"tr": "Hedef Data Sourcelar",
"en": "Target Data Sources"
},
{
"resourceName": "Platform",
"key": "App.Platform.Save",
"tr": "Kaydet",
"en": "Save"
},
{
"resourceName": "Platform",
"key": "App.Platform.SaveQuery",
@ -12324,12 +12294,6 @@
"tr": "Alan seçin...",
"en": "Select field..."
},
{
"resourceName": "Platform",
"key": "App.Platform.ChartDrawer.Save",
"tr": "Kaydet",
"en": "Save"
},
{
"resourceName": "Platform",
"key": "App.Platform.ChartDrawer.ChartSaved",
@ -18810,12 +18774,6 @@
"en": "If checked, existing object in the target will be dropped and recreated.",
"tr": "İşaretlenirse hedefteki mevcut obje silinip yeniden oluşturulur."
},
{
"resourceName": "Platform",
"key": "App.SqlQueryManager.Save",
"en": "Save",
"tr": "Kaydet"
},
{
"resourceName": "Platform",
"key": "App.SqlQueryManager.Next",
@ -19392,6 +19350,384 @@
"en": "Successfully Saved",
"tr": "Başarıyla Kaydedildi"
},
{
"resourceName": "Platform",
"key": "ListForms.ListFormFieldEdit.TodoTitleExpr",
"en": "Title Field",
"tr": "Başlık alanı"
},
{
"resourceName": "Platform",
"key": "ListForms.ListFormFieldEdit.TodoStatusExpr",
"en": "Column / Status Field",
"tr": "Kolon / durum alanı"
},
{
"resourceName": "Platform",
"key": "ListForms.ListFormFieldEdit.TodoDescriptionExpr",
"en": "Description Field",
"tr": "Açıklama alanı"
},
{
"resourceName": "Platform",
"key": "ListForms.ListFormFieldEdit.TodoDueDateExpr",
"en": "Due Date Field",
"tr": "Termin tarihi alanı"
},
{
"resourceName": "Platform",
"key": "ListForms.ListFormFieldEdit.TodoTagExpr",
"en": "Tag Field",
"tr": "Etiket alanı"
},
{
"resourceName": "Platform",
"key": "ListForms.ListFormFieldEdit.TodoAssigneeExpr",
"en": "Assignee Field",
"tr": "Atanan kişi alanı"
},
{
"resourceName": "Platform",
"key": "ListForms.ListFormFieldEdit.TodoPriorityExpr",
"en": "Priority Field",
"tr": "Öncelik alanı"
},
{
"resourceName": "Platform",
"key": "ListForms.ListFormFieldEdit.TodoCompletedExpr",
"en": "Completed Field",
"tr": "Tamamlandı alanı"
},
{
"resourceName": "Platform",
"key": "ListForms.ListFormFieldEdit.TodoOrderExpr",
"en": "Card Order Field",
"tr": "Kart sıra alanı"
},
{
"resourceName": "Platform",
"key": "ListForms.ListFormFieldEdit.TodoFieldsLoadFailed",
"en": "Fields could not be loaded: {0}",
"tr": "Alanlar getirilemedi: {0}"
},
{
"resourceName": "Platform",
"key": "ListForms.ListFormFieldEdit.TodoStatusOrder",
"en": "Status order (comma-separated)",
"tr": "Durum sırası (virgülle ayırın)"
},
{
"resourceName": "Platform",
"key": "ListForms.ListFormFieldEdit.TodoStatusOrderPlaceholder",
"en": "Inbox, Today, This Week, This Month, Later, Done",
"tr": "Gelen Kutusu, Bugün, Bu Hafta, Bu Ay, Daha Sonra, Tamamlandı"
},
{
"resourceName": "Platform",
"key": "ListForms.ListFormFieldEdit.TodoAllowDragging",
"en": "Allow cards to be dragged between columns",
"tr": "Kartları kolonlar arasında sürüklemeye izin ver"
},
{
"resourceName": "Platform",
"key": "ListForms.TodoBoard.Uncategorized",
"en": "Uncategorized",
"tr": "Kategorisiz"
},
{
"resourceName": "Platform",
"key": "ListForms.TodoBoard.PriorityRating",
"en": "{0}/5 priority",
"tr": "{0}/5 öncelik"
},
{
"resourceName": "Platform",
"key": "ListForms.TodoBoard.NoPriority",
"en": "No priority",
"tr": "Öncelik yok"
},
{
"resourceName": "Platform",
"key": "ListForms.TodoBoard.DuplicateColumnName",
"en": "A Kanban column with this name already exists.",
"tr": "Bu isimde bir kanban zaten var."
},
{
"resourceName": "Platform",
"key": "ListForms.TodoBoard.ColumnNotEmptyMessage",
"en": "This Kanban column cannot be deleted. First move its {0} todo item(s) to another column.",
"tr": "Bu kanban silinemez. Önce içindeki {0} todoyu başka bir kanbana taşıyın."
},
{
"resourceName": "Platform",
"key": "ListForms.TodoBoard.ConfirmDeleteColumn",
"en": "Delete the “{0}” Kanban column?",
"tr": "“{0}” kanbanı silinsin mi?"
},
{
"resourceName": "Platform",
"key": "ListForms.TodoBoard.RenameRequiresUpdateService",
"en": "An update service is required to rename a Kanban column that contains todo items.",
"tr": "Todo içeren bir kanbanı yeniden adlandırmak için update servisi gereklidir."
},
{
"resourceName": "Platform",
"key": "ListForms.TodoBoard.ColumnNameSaveFailed",
"en": "The Kanban column name could not be saved. Please try again.",
"tr": "Kanban adı kaydedilemedi. Lütfen tekrar deneyin."
},
{
"resourceName": "Platform",
"key": "ListForms.TodoBoard.ConfirmDeleteTodo",
"en": "Delete “{0}”?",
"tr": "“{0}” silinsin mi?"
},
{
"resourceName": "Platform",
"key": "ListForms.TodoBoard.Loading",
"en": "Loading…",
"tr": "Yükleniyor…"
},
{
"resourceName": "Platform",
"key": "ListForms.TodoBoard.BackToBoard",
"en": "Back to board",
"tr": "Panoya dön"
},
{
"resourceName": "Platform",
"key": "ListForms.TodoBoard.EditRequiresUpdateService",
"en": "An update service must be defined to edit.",
"tr": "Düzenlemek için update servisi tanımlanmalıdır."
},
{
"resourceName": "Platform",
"key": "ListForms.TodoBoard.DeleteTodo",
"en": "Delete todo",
"tr": "Todo sil"
},
{
"resourceName": "Platform",
"key": "ListForms.TodoBoard.Title",
"en": "Title",
"tr": "Başlık"
},
{
"resourceName": "Platform",
"key": "ListForms.TodoBoard.Description",
"en": "Description",
"tr": "Açıklama"
},
{
"resourceName": "Platform",
"key": "ListForms.TodoBoard.DescriptionPlaceholder",
"en": "Enter a todo description…",
"tr": "Todo açıklamasını yazın…"
},
{
"resourceName": "Platform",
"key": "ListForms.TodoBoard.Assignees",
"en": "Assignees",
"tr": "Atanan kişiler"
},
{
"resourceName": "Platform",
"key": "ListForms.TodoBoard.UsersLoading",
"en": "Loading users…",
"tr": "Kullanıcılar yükleniyor…"
},
{
"resourceName": "Platform",
"key": "ListForms.TodoBoard.SelectUser",
"en": "Select a user",
"tr": "Kullanıcı seçin"
},
{
"resourceName": "Platform",
"key": "ListForms.TodoBoard.Tags",
"en": "Tags",
"tr": "Etiketler"
},
{
"resourceName": "Platform",
"key": "ListForms.TodoBoard.TagsPlaceholder",
"en": "Select a tag or enter a new one",
"tr": "Etiket seçin veya yeni etiket yazın"
},
{
"resourceName": "Platform",
"key": "ListForms.TodoBoard.Status",
"en": "Kanban / Status",
"tr": "Kanban / Durum"
},
{
"resourceName": "Platform",
"key": "ListForms.TodoBoard.DueDate",
"en": "Due date",
"tr": "Termin tarihi"
},
{
"resourceName": "Platform",
"key": "ListForms.TodoBoard.Priority",
"en": "Priority",
"tr": "Öncelik"
},
{
"resourceName": "Platform",
"key": "ListForms.TodoBoard.PriorityVeryLow",
"en": "Very low",
"tr": "Çok düşük"
},
{
"resourceName": "Platform",
"key": "ListForms.TodoBoard.PriorityLow",
"en": "Low",
"tr": "Düşük"
},
{
"resourceName": "Platform",
"key": "ListForms.TodoBoard.PriorityMedium",
"en": "Medium",
"tr": "Orta"
},
{
"resourceName": "Platform",
"key": "ListForms.TodoBoard.PriorityHigh",
"en": "High",
"tr": "Yüksek"
},
{
"resourceName": "Platform",
"key": "ListForms.TodoBoard.PriorityUrgent",
"en": "Urgent",
"tr": "Acil"
},
{
"resourceName": "Platform",
"key": "ListForms.TodoBoard.Completed",
"en": "Completed",
"tr": "Tamamlandı"
},
{
"resourceName": "Platform",
"key": "ListForms.TodoBoard.UndoChanges",
"en": "Undo changes",
"tr": "Değişiklikleri geri al"
},
{
"resourceName": "Platform",
"key": "ListForms.TodoBoard.ReorderColumn",
"en": "Reorder Kanban column",
"tr": "Kanbanı sırala"
},
{
"resourceName": "Platform",
"key": "ListForms.TodoBoard.ColumnName",
"en": "Kanban column name",
"tr": "Kanban adı"
},
{
"resourceName": "Platform",
"key": "ListForms.TodoBoard.Cancel",
"en": "Cancel",
"tr": "Vazgeç"
},
{
"resourceName": "Platform",
"key": "ListForms.TodoBoard.RenameColumn",
"en": "Rename Kanban column",
"tr": "Kanban adını değiştir"
},
{
"resourceName": "Platform",
"key": "ListForms.TodoBoard.NewTodo",
"en": "New todo",
"tr": "Yeni todo"
},
{
"resourceName": "Platform",
"key": "ListForms.TodoBoard.ColumnWithTodosCannotBeDeleted",
"en": "A Kanban column containing todo items cannot be deleted",
"tr": "İçinde todo bulunan kanban silinemez"
},
{
"resourceName": "Platform",
"key": "ListForms.TodoBoard.DeleteColumn",
"en": "Delete Kanban column",
"tr": "Kanbanı sil"
},
{
"resourceName": "Platform",
"key": "ListForms.TodoBoard.Close",
"en": "Close",
"tr": "Kapat"
},
{
"resourceName": "Platform",
"key": "ListForms.TodoBoard.TodoTitlePlaceholder",
"en": "Todo title…",
"tr": "Todo başlığı…"
},
{
"resourceName": "Platform",
"key": "ListForms.TodoBoard.DropHere",
"en": "Drop here",
"tr": "Buraya bırak"
},
{
"resourceName": "Platform",
"key": "ListForms.TodoBoard.ChangePriority",
"en": "Change priority",
"tr": "Önceliği değiştir"
},
{
"resourceName": "Platform",
"key": "ListForms.TodoBoard.ReopenTodo",
"en": "Reopen todo",
"tr": "Todoyu yeniden aç"
},
{
"resourceName": "Platform",
"key": "ListForms.TodoBoard.CompleteTodo",
"en": "Complete todo",
"tr": "Todoyu tamamla"
},
{
"resourceName": "Platform",
"key": "ListForms.TodoBoard.NewColumn",
"en": "New Kanban column",
"tr": "Yeni kanban"
},
{
"resourceName": "Platform",
"key": "ListForms.TodoBoard.AddNewColumn",
"en": "Add a new Kanban column",
"tr": "Yeni kanban ekle"
},
{
"resourceName": "Platform",
"key": "ListForms.TodoBoard.NewColumnName",
"en": "New Kanban column name",
"tr": "Yeni kanban adı"
},
{
"resourceName": "Platform",
"key": "ListForms.TodoBoard.NewColumnPlaceholder",
"en": "E.g. In Review",
"tr": "Örn. İnceleniyor"
},
{
"resourceName": "Platform",
"key": "ListForms.TodoBoard.AddColumn",
"en": "Add Kanban column",
"tr": "Kanbanı ekle"
},
{
"resourceName": "Platform",
"key": "ListForms.ListFormEdit.TabTodo",
"en": "Todo",
"tr": "İşler"
},
{
"resourceName": "Platform",
"key": "ListForms.ListFormEdit.Workflow.Criteria",
@ -19466,7 +19802,7 @@
},
{
"resourceName": "Platform",
"key": "ListForms.ListFormEdit.Workflow.IsFilterUserName",
"key": "ListForms.ListFormEdit.Workflow.ApprovalIsFilterUserName",
"en": "Filter User Name?",
"tr": "Kullanıcı Adı Filtresin mi?"
},

View file

@ -73,6 +73,7 @@ public static class ListFormSeeder_DefaultJsons
Tree = true,
Gantt = true,
Scheduler = true,
Todo = true,
DefaultLayout = DefaultLayout,
});
public static readonly string DefaultExportJson = JsonSerializer.Serialize(new ChartExportDto

View file

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

View file

@ -0,0 +1,278 @@
{
"Wizard": {
"WizardName": "Todos",
"ListFormCode": "App.Wizard.Todos",
"MenuCode": "App.Wizard.Todos",
"IsTenant": false,
"IsBranch": false,
"IsOrganizationUnit": false,
"AllowAdding": true,
"AllowUpdating": true,
"AllowDeleting": true,
"AllowDetail": false,
"ConfirmDelete": true,
"DefaultLayout": "grid",
"Grid": true,
"Card": true,
"Pivot": true,
"Tree": true,
"Chart": true,
"Gantt": true,
"Scheduler": true,
"Todo": true,
"LanguageTextMenuEn": "Todos",
"LanguageTextMenuTr": "Todos",
"LanguageTextTitleEn": "Todos",
"LanguageTextTitleTr": "Todos",
"LanguageTextDescEn": "Todos",
"LanguageTextDescTr": "Todos",
"LanguageTextMenuParentEn": "",
"LanguageTextMenuParentTr": "",
"PermissionGroupName": "App.Administration",
"MenuParentCode": "App.Administration",
"MenuParentIcon": "FcOrganization",
"MenuIcon": "FcAndroidOs",
"DataSourceCode": "Default",
"DataSourceConnectionString": "",
"SelectCommandType": 1,
"SelectCommand": "Adm_D_Todos",
"KeyFieldName": "Id",
"KeyFieldDbSourceType": 9,
"TreeKeyExpr": "",
"TreeParentIdExpr": "",
"TreeAutoExpandAll": false,
"GanttKeyExpr": "",
"GanttParentIdExpr": "",
"GanttAutoExpandAll": false,
"GanttTitleExpr": "",
"GanttStartExpr": "",
"GanttEndExpr": "",
"GanttProgressExpr": "",
"SchedulerTextExpr": "",
"SchedulerStartDateExpr": "",
"SchedulerEndDateExpr": "",
"TodoTitleExpr": "Title",
"TodoStatusExpr": "Status",
"TodoDescriptionExpr": "Description",
"TodoDueDateExpr": "DueDate",
"TodoTagExpr": "Tags",
"TodoAssigneeExpr": "Assignees",
"TodoPriorityExpr": "Priority",
"TodoCompletedExpr": "Completed",
"TodoOrderExpr": "SortOrder",
"TodoStatusOrder": "Inbox, Today, This Week, This Month, Later, Done",
"TodoAllowDragging": true,
"ApprovalUserFieldName": "",
"ApprovalDateFieldName": "",
"ApprovalStatusFieldName": "",
"ApprovalDescriptionFieldName": "",
"ApprovalIsFilterUserName": false,
"Criteria": [],
"Groups": [
{
"Caption": "",
"ColCount": 1,
"Items": [
{
"DataField": "Id",
"CaptionName": "App.Listform.ListformField.Id",
"EditorType": "dxTextBox",
"EditorOptions": "",
"EditorScript": "",
"ColSpan": 1,
"IsRequired": true,
"IncludeInEditingForm": true,
"DbSourceType": 9,
"TurkishCaption": "Id",
"EnglishCaption": "Id",
"LookupDataSourceType": 1,
"ValueExpr": "Key",
"DisplayExpr": "Name",
"LookupQuery": ""
},
{
"DataField": "Title",
"CaptionName": "App.Listform.ListformField.Title",
"EditorType": "dxTextBox",
"EditorOptions": "",
"EditorScript": "",
"ColSpan": 1,
"IsRequired": true,
"IncludeInEditingForm": true,
"DbSourceType": 16,
"TurkishCaption": "Title",
"EnglishCaption": "Title",
"LookupDataSourceType": 1,
"ValueExpr": "Key",
"DisplayExpr": "Name",
"LookupQuery": ""
},
{
"DataField": "Status",
"CaptionName": "App.Listform.ListformField.Status",
"EditorType": "dxTextBox",
"EditorOptions": "",
"EditorScript": "",
"ColSpan": 1,
"IsRequired": true,
"IncludeInEditingForm": false,
"DbSourceType": 16,
"TurkishCaption": "Status",
"EnglishCaption": "Status",
"LookupDataSourceType": 1,
"ValueExpr": "Key",
"DisplayExpr": "Name",
"LookupQuery": ""
},
{
"DataField": "Description",
"CaptionName": "App.Listform.ListformField.Description",
"EditorType": "dxTextBox",
"EditorOptions": "",
"EditorScript": "",
"ColSpan": 1,
"IsRequired": false,
"IncludeInEditingForm": false,
"DbSourceType": 16,
"TurkishCaption": "Description",
"EnglishCaption": "Description",
"LookupDataSourceType": 1,
"ValueExpr": "Key",
"DisplayExpr": "Name",
"LookupQuery": ""
},
{
"DataField": "DueDate",
"CaptionName": "App.Listform.ListformField.DueDate",
"EditorType": "dxDateBox",
"EditorOptions": "",
"EditorScript": "",
"ColSpan": 1,
"IsRequired": false,
"IncludeInEditingForm": false,
"DbSourceType": 6,
"TurkishCaption": "Due Date",
"EnglishCaption": "Due Date",
"LookupDataSourceType": 1,
"ValueExpr": "Key",
"DisplayExpr": "Name",
"LookupQuery": ""
},
{
"DataField": "Tags",
"CaptionName": "App.Listform.ListformField.Tags",
"EditorType": "dxTextBox",
"EditorOptions": "",
"EditorScript": "",
"ColSpan": 1,
"IsRequired": false,
"IncludeInEditingForm": false,
"DbSourceType": 16,
"TurkishCaption": "Tags",
"EnglishCaption": "Tags",
"LookupDataSourceType": 1,
"ValueExpr": "Key",
"DisplayExpr": "Name",
"LookupQuery": ""
},
{
"DataField": "Assignees",
"CaptionName": "App.Listform.ListformField.Assignees",
"EditorType": "dxTextBox",
"EditorOptions": "",
"EditorScript": "",
"ColSpan": 1,
"IsRequired": false,
"IncludeInEditingForm": false,
"DbSourceType": 16,
"TurkishCaption": "Assignees",
"EnglishCaption": "Assignees",
"LookupDataSourceType": 1,
"ValueExpr": "Key",
"DisplayExpr": "Name",
"LookupQuery": ""
},
{
"DataField": "Priority",
"CaptionName": "App.Listform.ListformField.Priority",
"EditorType": "dxTextBox",
"EditorOptions": "",
"EditorScript": "",
"ColSpan": 1,
"IsRequired": false,
"IncludeInEditingForm": false,
"DbSourceType": 16,
"TurkishCaption": "Priority",
"EnglishCaption": "Priority",
"LookupDataSourceType": 1,
"ValueExpr": "Key",
"DisplayExpr": "Name",
"LookupQuery": ""
},
{
"DataField": "Completed",
"CaptionName": "App.Listform.ListformField.Completed",
"EditorType": "dxCheckBox",
"EditorOptions": "",
"EditorScript": "",
"ColSpan": 1,
"IsRequired": true,
"IncludeInEditingForm": false,
"DbSourceType": 3,
"TurkishCaption": "Completed",
"EnglishCaption": "Completed",
"LookupDataSourceType": 1,
"ValueExpr": "Key",
"DisplayExpr": "Name",
"LookupQuery": ""
},
{
"DataField": "SortOrder",
"CaptionName": "App.Listform.ListformField.SortOrder",
"EditorType": "dxNumberBox",
"EditorOptions": "",
"EditorScript": "",
"ColSpan": 1,
"IsRequired": true,
"IncludeInEditingForm": false,
"DbSourceType": 11,
"TurkishCaption": "Sort Order",
"EnglishCaption": "Sort Order",
"LookupDataSourceType": 1,
"ValueExpr": "Key",
"DisplayExpr": "Name",
"LookupQuery": ""
}
]
}
],
"SubForms": [],
"Widgets": []
},
"IsDeletedField": false,
"IsCreatedField": false,
"InsertedRecords": {
"LanguageKeys": [
"App.Wizard.Todos",
"App.Wizard.Todos.Title",
"App.Wizard.Todos.Desc",
"App.Listform.ListformField.Assignees",
"App.Listform.ListformField.Completed",
"App.Listform.ListformField.SortOrder"
],
"PermissionGroupNames": [],
"PermissionNames": [
"App.Wizard.Todos",
"App.Wizard.Todos.Create",
"App.Wizard.Todos.Update",
"App.Wizard.Todos.Delete",
"App.Wizard.Todos.Export",
"App.Wizard.Todos.Import",
"App.Wizard.Todos.Note"
],
"MenuCodes": [
"App.Wizard.Todos"
],
"DataSourceCodes": []
}
}

View file

@ -132,14 +132,17 @@ public class WizardDataSeeder : IDataSeedContributor, ITransientDependency
var isCreated = seedFile.IsCreatedField;
input.SubForms ??= new List<SubFormDto>();
input.Widgets ??= new List<WidgetEditDto>();
input.Workflow ??= new WorkflowDto();
input.WorkflowCriteria ??= new List<ListFormWorkflowCriteriaDto>();
if (input.WorkflowCriteria.Count == 0 && input.Workflow.Criteria?.Count > 0)
input.Criteria ??= [];
EnsureUniqueWorkflowCriteriaTitles(input.Criteria);
var workflow = new WorkflowDto
{
input.WorkflowCriteria = input.Workflow.Criteria;
}
input.Workflow.Criteria = input.WorkflowCriteria;
EnsureUniqueWorkflowCriteriaTitles(input.WorkflowCriteria);
ApprovalUserFieldName = input.ApprovalUserFieldName,
ApprovalIsFilterUserName = input.ApprovalIsFilterUserName,
ApprovalDateFieldName = input.ApprovalDateFieldName,
ApprovalStatusFieldName = input.ApprovalStatusFieldName,
ApprovalDescriptionFieldName = input.ApprovalDescriptionFieldName,
Criteria = input.Criteria,
};
var wizardName = input.WizardName.Trim();
var code = string.IsNullOrWhiteSpace(input.MenuCode)
@ -148,7 +151,7 @@ public class WizardDataSeeder : IDataSeedContributor, ITransientDependency
var listFormCode = string.IsNullOrWhiteSpace(input.ListFormCode)
? code
: input.ListFormCode.Trim();
NormalizeWorkflowCriteriaIds(listFormCode, input.WorkflowCriteria);
NormalizeWorkflowCriteriaIds(listFormCode, input.Criteria);
var titleLangKey = $"{listFormCode}.Title";
var nameLangKey = code;
var descLangKey = $"{listFormCode}.Desc";
@ -392,8 +395,8 @@ public class WizardDataSeeder : IDataSeedContributor, ITransientDependency
PageSize = 10,
ExportJson = WizardConsts.DefaultExportJson,
IsSubForm = false,
ShowNote = input.SubForms.Count > 0 || input.WorkflowCriteria.Count > 0,
LayoutJson = WizardConsts.DefaultLayoutJson(input.DefaultLayout, input.Grid, input.Card, input.Pivot, input.Tree, input.Chart, input.Gantt, input.Scheduler),
ShowNote = input.SubForms.Count > 0 || input.Criteria.Count > 0,
LayoutJson = WizardConsts.DefaultLayoutJson(input.DefaultLayout, input.Grid, input.Card, input.Pivot, input.Tree, input.Chart, input.Gantt, input.Scheduler, input.Todo),
CultureName = LanguageCodes.En,
ListFormCode = input.ListFormCode,
Name = nameLangKey,
@ -413,7 +416,7 @@ public class WizardDataSeeder : IDataSeedContributor, ITransientDependency
HeaderFilterJson = WizardConsts.DefaultHeaderFilterJson(),
SearchPanelJson = WizardConsts.DefaultSearchPanelJson(),
GroupPanelJson = WizardConsts.DefaultGroupPanelJson(),
SelectionJson = WizardConsts.DefaultSelectionSingleJson(input.WorkflowCriteria.Count > 0 ? GridOptions.SelectionModeSingle : GridOptions.SelectionModeNone),
SelectionJson = WizardConsts.DefaultSelectionSingleJson(input.Criteria.Count > 0 ? GridOptions.SelectionModeSingle : GridOptions.SelectionModeNone),
ColumnOptionJson = WizardConsts.DefaultColumnOptionJson(),
PermissionJson = WizardConsts.DefaultPermissionJson(code),
DeleteCommand = isDeleted ? WizardConsts.DefaultDeleteCommand(input.SelectCommand) : null,
@ -428,7 +431,25 @@ public class WizardDataSeeder : IDataSeedContributor, ITransientDependency
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,
WorkflowJson = HasWorkflow(workflow, input.Criteria) ? JsonSerializer.Serialize(workflow) : null,
TodoOptionJson = (input.Todo || input.DefaultLayout == "todo") &&
!string.IsNullOrEmpty(input.TodoTitleExpr) &&
!string.IsNullOrEmpty(input.TodoStatusExpr)
? JsonSerializer.Serialize(new TodoOptionDto
{
TitleExpr = input.TodoTitleExpr,
StatusExpr = input.TodoStatusExpr,
DescriptionExpr = input.TodoDescriptionExpr,
DueDateExpr = input.TodoDueDateExpr,
TagExpr = input.TodoTagExpr,
AssigneeExpr = input.TodoAssigneeExpr,
PriorityExpr = input.TodoPriorityExpr,
CompletedExpr = input.TodoCompletedExpr,
OrderExpr = input.TodoOrderExpr,
StatusOrder = input.TodoStatusOrder,
AllowDragging = input.TodoAllowDragging,
})
: null,
}, autoSave: true);
// ListFormFields
@ -466,7 +487,7 @@ public class WizardDataSeeder : IDataSeedContributor, ITransientDependency
}
}
foreach (var criteria in input.WorkflowCriteria)
foreach (var criteria in input.Criteria)
{
if (string.IsNullOrWhiteSpace(criteria.Id))
{

View file

@ -84,7 +84,7 @@ public static class WizardConsts
Margin = 10
});
public static string DefaultLayoutJson(string DefaultLayout = "grid", bool Grid = true, bool Card = true, bool Pivot = true, bool Chart = true, bool Tree = true, bool Gantt = true, bool Scheduler = true) => JsonSerializer.Serialize(new
public static string DefaultLayoutJson(string DefaultLayout = "grid", bool Grid = true, bool Card = true, bool Pivot = true, bool Chart = true, bool Tree = true, bool Gantt = true, bool Scheduler = true, bool Todo = true) => JsonSerializer.Serialize(new
{
Grid = Grid,
Card = Card,
@ -93,6 +93,7 @@ public static class WizardConsts
Tree = Tree,
Gantt = Gantt,
Scheduler = Scheduler,
Todo = Todo,
DefaultLayout = DefaultLayout,
});

View file

@ -36,6 +36,7 @@ public class ListForm : Entity<Guid>
public string TreeOptionJson { get; set; } // Tree yapisi ile ilgili ayarlar
public string GanttOptionJson { get; set; } // Gantt yapisi ile ilgili ayarlar
public string SchedulerOptionJson { get; set; } // Scheduler yapisi ile ilgili ayarlar
public string TodoOptionJson { get; set; } // Todo board alan esleme ayarlari
public string FilterRowJson { get; set; } // Filtre ayarlari, Json olarak tutulur, donus sinifi FilterRowDto
public string RowJson { get; set; } // Row ayarları, Json olarak tutulur, donus sinifi FilterRowDto
public string HeaderFilterJson { get; set; } // Header filtreleme ayarlari, Json olarak tutulur

View file

@ -469,7 +469,7 @@ public class SelectQueryManager : PlatformDomainService, ISelectQueryManager
if (listform.WorkflowJson.IsNullOrWhiteSpace() == false)
{
var workflow = JsonSerializer.Deserialize<Workflow>(listform.WorkflowJson);
if (workflow != null && workflow.IsFilterUserName)
if (workflow != null && workflow.ApprovalIsFilterUserName)
{
if (whereParts.Any())
{

View file

@ -6,18 +6,18 @@ namespace Sozsoft.Platform.Queries;
public class Workflow : ValueObject
{
public string ApprovalUserFieldName { get; set; }
public bool IsFilterUserName { get; set; }
public string ApprovalDateFieldName { get; set; }
public string ApprovalStatusFieldName { get; set; }
public string ApprovalDescriptionFieldName { get; set; }
public bool ApprovalIsFilterUserName { get; set; }
protected override IEnumerable<object> GetAtomicValues()
{
yield return ApprovalUserFieldName;
yield return IsFilterUserName;
yield return ApprovalDateFieldName;
yield return ApprovalStatusFieldName;
yield return ApprovalDescriptionFieldName;
yield return ApprovalIsFilterUserName;
}
}

View file

@ -378,6 +378,7 @@ public class PlatformDbContext :
b.Property(a => a.WorkflowJson).HasColumnType("text");
b.Property(a => a.ExtraFilterJson).HasColumnType("text");
b.Property(a => a.LayoutJson).HasColumnType("text");
b.Property(a => a.TodoOptionJson).HasColumnType("text");
b.Property(a => a.CommonJson).HasColumnType("text");
b.Property(a => a.DataSourceJson).HasColumnType("text");
b.Property(a => a.AdaptiveLayoutJson).HasColumnType("text");

View file

@ -13,7 +13,7 @@ using Volo.Abp.EntityFrameworkCore;
namespace Sozsoft.Platform.Migrations
{
[DbContext(typeof(PlatformDbContext))]
[Migration("20260713103339_Initial")]
[Migration("20260716120523_Initial")]
partial class Initial
{
/// <inheritdoc />
@ -3264,6 +3264,9 @@ namespace Sozsoft.Platform.Migrations
b.Property<string>("TitleJson")
.HasColumnType("text");
b.Property<string>("TodoOptionJson")
.HasColumnType("text");
b.Property<string>("TooltipJson")
.HasColumnType("text");
@ -8818,7 +8821,8 @@ namespace Sozsoft.Platform.Migrations
b.HasOne("Sozsoft.Platform.Entities.ReportCategory", "ReportCategory")
.WithMany("ReportTemplates")
.HasForeignKey("CategoryId")
.OnDelete(DeleteBehavior.Restrict);
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("ReportCategory");
});

View file

@ -1392,6 +1392,7 @@ namespace Sozsoft.Platform.Migrations
TreeOptionJson = table.Column<string>(type: "nvarchar(max)", nullable: true),
GanttOptionJson = table.Column<string>(type: "nvarchar(max)", nullable: true),
SchedulerOptionJson = table.Column<string>(type: "nvarchar(max)", nullable: true),
TodoOptionJson = table.Column<string>(type: "text", nullable: true),
FilterRowJson = table.Column<string>(type: "text", nullable: true),
RowJson = table.Column<string>(type: "nvarchar(max)", nullable: true),
HeaderFilterJson = table.Column<string>(type: "text", nullable: true),

View file

@ -3261,6 +3261,9 @@ namespace Sozsoft.Platform.Migrations
b.Property<string>("TitleJson")
.HasColumnType("text");
b.Property<string>("TodoOptionJson")
.HasColumnType("text");
b.Property<string>("TooltipJson")
.HasColumnType("text");
@ -8815,7 +8818,8 @@ namespace Sozsoft.Platform.Migrations
b.HasOne("Sozsoft.Platform.Entities.ReportCategory", "ReportCategory")
.WithMany("ReportTemplates")
.HasForeignKey("CategoryId")
.OnDelete(DeleteBehavior.Restrict);
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("ReportCategory");
});

View file

@ -33,4 +33,5 @@ export interface ListFormJsonRowDto {
export interface ListState {
listFormCode: string
layout: ListViewLayoutType
cardColumnCount?: number
}

View file

@ -42,6 +42,7 @@ export const ListFormEditTabs = {
TreeForm: 'tree',
GanttForm: 'gantt',
SchedulerForm: 'scheduler',
TodoForm: 'todo',
PagerForm: 'pager',
StateForm: 'state',
SubForm: 'subForm',
@ -93,6 +94,7 @@ export const tabVisibilityConfig: Record<string, string[]> = {
'tree',
'gantt',
'scheduler',
'todo',
'pager',
'state',
'extrafilter',

View file

@ -2,8 +2,7 @@ import {
ListFormWorkflowCriteriaDto,
SubFormDto,
WidgetEditDto,
WorkflowDto,
SelectCommandTypeEnum
SelectCommandTypeEnum,
} from '@/proxy/form/models'
import { ListViewLayoutType } from '@/views/admin/listForm/edit/types'
@ -45,6 +44,7 @@ export interface ListFormWizardDto {
chart: boolean
gantt: boolean
scheduler: boolean
todo: boolean
languageTextMenuEn: string
languageTextMenuTr: string
@ -84,11 +84,30 @@ export interface ListFormWizardDto {
schedulerStartDateExpr?: string
schedulerEndDateExpr?: string
// Todo options (required when defaultLayout = 'todo')
todoTitleExpr: string
todoStatusExpr: string
todoDescriptionExpr: string
todoDueDateExpr: string
todoTagExpr: string
todoAssigneeExpr: string
todoPriorityExpr: string
todoCompletedExpr: string
todoOrderExpr: string
todoStatusOrder: string
todoAllowDragging: boolean
// Workflow options (required when defaultLayout = 'workflow')
approvalUserFieldName: string
approvalDateFieldName: string
approvalStatusFieldName: string
approvalDescriptionFieldName: string
approvalIsFilterUserName: boolean
criteria: ListFormWorkflowCriteriaDto[]
groups?: ListFormWizardColumnGroupDto[]
subForms?: SubFormDto[]
widgets?: WidgetEditDto[]
workflow?: WorkflowDto
workflowCriteria?: ListFormWorkflowCriteriaDto[]
}
export interface WizardFileInfoDto {

View file

@ -442,6 +442,20 @@ export interface SchedulerOptionDto {
resources?: SchedulerResourceDto[]
}
export interface TodoOptionDto {
titleExpr?: string
statusExpr?: string
descriptionExpr?: string
dueDateExpr?: string
tagExpr?: string
assigneeExpr?: string
priorityExpr?: string
completedExpr?: string
orderExpr?: string
statusOrder?: string
allowDragging?: boolean
}
export interface SchedulerResourceDto {
fieldExpr?: string
dataSource?: any[]
@ -557,6 +571,8 @@ export interface GridOptionsDto extends AuditedEntityDto<string> {
ganttOptionDto: GanttOptionDto
schedulerOptionJson?: string
schedulerOptionDto: SchedulerOptionDto
todoOptionJson?: string
todoOptionDto: TodoOptionDto
pagerOptionJson?: string
pagerOptionDto: GridPagerOptionDto
editingOptionJson?: string
@ -911,10 +927,10 @@ export interface WidgetEditDto {
export interface WorkflowDto {
approvalUserFieldName: string
isFilterUserName: boolean
approvalDateFieldName: string
approvalStatusFieldName: string
approvalDescriptionFieldName: string
approvalIsFilterUserName: boolean
criteria: ListFormWorkflowCriteriaDto[]
}
@ -957,6 +973,7 @@ export interface LayoutDto {
chart: boolean
gantt: boolean
scheduler: boolean
todo: boolean
defaultLayout: ListViewLayoutType
}

View file

@ -82,7 +82,7 @@ export const adminModel: AdminModel = {
setStates: action((state, payload) => {
const stateId = state.states.findIndex((s) => s.listFormCode === payload.listFormCode)
if (stateId !== -1) {
state.states[stateId] = payload
state.states[stateId] = { ...state.states[stateId], ...payload }
} else {
state.states = [...state.states, payload]
}

View file

@ -27,7 +27,11 @@ import FormTabSubForm from './FormTabSubForm'
import FormFields from './form-fields/FormFields'
import { putListForms } from '@/services/admin/list-form.service'
import { getRoles, getUsers } from '@/services/identity.service'
import { SelectCommandTypeEnum, GridOptionsEditDto, ListFormCustomizationDto } from '@/proxy/form/models'
import {
SelectCommandTypeEnum,
GridOptionsEditDto,
ListFormCustomizationDto,
} from '@/proxy/form/models'
import { IdentityRoleDto, IdentityUserDto } from '@/proxy/admin/models'
import { getListFormCustomizations } from '@/services/admin/list-form-customization.service'
import { sqlObjectManagerService } from '@/services/sql-query-manager.service'
@ -54,6 +58,7 @@ import FormTabSorting from './FormTabSorting'
import FormTabRow from './FormTabRow'
import FormTabGantt from './FormTabGantt'
import FormTabScheduler from './FormTabScheduler'
import FormTabTodo from './FormTabTodo'
import { APP_NAME } from '@/constants/app.constant'
import { FormTabWorkflow } from './FormTabWorkflow'
@ -318,6 +323,7 @@ const FormEdit = () => {
{visibleTabs.includes('scheduler') && (
<TabNav value="scheduler">{translate('::ListForms.ListFormEdit.TabScheduler')}</TabNav>
)}
{visibleTabs.includes('todo') && <TabNav value="todo">Todo</TabNav>}
{visibleTabs.includes('subForms') && (
<TabNav value="subForms">{translate('::ListForms.ListFormEdit.SubForms')}</TabNav>
)}
@ -431,6 +437,9 @@ const FormEdit = () => {
<TabContent value="scheduler" className="px-2">
<FormTabScheduler onSubmit={onSubmit} />
</TabContent>
<TabContent value="todo" className="px-2">
<FormTabTodo onSubmit={onSubmit} />
</TabContent>
<TabContent value="pager" className="px-2">
<FormTabPager onSubmit={onSubmit} />
</TabContent>

View file

@ -410,6 +410,15 @@ function FormTabDetails(
component={Checkbox}
/>
</FormItem>
<FormItem label="Todo Layout">
<Field
className="w-20"
autoComplete="off"
name="layoutDto.todo"
component={Checkbox}
/>
</FormItem>
</div>
</>
)}

View file

@ -0,0 +1,137 @@
import {
Button,
Checkbox,
FormContainer,
FormItem,
Input,
Notification,
Select,
toast,
} from '@/components/ui'
import { ListFormEditTabs } from '@/proxy/admin/list-form/options'
import type { GridOptionsEditDto } from '@/proxy/form/models'
import { getListFormFields } from '@/services/admin/list-form-field.service'
import { useStoreState } from '@/store'
import type { SelectBoxOption } from '@/types/shared'
import { Field, FieldProps, Form, Formik } from 'formik'
import groupBy from 'lodash/groupBy'
import { useCallback, useEffect, useRef, useState } from 'react'
import { useParams } from 'react-router-dom'
import type { FormEditProps } from './FormEdit'
import { useLocalization } from '@/utils/hooks/useLocalization'
const mappings: Array<{
name: keyof GridOptionsEditDto['todoOptionDto']
labelKey: string
required?: boolean
}> = [
{ name: 'titleExpr', labelKey: 'TodoTitleExpr', required: true },
{ name: 'statusExpr', labelKey: 'TodoStatusExpr', required: true },
{ name: 'descriptionExpr', labelKey: 'TodoDescriptionExpr' },
{ name: 'dueDateExpr', labelKey: 'TodoDueDateExpr' },
{ name: 'tagExpr', labelKey: 'TodoTagExpr' },
{ name: 'assigneeExpr', labelKey: 'TodoAssigneeExpr' },
{ name: 'priorityExpr', labelKey: 'TodoPriorityExpr' },
{ name: 'completedExpr', labelKey: 'TodoCompletedExpr' },
{ name: 'orderExpr', labelKey: 'TodoOrderExpr' },
]
const FormTabTodo = ({ onSubmit }: FormEditProps) => {
const { listFormCode } = useParams()
const values = useStoreState((state) => state.admin.lists.values)
const [fields, setFields] = useState<SelectBoxOption[]>([])
const { translate } = useLocalization()
const translateRef = useRef(translate)
translateRef.current = translate
const loadFields = useCallback(async () => {
if (!listFormCode) return
try {
const response = await getListFormFields({
listFormCode,
sorting: 'ListOrderNo',
maxResultCount: 1000,
})
const names = groupBy(response.data?.items ?? [], 'fieldName')
setFields(Object.keys(names).map((name) => ({ value: name, label: name })))
} catch (error: any) {
toast.push(
<Notification type="danger">
{translateRef.current('::ListForms.ListFormFieldEdit.TodoFieldsLoadFailed', {
0: error.toString(),
})}
</Notification>,
)
}
}, [listFormCode])
useEffect(() => {
loadFields()
}, [loadFields])
if (!values) return null
return (
<Formik
initialValues={values}
onSubmit={(formValues, helpers) => onSubmit(ListFormEditTabs.TodoForm, formValues, helpers)}
>
{({ isSubmitting, values: formValues }) => (
<Form>
<FormContainer size="sm">
<div className="grid grid-cols-1 gap-3 md:grid-cols-2 lg:grid-cols-4">
{mappings.map((mapping) => (
<FormItem
key={mapping.name}
label={`${translate(
`::ListForms.ListFormFieldEdit.${mapping.labelKey}`,
)}${mapping.required ? ' *' : ''}`}
>
<Field name={`todoOptionDto.${mapping.name}`}>
{({ field, form }: FieldProps) => (
<Select
field={field}
form={form}
isClearable={!mapping.required}
options={fields}
value={fields.find(
(option) => option.value === formValues.todoOptionDto?.[mapping.name],
)}
onChange={(option) => form.setFieldValue(field.name, option?.value)}
/>
)}
</Field>
</FormItem>
))}
</div>
<div className="mt-3 grid grid-cols-1 gap-3 md:grid-cols-2">
<FormItem label={translate('::ListForms.ListFormFieldEdit.TodoStatusOrder')}>
<Field
name="todoOptionDto.statusOrder"
component={Input}
placeholder={translate(
'::ListForms.ListFormFieldEdit.TodoStatusOrderPlaceholder',
)}
/>
</FormItem>
<FormItem label={translate('::ListForms.ListFormFieldEdit.TodoAllowDragging')}>
<Field name="todoOptionDto.allowDragging" component={Checkbox} />
</FormItem>
</div>
<Button
block
size="sm"
variant="solid"
loading={isSubmitting}
type="submit"
className="my-3"
>
{translate('::Save')}
</Button>
</FormContainer>
</Form>
)}
</Formik>
)
}
export default FormTabTodo

View file

@ -314,10 +314,10 @@ export function FormTabWorkflow(
const schema = object().shape({
workflowDto: object().shape({
approvalUserFieldName: string().required(),
isFilterUserName: bool(),
approvalStatusFieldName: string().required(),
approvalDateFieldName: string(),
approvalDescriptionFieldName: string(),
approvalIsFilterUserName: bool(),
}),
})
@ -442,16 +442,16 @@ export function FormTabWorkflow(
</FormItem>
<FormItem
label={translate('::ListForms.ListFormEdit.Workflow.IsFilterUserName')}
label={translate('::ListForms.ListFormEdit.Workflow.ApprovalIsFilterUserName')}
invalid={
!!(
errors.workflowDto?.isFilterUserName &&
touched.workflowDto?.isFilterUserName
errors.workflowDto?.approvalIsFilterUserName &&
touched.workflowDto?.approvalIsFilterUserName
)
}
errorMessage={errors.workflowDto?.isFilterUserName as string}
errorMessage={errors.workflowDto?.approvalIsFilterUserName as string}
>
<Field name="workflowDto.isFilterUserName" component={Checkbox} />
<Field name="workflowDto.approvalIsFilterUserName" component={Checkbox} />
</FormItem>
</div>

View file

@ -230,6 +230,7 @@ export const listFormDefaultLayoutOptions = [
{ value: 'tree', label: 'Tree' },
{ value: 'gantt', label: 'Gantt' },
{ value: 'scheduler', label: 'Scheduler' },
{ value: 'todo', label: 'Todo' },
]
export const listFormAlignmentOptions = [

View file

@ -8,6 +8,7 @@ export type ListViewLayoutType =
| 'chart'
| 'gantt'
| 'scheduler'
| 'todo'
export const layoutTypes = {
grid: 'Grid',
@ -17,4 +18,5 @@ export const layoutTypes = {
chart: 'Chart',
gantt: 'Gantt',
scheduler: 'Scheduler',
todo: 'Todo',
}

View file

@ -24,6 +24,7 @@ import WizardStep1, {
import WizardStep2 from './WizardStep2'
import WizardStep3, { WizardGroup, WizardGroupItem } from './WizardStep3'
import WizardStep7 from './WizardStep7'
import WizardStep8 from './WizardStep8'
import WizardStep4 from './WizardStep4'
import WizardStep5 from './WizardStep5'
import WizardStep6 from './WizardStep6'
@ -57,11 +58,24 @@ const initialValues: ListFormWizardDto = {
allowDetail: false,
defaultLayout: 'grid',
grid: true,
card: true,
pivot: true,
tree: true,
chart: true,
gantt: true,
scheduler: true,
todo: true,
todoTitleExpr: '',
todoStatusExpr: '',
todoDescriptionExpr: '',
todoDueDateExpr: '',
todoTagExpr: '',
todoAssigneeExpr: '',
todoPriorityExpr: '',
todoCompletedExpr: '',
todoOrderExpr: '',
todoStatusOrder: 'Inbox, Today, This Week, This Month, Later, Done',
todoAllowDragging: true,
languageTextMenuEn: '',
languageTextMenuTr: '',
languageTextTitleEn: '',
@ -95,15 +109,12 @@ const initialValues: ListFormWizardDto = {
schedulerEndDateExpr: '',
subForms: [],
widgets: [],
workflow: {
approvalUserFieldName: '',
isFilterUserName: false,
approvalDateFieldName: '',
approvalStatusFieldName: '',
approvalDescriptionFieldName: '',
criteria: [],
},
workflowCriteria: [],
approvalUserFieldName: '',
approvalDateFieldName: '',
approvalStatusFieldName: '',
approvalDescriptionFieldName: '',
approvalIsFilterUserName: false,
criteria: [],
}
const step1ValidationSchema = Yup.object().shape({
@ -225,13 +236,13 @@ const Wizard = () => {
const [widgets, setWidgets] = useState<WidgetEditDto[]>([])
const [workflow, setWorkflow] = useState<WorkflowDto>({
approvalUserFieldName: '',
isFilterUserName: false,
approvalDateFieldName: '',
approvalStatusFieldName: '',
approvalDescriptionFieldName: '',
approvalIsFilterUserName: false,
criteria: [],
})
const [workflowCriteria, setWorkflowCriteria] = useState<ListFormWorkflowCriteriaDto[]>([])
const [criteria, setCriteria] = useState<ListFormWorkflowCriteriaDto[]>([])
// Audit columns that should not be selected by default
const AUDIT_COLUMNS = new Set([
@ -406,11 +417,24 @@ const Wizard = () => {
allowDetail: w.allowDetail ?? false,
defaultLayout: w.defaultLayout ?? 'grid',
grid: w.grid ?? true,
card: w.card ?? true,
pivot: w.pivot ?? true,
tree: w.tree ?? true,
chart: w.chart ?? true,
gantt: w.gantt ?? true,
scheduler: w.scheduler ?? true,
todo: w.todo ?? true,
todoTitleExpr: w.todoTitleExpr ?? '',
todoStatusExpr: w.todoStatusExpr ?? '',
todoDescriptionExpr: w.todoDescriptionExpr ?? '',
todoDueDateExpr: w.todoDueDateExpr ?? '',
todoTagExpr: w.todoTagExpr ?? '',
todoAssigneeExpr: w.todoAssigneeExpr ?? '',
todoPriorityExpr: w.todoPriorityExpr ?? '',
todoCompletedExpr: w.todoCompletedExpr ?? '',
todoOrderExpr: w.todoOrderExpr ?? '',
todoStatusOrder: w.todoStatusOrder ?? 'Inbox, Today, This Week, This Month, Later, Done',
todoAllowDragging: w.todoAllowDragging ?? true,
languageTextMenuEn: w.languageTextMenuEn ?? '',
languageTextMenuTr: w.languageTextMenuTr ?? '',
languageTextTitleEn: w.languageTextTitleEn ?? '',
@ -444,15 +468,12 @@ const Wizard = () => {
schedulerEndDateExpr: w.schedulerEndDateExpr ?? '',
subForms: w.subForms ?? [],
widgets: w.widgets ?? [],
workflow: w.workflow ?? {
approvalUserFieldName: '',
isFilterUserName: false,
approvalDateFieldName: '',
approvalStatusFieldName: '',
approvalDescriptionFieldName: '',
criteria: [],
},
workflowCriteria: w.workflowCriteria ?? w.workflow?.criteria ?? [],
approvalUserFieldName: w.approvalUserFieldName ?? '',
approvalDateFieldName: w.approvalDateFieldName ?? '',
approvalStatusFieldName: w.approvalStatusFieldName ?? '',
approvalDescriptionFieldName: w.approvalDescriptionFieldName ?? '',
approvalIsFilterUserName: w.approvalIsFilterUserName ?? false,
criteria: w.criteria ?? [],
})
// Queue column load to run once dbObjects is available
@ -507,17 +528,15 @@ const Wizard = () => {
setSubForms(w.subForms ?? [])
setWidgets(w.widgets ?? [])
setWorkflow(
w.workflow ?? {
approvalUserFieldName: '',
isFilterUserName: false,
approvalDateFieldName: '',
approvalStatusFieldName: '',
approvalDescriptionFieldName: '',
criteria: [],
},
)
setWorkflowCriteria(w.workflowCriteria ?? w.workflow?.criteria ?? [])
setWorkflow({
approvalUserFieldName: w.approvalUserFieldName ?? '',
approvalDateFieldName: w.approvalDateFieldName ?? '',
approvalStatusFieldName: w.approvalStatusFieldName ?? '',
approvalDescriptionFieldName: w.approvalDescriptionFieldName ?? '',
approvalIsFilterUserName: w.approvalIsFilterUserName ?? false,
criteria: w.criteria ?? [],
})
setCriteria(w.criteria ?? [])
} catch {
toast.push(
<Notification type="danger">
@ -730,11 +749,12 @@ const Wizard = () => {
})),
subForms,
widgets,
workflow: {
...workflow,
criteria: workflowCriteria,
},
workflowCriteria,
approvalUserFieldName: workflow.approvalUserFieldName,
approvalDateFieldName: workflow.approvalDateFieldName,
approvalStatusFieldName: workflow.approvalStatusFieldName,
approvalDescriptionFieldName: workflow.approvalDescriptionFieldName,
approvalIsFilterUserName: workflow.approvalIsFilterUserName,
criteria,
})
// ✅ sonra config çek
@ -780,6 +800,7 @@ const Wizard = () => {
<Steps.Item title={translate('::ListForms.ListFormEdit.SubForms') || 'Sub Forms'} />
<Steps.Item title={translate('::ListForms.ListFormEdit.TabWidgets') || 'Widgets'} />
<Steps.Item title={translate('::ListForms.ListFormEdit.TabWorkflow') || 'Workflow'} />
<Steps.Item title={translate('::ListForms.ListFormEdit.TabTodo') || 'Todo / Kanban'} />
<Steps.Item title={translate('::App.Platform.Deploy') || 'Deploy'} />
</Steps>
</div>
@ -900,19 +921,30 @@ const Wizard = () => {
<WizardStep6
listFormCode={values.listFormCode}
workflow={workflow}
criteria={workflowCriteria}
criteria={criteria}
selectCommandColumns={selectCommandColumns}
translate={translate}
onWorkflowChange={setWorkflow}
onCriteriaChange={setWorkflowCriteria}
onCriteriaChange={setCriteria}
onBack={() => setCurrentStep(4)}
onNext={() => setCurrentStep(6)}
/>
)}
{/* ─── Step 7: Deploy ───────────────────────────── */}
{/* ─── Step 7: Todo / Kanban ────────────────────── */}
{currentStep === 6 && (
<WizardStep7
selectedColumns={selectedColumns}
selectCommandColumns={selectCommandColumns}
translate={translate}
onBack={() => setCurrentStep(5)}
onNext={() => setCurrentStep(7)}
/>
)}
{/* ─── Step 8: Deploy ───────────────────────────── */}
{currentStep === 7 && (
<WizardStep8
values={values}
wizardName={values.wizardName}
selectedColumns={selectedColumns}
@ -921,9 +953,9 @@ const Wizard = () => {
subForms={subForms}
widgets={widgets}
workflow={workflow}
workflowCriteria={workflowCriteria}
criteria={criteria}
translate={translate}
onBack={() => setCurrentStep(5)}
onBack={() => setCurrentStep(6)}
onSubmit={handleDeploy}
/>
)}

View file

@ -599,6 +599,20 @@ const WizardStep2 = ({
component={Checkbox}
/>
</FormItem>
<FormItem
label={translate('::ListForms.ListFormEdit.DetailsLayoutDto.TodoLayout')}
invalid={errors?.todo && touched?.todo}
errorMessage={errors?.todo}
>
<Field
className="w-20"
autoComplete="off"
name="todo"
placeholder={translate('::ListForms.ListFormEdit.DetailsLayoutDto.TodoLayout')}
component={Checkbox}
/>
</FormItem>
</div>
</div>

View file

@ -306,18 +306,18 @@ function WizardStep6({
</FormItem>
))}
<FormItem
label={translate('::ListForms.ListFormEdit.Workflow.IsFilterUserName')}
label={translate('::ListForms.ListFormEdit.Workflow.ApprovalIsFilterUserName')}
>
<Field name="isFilterUserName">
<Field name="approvalIsFilterUserName">
{({ field, form }: FieldProps<boolean>) => (
<Checkbox
name={field.name}
checked={Boolean(values.isFilterUserName)}
checked={Boolean(values.approvalIsFilterUserName)}
onChange={(checked) => {
form.setFieldValue(field.name, checked)
onWorkflowChange({
...values,
isFilterUserName: checked,
approvalIsFilterUserName: checked,
criteria,
})
}}

View file

@ -1,714 +1,151 @@
import { Button } from '@/components/ui'
import { Button, Checkbox, FormItem, Input, Select } from '@/components/ui'
import type { ListFormWizardDto } from '@/proxy/admin/wizard/models'
import type { DatabaseColumnDto } from '@/proxy/sql-query-manager/models'
import React, { useState } from 'react'
import {
FaArrowLeft,
FaCheckCircle,
FaChevronDown,
FaChevronRight,
FaCircle,
FaExclamationCircle,
FaRocket,
FaSpinner,
} from 'react-icons/fa'
import { WizardGroup } from './WizardStep3'
import { dbSourceTypeOptions, selectCommandTypeOptions } from '../edit/options'
import { ListFormWizardDto } from '@/proxy/admin/wizard/models'
import {
ListFormWorkflowCriteriaDto,
SubFormDto,
WidgetEditDto,
WorkflowDto,
} from '@/proxy/form/models'
import { formatWorkflowApprovers } from '@/utils/workflow/workflowHelpers'
import type { SelectBoxOption } from '@/types/shared'
import { Field, type FieldProps, useFormikContext } from 'formik'
import { useMemo, useState } from 'react'
import { FaArrowLeft, FaArrowRight, FaColumns } from 'react-icons/fa'
// ─── Types ────────────────────────────────────────────────────────────────────
export interface WizardStep7Props {
values: ListFormWizardDto
wizardName: string
interface WizardStep7Props {
selectedColumns: Set<string>
selectCommandColumns: DatabaseColumnDto[]
groups: WizardGroup[]
subForms: SubFormDto[]
widgets: WidgetEditDto[]
workflow: WorkflowDto
workflowCriteria: ListFormWorkflowCriteriaDto[]
translate: (key: string) => string
onBack: () => void
onSubmit: () => Promise<void>
onNext: () => void
}
type LogStatus = 'pending' | 'running' | 'success' | 'error'
interface LogEntry {
id: number
const mappings: Array<{
name:
| 'todoTitleExpr'
| 'todoStatusExpr'
| 'todoDescriptionExpr'
| 'todoDueDateExpr'
| 'todoTagExpr'
| 'todoAssigneeExpr'
| 'todoPriorityExpr'
| 'todoCompletedExpr'
| 'todoOrderExpr'
label: string
status: LogStatus
detail?: string
}
required?: boolean
}> = [
{ name: 'todoTitleExpr', label: 'Başlık alanı', required: true },
{ name: 'todoStatusExpr', label: 'Kanban / durum alanı', required: true },
{ name: 'todoDescriptionExpr', label: 'Açıklama alanı' },
{ name: 'todoDueDateExpr', label: 'Termin tarihi alanı' },
{ name: 'todoTagExpr', label: 'Etiket alanı' },
{ name: 'todoAssigneeExpr', label: 'Atanan kişi alanı' },
{ name: 'todoPriorityExpr', label: 'Öncelik alanı' },
{ name: 'todoCompletedExpr', label: 'Tamamlandı alanı' },
{ name: 'todoOrderExpr', label: 'Kart sıra alanı' },
]
// ─── 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)}
variant="plain"
shape="none"
className="w-full !h-auto !items-center !justify-between !rounded-none !bg-gray-50 !px-4 !py-2.5 transition-colors hover:!bg-gray-100 dark:!bg-gray-800 dark:hover:!bg-gray-700"
>
<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,
onNext,
}: WizardStep7Props) => {
const [logs, setLogs] = useState<LogEntry[]>([])
const [isDeploying, setIsDeploying] = useState(false)
const [isDone, setIsDone] = useState(false)
const [hasError, setHasError] = useState(false)
const { values, setFieldValue } = useFormikContext<ListFormWizardDto>()
const [validationError, setValidationError] = useState('')
const fieldOptions = useMemo<SelectBoxOption[]>(
() =>
selectCommandColumns
.filter((column) => selectedColumns.has(column.columnName))
.map((column) => ({ value: column.columnName, label: column.columnName })),
[selectCommandColumns, selectedColumns],
)
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 continueToDeploy = () => {
if (values.todo && (!values.todoTitleExpr || !values.todoStatusExpr)) {
setValidationError('Başlık alanı ve Kanban / durum alanı zorunludur.')
return
}
setValidationError('')
onNext()
}
const totalFields = groups.reduce((acc, g) => acc + g.items.length, 0)
const editingFormFields = groups.flatMap((g) =>
g.items
.filter((item) => item.includeInEditingForm && item.dataField !== values.keyFieldName)
.map((item) => ({ ...item, groupCaption: g.caption })),
)
const groupedFieldNames = new Set(groups.flatMap((g) => g.items.map((item) => item.dataField)))
const ungroupedSelectedColumns = [...selectedColumns].filter((col) => !groupedFieldNames.has(col))
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.SelectCommand')}
value={`${values.selectCommand} (${
selectCommandTypeOptions.find((o: any) => o.value === values.selectCommandType)
?.label ?? String(values.selectCommandType)
})`}
/>
<Row
label={translate('::ListForms.Wizard.Step4.KeyField')}
value={`${values.keyFieldName} (${
dbSourceTypeOptions.find((o: any) => o.value === values.keyFieldDbSourceType)
?.label ?? String(values.keyFieldDbSourceType)
})`}
/>
</Section>
<div className="pb-20">
<div className="mb-5 rounded-xl border border-indigo-200 bg-indigo-50/60 p-4 dark:border-indigo-900 dark:bg-indigo-950/20">
<div className="flex items-center gap-2 font-semibold text-indigo-700 dark:text-indigo-300">
<FaColumns /> Todo / Kanban ayrıntıları
</div>
<Section
title={
translate('::ListForms.Wizard.Step4.ColumnsAndFormLayout') || 'Columns & Form Layout'
}
badge={`${selectedColumns.size} ${translate('::App.Listform.ListformField.Column')} / ${editingFormFields.length} ${translate('::ListForms.Wizard.Step4.EditingForm') || 'Popup Form'}`}
>
<div className="mb-3 grid grid-cols-3 gap-2">
{[
{
label: translate('::ListForms.Wizard.Step4.SelectedColumns'),
value: selectedColumns.size,
className: 'text-indigo-600 dark:text-indigo-400',
},
{
label: translate('::ListForms.Wizard.Step4.EditingForm') || 'Popup Form',
value: editingFormFields.length,
className: 'text-emerald-600 dark:text-emerald-400',
},
{
label: translate('::ListForms.Wizard.Step4.FormGroups'),
value: groups.length,
className: 'text-gray-700 dark:text-gray-200',
},
].map((item) => (
<div
key={item.label}
className="rounded-lg border border-gray-100 dark:border-gray-800 bg-gray-50 dark:bg-gray-800 px-3 py-2"
>
<div className={`text-lg font-semibold leading-none ${item.className}`}>
{item.value}
</div>
<div className="mt-1 text-[11px] text-gray-400 truncate">{item.label}</div>
</div>
))}
</div>
<div className="flex flex-col gap-2">
{groups.map((g) => (
<div
key={g.id}
className="overflow-hidden rounded-lg border border-gray-100 dark:border-gray-800"
>
<div className="flex items-center justify-between gap-3 bg-gray-50 dark:bg-gray-800 px-3 py-2">
<span className="text-xs font-semibold text-gray-700 dark:text-gray-200 truncate">
{g.caption || `(${translate('::ListForms.Wizard.Step4.StatGroup')})`}
</span>
<div className="flex items-center gap-1.5 text-[10px] text-gray-400 shrink-0">
<span className="rounded bg-white dark:bg-gray-900 px-1.5 py-0.5">
{g.items.length} {translate('::ListForms.Wizard.Step4.StatField')}
</span>
<span className="rounded bg-white dark:bg-gray-900 px-1.5 py-0.5">
{g.colCount} {translate('::App.Listform.ListformField.Column')}
</span>
</div>
</div>
<div className="divide-y divide-gray-100 dark:divide-gray-800">
{g.items.length === 0 ? (
<span className="block px-3 py-2 text-xs text-gray-300 italic">
{translate('::ListForms.Wizard.Step4.NoFields') || 'Alan yok'}
</span>
) : (
g.items.map((item) => {
const meta = selectCommandColumns.find((c) => c.columnName === item.dataField)
const isKeyField = item.dataField === values.keyFieldName
const isPopupField = item.includeInEditingForm && !isKeyField
return (
<div
key={item.id}
className="grid grid-cols-[minmax(150px,1fr)_auto] items-center gap-3 px-3 py-1.5"
>
<div className="flex min-w-0 items-center gap-2">
<span className="text-xs font-medium text-indigo-600 dark:text-indigo-400 truncate">
{item.dataField}
</span>
{meta?.dataType && (
<span className="text-[10px] text-gray-400 truncate">
{meta.dataType}
</span>
)}
</div>
<div className="flex flex-wrap justify-end gap-1">
<span className="rounded bg-indigo-50 dark:bg-indigo-900/30 px-1.5 py-0.5 text-[10px] text-indigo-600 dark:text-indigo-300">
{translate('::ListForms.Wizard.Step4.SelectedColumns') || 'List'}
</span>
{isPopupField && (
<span className="rounded bg-emerald-50 dark:bg-emerald-900/30 px-1.5 py-0.5 text-[10px] text-emerald-600 dark:text-emerald-300">
{translate('::ListForms.Wizard.Step4.EditingForm') || 'Popup Form'}
</span>
)}
{isKeyField && (
<span className="rounded bg-amber-50 dark:bg-amber-900/30 px-1.5 py-0.5 text-[10px] text-amber-600 dark:text-amber-300">
{translate('::ListForms.Wizard.Step4.KeyField')}
</span>
)}
<span className="rounded bg-gray-100 dark:bg-gray-800 px-1.5 py-0.5 text-[10px] text-gray-400">
{item.editorType}
</span>
<span className="rounded bg-gray-100 dark:bg-gray-800 px-1.5 py-0.5 text-[10px] text-gray-400">
span-{item.colSpan}
{item.isRequired && <span className="ml-1 text-red-400">*</span>}
</span>
</div>
</div>
)
})
)}
</div>
</div>
))}
{ungroupedSelectedColumns.length > 0 && (
<div className="rounded-lg border border-amber-100 dark:border-amber-900/40 bg-amber-50/50 dark:bg-amber-900/10 px-3 py-2">
<div className="mb-1.5 text-xs font-semibold text-amber-700 dark:text-amber-300">
{translate('::ListForms.Wizard.Step4.UngroupedColumns') || 'Ungrouped columns'}
</div>
<div className="flex flex-wrap gap-1.5">
{ungroupedSelectedColumns.map((col) => {
const meta = selectCommandColumns.find((c) => c.columnName === col)
return (
<span
key={col}
className="inline-flex items-center gap-1 rounded-full border border-amber-200 dark:border-amber-800 bg-white dark:bg-gray-900 px-2 py-0.5 text-xs text-amber-700 dark:text-amber-300"
>
{col}
{meta?.dataType && (
<span className="text-[10px] text-amber-500 opacity-70">
{meta.dataType}
</span>
)}
</span>
)
})}
</div>
</div>
)}
</div>
</Section>
{subForms.length > 0 && (
<Section
title={translate('::ListForms.ListFormEdit.SubForms') || 'Sub Forms'}
badge={subForms.length}
>
<div className="flex flex-col gap-3">
{subForms.map((subForm, index) => (
<div
key={`${subForm.code}-${index}`}
className="rounded-lg border border-gray-100 dark:border-gray-800 px-3 py-2"
>
<Row label="Tab Title" value={subForm.tabTitle} />
<Row label="List Form Code" value={subForm.code} />
<Row label="Tab Type" value={subForm.tabType} />
<Row label="Refresh" value={subForm.isRefresh ? 'Active' : 'Inactive'} />
{(subForm.relation || []).length > 0 && (
<div className="pt-2">
<span className="text-xs text-gray-400">Relations</span>
<div className="mt-1 flex flex-wrap gap-1.5">
{subForm.relation.map((relation, relationIndex) => (
<span
key={`${relation.parentFieldName}-${relation.childFieldName}-${relationIndex}`}
className="rounded bg-gray-100 dark:bg-gray-800 px-2 py-0.5 text-xs text-gray-600 dark:text-gray-300"
>
{relation.parentFieldName} : {relation.childFieldName}
</span>
))}
</div>
</div>
)}
</div>
))}
</div>
</Section>
)}
{widgets.length > 0 && (
<Section
title={translate('::ListForms.ListFormEdit.TabWidgets') || 'Widgets'}
badge={widgets.length}
>
<div className="flex flex-col gap-3">
{widgets.map((widget, index) => (
<div
key={`${widget.title}-${widget.value}-${index}`}
className="rounded-lg border border-gray-100 dark:border-gray-800 px-3 py-2"
>
<Row label="Title Field" value={widget.title} />
<Row label="Value Field" value={widget.value} />
<Row label="Color Field" value={widget.color} />
<Row label="Icon Field" value={widget.icon} />
<Row label="Column Gap" value={widget.colGap} />
<Row label="Column Span" value={widget.colSpan} />
<Row label="Class Name" value={widget.className} />
<Row label="Value Class Name" value={widget.valueClassName} />
<Row label="Status" value={widget.isActive ? 'Active' : 'Inactive'} />
<Row label="Sql Query" value={widget.sqlQuery} />
</div>
))}
</div>
</Section>
)}
{(hasWorkflowFields || workflowItems.length > 0) && (
<Section
title={translate('::ListForms.ListFormEdit.TabWorkflow') || 'Workflow'}
badge={workflowItems.length}
>
<div className="grid grid-cols-2 gap-3">
<div>
<Row label="Approval User" value={workflow.approvalUserFieldName} />
<Row label="Approval Date" value={workflow.approvalDateFieldName} />
<Row label="Approval Status" value={workflow.approvalStatusFieldName} />
<Row label="Approval Description" value={workflow.approvalDescriptionFieldName} />
<Row
label="Is Filter User Name?"
value={workflow.isFilterUserName === true ? 'Yes' : 'No'}
/>
</div>
{workflowItems.length > 0 && (
<div className="flex flex-col gap-2">
{workflowItems.map((criteria, index) => (
<div
key={criteria.id || index}
className="rounded bg-gray-50 dark:bg-gray-800 px-3 py-2"
>
<div className="text-xs font-semibold text-gray-700 dark:text-gray-200">
{criteria.title || criteria.kind || `Criteria ${index + 1}`}
</div>
{criteria.kind === 'Compare' && (
<div className="mt-1 text-[11px] text-gray-500 dark:text-gray-400">
{criteria.compareColumn} {criteria.compareOperator}{' '}
{criteria.compareValue}
</div>
)}
{(criteria.kind === 'Approval' || criteria.kind === 'Inform') &&
criteria.approver && (
<div className="text-[11px] text-gray-500 dark:text-gray-400">
Approver: {formatWorkflowApprovers(criteria.approver)}
</div>
)}
</div>
))}
</div>
)}
</div>
</Section>
)}
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
Liste alanlarını Todo kartının başlık, durum ve diğer özellikleriyle eşleştirin.
</p>
</div>
{/* ── Right: Deploy ──────────────────────────────────────────── */}
<div className="sticky top-4 flex flex-col gap-3 max-h-[calc(100vh-200px)]">
{/* Stats */}
<div className="grid grid-cols-7 gap-2">
{[
{ label: translate('::ListForms.Wizard.Step4.StatGroup'), value: groups.length },
{ label: translate('::ListForms.Wizard.Step4.StatField'), value: totalFields },
{
label: translate('::ListForms.Wizard.Step4.EditingForm') || 'Popup Form',
value: editingFormFields.length,
},
{
label: translate('::App.Listform.ListformField.Column'),
value: selectedColumns.size,
},
{
label: translate('::ListForms.ListFormEdit.SubForms') || 'Sub Forms',
value: subForms.length,
},
{
label: translate('::ListForms.ListFormEdit.TabWidgets') || 'Widgets',
value: widgets.length,
},
{
label: translate('::ListForms.ListFormEdit.TabWorkflow') || 'Workflow',
value: workflowItems.length,
},
].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>
))}
{!values.todo ? (
<div className="rounded-xl border border-amber-200 bg-amber-50 p-4 text-sm text-amber-700 dark:border-amber-900 dark:bg-amber-950/30 dark:text-amber-200">
Todo görünümü liste ayarlarında kapalı. Bu adımı atlayabilir veya önceki adımlardan Todo
görünümünü etkinleştirebilirsiniz.
</div>
{/* 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 className="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3">
{mappings.map((mapping) => (
<FormItem
key={mapping.name}
label={`${mapping.label}${mapping.required ? ' *' : ''}`}
>
<Field name={mapping.name}>
{({ field, form }: FieldProps) => (
<Select
field={field}
form={form}
isClearable={!mapping.required}
options={fieldOptions}
value={fieldOptions.find((option) => option.value === values[mapping.name])}
onChange={(option) => {
form.setFieldValue(field.name, option?.value ?? '')
setValidationError('')
}}
/>
)}
</Field>
</FormItem>
))}
</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 className="mt-5 grid grid-cols-1 gap-4 md:grid-cols-2">
<FormItem label="Durum sırası (virgülle ayırın)">
<Field
name="todoStatusOrder"
component={Input}
placeholder="Inbox, Today, This Week, This Month, Later, Done"
/>
</FormItem>
<FormItem label="Kartları kolonlar arasında sürüklemeye izin ver">
<Field name="todoAllowDragging">
{({ field }: FieldProps<boolean>) => (
<Checkbox
checked={Boolean(field.value)}
onChange={(checked) => setFieldValue(field.name, checked)}
/>
)}
</div>
)}
</Field>
</FormItem>
</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">
{validationError && <p className="mt-4 text-sm text-red-500">{validationError}</p>}
<div className="fixed bottom-0 left-0 right-0 z-10 flex h-16 items-center border-t border-gray-200 bg-white px-6 dark:border-gray-700 dark:bg-gray-900">
<div className="flex w-full items-center justify-between gap-3">
<Button size="sm" variant="default" type="button" icon={<FaArrowLeft />} onClick={onBack}>
{translate('::Back') || 'Geri'}
</Button>
<Button
size="sm"
variant="default"
variant="solid"
type="button"
icon={<FaArrowLeft />}
onClick={onBack}
disabled={isDeploying}
icon={<FaArrowRight />}
onClick={continueToDeploy}
>
{translate('::Back') || 'Back'}
{translate('::Next') || 'Deploy adımına geç'}
</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>

View file

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

View file

@ -189,8 +189,8 @@ export default ${pascalCaseName}Component;`
>
<FaRegSave className="w-4 h-4" />
{isSubmitting
? translate('::App.DeveloperKit.ComponentEditor.Saving')
: translate('::App.DeveloperKit.ComponentEditor.Save')}
? translate('::Saving')
: translate('::Save')}
</Button>
</div>
</div>

View file

@ -270,6 +270,99 @@ const WORKFLOW_COLUMNS: ColumnDefinition[] = [
},
]
const TODO_COLUMNS: ColumnDefinition[] = [
{
id: '__TodoId',
columnName: 'Id',
dataType: 'uniqueidentifier',
maxLength: '',
isNullable: false,
defaultValue: 'NEWID()',
description: 'Todo primary key',
},
{
id: '__TodoTitle',
columnName: 'Title',
dataType: 'nvarchar',
maxLength: '300',
isNullable: false,
defaultValue: '',
description: 'Todo title',
},
{
id: '__TodoStatus',
columnName: 'Status',
dataType: 'nvarchar',
maxLength: '50',
isNullable: false,
defaultValue: "N'Inbox'",
description: 'Todo board column/status',
},
{
id: '__TodoDescription',
columnName: 'Description',
dataType: 'nvarchar(MAX)',
maxLength: '',
isNullable: true,
defaultValue: '',
description: 'Todo description',
},
{
id: '__TodoDueDate',
columnName: 'DueDate',
dataType: 'datetime2',
maxLength: '',
isNullable: true,
defaultValue: '',
description: 'Todo deadline',
},
{
id: '__TodoTags',
columnName: 'Tags',
dataType: 'nvarchar',
maxLength: '500',
isNullable: true,
defaultValue: '',
description: 'Comma-separated todo tags',
},
{
id: '__TodoAssignees',
columnName: 'Assignees',
dataType: 'nvarchar',
maxLength: '500',
isNullable: true,
defaultValue: '',
description: 'Comma-separated todo assignees',
},
{
id: '__TodoPriority',
columnName: 'Priority',
dataType: 'nvarchar',
maxLength: '50',
isNullable: true,
defaultValue: '',
description: 'Todo priority',
},
{
id: '__TodoCompleted',
columnName: 'Completed',
dataType: 'bit',
maxLength: '',
isNullable: false,
defaultValue: '0',
description: 'Todo completion flag',
},
{
id: '__TodoSortOrder',
columnName: 'SortOrder',
dataType: 'int',
maxLength: '',
isNullable: false,
defaultValue: '0',
description: 'Todo order inside its board column',
},
]
// ─── T-SQL Generator ──────────────────────────────────────────────────────────
function colToSqlLine(col: ColumnDefinition, addComma = true): string {
@ -1281,6 +1374,35 @@ const SqlTableDesignerDialog = ({
}
}
const addTodoColumns = () => {
const existingNames = new Set(columns.map((c) => c.columnName.trim().toLowerCase()))
const toAdd = TODO_COLUMNS.filter((c) => !existingNames.has(c.columnName.toLowerCase()))
if (toAdd.length > 0) {
setColumns((prev) => {
const nonEmpty = prev.filter((c) => c.columnName.trim() !== '')
return [...nonEmpty, ...toAdd.map((c) => ({ ...c })), createEmptyColumn()]
})
}
setIndexes((prev) => {
if (prev.some((index) => index.indexType === 'PrimaryKey')) return prev
const tableName = settings.tableName || initialTableData?.tableName || 'Table'
return [
...prev,
{
id: `auto-todo-pk-${crypto.randomUUID()}`,
indexName: `PK_${tableName}`,
indexType: 'PrimaryKey',
isClustered: false,
columns: [{ columnName: 'Id', order: 'ASC' }],
description: 'Todo primary key (auto)',
},
]
})
}
const importColumnsFromRememberedCreateTable = async () => {
let script = ''
@ -1749,6 +1871,9 @@ const SqlTableDesignerDialog = ({
<Button size="xs" variant="solid" color="indigo-600" onClick={addWorkflowColumns}>
{translate('::App.SqlQueryManager.AddWorkflowColumns')}
</Button>
<Button size="xs" variant="solid" color="cyan-600" onClick={addTodoColumns}>
Add Todo Columns
</Button>
<Button
size="xs"
variant="solid"
@ -2406,7 +2531,7 @@ const SqlTableDesignerDialog = ({
color="indigo-600"
className="hover:!bg-indigo-700 disabled:opacity-50 !rounded-lg transition-colors"
>
{translate('::App.SqlQueryManager.Save')}
{translate('::Save')}
</Button>
</div>
</div>
@ -2756,7 +2881,7 @@ const SqlTableDesignerDialog = ({
color="indigo-600"
className="hover:!bg-indigo-700 disabled:opacity-50 !rounded-lg transition-colors"
>
{translate('::App.SqlQueryManager.Save')}
{translate('::Save')}
</Button>
</div>
</div>
@ -2792,7 +2917,7 @@ const SqlTableDesignerDialog = ({
// ── Render ─────────────────────────────────────────────────────────────────
return (
<Dialog isOpen={isOpen} onClose={handleClose} onRequestClose={handleClose} width={1100}>
<Dialog isOpen={isOpen} onClose={handleClose} onRequestClose={handleClose} width={1200}>
<Dialog.Header className="flex flex-col gap-2 -mt-5 p-2 ">
{/* Header */}
<div className="flex items-center gap-3 border-b pb-3 flex-shrink-0">

View file

@ -13,7 +13,7 @@ import {
import { getList } from '@/services/form.service'
import { useLocalization } from '@/utils/hooks/useLocalization'
import { usePermission } from '@/utils/hooks/usePermission'
import { useStoreState } from '@/store'
import { useStoreActions, useStoreState } from '@/store'
import DxCardView, {
CardViewRef,
CardViewTypes,
@ -59,6 +59,9 @@ interface CardViewProps {
gridDto?: GridDto
}
const cardColumnCountOptions = [1, 2, 3, 4, 5] as const
const defaultCardColumnCount = 3
const flattenColumns = (columns: any[] = []): any[] =>
columns.flatMap((column) => (column?.columns?.length ? flattenColumns(column.columns) : [column]))
@ -158,6 +161,11 @@ const getValueByField = (data: Record<string, any>, field?: string | null) => {
return key ? data[key] : undefined
}
const getEditingFieldName = (dataField?: string | null) =>
String(dataField ?? '')
.split(':')[0]
.trim()
const lookupDisplayValuesEqual = (
left: Record<string, Map<string, string>>,
right: Record<string, Map<string, string>>,
@ -195,11 +203,11 @@ const loadCascadeLookupDisplayValues = async (
.filter((column) => column.colData?.lookupDto?.cascadeParentFields)
.map(async (column) => {
const lookup = column.colData.lookupDto
const parentFields = lookup.cascadeParentFields
const parentFields: string[] = String(lookup.cascadeParentFields)
.split(',')
.map((field: string) => field.trim())
.map((field) => field.trim())
.filter(Boolean)
const parentValueLists = parentFields.map((field: string) => {
const parentValueLists: string[][] = parentFields.map((field) => {
const values = Object.entries(knownValues).find(
([key]) => key.toLowerCase() === field.toLowerCase(),
)?.[1]
@ -340,9 +348,7 @@ const deduplicateCardStoreLoads = (store: CustomStore<any, any>) => {
if (cachedCount !== undefined) return Promise.resolve(cachedCount)
const pendingCount = pendingTotalCounts.get(countKey)
return pendingCount
? pendingCount.then((count) =>
count !== undefined ? count : originalTotalCount(options),
)
? pendingCount.then((count) => (count !== undefined ? count : originalTotalCount(options)))
: originalTotalCount(options)
}
}
@ -371,6 +377,15 @@ const CardView = (props: CardViewProps) => {
const { translate } = useLocalization()
const { checkPermission } = usePermission()
const currentUser = useStoreState((state) => state.auth.user)
const savedListState = useStoreState((state) =>
state.admin.lists.states.find((item) => item.listFormCode === listFormCode),
)
const cardColumnCount = savedListState?.cardColumnCount ?? defaultCardColumnCount
const setListState = useStoreActions((actions) => actions.admin.lists.setStates)
const currentCulture = useStoreState(
(state) => state.abpConfig.config?.localization.currentCulture.cultureName,
)
const localizationTexts = useStoreState((state) => state.abpConfig.texts)
const useMobileEditPopup = isMobileViewport() || isTouchLikeDevice()
const cardViewRef = useRef<CardViewRef>()
const searchParamsRef = useRef(searchParams)
@ -472,6 +487,16 @@ const CardView = (props: CardViewProps) => {
const reload = cardDataSource?.reload()
Promise.resolve(reload).finally(() => refreshParentData?.())
}, [refreshParentData])
const setCardColumnCount = useCallback(
(cardColumnCount: number) => {
setListState({
listFormCode,
layout: savedListState?.layout ?? 'card',
cardColumnCount,
})
},
[listFormCode, savedListState?.layout, setListState],
)
const listFormColumnsRef = useRef({
current: {
instance: () => ({ refresh: refreshData }),
@ -498,21 +523,45 @@ const CardView = (props: CardViewProps) => {
gridRef: listFormColumnsRef,
onShowNote: openNotePanel,
})
const listColumnsCacheRef = useRef<{ gridDto?: GridDto; columns: any[] }>({ columns: [] })
if (listColumnsCacheRef.current.gridDto !== gridDto) {
const localizationRevisionRef = useRef({
culture: currentCulture,
texts: localizationTexts,
value: 0,
})
if (
localizationRevisionRef.current.culture !== currentCulture ||
localizationRevisionRef.current.texts !== localizationTexts
) {
localizationRevisionRef.current = {
culture: currentCulture,
texts: localizationTexts,
value: localizationRevisionRef.current.value + 1,
}
}
const localizationRevision = localizationRevisionRef.current.value
const listColumnsCacheRef = useRef<{
gridDto?: GridDto
localizationRevision?: number
columns: any[]
}>({ columns: [] })
if (
listColumnsCacheRef.current.gridDto !== gridDto ||
listColumnsCacheRef.current.localizationRevision !== localizationRevision
) {
listColumnsCacheRef.current = {
gridDto,
localizationRevision,
columns: flattenColumns(getBandedColumns()),
}
}
const listColumns = listColumnsCacheRef.current.columns
const cardColumnsCacheRef = useRef<{
gridDto?: GridDto
listColumns?: any[]
columns: CardViewTypes.ColumnProperties[]
}>({ columns: [] })
if (cardColumnsCacheRef.current.gridDto !== gridDto) {
if (cardColumnsCacheRef.current.listColumns !== listColumns) {
cardColumnsCacheRef.current = {
gridDto,
listColumns,
columns: listColumns
.filter((column) => column?.type !== 'buttons' && column?.dataField)
.map((column) => toCardColumn(column, lookupDisplayValuesRef)),
@ -526,26 +575,33 @@ const CardView = (props: CardViewProps) => {
commandButtonsRef.current = commandButtons
const renderImageUploadEditor = useCallback((templateData: any) => {
const dataField =
const configuredDataField =
templateData?.dataField ??
templateData?.item?.dataField ??
templateData?.editorOptions?.name ??
templateData?.name
const dataField = getEditingFieldName(configuredDataField)
const column = listColumnsCacheRef.current.columns.find(
(candidate) =>
candidate.dataField?.toLowerCase() === String(dataField ?? '').toLowerCase(),
(candidate) => candidate.dataField?.toLowerCase() === String(dataField ?? '').toLowerCase(),
)
const component = templateData?.component
const currentFormData = component?.option?.('formData') ?? formDataRef.current ?? {}
const editorValue = templateData?.editorOptions?.value
return (
<ImageUploadEditorComponent
{...templateData}
column={column}
value={getValueByField(currentFormData, dataField)}
value={
editorValue !== undefined ? editorValue : getValueByField(currentFormData, dataField)
}
setValue={(value: any) => {
if (!dataField) return
component?.updateData?.(dataField, value)
if (typeof templateData?.editorOptions?.onValueChanged === 'function') {
templateData.editorOptions.onValueChanged({ value })
} else {
component?.updateData?.(dataField, value)
}
formDataRef.current = { ...currentFormData, [dataField]: value }
}}
/>
@ -612,9 +668,7 @@ const CardView = (props: CardViewProps) => {
?.split(',')
.map((field: string) => field.trim())
.filter(Boolean)
const disabled = parentFields?.some(
(field: string) => !getValueByField(nextData, field),
)
const disabled = parentFields?.some((field: string) => !getValueByField(nextData, field))
const editor = event.component?.getEditor?.(childField)
editor?.option?.({ dataSource: source, disabled: Boolean(disabled), value: null })
@ -627,13 +681,10 @@ const CardView = (props: CardViewProps) => {
[cascadeChildrenByParent],
)
const applyLookupDisplayValues = useCallback(
(values: Record<string, Map<string, string>>) => {
if (lookupDisplayValuesEqual(lookupDisplayValuesRef.current, values)) return
lookupDisplayValuesRef.current = values
},
[],
)
const applyLookupDisplayValues = useCallback((values: Record<string, Map<string, string>>) => {
if (lookupDisplayValuesEqual(lookupDisplayValuesRef.current, values)) return
lookupDisplayValuesRef.current = values
}, [])
const getLookupColumns = useCallback(
() =>
@ -677,9 +728,7 @@ const CardView = (props: CardViewProps) => {
Promise.all([
loadLookupDisplayValues(columns),
loadCascadeLookupDisplayValues(columns, rows),
]).then(([values, cascadeValues]) =>
applyLookupDisplayValues({ ...values, ...cascadeValues }),
)
]).then(([values, cascadeValues]) => applyLookupDisplayValues({ ...values, ...cascadeValues }))
}, [applyLookupDisplayValues, getLookupColumns, loadLookupDisplayValues])
const { createSelectDataSource } = useListFormCustomDataSource({ gridRef: cardViewRef })
@ -809,8 +858,8 @@ const CardView = (props: CardViewProps) => {
return {
itemType: 'simple',
dataField: item.dataField,
name: item.dataField,
dataField: fieldName,
name: fieldName,
colSpan: item.colSpan,
label: {
text: item.dataField.includes(':')
@ -896,59 +945,56 @@ const CardView = (props: CardViewProps) => {
cardViewRef.current?.instance().getDataSource()?.reload()
}, [mergedExtraFilter])
const renderCardHeader = useCallback(
(templateOptions: any, suppliedContainer?: HTMLElement) => {
const templateData = templateOptions?.model ?? templateOptions
const card = templateData?.card ?? templateData
const row = { data: card?.data ?? {}, isEditing: false, key: card?.key }
const templateContainer = templateOptions?.container ?? suppliedContainer
const domContainer =
templateContainer instanceof HTMLElement ? templateContainer : templateContainer?.get?.(0)
domContainer?.replaceChildren()
const actionsElement = document.createElement('div')
actionsElement.className =
'flex min-h-10 w-full flex-wrap items-center justify-end gap-1 px-2 py-1'
const renderCardHeader = useCallback((templateOptions: any, suppliedContainer?: HTMLElement) => {
const templateData = templateOptions?.model ?? templateOptions
const card = templateData?.card ?? templateData
const row = { data: card?.data ?? {}, isEditing: false, key: card?.key }
const templateContainer = templateOptions?.container ?? suppliedContainer
const domContainer =
templateContainer instanceof HTMLElement ? templateContainer : templateContainer?.get?.(0)
domContainer?.replaceChildren()
const actionsElement = document.createElement('div')
actionsElement.className =
'flex min-h-10 w-full flex-wrap items-center justify-end gap-1 px-2 py-1'
commandButtonsRef.current.forEach((action: any, index: number) => {
let visible = action.visible ?? true
if (typeof visible === 'function') {
try {
visible = visible({ row, card })
} catch {
visible = false
}
commandButtonsRef.current.forEach((action: any, index: number) => {
let visible = action.visible ?? true
if (typeof visible === 'function') {
try {
visible = visible({ row, card })
} catch {
visible = false
}
if (!visible) return
}
if (!visible) return
const name = String(action.name ?? action.hint ?? `action-${index}`)
const button = document.createElement('button')
button.type = 'button'
button.className =
'rounded px-2 py-1 text-xs font-medium hover:bg-gray-100 dark:hover:bg-gray-700'
button.textContent = action.text || action.hint || name
button.title = action.hint ?? action.text ?? name
button.addEventListener('click', (event) => {
event.stopPropagation()
if (name === 'edit') {
cardViewRef.current?.instance().editCard(card.index)
} else if (name === 'delete') {
cardViewRef.current?.instance().deleteCard(card.index)
} else {
action.onClick?.({
event,
row,
card,
component: cardViewRef.current?.instance(),
})
}
})
actionsElement.appendChild(button)
const name = String(action.name ?? action.hint ?? `action-${index}`)
const button = document.createElement('button')
button.type = 'button'
button.className =
'rounded px-2 py-1 text-xs font-medium hover:bg-gray-100 dark:hover:bg-gray-700'
button.textContent = action.text || action.hint || name
button.title = action.hint ?? action.text ?? name
button.addEventListener('click', (event) => {
event.stopPropagation()
if (name === 'edit') {
cardViewRef.current?.instance().editCard(card.index)
} else if (name === 'delete') {
cardViewRef.current?.instance().deleteCard(card.index)
} else {
action.onClick?.({
event,
row,
card,
component: cardViewRef.current?.instance(),
})
}
})
actionsElement.appendChild(button)
})
return actionsElement
},
[],
)
return actionsElement
}, [])
const editingOptions = gridDto?.gridOptions.editingOptionDto
const selectionOptions = gridDto?.gridOptions.selectionDto
@ -1000,7 +1046,7 @@ const CardView = (props: CardViewProps) => {
) : (
<div className="p-1">
<DxCardView
key={`CardView-${listFormCode}`}
key={`CardView-${listFormCode}-${localizationRevision}`}
ref={cardViewRef as any}
fieldHintEnabled
id={`CardView-${listFormCode}`}
@ -1009,6 +1055,9 @@ const CardView = (props: CardViewProps) => {
height={gridDto.gridOptions.height > 0 ? gridDto.gridOptions.height : undefined}
width={gridDto.gridOptions.width || '100%'}
cardMinWidth={280}
cardsPerRow={cardColumnCount}
allowColumnReordering={gridDto.gridOptions.columnOptionDto?.allowColumnReordering}
rtlEnabled={gridDto.gridOptions.columnOptionDto?.rtlEnabled}
className="list-card-view"
hoverStateEnabled={gridDto.gridOptions.columnOptionDto?.hoverStateEnabled}
remoteOperations={{ paging: true, filtering: true, sorting: true }}
@ -1016,14 +1065,18 @@ const CardView = (props: CardViewProps) => {
onContentReady={onContentReady}
onSelectionChanged={onSelectionChanged}
onInitNewCard={(event) => {
setFormData((event.data as Record<string, any>) ?? {})
const nextFormData = (event.data as Record<string, any>) ?? {}
formDataRef.current = nextFormData
setFormData(nextFormData)
setMode('new')
setIsPopupFullScreen(
useMobileEditPopup || (editingOptions?.popup?.fullScreen ?? false),
)
}}
onEditingStart={(event) => {
setFormData((event.data as Record<string, any>) ?? {})
const nextFormData = (event.data as Record<string, any>) ?? {}
formDataRef.current = nextFormData
setFormData(nextFormData)
setMode('edit')
setIsPopupFullScreen(
useMobileEditPopup || (editingOptions?.popup?.fullScreen ?? false),
@ -1137,7 +1190,29 @@ const CardView = (props: CardViewProps) => {
],
}}
/>
<Toolbar visible={orderedToolbarData.length > 0}>
<Toolbar visible>
{cardColumnCountOptions.map((columnCount) => {
const selected = cardColumnCount === columnCount
return (
<ToolbarItem
key={`card-column-count-${columnCount}`}
location="before"
locateInMenu="never"
widget="dxButton"
options={{
text: String(columnCount),
hint: String(columnCount),
type: selected ? 'default' : 'normal',
stylingMode: selected ? 'contained' : 'text',
elementAttr: {
'aria-label': String(columnCount),
'aria-pressed': selected,
},
onClick: () => setCardColumnCount(columnCount),
}}
/>
)
})}
{orderedToolbarData.map((item, index) => (
<ToolbarItem key={`${item.name ?? 'toolbar'}-${index}`} {...(item as any)} />
))}
@ -1160,8 +1235,20 @@ const CardView = (props: CardViewProps) => {
width={gridDto.gridOptions.searchPanelDto.width}
placeholder={translate('::App.Search')}
/>
<FilterPanel visible={gridDto.gridOptions.filterPanelDto.visible} />
<HeaderFilter visible={gridDto.gridOptions.headerFilterDto.visible} />
<FilterPanel
visible={gridDto.gridOptions.filterPanelDto.visible}
filterEnabled={gridDto.gridOptions.filterPanelDto.filterEnabled}
texts={gridDto.gridOptions.filterPanelDto.texts}
/>
<HeaderFilter
visible={gridDto.gridOptions.headerFilterDto.visible}
width={gridDto.gridOptions.headerFilterDto.width}
height={gridDto.gridOptions.headerFilterDto.height}
search={{
enabled: gridDto.gridOptions.headerFilterDto.allowSearch,
timeout: gridDto.gridOptions.headerFilterDto.searchTimeout,
}}
/>
<Selection
mode={selectionOptions?.mode}
allowSelectAll={selectionOptions?.allowSelectAll}
@ -1175,6 +1262,8 @@ const CardView = (props: CardViewProps) => {
?.split(',')
.map(Number)}
showPageSizeSelector={gridDto.gridOptions.pagerOptionDto?.showPageSizeSelector}
showInfo={gridDto.gridOptions.pagerOptionDto?.showInfo}
infoText={gridDto.gridOptions.pagerOptionDto?.infoText}
showNavigationButtons={gridDto.gridOptions.pagerOptionDto?.showNavigationButtons}
displayMode={gridDto.gridOptions.pagerOptionDto?.displayMode}
/>
@ -1182,7 +1271,10 @@ const CardView = (props: CardViewProps) => {
enabled={gridDto.gridOptions.columnOptionDto?.columnChooserEnabled}
mode={gridDto.gridOptions.columnOptionDto?.columnChooserMode}
/>
<LoadPanel visible={gridDto.gridOptions.pagerOptionDto?.loadPanelEnabled !== false} />
<LoadPanel
visible={gridDto.gridOptions.pagerOptionDto?.loadPanelEnabled !== false}
message={gridDto.gridOptions.pagerOptionDto?.loadPanelText}
/>
</DxCardView>
{gridDto.gridOptions.subFormsDto?.length > 0 &&

View file

@ -376,7 +376,7 @@ const ChartDrawer = ({
<div className="flex items-center justify-center gap-2">
{isSubmitting
? translate('::Saving')
: translate('::App.Platform.ChartDrawer.Save')}
: translate('::Save')}
</div>
</Button>
</div>

View file

@ -21,6 +21,7 @@ import {
FaTable,
FaCalendarAlt,
FaProjectDiagram,
FaTasks,
} from 'react-icons/fa'
/* =======================
@ -33,6 +34,7 @@ const Tree = React.lazy(() => import('./Tree'))
const Chart = React.lazy(() => import('./Chart'))
const GanttView = React.lazy(() => import('./GanttView'))
const SchedulerView = React.lazy(() => import('./SchedulerView'))
const TodoBoard = React.lazy(() => import('./TodoBoard'))
function isLayoutValid(dto: GridDto, layout: ListViewLayoutType | undefined): boolean {
if (!layout || layout === 'grid') return true
@ -62,6 +64,12 @@ function isLayoutValid(dto: GridDto, layout: ListViewLayoutType | undefined): bo
dto.gridOptions?.schedulerOptionDto?.startDateExpr &&
dto.gridOptions?.schedulerOptionDto?.endDateExpr
)
if (layout === 'todo')
return !!(
dto.gridOptions?.layoutDto?.todo &&
dto.gridOptions?.todoOptionDto?.titleExpr &&
dto.gridOptions?.todoOptionDto?.statusExpr
)
return false
}
@ -134,6 +142,7 @@ const List: React.FC = () => {
chart: () => import('./Chart'),
gantt: () => import('./GanttView'),
scheduler: () => import('./SchedulerView'),
todo: () => import('./TodoBoard'),
}),
[],
)
@ -161,6 +170,17 @@ const List: React.FC = () => {
VIEW BUTTONS
======================= */}
<div className="flex gap-1 ml-auto">
{gridDto?.gridOptions?.layoutDto.todo &&
gridDto?.gridOptions?.todoOptionDto?.titleExpr &&
gridDto?.gridOptions?.todoOptionDto?.statusExpr && (
<Button
size="sm"
icon={<FaTasks />}
variant={viewMode === 'todo' ? 'solid' : 'default'}
onClick={() => setLayout('todo')}
onMouseEnter={() => preload.todo()}
/>
)}
{gridDto?.gridOptions?.layoutDto.scheduler &&
gridDto?.gridOptions?.schedulerOptionDto?.textExpr &&
gridDto?.gridOptions?.schedulerOptionDto?.startDateExpr && (
@ -295,6 +315,15 @@ const List: React.FC = () => {
/>
)}
{viewMode === 'todo' && (
<TodoBoard
listFormCode={listFormCode}
searchParams={searchParams}
isSubForm={false}
gridDto={gridDto}
/>
)}
{viewMode === 'chart' && (
<Chart
id={gridDto?.gridOptions.id as string}

File diff suppressed because it is too large Load diff

View file

@ -20,7 +20,7 @@ type PendingSave = {
type UseListFormStateStoringParams = {
listFormCode: string
storageKey?: string | null
filterPrefix: 'list' | 'tree' | 'pivot'
filterPrefix: 'list' | 'tree' | 'pivot' | 'todo'
skipSaveRef?: MutableRefObject<boolean>
showSaveToast?: boolean
saveDebounceMs?: number
@ -151,7 +151,14 @@ export const useListFormStateStoring = ({
return pendingSaveRef.current.promise
},
[filterName, listFormCode, saveDebounceMs, showSaveErrorToast, showSaveSuccessToast, skipSaveRef],
[
filterName,
listFormCode,
saveDebounceMs,
showSaveErrorToast,
showSaveSuccessToast,
skipSaveRef,
],
)
const customLoadState = useCallback(() => {

View file

@ -145,7 +145,7 @@ const DesignerDrawer: React.FC<DesignerDrawerProps> = ({
<div className="flex items-center justify-between border-t border-slate-200 dark:border-gray-700 px-5 py-4">
<Button variant="solid" block onClick={onSave}>
{translate('::Public.designer.save')}
{translate('::Save')}
</Button>
</div>
</div>