LisformWorkflow çalışması
This commit is contained in:
parent
7b0f4acced
commit
73cb479e50
22 changed files with 1058 additions and 753 deletions
|
|
@ -1,9 +1,8 @@
|
|||
using System;
|
||||
|
||||
namespace Sozsoft.Platform.ListForms;
|
||||
namespace Sozsoft.Platform.ListForms;
|
||||
|
||||
public class WorkflowDto
|
||||
{
|
||||
public string ApprovalFieldName { get; set; }
|
||||
public DateTime ApprovalDateFieldName { get; set; }
|
||||
public string ApprovalDateFieldName { get; set; }
|
||||
public string ApprovalStatusFieldName { get; set; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ namespace Sozsoft.Platform.ListForms.Workflow;
|
|||
|
||||
public class CreateUpdateListFormWorkflowCriteriaDto
|
||||
{
|
||||
public Guid? Id { get; set; }
|
||||
public string Id { get; set; }
|
||||
public string ListFormCode { get; set; }
|
||||
public string Kind { get; set; }
|
||||
public string Title { get; set; }
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ public interface IListFormWorkflowAppService : IApplicationService
|
|||
{
|
||||
Task<ListFormWorkflowStateDto> GetStateAsync(string listFormCode = null);
|
||||
Task<ListFormWorkflowCriteriaDto> SaveCriteriaAsync(CreateUpdateListFormWorkflowCriteriaDto input);
|
||||
Task DeleteCriteriaAsync(Guid id);
|
||||
Task DeleteCriteriaAsync(string id);
|
||||
Task<ListFormWorkflowStateDto> ResetDemoAsync(string listFormCode = null);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ using Volo.Abp.Application.Dtos;
|
|||
|
||||
namespace Sozsoft.Platform.ListForms.Workflow;
|
||||
|
||||
public class ListFormWorkflowCriteriaDto : AuditedEntityDto<Guid>
|
||||
public class ListFormWorkflowCriteriaDto : AuditedEntityDto<string>
|
||||
{
|
||||
public string ListFormCode { get; set; }
|
||||
public string Kind { get; set; }
|
||||
|
|
|
|||
|
|
@ -16,10 +16,12 @@ namespace Sozsoft.Platform.ListForms.Workflow;
|
|||
public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowAppService
|
||||
{
|
||||
private const string DefaultListFormCode = "workflow";
|
||||
private const string CriteriaIdPrefix = "N";
|
||||
private const int CriteriaIdPadding = 3;
|
||||
|
||||
private readonly IRepository<ListFormWorkflow, Guid> criteriaRepository;
|
||||
private readonly IRepository<ListFormWorkflow, string> criteriaRepository;
|
||||
|
||||
public ListFormWorkflowAppService(IRepository<ListFormWorkflow, Guid> criteriaRepository)
|
||||
public ListFormWorkflowAppService(IRepository<ListFormWorkflow, string> criteriaRepository)
|
||||
{
|
||||
this.criteriaRepository = criteriaRepository;
|
||||
}
|
||||
|
|
@ -43,10 +45,10 @@ public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowA
|
|||
public async Task<ListFormWorkflowCriteriaDto> SaveCriteriaAsync(CreateUpdateListFormWorkflowCriteriaDto input)
|
||||
{
|
||||
var code = NormalizeListFormCode(input.ListFormCode);
|
||||
var isNew = !input.Id.HasValue || input.Id.Value == Guid.Empty;
|
||||
var isNew = input.Id.IsNullOrWhiteSpace();
|
||||
var criteria = isNew
|
||||
? new ListFormWorkflow(GuidGenerator.Create())
|
||||
: await criteriaRepository.GetAsync(input.Id.Value);
|
||||
? new ListFormWorkflow(await GenerateNextCriteriaIdAsync())
|
||||
: await criteriaRepository.GetAsync(input.Id);
|
||||
|
||||
if (!isNew && criteria.ListFormCode != code)
|
||||
{
|
||||
|
|
@ -89,7 +91,7 @@ public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowA
|
|||
}
|
||||
|
||||
[HttpDelete("criteria/{id}")]
|
||||
public async Task DeleteCriteriaAsync(Guid id)
|
||||
public async Task DeleteCriteriaAsync(string id)
|
||||
{
|
||||
var criteria = await criteriaRepository.GetAsync(id);
|
||||
await criteriaRepository.DeleteAsync(criteria, autoSave: true);
|
||||
|
|
@ -97,7 +99,7 @@ public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowA
|
|||
var remaining = await criteriaRepository.GetListAsync(x => x.ListFormCode == criteria.ListFormCode);
|
||||
foreach (var item in remaining)
|
||||
{
|
||||
var changed = ClearDeletedTarget(item, id.ToString());
|
||||
var changed = ClearDeletedTarget(item, id);
|
||||
if (changed)
|
||||
{
|
||||
await criteriaRepository.UpdateAsync(item, autoSave: true);
|
||||
|
|
@ -117,30 +119,30 @@ public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowA
|
|||
|
||||
var start = await CreateCriteriaAsync(code, "Start", "İş Akışı Başlat", 80, 150);
|
||||
var compare = await CreateCriteriaAsync(code, "Compare", "Tutar kontrolü", 330, 130);
|
||||
var approval = await CreateCriteriaAsync(code, "Approval", "Yönetici Onayı", 590, 60, "ayse.yilmaz");
|
||||
var inform = await CreateCriteriaAsync(code, "Inform", "Muhasebe Bilgilendirme", 590, 230, "muhasebe");
|
||||
var approval = await CreateCriteriaAsync(code, "Approval", "Yönetici Onayı", 590, 60, PlatformConsts.AbpIdentity.User.AdminEmailDefaultValue);
|
||||
var inform = await CreateCriteriaAsync(code, "Inform", "Muhasebe Bilgilendirme", 590, 230, PlatformConsts.AbpIdentity.User.AdminEmailDefaultValue);
|
||||
var end = await CreateCriteriaAsync(code, "End", "Akışı Bitir", 850, 150);
|
||||
|
||||
start.NextOnStart = compare.Id.ToString();
|
||||
compare.NextOnTrue = approval.Id.ToString();
|
||||
compare.NextOnFalse = inform.Id.ToString();
|
||||
start.NextOnStart = compare.Id;
|
||||
compare.NextOnTrue = approval.Id;
|
||||
compare.NextOnFalse = inform.Id;
|
||||
compare.CompareOutcomesJson = SerializeCompareOutcomes([
|
||||
new CompareOutcomeDto
|
||||
{
|
||||
Label = "Onay gerekir",
|
||||
TargetId = approval.Id.ToString(),
|
||||
TargetId = approval.Id,
|
||||
Conditions = [new WorkflowConditionDto { CompareColumn = "Tutar", CompareOperator = ">", CompareValue = 5000 }]
|
||||
},
|
||||
new CompareOutcomeDto
|
||||
{
|
||||
Label = "Bilgilendir",
|
||||
TargetId = inform.Id.ToString(),
|
||||
TargetId = inform.Id,
|
||||
Conditions = [new WorkflowConditionDto { CompareColumn = "Tutar", CompareOperator = "<=", CompareValue = 5000 }]
|
||||
}
|
||||
]);
|
||||
approval.NextOnApprove = inform.Id.ToString();
|
||||
approval.NextOnReject = end.Id.ToString();
|
||||
inform.NextOnStart = end.Id.ToString();
|
||||
approval.NextOnApprove = inform.Id;
|
||||
approval.NextOnReject = end.Id;
|
||||
inform.NextOnStart = end.Id;
|
||||
|
||||
await criteriaRepository.UpdateAsync(start, autoSave: true);
|
||||
await criteriaRepository.UpdateAsync(compare, autoSave: true);
|
||||
|
|
@ -158,7 +160,7 @@ public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowA
|
|||
int positionY,
|
||||
string approver = "")
|
||||
{
|
||||
var criteria = new ListFormWorkflow(GuidGenerator.Create())
|
||||
var criteria = new ListFormWorkflow(await GenerateNextCriteriaIdAsync())
|
||||
{
|
||||
ListFormCode = listFormCode,
|
||||
Kind = kind,
|
||||
|
|
@ -181,6 +183,43 @@ public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowA
|
|||
return criteria;
|
||||
}
|
||||
|
||||
private async Task<string> GenerateNextCriteriaIdAsync()
|
||||
{
|
||||
var criteria = await criteriaRepository.GetListAsync();
|
||||
var maxNumber = criteria
|
||||
.Select(x => TryParseCriteriaIdNumber(x.Id))
|
||||
.DefaultIfEmpty(0)
|
||||
.Max();
|
||||
|
||||
var nextNumber = maxNumber + 1;
|
||||
var nextId = FormatCriteriaId(nextNumber);
|
||||
var existingIds = criteria.Select(x => x.Id).ToHashSet();
|
||||
|
||||
while (existingIds.Contains(nextId))
|
||||
{
|
||||
nextNumber++;
|
||||
nextId = FormatCriteriaId(nextNumber);
|
||||
}
|
||||
|
||||
return nextId;
|
||||
}
|
||||
|
||||
private static int TryParseCriteriaIdNumber(string id)
|
||||
{
|
||||
if (id.IsNullOrWhiteSpace() ||
|
||||
!id.StartsWith(CriteriaIdPrefix, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
return int.TryParse(id[CriteriaIdPrefix.Length..], out var number) ? number : 0;
|
||||
}
|
||||
|
||||
private static string FormatCriteriaId(int number)
|
||||
{
|
||||
return $"{CriteriaIdPrefix}{number.ToString().PadLeft(CriteriaIdPadding, '0')}";
|
||||
}
|
||||
|
||||
private static bool ClearDeletedTarget(ListFormWorkflow criteria, string deletedId)
|
||||
{
|
||||
var changed = false;
|
||||
|
|
|
|||
|
|
@ -18673,6 +18673,78 @@
|
|||
"key": "SuccessfullySaved",
|
||||
"en": "Successfully Saved",
|
||||
"tr": "Başarıyla Kaydedildi"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "ListForms.ListFormEdit.Workflow.Criteria",
|
||||
"en": "Criteria",
|
||||
"tr": "Kriterler"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "ListForms.ListFormEdit.Workflow.CriteriaStart",
|
||||
"en": "Start",
|
||||
"tr": "Başlat"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "ListForms.ListFormEdit.Workflow.CriteriaCompare",
|
||||
"en": "Compare",
|
||||
"tr": "Karşılaştır"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "ListForms.ListFormEdit.Workflow.CriteriaApproval",
|
||||
"en": "Approval",
|
||||
"tr": "Onaylanacak Kişi"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "ListForms.ListFormEdit.Workflow.CriteriaInform",
|
||||
"en": "Information",
|
||||
"tr": "Bilgilendirilecek Kişi"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "ListForms.ListFormEdit.Workflow.CriteriaEnd",
|
||||
"en": "Finish",
|
||||
"tr": "Akışı Bitir"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "ListForms.ListFormEdit.Workflow.CriteriaTitleRule",
|
||||
"en": "Title / Rule",
|
||||
"tr": "Başlık / Kural"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "ListForms.ListFormEdit.Workflow.CriteriaConnections",
|
||||
"en": "Connections",
|
||||
"tr": "Bağlantılar"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "ListForms.ListFormEdit.WorkflowNoCriteria",
|
||||
"en": "No criteria defined for the selected workflow.",
|
||||
"tr": "Seçili iş akışı için açıklama kaydı yok."
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "ListForms.ListFormEdit.Workflow.ApprovalFieldName",
|
||||
"en": "Approval Field Name",
|
||||
"tr": "Onay Alanı Adı"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "ListForms.ListFormEdit.Workflow.ApprovalDateFieldName",
|
||||
"en": "Approval Date Field Name",
|
||||
"tr": "Onay Tarihi Alanı Adı"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "ListForms.ListFormEdit.Workflow.ApprovalStatusFieldName",
|
||||
"en": "Approval Status Field Name",
|
||||
"tr": "Onay Durumu Alanı Adı"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -3,13 +3,13 @@ using Volo.Abp.Domain.Entities;
|
|||
|
||||
namespace Sozsoft.Platform.Entities;
|
||||
|
||||
public class ListFormWorkflow : Entity<Guid>
|
||||
public class ListFormWorkflow : Entity<string>
|
||||
{
|
||||
protected ListFormWorkflow()
|
||||
{
|
||||
}
|
||||
|
||||
public ListFormWorkflow(Guid id) : base(id)
|
||||
public ListFormWorkflow(string id) : base(id)
|
||||
{
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -487,6 +487,7 @@ public class PlatformDbContext :
|
|||
b.ToTable(TableNameResolver.GetFullTableName(nameof(TableNameEnum.ListFormWorkflow)), Prefix.DbSchema);
|
||||
b.ConfigureByConvention();
|
||||
|
||||
b.Property(x => x.Id).HasMaxLength(50);
|
||||
b.Property(x => x.ListFormCode).IsRequired().HasMaxLength(64);
|
||||
b.Property(x => x.Kind).IsRequired().HasMaxLength(50);
|
||||
b.Property(x => x.Title).IsRequired().HasMaxLength(250);
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ using Volo.Abp.EntityFrameworkCore;
|
|||
namespace Sozsoft.Platform.Migrations
|
||||
{
|
||||
[DbContext(typeof(PlatformDbContext))]
|
||||
[Migration("20260522200739_Initial")]
|
||||
[Migration("20260523104659_Initial")]
|
||||
partial class Initial
|
||||
{
|
||||
/// <inheritdoc />
|
||||
|
|
@ -3414,8 +3414,9 @@ namespace Sozsoft.Platform.Migrations
|
|||
|
||||
modelBuilder.Entity("Sozsoft.Platform.Entities.ListFormWorkflow", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
b.Property<string>("Id")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("nvarchar(50)");
|
||||
|
||||
b.Property<string>("Approver")
|
||||
.IsRequired()
|
||||
|
|
@ -1393,7 +1393,7 @@ namespace Sozsoft.Platform.Migrations
|
|||
name: "Sas_H_ListFormWorkflow",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
Id = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: false),
|
||||
ListFormCode = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false),
|
||||
Kind = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: false),
|
||||
Title = table.Column<string>(type: "nvarchar(250)", maxLength: 250, nullable: false),
|
||||
|
|
@ -3411,8 +3411,9 @@ namespace Sozsoft.Platform.Migrations
|
|||
|
||||
modelBuilder.Entity("Sozsoft.Platform.Entities.ListFormWorkflow", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
b.Property<string>("Id")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("nvarchar(50)");
|
||||
|
||||
b.Property<string>("Approver")
|
||||
.IsRequired()
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ export const ListFormEditTabs = {
|
|||
StateForm: 'state',
|
||||
SubForm: 'subForm',
|
||||
Widget: 'widget',
|
||||
Workflow: 'workflow',
|
||||
Fields: 'fields',
|
||||
Customization: 'customization',
|
||||
ExtraFilter: 'extraFilter',
|
||||
|
|
|
|||
|
|
@ -663,6 +663,7 @@ export interface GridOptionsEditDto extends GridOptionsDto, Record<string, any>
|
|||
formFieldsDefaultValueDto: FieldsDefaultValueDto[]
|
||||
widgetsJson?: string
|
||||
widgetsDto: WidgetEditDto[]
|
||||
workflowDto: WorkflowDto
|
||||
extraFilterEditDto: ExtraFilterEditDto[]
|
||||
}
|
||||
|
||||
|
|
@ -905,6 +906,12 @@ export interface WidgetEditDto {
|
|||
isActive: boolean
|
||||
}
|
||||
|
||||
export interface WorkflowDto {
|
||||
approvalFieldName: string
|
||||
approvalDateFieldName: string
|
||||
approvalStatusFieldName: string
|
||||
}
|
||||
|
||||
export interface LayoutDto {
|
||||
grid: boolean
|
||||
pivot: boolean
|
||||
|
|
|
|||
|
|
@ -15,11 +15,6 @@ export const operatorOptions = [">", ">=", "<", "<=", "=", "!="].map(
|
|||
}),
|
||||
);
|
||||
|
||||
export const columnOptions = ["Tutar", "Id"].map((value) => ({
|
||||
value,
|
||||
label: value,
|
||||
}));
|
||||
|
||||
export const kindIcon: Record<string, any> = {
|
||||
Start: FiPlay as any,
|
||||
Compare: FiGitBranch as any,
|
||||
|
|
|
|||
|
|
@ -321,7 +321,7 @@ export function emptyCriteria(kind = 'Compare', listFormCode = ''): WorkflowCrit
|
|||
nextOnApprove: '',
|
||||
nextOnReject: '',
|
||||
compareOutcomes:
|
||||
kind === 'Compare' ? [emptyCompareOutcome('Durum 1'), emptyCompareOutcome('Durum 2')] : [],
|
||||
kind === 'Compare' ? [emptyCompareOutcome1('>5000'), emptyCompareOutcome2('<=5000')] : [],
|
||||
positionX: 32,
|
||||
positionY: 150,
|
||||
}
|
||||
|
|
@ -375,7 +375,7 @@ export function defaultTitle(kind: string) {
|
|||
)
|
||||
}
|
||||
|
||||
export function emptyCompareOutcome(label = 'Durum'): CompareOutcomeDto {
|
||||
export function emptyCompareOutcome1(label = 'Durum'): CompareOutcomeDto {
|
||||
return {
|
||||
label,
|
||||
targetId: '',
|
||||
|
|
@ -383,6 +383,14 @@ export function emptyCompareOutcome(label = 'Durum'): CompareOutcomeDto {
|
|||
}
|
||||
}
|
||||
|
||||
export function emptyCompareOutcome2(label = 'Durum'): CompareOutcomeDto {
|
||||
return {
|
||||
label,
|
||||
targetId: '',
|
||||
conditions: [{ compareColumn: 'Tutar', compareOperator: '<=', compareValue: 5000 }],
|
||||
}
|
||||
}
|
||||
|
||||
export function toCompareOutcomeForm(
|
||||
outcome: Partial<CompareOutcomeDto> &
|
||||
Partial<WorkflowConditionDto> & {
|
||||
|
|
|
|||
|
|
@ -28,8 +28,11 @@ import FormFields from './form-fields/FormFields'
|
|||
import { putListForms } from '@/services/admin/list-form.service'
|
||||
import { getRoles, getUsers } from '@/services/identity.service'
|
||||
import { GridOptionsEditDto, ListFormCustomizationDto } from '@/proxy/form/models'
|
||||
import { SelectCommandTypeEnum } 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'
|
||||
import type { DatabaseColumnDto, SqlObjectExplorerDto } from '@/proxy/sql-query-manager/models'
|
||||
import { Container } from '@/components/shared'
|
||||
import { ROUTES_ENUM } from '@/routes/route.constant'
|
||||
import FormTabWidgets from './FormTabWidgets'
|
||||
|
|
@ -71,6 +74,8 @@ const FormEdit = () => {
|
|||
const [langOptions, setLangOptions] = useState<SelectBoxOption[]>([])
|
||||
const [roleList, setRoleList] = useState<SelectBoxOption[]>([])
|
||||
const [userList, setUserList] = useState<SelectBoxOption[]>([])
|
||||
const [workflowColumns, setWorkflowColumns] = useState<DatabaseColumnDto[]>([])
|
||||
const [isLoadingWorkflowColumns, setIsLoadingWorkflowColumns] = useState(false)
|
||||
|
||||
const languages: LanguageInfo[] | undefined = useStoreState(
|
||||
(state) => state.abpConfig.config?.localization.languages,
|
||||
|
|
@ -141,6 +146,51 @@ const FormEdit = () => {
|
|||
refreshData()
|
||||
}, [listFormCode])
|
||||
|
||||
useEffect(() => {
|
||||
const loadWorkflowColumns = async () => {
|
||||
const dataSourceCode = listFormValues?.dataSourceCode
|
||||
const selectCommand = listFormValues?.selectCommand
|
||||
|
||||
if (!dataSourceCode || !selectCommand) {
|
||||
setWorkflowColumns([])
|
||||
return
|
||||
}
|
||||
|
||||
setIsLoadingWorkflowColumns(true)
|
||||
try {
|
||||
const objectsResponse = await sqlObjectManagerService.getAllObjects(dataSourceCode)
|
||||
const objects = objectsResponse.data
|
||||
const objectInfo = findSelectCommandObject(
|
||||
objects,
|
||||
selectCommand,
|
||||
listFormValues.selectCommandType,
|
||||
)
|
||||
|
||||
if (!objectInfo) {
|
||||
setWorkflowColumns([])
|
||||
return
|
||||
}
|
||||
|
||||
const columnsResponse = await sqlObjectManagerService.getTableColumns(
|
||||
dataSourceCode,
|
||||
objectInfo.schemaName,
|
||||
objectInfo.objectName,
|
||||
)
|
||||
setWorkflowColumns(columnsResponse.data ?? [])
|
||||
} catch {
|
||||
setWorkflowColumns([])
|
||||
} finally {
|
||||
setIsLoadingWorkflowColumns(false)
|
||||
}
|
||||
}
|
||||
|
||||
loadWorkflowColumns()
|
||||
}, [
|
||||
listFormValues?.dataSourceCode,
|
||||
listFormValues?.selectCommand,
|
||||
listFormValues?.selectCommandType,
|
||||
])
|
||||
|
||||
const onSubmit = async (
|
||||
editType: string,
|
||||
values: GridOptionsEditDto,
|
||||
|
|
@ -189,9 +239,12 @@ const FormEdit = () => {
|
|||
|
||||
{/* SAĞ TARAF */}
|
||||
{listFormValues.isTenant && (
|
||||
<Badge className='font-semibold' content="Bu bir MULTI TENANT form'dur, veri kaybı olmaması için, sorgularda TENANTID
|
||||
parametresini kullanmayı unutmayınız." innerClass="p-1 bg-red-50 text-red-500">
|
||||
</Badge>
|
||||
<Badge
|
||||
className="font-semibold"
|
||||
content="Bu bir MULTI TENANT form'dur, veri kaybı olmaması için, sorgularda TENANTID
|
||||
parametresini kullanmayı unutmayınız."
|
||||
innerClass="p-1 bg-red-50 text-red-500"
|
||||
></Badge>
|
||||
)}
|
||||
</div>
|
||||
<Tabs defaultValue="details" variant="underline">
|
||||
|
|
@ -392,7 +445,13 @@ const FormEdit = () => {
|
|||
<FormTabWidgets listFormCode={listFormCode} />
|
||||
</TabContent>
|
||||
<TabContent value="workflow" className="px-2">
|
||||
<FormTabWorkflow listFormCode={listFormCode} />
|
||||
<FormTabWorkflow
|
||||
listFormCode={listFormCode}
|
||||
userList={userList}
|
||||
selectCommandColumns={workflowColumns}
|
||||
isLoadingSelectCommandColumns={isLoadingWorkflowColumns}
|
||||
onSubmit={onSubmit}
|
||||
/>
|
||||
</TabContent>
|
||||
<TabContent value="fields" className="px-2">
|
||||
<FormFields
|
||||
|
|
@ -451,4 +510,48 @@ const FormEdit = () => {
|
|||
)
|
||||
}
|
||||
|
||||
function findSelectCommandObject(
|
||||
objects: SqlObjectExplorerDto | undefined,
|
||||
selectCommand: string,
|
||||
selectCommandType?: SelectCommandTypeEnum,
|
||||
) {
|
||||
if (!objects || !selectCommand || selectCommandType === SelectCommandTypeEnum.Query) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (selectCommandType === SelectCommandTypeEnum.Table) {
|
||||
const table = objects.tables.find((item) => item.tableName === selectCommand)
|
||||
return table ? { schemaName: table.schemaName, objectName: table.tableName } : null
|
||||
}
|
||||
|
||||
if (selectCommandType === SelectCommandTypeEnum.View) {
|
||||
const view = objects.views.find((item) => item.objectName === selectCommand)
|
||||
return view ? { schemaName: view.schemaName, objectName: view.objectName } : null
|
||||
}
|
||||
|
||||
if (selectCommandType === SelectCommandTypeEnum.TableValuedFunction) {
|
||||
const fn = objects.functions.find((item) => item.objectName === selectCommand)
|
||||
return fn ? { schemaName: fn.schemaName, objectName: fn.objectName } : null
|
||||
}
|
||||
|
||||
if (selectCommandType === SelectCommandTypeEnum.StoredProcedure) {
|
||||
const sp = objects.storedProcedures.find((item) => item.objectName === selectCommand)
|
||||
return sp ? { schemaName: sp.schemaName, objectName: sp.objectName } : null
|
||||
}
|
||||
|
||||
const table = objects.tables.find((item) => item.tableName === selectCommand)
|
||||
if (table) return { schemaName: table.schemaName, objectName: table.tableName }
|
||||
|
||||
const view = objects.views.find((item) => item.objectName === selectCommand)
|
||||
if (view) return { schemaName: view.schemaName, objectName: view.objectName }
|
||||
|
||||
const fn = objects.functions.find((item) => item.objectName === selectCommand)
|
||||
if (fn) return { schemaName: fn.schemaName, objectName: fn.objectName }
|
||||
|
||||
const sp = objects.storedProcedures.find((item) => item.objectName === selectCommand)
|
||||
if (sp) return { schemaName: sp.schemaName, objectName: sp.objectName }
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export default FormEdit
|
||||
|
|
|
|||
|
|
@ -8,7 +8,16 @@ import {
|
|||
type WorkflowCriteriaForm,
|
||||
} from '@/utils/workflow/workflowHelpers'
|
||||
import { workflowService, type WorkflowCriteriaDto } from '@/services/workflow.service'
|
||||
import { DashboardShell } from '../workflow/DashboardShell'
|
||||
import { WorkflowDesigner } from '../workflow/WorkflowDesigner'
|
||||
import { SelectBoxOption } from '@/types/shared'
|
||||
import { Field, FieldProps, Form, Formik } from 'formik'
|
||||
import { Button, Card, FormContainer, FormItem, Input, Select } from '@/components/ui'
|
||||
import { ListFormEditTabs } from '@/proxy/admin/list-form/options'
|
||||
import { object, string } from 'yup'
|
||||
import { useStoreState } from '@/store/store'
|
||||
import { FormEditProps } from './FormEdit'
|
||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||
import type { DatabaseColumnDto } from '@/proxy/sql-query-manager/models'
|
||||
|
||||
type PendingLink = {
|
||||
sourceId: string
|
||||
|
|
@ -25,18 +34,33 @@ type DragEndEvent = {
|
|||
delta: { x: number; y: number }
|
||||
}
|
||||
|
||||
export function FormTabWorkflow({ listFormCode }: { listFormCode: string }) {
|
||||
export function FormTabWorkflow(
|
||||
props: FormEditProps & {
|
||||
listFormCode: string
|
||||
userList: SelectBoxOption[]
|
||||
selectCommandColumns: DatabaseColumnDto[]
|
||||
isLoadingSelectCommandColumns: boolean
|
||||
},
|
||||
) {
|
||||
const columnOptions: SelectBoxOption[] = props.selectCommandColumns.length
|
||||
? props.selectCommandColumns.map((column) => ({
|
||||
value: column.columnName,
|
||||
label: `${column.columnName} (${column.dataType})`,
|
||||
}))
|
||||
: []
|
||||
|
||||
const [criteria, setCriteria] = useState<WorkflowCriteriaDto[]>([])
|
||||
const [selectedId, setSelectedId] = useState('')
|
||||
const [pendingLink, setPendingLink] = useState<PendingLink>(null)
|
||||
const [criteriaForm, setCriteriaForm] = useState<WorkflowCriteriaForm>(
|
||||
emptyCriteria('Start', listFormCode),
|
||||
emptyCriteria('Start', props.listFormCode),
|
||||
)
|
||||
const [dragPreview, setDragPreview] = useState<DragPreview>(null)
|
||||
const [canvasZoom, setCanvasZoom] = useState(1)
|
||||
const [designerTab, setDesignerTab] = useState('flow')
|
||||
const [busy, setBusy] = useState(false)
|
||||
const canvasRef = useRef<HTMLDivElement | null>(null)
|
||||
const { translate } = useLocalization()
|
||||
|
||||
const currentCriteria = useMemo(() => criteria, [criteria])
|
||||
|
||||
|
|
@ -46,10 +70,10 @@ export function FormTabWorkflow({ listFormCode }: { listFormCode: string }) {
|
|||
)
|
||||
|
||||
const loadState = useCallback(async () => {
|
||||
const data = await workflowService.getState(listFormCode)
|
||||
const data = await workflowService.getState(props.listFormCode)
|
||||
setCriteria(data.criteria)
|
||||
return data
|
||||
}, [listFormCode])
|
||||
}, [props.listFormCode])
|
||||
|
||||
const runAction = useCallback(
|
||||
async (action: () => Promise<unknown>) => {
|
||||
|
|
@ -72,9 +96,9 @@ export function FormTabWorkflow({ listFormCode }: { listFormCode: string }) {
|
|||
if (selectedCriteria) {
|
||||
setCriteriaForm(toCriteriaForm(selectedCriteria))
|
||||
} else {
|
||||
setCriteriaForm(emptyCriteria('Start', listFormCode))
|
||||
setCriteriaForm(emptyCriteria('Start', props.listFormCode))
|
||||
}
|
||||
}, [listFormCode, selectedCriteria])
|
||||
}, [props.listFormCode, selectedCriteria])
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedId) return
|
||||
|
|
@ -90,7 +114,7 @@ export function FormTabWorkflow({ listFormCode }: { listFormCode: string }) {
|
|||
runAction(async () => {
|
||||
await workflowService.saveCriteria({
|
||||
...normalizeCriteria(criteriaForm),
|
||||
listFormCode,
|
||||
listFormCode: props.listFormCode,
|
||||
})
|
||||
setSelectedId('')
|
||||
})
|
||||
|
|
@ -100,8 +124,8 @@ export function FormTabWorkflow({ listFormCode }: { listFormCode: string }) {
|
|||
setDesignerTab('flow')
|
||||
runAction(async () => {
|
||||
const saved = await workflowService.saveCriteria({
|
||||
...normalizeCriteria(emptyCriteria(kind, listFormCode)),
|
||||
listFormCode,
|
||||
...normalizeCriteria(emptyCriteria(kind, props.listFormCode)),
|
||||
listFormCode: props.listFormCode,
|
||||
positionX: 80 + (currentCriteria.length % 5) * 230,
|
||||
positionY: 220 + Math.floor(currentCriteria.length / 5) * 140,
|
||||
})
|
||||
|
|
@ -145,12 +169,15 @@ export function FormTabWorkflow({ listFormCode }: { listFormCode: string }) {
|
|||
}
|
||||
|
||||
runAction(async () => {
|
||||
await workflowService.saveCriteria({ ...normalizeCriteria(next), listFormCode })
|
||||
await workflowService.saveCriteria({
|
||||
...normalizeCriteria(next),
|
||||
listFormCode: props.listFormCode,
|
||||
})
|
||||
setPendingLink(null)
|
||||
setSelectedId(sourceId)
|
||||
})
|
||||
},
|
||||
[busy, currentCriteria, listFormCode, runAction],
|
||||
[busy, currentCriteria, props.listFormCode, runAction],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -190,7 +217,10 @@ export function FormTabWorkflow({ listFormCode }: { listFormCode: string }) {
|
|||
}
|
||||
|
||||
runAction(async () => {
|
||||
await workflowService.saveCriteria({ ...normalizeCriteria(next), listFormCode })
|
||||
await workflowService.saveCriteria({
|
||||
...normalizeCriteria(next),
|
||||
listFormCode: props.listFormCode,
|
||||
})
|
||||
setSelectedId(next.id)
|
||||
})
|
||||
}
|
||||
|
|
@ -215,7 +245,10 @@ export function FormTabWorkflow({ listFormCode }: { listFormCode: string }) {
|
|||
|
||||
setPendingLink(null)
|
||||
runAction(async () => {
|
||||
await workflowService.saveCriteria({ ...normalizeCriteria(next), listFormCode })
|
||||
await workflowService.saveCriteria({
|
||||
...normalizeCriteria(next),
|
||||
listFormCode: props.listFormCode,
|
||||
})
|
||||
setSelectedId('')
|
||||
})
|
||||
}
|
||||
|
|
@ -234,7 +267,7 @@ export function FormTabWorkflow({ listFormCode }: { listFormCode: string }) {
|
|||
|
||||
await workflowService.saveCriteria({
|
||||
...normalizeCriteria(item),
|
||||
listFormCode,
|
||||
listFormCode: props.listFormCode,
|
||||
positionX: position.x,
|
||||
positionY: position.y,
|
||||
})
|
||||
|
|
@ -262,38 +295,149 @@ export function FormTabWorkflow({ listFormCode }: { listFormCode: string }) {
|
|||
setSelectedId(sourceId)
|
||||
}
|
||||
|
||||
const schema = object().shape({
|
||||
approvalFieldName: string(),
|
||||
approvalDateFieldName: string(),
|
||||
})
|
||||
|
||||
const initialValues = useStoreState((s) => s.admin.lists.values)
|
||||
if (!initialValues) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<DashboardShell
|
||||
busy={busy}
|
||||
canvasRef={canvasRef}
|
||||
canvasZoom={canvasZoom}
|
||||
criteriaForm={criteriaForm}
|
||||
currentCriteria={currentCriteria}
|
||||
designerTab={designerTab}
|
||||
dragPreview={dragPreview}
|
||||
pendingLink={pendingLink}
|
||||
selectedId={selectedId}
|
||||
onAddCriteria={addCriteria}
|
||||
onBeginLink={beginLink}
|
||||
onChangeCriteriaForm={setCriteriaForm}
|
||||
onClearCanvasSelection={clearCanvasSelection}
|
||||
onConnectNodes={connectNodes}
|
||||
onDeleteSelectedCriteria={deleteSelectedCriteria}
|
||||
onDisconnectLink={disconnectLink}
|
||||
onDragMove={(event: DragEndEvent | null) =>
|
||||
setDragPreview(event ? { id: event.active.id, delta: event.delta } : null)
|
||||
}
|
||||
onFitFlowLayout={fitFlowLayout}
|
||||
onOpenCriteriaDetails={openCriteriaDetails}
|
||||
onResetDemo={() => runAction(() => workflowService.resetDemo(listFormCode))}
|
||||
onSaveCriteria={saveCriteria}
|
||||
onSelectCriteria={setSelectedId}
|
||||
onSetDesignerTab={setDesignerTab}
|
||||
onUpdateNodePosition={updateNodePosition}
|
||||
onZoomIn={() => setCanvasZoom((current) => Math.min(1.5, Number((current + 0.1).toFixed(2))))}
|
||||
onZoomOut={() =>
|
||||
setCanvasZoom((current) => Math.max(0.6, Number((current - 0.1).toFixed(2))))
|
||||
}
|
||||
/>
|
||||
<div>
|
||||
<Formik
|
||||
initialValues={initialValues}
|
||||
validationSchema={schema}
|
||||
onSubmit={async (values, formikHelpers) => {
|
||||
await props.onSubmit(ListFormEditTabs.Workflow, values, formikHelpers)
|
||||
}}
|
||||
>
|
||||
{({ touched, errors, values, isSubmitting }) => (
|
||||
<Form>
|
||||
<FormContainer size="sm">
|
||||
<Card className="my-2">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
|
||||
<FormItem
|
||||
label={translate('::ListForms.ListFormEdit.Workflow.ApprovalFieldName')}
|
||||
invalid={
|
||||
errors.workflowDto?.approvalFieldName &&
|
||||
touched.workflowDto?.approvalFieldName
|
||||
}
|
||||
errorMessage={errors.workflowDto?.approvalFieldName}
|
||||
>
|
||||
<Field type="text" name="workflowDto.approvalFieldName">
|
||||
{({ field, form }: FieldProps<SelectBoxOption>) => (
|
||||
<Select
|
||||
field={field}
|
||||
form={form}
|
||||
options={columnOptions}
|
||||
isClearable={true}
|
||||
value={columnOptions.filter(
|
||||
(option) => option.value === values.workflowDto.approvalFieldName,
|
||||
)}
|
||||
onChange={(option) => form.setFieldValue(field.name, option?.value)}
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
</FormItem>
|
||||
|
||||
<FormItem
|
||||
label={translate('::ListForms.ListFormEdit.Workflow.ApprovalDateFieldName')}
|
||||
invalid={
|
||||
errors.workflowDto?.approvalDateFieldName &&
|
||||
touched.workflowDto?.approvalDateFieldName
|
||||
}
|
||||
errorMessage={errors.workflowDto?.approvalDateFieldName}
|
||||
>
|
||||
<Field type="text" name="workflowDto.approvalDateFieldName">
|
||||
{({ field, form }: FieldProps<SelectBoxOption>) => (
|
||||
<Select
|
||||
field={field}
|
||||
form={form}
|
||||
options={columnOptions}
|
||||
isClearable={true}
|
||||
value={columnOptions.filter(
|
||||
(option) => option.value === values.workflowDto.approvalDateFieldName,
|
||||
)}
|
||||
onChange={(option) => form.setFieldValue(field.name, option?.value)}
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
</FormItem>
|
||||
|
||||
<FormItem
|
||||
label={translate('::ListForms.ListFormEdit.Workflow.ApprovalStatusFieldName')}
|
||||
invalid={
|
||||
errors.workflowDto?.approvalStatusFieldName &&
|
||||
touched.workflowDto?.approvalStatusFieldName
|
||||
}
|
||||
errorMessage={errors.workflowDto?.approvalStatusFieldName}
|
||||
>
|
||||
<Field type="text" name="workflowDto.approvalStatusFieldName">
|
||||
{({ field, form }: FieldProps<SelectBoxOption>) => (
|
||||
<Select
|
||||
field={field}
|
||||
form={form}
|
||||
options={columnOptions}
|
||||
isClearable={true}
|
||||
value={columnOptions.filter(
|
||||
(option) => option.value === values.workflowDto.approvalStatusFieldName,
|
||||
)}
|
||||
onChange={(option) => form.setFieldValue(field.name, option?.value)}
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
</FormItem>
|
||||
</div>
|
||||
|
||||
<Button block variant="solid" loading={isSubmitting}>
|
||||
{isSubmitting ? translate('::SavingWithThreeDot') : translate('::Save')}
|
||||
</Button>
|
||||
</Card>
|
||||
</FormContainer>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
|
||||
<WorkflowDesigner
|
||||
busy={busy}
|
||||
canvasRef={canvasRef}
|
||||
canvasZoom={canvasZoom}
|
||||
criteriaForm={criteriaForm}
|
||||
currentCriteria={currentCriteria}
|
||||
designerTab={designerTab}
|
||||
dragPreview={dragPreview}
|
||||
pendingLink={pendingLink}
|
||||
selectedCriteriaId={selectedId}
|
||||
userList={props.userList}
|
||||
selectCommandColumns={props.selectCommandColumns}
|
||||
isLoadingSelectCommandColumns={props.isLoadingSelectCommandColumns}
|
||||
onAddCriteria={addCriteria}
|
||||
onBeginLink={beginLink}
|
||||
onChangeCriteriaForm={setCriteriaForm}
|
||||
onClearSelection={clearCanvasSelection}
|
||||
onConnect={connectNodes}
|
||||
onDeleteCriteria={deleteSelectedCriteria}
|
||||
onDeleteLink={disconnectLink}
|
||||
onDragMove={(event: DragEndEvent | null) =>
|
||||
setDragPreview(event ? { id: event.active.id, delta: event.delta } : null)
|
||||
}
|
||||
onFitLayout={fitFlowLayout}
|
||||
onOpenDetails={openCriteriaDetails}
|
||||
onResetDemo={() => runAction(() => workflowService.resetDemo(props.listFormCode))}
|
||||
onSaveCriteria={saveCriteria}
|
||||
onSelectCriteria={setSelectedId}
|
||||
onSetDesignerTab={setDesignerTab}
|
||||
onUpdateNodePosition={updateNodePosition}
|
||||
onZoomIn={() =>
|
||||
setCanvasZoom((current) => Math.min(1.5, Number((current + 0.1).toFixed(2))))
|
||||
}
|
||||
onZoomOut={() =>
|
||||
setCanvasZoom((current) => Math.max(0.6, Number((current - 0.1).toFixed(2))))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,484 +0,0 @@
|
|||
import React from 'react'
|
||||
import { FiSave, FiTrash2 } from 'react-icons/fi'
|
||||
import classNames from 'classnames'
|
||||
import {
|
||||
columnOptions,
|
||||
kindIcon,
|
||||
kindOptions,
|
||||
operatorOptions,
|
||||
} from '@/utils/workflow/workflowConstants'
|
||||
import {
|
||||
compareOutcomeRuleText,
|
||||
criteriaSummary,
|
||||
emptyCompareOutcome,
|
||||
targetTitle,
|
||||
} from '@/utils/workflow/workflowHelpers'
|
||||
import type { CompareOutcomeDto, WorkflowCriteriaDto } from '@/services/workflow.service'
|
||||
|
||||
const SaveIcon = FiSave as any
|
||||
const TrashIcon = FiTrash2 as any
|
||||
|
||||
const tableButtonClass =
|
||||
'inline-flex min-h-8 items-center justify-center gap-1.5 rounded-md border px-2.5 py-1 text-[13px] font-medium leading-none transition-colors disabled:cursor-not-allowed disabled:opacity-50'
|
||||
|
||||
type CriteriaTableProps = {
|
||||
criteria: WorkflowCriteriaDto[]
|
||||
selectedId: string
|
||||
form: any
|
||||
busy: boolean
|
||||
onSelect: (id: string) => void
|
||||
onChange: (form: any) => void
|
||||
onSubmit: (event: React.FormEvent<HTMLFormElement>) => void
|
||||
onDelete: (id: string) => void
|
||||
}
|
||||
|
||||
export function CriteriaTable({
|
||||
criteria,
|
||||
selectedId,
|
||||
form,
|
||||
busy,
|
||||
onSelect,
|
||||
onChange,
|
||||
onSubmit,
|
||||
onDelete,
|
||||
}: CriteriaTableProps) {
|
||||
const setField = (name: string, value: unknown) => onChange({ ...form, [name]: value })
|
||||
const targetOptions = [
|
||||
{ value: '', label: 'Bağlantı yok' },
|
||||
...criteria
|
||||
.filter((item) => item.id !== form.id)
|
||||
.map((item) => ({ value: item.id, label: `${item.id} - ${item.title}` })),
|
||||
]
|
||||
const updateCompareOutcome = (index: number, patch: Partial<CompareOutcomeDto>) => {
|
||||
const next = [...(form.compareOutcomes || [])]
|
||||
next[index] = { ...next[index], ...patch }
|
||||
setField('compareOutcomes', next)
|
||||
}
|
||||
const updateCompareCondition = (
|
||||
outcomeIndex: number,
|
||||
conditionIndex: number,
|
||||
patch: Record<string, unknown>,
|
||||
) => {
|
||||
const next = [...(form.compareOutcomes || [])]
|
||||
const conditions = [...(next[outcomeIndex]?.conditions || [])]
|
||||
conditions[conditionIndex] = { ...conditions[conditionIndex], ...patch }
|
||||
next[outcomeIndex] = { ...next[outcomeIndex], conditions }
|
||||
setField('compareOutcomes', next)
|
||||
}
|
||||
const addCompareCondition = (outcomeIndex: number) => {
|
||||
const next = [...(form.compareOutcomes || [])]
|
||||
next[outcomeIndex] = {
|
||||
...next[outcomeIndex],
|
||||
conditions: [
|
||||
...(next[outcomeIndex]?.conditions || []),
|
||||
{ compareColumn: 'Tutar', compareOperator: '>', compareValue: 0 },
|
||||
],
|
||||
}
|
||||
setField('compareOutcomes', next)
|
||||
}
|
||||
const removeCompareCondition = (outcomeIndex: number, conditionIndex: number) => {
|
||||
const next = [...(form.compareOutcomes || [])]
|
||||
const conditions = (next[outcomeIndex]?.conditions || []).filter(
|
||||
(_: unknown, index: number) => index !== conditionIndex,
|
||||
)
|
||||
next[outcomeIndex] = { ...next[outcomeIndex], conditions }
|
||||
setField('compareOutcomes', next)
|
||||
}
|
||||
const removeCompareOutcome = (index: number) => {
|
||||
setField(
|
||||
'compareOutcomes',
|
||||
(form.compareOutcomes || []).filter((_: unknown, itemIndex: number) => itemIndex !== index),
|
||||
)
|
||||
}
|
||||
const targetSelect = (
|
||||
value: string | null | undefined,
|
||||
onSelectTarget: (value: string) => void,
|
||||
required = false,
|
||||
) => (
|
||||
<select
|
||||
required={required}
|
||||
value={value || ''}
|
||||
onChange={(event) => onSelectTarget(event.target.value)}
|
||||
>
|
||||
{targetOptions.map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
)
|
||||
const toggleRow = (id: string) => onSelect(id === selectedId ? '' : id)
|
||||
|
||||
return (
|
||||
<section className="min-w-0 rounded-lg">
|
||||
<form className="block" onSubmit={onSubmit}>
|
||||
<div className="overflow-auto rounded-md border border-gray-200 text-sm">
|
||||
<div className="min-w-[920px]">
|
||||
<div className="grid grid-cols-[minmax(110px,0.8fr)_120px_minmax(220px,1.4fr)_minmax(220px,1.2fr)_110px] bg-slate-50 font-semibold text-slate-700">
|
||||
<div className="px-4 py-3">Id</div>
|
||||
<div className="px-4 py-3">Tip</div>
|
||||
<div className="px-4 py-3">Başlık / Kural</div>
|
||||
<div className="px-4 py-3">Bağlantılar</div>
|
||||
<div className="px-4 py-3">İşlem</div>
|
||||
</div>
|
||||
|
||||
{criteria.length === 0 && (
|
||||
<div className="border-t border-gray-100 px-4 py-6 text-center text-slate-500">
|
||||
Seçili iş akışı için açıklama kaydı yok.
|
||||
</div>
|
||||
)}
|
||||
|
||||
{criteria.map((item) => {
|
||||
const isSelected = item.id === selectedId
|
||||
const connectionSummary = criteriaConnectionSummary(item, criteria)
|
||||
|
||||
return (
|
||||
<React.Fragment key={item.id}>
|
||||
<div
|
||||
className={classNames(
|
||||
'grid cursor-pointer grid-cols-[minmax(110px,0.8fr)_120px_minmax(220px,1.4fr)_minmax(220px,1.2fr)_110px] border-t border-gray-100',
|
||||
{
|
||||
'bg-blue-50': isSelected,
|
||||
},
|
||||
)}
|
||||
onClick={() => toggleRow(item.id)}
|
||||
>
|
||||
<div className="min-w-0 px-4 py-3">
|
||||
<strong className="break-words">{item.id}</strong>
|
||||
</div>
|
||||
<div className="px-4 py-3">
|
||||
{kindOptions.find((option) => option.value === item.kind)?.label}
|
||||
</div>
|
||||
<div className="min-w-0 break-words px-4 py-3">
|
||||
{criteriaSummaryContent(item)}
|
||||
</div>
|
||||
<div className="min-w-0 break-words px-4 py-3">{connectionSummary}</div>
|
||||
<div className="px-4 py-3">
|
||||
<button
|
||||
type="button"
|
||||
className={classNames(
|
||||
tableButtonClass,
|
||||
'ml-1.5 border-gray-300 bg-white text-slate-700',
|
||||
)}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
toggleRow(item.id)
|
||||
}}
|
||||
>
|
||||
{isSelected ? 'Kapat' : 'Düzenle'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{isSelected && (
|
||||
<div className="grid gap-3.5 border-t border-gray-100 bg-slate-50 p-3.5">
|
||||
<div className="grid grid-cols-3 gap-2.5 max-[720px]:grid-cols-1">
|
||||
<Field label="Tip" required>
|
||||
<select
|
||||
value={form.kind}
|
||||
onChange={(event) => setField('kind', event.target.value)}
|
||||
>
|
||||
{kindOptions.map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</Field>
|
||||
<Field label="Başlık" required>
|
||||
<input
|
||||
required
|
||||
value={form.title}
|
||||
onChange={(event) => setField('title', event.target.value)}
|
||||
/>
|
||||
</Field>
|
||||
<Field
|
||||
label="Onaylayacak Kişi"
|
||||
required={form.kind === 'Approval' || form.kind === 'Inform'}
|
||||
>
|
||||
<input
|
||||
required={form.kind === 'Approval' || form.kind === 'Inform'}
|
||||
value={form.approver}
|
||||
onChange={(event) => setField('approver', event.target.value)}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
{(form.kind === 'Start' || form.kind === 'Inform') && (
|
||||
<Field label="Sonraki adım" required>
|
||||
{targetSelect(
|
||||
form.nextOnStart,
|
||||
(value) => setField('nextOnStart', value),
|
||||
true,
|
||||
)}
|
||||
</Field>
|
||||
)}
|
||||
|
||||
{form.kind === 'Approval' && (
|
||||
<>
|
||||
<Field label="Onay adımı" required>
|
||||
{targetSelect(
|
||||
form.nextOnApprove,
|
||||
(value) => setField('nextOnApprove', value),
|
||||
true,
|
||||
)}
|
||||
</Field>
|
||||
<Field label="Red adımı" required>
|
||||
{targetSelect(
|
||||
form.nextOnReject,
|
||||
(value) => setField('nextOnReject', value),
|
||||
true,
|
||||
)}
|
||||
</Field>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{form.kind === 'Compare' && (
|
||||
<div className="grid gap-2.5 rounded-lg border border-gray-200 bg-slate-50 p-2.5">
|
||||
<div className="flex items-center justify-between gap-2 text-[13px] font-bold text-slate-700">
|
||||
<span>Karşılaştırma durumları</span>
|
||||
<button
|
||||
type="button"
|
||||
className={classNames(
|
||||
tableButtonClass,
|
||||
'ml-1.5 border-gray-300 bg-white text-slate-700',
|
||||
)}
|
||||
disabled={(form.compareOutcomes || []).length >= 4}
|
||||
onClick={() =>
|
||||
setField('compareOutcomes', [
|
||||
...(form.compareOutcomes || []),
|
||||
emptyCompareOutcome(
|
||||
`Durum ${(form.compareOutcomes || []).length + 1}`,
|
||||
),
|
||||
])
|
||||
}
|
||||
>
|
||||
Ekle
|
||||
</button>
|
||||
</div>
|
||||
{(form.compareOutcomes || []).map(
|
||||
(outcome: CompareOutcomeDto, index: number) => (
|
||||
<div
|
||||
key={index}
|
||||
className="grid gap-2 border-t border-gray-200 pt-2 first:border-t-0 first:pt-0"
|
||||
>
|
||||
<div className="grid grid-cols-[minmax(130px,0.8fr)_minmax(200px,1.4fr)_auto] items-center gap-2 max-[720px]:grid-cols-1">
|
||||
<input
|
||||
required
|
||||
value={outcome.label}
|
||||
aria-label="Durum adı zorunlu"
|
||||
onChange={(event) =>
|
||||
updateCompareOutcome(index, {
|
||||
label: event.target.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
{targetSelect(
|
||||
outcome.targetId,
|
||||
(targetId) =>
|
||||
updateCompareOutcome(index, {
|
||||
targetId,
|
||||
}),
|
||||
true,
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className={classNames(
|
||||
tableButtonClass,
|
||||
'ml-1.5 border-gray-300 bg-white text-slate-700',
|
||||
)}
|
||||
disabled={(form.compareOutcomes || []).length <= 2}
|
||||
onClick={() => removeCompareOutcome(index)}
|
||||
>
|
||||
Sil
|
||||
</button>
|
||||
</div>
|
||||
<div className="grid gap-2.5">
|
||||
{(outcome.conditions || []).map((condition, conditionIndex) => (
|
||||
<div
|
||||
key={conditionIndex}
|
||||
className="grid grid-cols-[minmax(100px,0.7fr)_82px_minmax(110px,0.8fr)_auto] items-center gap-1.5 max-[720px]:grid-cols-1"
|
||||
>
|
||||
<select
|
||||
value={condition.compareColumn}
|
||||
onChange={(event) =>
|
||||
updateCompareCondition(index, conditionIndex, {
|
||||
column: event.target.value,
|
||||
})
|
||||
}
|
||||
>
|
||||
{columnOptions.map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<select
|
||||
value={condition.compareOperator}
|
||||
onChange={(event) =>
|
||||
updateCompareCondition(index, conditionIndex, {
|
||||
operator: event.target.value,
|
||||
})
|
||||
}
|
||||
>
|
||||
{operatorOptions.map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<input
|
||||
required
|
||||
type="number"
|
||||
step="0.01"
|
||||
value={condition.compareValue}
|
||||
onChange={(event) =>
|
||||
updateCompareCondition(index, conditionIndex, {
|
||||
compareValue: event.target.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className={classNames(
|
||||
tableButtonClass,
|
||||
'ml-1.5 border-gray-300 bg-white text-slate-700',
|
||||
)}
|
||||
disabled={(outcome.conditions || []).length <= 1}
|
||||
onClick={() =>
|
||||
removeCompareCondition(index, conditionIndex)
|
||||
}
|
||||
>
|
||||
Koşulu sil
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
<button
|
||||
type="button"
|
||||
className={classNames(
|
||||
tableButtonClass,
|
||||
'ml-1.5 border-gray-300 bg-white text-slate-700',
|
||||
)}
|
||||
onClick={() => addCompareCondition(index)}
|
||||
>
|
||||
Koşul ekle
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<button
|
||||
type="submit"
|
||||
className={classNames(
|
||||
tableButtonClass,
|
||||
'border-blue-600 bg-blue-600 text-white',
|
||||
)}
|
||||
disabled={busy}
|
||||
>
|
||||
<SaveIcon />
|
||||
Kaydet
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={classNames(
|
||||
tableButtonClass,
|
||||
'border-red-600 bg-red-600 text-white',
|
||||
)}
|
||||
disabled={busy || !form.id}
|
||||
onClick={() => onDelete(form.id)}
|
||||
>
|
||||
<TrashIcon />
|
||||
Sil
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</React.Fragment>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
function Field({
|
||||
label,
|
||||
children,
|
||||
required = false,
|
||||
}: {
|
||||
label: string
|
||||
children: React.ReactNode
|
||||
required?: boolean
|
||||
}) {
|
||||
return (
|
||||
<label className="grid gap-1.5 text-[12px] text-slate-700">
|
||||
<span>
|
||||
{label}
|
||||
{required && <span className="font-bold text-red-600"> *</span>}
|
||||
</span>
|
||||
{children}
|
||||
</label>
|
||||
)
|
||||
}
|
||||
|
||||
function criteriaSummaryContent(item: WorkflowCriteriaDto) {
|
||||
if (item.kind === 'Compare') {
|
||||
const outcomes = item.compareOutcomes || []
|
||||
if (!outcomes.length) return '-'
|
||||
|
||||
return (
|
||||
<ul className="m-0 grid gap-1">
|
||||
{outcomes.map((outcome, index: number) => (
|
||||
<li key={`${outcome.label || 'outcome'}-${index}`}>
|
||||
<strong>{outcome.label || `Durum ${index + 1}`}:</strong>{' '}
|
||||
{compareOutcomeRuleText(outcome)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
|
||||
return criteriaSummary(item)
|
||||
}
|
||||
|
||||
function criteriaConnectionSummary(item: WorkflowCriteriaDto, criteria: WorkflowCriteriaDto[]) {
|
||||
if (item.kind === 'Compare') {
|
||||
const outcomes = item.compareOutcomes || []
|
||||
if (!outcomes.length) return '-'
|
||||
|
||||
return (
|
||||
<ul className="m-0 grid gap-1">
|
||||
{outcomes.map((outcome, index: number) => (
|
||||
<li key={`${outcome.label || 'target'}-${index}`}>
|
||||
<strong>{outcome.label || `Durum ${index + 1}`}:</strong>{' '}
|
||||
{targetTitle(criteria, outcome.targetId)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
|
||||
if (item.kind === 'Approval') {
|
||||
return (
|
||||
<ul className="m-0 grid gap-1">
|
||||
<li>
|
||||
<strong>Onay:</strong> {targetTitle(criteria, item.nextOnApprove)}
|
||||
</li>
|
||||
<li>
|
||||
<strong>Red:</strong> {targetTitle(criteria, item.nextOnReject)}
|
||||
</li>
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
|
||||
if (item.kind === 'Start' || item.kind === 'Inform') {
|
||||
return targetTitle(criteria, item.nextOnStart)
|
||||
}
|
||||
|
||||
return '-'
|
||||
}
|
||||
|
|
@ -1,117 +0,0 @@
|
|||
import { WorkflowDesigner } from './WorkflowDesigner'
|
||||
import { useEffect, useState, type FormEvent, type RefObject } from 'react'
|
||||
import type { WorkflowCriteriaDto } from '@/services/workflow.service'
|
||||
import { DatabaseColumnDto } from '@/proxy/sql-query-manager/models'
|
||||
import { sqlObjectManagerService } from '@/services/sql-query-manager.service'
|
||||
|
||||
type DashboardShellProps = {
|
||||
busy: boolean
|
||||
canvasRef: RefObject<HTMLDivElement>
|
||||
canvasZoom: number
|
||||
criteriaForm: any
|
||||
currentCriteria: WorkflowCriteriaDto[]
|
||||
designerTab: string
|
||||
dragPreview: any
|
||||
pendingLink: any
|
||||
selectedId: string
|
||||
onAddCriteria: (kind: string) => void
|
||||
onBeginLink: (sourceId: string, outcome: string) => void
|
||||
onChangeCriteriaForm: (form: any) => void
|
||||
onClearCanvasSelection: () => void
|
||||
onConnectNodes: (sourceId: string, outcome: string, targetId: string) => void
|
||||
onDeleteSelectedCriteria: (criteriaId?: string) => void
|
||||
onDisconnectLink: (sourceId: string, outcome: string) => void
|
||||
onDragMove: (event: any) => void
|
||||
onFitFlowLayout: () => void
|
||||
onOpenCriteriaDetails: (id: string) => void
|
||||
onResetDemo: () => void
|
||||
onSaveCriteria: (event: FormEvent<HTMLFormElement>) => void
|
||||
onSelectCriteria: (id: string) => void
|
||||
onSetDesignerTab: (tab: string) => void
|
||||
onUpdateNodePosition: (event: any) => void
|
||||
onZoomIn: () => void
|
||||
onZoomOut: () => void
|
||||
}
|
||||
|
||||
export function DashboardShell({
|
||||
busy,
|
||||
canvasRef,
|
||||
canvasZoom,
|
||||
criteriaForm,
|
||||
currentCriteria,
|
||||
designerTab,
|
||||
dragPreview,
|
||||
pendingLink,
|
||||
selectedId,
|
||||
onAddCriteria,
|
||||
onBeginLink,
|
||||
onChangeCriteriaForm,
|
||||
onClearCanvasSelection,
|
||||
onConnectNodes,
|
||||
onDeleteSelectedCriteria,
|
||||
onDisconnectLink,
|
||||
onDragMove,
|
||||
onFitFlowLayout,
|
||||
onOpenCriteriaDetails,
|
||||
onResetDemo,
|
||||
onSaveCriteria,
|
||||
onSelectCriteria,
|
||||
onSetDesignerTab,
|
||||
onUpdateNodePosition,
|
||||
onZoomIn,
|
||||
onZoomOut,
|
||||
}: DashboardShellProps) {
|
||||
const [selectCommandColumns, setSelectCommandColumns] = useState<DatabaseColumnDto[]>([])
|
||||
const [isLoadingColumns, setIsLoadingColumns] = useState(false)
|
||||
|
||||
const loadColumns = async (dsCode: string, schema: string, name: string) => {
|
||||
if (!dsCode || !name) {
|
||||
setSelectCommandColumns([])
|
||||
return
|
||||
}
|
||||
setIsLoadingColumns(true)
|
||||
try {
|
||||
const res = await sqlObjectManagerService.getTableColumns(dsCode, schema, name)
|
||||
setSelectCommandColumns(res.data ?? [])
|
||||
} catch {
|
||||
setSelectCommandColumns([])
|
||||
} finally {
|
||||
setIsLoadingColumns(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen">
|
||||
<main className="grid">
|
||||
<WorkflowDesigner
|
||||
busy={busy}
|
||||
canvasRef={canvasRef}
|
||||
canvasZoom={canvasZoom}
|
||||
criteriaForm={criteriaForm}
|
||||
currentCriteria={currentCriteria}
|
||||
designerTab={designerTab}
|
||||
dragPreview={dragPreview}
|
||||
pendingLink={pendingLink}
|
||||
selectedCriteriaId={selectedId}
|
||||
onAddCriteria={onAddCriteria}
|
||||
onBeginLink={onBeginLink}
|
||||
onChangeCriteriaForm={onChangeCriteriaForm}
|
||||
onClearSelection={onClearCanvasSelection}
|
||||
onConnect={onConnectNodes}
|
||||
onDeleteCriteria={onDeleteSelectedCriteria}
|
||||
onDeleteLink={onDisconnectLink}
|
||||
onDragMove={onDragMove}
|
||||
onFitLayout={onFitFlowLayout}
|
||||
onOpenDetails={onOpenCriteriaDetails}
|
||||
onResetDemo={onResetDemo}
|
||||
onSaveCriteria={onSaveCriteria}
|
||||
onSelectCriteria={onSelectCriteria}
|
||||
onSetDesignerTab={onSetDesignerTab}
|
||||
onUpdateNodePosition={onUpdateNodePosition}
|
||||
onZoomIn={onZoomIn}
|
||||
onZoomOut={onZoomOut}
|
||||
/>
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -12,6 +12,7 @@ import {
|
|||
} from '@/utils/workflow/workflowHelpers'
|
||||
import type { KeyboardEvent, MouseEvent, RefObject } from 'react'
|
||||
import type { WorkflowCriteriaDto } from '@/services/workflow.service'
|
||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||
|
||||
type PendingLink = {
|
||||
sourceId: string
|
||||
|
|
@ -36,7 +37,7 @@ type FlowNodeProps = {
|
|||
onBeginLink: (sourceId: string, outcome: string) => void
|
||||
}
|
||||
|
||||
export function FlowCanvas({
|
||||
export function WorkflowCanvas({
|
||||
currentCriteria,
|
||||
dragPreview,
|
||||
zoom,
|
||||
|
|
@ -126,7 +127,6 @@ export function FlowCanvas({
|
|||
)}
|
||||
{currentCriteria.length === 0 && (
|
||||
<div className="sticky left-[18px] top-[18px] z-30 inline-grid max-w-[360px] gap-1 rounded-lg border border-[#cfd6e2] bg-white/95 p-3.5 text-[#475467] shadow-lg">
|
||||
<strong className="text-slate-700">Boş canvas</strong>
|
||||
<span>
|
||||
Üstteki butonlardan adım ekleyin, sonra çıkış etiketleriyle bağlantıları kurun.
|
||||
</span>
|
||||
|
|
@ -286,6 +286,7 @@ function FlowNode({
|
|||
top: item.positionY,
|
||||
transform: CSS.Translate.toString(transform),
|
||||
}
|
||||
const { translate } = useLocalization()
|
||||
|
||||
return (
|
||||
<button
|
||||
|
|
@ -334,7 +335,7 @@ function FlowNode({
|
|||
})}
|
||||
>
|
||||
<Icon />
|
||||
{kindOptions.find((option) => option.value === item.kind)?.label}
|
||||
{translate('::' + kindOptions.find((option) => option.value === item.kind)?.value)}
|
||||
</span>
|
||||
<strong className="break-words text-sm leading-tight [overflow-wrap:anywhere]">
|
||||
{item.title}
|
||||
512
ui/src/views/admin/listForm/workflow/WorkflowCriteria.tsx
Normal file
512
ui/src/views/admin/listForm/workflow/WorkflowCriteria.tsx
Normal file
|
|
@ -0,0 +1,512 @@
|
|||
import React from 'react'
|
||||
import { FaEdit, FaTrash } from 'react-icons/fa'
|
||||
import classNames from 'classnames'
|
||||
import { Button, Dialog, FormContainer, FormItem, Input, Select, Table } from '@/components/ui'
|
||||
import TBody from '@/components/ui/Table/TBody'
|
||||
import THead from '@/components/ui/Table/THead'
|
||||
import Td from '@/components/ui/Table/Td'
|
||||
import Th from '@/components/ui/Table/Th'
|
||||
import Tr from '@/components/ui/Table/Tr'
|
||||
import { kindOptions, operatorOptions } from '@/utils/workflow/workflowConstants'
|
||||
import {
|
||||
compareOutcomeRuleText,
|
||||
criteriaSummary,
|
||||
emptyCompareOutcome1,
|
||||
emptyCompareOutcome2,
|
||||
targetTitle,
|
||||
} from '@/utils/workflow/workflowHelpers'
|
||||
import type { CompareOutcomeDto, WorkflowCriteriaDto } from '@/services/workflow.service'
|
||||
import { SelectBoxOption } from '@/types/shared'
|
||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||
import type { DatabaseColumnDto } from '@/proxy/sql-query-manager/models'
|
||||
|
||||
type WorkflowCriteriaProps = {
|
||||
criteria: WorkflowCriteriaDto[]
|
||||
selectedId: string
|
||||
formValues: any
|
||||
busy: boolean
|
||||
userList: SelectBoxOption[]
|
||||
selectCommandColumns: DatabaseColumnDto[]
|
||||
isLoadingSelectCommandColumns: boolean
|
||||
onSelect: (id: string) => void
|
||||
onChange: (form: any) => void
|
||||
onSubmit: (event: React.FormEvent<HTMLFormElement>) => void
|
||||
onDelete: (id: string) => void
|
||||
}
|
||||
|
||||
export function WorkflowCriteria({
|
||||
criteria,
|
||||
selectedId,
|
||||
formValues,
|
||||
busy,
|
||||
userList,
|
||||
selectCommandColumns,
|
||||
isLoadingSelectCommandColumns,
|
||||
onSelect,
|
||||
onChange,
|
||||
onSubmit,
|
||||
onDelete,
|
||||
}: WorkflowCriteriaProps) {
|
||||
const setField = (name: string, value: unknown) => onChange({ ...formValues, [name]: value })
|
||||
const targetOptions = [
|
||||
{ value: '', label: 'Bağlantı yok' },
|
||||
...criteria
|
||||
.filter((item) => item.id !== formValues.id)
|
||||
.map((item) => ({ value: item.id, label: `${item.id} - ${item.title}` })),
|
||||
]
|
||||
const compareColumnOptions = selectCommandColumns.length
|
||||
? selectCommandColumns.map((column) => ({
|
||||
value: column.columnName,
|
||||
label: `${column.columnName} (${column.dataType})`,
|
||||
}))
|
||||
: []
|
||||
const defaultCompareColumn = compareColumnOptions[0]?.value ?? 'Tutar'
|
||||
const updateCompareOutcome = (index: number, patch: Partial<CompareOutcomeDto>) => {
|
||||
const next = [...(formValues.compareOutcomes || [])]
|
||||
next[index] = { ...next[index], ...patch }
|
||||
setField('compareOutcomes', next)
|
||||
}
|
||||
const updateCompareCondition = (
|
||||
outcomeIndex: number,
|
||||
conditionIndex: number,
|
||||
patch: Record<string, unknown>,
|
||||
) => {
|
||||
const next = [...(formValues.compareOutcomes || [])]
|
||||
const conditions = [...(next[outcomeIndex]?.conditions || [])]
|
||||
conditions[conditionIndex] = { ...conditions[conditionIndex], ...patch }
|
||||
next[outcomeIndex] = { ...next[outcomeIndex], conditions }
|
||||
setField('compareOutcomes', next)
|
||||
}
|
||||
const addCompareCondition = (outcomeIndex: number) => {
|
||||
const next = [...(formValues.compareOutcomes || [])]
|
||||
next[outcomeIndex] = {
|
||||
...next[outcomeIndex],
|
||||
conditions: [
|
||||
...(next[outcomeIndex]?.conditions || []),
|
||||
{ compareColumn: defaultCompareColumn, compareOperator: '>', compareValue: 0 },
|
||||
],
|
||||
}
|
||||
setField('compareOutcomes', next)
|
||||
}
|
||||
const removeCompareCondition = (outcomeIndex: number, conditionIndex: number) => {
|
||||
const next = [...(formValues.compareOutcomes || [])]
|
||||
const conditions = (next[outcomeIndex]?.conditions || []).filter(
|
||||
(_: unknown, index: number) => index !== conditionIndex,
|
||||
)
|
||||
next[outcomeIndex] = { ...next[outcomeIndex], conditions }
|
||||
setField('compareOutcomes', next)
|
||||
}
|
||||
const removeCompareOutcome = (index: number) => {
|
||||
setField(
|
||||
'compareOutcomes',
|
||||
(formValues.compareOutcomes || []).filter(
|
||||
(_: unknown, itemIndex: number) => itemIndex !== index,
|
||||
),
|
||||
)
|
||||
}
|
||||
const targetSelect = (
|
||||
value: string | null | undefined,
|
||||
onSelectTarget: (value: string) => void,
|
||||
required = false,
|
||||
className = '',
|
||||
) => (
|
||||
<SelectField
|
||||
required={required}
|
||||
options={targetOptions}
|
||||
value={value || ''}
|
||||
onChange={onSelectTarget}
|
||||
className={className}
|
||||
/>
|
||||
)
|
||||
const closeDialog = () => onSelect('')
|
||||
const { translate } = useLocalization()
|
||||
|
||||
return (
|
||||
<>
|
||||
<Table compact>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th className="text-center w-2/12">{translate('::Operation')}</Th>
|
||||
<Th>{translate('::App.Platform.ID')}</Th>
|
||||
<Th>{translate('::App.Platform.Type')}</Th>
|
||||
<Th>{translate('::ListForms.ListFormEdit.Workflow.CriteriaTitleRule')}</Th>
|
||||
<Th>{translate('::ListForms.ListFormEdit.Workflow.CriteriaConnections')}</Th>
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{criteria.length === 0 && (
|
||||
<Tr>
|
||||
<Td colSpan={5} className="text-center">
|
||||
{translate('::ListForms.ListFormEdit.WorkflowNoCriteria')}
|
||||
</Td>
|
||||
</Tr>
|
||||
)}
|
||||
|
||||
{criteria.map((item) => {
|
||||
const isSelected = item.id === selectedId
|
||||
const connectionSummary = criteriaConnectionSummary(item, criteria)
|
||||
|
||||
return (
|
||||
<Tr key={item.id} className={classNames(isSelected && 'bg-blue-50')}>
|
||||
<Td>
|
||||
<div className="flex-wrap inline-flex xl:flex items-center gap-2">
|
||||
<Button
|
||||
shape="circle"
|
||||
variant="plain"
|
||||
type="button"
|
||||
size="sm"
|
||||
title="Edit"
|
||||
icon={<FaEdit />}
|
||||
onClick={(event) => {
|
||||
event.preventDefault()
|
||||
onSelect(item.id)
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
shape="circle"
|
||||
variant="plain"
|
||||
type="button"
|
||||
size="sm"
|
||||
title="Delete"
|
||||
icon={<FaTrash />}
|
||||
disabled={busy}
|
||||
onClick={(event) => {
|
||||
event.preventDefault()
|
||||
onDelete(item.id)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Td>
|
||||
<Td>
|
||||
<strong className="break-words">{item.id}</strong>
|
||||
</Td>
|
||||
<Td>
|
||||
{translate(
|
||||
'::' + kindOptions.find((option) => option.value === item.kind)?.value,
|
||||
)}
|
||||
</Td>
|
||||
<Td className="min-w-[220px] break-words">{criteriaSummaryContent(item)}</Td>
|
||||
<Td className="min-w-[220px] break-words">{connectionSummary}</Td>
|
||||
</Tr>
|
||||
)
|
||||
})}
|
||||
</TBody>
|
||||
</Table>
|
||||
|
||||
<Dialog isOpen={!!selectedId} onClose={closeDialog} onRequestClose={closeDialog} width="lg">
|
||||
<form onSubmit={onSubmit} className="flex flex-1 flex-col min-h-0">
|
||||
<Dialog.Body className="flex flex-col">
|
||||
<div className="flex-1 min-h-0 overflow-y-auto pr-1">
|
||||
<FormContainer size="sm">
|
||||
<div className="grid grid-cols-2 gap-1">
|
||||
<FormItem label="Tip" asterisk>
|
||||
<SelectField
|
||||
required
|
||||
options={kindOptions}
|
||||
value={formValues.kind}
|
||||
onChange={(value) => setField('kind', value)}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label="Başlık" asterisk>
|
||||
<Input
|
||||
required
|
||||
value={formValues.title}
|
||||
onChange={(event) => setField('title', event.target.value)}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem
|
||||
label="Onaylayacak Kişi"
|
||||
asterisk={formValues.kind === 'Approval' || formValues.kind === 'Inform'}
|
||||
>
|
||||
<SelectField
|
||||
required
|
||||
options={userList}
|
||||
value={formValues.approver}
|
||||
onChange={(value) => setField('approver', value)}
|
||||
/>
|
||||
</FormItem>
|
||||
|
||||
{(formValues.kind === 'Start' || formValues.kind === 'Inform') && (
|
||||
<FormItem label="Sonraki adım" asterisk>
|
||||
{targetSelect(
|
||||
formValues.nextOnStart,
|
||||
(value) => setField('nextOnStart', value),
|
||||
true,
|
||||
)}
|
||||
</FormItem>
|
||||
)}
|
||||
|
||||
{formValues.kind === 'Approval' && (
|
||||
<>
|
||||
<FormItem label="Onay adımı" asterisk>
|
||||
{targetSelect(
|
||||
formValues.nextOnApprove,
|
||||
(value) => setField('nextOnApprove', value),
|
||||
true,
|
||||
)}
|
||||
</FormItem>
|
||||
<FormItem label="Red adımı" asterisk>
|
||||
{targetSelect(
|
||||
formValues.nextOnReject,
|
||||
(value) => setField('nextOnReject', value),
|
||||
true,
|
||||
)}
|
||||
</FormItem>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{formValues.kind === 'Compare' && (
|
||||
<div className="mb-4">
|
||||
<div className="mb-3 flex items-center justify-between gap-2">
|
||||
<h6>
|
||||
Karşılaştırma durumları
|
||||
{isLoadingSelectCommandColumns && (
|
||||
<span className="ml-2 text-xs font-normal text-gray-400">
|
||||
Sütunlar yükleniyor...
|
||||
</span>
|
||||
)}
|
||||
</h6>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
disabled={(formValues.compareOutcomes || []).length >= 4}
|
||||
onClick={() =>
|
||||
setField('compareOutcomes', [
|
||||
...(formValues.compareOutcomes || []),
|
||||
emptyCompareOutcome1(
|
||||
`Durum ${(formValues.compareOutcomes || []).length + 1}`,
|
||||
),
|
||||
])
|
||||
}
|
||||
>
|
||||
Karşılaştırma Ekle
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3">
|
||||
{(formValues.compareOutcomes || []).map(
|
||||
(outcome: CompareOutcomeDto, index: number) => (
|
||||
<div key={index} className="rounded border border-gray-200 p-3">
|
||||
<div className="flex flex-col-11 items-center gap-2 mb-2">
|
||||
<strong className="flex-[5]">Durum {index + 1}</strong>
|
||||
<strong className="flex-[5]">Bağlantı</strong>
|
||||
<span className="flex-1" />
|
||||
</div>
|
||||
<div className="flex flex-col-11 items-center gap-2">
|
||||
<Input
|
||||
required
|
||||
value={outcome.label}
|
||||
aria-label="Durum adı zorunlu"
|
||||
onChange={(event) =>
|
||||
updateCompareOutcome(index, {
|
||||
label: event.target.value,
|
||||
})
|
||||
}
|
||||
className="flex-[5]"
|
||||
/>
|
||||
{targetSelect(
|
||||
outcome.targetId,
|
||||
(targetId) =>
|
||||
updateCompareOutcome(index, {
|
||||
targetId,
|
||||
}),
|
||||
true,
|
||||
'flex-[5]',
|
||||
)}
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="plain"
|
||||
className="flex-1"
|
||||
disabled={(formValues.compareOutcomes || []).length <= 2}
|
||||
onClick={() => removeCompareOutcome(index)}
|
||||
>
|
||||
Sil
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="mt-3 grid gap-2">
|
||||
{(outcome.conditions || []).map((condition, conditionIndex) => (
|
||||
<div
|
||||
key={conditionIndex}
|
||||
className="flex flex-col-12 items-center gap-2"
|
||||
>
|
||||
<SelectField
|
||||
options={compareColumnOptions}
|
||||
value={condition.compareColumn}
|
||||
onChange={(value) =>
|
||||
updateCompareCondition(index, conditionIndex, {
|
||||
compareColumn: value,
|
||||
})
|
||||
}
|
||||
className="flex-[4]"
|
||||
/>
|
||||
<SelectField
|
||||
options={operatorOptions}
|
||||
value={condition.compareOperator}
|
||||
onChange={(value) =>
|
||||
updateCompareCondition(index, conditionIndex, {
|
||||
compareOperator: value,
|
||||
})
|
||||
}
|
||||
className="flex-[3]"
|
||||
/>
|
||||
<Input
|
||||
required
|
||||
type="number"
|
||||
step="0.01"
|
||||
value={condition.compareValue}
|
||||
onChange={(event) =>
|
||||
updateCompareCondition(index, conditionIndex, {
|
||||
compareValue: event.target.value,
|
||||
})
|
||||
}
|
||||
className="flex-[3]"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="plain"
|
||||
disabled={(outcome.conditions || []).length <= 1}
|
||||
onClick={() => removeCompareCondition(index, conditionIndex)}
|
||||
className="flex-[1]"
|
||||
>
|
||||
Sil
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
onClick={() => addCompareCondition(index)}
|
||||
className="flex-[1]"
|
||||
>
|
||||
Ekle
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</FormContainer>
|
||||
</div>
|
||||
</Dialog.Body>
|
||||
|
||||
<Dialog.Footer className="flex justify-end gap-2 border-t pt-3 mt-1">
|
||||
<Button type="button" variant="plain" disabled={busy} onClick={closeDialog}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="plain"
|
||||
disabled={busy || !formValues.id}
|
||||
onClick={() => onDelete(formValues.id)}
|
||||
>
|
||||
Sil
|
||||
</Button>
|
||||
<Button variant="solid" loading={busy} type="submit">
|
||||
Kaydet
|
||||
</Button>
|
||||
</Dialog.Footer>
|
||||
</form>
|
||||
</Dialog>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectField({
|
||||
options,
|
||||
value,
|
||||
onChange,
|
||||
required = false,
|
||||
className = '',
|
||||
}: {
|
||||
options: SelectBoxOption[]
|
||||
value: string
|
||||
onChange: (value: string) => void
|
||||
required?: boolean
|
||||
className?: string
|
||||
}) {
|
||||
const selectedOption = options.find((option) => option.value === value) ?? null
|
||||
|
||||
return (
|
||||
<>
|
||||
<Select
|
||||
options={options}
|
||||
value={selectedOption}
|
||||
onChange={(option: any) => onChange(option?.value ?? '')}
|
||||
className={className}
|
||||
/>
|
||||
{required && (
|
||||
<input
|
||||
tabIndex={-1}
|
||||
className="pointer-events-none h-0 w-0 opacity-0"
|
||||
required
|
||||
value={value}
|
||||
onChange={() => undefined}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function criteriaSummaryContent(item: WorkflowCriteriaDto) {
|
||||
if (item.kind === 'Compare') {
|
||||
const outcomes = item.compareOutcomes || []
|
||||
if (!outcomes.length) return '-'
|
||||
|
||||
return (
|
||||
<ul className="m-0 grid gap-1">
|
||||
{outcomes.map((outcome, index: number) => (
|
||||
<li key={`${outcome.label || 'outcome'}-${index}`}>
|
||||
<strong>{outcome.label || `Durum ${index + 1}`}:</strong>{' '}
|
||||
{compareOutcomeRuleText(outcome)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
|
||||
return criteriaSummary(item)
|
||||
}
|
||||
|
||||
function criteriaConnectionSummary(item: WorkflowCriteriaDto, criteria: WorkflowCriteriaDto[]) {
|
||||
if (item.kind === 'Compare') {
|
||||
const outcomes = item.compareOutcomes || []
|
||||
if (!outcomes.length) return '-'
|
||||
|
||||
return (
|
||||
<ul className="m-0 grid gap-1">
|
||||
{outcomes.map((outcome, index: number) => (
|
||||
<li key={`${outcome.label || 'target'}-${index}`}>
|
||||
<strong>{outcome.label || `Durum ${index + 1}`}:</strong>{' '}
|
||||
{targetTitle(criteria, outcome.targetId)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
|
||||
if (item.kind === 'Approval') {
|
||||
return (
|
||||
<ul className="m-0 grid gap-1">
|
||||
<li>
|
||||
<strong>Onay:</strong> {targetTitle(criteria, item.nextOnApprove)}
|
||||
</li>
|
||||
<li>
|
||||
<strong>Red:</strong> {targetTitle(criteria, item.nextOnReject)}
|
||||
</li>
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
|
||||
if (item.kind === 'Start' || item.kind === 'Inform') {
|
||||
return targetTitle(criteria, item.nextOnStart)
|
||||
}
|
||||
|
||||
return '-'
|
||||
}
|
||||
|
|
@ -2,10 +2,13 @@ import { DndContext } from '@dnd-kit/core'
|
|||
import classNames from 'classnames'
|
||||
import { FiMaximize2, FiRefreshCw, FiZoomIn, FiZoomOut } from 'react-icons/fi'
|
||||
import { kindIcon, kindOptions } from '@/utils/workflow/workflowConstants'
|
||||
import { CriteriaTable } from './CriteriaTable'
|
||||
import { FlowCanvas } from './FlowCanvas'
|
||||
import { WorkflowCriteria } from './WorkflowCriteria'
|
||||
import { WorkflowCanvas } from './WorkflowCanvas'
|
||||
import type { FormEvent, RefObject } from 'react'
|
||||
import type { WorkflowCriteriaDto } from '@/services/workflow.service'
|
||||
import { SelectBoxOption } from '@/types/shared'
|
||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||
import type { DatabaseColumnDto } from '@/proxy/sql-query-manager/models'
|
||||
|
||||
type WorkflowDesignerProps = {
|
||||
busy: boolean
|
||||
|
|
@ -17,6 +20,9 @@ type WorkflowDesignerProps = {
|
|||
dragPreview: any
|
||||
pendingLink: any
|
||||
selectedCriteriaId: string
|
||||
userList: SelectBoxOption[]
|
||||
selectCommandColumns: DatabaseColumnDto[]
|
||||
isLoadingSelectCommandColumns: boolean
|
||||
onAddCriteria: (kind: string) => void
|
||||
onBeginLink: (sourceId: string, outcome: string) => void
|
||||
onChangeCriteriaForm: (form: any) => void
|
||||
|
|
@ -54,6 +60,9 @@ export function WorkflowDesigner({
|
|||
dragPreview,
|
||||
pendingLink,
|
||||
selectedCriteriaId,
|
||||
userList,
|
||||
selectCommandColumns,
|
||||
isLoadingSelectCommandColumns,
|
||||
onAddCriteria,
|
||||
onBeginLink,
|
||||
onChangeCriteriaForm,
|
||||
|
|
@ -72,64 +81,73 @@ export function WorkflowDesigner({
|
|||
onZoomIn,
|
||||
onZoomOut,
|
||||
}: WorkflowDesignerProps) {
|
||||
const { translate } = useLocalization()
|
||||
|
||||
return (
|
||||
<section className="relative min-w-0 rounded-lg border border-gray-200 bg-white p-4 max-[1080px]:pr-4">
|
||||
<div className="mb-3.5 flex items-center justify-between gap-4 max-[720px]:flex-col max-[720px]:items-stretch">
|
||||
<DesignerTabs activeTab={designerTab} onChange={onSetDesignerTab} />
|
||||
<div className="min-h-screen">
|
||||
<main className="grid">
|
||||
<section className="relative min-w-0 rounded-lg border border-gray-200 bg-white p-4 max-[1080px]:pr-4">
|
||||
<div className="mb-3.5 flex items-center justify-between gap-4 max-[720px]:flex-col max-[720px]:items-stretch">
|
||||
<DesignerTabs activeTab={designerTab} onChange={onSetDesignerTab} />
|
||||
|
||||
{designerTab === 'flow' && (
|
||||
<DesignerToolbar
|
||||
busy={busy}
|
||||
currentCriteria={currentCriteria}
|
||||
zoom={canvasZoom}
|
||||
onAddCriteria={onAddCriteria}
|
||||
onFitLayout={onFitLayout}
|
||||
onResetDemo={onResetDemo}
|
||||
onZoomIn={onZoomIn}
|
||||
onZoomOut={onZoomOut}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{designerTab === 'flow' && (
|
||||
<DesignerToolbar
|
||||
busy={busy}
|
||||
currentCriteria={currentCriteria}
|
||||
zoom={canvasZoom}
|
||||
onAddCriteria={onAddCriteria}
|
||||
onFitLayout={onFitLayout}
|
||||
onResetDemo={onResetDemo}
|
||||
onZoomIn={onZoomIn}
|
||||
onZoomOut={onZoomOut}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{designerTab === 'flow' && (
|
||||
<div className="block min-w-0 max-[1080px]:grid-cols-1">
|
||||
<DndContext
|
||||
onDragMove={onDragMove}
|
||||
onDragCancel={() => onDragMove(null)}
|
||||
onDragEnd={onUpdateNodePosition}
|
||||
>
|
||||
<FlowCanvas
|
||||
currentCriteria={currentCriteria}
|
||||
dragPreview={dragPreview}
|
||||
zoom={canvasZoom}
|
||||
{designerTab === 'flow' && (
|
||||
<div className="block min-w-0 max-[1080px]:grid-cols-1">
|
||||
<DndContext
|
||||
onDragMove={onDragMove}
|
||||
onDragCancel={() => onDragMove(null)}
|
||||
onDragEnd={onUpdateNodePosition}
|
||||
>
|
||||
<WorkflowCanvas
|
||||
currentCriteria={currentCriteria}
|
||||
dragPreview={dragPreview}
|
||||
zoom={canvasZoom}
|
||||
selectedId={selectedCriteriaId}
|
||||
pendingLink={pendingLink}
|
||||
canvasRef={canvasRef}
|
||||
onSelect={onSelectCriteria}
|
||||
onOpenDetails={onOpenDetails}
|
||||
onClearSelection={onClearSelection}
|
||||
onDelete={onDeleteCriteria}
|
||||
onDeleteLink={onDeleteLink}
|
||||
onBeginLink={onBeginLink}
|
||||
onConnect={onConnect}
|
||||
/>
|
||||
</DndContext>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{designerTab === 'criteria' && (
|
||||
<WorkflowCriteria
|
||||
criteria={currentCriteria}
|
||||
selectedId={selectedCriteriaId}
|
||||
pendingLink={pendingLink}
|
||||
canvasRef={canvasRef}
|
||||
formValues={criteriaForm}
|
||||
busy={busy}
|
||||
userList={userList}
|
||||
selectCommandColumns={selectCommandColumns}
|
||||
isLoadingSelectCommandColumns={isLoadingSelectCommandColumns}
|
||||
onSelect={onSelectCriteria}
|
||||
onOpenDetails={onOpenDetails}
|
||||
onClearSelection={onClearSelection}
|
||||
onChange={onChangeCriteriaForm}
|
||||
onSubmit={onSaveCriteria}
|
||||
onDelete={onDeleteCriteria}
|
||||
onDeleteLink={onDeleteLink}
|
||||
onBeginLink={onBeginLink}
|
||||
onConnect={onConnect}
|
||||
/>
|
||||
</DndContext>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{designerTab === 'criteria' && (
|
||||
<CriteriaTable
|
||||
criteria={currentCriteria}
|
||||
selectedId={selectedCriteriaId}
|
||||
form={criteriaForm}
|
||||
busy={busy}
|
||||
onSelect={onSelectCriteria}
|
||||
onChange={onChangeCriteriaForm}
|
||||
onSubmit={onSaveCriteria}
|
||||
onDelete={onDeleteCriteria}
|
||||
/>
|
||||
)}
|
||||
</section>
|
||||
)}
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -152,9 +170,11 @@ function DesignerToolbar({
|
|||
onZoomIn: () => void
|
||||
onZoomOut: () => void
|
||||
}) {
|
||||
const { translate } = useLocalization()
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap justify-end gap-2">
|
||||
<button
|
||||
{/* <button
|
||||
type="button"
|
||||
className={classNames(designerButtonClass, 'border-gray-300 bg-white text-slate-700')}
|
||||
disabled={busy}
|
||||
|
|
@ -163,7 +183,7 @@ function DesignerToolbar({
|
|||
>
|
||||
<FiRefreshCw />
|
||||
Demo
|
||||
</button>
|
||||
</button> */}
|
||||
<button
|
||||
type="button"
|
||||
className={classNames(designerButtonClass, 'border-blue-600 bg-white text-blue-600')}
|
||||
|
|
@ -204,7 +224,7 @@ function DesignerToolbar({
|
|||
onClick={() => onAddCriteria(option.value)}
|
||||
>
|
||||
<Icon />
|
||||
{option.label}
|
||||
{translate(`::ListForms.ListFormEdit.Workflow.Criteria${option.value}`)}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
|
|
@ -219,6 +239,8 @@ function DesignerTabs({
|
|||
activeTab: string
|
||||
onChange: (tab: string) => void
|
||||
}) {
|
||||
const { translate } = useLocalization()
|
||||
|
||||
return (
|
||||
<div className="inline-flex gap-1 rounded-lg" role="tablist" aria-label="Akış tasarımı">
|
||||
<button
|
||||
|
|
@ -232,7 +254,7 @@ function DesignerTabs({
|
|||
)}
|
||||
onClick={() => onChange('flow')}
|
||||
>
|
||||
Akış
|
||||
{translate('::ListForms.ListFormEdit.TabWorkflow')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -245,7 +267,7 @@ function DesignerTabs({
|
|||
)}
|
||||
onClick={() => onChange('criteria')}
|
||||
>
|
||||
Adımlar
|
||||
{translate('::ListForms.ListFormEdit.Workflow.Criteria')}
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
Loading…
Reference in a new issue