@brggroup/share-lib 0.0.35 → 0.0.37
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/fesm2022/brggroup-share-lib.mjs +1562 -10
- package/fesm2022/brggroup-share-lib.mjs.map +1 -1
- package/lib/auth/auth.interceptor.d.ts +1 -3
- package/lib/auth/auth.interceptor.d.ts.map +1 -1
- package/lib/components/extend-select/extend-select.d.ts.map +1 -1
- package/lib/components/wf-editor/workflow-editor.component.d.ts +339 -0
- package/lib/components/wf-editor/workflow-editor.component.d.ts.map +1 -0
- package/package.json +1 -1
- package/public-api.d.ts +1 -0
- package/public-api.d.ts.map +1 -1
|
@@ -5,7 +5,7 @@ import { Router, ActivatedRoute, NavigationEnd } from '@angular/router';
|
|
|
5
5
|
import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
|
|
6
6
|
import * as i3 from '@ngx-translate/core';
|
|
7
7
|
import { TranslateService, TranslateModule } from '@ngx-translate/core';
|
|
8
|
-
import { firstValueFrom, BehaviorSubject, of, catchError, throwError, finalize, from, switchMap as switchMap$1,
|
|
8
|
+
import { firstValueFrom, BehaviorSubject, of, catchError, throwError, finalize, from, switchMap as switchMap$1, take, filter, fromEvent } from 'rxjs';
|
|
9
9
|
import { NzNotificationService } from 'ng-zorro-antd/notification';
|
|
10
10
|
import * as i1 from 'ng-zorro-antd/modal';
|
|
11
11
|
import { NzModalService } from 'ng-zorro-antd/modal';
|
|
@@ -42,6 +42,26 @@ import { NgxExtendedPdfViewerModule } from 'ngx-extended-pdf-viewer';
|
|
|
42
42
|
import { trigger, transition, animate, style } from '@angular/animations';
|
|
43
43
|
import * as i2$3 from 'ng-zorro-antd/spin';
|
|
44
44
|
import { NzSpinModule } from 'ng-zorro-antd/spin';
|
|
45
|
+
import * as i3$1 from 'ng-zorro-antd/card';
|
|
46
|
+
import { NzCardModule } from 'ng-zorro-antd/card';
|
|
47
|
+
import * as i4$1 from 'ng-zorro-antd/drawer';
|
|
48
|
+
import { NzDrawerModule } from 'ng-zorro-antd/drawer';
|
|
49
|
+
import * as i5$2 from 'ng-zorro-antd/tag';
|
|
50
|
+
import { NzTagModule } from 'ng-zorro-antd/tag';
|
|
51
|
+
import * as i6$3 from 'ng-zorro-antd/radio';
|
|
52
|
+
import { NzRadioModule } from 'ng-zorro-antd/radio';
|
|
53
|
+
import * as i8 from 'ng-zorro-antd/button';
|
|
54
|
+
import { NzButtonModule } from 'ng-zorro-antd/button';
|
|
55
|
+
import * as i11 from 'ng-zorro-antd/tooltip';
|
|
56
|
+
import { NzToolTipModule } from 'ng-zorro-antd/tooltip';
|
|
57
|
+
import * as i14 from 'ng-zorro-antd/flex';
|
|
58
|
+
import { NzFlexModule } from 'ng-zorro-antd/flex';
|
|
59
|
+
import * as i15 from 'ng-zorro-antd/table';
|
|
60
|
+
import { NzTableModule } from 'ng-zorro-antd/table';
|
|
61
|
+
import * as i16 from 'ng-zorro-antd/divider';
|
|
62
|
+
import { NzDividerModule } from 'ng-zorro-antd/divider';
|
|
63
|
+
import * as i9 from 'ng-zorro-antd/core/transition-patch';
|
|
64
|
+
import * as i10 from 'ng-zorro-antd/core/wave';
|
|
45
65
|
import * as XLSX from 'xlsx';
|
|
46
66
|
|
|
47
67
|
var TranslateKey;
|
|
@@ -756,10 +776,8 @@ function handle401(req, next, router, authService) {
|
|
|
756
776
|
router.navigate(['/login'], {
|
|
757
777
|
queryParams: { returnUrl: initialReturnUrl || '/' },
|
|
758
778
|
});
|
|
759
|
-
refreshTokenSubject.
|
|
760
|
-
return
|
|
761
|
-
IsSuccess: false,
|
|
762
|
-
});
|
|
779
|
+
refreshTokenSubject.error('Refresh token failed');
|
|
780
|
+
return throwError(() => new Error('Refresh token failed'));
|
|
763
781
|
}
|
|
764
782
|
// Phát token mới cho các request đang chờ
|
|
765
783
|
refreshTokenSubject.next(TokenStorage.getToken());
|
|
@@ -782,9 +800,9 @@ function handle401(req, next, router, authService) {
|
|
|
782
800
|
}
|
|
783
801
|
else {
|
|
784
802
|
// Nếu đang refresh → chờ token mới rồi retry
|
|
785
|
-
return refreshTokenSubject.pipe(
|
|
803
|
+
return refreshTokenSubject.pipe(take(1), switchMap$1((token) => next(req.clone({
|
|
786
804
|
setHeaders: { Authorization: `Bearer ${token}` },
|
|
787
|
-
}))));
|
|
805
|
+
}))), catchError((err) => throwError(() => err)));
|
|
788
806
|
}
|
|
789
807
|
}
|
|
790
808
|
function convertDatesInBody(obj) {
|
|
@@ -2327,7 +2345,10 @@ class ExtendSelectComponent {
|
|
|
2327
2345
|
return item[this.displayField];
|
|
2328
2346
|
}
|
|
2329
2347
|
if (this.displayFields?.length) {
|
|
2330
|
-
const display = this.displayFields
|
|
2348
|
+
const display = this.displayFields
|
|
2349
|
+
.map((field) => item[field])
|
|
2350
|
+
.filter((x) => x)
|
|
2351
|
+
.join(' - ');
|
|
2331
2352
|
return display;
|
|
2332
2353
|
}
|
|
2333
2354
|
return item;
|
|
@@ -2682,6 +2703,1537 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.1.7", ngImpor
|
|
|
2682
2703
|
type: Input
|
|
2683
2704
|
}] } });
|
|
2684
2705
|
|
|
2706
|
+
class WF_Template {
|
|
2707
|
+
// #region properties
|
|
2708
|
+
WF_Template_Id;
|
|
2709
|
+
App_Org_Id;
|
|
2710
|
+
CreatedDate;
|
|
2711
|
+
CreatedUser;
|
|
2712
|
+
UpdatedDate;
|
|
2713
|
+
UpdatedUser;
|
|
2714
|
+
IsActive;
|
|
2715
|
+
Code;
|
|
2716
|
+
Name;
|
|
2717
|
+
DocType;
|
|
2718
|
+
Description;
|
|
2719
|
+
TemplatePrint;
|
|
2720
|
+
// #endregion properties
|
|
2721
|
+
constructor(obj) {
|
|
2722
|
+
obj = obj || {};
|
|
2723
|
+
this.WF_Template_Id = obj.WF_Template_Id || null;
|
|
2724
|
+
this.App_Org_Id = obj.App_Org_Id || '';
|
|
2725
|
+
this.CreatedDate = obj.CreatedDate || null;
|
|
2726
|
+
this.CreatedUser = obj.CreatedUser || '';
|
|
2727
|
+
this.UpdatedDate = obj.UpdatedDate || null;
|
|
2728
|
+
this.UpdatedUser = obj.UpdatedUser || '';
|
|
2729
|
+
this.IsActive = obj.IsActive || null;
|
|
2730
|
+
this.Code = obj.Code || '';
|
|
2731
|
+
this.Name = obj.Name || '';
|
|
2732
|
+
this.DocType = obj.DocType || '';
|
|
2733
|
+
this.Description = obj.Description || '';
|
|
2734
|
+
this.TemplatePrint = obj.TemplatePrint || '';
|
|
2735
|
+
}
|
|
2736
|
+
}
|
|
2737
|
+
class WF_TemplateStage {
|
|
2738
|
+
// #region properties
|
|
2739
|
+
WF_TemplateStage_Id;
|
|
2740
|
+
App_Org_Id;
|
|
2741
|
+
CreatedDate;
|
|
2742
|
+
CreatedUser;
|
|
2743
|
+
UpdatedDate;
|
|
2744
|
+
UpdatedUser;
|
|
2745
|
+
IsActive;
|
|
2746
|
+
WF_Template_Id;
|
|
2747
|
+
Code;
|
|
2748
|
+
Name;
|
|
2749
|
+
StageType;
|
|
2750
|
+
SeqValue;
|
|
2751
|
+
LimitValue;
|
|
2752
|
+
IsRequireUser;
|
|
2753
|
+
// #endregion properties
|
|
2754
|
+
constructor(obj) {
|
|
2755
|
+
obj = obj || {};
|
|
2756
|
+
this.WF_TemplateStage_Id = obj.WF_TemplateStage_Id || null;
|
|
2757
|
+
this.App_Org_Id = obj.App_Org_Id || '';
|
|
2758
|
+
this.CreatedDate = obj.CreatedDate || null;
|
|
2759
|
+
this.CreatedUser = obj.CreatedUser || '';
|
|
2760
|
+
this.UpdatedDate = obj.UpdatedDate || null;
|
|
2761
|
+
this.UpdatedUser = obj.UpdatedUser || '';
|
|
2762
|
+
this.IsActive = obj.IsActive || null;
|
|
2763
|
+
this.WF_Template_Id = obj.WF_Template_Id || null;
|
|
2764
|
+
this.Code = obj.Code || '';
|
|
2765
|
+
this.Name = obj.Name || '';
|
|
2766
|
+
this.StageType = obj.StageType || '';
|
|
2767
|
+
this.SeqValue = obj.SeqValue || null;
|
|
2768
|
+
this.LimitValue = obj.LimitValue || null;
|
|
2769
|
+
this.IsRequireUser = obj.IsRequireUser || null;
|
|
2770
|
+
}
|
|
2771
|
+
}
|
|
2772
|
+
class WF_TemplateStageAction {
|
|
2773
|
+
// #region properties
|
|
2774
|
+
WF_TemplateStageAction_Id;
|
|
2775
|
+
App_Org_Id;
|
|
2776
|
+
CreatedDate;
|
|
2777
|
+
CreatedUser;
|
|
2778
|
+
UpdatedDate;
|
|
2779
|
+
UpdatedUser;
|
|
2780
|
+
IsActive;
|
|
2781
|
+
WF_Template_Id;
|
|
2782
|
+
WF_TemplateStage_Id;
|
|
2783
|
+
ActionType;
|
|
2784
|
+
ActionStatus;
|
|
2785
|
+
ActionText;
|
|
2786
|
+
StageStatus;
|
|
2787
|
+
AllowNote;
|
|
2788
|
+
AllowEmail;
|
|
2789
|
+
WF_EmailTemplate;
|
|
2790
|
+
To_WF_TemplateStage_Id;
|
|
2791
|
+
To_StageStatus;
|
|
2792
|
+
WF_TransitionsRule_Id;
|
|
2793
|
+
// #endregion properties
|
|
2794
|
+
constructor(obj) {
|
|
2795
|
+
obj = obj || {};
|
|
2796
|
+
this.WF_TemplateStageAction_Id = obj.WF_TemplateStageAction_Id || null;
|
|
2797
|
+
this.App_Org_Id = obj.App_Org_Id || '';
|
|
2798
|
+
this.CreatedDate = obj.CreatedDate || null;
|
|
2799
|
+
this.CreatedUser = obj.CreatedUser || '';
|
|
2800
|
+
this.UpdatedDate = obj.UpdatedDate || null;
|
|
2801
|
+
this.UpdatedUser = obj.UpdatedUser || '';
|
|
2802
|
+
this.IsActive = obj.IsActive || null;
|
|
2803
|
+
this.WF_Template_Id = obj.WF_Template_Id || null;
|
|
2804
|
+
this.WF_TemplateStage_Id = obj.WF_TemplateStage_Id || null;
|
|
2805
|
+
this.ActionType = obj.ActionType || '';
|
|
2806
|
+
this.ActionStatus = obj.ActionStatus || '';
|
|
2807
|
+
this.ActionText = obj.ActionText || '';
|
|
2808
|
+
this.StageStatus = obj.StageStatus || '';
|
|
2809
|
+
this.AllowNote = obj.AllowNote || null;
|
|
2810
|
+
this.AllowEmail = obj.AllowEmail || null;
|
|
2811
|
+
this.WF_EmailTemplate = obj.WF_EmailTemplate || '';
|
|
2812
|
+
this.To_WF_TemplateStage_Id = obj.To_WF_TemplateStage_Id || null;
|
|
2813
|
+
this.To_StageStatus = obj.To_StageStatus || '';
|
|
2814
|
+
this.WF_TransitionsRule_Id = obj.WF_TransitionsRule_Id || null;
|
|
2815
|
+
}
|
|
2816
|
+
}
|
|
2817
|
+
class Workflow extends WF_Template {
|
|
2818
|
+
lstStageStatus = [];
|
|
2819
|
+
lstActionType = [];
|
|
2820
|
+
lstActionStatus = [];
|
|
2821
|
+
}
|
|
2822
|
+
/**
|
|
2823
|
+
* STAGE
|
|
2824
|
+
*/
|
|
2825
|
+
class WorkflowStage extends WF_TemplateStage {
|
|
2826
|
+
x = 0;
|
|
2827
|
+
y = 0;
|
|
2828
|
+
isReverse;
|
|
2829
|
+
isReverseLabel;
|
|
2830
|
+
lstAction;
|
|
2831
|
+
drawerPosition;
|
|
2832
|
+
constructor(obj) {
|
|
2833
|
+
obj = obj || {};
|
|
2834
|
+
super(obj);
|
|
2835
|
+
this.x = obj.x;
|
|
2836
|
+
this.y = obj.y;
|
|
2837
|
+
this.isReverse = obj.isReverse;
|
|
2838
|
+
this.isReverseLabel = obj.isReverseLabel;
|
|
2839
|
+
this.lstAction = obj.lstAction;
|
|
2840
|
+
this.drawerPosition = obj.drawerPosition;
|
|
2841
|
+
}
|
|
2842
|
+
}
|
|
2843
|
+
/**
|
|
2844
|
+
* Action
|
|
2845
|
+
*/
|
|
2846
|
+
class WorkflowAction extends WF_TemplateStageAction {
|
|
2847
|
+
NextStageStatus; // string
|
|
2848
|
+
FromStage;
|
|
2849
|
+
FromSide;
|
|
2850
|
+
ToStage;
|
|
2851
|
+
ToSide;
|
|
2852
|
+
allowBack; // true = 2 đầu, false/undefined = 1 đầu
|
|
2853
|
+
labelBack;
|
|
2854
|
+
points;
|
|
2855
|
+
isBackAction;
|
|
2856
|
+
}
|
|
2857
|
+
class WorkflowEditorComponent extends BaseComponent {
|
|
2858
|
+
//#region NGHIỆP VỤ
|
|
2859
|
+
lstOrg = [];
|
|
2860
|
+
lstTemplateType = [];
|
|
2861
|
+
lstTemplatePrint = [];
|
|
2862
|
+
template = new Workflow();
|
|
2863
|
+
lstStage = [
|
|
2864
|
+
new WorkflowStage({ Code: 'start', Name: 'Start', x: -35, y: 120, StageType: 'START', SeqValue: 1 }),
|
|
2865
|
+
new WorkflowStage({ Code: 'end', Name: 'End', x: 780, y: 120, StageType: 'END', SeqValue: 999 }),
|
|
2866
|
+
];
|
|
2867
|
+
lstAction = [];
|
|
2868
|
+
onSave = new EventEmitter();
|
|
2869
|
+
//#endregion
|
|
2870
|
+
Math = Math;
|
|
2871
|
+
drawTemplateVisibel = false;
|
|
2872
|
+
settingVisible = false;
|
|
2873
|
+
wfcSetting = { ShowGrid: true };
|
|
2874
|
+
selectedStage;
|
|
2875
|
+
hoverStage;
|
|
2876
|
+
selectedAction;
|
|
2877
|
+
selectedBackAction;
|
|
2878
|
+
hoverEdge;
|
|
2879
|
+
// vẽ đường liên kết mới
|
|
2880
|
+
connectingFrom;
|
|
2881
|
+
connectingPoints = [];
|
|
2882
|
+
previewX = 0;
|
|
2883
|
+
previewY = 0;
|
|
2884
|
+
/** điểm đang chọn để di chuyển */
|
|
2885
|
+
draggingPoint;
|
|
2886
|
+
/** đang kéo chọn nhiều node */
|
|
2887
|
+
isSelecting = false;
|
|
2888
|
+
/** điểm bắt đầu kéo */
|
|
2889
|
+
selectStart;
|
|
2890
|
+
/** điểm kết thúc kéo */
|
|
2891
|
+
selectEnd;
|
|
2892
|
+
draggingGroup = false;
|
|
2893
|
+
dragStartMouse;
|
|
2894
|
+
dragStartPositions = new Map();
|
|
2895
|
+
/** list các điểm đang được chọn */
|
|
2896
|
+
lstSelectedStageCode = new Set();
|
|
2897
|
+
// SNAP point
|
|
2898
|
+
gridSize = 20; // 10 / 20 / 25 tuỳ thích
|
|
2899
|
+
snapToGrid = true;
|
|
2900
|
+
// ===== drag node =====
|
|
2901
|
+
draggingNode;
|
|
2902
|
+
isDragging = false;
|
|
2903
|
+
dragStartX = 0;
|
|
2904
|
+
dragStartY = 0;
|
|
2905
|
+
offsetX = 0;
|
|
2906
|
+
offsetY = 0;
|
|
2907
|
+
KEY_MOVE_STEP = 5; // px
|
|
2908
|
+
KEY_MOVE_STEP_BIG = 20; // Shift + arrow
|
|
2909
|
+
// pan: dịch chuyển cả canvas
|
|
2910
|
+
isPanning = false;
|
|
2911
|
+
panStart;
|
|
2912
|
+
// panX = 0;
|
|
2913
|
+
// panY = 0;
|
|
2914
|
+
panX = -5000;
|
|
2915
|
+
panY = -5000;
|
|
2916
|
+
isMoveMode = true;
|
|
2917
|
+
canvasRef;
|
|
2918
|
+
svgRef;
|
|
2919
|
+
get normalEdges() {
|
|
2920
|
+
return this.lstAction.filter((e) => e !== this.hoverEdge && !e.isBackAction);
|
|
2921
|
+
}
|
|
2922
|
+
get hoverEdges() {
|
|
2923
|
+
if (this.hoverEdge && !this.hoverEdge.isBackAction) {
|
|
2924
|
+
return [this.hoverEdge];
|
|
2925
|
+
}
|
|
2926
|
+
if (this.selectedAction && !this.selectedAction.isBackAction) {
|
|
2927
|
+
return [this.selectedAction];
|
|
2928
|
+
}
|
|
2929
|
+
return [];
|
|
2930
|
+
}
|
|
2931
|
+
axisLock = null;
|
|
2932
|
+
onKeyDown(e) {
|
|
2933
|
+
// bỏ qua nếu đang gõ input
|
|
2934
|
+
const target = e.target;
|
|
2935
|
+
if (['INPUT', 'TEXTAREA'].includes(target.tagName))
|
|
2936
|
+
return;
|
|
2937
|
+
if (e.key === 'Escape') {
|
|
2938
|
+
this.cancelConnecting();
|
|
2939
|
+
}
|
|
2940
|
+
if (e.ctrlKey && e.key === 'z') {
|
|
2941
|
+
console.log('ctrl z');
|
|
2942
|
+
e.preventDefault();
|
|
2943
|
+
this.undo();
|
|
2944
|
+
}
|
|
2945
|
+
if (e.ctrlKey && (e.key === 'y' || (e.shiftKey && e.key === 'Z'))) {
|
|
2946
|
+
console.log('ctrl y');
|
|
2947
|
+
e.preventDefault();
|
|
2948
|
+
this.redo();
|
|
2949
|
+
}
|
|
2950
|
+
if (['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].indexOf(e.key) >= 0) {
|
|
2951
|
+
let dx = 0;
|
|
2952
|
+
let dy = 0;
|
|
2953
|
+
const step = e.shiftKey ? this.KEY_MOVE_STEP_BIG : this.KEY_MOVE_STEP;
|
|
2954
|
+
switch (e.key) {
|
|
2955
|
+
case 'ArrowLeft':
|
|
2956
|
+
dx = -step;
|
|
2957
|
+
break;
|
|
2958
|
+
case 'ArrowRight':
|
|
2959
|
+
dx = step;
|
|
2960
|
+
break;
|
|
2961
|
+
case 'ArrowUp':
|
|
2962
|
+
dy = -step;
|
|
2963
|
+
break;
|
|
2964
|
+
case 'ArrowDown':
|
|
2965
|
+
dy = step;
|
|
2966
|
+
break;
|
|
2967
|
+
default:
|
|
2968
|
+
return;
|
|
2969
|
+
}
|
|
2970
|
+
e.preventDefault();
|
|
2971
|
+
this.moveSelectedNodesBy(dx, dy);
|
|
2972
|
+
}
|
|
2973
|
+
}
|
|
2974
|
+
ngOnInit() {
|
|
2975
|
+
console.log('ngOnInit', this.template, this.lstStage, this.lstAction);
|
|
2976
|
+
console.log('template', this.template);
|
|
2977
|
+
console.log('lstStage', this.lstStage);
|
|
2978
|
+
console.log('lstAction', this.lstAction);
|
|
2979
|
+
this.lstStage.forEach((x) => {
|
|
2980
|
+
x.lstAction = this.lstAction.filter((y) => y.FromStage == x.Code);
|
|
2981
|
+
});
|
|
2982
|
+
}
|
|
2983
|
+
ngAfterViewInit() {
|
|
2984
|
+
this.updateCanvasSize();
|
|
2985
|
+
}
|
|
2986
|
+
moveSelectedNodesBy(dx, dy) {
|
|
2987
|
+
if (this.lstSelectedStageCode.size === 0)
|
|
2988
|
+
return;
|
|
2989
|
+
// ⭐ UNDO SNAPSHOT
|
|
2990
|
+
this.pushUndo({
|
|
2991
|
+
type: 'move-nodes',
|
|
2992
|
+
before: this.cloneNodePositions(this.lstSelectedStageCode),
|
|
2993
|
+
});
|
|
2994
|
+
for (const n of this.lstStage) {
|
|
2995
|
+
if (!this.lstSelectedStageCode.has(n.Code))
|
|
2996
|
+
continue;
|
|
2997
|
+
n.x = this.clamp(n.x + dx, 0, this.CANVAS_WIDTH - this.NODE_WIDTH);
|
|
2998
|
+
n.y = this.clamp(n.y + dy, 0, this.CANVAS_HEIGHT - this.NODE_HEIGHT);
|
|
2999
|
+
}
|
|
3000
|
+
}
|
|
3001
|
+
save() {
|
|
3002
|
+
this.onSave.emit({ template: this.template, lstStage: this.lstStage, lstAction: this.lstAction });
|
|
3003
|
+
}
|
|
3004
|
+
addStage() {
|
|
3005
|
+
console.log('addStage');
|
|
3006
|
+
const code = 'New_Stage_' + Math.round(Math.random() * 1000);
|
|
3007
|
+
// tìm vị trí trống (đơn giản & hiệu quả)
|
|
3008
|
+
const baseX = 200;
|
|
3009
|
+
const baseY = 120;
|
|
3010
|
+
const GAP_X = 200;
|
|
3011
|
+
const GAP_Y = 120;
|
|
3012
|
+
let x = baseX;
|
|
3013
|
+
let y = baseY;
|
|
3014
|
+
while (this.isPositionOccupied(x, y)) {
|
|
3015
|
+
y += GAP_Y;
|
|
3016
|
+
}
|
|
3017
|
+
const stage = new WorkflowStage();
|
|
3018
|
+
stage.Code = code;
|
|
3019
|
+
stage.Name = 'New Stage';
|
|
3020
|
+
stage.x = x;
|
|
3021
|
+
stage.y = y;
|
|
3022
|
+
stage.StageType = 'NODE';
|
|
3023
|
+
stage.SeqValue = 10;
|
|
3024
|
+
this.lstStage.push(stage);
|
|
3025
|
+
this.lstStage = this.lstStage.sort((a, b) => a.SeqValue - b.SeqValue);
|
|
3026
|
+
// this.selectedStage = node;
|
|
3027
|
+
this.hoverStage = stage;
|
|
3028
|
+
}
|
|
3029
|
+
isConnectingFrom(node) {
|
|
3030
|
+
return this.connectingFrom?.node.Code === node.Code;
|
|
3031
|
+
}
|
|
3032
|
+
isDraggingFrom(node) {
|
|
3033
|
+
return this.draggingNode?.Code === node.Code;
|
|
3034
|
+
}
|
|
3035
|
+
isSelectedNode(node) {
|
|
3036
|
+
return this.selectedStage?.Code === node.Code;
|
|
3037
|
+
}
|
|
3038
|
+
cancelConnecting() {
|
|
3039
|
+
if (this.connectingFrom) {
|
|
3040
|
+
this.connectingFrom = undefined;
|
|
3041
|
+
this.previewX = 0;
|
|
3042
|
+
this.previewY = 0;
|
|
3043
|
+
this.connectingPoints = [];
|
|
3044
|
+
}
|
|
3045
|
+
}
|
|
3046
|
+
//#region NGHIỆP VỤ
|
|
3047
|
+
addAction() {
|
|
3048
|
+
if (!this.selectedStage)
|
|
3049
|
+
return;
|
|
3050
|
+
const action = new WorkflowAction();
|
|
3051
|
+
action.FromStage = this.selectedStage.Code;
|
|
3052
|
+
action.FromSide = 'right';
|
|
3053
|
+
this.selectedStage.lstAction = this.selectedStage.lstAction ? [...this.selectedStage.lstAction, action] : [action];
|
|
3054
|
+
this.lstAction = this.lstAction ? [...this.lstAction, action] : [action];
|
|
3055
|
+
}
|
|
3056
|
+
onchangeAllowBack(action) {
|
|
3057
|
+
console.log('onchangeAllowBack', action);
|
|
3058
|
+
const stage = this.lstStage.find((x) => x.Code == action.ToStage);
|
|
3059
|
+
if (!stage) {
|
|
3060
|
+
this.notiService.error('Stage not found');
|
|
3061
|
+
return;
|
|
3062
|
+
}
|
|
3063
|
+
if (action.allowBack) {
|
|
3064
|
+
const newAction = new WorkflowAction();
|
|
3065
|
+
newAction.FromStage = action.ToStage;
|
|
3066
|
+
newAction.FromSide = 'right';
|
|
3067
|
+
newAction.ToStage = action.FromStage;
|
|
3068
|
+
newAction.ToSide = 'left';
|
|
3069
|
+
newAction.isBackAction = true;
|
|
3070
|
+
console.log('newAction', newAction);
|
|
3071
|
+
this.lstAction = [...this.lstAction, newAction];
|
|
3072
|
+
stage.lstAction = [...stage.lstAction, newAction];
|
|
3073
|
+
this.selectedBackAction = newAction;
|
|
3074
|
+
}
|
|
3075
|
+
else {
|
|
3076
|
+
action.labelBack = '';
|
|
3077
|
+
const backAction = this.lstAction.find((x) => x.FromStage == action.ToStage && x.ToStage == action.FromStage && x.isBackAction);
|
|
3078
|
+
console.log('remove action', backAction);
|
|
3079
|
+
if (!backAction) {
|
|
3080
|
+
this.notiService.error('Action not found');
|
|
3081
|
+
}
|
|
3082
|
+
else {
|
|
3083
|
+
this.lstAction = [...this.lstAction.filter((x) => x != backAction)];
|
|
3084
|
+
stage.lstAction = [...stage.lstAction?.filter((x) => x != backAction)];
|
|
3085
|
+
this.selectedBackAction = undefined;
|
|
3086
|
+
}
|
|
3087
|
+
}
|
|
3088
|
+
}
|
|
3089
|
+
onchangeLabelBack(action) {
|
|
3090
|
+
const backAction = this.lstAction.find((x) => x.FromStage == action.ToStage && x.ToStage == action.FromStage && x.isBackAction);
|
|
3091
|
+
if (!backAction) {
|
|
3092
|
+
this.notiService.error('Action not found');
|
|
3093
|
+
}
|
|
3094
|
+
else {
|
|
3095
|
+
backAction.ActionText = action.labelBack || '';
|
|
3096
|
+
}
|
|
3097
|
+
}
|
|
3098
|
+
onchangeActionText(action) {
|
|
3099
|
+
if (action.isBackAction) {
|
|
3100
|
+
const goAction = this.lstAction.find((x) => x.FromStage == action.ToStage && x.ToStage == action.FromStage && !x.isBackAction);
|
|
3101
|
+
if (!goAction) {
|
|
3102
|
+
this.notiService.error('Action not found');
|
|
3103
|
+
}
|
|
3104
|
+
else {
|
|
3105
|
+
goAction.labelBack = action.ActionText || '';
|
|
3106
|
+
}
|
|
3107
|
+
}
|
|
3108
|
+
}
|
|
3109
|
+
onchangeNextStage(action) {
|
|
3110
|
+
console.log('onchangeNextStage', action);
|
|
3111
|
+
}
|
|
3112
|
+
async deleteNode() {
|
|
3113
|
+
if (!this.selectedStage)
|
|
3114
|
+
return;
|
|
3115
|
+
if (!(await this.confirm(`Xoá node: ${this.selectedStage.Name}`)))
|
|
3116
|
+
return;
|
|
3117
|
+
const node = this.selectedStage;
|
|
3118
|
+
// 🔹 snapshot edges liên quan
|
|
3119
|
+
const relatedEdges = this.lstAction.filter((e) => e.FromStage === node.Code || e.ToStage === node.Code);
|
|
3120
|
+
// 🔹 snapshot vị trí node trong mảng
|
|
3121
|
+
const nodeIndex = this.lstStage.findIndex((n) => n.Code === node.Code);
|
|
3122
|
+
// ⭐ PUSH UNDO
|
|
3123
|
+
this.pushUndo({
|
|
3124
|
+
type: 'delete-node',
|
|
3125
|
+
node: { ...node }, // clone
|
|
3126
|
+
nodeIndex,
|
|
3127
|
+
edges: relatedEdges.map((e) => ({ ...e })),
|
|
3128
|
+
});
|
|
3129
|
+
// ❌ xoá edges
|
|
3130
|
+
this.lstAction = this.lstAction.filter((e) => e.FromStage !== node.Code && e.ToStage !== node.Code);
|
|
3131
|
+
// ❌ xoá node
|
|
3132
|
+
this.lstStage.splice(nodeIndex, 1);
|
|
3133
|
+
this.selectedStage = undefined;
|
|
3134
|
+
}
|
|
3135
|
+
async deleteAction(action) {
|
|
3136
|
+
if (!(await this.confirm(`Delete action ???`)))
|
|
3137
|
+
return;
|
|
3138
|
+
const stage = this.lstStage.find((x) => x.Code == action.FromStage);
|
|
3139
|
+
if (!stage)
|
|
3140
|
+
return;
|
|
3141
|
+
stage.lstAction = stage.lstAction?.filter((x) => x != action);
|
|
3142
|
+
this.lstAction = this.lstAction.filter((x) => x != action);
|
|
3143
|
+
}
|
|
3144
|
+
//#endregion
|
|
3145
|
+
//#region Action type
|
|
3146
|
+
inputStageStatusVisible = false;
|
|
3147
|
+
inputStageStatusCode = '';
|
|
3148
|
+
inputStageStatusName = '';
|
|
3149
|
+
inputStageStatusElement;
|
|
3150
|
+
checkRemoveableStageStatus(tag) {
|
|
3151
|
+
if (this.lstAction.find((x) => x.StageStatus === tag.Code) ||
|
|
3152
|
+
this.lstAction.find((x) => x.NextStageStatus === tag.Code)) {
|
|
3153
|
+
return false;
|
|
3154
|
+
}
|
|
3155
|
+
return true;
|
|
3156
|
+
}
|
|
3157
|
+
handleCloseStageStatus(removedTag) {
|
|
3158
|
+
this.template.lstStageStatus = this.template.lstStageStatus.filter((tag) => tag !== removedTag);
|
|
3159
|
+
}
|
|
3160
|
+
showInputStageStatus() {
|
|
3161
|
+
this.inputStageStatusVisible = true;
|
|
3162
|
+
setTimeout(() => {
|
|
3163
|
+
this.inputStageStatusElement?.nativeElement.focus();
|
|
3164
|
+
}, 10);
|
|
3165
|
+
}
|
|
3166
|
+
handleInputStageStatusConfirm() {
|
|
3167
|
+
if (this.inputStageStatusCode && !this.template.lstStageStatus.find((x) => x.Code === this.inputValue)) {
|
|
3168
|
+
this.template.lstStageStatus = [
|
|
3169
|
+
...this.template.lstStageStatus,
|
|
3170
|
+
{ Code: this.inputStageStatusCode, Name: this.inputStageStatusName },
|
|
3171
|
+
];
|
|
3172
|
+
}
|
|
3173
|
+
this.inputStageStatusCode = '';
|
|
3174
|
+
this.inputStageStatusName = '';
|
|
3175
|
+
this.inputStageStatusVisible = false;
|
|
3176
|
+
}
|
|
3177
|
+
//#endregion
|
|
3178
|
+
//#region Action type
|
|
3179
|
+
sliceTagName(tag) {
|
|
3180
|
+
// const isLongTag = tag.length > 20;
|
|
3181
|
+
// return isLongTag ? `${tag.slice(0, 20)}...` : tag;
|
|
3182
|
+
return tag;
|
|
3183
|
+
}
|
|
3184
|
+
inputActionTypeVisible = false;
|
|
3185
|
+
inputActionTypeCode = '';
|
|
3186
|
+
inputActionTypeName = '';
|
|
3187
|
+
inputActionTypeElement;
|
|
3188
|
+
checkRemoveableActionType(tag) {
|
|
3189
|
+
if (this.lstAction.find((x) => x.ActionType === tag.Code)) {
|
|
3190
|
+
return false;
|
|
3191
|
+
}
|
|
3192
|
+
return true;
|
|
3193
|
+
}
|
|
3194
|
+
handleCloseActionType(removedTag) {
|
|
3195
|
+
this.template.lstActionType = this.template.lstActionType.filter((tag) => tag !== removedTag);
|
|
3196
|
+
}
|
|
3197
|
+
showInputActionType() {
|
|
3198
|
+
this.inputActionTypeVisible = true;
|
|
3199
|
+
setTimeout(() => {
|
|
3200
|
+
console.log(this.inputActionTypeElement);
|
|
3201
|
+
this.inputActionTypeElement?.nativeElement.focus();
|
|
3202
|
+
}, 10);
|
|
3203
|
+
}
|
|
3204
|
+
handleInputActionTypeConfirm() {
|
|
3205
|
+
if (this.inputActionTypeCode && !this.template.lstActionType.find((x) => x.Code === this.inputValue)) {
|
|
3206
|
+
this.template.lstActionType = [
|
|
3207
|
+
...this.template.lstActionType,
|
|
3208
|
+
{ Code: this.inputActionTypeCode, Name: this.inputActionTypeName },
|
|
3209
|
+
];
|
|
3210
|
+
}
|
|
3211
|
+
this.inputActionTypeCode = '';
|
|
3212
|
+
this.inputActionTypeName = '';
|
|
3213
|
+
this.inputActionTypeVisible = false;
|
|
3214
|
+
}
|
|
3215
|
+
//#endregion
|
|
3216
|
+
//#region Action status
|
|
3217
|
+
inputVisible = false;
|
|
3218
|
+
inputValue = '';
|
|
3219
|
+
inputElement;
|
|
3220
|
+
checkRemoveableActionStatus(tag) {
|
|
3221
|
+
if (this.lstAction.find((x) => x.ActionStatus === tag.Code)) {
|
|
3222
|
+
return false;
|
|
3223
|
+
}
|
|
3224
|
+
return true;
|
|
3225
|
+
}
|
|
3226
|
+
handleClose(removedTag) {
|
|
3227
|
+
this.template.lstActionStatus = this.template.lstActionStatus.filter((tag) => tag !== removedTag);
|
|
3228
|
+
}
|
|
3229
|
+
showInput() {
|
|
3230
|
+
this.inputVisible = true;
|
|
3231
|
+
setTimeout(() => {
|
|
3232
|
+
console.log(this.inputElement);
|
|
3233
|
+
this.inputElement?.nativeElement.focus();
|
|
3234
|
+
}, 10);
|
|
3235
|
+
}
|
|
3236
|
+
handleInputConfirm() {
|
|
3237
|
+
if (this.inputValue && !this.template.lstActionStatus.find((x) => x.Code === this.inputValue)) {
|
|
3238
|
+
this.template.lstActionStatus = [...this.template.lstActionStatus, { Code: this.inputValue }];
|
|
3239
|
+
}
|
|
3240
|
+
this.inputValue = '';
|
|
3241
|
+
this.inputVisible = false;
|
|
3242
|
+
}
|
|
3243
|
+
//#endregion
|
|
3244
|
+
//#region Canvas action
|
|
3245
|
+
onMouseDownCanvas(event) {
|
|
3246
|
+
console.log('onMouseDownCanvas > event.button', event.button);
|
|
3247
|
+
if (event.button !== 0)
|
|
3248
|
+
return; // button === 0 : chuột trái
|
|
3249
|
+
// click vào canvas trống
|
|
3250
|
+
if (event.target.closest('.workflow-node'))
|
|
3251
|
+
return;
|
|
3252
|
+
// 🖐 SPACE + drag → pan
|
|
3253
|
+
if (this.isMoveMode) {
|
|
3254
|
+
this.isPanning = true;
|
|
3255
|
+
this.panStart = {
|
|
3256
|
+
x: event.clientX,
|
|
3257
|
+
y: event.clientY,
|
|
3258
|
+
};
|
|
3259
|
+
this.canvasRef?.nativeElement.classList.add('panning');
|
|
3260
|
+
return;
|
|
3261
|
+
}
|
|
3262
|
+
const p = this.getSvgPoint(event, true);
|
|
3263
|
+
this.isSelecting = true;
|
|
3264
|
+
this.selectStart = p;
|
|
3265
|
+
this.selectEnd = p;
|
|
3266
|
+
this.lstSelectedStageCode.clear();
|
|
3267
|
+
}
|
|
3268
|
+
onMouseMoveCanvas(event) {
|
|
3269
|
+
if (this.isPanning && this.panStart) {
|
|
3270
|
+
const dx = event.clientX - this.panStart.x;
|
|
3271
|
+
const dy = event.clientY - this.panStart.y;
|
|
3272
|
+
this.panX += dx;
|
|
3273
|
+
this.panY += dy;
|
|
3274
|
+
this.panStart = {
|
|
3275
|
+
x: event.clientX,
|
|
3276
|
+
y: event.clientY,
|
|
3277
|
+
};
|
|
3278
|
+
return;
|
|
3279
|
+
}
|
|
3280
|
+
const p = this.getSvgPoint(event);
|
|
3281
|
+
const dx = Math.abs(event.clientX - this.dragStartX);
|
|
3282
|
+
const dy = Math.abs(event.clientY - this.dragStartY);
|
|
3283
|
+
if (dx > 4 || dy > 4) {
|
|
3284
|
+
this.isDragging = true;
|
|
3285
|
+
}
|
|
3286
|
+
// 1️⃣ đang kéo vùng chọn
|
|
3287
|
+
if (this.isSelecting && this.selectStart) {
|
|
3288
|
+
const p1 = this.getSvgPoint(event, true);
|
|
3289
|
+
this.selectEnd = p1;
|
|
3290
|
+
this.updateSelection();
|
|
3291
|
+
return;
|
|
3292
|
+
}
|
|
3293
|
+
// 2️⃣ đang kéo nhiều node
|
|
3294
|
+
if (this.draggingGroup && this.dragStartMouse) {
|
|
3295
|
+
let dx = p.x - this.dragStartMouse.x;
|
|
3296
|
+
let dy = p.y - this.dragStartMouse.y;
|
|
3297
|
+
// ⭐ xác định trục lock
|
|
3298
|
+
if (event.shiftKey && !this.axisLock) {
|
|
3299
|
+
this.axisLock = Math.abs(dx) > Math.abs(dy) ? 'x' : 'y';
|
|
3300
|
+
}
|
|
3301
|
+
if (event.shiftKey && this.axisLock === 'x') {
|
|
3302
|
+
dy = 0;
|
|
3303
|
+
}
|
|
3304
|
+
if (event.shiftKey && this.axisLock === 'y') {
|
|
3305
|
+
dx = 0;
|
|
3306
|
+
}
|
|
3307
|
+
const snap = this.snapPoint({
|
|
3308
|
+
x: this.dragStartMouse.x + dx,
|
|
3309
|
+
y: this.dragStartMouse.y + dy,
|
|
3310
|
+
}, event);
|
|
3311
|
+
const sdx = snap.x - this.dragStartMouse.x;
|
|
3312
|
+
const sdy = snap.y - this.dragStartMouse.y;
|
|
3313
|
+
// for (const n of this.lstStage) {
|
|
3314
|
+
// if (!this.lstSelectedStageCode.has(n.Code)) continue;
|
|
3315
|
+
// const start = this.dragStartPositions.get(n.Code)!;
|
|
3316
|
+
// n.x = start.x + sdx;
|
|
3317
|
+
// n.y = start.y + sdy;
|
|
3318
|
+
// }
|
|
3319
|
+
const nodeW = this.NODE_WIDTH;
|
|
3320
|
+
const nodeH = this.NODE_HEIGHT;
|
|
3321
|
+
// tìm biên group
|
|
3322
|
+
let minX = Infinity;
|
|
3323
|
+
let minY = Infinity;
|
|
3324
|
+
let maxX = -Infinity;
|
|
3325
|
+
let maxY = -Infinity;
|
|
3326
|
+
for (const n of this.lstStage) {
|
|
3327
|
+
if (!this.lstSelectedStageCode.has(n.Code))
|
|
3328
|
+
continue;
|
|
3329
|
+
const start = this.dragStartPositions.get(n.Code);
|
|
3330
|
+
minX = Math.min(minX, start.x);
|
|
3331
|
+
minY = Math.min(minY, start.y);
|
|
3332
|
+
maxX = Math.max(maxX, start.x + nodeW);
|
|
3333
|
+
maxY = Math.max(maxY, start.y + nodeH);
|
|
3334
|
+
}
|
|
3335
|
+
// clamp delta
|
|
3336
|
+
const clampedDx = this.clamp(sdx, -minX, this.CANVAS_WIDTH - maxX);
|
|
3337
|
+
const clampedDy = this.clamp(sdy, -minY, this.CANVAS_HEIGHT - maxY);
|
|
3338
|
+
// apply
|
|
3339
|
+
for (const n of this.lstStage) {
|
|
3340
|
+
if (!this.lstSelectedStageCode.has(n.Code))
|
|
3341
|
+
continue;
|
|
3342
|
+
const start = this.dragStartPositions.get(n.Code);
|
|
3343
|
+
n.x = start.x + clampedDx;
|
|
3344
|
+
n.y = start.y + clampedDy;
|
|
3345
|
+
}
|
|
3346
|
+
return;
|
|
3347
|
+
}
|
|
3348
|
+
// 4️⃣ đang kéo 1 node
|
|
3349
|
+
if (this.draggingNode) {
|
|
3350
|
+
const dx = p.x;
|
|
3351
|
+
const dy = p.y;
|
|
3352
|
+
// ⭐ xác định trục khi giữ Shift
|
|
3353
|
+
if (event.shiftKey && !this.axisLock) {
|
|
3354
|
+
if (Math.abs(dx) > Math.abs(dy)) {
|
|
3355
|
+
this.axisLock = 'x';
|
|
3356
|
+
}
|
|
3357
|
+
else {
|
|
3358
|
+
this.axisLock = 'y';
|
|
3359
|
+
}
|
|
3360
|
+
}
|
|
3361
|
+
let rawX = p.x - this.offsetX;
|
|
3362
|
+
let rawY = p.y - this.offsetY;
|
|
3363
|
+
// ⭐ khoá trục
|
|
3364
|
+
if (event.shiftKey && this.axisLock === 'x') {
|
|
3365
|
+
rawY = this.draggingNode.y;
|
|
3366
|
+
}
|
|
3367
|
+
if (event.shiftKey && this.axisLock === 'y') {
|
|
3368
|
+
rawX = this.draggingNode.x;
|
|
3369
|
+
}
|
|
3370
|
+
const snap = this.snapPoint({ x: rawX, y: rawY }, event);
|
|
3371
|
+
// this.draggingNode.x = snap.x;
|
|
3372
|
+
// this.draggingNode.y = snap.y;
|
|
3373
|
+
const nodeW = this.NODE_WIDTH;
|
|
3374
|
+
const nodeH = this.NODE_HEIGHT;
|
|
3375
|
+
this.draggingNode.x = this.clamp(snap.x, 0, this.CANVAS_WIDTH - nodeW);
|
|
3376
|
+
this.draggingNode.y = this.clamp(snap.y, 0, this.CANVAS_HEIGHT - nodeH);
|
|
3377
|
+
return;
|
|
3378
|
+
}
|
|
3379
|
+
// 3️⃣ đang kéo waypoint
|
|
3380
|
+
if (this.draggingPoint) {
|
|
3381
|
+
const p1 = this.getSvgPoint(event, true);
|
|
3382
|
+
const snap = this.snapPoint(p1, event);
|
|
3383
|
+
// this.draggingPoint.x = snap.x;
|
|
3384
|
+
// this.draggingPoint.y = snap.y;
|
|
3385
|
+
this.draggingPoint.x = this.clamp(snap.x, 0, this.CANVAS_WIDTH);
|
|
3386
|
+
this.draggingPoint.y = this.clamp(snap.y, 0, this.CANVAS_HEIGHT);
|
|
3387
|
+
return;
|
|
3388
|
+
}
|
|
3389
|
+
// 5️⃣ đang kéo connection preview
|
|
3390
|
+
if (this.connectingFrom) {
|
|
3391
|
+
const p = this.getSvgPoint(event, true);
|
|
3392
|
+
this.previewX = p.x;
|
|
3393
|
+
this.previewY = p.y;
|
|
3394
|
+
}
|
|
3395
|
+
}
|
|
3396
|
+
onMouseUpCanvas(event) {
|
|
3397
|
+
this.isSelecting = false;
|
|
3398
|
+
this.draggingGroup = false;
|
|
3399
|
+
this.draggingNode = undefined;
|
|
3400
|
+
this.draggingPoint = undefined;
|
|
3401
|
+
this.isPanning = false;
|
|
3402
|
+
setTimeout(() => (this.isDragging = false));
|
|
3403
|
+
}
|
|
3404
|
+
onClickCanvas(e) {
|
|
3405
|
+
console.log('onClickCanvas', e.offsetX, e.offsetY);
|
|
3406
|
+
if (!this.connectingFrom)
|
|
3407
|
+
return;
|
|
3408
|
+
this.connectingPoints.push({
|
|
3409
|
+
x: e.offsetX,
|
|
3410
|
+
y: e.offsetY,
|
|
3411
|
+
});
|
|
3412
|
+
console.log(this.connectingPoints);
|
|
3413
|
+
}
|
|
3414
|
+
//#endregion
|
|
3415
|
+
//#region NODE action
|
|
3416
|
+
onMouseDownNode(event, node) {
|
|
3417
|
+
console.log('onMouseDownNode', node);
|
|
3418
|
+
event.stopPropagation();
|
|
3419
|
+
event.preventDefault();
|
|
3420
|
+
this.axisLock = null;
|
|
3421
|
+
const p = this.getSvgPoint(event);
|
|
3422
|
+
// CASE 1️⃣: node nằm trong selection → kéo cả group
|
|
3423
|
+
if (this.lstSelectedStageCode.has(node.Code)) {
|
|
3424
|
+
/* =============================
|
|
3425
|
+
* UNDO SNAPSHOT (GROUP)
|
|
3426
|
+
* ============================= */
|
|
3427
|
+
this.pushUndo({
|
|
3428
|
+
type: 'move-nodes',
|
|
3429
|
+
before: this.cloneNodePositions(this.lstSelectedStageCode),
|
|
3430
|
+
});
|
|
3431
|
+
this.draggingGroup = true;
|
|
3432
|
+
this.dragStartMouse = p;
|
|
3433
|
+
this.dragStartPositions.clear();
|
|
3434
|
+
for (const n of this.lstStage) {
|
|
3435
|
+
if (this.lstSelectedStageCode.has(n.Code)) {
|
|
3436
|
+
this.dragStartPositions.set(n.Code, { x: n.x, y: n.y });
|
|
3437
|
+
}
|
|
3438
|
+
}
|
|
3439
|
+
return;
|
|
3440
|
+
}
|
|
3441
|
+
// CASE 2️⃣: node chưa được chọn → kéo node đơn
|
|
3442
|
+
this.lstSelectedStageCode.clear();
|
|
3443
|
+
// this.selectedNodeIds.add(node.id);
|
|
3444
|
+
/* =============================
|
|
3445
|
+
* UNDO SNAPSHOT (1 NODE)
|
|
3446
|
+
* ============================= */
|
|
3447
|
+
this.pushUndo({
|
|
3448
|
+
type: 'move-nodes',
|
|
3449
|
+
before: this.cloneNodePositions(new Set([node.Code])),
|
|
3450
|
+
});
|
|
3451
|
+
this.draggingNode = node;
|
|
3452
|
+
this.dragStartX = event.clientX;
|
|
3453
|
+
this.dragStartY = event.clientY;
|
|
3454
|
+
this.offsetX = event.clientX - node.x;
|
|
3455
|
+
this.offsetY = event.clientY - node.y;
|
|
3456
|
+
this.isDragging = false;
|
|
3457
|
+
}
|
|
3458
|
+
onClickStage(node) {
|
|
3459
|
+
console.log('onClickStage', node);
|
|
3460
|
+
this.hoverStage = undefined;
|
|
3461
|
+
if (this.isDragging)
|
|
3462
|
+
return;
|
|
3463
|
+
if (this.connectingFrom) {
|
|
3464
|
+
if (this.connectingFrom.node.Code !== node.Code) {
|
|
3465
|
+
const existed = this.lstAction.some((e) => e.FromStage === this.connectingFrom.node.Code && e.ToStage === node.Code);
|
|
3466
|
+
if (!existed) {
|
|
3467
|
+
const newAction = new WorkflowAction();
|
|
3468
|
+
newAction.FromStage = this.connectingFrom.node.Code;
|
|
3469
|
+
newAction.FromSide = this.connectingFrom.side;
|
|
3470
|
+
newAction.ToStage = node.Code;
|
|
3471
|
+
newAction.ToSide = 'left';
|
|
3472
|
+
// ⭐ NẾU CÓ WAYPOINT → MANUAL ROUTE
|
|
3473
|
+
if (this.connectingPoints.length > 0) {
|
|
3474
|
+
newAction.points = [...this.connectingPoints];
|
|
3475
|
+
}
|
|
3476
|
+
this.lstAction.push(newAction);
|
|
3477
|
+
const stage = this.lstStage.find((x) => x.Code == newAction.FromStage);
|
|
3478
|
+
if (stage) {
|
|
3479
|
+
stage.lstAction = stage.lstAction ? [...stage.lstAction, newAction] : [newAction];
|
|
3480
|
+
}
|
|
3481
|
+
}
|
|
3482
|
+
}
|
|
3483
|
+
this.cancelConnecting();
|
|
3484
|
+
return;
|
|
3485
|
+
}
|
|
3486
|
+
if (this.isDragging)
|
|
3487
|
+
return;
|
|
3488
|
+
if (this.lstSelectedStageCode.size)
|
|
3489
|
+
return;
|
|
3490
|
+
// if (node.id == 'start' || node.id == 'end') return;
|
|
3491
|
+
this.selectedStage = node;
|
|
3492
|
+
console.log(this.selectedStage);
|
|
3493
|
+
}
|
|
3494
|
+
//#endregion
|
|
3495
|
+
//#region EDGE action
|
|
3496
|
+
onMouseEnterEdge(e) {
|
|
3497
|
+
// console.log('onMouseEnterEdge', e);
|
|
3498
|
+
if (this.connectingFrom)
|
|
3499
|
+
return;
|
|
3500
|
+
this.hoverEdge = e;
|
|
3501
|
+
this.connectingPoints = e.points || [];
|
|
3502
|
+
}
|
|
3503
|
+
onMouseLeaveEdge(e) {
|
|
3504
|
+
// console.log('onEdgeLeave', e);
|
|
3505
|
+
if (this.connectingFrom)
|
|
3506
|
+
return;
|
|
3507
|
+
this.hoverEdge = undefined;
|
|
3508
|
+
this.connectingPoints = [];
|
|
3509
|
+
}
|
|
3510
|
+
edgeClickTimer = null;
|
|
3511
|
+
CLICK_DELAY = 250;
|
|
3512
|
+
onClickEdge(edge, event, doAction = false) {
|
|
3513
|
+
event.stopPropagation();
|
|
3514
|
+
event.preventDefault();
|
|
3515
|
+
if (this.edgeClickTimer) {
|
|
3516
|
+
// 👉 DBL CLICK
|
|
3517
|
+
clearTimeout(this.edgeClickTimer);
|
|
3518
|
+
this.edgeClickTimer = null;
|
|
3519
|
+
this.onDoubleClickEdge(event, edge);
|
|
3520
|
+
return;
|
|
3521
|
+
}
|
|
3522
|
+
if (doAction) {
|
|
3523
|
+
this.selectedAction = edge;
|
|
3524
|
+
this.selectedBackAction = this.lstAction.find((x) => x.isBackAction == true &&
|
|
3525
|
+
x.FromStage == this.selectedAction?.ToStage &&
|
|
3526
|
+
x.ToStage == this.selectedAction?.FromStage);
|
|
3527
|
+
}
|
|
3528
|
+
else {
|
|
3529
|
+
// 👉 SINGLE CLICK (chờ xem có dbl không)
|
|
3530
|
+
this.edgeClickTimer = setTimeout(() => {
|
|
3531
|
+
this.edgeClickTimer = null;
|
|
3532
|
+
console.log('onClickEdge', edge);
|
|
3533
|
+
this.onClickEdge(edge, event, true);
|
|
3534
|
+
}, this.CLICK_DELAY);
|
|
3535
|
+
}
|
|
3536
|
+
}
|
|
3537
|
+
onDoubleClickEdge(event, edge) {
|
|
3538
|
+
console.log('onDoubleClickEdge', edge);
|
|
3539
|
+
event.stopPropagation();
|
|
3540
|
+
event.preventDefault();
|
|
3541
|
+
const p = this.getSvgPoint(event, true);
|
|
3542
|
+
// Nếu edge chưa có manual points → convert auto → manual
|
|
3543
|
+
if (!edge.points || edge.points.length === 0) {
|
|
3544
|
+
edge.points = [];
|
|
3545
|
+
}
|
|
3546
|
+
const insertIndex = this.findInsertIndex(edge, p);
|
|
3547
|
+
edge.points.splice(insertIndex, 0, {
|
|
3548
|
+
x: p.x,
|
|
3549
|
+
y: p.y,
|
|
3550
|
+
});
|
|
3551
|
+
}
|
|
3552
|
+
//#endregion
|
|
3553
|
+
//#region Connector action
|
|
3554
|
+
onClickConnector(event, node, side) {
|
|
3555
|
+
console.log('onClickConnector', side, node);
|
|
3556
|
+
event.stopPropagation();
|
|
3557
|
+
event.preventDefault();
|
|
3558
|
+
const p = this.getSvgPoint(event, true);
|
|
3559
|
+
this.previewX = p.x;
|
|
3560
|
+
this.previewY = p.y;
|
|
3561
|
+
/* =============================
|
|
3562
|
+
* CLICK OUT (RIGHT) → BẮT ĐẦU
|
|
3563
|
+
* ============================= */
|
|
3564
|
+
if (side === 'right') {
|
|
3565
|
+
this.connectingFrom = {
|
|
3566
|
+
node: node,
|
|
3567
|
+
side: side,
|
|
3568
|
+
};
|
|
3569
|
+
this.connectingPoints = []; // ⭐ reset waypoint
|
|
3570
|
+
return;
|
|
3571
|
+
}
|
|
3572
|
+
/* =============================
|
|
3573
|
+
* CLICK IN (LEFT) → KẾT THÚC
|
|
3574
|
+
* ============================= */
|
|
3575
|
+
if (side === 'left' && this.connectingFrom) {
|
|
3576
|
+
if (this.connectingFrom.node.Code !== node.Code) {
|
|
3577
|
+
const existed = this.lstAction.some((e) => e.FromStage === this.connectingFrom.node.Code && e.ToStage === node.Code);
|
|
3578
|
+
if (!existed) {
|
|
3579
|
+
const newAction = new WorkflowAction();
|
|
3580
|
+
newAction.FromStage = this.connectingFrom.node.Code;
|
|
3581
|
+
newAction.FromSide = this.connectingFrom.side;
|
|
3582
|
+
newAction.ToStage = node.Code;
|
|
3583
|
+
newAction.ToSide = 'left';
|
|
3584
|
+
// ⭐ NẾU CÓ WAYPOINT → MANUAL ROUTE
|
|
3585
|
+
if (this.connectingPoints.length > 0) {
|
|
3586
|
+
newAction.points = [...this.connectingPoints];
|
|
3587
|
+
}
|
|
3588
|
+
this.lstAction.push(newAction);
|
|
3589
|
+
const stage = this.lstStage.find((x) => x.Code == newAction.FromStage);
|
|
3590
|
+
if (stage) {
|
|
3591
|
+
stage.lstAction = stage.lstAction ? [...stage.lstAction, newAction] : [newAction];
|
|
3592
|
+
}
|
|
3593
|
+
console.log(newAction);
|
|
3594
|
+
console.log(stage);
|
|
3595
|
+
}
|
|
3596
|
+
}
|
|
3597
|
+
this.cancelConnecting();
|
|
3598
|
+
}
|
|
3599
|
+
}
|
|
3600
|
+
//#endregion
|
|
3601
|
+
//#region Point action
|
|
3602
|
+
onmousedownPoint(event, p) {
|
|
3603
|
+
console.log('onmousedownPoint', p);
|
|
3604
|
+
event.stopPropagation();
|
|
3605
|
+
event.preventDefault();
|
|
3606
|
+
if (!this.draggingPoint) {
|
|
3607
|
+
this.draggingPoint = p;
|
|
3608
|
+
}
|
|
3609
|
+
else {
|
|
3610
|
+
this.draggingPoint = undefined;
|
|
3611
|
+
}
|
|
3612
|
+
}
|
|
3613
|
+
onRightClickPoint(event, edge, index) {
|
|
3614
|
+
console.log('onRightClickPoint', edge, index);
|
|
3615
|
+
event.preventDefault();
|
|
3616
|
+
event.stopPropagation();
|
|
3617
|
+
if (!edge.points)
|
|
3618
|
+
return;
|
|
3619
|
+
edge.points.splice(index, 1);
|
|
3620
|
+
// Nếu xoá hết → quay về auto route
|
|
3621
|
+
if (edge.points.length === 0) {
|
|
3622
|
+
delete edge.points;
|
|
3623
|
+
}
|
|
3624
|
+
}
|
|
3625
|
+
//#endregion
|
|
3626
|
+
//#region UNDO REDU
|
|
3627
|
+
undoStack = [];
|
|
3628
|
+
redoStack = [];
|
|
3629
|
+
cloneNodePositions(nodeIds) {
|
|
3630
|
+
const map = new Map();
|
|
3631
|
+
for (const n of this.lstStage) {
|
|
3632
|
+
if (nodeIds.has(n.Code)) {
|
|
3633
|
+
map.set(n.Code, { x: n.x, y: n.y });
|
|
3634
|
+
}
|
|
3635
|
+
}
|
|
3636
|
+
return map;
|
|
3637
|
+
}
|
|
3638
|
+
pushAlignUndo() {
|
|
3639
|
+
this.pushUndo({
|
|
3640
|
+
type: 'move-nodes',
|
|
3641
|
+
before: this.cloneNodePositions(this.lstSelectedStageCode),
|
|
3642
|
+
});
|
|
3643
|
+
}
|
|
3644
|
+
pushUndo(action) {
|
|
3645
|
+
this.undoStack.push(action);
|
|
3646
|
+
this.redoStack = [];
|
|
3647
|
+
}
|
|
3648
|
+
undo() {
|
|
3649
|
+
const action = this.undoStack.pop();
|
|
3650
|
+
if (!action)
|
|
3651
|
+
return;
|
|
3652
|
+
switch (action.type) {
|
|
3653
|
+
case 'move-nodes':
|
|
3654
|
+
this.undoMoveNodes(action);
|
|
3655
|
+
break;
|
|
3656
|
+
case 'delete-node': {
|
|
3657
|
+
// 🔙 restore node
|
|
3658
|
+
this.lstStage.splice(action.nodeIndex, 0, action.node);
|
|
3659
|
+
// 🔙 restore edges
|
|
3660
|
+
this.lstAction.push(...action.edges);
|
|
3661
|
+
// redo snapshot
|
|
3662
|
+
this.redoStack.push(action);
|
|
3663
|
+
break;
|
|
3664
|
+
}
|
|
3665
|
+
}
|
|
3666
|
+
}
|
|
3667
|
+
undoMoveNodes(action) {
|
|
3668
|
+
const after = this.cloneNodePositions(new Set(action.before.keys()));
|
|
3669
|
+
this.redoStack.push({
|
|
3670
|
+
type: 'move-nodes',
|
|
3671
|
+
before: after,
|
|
3672
|
+
});
|
|
3673
|
+
// restore
|
|
3674
|
+
action.before.forEach((pos, id) => {
|
|
3675
|
+
const node = this.lstStage.find((n) => n.Code === id);
|
|
3676
|
+
if (node) {
|
|
3677
|
+
node.x = pos.x;
|
|
3678
|
+
node.y = pos.y;
|
|
3679
|
+
}
|
|
3680
|
+
});
|
|
3681
|
+
}
|
|
3682
|
+
redo() {
|
|
3683
|
+
const action = this.redoStack.pop();
|
|
3684
|
+
if (!action)
|
|
3685
|
+
return;
|
|
3686
|
+
switch (action.type) {
|
|
3687
|
+
case 'move-nodes':
|
|
3688
|
+
this.redoMoveNodes(action);
|
|
3689
|
+
break;
|
|
3690
|
+
case 'delete-node': {
|
|
3691
|
+
const nodeId = action.node.id;
|
|
3692
|
+
// redo = xoá lại
|
|
3693
|
+
this.lstAction = this.lstAction.filter((e) => e.FromStage !== nodeId && e.ToStage !== nodeId);
|
|
3694
|
+
const idx = this.lstStage.findIndex((n) => n.Code === nodeId);
|
|
3695
|
+
if (idx >= 0)
|
|
3696
|
+
this.lstStage.splice(idx, 1);
|
|
3697
|
+
this.undoStack.push(action);
|
|
3698
|
+
break;
|
|
3699
|
+
}
|
|
3700
|
+
}
|
|
3701
|
+
}
|
|
3702
|
+
redoMoveNodes(action) {
|
|
3703
|
+
const after = this.cloneNodePositions(new Set(action.before.keys()));
|
|
3704
|
+
this.undoStack.push({
|
|
3705
|
+
type: 'move-nodes',
|
|
3706
|
+
before: after,
|
|
3707
|
+
});
|
|
3708
|
+
action.before.forEach((pos, id) => {
|
|
3709
|
+
const node = this.lstStage.find((n) => n.Code === id);
|
|
3710
|
+
if (node) {
|
|
3711
|
+
node.x = pos.x;
|
|
3712
|
+
node.y = pos.y;
|
|
3713
|
+
}
|
|
3714
|
+
});
|
|
3715
|
+
}
|
|
3716
|
+
//#endregion
|
|
3717
|
+
CANVAS_WIDTH = 10000;
|
|
3718
|
+
CANVAS_HEIGHT = 10000;
|
|
3719
|
+
canvasWidth = 0;
|
|
3720
|
+
canvasHeight = 0;
|
|
3721
|
+
NODE_WIDTH = 160;
|
|
3722
|
+
NODE_HEIGHT = 80;
|
|
3723
|
+
//#region ALIGN
|
|
3724
|
+
getSelectedNodes() {
|
|
3725
|
+
return this.lstStage.filter((n) => this.lstSelectedStageCode.has(n.Code));
|
|
3726
|
+
}
|
|
3727
|
+
alignLeft() {
|
|
3728
|
+
if (this.lstSelectedStageCode.size < 2)
|
|
3729
|
+
return;
|
|
3730
|
+
this.pushAlignUndo();
|
|
3731
|
+
const nodes = this.getSelectedNodes();
|
|
3732
|
+
const minX = Math.min(...nodes.map((n) => n.x));
|
|
3733
|
+
nodes.forEach((n) => (n.x = minX));
|
|
3734
|
+
}
|
|
3735
|
+
alignCenter() {
|
|
3736
|
+
if (this.lstSelectedStageCode.size < 2)
|
|
3737
|
+
return;
|
|
3738
|
+
this.pushAlignUndo();
|
|
3739
|
+
const nodes = this.getSelectedNodes();
|
|
3740
|
+
const centers = nodes.map((n) => n.x + this.NODE_WIDTH / 2);
|
|
3741
|
+
const centerX = centers.reduce((a, b) => a + b) / centers.length;
|
|
3742
|
+
nodes.forEach((n) => {
|
|
3743
|
+
n.x = centerX - this.NODE_WIDTH / 2;
|
|
3744
|
+
});
|
|
3745
|
+
}
|
|
3746
|
+
alignRight() {
|
|
3747
|
+
if (this.lstSelectedStageCode.size < 2)
|
|
3748
|
+
return;
|
|
3749
|
+
this.pushAlignUndo();
|
|
3750
|
+
const nodes = this.getSelectedNodes();
|
|
3751
|
+
const maxX = Math.max(...nodes.map((n) => n.x + this.NODE_WIDTH));
|
|
3752
|
+
nodes.forEach((n) => (n.x = maxX - this.NODE_WIDTH));
|
|
3753
|
+
}
|
|
3754
|
+
alignTop() {
|
|
3755
|
+
if (this.lstSelectedStageCode.size < 2)
|
|
3756
|
+
return;
|
|
3757
|
+
this.pushAlignUndo();
|
|
3758
|
+
const nodes = this.getSelectedNodes();
|
|
3759
|
+
const minY = Math.min(...nodes.map((n) => n.y));
|
|
3760
|
+
nodes.forEach((n) => (n.y = minY));
|
|
3761
|
+
}
|
|
3762
|
+
alignMiddle() {
|
|
3763
|
+
if (this.lstSelectedStageCode.size < 2)
|
|
3764
|
+
return;
|
|
3765
|
+
this.pushAlignUndo();
|
|
3766
|
+
const nodes = this.getSelectedNodes();
|
|
3767
|
+
const centers = nodes.map((n) => n.y + this.NODE_HEIGHT / 2);
|
|
3768
|
+
const centerY = centers.reduce((a, b) => a + b) / centers.length;
|
|
3769
|
+
nodes.forEach((n) => {
|
|
3770
|
+
n.y = centerY - this.NODE_HEIGHT / 2;
|
|
3771
|
+
});
|
|
3772
|
+
}
|
|
3773
|
+
alignBottom() {
|
|
3774
|
+
if (this.lstSelectedStageCode.size < 2)
|
|
3775
|
+
return;
|
|
3776
|
+
this.pushAlignUndo();
|
|
3777
|
+
const nodes = this.getSelectedNodes();
|
|
3778
|
+
const maxY = Math.max(...nodes.map((n) => n.y + this.NODE_HEIGHT));
|
|
3779
|
+
nodes.forEach((n) => (n.y = maxY - this.NODE_HEIGHT));
|
|
3780
|
+
}
|
|
3781
|
+
distributeHorizontal() {
|
|
3782
|
+
if (this.lstSelectedStageCode.size < 3)
|
|
3783
|
+
return;
|
|
3784
|
+
this.pushAlignUndo();
|
|
3785
|
+
const nodes = this.getSelectedNodes().sort((a, b) => a.x - b.x);
|
|
3786
|
+
const first = nodes[0];
|
|
3787
|
+
const last = nodes[nodes.length - 1];
|
|
3788
|
+
const totalWidth = nodes.reduce((s, n) => s + this.NODE_WIDTH, 0);
|
|
3789
|
+
const space = (last.x - first.x - totalWidth + this.NODE_WIDTH) / (nodes.length - 1);
|
|
3790
|
+
let x = first.x;
|
|
3791
|
+
nodes.forEach((n, i) => {
|
|
3792
|
+
if (i === 0 || i === nodes.length - 1) {
|
|
3793
|
+
x = n.x + this.NODE_WIDTH + space;
|
|
3794
|
+
return;
|
|
3795
|
+
}
|
|
3796
|
+
n.x = x;
|
|
3797
|
+
x += this.NODE_WIDTH + space;
|
|
3798
|
+
});
|
|
3799
|
+
}
|
|
3800
|
+
distributeVertical() {
|
|
3801
|
+
if (this.lstSelectedStageCode.size < 3)
|
|
3802
|
+
return;
|
|
3803
|
+
this.pushAlignUndo();
|
|
3804
|
+
const nodes = this.getSelectedNodes().sort((a, b) => a.y - b.y);
|
|
3805
|
+
const first = nodes[0];
|
|
3806
|
+
const last = nodes[nodes.length - 1];
|
|
3807
|
+
const totalHeight = nodes.reduce((s, n) => s + this.NODE_HEIGHT, 0);
|
|
3808
|
+
const space = (last.y - first.y - totalHeight + this.NODE_HEIGHT) / (nodes.length - 1);
|
|
3809
|
+
let y = first.y;
|
|
3810
|
+
nodes.forEach((n, i) => {
|
|
3811
|
+
if (i === 0 || i === nodes.length - 1) {
|
|
3812
|
+
y = n.y + this.NODE_HEIGHT + space;
|
|
3813
|
+
return;
|
|
3814
|
+
}
|
|
3815
|
+
n.y = y;
|
|
3816
|
+
y += this.NODE_HEIGHT + space;
|
|
3817
|
+
});
|
|
3818
|
+
}
|
|
3819
|
+
//#endregion
|
|
3820
|
+
//#region zoom
|
|
3821
|
+
zoom = 1;
|
|
3822
|
+
minZoom = 0.5;
|
|
3823
|
+
maxZoom = 2;
|
|
3824
|
+
zoomStep = 0.1;
|
|
3825
|
+
get svgTransform() {
|
|
3826
|
+
const cx = this.getCanvasWidth() / 2;
|
|
3827
|
+
const cy = this.getCanvasHeight() / 2;
|
|
3828
|
+
return `
|
|
3829
|
+
translate(${cx} ${cy})
|
|
3830
|
+
scale(${this.zoom})
|
|
3831
|
+
translate(${-cx} ${-cy})
|
|
3832
|
+
`;
|
|
3833
|
+
}
|
|
3834
|
+
zoomIn() {
|
|
3835
|
+
this.zoom = Math.min(this.zoom + this.zoomStep, this.maxZoom);
|
|
3836
|
+
}
|
|
3837
|
+
zoomOut() {
|
|
3838
|
+
this.zoom = Math.max(this.zoom - this.zoomStep, this.minZoom);
|
|
3839
|
+
}
|
|
3840
|
+
//#endregion
|
|
3841
|
+
//#region HELPER
|
|
3842
|
+
updateCanvasSize() {
|
|
3843
|
+
const rect = this.canvasRef?.nativeElement.getBoundingClientRect();
|
|
3844
|
+
this.canvasWidth = rect?.width;
|
|
3845
|
+
this.canvasHeight = rect?.height;
|
|
3846
|
+
}
|
|
3847
|
+
clamp(value, min, max) {
|
|
3848
|
+
return Math.max(min, Math.min(max, value));
|
|
3849
|
+
}
|
|
3850
|
+
findInsertIndex(edge, p) {
|
|
3851
|
+
const points = this.getEdgeFullPoints(edge);
|
|
3852
|
+
if (!points)
|
|
3853
|
+
return 0;
|
|
3854
|
+
let minDist = Infinity;
|
|
3855
|
+
let index = 0;
|
|
3856
|
+
for (let i = 0; i < points.length - 1; i++) {
|
|
3857
|
+
const d = this.pointToSegmentDistance(p, points[i], points[i + 1]);
|
|
3858
|
+
if (d < minDist) {
|
|
3859
|
+
minDist = d;
|
|
3860
|
+
index = i;
|
|
3861
|
+
}
|
|
3862
|
+
}
|
|
3863
|
+
// index tương ứng với vị trí trong edge.points
|
|
3864
|
+
return Math.max(0, index);
|
|
3865
|
+
}
|
|
3866
|
+
getEdgeFullPoints(edge) {
|
|
3867
|
+
if (!edge.FromStage || !edge.ToStage)
|
|
3868
|
+
return null;
|
|
3869
|
+
const from = this.getNode(edge.FromStage);
|
|
3870
|
+
const to = this.getNode(edge.ToStage);
|
|
3871
|
+
return [{ x: from.x + 160, y: from.y + 40 }, ...(edge.points || []), { x: to.x, y: to.y + 40 }];
|
|
3872
|
+
}
|
|
3873
|
+
pointToSegmentDistance(p, a, b) {
|
|
3874
|
+
const dx = b.x - a.x;
|
|
3875
|
+
const dy = b.y - a.y;
|
|
3876
|
+
if (dx === 0 && dy === 0) {
|
|
3877
|
+
return Math.hypot(p.x - a.x, p.y - a.y);
|
|
3878
|
+
}
|
|
3879
|
+
const t = ((p.x - a.x) * dx + (p.y - a.y) * dy) / (dx * dx + dy * dy);
|
|
3880
|
+
const clamped = Math.max(0, Math.min(1, t));
|
|
3881
|
+
const proj = {
|
|
3882
|
+
x: a.x + clamped * dx,
|
|
3883
|
+
y: a.y + clamped * dy,
|
|
3884
|
+
};
|
|
3885
|
+
return Math.hypot(p.x - proj.x, p.y - proj.y);
|
|
3886
|
+
}
|
|
3887
|
+
isPositionOccupied(x, y) {
|
|
3888
|
+
return this.lstStage.some((n) => Math.abs(n.x - x) < 160 && Math.abs(n.y - y) < 80);
|
|
3889
|
+
}
|
|
3890
|
+
getCanvasWidth() {
|
|
3891
|
+
// return 3000;
|
|
3892
|
+
const PADDING = 200;
|
|
3893
|
+
const MIN_WIDTH = 800;
|
|
3894
|
+
const maxX = Math.max(...this.lstStage.map((n) => (n.StageType == 'END' ? n.x : n.x + 160)), // 160 = node width
|
|
3895
|
+
MIN_WIDTH);
|
|
3896
|
+
return maxX + PADDING;
|
|
3897
|
+
}
|
|
3898
|
+
getCanvasHeight() {
|
|
3899
|
+
// return 2000;
|
|
3900
|
+
const PADDING = 100;
|
|
3901
|
+
const maxY = Math.max(...this.lstStage.map((n) => n.y + 80), 400);
|
|
3902
|
+
return maxY + PADDING;
|
|
3903
|
+
}
|
|
3904
|
+
/* =============================
|
|
3905
|
+
* CONVERT MOUSE → SVG COORD
|
|
3906
|
+
* ============================= */
|
|
3907
|
+
getSvgPoint(event, preview = false) {
|
|
3908
|
+
const svg = this.svgRef.nativeElement;
|
|
3909
|
+
const pt = svg.createSVGPoint();
|
|
3910
|
+
pt.x = event.clientX;
|
|
3911
|
+
pt.y = event.clientY;
|
|
3912
|
+
const result = pt.matrixTransform(svg.getScreenCTM().inverse());
|
|
3913
|
+
if (preview) {
|
|
3914
|
+
return { x: result.x, y: result.y };
|
|
3915
|
+
}
|
|
3916
|
+
else {
|
|
3917
|
+
return { x: event.clientX, y: event.clientY };
|
|
3918
|
+
}
|
|
3919
|
+
}
|
|
3920
|
+
updateSelection() {
|
|
3921
|
+
if (!this.selectStart || !this.selectEnd)
|
|
3922
|
+
return;
|
|
3923
|
+
const x1 = Math.min(this.selectStart.x, this.selectEnd.x);
|
|
3924
|
+
const y1 = Math.min(this.selectStart.y, this.selectEnd.y);
|
|
3925
|
+
const x2 = Math.max(this.selectStart.x, this.selectEnd.x);
|
|
3926
|
+
const y2 = Math.max(this.selectStart.y, this.selectEnd.y);
|
|
3927
|
+
this.lstSelectedStageCode.clear();
|
|
3928
|
+
for (const n of this.lstStage) {
|
|
3929
|
+
const nx1 = n.x;
|
|
3930
|
+
const ny1 = n.y;
|
|
3931
|
+
const nx2 = n.x + 160;
|
|
3932
|
+
const ny2 = n.y + 80;
|
|
3933
|
+
const intersect = nx2 >= x1 && nx1 <= x2 && ny2 >= y1 && ny1 <= y2;
|
|
3934
|
+
if (intersect) {
|
|
3935
|
+
this.lstSelectedStageCode.add(n.Code);
|
|
3936
|
+
}
|
|
3937
|
+
}
|
|
3938
|
+
}
|
|
3939
|
+
snapPoint(p, event) {
|
|
3940
|
+
if (!this.snapToGrid || event?.altKey)
|
|
3941
|
+
return p;
|
|
3942
|
+
if (!this.snapToGrid)
|
|
3943
|
+
return p;
|
|
3944
|
+
return {
|
|
3945
|
+
x: this.snap(p.x),
|
|
3946
|
+
y: this.snap(p.y),
|
|
3947
|
+
};
|
|
3948
|
+
}
|
|
3949
|
+
snap(value) {
|
|
3950
|
+
return Math.round(value / this.gridSize) * this.gridSize;
|
|
3951
|
+
}
|
|
3952
|
+
getNode(id) {
|
|
3953
|
+
return this.lstStage.find((n) => n.Code === id);
|
|
3954
|
+
}
|
|
3955
|
+
//#endregion
|
|
3956
|
+
//#region BUILD PATH
|
|
3957
|
+
buildPath(edge) {
|
|
3958
|
+
if (!edge.FromStage || !edge.ToStage)
|
|
3959
|
+
return '';
|
|
3960
|
+
const from = this.getNode(edge.FromStage);
|
|
3961
|
+
const to = this.getNode(edge.ToStage);
|
|
3962
|
+
const start = {
|
|
3963
|
+
x: from.x + (from.isReverse ? 0 : 160),
|
|
3964
|
+
y: from.y + 40,
|
|
3965
|
+
};
|
|
3966
|
+
const end = {
|
|
3967
|
+
x: to.x + (to.isReverse ? 160 : 0),
|
|
3968
|
+
y: to.y + 40,
|
|
3969
|
+
};
|
|
3970
|
+
if (edge.points && edge.points.length) {
|
|
3971
|
+
return this.buildPathThroughPoints([start, ...edge.points, end]);
|
|
3972
|
+
}
|
|
3973
|
+
// return this.buildRoutedPath({ node: from, side: edge.fromSide }, to.x, to.y + 40);
|
|
3974
|
+
return this.buildStragePath(start.x, start.y, end.x, end.y);
|
|
3975
|
+
}
|
|
3976
|
+
buildPreviewPath() {
|
|
3977
|
+
if (!this.connectingFrom)
|
|
3978
|
+
return '';
|
|
3979
|
+
const start = {
|
|
3980
|
+
x: this.connectingFrom.node.x + (this.connectingFrom.node.isReverse ? 0 : 160),
|
|
3981
|
+
y: this.connectingFrom.node.y + 40,
|
|
3982
|
+
};
|
|
3983
|
+
const end = {
|
|
3984
|
+
x: this.previewX,
|
|
3985
|
+
y: this.previewY,
|
|
3986
|
+
};
|
|
3987
|
+
// ⭐ ĐÃ CÓ WAYPOINT → MANUAL PREVIEW
|
|
3988
|
+
if (this.connectingPoints.length > 0) {
|
|
3989
|
+
return this.buildPathThroughPoints([start, ...this.connectingPoints, end], 0);
|
|
3990
|
+
}
|
|
3991
|
+
// ⭐ CHƯA CÓ WAYPOINT → AUTO PREVIEW
|
|
3992
|
+
// return this.buildRoutedPath(this.connectingFrom, end.x, end.y, 0);
|
|
3993
|
+
return this.buildStragePath(start.x, start.y, end.x, end.y, 0);
|
|
3994
|
+
}
|
|
3995
|
+
buildStragePath(fromX, fromY, toX, toY, ARROW_GAP = 12) {
|
|
3996
|
+
const x1 = fromX;
|
|
3997
|
+
const y1 = fromY;
|
|
3998
|
+
const x2 = toX;
|
|
3999
|
+
const y2 = toY;
|
|
4000
|
+
/* =============================
|
|
4001
|
+
* 1. VECTOR GỐC
|
|
4002
|
+
* ============================= */
|
|
4003
|
+
const dx = x2 - x1;
|
|
4004
|
+
const dy = y2 - y1;
|
|
4005
|
+
const len = Math.sqrt(dx * dx + dy * dy) || 1;
|
|
4006
|
+
/* =============================
|
|
4007
|
+
* 2. SHRINK START & END
|
|
4008
|
+
* ============================= */
|
|
4009
|
+
const sx = x1 + (dx / len) * ARROW_GAP;
|
|
4010
|
+
const sy = y1 + (dy / len) * ARROW_GAP;
|
|
4011
|
+
const ex = x2 - (dx / len) * ARROW_GAP;
|
|
4012
|
+
const ey = y2 - (dy / len) * ARROW_GAP;
|
|
4013
|
+
/* =============================
|
|
4014
|
+
* 3. PATH THẲNG
|
|
4015
|
+
* ============================= */
|
|
4016
|
+
return `
|
|
4017
|
+
M ${sx} ${sy}
|
|
4018
|
+
L ${ex} ${ey}
|
|
4019
|
+
`;
|
|
4020
|
+
}
|
|
4021
|
+
buildRoutedPath(from, toX, toY, ARROW_GAP = 12) {
|
|
4022
|
+
const x1 = from.node.isReverse ? from.node.x : from.node.x + 160;
|
|
4023
|
+
const y1 = from.node.y + 40;
|
|
4024
|
+
const x2 = toX;
|
|
4025
|
+
const y2 = toY;
|
|
4026
|
+
// const ARROW_GAP = 12;
|
|
4027
|
+
/* =============================
|
|
4028
|
+
* 1. VECTOR GỐC
|
|
4029
|
+
* ============================= */
|
|
4030
|
+
const dx = x2 - x1;
|
|
4031
|
+
const dy = y2 - y1;
|
|
4032
|
+
const len = Math.sqrt(dx * dx + dy * dy) || 1;
|
|
4033
|
+
const ux = dx / len;
|
|
4034
|
+
const uy = dy / len;
|
|
4035
|
+
/* =============================
|
|
4036
|
+
* 2. START / END (CÁCH CONNECTOR ĐỀU)
|
|
4037
|
+
* ============================= */
|
|
4038
|
+
let sx = x1 + ux * ARROW_GAP;
|
|
4039
|
+
let sy = y1 + uy * ARROW_GAP;
|
|
4040
|
+
let ex = x2 - ux * ARROW_GAP;
|
|
4041
|
+
let ey = y2 - uy * ARROW_GAP;
|
|
4042
|
+
/* =============================
|
|
4043
|
+
* 3. KIỂM TRA BỊ CHẮN KHÔNG
|
|
4044
|
+
* ============================= */
|
|
4045
|
+
const blocked = this.isBlockedPoint(from, x2, y2);
|
|
4046
|
+
/* =============================
|
|
4047
|
+
* 4. ĐỘ CONG CƠ BẢN
|
|
4048
|
+
* ============================= */
|
|
4049
|
+
const dist = Math.sqrt((ex - sx) * (ex - sx) + (ey - sy) * (ey - sy));
|
|
4050
|
+
const BASE_CURVE = Math.max(30, Math.min(80, dist / 2));
|
|
4051
|
+
/* =============================
|
|
4052
|
+
* 5. KHÔNG BỊ CHẮN → CONG TRỰC TIẾP
|
|
4053
|
+
* ============================= */
|
|
4054
|
+
if (!blocked) {
|
|
4055
|
+
let c1x, c1y;
|
|
4056
|
+
let c2x, c2y;
|
|
4057
|
+
if (Math.abs(dy) < Math.abs(dx)) {
|
|
4058
|
+
// thiên ngang
|
|
4059
|
+
c1x = sx + BASE_CURVE;
|
|
4060
|
+
c1y = sy;
|
|
4061
|
+
c2x = ex - BASE_CURVE;
|
|
4062
|
+
c2y = ey;
|
|
4063
|
+
}
|
|
4064
|
+
else {
|
|
4065
|
+
// thiên dọc
|
|
4066
|
+
const dir = ey > sy ? 1 : -1;
|
|
4067
|
+
c1x = sx;
|
|
4068
|
+
c1y = sy + dir * BASE_CURVE;
|
|
4069
|
+
c2x = ex;
|
|
4070
|
+
c2y = ey - dir * BASE_CURVE;
|
|
4071
|
+
}
|
|
4072
|
+
return `
|
|
4073
|
+
M ${sx} ${sy}
|
|
4074
|
+
C ${c1x} ${c1y},
|
|
4075
|
+
${c2x} ${c2y},
|
|
4076
|
+
${ex} ${ey}
|
|
4077
|
+
`;
|
|
4078
|
+
}
|
|
4079
|
+
/* =============================
|
|
4080
|
+
* 6. BỊ CHẮN → ROUTE NÉ (S-CURVE)
|
|
4081
|
+
* ============================= */
|
|
4082
|
+
const routeY = this.findBestRouteY(from, ex, ey);
|
|
4083
|
+
const midX = (sx + ex) / 2;
|
|
4084
|
+
const CURVE = 40;
|
|
4085
|
+
return `
|
|
4086
|
+
M ${sx} ${sy}
|
|
4087
|
+
C ${sx + CURVE} ${sy},
|
|
4088
|
+
${sx + CURVE} ${routeY},
|
|
4089
|
+
${midX} ${routeY}
|
|
4090
|
+
S ${ex - CURVE} ${routeY},
|
|
4091
|
+
${ex} ${ey}
|
|
4092
|
+
`;
|
|
4093
|
+
}
|
|
4094
|
+
buildPathThroughPoints(points, ARROW_GAP = 12, tension = 0.1) {
|
|
4095
|
+
if (points.length < 2)
|
|
4096
|
+
return '';
|
|
4097
|
+
const start = this.shrinkPoint(points[0], points[1], ARROW_GAP);
|
|
4098
|
+
const end = this.shrinkPoint(points[points.length - 1], points[points.length - 2], ARROW_GAP);
|
|
4099
|
+
const pts = [start, ...points.slice(1, -1), end];
|
|
4100
|
+
let d = `M ${pts[0].x} ${pts[0].y}`;
|
|
4101
|
+
for (let i = 0; i < pts.length - 1; i++) {
|
|
4102
|
+
const p0 = pts[i - 1] || pts[i];
|
|
4103
|
+
const p1 = pts[i];
|
|
4104
|
+
const p2 = pts[i + 1];
|
|
4105
|
+
const p3 = pts[i + 2] || p2;
|
|
4106
|
+
const c1x = p1.x + (p2.x - p0.x) * tension;
|
|
4107
|
+
const c1y = p1.y + (p2.y - p0.y) * tension;
|
|
4108
|
+
const c2x = p2.x - (p3.x - p1.x) * tension;
|
|
4109
|
+
const c2y = p2.y - (p3.y - p1.y) * tension;
|
|
4110
|
+
d += ` C ${c1x} ${c1y}, ${c2x} ${c2y}, ${p2.x} ${p2.y}`;
|
|
4111
|
+
}
|
|
4112
|
+
return d;
|
|
4113
|
+
}
|
|
4114
|
+
shrinkPoint(from, to, gap) {
|
|
4115
|
+
const dx = to.x - from.x;
|
|
4116
|
+
const dy = to.y - from.y;
|
|
4117
|
+
const len = Math.sqrt(dx * dx + dy * dy) || 1;
|
|
4118
|
+
return {
|
|
4119
|
+
x: from.x + (dx / len) * gap,
|
|
4120
|
+
y: from.y + (dy / len) * gap,
|
|
4121
|
+
};
|
|
4122
|
+
}
|
|
4123
|
+
isBlockedPoint(from, x2, y2) {
|
|
4124
|
+
const x1 = from.node.isReverse ? from.node.x + 160 : from.node.x;
|
|
4125
|
+
const y1 = from.node.y + 40;
|
|
4126
|
+
return this.lstStage.some((n) => {
|
|
4127
|
+
if (n.Code === from.node.Code)
|
|
4128
|
+
return false;
|
|
4129
|
+
// node phải nằm giữa theo trục X
|
|
4130
|
+
if (!this.isNodeBetweenX(x1, x2, n))
|
|
4131
|
+
return false;
|
|
4132
|
+
// line phải cắt node theo trục Y
|
|
4133
|
+
return this.doesLineCrossNodeY(y1, n);
|
|
4134
|
+
});
|
|
4135
|
+
}
|
|
4136
|
+
findBestRouteY(from, toX, toY) {
|
|
4137
|
+
const baseY = from.node.y + 40;
|
|
4138
|
+
const OFFSETS = [0, 80, -80, 160, -160, 240, -240];
|
|
4139
|
+
for (const offset of OFFSETS) {
|
|
4140
|
+
const routeY = baseY + offset;
|
|
4141
|
+
if (this.isLaneClear(from, toX, routeY)) {
|
|
4142
|
+
return routeY;
|
|
4143
|
+
}
|
|
4144
|
+
}
|
|
4145
|
+
// fallback: dùng lane xa nhất
|
|
4146
|
+
return baseY + OFFSETS[OFFSETS.length - 1];
|
|
4147
|
+
}
|
|
4148
|
+
isNodeBetweenX(fromX, toX, node) {
|
|
4149
|
+
const nx1 = node.x;
|
|
4150
|
+
const nx2 = node.x + 160;
|
|
4151
|
+
const minX = Math.min(fromX, toX);
|
|
4152
|
+
const maxX = Math.max(fromX, toX);
|
|
4153
|
+
return nx2 > minX && nx1 < maxX;
|
|
4154
|
+
}
|
|
4155
|
+
doesLineCrossNodeY(y, node, tolerance = 6) {
|
|
4156
|
+
const top = node.y;
|
|
4157
|
+
const bottom = node.y + 80;
|
|
4158
|
+
return y > top + tolerance && y < bottom - tolerance;
|
|
4159
|
+
}
|
|
4160
|
+
isLaneClear(from, toX, routeY) {
|
|
4161
|
+
const x1 = from.node.x;
|
|
4162
|
+
const y1 = from.node.y + 40;
|
|
4163
|
+
const x2 = toX;
|
|
4164
|
+
const y2 = routeY;
|
|
4165
|
+
return !this.lstStage.some((n) => {
|
|
4166
|
+
if (n.Code === from.node.Code)
|
|
4167
|
+
return false;
|
|
4168
|
+
// node phải nằm giữa theo trục X
|
|
4169
|
+
if (!this.isNodeBetweenX(x1, x2, n))
|
|
4170
|
+
return false;
|
|
4171
|
+
// lane Y cắt node
|
|
4172
|
+
return this.doesLineCrossNodeY(routeY, n);
|
|
4173
|
+
});
|
|
4174
|
+
}
|
|
4175
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.1.7", ngImport: i0, type: WorkflowEditorComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
|
|
4176
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.1.7", type: WorkflowEditorComponent, isStandalone: true, selector: "app-workflow-editor", inputs: { lstOrg: "lstOrg", lstTemplateType: "lstTemplateType", lstTemplatePrint: "lstTemplatePrint", template: "template", lstStage: "lstStage", lstAction: "lstAction" }, outputs: { onSave: "onSave" }, host: { listeners: { "window:keydown": "onKeyDown($event)" } }, viewQueries: [{ propertyName: "canvasRef", first: true, predicate: ["canvas"], descendants: true, static: true }, { propertyName: "svgRef", first: true, predicate: ["svg"], descendants: true, static: true }, { propertyName: "inputStageStatusElement", first: true, predicate: ["inputStageStatusElement"], descendants: true }, { propertyName: "inputActionTypeElement", first: true, predicate: ["inputActionTypeElement"], descendants: true }, { propertyName: "inputElement", first: true, predicate: ["inputElement"], descendants: true }], usesInheritance: true, ngImport: i0, template: "<div\n #canvasRef\n class=\"workflow-canvas\"\n [class.connecting]=\"!!connectingFrom\"\n [class.dragging-point]=\"!!draggingPoint\"\n [style.width.px]=\"CANVAS_WIDTH\"\n [style.height.px]=\"CANVAS_HEIGHT\"\n (mousedown)=\"onMouseDownCanvas($event)\"\n (mousemove)=\"onMouseMoveCanvas($event)\"\n (mouseup)=\"onMouseUpCanvas($event)\"\n (click)=\"onClickCanvas($event)\"\n>\n <!-- \n\n [style.width.px]=\"getCanvasWidth()\"\n [style.height.px]=\"getCanvasHeight()\" \n\n style=\"width: 100%; height: calc(100vh - 120px)\"\n \n -->\n\n <div class=\"toolbar\" (mousedown)=\"$event.stopPropagation()\" (click)=\"$event.stopPropagation()\">\n <button nz-button nzSize=\"small\" nz-tooltip=\"Editor setting\" (click)=\"settingVisible = true\">\n <nz-icon nzType=\"setting\"></nz-icon>\n </button>\n\n <box [width]=\"1\"></box>\n <span class=\"divider\"></span>\n <box [width]=\"1\"></box>\n\n <button nz-button nzSize=\"small\" nz-tooltip=\"Template setting\" (click)=\"drawTemplateVisibel = true\">\n <nz-icon nzType=\"setting\"></nz-icon> T\n </button>\n\n <box [width]=\"1\"></box>\n <span class=\"divider\"></span>\n <box [width]=\"1\"></box>\n\n <button nz-button nzSize=\"small\" nzDanger nz-tooltip=\"Save\" (click)=\"save()\">\n <nz-icon nzType=\"save\"></nz-icon>\n </button>\n <button nz-button nzSize=\"small\" nz-tooltip=\"Add stage\" (click)=\"addStage()\">\n <nz-icon nzType=\"plus\" class=\"color-primary\"></nz-icon>\n </button>\n\n <box [width]=\"1\"></box>\n <span class=\"divider\"></span>\n <box [width]=\"1\"></box>\n\n <button\n nz-button\n nzSize=\"small\"\n [nzType]=\"isMoveMode ? 'primary' : 'default'\"\n nz-tooltip=\"Move mode\"\n (click)=\"isMoveMode = true\"\n >\n <nz-icon><img src=\"/assets/icon/hand-palm.png\" width=\"19\" height=\"19\" /></nz-icon>\n </button>\n <button\n nz-button\n nzSize=\"small\"\n [nzType]=\"!isMoveMode ? 'primary' : 'default'\"\n nz-tooltip=\"Select mode\"\n (click)=\"isMoveMode = false\"\n >\n <nz-icon><img src=\"/assets/icon/cursor.png\" width=\"19\" height=\"19\" /></nz-icon>\n </button>\n\n <box [width]=\"1\"></box>\n <span class=\"divider\"></span>\n <box [width]=\"1\"></box>\n\n <!-- UNDO / REDO -->\n <button\n nz-button\n nzSize=\"small\"\n nzType=\"default\"\n [disabled]=\"undoStack.length === 0\"\n nz-tooltip=\"Undo\"\n (click)=\"undo()\"\n >\n <nz-icon nzType=\"undo\"></nz-icon>\n </button>\n\n <button\n nz-button\n nzSize=\"small\"\n nzType=\"default\"\n [disabled]=\"redoStack.length === 0\"\n nz-tooltip=\"Redo\"\n (click)=\"redo()\"\n >\n <nz-icon nzType=\"redo\"></nz-icon>\n </button>\n\n <box [width]=\"1\"></box>\n <span class=\"divider\"></span>\n <box [width]=\"1\"></box>\n\n <!-- ALIGN -->\n <button nz-button nzSize=\"small\" [disabled]=\"lstSelectedStageCode.size < 2\" (click)=\"alignLeft()\">\u27F8</button>\n <button nz-button nzSize=\"small\" [disabled]=\"lstSelectedStageCode.size < 2\" (click)=\"alignCenter()\">\u2261</button>\n <button nz-button nzSize=\"small\" [disabled]=\"lstSelectedStageCode.size < 2\" (click)=\"alignRight()\">\u27F9</button>\n\n <span class=\"divider\"></span>\n\n <button nz-button nzSize=\"small\" [disabled]=\"lstSelectedStageCode.size < 2\" (click)=\"alignTop()\">\u21D1</button>\n <button nz-button nzSize=\"small\" [disabled]=\"lstSelectedStageCode.size < 2\" (click)=\"alignMiddle()\">\u2550</button>\n <button nz-button nzSize=\"small\" [disabled]=\"lstSelectedStageCode.size < 2\" (click)=\"alignBottom()\">\u21D3</button>\n\n <span class=\"divider\"></span>\n\n <!-- DISTRIBUTE -->\n <button nz-button nzSize=\"small\" [disabled]=\"lstSelectedStageCode.size < 3\" (click)=\"distributeHorizontal()\">\n \u21C4\n </button>\n <button nz-button nzSize=\"small\" [disabled]=\"lstSelectedStageCode.size < 3\" (click)=\"distributeVertical()\">\n \u21C5\n </button>\n </div>\n\n <div\n class=\"canvas-content\"\n [class.panning]=\"isMoveMode\"\n [style.transform]=\"'translate(' + panX + 'px,' + panY + 'px)'\"\n >\n <!-- GRID -->\n <div *ngIf=\"wfcSetting.ShowGrid\" class=\"grid-layer\"></div>\n\n <!-- <div class=\"zoom-toolbar\">\n <button nz-button nzSize=\"small\" (click)=\"zoomOut()\">\u2212</button>\n <span>{{ zoom * 100 | number: \"1.0-0\" }}%</span>\n <button nz-button nzSize=\"small\" (click)=\"zoomIn()\">+</button>\n </div> -->\n\n <!-- SVG EDGES -->\n <svg #svg class=\"edges-layer\" width=\"100%\" height=\"100%\" preserveAspectRatio=\"none\">\n <g [attr.transform]=\"svgTransform\">\n <!-- DEFS -->\n <defs>\n <!-- glow effect -->\n <filter id=\"edge-glow\" x=\"-50%\" y=\"-50%\" width=\"200%\" height=\"200%\">\n <feGaussianBlur stdDeviation=\"2\" result=\"blur\" />\n <feMerge>\n <feMergeNode in=\"blur\" />\n <feMergeNode in=\"SourceGraphic\" />\n </feMerge>\n </filter>\n\n <!-- arrow markers -->\n <marker\n id=\"arrow-blue\"\n markerWidth=\"10\"\n markerHeight=\"10\"\n refX=\"10\"\n refY=\"5\"\n orient=\"auto\"\n markerUnits=\"strokeWidth\"\n >\n <path class=\"blue\" d=\"M0,0 L10,5 L0,10\"></path>\n </marker>\n\n <marker\n id=\"arrow-red\"\n markerWidth=\"10\"\n markerHeight=\"10\"\n refX=\"10\"\n refY=\"5\"\n orient=\"auto\"\n markerUnits=\"strokeWidth\"\n >\n <path class=\"red\" d=\"M0,0 L10,5 L0,10\"></path>\n </marker>\n\n <marker\n id=\"arrow-blue-start\"\n markerWidth=\"10\"\n markerHeight=\"10\"\n viewBox=\"0 0 10 10\"\n refX=\"0\"\n refY=\"5\"\n orient=\"auto\"\n markerUnits=\"strokeWidth\"\n >\n <path class=\"blue\" d=\"M10,0 L0,5 L10,10\"></path>\n </marker>\n\n <marker\n id=\"arrow-red-start\"\n markerWidth=\"10\"\n markerHeight=\"10\"\n viewBox=\"0 0 10 10\"\n refX=\"0\"\n refY=\"5\"\n orient=\"auto\"\n markerUnits=\"strokeWidth\"\n >\n <path class=\"red\" d=\"M10,0 L0,5 L10,10\"></path>\n </marker>\n </defs>\n\n <!-- EDGES B\u00CCNH TH\u01AF\u1EDCNG -->\n <ng-container *ngFor=\"let e of normalEdges; let i = index\">\n <path\n class=\"edge-hit\"\n [attr.d]=\"buildPath(e)\"\n (mouseenter)=\"onMouseEnterEdge(e)\"\n (mouseleave)=\"onMouseLeaveEdge(e)\"\n (click)=\"onClickEdge(e, $event)\"\n ></path>\n\n <path\n class=\"edge\"\n [attr.id]=\"'edge-path-' + i\"\n [attr.d]=\"buildPath(e)\"\n marker-end=\"url(#arrow-blue)\"\n [attr.marker-start]=\"e.allowBack ? 'url(#arrow-blue-start)' : ''\"\n ></path>\n\n <!-- LABEL -->\n <text *ngIf=\"e.ActionText\" class=\"edge-label\" dy=\"-6\">\n <textPath [attr.href]=\"'#edge-path-' + i\" startOffset=\"50%\" text-anchor=\"middle\">\n {{ e.ActionText }}\n </textPath>\n </text>\n\n <text *ngIf=\"e.allowBack && e.labelBack\" class=\"edge-label\" dy=\"14\">\n <textPath [attr.href]=\"'#edge-path-' + i\" startOffset=\"50%\" text-anchor=\"middle\">\n {{ e.labelBack }}\n </textPath>\n </text>\n </ng-container>\n\n <!-- EDGE \u0110ANG HOVER (LU\u00D4N TR\u00CAN C\u00D9NG) -->\n <ng-container *ngFor=\"let e of hoverEdges; let i = index\">\n <path\n class=\"edge-hit\"\n [attr.d]=\"buildPath(e)\"\n (mouseleave)=\"onMouseLeaveEdge(e)\"\n (click)=\"onClickEdge(e, $event)\"\n ></path>\n\n <path\n class=\"edge red\"\n [attr.id]=\"'edge-path-hover-' + i\"\n [attr.d]=\"buildPath(e)\"\n marker-end=\"url(#arrow-red)\"\n [attr.marker-start]=\"e.allowBack ? 'url(#arrow-red-start)' : ''\"\n ></path>\n\n <!-- LABEL -->\n <text *ngIf=\"e.ActionText\" class=\"edge-label\" dy=\"-6\">\n <textPath [attr.href]=\"'#edge-path-hover-' + i\" startOffset=\"50%\" text-anchor=\"middle\">\n {{ e.ActionText }}\n </textPath>\n </text>\n\n <text *ngIf=\"e.allowBack && e.labelBack\" class=\"edge-label\" dy=\"14\">\n <textPath [attr.href]=\"'#edge-path-hover-' + i\" startOffset=\"50%\" text-anchor=\"middle\">\n {{ e.labelBack }}\n </textPath>\n </text>\n </ng-container>\n\n <!-- preview -->\n <path\n *ngIf=\"connectingFrom\"\n class=\"edge\"\n [attr.d]=\"buildPreviewPath()\"\n stroke-dasharray=\"5,5\"\n marker-end=\"url(#arrow-blue)\"\n ></path>\n </g>\n </svg>\n\n <div\n *ngIf=\"isSelecting && selectStart && selectEnd\"\n class=\"selection-box\"\n [style.left.px]=\"Math.min(selectStart.x, selectEnd.x)\"\n [style.top.px]=\"Math.min(selectStart.y, selectEnd.y)\"\n [style.width.px]=\"Math.abs(selectEnd.x - selectStart.x)\"\n [style.height.px]=\"Math.abs(selectEnd.y - selectStart.y)\"\n ></div>\n\n <!-- lstStage -->\n @for (n of lstStage; track $index) {\n @if (n.StageType == \"NODE\") {\n <nz-card\n #nodeEl\n class=\"workflow-node\"\n [attr.data-id]=\"n.Code\"\n [class.selected]=\"n == selectedStage || n == hoverStage || lstSelectedStageCode.has(n.Code)\"\n [style.left.px]=\"n.x\"\n [style.top.px]=\"n.y\"\n [class.connecting-source]=\"isConnectingFrom(n) || isDraggingFrom(n) || isSelectedNode(n)\"\n nzSize=\"small\"\n (mousedown)=\"onMouseDownNode($event, n)\"\n (click)=\"$event.stopPropagation(); onClickStage(n)\"\n >\n <div class=\"title\">{{ n.Code }}</div>\n\n <div>{{ n.Name }}</div>\n\n @if (!n.isReverse) {\n <!-- connector RIGHT: output -->\n <div class=\"connector connector-right\" (click)=\"onClickConnector($event, n, 'right')\"></div>\n\n <!-- connector LEFT: input -->\n <div class=\"connector connector-left\" (click)=\"onClickConnector($event, n, 'left')\"></div>\n } @else {\n <div class=\"connector connector-right reverse\" (click)=\"onClickConnector($event, n, 'right')\"></div>\n <div class=\"connector connector-left reverse\" (click)=\"onClickConnector($event, n, 'left')\"></div>\n }\n </nz-card>\n }\n <!-- START NODE -->\n @else if (n.StageType === \"START\") {\n <nz-card\n class=\"workflow-node workflow-node-start\"\n style=\"border: unset\"\n [style.left.px]=\"n.x\"\n [style.top.px]=\"n.y\"\n nzSize=\"small\"\n (mousedown)=\"onMouseDownNode($event, n)\"\n (click)=\"$event.stopPropagation(); onClickStage(n)\"\n >\n <div class=\"title\"> </div>\n\n <nz-tag\n [class.selected]=\"n == selectedStage || lstSelectedStageCode.has(n.Code)\"\n style=\"position: relative; top: -13px\"\n [style.right.px]=\"n.isReverseLabel ? -83 : 1\"\n [nzColor]=\"'green'\"\n >\n {{ n.StageType }}\n </nz-tag>\n\n <!-- connector RIGHT: output -->\n <div class=\"connector connector-right connector-start\" (click)=\"onClickConnector($event, n, 'right')\"></div>\n </nz-card>\n }\n\n <!-- END NODE -->\n @else if (n.StageType === \"END\") {\n <nz-card\n class=\"workflow-node workflow-node-end\"\n style=\"border: unset\"\n [style.left.px]=\"n.x\"\n [style.top.px]=\"n.y\"\n [class.connecting-source]=\"isConnectingFrom(n)\"\n nzSize=\"small\"\n (mousedown)=\"onMouseDownNode($event, n)\"\n (click)=\"$event.stopPropagation(); onClickStage(n)\"\n >\n <div class=\"title\"> </div>\n\n <nz-tag\n [class.selected]=\"n == selectedStage || lstSelectedStageCode.has(n.Code)\"\n style=\"position: relative; top: -13px\"\n [style.right.px]=\"n.isReverseLabel ? 65 : -5\"\n [nzColor]=\"'red'\"\n >\n {{ n.StageType }}\n </nz-tag>\n\n <!-- connector LEFT: input -->\n <div class=\"connector connector-end\" (click)=\"onClickConnector($event, n, 'left')\"></div>\n </nz-card>\n }\n }\n\n <!-- POINTS -->\n @for (p of connectingPoints; track $index) {\n @if (p != draggingPoint) {\n <div\n class=\"waypoint\"\n [style.left.px]=\"p.x\"\n [style.top.px]=\"p.y\"\n (mousedown)=\"onmousedownPoint($event, p)\"\n (contextmenu)=\"onRightClickPoint($event, hoverEdge!, $index)\"\n ></div>\n } @else {\n <div\n class=\"waypoint dragging\"\n [style.left.px]=\"p.x\"\n [style.top.px]=\"p.y\"\n (mousedown)=\"onmousedownPoint($event, p)\"\n ></div>\n }\n }\n </div>\n</div>\n\n<!-- TEMPLATE -->\n<nz-drawer\n [nzTitle]=\"drawTemplateTitle\"\n nzPlacement=\"right\"\n [nzWidth]=\"500\"\n [nzClosable]=\"false\"\n [nzVisible]=\"drawTemplateVisibel\"\n [nzFooter]=\"footerTplTemplate\"\n (nzOnClose)=\"drawTemplateVisibel = false\"\n>\n <ng-template #drawTemplateTitle> <nz-icon nzType=\"setting\"></nz-icon> Template </ng-template>\n <ng-container *nzDrawerContent>\n <extend-input [label]=\"'Code'\" [(_ngModel)]=\"template.Code\"></extend-input>\n <extend-input [label]=\"'Name'\" [(_ngModel)]=\"template.Name\"></extend-input>\n <extend-select\n [label]=\"'Type'\"\n [lstItem]=\"lstTemplateType\"\n [valueField]=\"'Code'\"\n [displayFields]=\"['Code', 'Name']\"\n [(_ngModel)]=\"template.DocType\"\n ></extend-select>\n <extend-textarea [label]=\"'Desscription'\" [(_ngModel)]=\"template.Description\"></extend-textarea>\n <extend-select\n [label]=\"'Print template'\"\n [lstItem]=\"lstTemplatePrint\"\n [valueField]=\"'Code'\"\n [displayFields]=\"['Code', 'Name']\"\n [(_ngModel)]=\"template.TemplatePrint\"\n ></extend-select>\n <extend-checkbox [label]=\"'Active'\" [(_ngModel)]=\"template.IsActive\"></extend-checkbox>\n\n <h3 nz-col><nz-icon nzType=\"send\" nzTheme=\"outline\" /> Stage status</h3>\n\n @for (tag of template.lstStageStatus; track tag) {\n <nz-tag\n [nzMode]=\"checkRemoveableStageStatus(tag) ? 'closeable' : 'default'\"\n (nzOnClose)=\"handleCloseStageStatus(tag)\"\n >\n {{ sliceTagName(tag.Code + (tag.Name ? \" - \" + tag.Name : \"\")) }}\n </nz-tag>\n }\n\n <br *ngIf=\"template.lstStageStatus && template.lstStageStatus.length\" />\n\n @if (!inputStageStatusVisible) {\n <nz-tag class=\"editable-tag\" nzNoAnimation (click)=\"showInputStageStatus()\">\n <nz-icon nzType=\"plus\" />\n New Action type\n </nz-tag>\n } @else {\n <input\n #inputStageStatusElement\n nz-input\n nzSize=\"small\"\n type=\"text\"\n style=\"width: 150px\"\n placeholder=\"Code\"\n [(ngModel)]=\"inputStageStatusCode\"\n (keydown.enter)=\"inputStageStatusNameElement.select()\"\n />\n <input\n #inputStageStatusNameElement\n nz-input\n nzSize=\"small\"\n type=\"text\"\n style=\"width: 150px\"\n placeholder=\"Name\"\n [(ngModel)]=\"inputStageStatusName\"\n (blur)=\"handleInputStageStatusConfirm()\"\n (keydown.enter)=\"handleInputStageStatusConfirm()\"\n />\n }\n\n <box [height]=\"16\"></box>\n\n <h3 nz-col><nz-icon nzType=\"send\" nzTheme=\"outline\" /> Action type</h3>\n\n @for (tag of template.lstActionType; track tag) {\n <nz-tag\n [nzMode]=\"checkRemoveableActionType(tag) ? 'closeable' : 'default'\"\n (nzOnClose)=\"handleCloseActionType(tag)\"\n >\n {{ sliceTagName(tag.Code + (tag.Name ? \" - \" + tag.Name : \"\")) }}\n </nz-tag>\n }\n\n <br *ngIf=\"template.lstActionType && template.lstActionType.length\" />\n\n @if (!inputActionTypeVisible) {\n <nz-tag class=\"editable-tag\" nzNoAnimation (click)=\"showInputActionType()\">\n <nz-icon nzType=\"plus\" />\n New Action type\n </nz-tag>\n } @else {\n <input\n #inputActionTypeElement\n nz-input\n nzSize=\"small\"\n type=\"text\"\n style=\"width: 150px\"\n placeholder=\"Code\"\n [(ngModel)]=\"inputActionTypeCode\"\n (keydown.enter)=\"inputActionTypeNameElement.select()\"\n />\n <input\n #inputActionTypeNameElement\n nz-input\n nzSize=\"small\"\n type=\"text\"\n style=\"width: 150px\"\n placeholder=\"Name\"\n [(ngModel)]=\"inputActionTypeName\"\n (blur)=\"handleInputActionTypeConfirm()\"\n (keydown.enter)=\"handleInputActionTypeConfirm()\"\n />\n }\n\n <box [height]=\"16\"></box>\n\n <h3 nz-col><nz-icon nzType=\"send\" nzTheme=\"outline\" /> Action status</h3>\n\n @for (tag of template.lstActionStatus; track tag) {\n <nz-tag [nzMode]=\"checkRemoveableActionStatus(tag) ? 'closeable' : 'default'\" (nzOnClose)=\"handleClose(tag)\">\n {{ sliceTagName(tag.Code + (tag.Name ? \" - \" + tag.Name : \"\")) }}\n </nz-tag>\n }\n\n <br *ngIf=\"template.lstActionStatus && template.lstActionStatus.length\" />\n\n @if (!inputVisible) {\n <nz-tag class=\"editable-tag\" nzNoAnimation (click)=\"showInput()\">\n <nz-icon nzType=\"plus\" />\n New Action status\n </nz-tag>\n } @else {\n <input\n #inputElement\n nz-input\n nzSize=\"small\"\n type=\"text\"\n style=\"width: 150px\"\n placeholder=\"+ New Action status\"\n [(ngModel)]=\"inputValue\"\n (blur)=\"handleInputConfirm()\"\n (keydown.enter)=\"handleInputConfirm()\"\n />\n }\n </ng-container>\n\n <ng-template #footerTplTemplate>\n <div nz-flex [nzGap]=\"6\">\n <button nz-button (click)=\"drawTemplateVisibel = false\">Close</button>\n <button nz-button nzDanger (click)=\"save()\">Save</button>\n </div>\n </ng-template>\n</nz-drawer>\n\n<!-- STAGE -->\n<nz-drawer\n [nzTitle]=\"drawerTitleStage\"\n [nzPlacement]=\"selectedStage?.drawerPosition ? selectedStage?.drawerPosition : 'right'\"\n [nzVisible]=\"!!selectedStage\"\n [nzWidth]=\"750\"\n [nzClosable]=\"false\"\n [nzFooter]=\"footerTplNode\"\n (nzOnClose)=\"selectedStage = undefined\"\n>\n <ng-template #drawerTitleStage>\n <div nz-row nzJustify=\"space-between\">\n @if (selectedStage) {\n @if (selectedStage.drawerPosition == \"left\") {\n <button nz-button nzSize=\"small\" nz-tooltip=\"To the right\" (click)=\"selectedStage.drawerPosition = 'right'\">\n <nz-icon nzType=\"double-right\"></nz-icon>\n </button>\n STAGE\n } @else {\n STAGE\n <button nz-button nzSize=\"small\" nz-tooltip=\"To the left\" (click)=\"selectedStage.drawerPosition = 'left'\">\n <nz-icon nzType=\"double-left\"></nz-icon>\n </button>\n }\n }\n </div>\n </ng-template>\n <ng-container *nzDrawerContent>\n <ng-container *ngIf=\"selectedStage\">\n <nz-radio-group [(ngModel)]=\"selectedStage.StageType\">\n <label nz-radio nzValue=\"START\" [nzDisabled]=\"true\">START</label>\n <label nz-radio nzValue=\"NODE\" [nzDisabled]=\"true\">NODE</label>\n <label nz-radio nzValue=\"END\" [nzDisabled]=\"true\">END</label>\n </nz-radio-group>\n\n <box [height]=\"16\"></box>\n\n <div nz-row [nzGutter]=\"32\">\n <extend-input\n nz-col\n [nzSpan]=\"8\"\n [label]=\"'Code'\"\n [layOutType]=\"'vertical'\"\n [disabled]=\"['START', 'END'].indexOf(selectedStage!.StageType) >= 0\"\n [required]=\"true\"\n [(_ngModel)]=\"selectedStage.Code\"\n ></extend-input>\n\n <extend-textarea\n nz-col\n [nzSpan]=\"16\"\n [label]=\"'Name'\"\n [layOutType]=\"'vertical'\"\n [autoSize]=\"true\"\n [disabled]=\"['START', 'END'].indexOf(selectedStage!.StageType) >= 0\"\n [required]=\"true\"\n [(_ngModel)]=\"selectedStage.Name\"\n ></extend-textarea>\n </div>\n\n <div nz-row [nzGutter]=\"32\">\n <extend-input-number\n nz-col\n [nzSpan]=\"8\"\n [label]=\"'Sequence'\"\n [layOutType]=\"'vertical'\"\n [(_ngModel)]=\"selectedStage.SeqValue\"\n ></extend-input-number>\n\n <extend-select\n nz-col\n [nzSpan]=\"16\"\n [label]=\"'Org implement'\"\n [layOutType]=\"'vertical'\"\n [lstItem]=\"lstOrg\"\n [valueField]=\"'Code'\"\n [displayFields]=\"['Code', 'Name']\"\n [(_ngModel)]=\"selectedStage.App_Org_Id\"\n ></extend-select>\n </div>\n\n @if ([\"START\", \"END\"].indexOf(selectedStage.StageType) < 0) {\n <extend-checkbox [label]=\"'Reverse'\" [labelSpan]=\"0\" [(_ngModel)]=\"selectedStage.isReverse\"></extend-checkbox>\n\n <extend-checkbox\n [label]=\"'Require user action'\"\n [labelSpan]=\"0\"\n [(_ngModel)]=\"selectedStage.IsRequireUser\"\n ></extend-checkbox>\n } @else {\n <extend-checkbox\n [label]=\"'Reverse'\"\n [labelSpan]=\"0\"\n [(_ngModel)]=\"selectedStage.isReverseLabel\"\n ></extend-checkbox>\n }\n\n <box [height]=\"16\"></box>\n\n <div nz-row [nzGutter]=\"16\">\n <h3 nz-col><nz-icon nzType=\"send\" nzTheme=\"outline\" /> List Action</h3>\n <div nz-col>\n <button nz-button nzType=\"primary\" nzSize=\"small\" (click)=\"addAction()\">\n <nz-icon nzType=\"plus\"></nz-icon>\n </button>\n </div>\n </div>\n\n <nz-table\n nzSize=\"small\"\n [nzData]=\"selectedStage.lstAction || []\"\n [nzShowPagination]=\"false\"\n [nzNoResult]=\"' '\"\n >\n <thead>\n <tr [hidden]=\"true\">\n <th nzWidth=\"50px\"></th>\n <th></th>\n </tr>\n </thead>\n <tbody>\n @for (action of selectedStage.lstAction; track $index) {\n <tr>\n <td>\n <nz-icon\n nzType=\"delete\"\n nzTheme=\"outline\"\n class=\"color-warn cursor-pointer icon-size-16\"\n (click)=\"$event.stopPropagation(); deleteAction(action)\"\n ></nz-icon>\n </td>\n <td (click)=\"hoverEdge = action\">\n <div nz-row [nzGutter]=\"16\" class=\"form-item-no-bottom\">\n <extend-input\n nz-col\n [nzSpan]=\"8\"\n [label]=\"'Action text'\"\n [layOutType]=\"'vertical'\"\n [(_ngModel)]=\"action.ActionText\"\n (_ngModelChange)=\"onchangeActionText(action)\"\n ></extend-input>\n <extend-select\n nz-col\n [nzSpan]=\"8\"\n [label]=\"'Action type'\"\n [layOutType]=\"'vertical'\"\n [lstItem]=\"template.lstActionType\"\n [valueField]=\"'Code'\"\n [displayFields]=\"['Code', 'Name']\"\n [(_ngModel)]=\"action.ActionType\"\n ></extend-select>\n <extend-select\n nz-col\n [nzSpan]=\"8\"\n [label]=\"'Action status'\"\n [layOutType]=\"'vertical'\"\n [lstItem]=\"template.lstActionStatus\"\n [valueField]=\"'Code'\"\n [displayFields]=\"['Code', 'Name']\"\n [(_ngModel)]=\"action.ActionStatus\"\n ></extend-select>\n\n <extend-select\n nz-col\n [nzSpan]=\"8\"\n [label]=\"'Stage status'\"\n [layOutType]=\"'vertical'\"\n [lstItem]=\"template.lstStageStatus\"\n [valueField]=\"'Code'\"\n [displayFields]=\"['Code', 'Name']\"\n [(_ngModel)]=\"action.StageStatus\"\n ></extend-select>\n\n <extend-select\n nz-col\n [nzSpan]=\"8\"\n [label]=\"'Next stage'\"\n [layOutType]=\"'vertical'\"\n [lstItem]=\"lstStage\"\n [valueField]=\"'Code'\"\n [displayFields]=\"['Code', 'Name']\"\n [(_ngModel)]=\"action.ToStage\"\n (_ngModelChange)=\"onchangeNextStage(action)\"\n ></extend-select>\n\n <extend-select\n nz-col\n [nzSpan]=\"8\"\n [label]=\"'Next stage status'\"\n [layOutType]=\"'vertical'\"\n [lstItem]=\"template.lstStageStatus\"\n [valueField]=\"'Code'\"\n [displayFields]=\"['Code', 'Name']\"\n [(_ngModel)]=\"action.NextStageStatus\"\n ></extend-select>\n\n <nz-divider\n *ngIf=\"selectedStage.lstAction && $index + 1 < selectedStage.lstAction.length\"\n style=\"margin: 16px 0px 8px 0px; background-color: orange\"\n ></nz-divider>\n </div>\n </td>\n </tr>\n }\n </tbody>\n </nz-table>\n\n <box [height]=\"16\"></box>\n </ng-container>\n </ng-container>\n\n <ng-template #footerTplNode>\n <div nz-flex [nzGap]=\"6\" [nzJustify]=\"selectedStage && selectedStage.drawerPosition == 'left' ? 'end' : 'start'\">\n <button nz-button (click)=\"selectedStage = undefined\">Close</button>\n <button\n *ngIf=\"selectedStage && ['START', 'END'].indexOf(selectedStage.StageType) < 0\"\n nz-button\n nzDanger\n (click)=\"deleteNode()\"\n >\n Delete\n </button>\n </div>\n </ng-template>\n</nz-drawer>\n\n<!-- EDGE -->\n<nz-drawer\n nzTitle=\"ACTION\"\n nzPlacement=\"right\"\n [nzClosable]=\"false\"\n [nzWidth]=\"750\"\n [nzVisible]=\"!!selectedAction\"\n (nzOnClose)=\"selectedAction = undefined\"\n>\n <ng-container *nzDrawerContent>\n <ng-container *ngIf=\"selectedAction\">\n <strong>{{ selectedAction.FromStage }} -> {{ selectedAction.ToStage }}</strong>\n\n <box [height]=\"16\"></box>\n\n <div nz-row [nzGutter]=\"16\" class=\"form-item-no-bottom\">\n <extend-input\n nz-col\n [nzSpan]=\"8\"\n [label]=\"'Action text'\"\n [layOutType]=\"'vertical'\"\n [(_ngModel)]=\"selectedAction.ActionText\"\n (_ngModelChange)=\"onchangeActionText(selectedAction)\"\n ></extend-input>\n <extend-select\n nz-col\n [nzSpan]=\"8\"\n [label]=\"'Action type'\"\n [layOutType]=\"'vertical'\"\n [lstItem]=\"template.lstActionType\"\n [valueField]=\"'Code'\"\n [displayFields]=\"['Code', 'Name']\"\n [(_ngModel)]=\"selectedAction.ActionType\"\n ></extend-select>\n <extend-select\n nz-col\n [nzSpan]=\"8\"\n [label]=\"'Action status'\"\n [layOutType]=\"'vertical'\"\n [lstItem]=\"template.lstActionStatus\"\n [valueField]=\"'Code'\"\n [displayFields]=\"['Code', 'Name']\"\n [(_ngModel)]=\"selectedAction.ActionStatus\"\n ></extend-select>\n\n <extend-select\n nz-col\n [nzSpan]=\"8\"\n [label]=\"'Stage status'\"\n [layOutType]=\"'vertical'\"\n [lstItem]=\"template.lstStageStatus\"\n [valueField]=\"'Code'\"\n [displayFields]=\"['Code', 'Name']\"\n [(_ngModel)]=\"selectedAction.StageStatus\"\n ></extend-select>\n\n <extend-select\n nz-col\n [nzSpan]=\"8\"\n [label]=\"'Next stage'\"\n [layOutType]=\"'vertical'\"\n [lstItem]=\"lstStage\"\n [valueField]=\"'Code'\"\n [displayFields]=\"['Code', 'Name']\"\n [(_ngModel)]=\"selectedAction.ToStage\"\n (_ngModelChange)=\"onchangeNextStage(selectedAction)\"\n ></extend-select>\n\n <extend-select\n nz-col\n [nzSpan]=\"8\"\n [label]=\"'Next stage status'\"\n [layOutType]=\"'vertical'\"\n [lstItem]=\"template.lstStageStatus\"\n [valueField]=\"'Code'\"\n [displayFields]=\"['Code', 'Name']\"\n [(_ngModel)]=\"selectedAction.NextStageStatus\"\n ></extend-select>\n </div>\n\n <!-- <box [height]=\"16\"></box> -->\n <nz-divider></nz-divider>\n\n <extend-checkbox\n [label]=\"'Allow back'\"\n [labelSpan]=\"0\"\n [(_ngModel)]=\"selectedAction.allowBack\"\n (_ngModelChange)=\"onchangeAllowBack(selectedAction)\"\n ></extend-checkbox>\n\n <box [height]=\"16\"></box>\n\n @if (selectedAction.allowBack) {\n <strong>{{ selectedAction.ToStage }} -> {{ selectedAction.FromStage }}</strong>\n\n @if (selectedBackAction) {\n <div nz-row [nzGutter]=\"16\" class=\"form-item-no-bottom\">\n <extend-input\n nz-col\n [nzSpan]=\"8\"\n [label]=\"'Action text'\"\n [layOutType]=\"'vertical'\"\n [(_ngModel)]=\"selectedBackAction.ActionText\"\n (_ngModelChange)=\"onchangeActionText(selectedBackAction)\"\n ></extend-input>\n <extend-select\n nz-col\n [nzSpan]=\"8\"\n [label]=\"'Action type'\"\n [layOutType]=\"'vertical'\"\n [lstItem]=\"template.lstActionType\"\n [valueField]=\"'Code'\"\n [displayFields]=\"['Code', 'Name']\"\n [(_ngModel)]=\"selectedBackAction.ActionType\"\n ></extend-select>\n <extend-select\n nz-col\n [nzSpan]=\"8\"\n [label]=\"'Action status'\"\n [layOutType]=\"'vertical'\"\n [lstItem]=\"template.lstActionStatus\"\n [valueField]=\"'Code'\"\n [displayFields]=\"['Code', 'Name']\"\n [(_ngModel)]=\"selectedBackAction.ActionStatus\"\n ></extend-select>\n\n <extend-select\n nz-col\n [nzSpan]=\"8\"\n [label]=\"'Stage status'\"\n [layOutType]=\"'vertical'\"\n [lstItem]=\"template.lstStageStatus\"\n [valueField]=\"'Code'\"\n [displayFields]=\"['Code', 'Name']\"\n [(_ngModel)]=\"selectedBackAction.StageStatus\"\n ></extend-select>\n\n <extend-select\n nz-col\n [nzSpan]=\"8\"\n [label]=\"'Next stage'\"\n [layOutType]=\"'vertical'\"\n [lstItem]=\"lstStage\"\n [valueField]=\"'Code'\"\n [displayFields]=\"['Code', 'Name']\"\n [(_ngModel)]=\"selectedBackAction.ToStage\"\n (_ngModelChange)=\"onchangeNextStage(selectedBackAction)\"\n ></extend-select>\n\n <extend-select\n nz-col\n [nzSpan]=\"8\"\n [label]=\"'Next stage status'\"\n [layOutType]=\"'vertical'\"\n [lstItem]=\"template.lstStageStatus\"\n [valueField]=\"'Code'\"\n [displayFields]=\"['Code', 'Name']\"\n [(_ngModel)]=\"selectedBackAction.NextStageStatus\"\n ></extend-select>\n </div>\n }\n }\n </ng-container>\n </ng-container>\n</nz-drawer>\n\n<!-- setting editor -->\n<nz-drawer\n [nzTitle]=\"drawSettingTitle\"\n nzPlacement=\"right\"\n [nzClosable]=\"false\"\n [nzVisible]=\"settingVisible\"\n (nzOnClose)=\"settingVisible = false\"\n>\n <ng-template #drawSettingTitle> <nz-icon nzType=\"setting\"></nz-icon> Editor </ng-template>\n <ng-container *nzDrawerContent>\n <div class=\"form-item-no-bottom\">\n <extend-checkbox [label]=\"'Show grid'\" [labelSpan]=\"0\" [(_ngModel)]=\"wfcSetting.ShowGrid\"></extend-checkbox>\n\n <box [height]=\"16\"></box>\n\n <extend-input-number [label]=\"'Canvas width'\" [(_ngModel)]=\"CANVAS_WIDTH\"></extend-input-number>\n <box [height]=\"16\"></box>\n <extend-input-number [label]=\"'Canvas height'\" [(_ngModel)]=\"CANVAS_HEIGHT\"></extend-input-number>\n </div>\n </ng-container>\n</nz-drawer>\n", styles: ["@charset \"UTF-8\";.workflow-canvas{position:relative;width:100%;height:600px;background:#f0f2f5;cursor:default;overflow:hidden;outline:1px dashed rgba(0,0,0,.1)}.workflow-canvas .edges-layer{position:absolute;inset:0;width:100%;height:100%;z-index:1}.workflow-canvas .edges-layer path{stroke-width:2;fill:none}.canvas-content{transform-origin:0 0;width:10000px;height:10000px}.canvas-content{outline:1px dashed red}.canvas-content.panning{cursor:grab}.canvas-content.panning:active{cursor:grabbing}.workflow-canvas.panning{cursor:grab}.workflow-canvas.panning:active{cursor:grabbing}.workflow-canvas.connecting,.workflow-canvas.connecting .workflow-node{cursor:crosshair}.workflow-node{position:absolute;width:160px;cursor:grab;-webkit-user-select:none;user-select:none;z-index:2}.workflow-node:active{cursor:grabbing}.workflow-node .title{font-weight:600;margin-bottom:6px}.workflow-node.connecting-source,.workflow-node.selected,.workflow-node:hover:not(.workflow-node-start,.workflow-node-end){outline:2px dashed #1890ff}.workflow-node-start{background-color:unset;display:flex;justify-content:right}.workflow-node-end{background-color:unset;display:flex}.connector{position:absolute;top:39px;width:12px;height:12px;background:#1890ff;border-radius:50%;transform:translateY(-50%);cursor:crosshair;transition:box-shadow .15s ease-out,transform .15s ease-out}.connector:hover{box-shadow:0 0 0 3px #1890ff4d;transform:translateY(-50%) scale(1.15)}.connector-right{right:-8px;background:orange}.connector-right.reverse,.connector-left{left:-8px}.connector-left.reverse{left:unset;right:-8px}.connector-start{width:16px;height:16px;right:-3px;background:green;box-shadow:0 0 0 3px #1890ff4d}.connector-end{width:16px;height:16px;left:-6px;background:#000;box-shadow:0 0 0 3px #1890ff4d}.edge{cursor:pointer;stroke:#1890ff;stroke-linecap:round;stroke-linejoin:round;stroke-width:2;fill:none;transition:stroke .15s ease}.edge-hit{stroke:transparent;stroke-width:18px!important;fill:none;cursor:pointer}.edge-hit:hover+.edge{stroke:#ff4d4f;stroke-width:2}.workflow-canvas.dragging-point .edge-hit{pointer-events:none}.edge:hover+.edge-label{fill:#1890ff;font-weight:500}.edge-label{font-size:12px;fill:#555;pointer-events:none;-webkit-user-select:none;user-select:none}.blue{stroke:#1890ff!important}.red{stroke:#ff4d4f!important;box-shadow:0 0 0 3px #1890ff4d!important}.workflow-svg{background:transparent;overflow:visible}.waypoint{position:absolute;width:10px;height:10px;background:#ff4d4f;border-radius:50%;transform:translate(-50%,-50%) scale(1);cursor:move;z-index:1;transition:transform .12s ease-out,background-color .12s ease-out,box-shadow .12s ease-out}.waypoint:hover{background:#ff7875;transform:translate(-50%,-50%) scale(1.25);box-shadow:0 0 0 4px #ff787559}.waypoint.dragging{transition:none;transform:translate(-50%,-50%) scale(1.15);pointer-events:none}.workflow-canvas.dragging-point svg{cursor:grabbing}.selection-box{position:absolute;border:1px dashed #1890ff;background:#1890ff1a;pointer-events:none}.toolbar{position:absolute;top:8px;left:8px;display:flex;gap:4px;padding:6px;background:#fff;border-radius:6px;box-shadow:0 2px 8px #00000026;z-index:100}.toolbar .divider{width:1px;background:#e5e5e5;margin:0 2px}.zoom-toolbar{position:absolute;top:8px;left:8px;z-index:10;display:flex;align-items:center;gap:6px;background:#fff;padding:4px 6px;border-radius:6px;box-shadow:0 2px 6px #00000026}.zoom-toolbar span{min-width:40px;text-align:center;font-size:12px}nz-tag.selected{outline:2px dashed #1890ff}.grid-layer{position:absolute;inset:0;pointer-events:none;z-index:0;background-image:radial-gradient(rgba(0,0,0,.08) 1px,transparent 1px);background-size:20px 20px}.grid-layer{background-image:linear-gradient(to right,rgba(0,0,0,.06) 1px,transparent 1px),linear-gradient(to bottom,rgba(0,0,0,.06) 1px,transparent 1px);background-size:20px 20px}.editable-tag{background:#fff;border-style:dashed}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2$2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2$2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: NzCardModule }, { kind: "component", type: i3$1.NzCardComponent, selector: "nz-card", inputs: ["nzBordered", "nzLoading", "nzHoverable", "nzBodyStyle", "nzCover", "nzActions", "nzType", "nzSize", "nzTitle", "nzExtra"], exportAs: ["nzCard"] }, { kind: "ngmodule", type: NzDrawerModule }, { kind: "component", type: i4$1.NzDrawerComponent, selector: "nz-drawer", inputs: ["nzContent", "nzCloseIcon", "nzClosable", "nzMaskClosable", "nzMask", "nzCloseOnNavigation", "nzNoAnimation", "nzKeyboard", "nzTitle", "nzExtra", "nzFooter", "nzPlacement", "nzSize", "nzMaskStyle", "nzBodyStyle", "nzWrapClassName", "nzWidth", "nzHeight", "nzZIndex", "nzOffsetX", "nzOffsetY", "nzVisible"], outputs: ["nzOnViewInit", "nzOnClose", "nzVisibleChange"], exportAs: ["nzDrawer"] }, { kind: "directive", type: i4$1.NzDrawerContentDirective, selector: "[nzDrawerContent]", exportAs: ["nzDrawerContent"] }, { kind: "ngmodule", type: NzTagModule }, { kind: "component", type: i5$2.NzTagComponent, selector: "nz-tag", inputs: ["nzMode", "nzColor", "nzChecked", "nzBordered"], outputs: ["nzOnClose", "nzCheckedChange"], exportAs: ["nzTag"] }, { kind: "ngmodule", type: NzRadioModule }, { kind: "component", type: i6$3.NzRadioComponent, selector: "[nz-radio],[nz-radio-button]", inputs: ["nzValue", "nzDisabled", "nzAutoFocus", "nz-radio-button"], exportAs: ["nzRadio"] }, { kind: "component", type: i6$3.NzRadioGroupComponent, selector: "nz-radio-group", inputs: ["nzDisabled", "nzButtonStyle", "nzSize", "nzName"], exportAs: ["nzRadioGroup"] }, { kind: "ngmodule", type: NzIconModule }, { kind: "directive", type: i2.NzIconDirective, selector: "nz-icon,[nz-icon]", inputs: ["nzSpin", "nzRotate", "nzType", "nzTheme", "nzTwotoneColor", "nzIconfont"], exportAs: ["nzIcon"] }, { kind: "component", type: Box, selector: "box", inputs: ["display", "width", "height"] }, { kind: "ngmodule", type: NzButtonModule }, { kind: "component", type: i8.NzButtonComponent, selector: "button[nz-button], a[nz-button]", inputs: ["nzBlock", "nzGhost", "nzSearch", "nzLoading", "nzDanger", "disabled", "tabIndex", "nzType", "nzShape", "nzSize"], exportAs: ["nzButton"] }, { kind: "directive", type: i9.ɵNzTransitionPatchDirective, selector: "[nz-button], nz-button-group, [nz-icon], nz-icon, [nz-menu-item], [nz-submenu], nz-select-top-control, nz-select-placeholder, nz-input-group", inputs: ["hidden"] }, { kind: "directive", type: i10.NzWaveDirective, selector: "[nz-wave],button[nz-button]:not([nzType=\"link\"]):not([nzType=\"text\"])", inputs: ["nzWaveExtraNode"], exportAs: ["nzWave"] }, { kind: "ngmodule", type: NzToolTipModule }, { kind: "directive", type: i11.NzTooltipDirective, selector: "[nz-tooltip]", inputs: ["nzTooltipTitle", "nzTooltipTitleContext", "nz-tooltip", "nzTooltipTrigger", "nzTooltipPlacement", "nzTooltipOrigin", "nzTooltipVisible", "nzTooltipMouseEnterDelay", "nzTooltipMouseLeaveDelay", "nzTooltipOverlayClassName", "nzTooltipOverlayStyle", "nzTooltipArrowPointAtCenter", "cdkConnectedOverlayPush", "nzTooltipColor"], outputs: ["nzTooltipVisibleChange"], exportAs: ["nzTooltip"] }, { kind: "component", type: ExtendInput, selector: "extend-input", inputs: ["layOutType", "label", "placeHolder", "labelAlign", "inputClass", "labelSpan", "allowClear", "disabled", "readOnly", "required", "noBottom", "selectModeType", "autocomplete", "inputWidth", "inputHeight", "borderBottomOnly", "displayInline", "size", "lstItem", "displayField", "valueField", "formData", "controlName", "_ngModel"], outputs: ["_ngModelChange", "onclickClearIcon", "onenter"] }, { kind: "component", type: ExtendSelectComponent, selector: "extend-select", inputs: ["layOutType", "label", "placeHolder", "labelAlign", "labelSpan", "disabled", "required", "noBottom", "multiple", "showSelectAll", "maxTagCount", "inputWidth", "inputHeight", "borderBottomOnly", "displayInline", "size", "lstItem", "displayField", "displayFields", "valueField", "formData", "controlName", "_ngModel"], outputs: ["_ngModelChange", "itemChange", "onFocus"] }, { kind: "component", type: ExtendCheckbox, selector: "extend-checkbox", inputs: ["label", "labelSpan", "disabled", "formData", "controlName", "valueType", "_ngModel"], outputs: ["_ngModelChange"] }, { kind: "component", type: ExtendTextArea, selector: "extend-textarea", inputs: ["layOutType", "label", "placeHolder", "labelAlign", "labelSpan", "inputSpan", "disabled", "required", "noBottom", "selectModeType", "inputClass", "minRows", "maxRows", "autoSize", "lstItem", "displayField", "valueField", "formData", "controlName", "_ngModel"], outputs: ["_ngModelChange"] }, { kind: "ngmodule", type: NzInputModule }, { kind: "directive", type: i6$1.NzInputDirective, selector: "input[nz-input],textarea[nz-input]", inputs: ["nzBorderless", "nzSize", "nzStepperless", "nzStatus", "disabled"], exportAs: ["nzInput"] }, { kind: "ngmodule", type: NzGridModule }, { kind: "directive", type: i4.NzColDirective, selector: "[nz-col],nz-col,nz-form-control,nz-form-label", inputs: ["nzFlex", "nzSpan", "nzOrder", "nzOffset", "nzPush", "nzPull", "nzXs", "nzSm", "nzMd", "nzLg", "nzXl", "nzXXl"], exportAs: ["nzCol"] }, { kind: "directive", type: i4.NzRowDirective, selector: "[nz-row],nz-row,nz-form-item", inputs: ["nzAlign", "nzJustify", "nzGutter"], exportAs: ["nzRow"] }, { kind: "ngmodule", type: NzFlexModule }, { kind: "directive", type: i14.NzFlexDirective, selector: "[nz-flex],nz-flex", inputs: ["nzVertical", "nzJustify", "nzAlign", "nzGap", "nzWrap", "nzFlex"], exportAs: ["nzFlex"] }, { kind: "component", type: ExtendInputNumber, selector: "extend-input-number", inputs: ["layOutType", "label", "placeHolder", "labelAlign", "labelSpan", "disabled", "required", "noBottom", "size", "min", "max", "precision", "inputWidth", "inputHeight", "borderBottomOnly", "displayInline", "separatorType", "formData", "controlName", "_ngModel"], outputs: ["_ngModelChange"] }, { kind: "ngmodule", type: NzTableModule }, { kind: "component", type: i15.NzTableComponent, selector: "nz-table", inputs: ["nzTableLayout", "nzShowTotal", "nzItemRender", "nzTitle", "nzFooter", "nzNoResult", "nzPageSizeOptions", "nzVirtualItemSize", "nzVirtualMaxBufferPx", "nzVirtualMinBufferPx", "nzVirtualForTrackBy", "nzLoadingDelay", "nzPageIndex", "nzPageSize", "nzTotal", "nzWidthConfig", "nzData", "nzCustomColumn", "nzPaginationPosition", "nzScroll", "noDataVirtualHeight", "nzPaginationType", "nzFrontPagination", "nzTemplateMode", "nzShowPagination", "nzLoading", "nzOuterBordered", "nzLoadingIndicator", "nzBordered", "nzSize", "nzShowSizeChanger", "nzHideOnSinglePage", "nzShowQuickJumper", "nzSimple"], outputs: ["nzPageSizeChange", "nzPageIndexChange", "nzQueryParams", "nzCurrentPageDataChange", "nzCustomColumnChange"], exportAs: ["nzTable"] }, { kind: "directive", type: i15.NzTableCellDirective, selector: "th:not(.nz-disable-th):not([mat-cell]), td:not(.nz-disable-td):not([mat-cell])" }, { kind: "directive", type: i15.NzThMeasureDirective, selector: "th", inputs: ["nzWidth", "colspan", "colSpan", "rowspan", "rowSpan"] }, { kind: "component", type: i15.NzTheadComponent, selector: "thead:not(.ant-table-thead)", outputs: ["nzSortOrderChange"] }, { kind: "component", type: i15.NzTbodyComponent, selector: "tbody" }, { kind: "directive", type: i15.NzTrDirective, selector: "tr:not([mat-row]):not([mat-header-row]):not([nz-table-measure-row]):not([nzExpand]):not([nz-table-fixed-row])" }, { kind: "ngmodule", type: NzDividerModule }, { kind: "component", type: i16.NzDividerComponent, selector: "nz-divider", inputs: ["nzText", "nzType", "nzOrientation", "nzVariant", "nzDashed", "nzPlain"], exportAs: ["nzDivider"] }] });
|
|
4177
|
+
}
|
|
4178
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.1.7", ngImport: i0, type: WorkflowEditorComponent, decorators: [{
|
|
4179
|
+
type: Component,
|
|
4180
|
+
args: [{ selector: 'app-workflow-editor', imports: [
|
|
4181
|
+
CommonModule,
|
|
4182
|
+
FormsModule,
|
|
4183
|
+
NzCardModule,
|
|
4184
|
+
NzDrawerModule,
|
|
4185
|
+
NzTagModule,
|
|
4186
|
+
NzRadioModule,
|
|
4187
|
+
NzIconModule,
|
|
4188
|
+
Box,
|
|
4189
|
+
NzButtonModule,
|
|
4190
|
+
NzToolTipModule,
|
|
4191
|
+
ExtendInput,
|
|
4192
|
+
ExtendSelectComponent,
|
|
4193
|
+
ExtendCheckbox,
|
|
4194
|
+
ExtendTextArea,
|
|
4195
|
+
NzInputModule,
|
|
4196
|
+
NzGridModule,
|
|
4197
|
+
NzFlexModule,
|
|
4198
|
+
ExtendInputNumber,
|
|
4199
|
+
NzTableModule,
|
|
4200
|
+
NzDividerModule,
|
|
4201
|
+
], template: "<div\n #canvasRef\n class=\"workflow-canvas\"\n [class.connecting]=\"!!connectingFrom\"\n [class.dragging-point]=\"!!draggingPoint\"\n [style.width.px]=\"CANVAS_WIDTH\"\n [style.height.px]=\"CANVAS_HEIGHT\"\n (mousedown)=\"onMouseDownCanvas($event)\"\n (mousemove)=\"onMouseMoveCanvas($event)\"\n (mouseup)=\"onMouseUpCanvas($event)\"\n (click)=\"onClickCanvas($event)\"\n>\n <!-- \n\n [style.width.px]=\"getCanvasWidth()\"\n [style.height.px]=\"getCanvasHeight()\" \n\n style=\"width: 100%; height: calc(100vh - 120px)\"\n \n -->\n\n <div class=\"toolbar\" (mousedown)=\"$event.stopPropagation()\" (click)=\"$event.stopPropagation()\">\n <button nz-button nzSize=\"small\" nz-tooltip=\"Editor setting\" (click)=\"settingVisible = true\">\n <nz-icon nzType=\"setting\"></nz-icon>\n </button>\n\n <box [width]=\"1\"></box>\n <span class=\"divider\"></span>\n <box [width]=\"1\"></box>\n\n <button nz-button nzSize=\"small\" nz-tooltip=\"Template setting\" (click)=\"drawTemplateVisibel = true\">\n <nz-icon nzType=\"setting\"></nz-icon> T\n </button>\n\n <box [width]=\"1\"></box>\n <span class=\"divider\"></span>\n <box [width]=\"1\"></box>\n\n <button nz-button nzSize=\"small\" nzDanger nz-tooltip=\"Save\" (click)=\"save()\">\n <nz-icon nzType=\"save\"></nz-icon>\n </button>\n <button nz-button nzSize=\"small\" nz-tooltip=\"Add stage\" (click)=\"addStage()\">\n <nz-icon nzType=\"plus\" class=\"color-primary\"></nz-icon>\n </button>\n\n <box [width]=\"1\"></box>\n <span class=\"divider\"></span>\n <box [width]=\"1\"></box>\n\n <button\n nz-button\n nzSize=\"small\"\n [nzType]=\"isMoveMode ? 'primary' : 'default'\"\n nz-tooltip=\"Move mode\"\n (click)=\"isMoveMode = true\"\n >\n <nz-icon><img src=\"/assets/icon/hand-palm.png\" width=\"19\" height=\"19\" /></nz-icon>\n </button>\n <button\n nz-button\n nzSize=\"small\"\n [nzType]=\"!isMoveMode ? 'primary' : 'default'\"\n nz-tooltip=\"Select mode\"\n (click)=\"isMoveMode = false\"\n >\n <nz-icon><img src=\"/assets/icon/cursor.png\" width=\"19\" height=\"19\" /></nz-icon>\n </button>\n\n <box [width]=\"1\"></box>\n <span class=\"divider\"></span>\n <box [width]=\"1\"></box>\n\n <!-- UNDO / REDO -->\n <button\n nz-button\n nzSize=\"small\"\n nzType=\"default\"\n [disabled]=\"undoStack.length === 0\"\n nz-tooltip=\"Undo\"\n (click)=\"undo()\"\n >\n <nz-icon nzType=\"undo\"></nz-icon>\n </button>\n\n <button\n nz-button\n nzSize=\"small\"\n nzType=\"default\"\n [disabled]=\"redoStack.length === 0\"\n nz-tooltip=\"Redo\"\n (click)=\"redo()\"\n >\n <nz-icon nzType=\"redo\"></nz-icon>\n </button>\n\n <box [width]=\"1\"></box>\n <span class=\"divider\"></span>\n <box [width]=\"1\"></box>\n\n <!-- ALIGN -->\n <button nz-button nzSize=\"small\" [disabled]=\"lstSelectedStageCode.size < 2\" (click)=\"alignLeft()\">\u27F8</button>\n <button nz-button nzSize=\"small\" [disabled]=\"lstSelectedStageCode.size < 2\" (click)=\"alignCenter()\">\u2261</button>\n <button nz-button nzSize=\"small\" [disabled]=\"lstSelectedStageCode.size < 2\" (click)=\"alignRight()\">\u27F9</button>\n\n <span class=\"divider\"></span>\n\n <button nz-button nzSize=\"small\" [disabled]=\"lstSelectedStageCode.size < 2\" (click)=\"alignTop()\">\u21D1</button>\n <button nz-button nzSize=\"small\" [disabled]=\"lstSelectedStageCode.size < 2\" (click)=\"alignMiddle()\">\u2550</button>\n <button nz-button nzSize=\"small\" [disabled]=\"lstSelectedStageCode.size < 2\" (click)=\"alignBottom()\">\u21D3</button>\n\n <span class=\"divider\"></span>\n\n <!-- DISTRIBUTE -->\n <button nz-button nzSize=\"small\" [disabled]=\"lstSelectedStageCode.size < 3\" (click)=\"distributeHorizontal()\">\n \u21C4\n </button>\n <button nz-button nzSize=\"small\" [disabled]=\"lstSelectedStageCode.size < 3\" (click)=\"distributeVertical()\">\n \u21C5\n </button>\n </div>\n\n <div\n class=\"canvas-content\"\n [class.panning]=\"isMoveMode\"\n [style.transform]=\"'translate(' + panX + 'px,' + panY + 'px)'\"\n >\n <!-- GRID -->\n <div *ngIf=\"wfcSetting.ShowGrid\" class=\"grid-layer\"></div>\n\n <!-- <div class=\"zoom-toolbar\">\n <button nz-button nzSize=\"small\" (click)=\"zoomOut()\">\u2212</button>\n <span>{{ zoom * 100 | number: \"1.0-0\" }}%</span>\n <button nz-button nzSize=\"small\" (click)=\"zoomIn()\">+</button>\n </div> -->\n\n <!-- SVG EDGES -->\n <svg #svg class=\"edges-layer\" width=\"100%\" height=\"100%\" preserveAspectRatio=\"none\">\n <g [attr.transform]=\"svgTransform\">\n <!-- DEFS -->\n <defs>\n <!-- glow effect -->\n <filter id=\"edge-glow\" x=\"-50%\" y=\"-50%\" width=\"200%\" height=\"200%\">\n <feGaussianBlur stdDeviation=\"2\" result=\"blur\" />\n <feMerge>\n <feMergeNode in=\"blur\" />\n <feMergeNode in=\"SourceGraphic\" />\n </feMerge>\n </filter>\n\n <!-- arrow markers -->\n <marker\n id=\"arrow-blue\"\n markerWidth=\"10\"\n markerHeight=\"10\"\n refX=\"10\"\n refY=\"5\"\n orient=\"auto\"\n markerUnits=\"strokeWidth\"\n >\n <path class=\"blue\" d=\"M0,0 L10,5 L0,10\"></path>\n </marker>\n\n <marker\n id=\"arrow-red\"\n markerWidth=\"10\"\n markerHeight=\"10\"\n refX=\"10\"\n refY=\"5\"\n orient=\"auto\"\n markerUnits=\"strokeWidth\"\n >\n <path class=\"red\" d=\"M0,0 L10,5 L0,10\"></path>\n </marker>\n\n <marker\n id=\"arrow-blue-start\"\n markerWidth=\"10\"\n markerHeight=\"10\"\n viewBox=\"0 0 10 10\"\n refX=\"0\"\n refY=\"5\"\n orient=\"auto\"\n markerUnits=\"strokeWidth\"\n >\n <path class=\"blue\" d=\"M10,0 L0,5 L10,10\"></path>\n </marker>\n\n <marker\n id=\"arrow-red-start\"\n markerWidth=\"10\"\n markerHeight=\"10\"\n viewBox=\"0 0 10 10\"\n refX=\"0\"\n refY=\"5\"\n orient=\"auto\"\n markerUnits=\"strokeWidth\"\n >\n <path class=\"red\" d=\"M10,0 L0,5 L10,10\"></path>\n </marker>\n </defs>\n\n <!-- EDGES B\u00CCNH TH\u01AF\u1EDCNG -->\n <ng-container *ngFor=\"let e of normalEdges; let i = index\">\n <path\n class=\"edge-hit\"\n [attr.d]=\"buildPath(e)\"\n (mouseenter)=\"onMouseEnterEdge(e)\"\n (mouseleave)=\"onMouseLeaveEdge(e)\"\n (click)=\"onClickEdge(e, $event)\"\n ></path>\n\n <path\n class=\"edge\"\n [attr.id]=\"'edge-path-' + i\"\n [attr.d]=\"buildPath(e)\"\n marker-end=\"url(#arrow-blue)\"\n [attr.marker-start]=\"e.allowBack ? 'url(#arrow-blue-start)' : ''\"\n ></path>\n\n <!-- LABEL -->\n <text *ngIf=\"e.ActionText\" class=\"edge-label\" dy=\"-6\">\n <textPath [attr.href]=\"'#edge-path-' + i\" startOffset=\"50%\" text-anchor=\"middle\">\n {{ e.ActionText }}\n </textPath>\n </text>\n\n <text *ngIf=\"e.allowBack && e.labelBack\" class=\"edge-label\" dy=\"14\">\n <textPath [attr.href]=\"'#edge-path-' + i\" startOffset=\"50%\" text-anchor=\"middle\">\n {{ e.labelBack }}\n </textPath>\n </text>\n </ng-container>\n\n <!-- EDGE \u0110ANG HOVER (LU\u00D4N TR\u00CAN C\u00D9NG) -->\n <ng-container *ngFor=\"let e of hoverEdges; let i = index\">\n <path\n class=\"edge-hit\"\n [attr.d]=\"buildPath(e)\"\n (mouseleave)=\"onMouseLeaveEdge(e)\"\n (click)=\"onClickEdge(e, $event)\"\n ></path>\n\n <path\n class=\"edge red\"\n [attr.id]=\"'edge-path-hover-' + i\"\n [attr.d]=\"buildPath(e)\"\n marker-end=\"url(#arrow-red)\"\n [attr.marker-start]=\"e.allowBack ? 'url(#arrow-red-start)' : ''\"\n ></path>\n\n <!-- LABEL -->\n <text *ngIf=\"e.ActionText\" class=\"edge-label\" dy=\"-6\">\n <textPath [attr.href]=\"'#edge-path-hover-' + i\" startOffset=\"50%\" text-anchor=\"middle\">\n {{ e.ActionText }}\n </textPath>\n </text>\n\n <text *ngIf=\"e.allowBack && e.labelBack\" class=\"edge-label\" dy=\"14\">\n <textPath [attr.href]=\"'#edge-path-hover-' + i\" startOffset=\"50%\" text-anchor=\"middle\">\n {{ e.labelBack }}\n </textPath>\n </text>\n </ng-container>\n\n <!-- preview -->\n <path\n *ngIf=\"connectingFrom\"\n class=\"edge\"\n [attr.d]=\"buildPreviewPath()\"\n stroke-dasharray=\"5,5\"\n marker-end=\"url(#arrow-blue)\"\n ></path>\n </g>\n </svg>\n\n <div\n *ngIf=\"isSelecting && selectStart && selectEnd\"\n class=\"selection-box\"\n [style.left.px]=\"Math.min(selectStart.x, selectEnd.x)\"\n [style.top.px]=\"Math.min(selectStart.y, selectEnd.y)\"\n [style.width.px]=\"Math.abs(selectEnd.x - selectStart.x)\"\n [style.height.px]=\"Math.abs(selectEnd.y - selectStart.y)\"\n ></div>\n\n <!-- lstStage -->\n @for (n of lstStage; track $index) {\n @if (n.StageType == \"NODE\") {\n <nz-card\n #nodeEl\n class=\"workflow-node\"\n [attr.data-id]=\"n.Code\"\n [class.selected]=\"n == selectedStage || n == hoverStage || lstSelectedStageCode.has(n.Code)\"\n [style.left.px]=\"n.x\"\n [style.top.px]=\"n.y\"\n [class.connecting-source]=\"isConnectingFrom(n) || isDraggingFrom(n) || isSelectedNode(n)\"\n nzSize=\"small\"\n (mousedown)=\"onMouseDownNode($event, n)\"\n (click)=\"$event.stopPropagation(); onClickStage(n)\"\n >\n <div class=\"title\">{{ n.Code }}</div>\n\n <div>{{ n.Name }}</div>\n\n @if (!n.isReverse) {\n <!-- connector RIGHT: output -->\n <div class=\"connector connector-right\" (click)=\"onClickConnector($event, n, 'right')\"></div>\n\n <!-- connector LEFT: input -->\n <div class=\"connector connector-left\" (click)=\"onClickConnector($event, n, 'left')\"></div>\n } @else {\n <div class=\"connector connector-right reverse\" (click)=\"onClickConnector($event, n, 'right')\"></div>\n <div class=\"connector connector-left reverse\" (click)=\"onClickConnector($event, n, 'left')\"></div>\n }\n </nz-card>\n }\n <!-- START NODE -->\n @else if (n.StageType === \"START\") {\n <nz-card\n class=\"workflow-node workflow-node-start\"\n style=\"border: unset\"\n [style.left.px]=\"n.x\"\n [style.top.px]=\"n.y\"\n nzSize=\"small\"\n (mousedown)=\"onMouseDownNode($event, n)\"\n (click)=\"$event.stopPropagation(); onClickStage(n)\"\n >\n <div class=\"title\"> </div>\n\n <nz-tag\n [class.selected]=\"n == selectedStage || lstSelectedStageCode.has(n.Code)\"\n style=\"position: relative; top: -13px\"\n [style.right.px]=\"n.isReverseLabel ? -83 : 1\"\n [nzColor]=\"'green'\"\n >\n {{ n.StageType }}\n </nz-tag>\n\n <!-- connector RIGHT: output -->\n <div class=\"connector connector-right connector-start\" (click)=\"onClickConnector($event, n, 'right')\"></div>\n </nz-card>\n }\n\n <!-- END NODE -->\n @else if (n.StageType === \"END\") {\n <nz-card\n class=\"workflow-node workflow-node-end\"\n style=\"border: unset\"\n [style.left.px]=\"n.x\"\n [style.top.px]=\"n.y\"\n [class.connecting-source]=\"isConnectingFrom(n)\"\n nzSize=\"small\"\n (mousedown)=\"onMouseDownNode($event, n)\"\n (click)=\"$event.stopPropagation(); onClickStage(n)\"\n >\n <div class=\"title\"> </div>\n\n <nz-tag\n [class.selected]=\"n == selectedStage || lstSelectedStageCode.has(n.Code)\"\n style=\"position: relative; top: -13px\"\n [style.right.px]=\"n.isReverseLabel ? 65 : -5\"\n [nzColor]=\"'red'\"\n >\n {{ n.StageType }}\n </nz-tag>\n\n <!-- connector LEFT: input -->\n <div class=\"connector connector-end\" (click)=\"onClickConnector($event, n, 'left')\"></div>\n </nz-card>\n }\n }\n\n <!-- POINTS -->\n @for (p of connectingPoints; track $index) {\n @if (p != draggingPoint) {\n <div\n class=\"waypoint\"\n [style.left.px]=\"p.x\"\n [style.top.px]=\"p.y\"\n (mousedown)=\"onmousedownPoint($event, p)\"\n (contextmenu)=\"onRightClickPoint($event, hoverEdge!, $index)\"\n ></div>\n } @else {\n <div\n class=\"waypoint dragging\"\n [style.left.px]=\"p.x\"\n [style.top.px]=\"p.y\"\n (mousedown)=\"onmousedownPoint($event, p)\"\n ></div>\n }\n }\n </div>\n</div>\n\n<!-- TEMPLATE -->\n<nz-drawer\n [nzTitle]=\"drawTemplateTitle\"\n nzPlacement=\"right\"\n [nzWidth]=\"500\"\n [nzClosable]=\"false\"\n [nzVisible]=\"drawTemplateVisibel\"\n [nzFooter]=\"footerTplTemplate\"\n (nzOnClose)=\"drawTemplateVisibel = false\"\n>\n <ng-template #drawTemplateTitle> <nz-icon nzType=\"setting\"></nz-icon> Template </ng-template>\n <ng-container *nzDrawerContent>\n <extend-input [label]=\"'Code'\" [(_ngModel)]=\"template.Code\"></extend-input>\n <extend-input [label]=\"'Name'\" [(_ngModel)]=\"template.Name\"></extend-input>\n <extend-select\n [label]=\"'Type'\"\n [lstItem]=\"lstTemplateType\"\n [valueField]=\"'Code'\"\n [displayFields]=\"['Code', 'Name']\"\n [(_ngModel)]=\"template.DocType\"\n ></extend-select>\n <extend-textarea [label]=\"'Desscription'\" [(_ngModel)]=\"template.Description\"></extend-textarea>\n <extend-select\n [label]=\"'Print template'\"\n [lstItem]=\"lstTemplatePrint\"\n [valueField]=\"'Code'\"\n [displayFields]=\"['Code', 'Name']\"\n [(_ngModel)]=\"template.TemplatePrint\"\n ></extend-select>\n <extend-checkbox [label]=\"'Active'\" [(_ngModel)]=\"template.IsActive\"></extend-checkbox>\n\n <h3 nz-col><nz-icon nzType=\"send\" nzTheme=\"outline\" /> Stage status</h3>\n\n @for (tag of template.lstStageStatus; track tag) {\n <nz-tag\n [nzMode]=\"checkRemoveableStageStatus(tag) ? 'closeable' : 'default'\"\n (nzOnClose)=\"handleCloseStageStatus(tag)\"\n >\n {{ sliceTagName(tag.Code + (tag.Name ? \" - \" + tag.Name : \"\")) }}\n </nz-tag>\n }\n\n <br *ngIf=\"template.lstStageStatus && template.lstStageStatus.length\" />\n\n @if (!inputStageStatusVisible) {\n <nz-tag class=\"editable-tag\" nzNoAnimation (click)=\"showInputStageStatus()\">\n <nz-icon nzType=\"plus\" />\n New Action type\n </nz-tag>\n } @else {\n <input\n #inputStageStatusElement\n nz-input\n nzSize=\"small\"\n type=\"text\"\n style=\"width: 150px\"\n placeholder=\"Code\"\n [(ngModel)]=\"inputStageStatusCode\"\n (keydown.enter)=\"inputStageStatusNameElement.select()\"\n />\n <input\n #inputStageStatusNameElement\n nz-input\n nzSize=\"small\"\n type=\"text\"\n style=\"width: 150px\"\n placeholder=\"Name\"\n [(ngModel)]=\"inputStageStatusName\"\n (blur)=\"handleInputStageStatusConfirm()\"\n (keydown.enter)=\"handleInputStageStatusConfirm()\"\n />\n }\n\n <box [height]=\"16\"></box>\n\n <h3 nz-col><nz-icon nzType=\"send\" nzTheme=\"outline\" /> Action type</h3>\n\n @for (tag of template.lstActionType; track tag) {\n <nz-tag\n [nzMode]=\"checkRemoveableActionType(tag) ? 'closeable' : 'default'\"\n (nzOnClose)=\"handleCloseActionType(tag)\"\n >\n {{ sliceTagName(tag.Code + (tag.Name ? \" - \" + tag.Name : \"\")) }}\n </nz-tag>\n }\n\n <br *ngIf=\"template.lstActionType && template.lstActionType.length\" />\n\n @if (!inputActionTypeVisible) {\n <nz-tag class=\"editable-tag\" nzNoAnimation (click)=\"showInputActionType()\">\n <nz-icon nzType=\"plus\" />\n New Action type\n </nz-tag>\n } @else {\n <input\n #inputActionTypeElement\n nz-input\n nzSize=\"small\"\n type=\"text\"\n style=\"width: 150px\"\n placeholder=\"Code\"\n [(ngModel)]=\"inputActionTypeCode\"\n (keydown.enter)=\"inputActionTypeNameElement.select()\"\n />\n <input\n #inputActionTypeNameElement\n nz-input\n nzSize=\"small\"\n type=\"text\"\n style=\"width: 150px\"\n placeholder=\"Name\"\n [(ngModel)]=\"inputActionTypeName\"\n (blur)=\"handleInputActionTypeConfirm()\"\n (keydown.enter)=\"handleInputActionTypeConfirm()\"\n />\n }\n\n <box [height]=\"16\"></box>\n\n <h3 nz-col><nz-icon nzType=\"send\" nzTheme=\"outline\" /> Action status</h3>\n\n @for (tag of template.lstActionStatus; track tag) {\n <nz-tag [nzMode]=\"checkRemoveableActionStatus(tag) ? 'closeable' : 'default'\" (nzOnClose)=\"handleClose(tag)\">\n {{ sliceTagName(tag.Code + (tag.Name ? \" - \" + tag.Name : \"\")) }}\n </nz-tag>\n }\n\n <br *ngIf=\"template.lstActionStatus && template.lstActionStatus.length\" />\n\n @if (!inputVisible) {\n <nz-tag class=\"editable-tag\" nzNoAnimation (click)=\"showInput()\">\n <nz-icon nzType=\"plus\" />\n New Action status\n </nz-tag>\n } @else {\n <input\n #inputElement\n nz-input\n nzSize=\"small\"\n type=\"text\"\n style=\"width: 150px\"\n placeholder=\"+ New Action status\"\n [(ngModel)]=\"inputValue\"\n (blur)=\"handleInputConfirm()\"\n (keydown.enter)=\"handleInputConfirm()\"\n />\n }\n </ng-container>\n\n <ng-template #footerTplTemplate>\n <div nz-flex [nzGap]=\"6\">\n <button nz-button (click)=\"drawTemplateVisibel = false\">Close</button>\n <button nz-button nzDanger (click)=\"save()\">Save</button>\n </div>\n </ng-template>\n</nz-drawer>\n\n<!-- STAGE -->\n<nz-drawer\n [nzTitle]=\"drawerTitleStage\"\n [nzPlacement]=\"selectedStage?.drawerPosition ? selectedStage?.drawerPosition : 'right'\"\n [nzVisible]=\"!!selectedStage\"\n [nzWidth]=\"750\"\n [nzClosable]=\"false\"\n [nzFooter]=\"footerTplNode\"\n (nzOnClose)=\"selectedStage = undefined\"\n>\n <ng-template #drawerTitleStage>\n <div nz-row nzJustify=\"space-between\">\n @if (selectedStage) {\n @if (selectedStage.drawerPosition == \"left\") {\n <button nz-button nzSize=\"small\" nz-tooltip=\"To the right\" (click)=\"selectedStage.drawerPosition = 'right'\">\n <nz-icon nzType=\"double-right\"></nz-icon>\n </button>\n STAGE\n } @else {\n STAGE\n <button nz-button nzSize=\"small\" nz-tooltip=\"To the left\" (click)=\"selectedStage.drawerPosition = 'left'\">\n <nz-icon nzType=\"double-left\"></nz-icon>\n </button>\n }\n }\n </div>\n </ng-template>\n <ng-container *nzDrawerContent>\n <ng-container *ngIf=\"selectedStage\">\n <nz-radio-group [(ngModel)]=\"selectedStage.StageType\">\n <label nz-radio nzValue=\"START\" [nzDisabled]=\"true\">START</label>\n <label nz-radio nzValue=\"NODE\" [nzDisabled]=\"true\">NODE</label>\n <label nz-radio nzValue=\"END\" [nzDisabled]=\"true\">END</label>\n </nz-radio-group>\n\n <box [height]=\"16\"></box>\n\n <div nz-row [nzGutter]=\"32\">\n <extend-input\n nz-col\n [nzSpan]=\"8\"\n [label]=\"'Code'\"\n [layOutType]=\"'vertical'\"\n [disabled]=\"['START', 'END'].indexOf(selectedStage!.StageType) >= 0\"\n [required]=\"true\"\n [(_ngModel)]=\"selectedStage.Code\"\n ></extend-input>\n\n <extend-textarea\n nz-col\n [nzSpan]=\"16\"\n [label]=\"'Name'\"\n [layOutType]=\"'vertical'\"\n [autoSize]=\"true\"\n [disabled]=\"['START', 'END'].indexOf(selectedStage!.StageType) >= 0\"\n [required]=\"true\"\n [(_ngModel)]=\"selectedStage.Name\"\n ></extend-textarea>\n </div>\n\n <div nz-row [nzGutter]=\"32\">\n <extend-input-number\n nz-col\n [nzSpan]=\"8\"\n [label]=\"'Sequence'\"\n [layOutType]=\"'vertical'\"\n [(_ngModel)]=\"selectedStage.SeqValue\"\n ></extend-input-number>\n\n <extend-select\n nz-col\n [nzSpan]=\"16\"\n [label]=\"'Org implement'\"\n [layOutType]=\"'vertical'\"\n [lstItem]=\"lstOrg\"\n [valueField]=\"'Code'\"\n [displayFields]=\"['Code', 'Name']\"\n [(_ngModel)]=\"selectedStage.App_Org_Id\"\n ></extend-select>\n </div>\n\n @if ([\"START\", \"END\"].indexOf(selectedStage.StageType) < 0) {\n <extend-checkbox [label]=\"'Reverse'\" [labelSpan]=\"0\" [(_ngModel)]=\"selectedStage.isReverse\"></extend-checkbox>\n\n <extend-checkbox\n [label]=\"'Require user action'\"\n [labelSpan]=\"0\"\n [(_ngModel)]=\"selectedStage.IsRequireUser\"\n ></extend-checkbox>\n } @else {\n <extend-checkbox\n [label]=\"'Reverse'\"\n [labelSpan]=\"0\"\n [(_ngModel)]=\"selectedStage.isReverseLabel\"\n ></extend-checkbox>\n }\n\n <box [height]=\"16\"></box>\n\n <div nz-row [nzGutter]=\"16\">\n <h3 nz-col><nz-icon nzType=\"send\" nzTheme=\"outline\" /> List Action</h3>\n <div nz-col>\n <button nz-button nzType=\"primary\" nzSize=\"small\" (click)=\"addAction()\">\n <nz-icon nzType=\"plus\"></nz-icon>\n </button>\n </div>\n </div>\n\n <nz-table\n nzSize=\"small\"\n [nzData]=\"selectedStage.lstAction || []\"\n [nzShowPagination]=\"false\"\n [nzNoResult]=\"' '\"\n >\n <thead>\n <tr [hidden]=\"true\">\n <th nzWidth=\"50px\"></th>\n <th></th>\n </tr>\n </thead>\n <tbody>\n @for (action of selectedStage.lstAction; track $index) {\n <tr>\n <td>\n <nz-icon\n nzType=\"delete\"\n nzTheme=\"outline\"\n class=\"color-warn cursor-pointer icon-size-16\"\n (click)=\"$event.stopPropagation(); deleteAction(action)\"\n ></nz-icon>\n </td>\n <td (click)=\"hoverEdge = action\">\n <div nz-row [nzGutter]=\"16\" class=\"form-item-no-bottom\">\n <extend-input\n nz-col\n [nzSpan]=\"8\"\n [label]=\"'Action text'\"\n [layOutType]=\"'vertical'\"\n [(_ngModel)]=\"action.ActionText\"\n (_ngModelChange)=\"onchangeActionText(action)\"\n ></extend-input>\n <extend-select\n nz-col\n [nzSpan]=\"8\"\n [label]=\"'Action type'\"\n [layOutType]=\"'vertical'\"\n [lstItem]=\"template.lstActionType\"\n [valueField]=\"'Code'\"\n [displayFields]=\"['Code', 'Name']\"\n [(_ngModel)]=\"action.ActionType\"\n ></extend-select>\n <extend-select\n nz-col\n [nzSpan]=\"8\"\n [label]=\"'Action status'\"\n [layOutType]=\"'vertical'\"\n [lstItem]=\"template.lstActionStatus\"\n [valueField]=\"'Code'\"\n [displayFields]=\"['Code', 'Name']\"\n [(_ngModel)]=\"action.ActionStatus\"\n ></extend-select>\n\n <extend-select\n nz-col\n [nzSpan]=\"8\"\n [label]=\"'Stage status'\"\n [layOutType]=\"'vertical'\"\n [lstItem]=\"template.lstStageStatus\"\n [valueField]=\"'Code'\"\n [displayFields]=\"['Code', 'Name']\"\n [(_ngModel)]=\"action.StageStatus\"\n ></extend-select>\n\n <extend-select\n nz-col\n [nzSpan]=\"8\"\n [label]=\"'Next stage'\"\n [layOutType]=\"'vertical'\"\n [lstItem]=\"lstStage\"\n [valueField]=\"'Code'\"\n [displayFields]=\"['Code', 'Name']\"\n [(_ngModel)]=\"action.ToStage\"\n (_ngModelChange)=\"onchangeNextStage(action)\"\n ></extend-select>\n\n <extend-select\n nz-col\n [nzSpan]=\"8\"\n [label]=\"'Next stage status'\"\n [layOutType]=\"'vertical'\"\n [lstItem]=\"template.lstStageStatus\"\n [valueField]=\"'Code'\"\n [displayFields]=\"['Code', 'Name']\"\n [(_ngModel)]=\"action.NextStageStatus\"\n ></extend-select>\n\n <nz-divider\n *ngIf=\"selectedStage.lstAction && $index + 1 < selectedStage.lstAction.length\"\n style=\"margin: 16px 0px 8px 0px; background-color: orange\"\n ></nz-divider>\n </div>\n </td>\n </tr>\n }\n </tbody>\n </nz-table>\n\n <box [height]=\"16\"></box>\n </ng-container>\n </ng-container>\n\n <ng-template #footerTplNode>\n <div nz-flex [nzGap]=\"6\" [nzJustify]=\"selectedStage && selectedStage.drawerPosition == 'left' ? 'end' : 'start'\">\n <button nz-button (click)=\"selectedStage = undefined\">Close</button>\n <button\n *ngIf=\"selectedStage && ['START', 'END'].indexOf(selectedStage.StageType) < 0\"\n nz-button\n nzDanger\n (click)=\"deleteNode()\"\n >\n Delete\n </button>\n </div>\n </ng-template>\n</nz-drawer>\n\n<!-- EDGE -->\n<nz-drawer\n nzTitle=\"ACTION\"\n nzPlacement=\"right\"\n [nzClosable]=\"false\"\n [nzWidth]=\"750\"\n [nzVisible]=\"!!selectedAction\"\n (nzOnClose)=\"selectedAction = undefined\"\n>\n <ng-container *nzDrawerContent>\n <ng-container *ngIf=\"selectedAction\">\n <strong>{{ selectedAction.FromStage }} -> {{ selectedAction.ToStage }}</strong>\n\n <box [height]=\"16\"></box>\n\n <div nz-row [nzGutter]=\"16\" class=\"form-item-no-bottom\">\n <extend-input\n nz-col\n [nzSpan]=\"8\"\n [label]=\"'Action text'\"\n [layOutType]=\"'vertical'\"\n [(_ngModel)]=\"selectedAction.ActionText\"\n (_ngModelChange)=\"onchangeActionText(selectedAction)\"\n ></extend-input>\n <extend-select\n nz-col\n [nzSpan]=\"8\"\n [label]=\"'Action type'\"\n [layOutType]=\"'vertical'\"\n [lstItem]=\"template.lstActionType\"\n [valueField]=\"'Code'\"\n [displayFields]=\"['Code', 'Name']\"\n [(_ngModel)]=\"selectedAction.ActionType\"\n ></extend-select>\n <extend-select\n nz-col\n [nzSpan]=\"8\"\n [label]=\"'Action status'\"\n [layOutType]=\"'vertical'\"\n [lstItem]=\"template.lstActionStatus\"\n [valueField]=\"'Code'\"\n [displayFields]=\"['Code', 'Name']\"\n [(_ngModel)]=\"selectedAction.ActionStatus\"\n ></extend-select>\n\n <extend-select\n nz-col\n [nzSpan]=\"8\"\n [label]=\"'Stage status'\"\n [layOutType]=\"'vertical'\"\n [lstItem]=\"template.lstStageStatus\"\n [valueField]=\"'Code'\"\n [displayFields]=\"['Code', 'Name']\"\n [(_ngModel)]=\"selectedAction.StageStatus\"\n ></extend-select>\n\n <extend-select\n nz-col\n [nzSpan]=\"8\"\n [label]=\"'Next stage'\"\n [layOutType]=\"'vertical'\"\n [lstItem]=\"lstStage\"\n [valueField]=\"'Code'\"\n [displayFields]=\"['Code', 'Name']\"\n [(_ngModel)]=\"selectedAction.ToStage\"\n (_ngModelChange)=\"onchangeNextStage(selectedAction)\"\n ></extend-select>\n\n <extend-select\n nz-col\n [nzSpan]=\"8\"\n [label]=\"'Next stage status'\"\n [layOutType]=\"'vertical'\"\n [lstItem]=\"template.lstStageStatus\"\n [valueField]=\"'Code'\"\n [displayFields]=\"['Code', 'Name']\"\n [(_ngModel)]=\"selectedAction.NextStageStatus\"\n ></extend-select>\n </div>\n\n <!-- <box [height]=\"16\"></box> -->\n <nz-divider></nz-divider>\n\n <extend-checkbox\n [label]=\"'Allow back'\"\n [labelSpan]=\"0\"\n [(_ngModel)]=\"selectedAction.allowBack\"\n (_ngModelChange)=\"onchangeAllowBack(selectedAction)\"\n ></extend-checkbox>\n\n <box [height]=\"16\"></box>\n\n @if (selectedAction.allowBack) {\n <strong>{{ selectedAction.ToStage }} -> {{ selectedAction.FromStage }}</strong>\n\n @if (selectedBackAction) {\n <div nz-row [nzGutter]=\"16\" class=\"form-item-no-bottom\">\n <extend-input\n nz-col\n [nzSpan]=\"8\"\n [label]=\"'Action text'\"\n [layOutType]=\"'vertical'\"\n [(_ngModel)]=\"selectedBackAction.ActionText\"\n (_ngModelChange)=\"onchangeActionText(selectedBackAction)\"\n ></extend-input>\n <extend-select\n nz-col\n [nzSpan]=\"8\"\n [label]=\"'Action type'\"\n [layOutType]=\"'vertical'\"\n [lstItem]=\"template.lstActionType\"\n [valueField]=\"'Code'\"\n [displayFields]=\"['Code', 'Name']\"\n [(_ngModel)]=\"selectedBackAction.ActionType\"\n ></extend-select>\n <extend-select\n nz-col\n [nzSpan]=\"8\"\n [label]=\"'Action status'\"\n [layOutType]=\"'vertical'\"\n [lstItem]=\"template.lstActionStatus\"\n [valueField]=\"'Code'\"\n [displayFields]=\"['Code', 'Name']\"\n [(_ngModel)]=\"selectedBackAction.ActionStatus\"\n ></extend-select>\n\n <extend-select\n nz-col\n [nzSpan]=\"8\"\n [label]=\"'Stage status'\"\n [layOutType]=\"'vertical'\"\n [lstItem]=\"template.lstStageStatus\"\n [valueField]=\"'Code'\"\n [displayFields]=\"['Code', 'Name']\"\n [(_ngModel)]=\"selectedBackAction.StageStatus\"\n ></extend-select>\n\n <extend-select\n nz-col\n [nzSpan]=\"8\"\n [label]=\"'Next stage'\"\n [layOutType]=\"'vertical'\"\n [lstItem]=\"lstStage\"\n [valueField]=\"'Code'\"\n [displayFields]=\"['Code', 'Name']\"\n [(_ngModel)]=\"selectedBackAction.ToStage\"\n (_ngModelChange)=\"onchangeNextStage(selectedBackAction)\"\n ></extend-select>\n\n <extend-select\n nz-col\n [nzSpan]=\"8\"\n [label]=\"'Next stage status'\"\n [layOutType]=\"'vertical'\"\n [lstItem]=\"template.lstStageStatus\"\n [valueField]=\"'Code'\"\n [displayFields]=\"['Code', 'Name']\"\n [(_ngModel)]=\"selectedBackAction.NextStageStatus\"\n ></extend-select>\n </div>\n }\n }\n </ng-container>\n </ng-container>\n</nz-drawer>\n\n<!-- setting editor -->\n<nz-drawer\n [nzTitle]=\"drawSettingTitle\"\n nzPlacement=\"right\"\n [nzClosable]=\"false\"\n [nzVisible]=\"settingVisible\"\n (nzOnClose)=\"settingVisible = false\"\n>\n <ng-template #drawSettingTitle> <nz-icon nzType=\"setting\"></nz-icon> Editor </ng-template>\n <ng-container *nzDrawerContent>\n <div class=\"form-item-no-bottom\">\n <extend-checkbox [label]=\"'Show grid'\" [labelSpan]=\"0\" [(_ngModel)]=\"wfcSetting.ShowGrid\"></extend-checkbox>\n\n <box [height]=\"16\"></box>\n\n <extend-input-number [label]=\"'Canvas width'\" [(_ngModel)]=\"CANVAS_WIDTH\"></extend-input-number>\n <box [height]=\"16\"></box>\n <extend-input-number [label]=\"'Canvas height'\" [(_ngModel)]=\"CANVAS_HEIGHT\"></extend-input-number>\n </div>\n </ng-container>\n</nz-drawer>\n", styles: ["@charset \"UTF-8\";.workflow-canvas{position:relative;width:100%;height:600px;background:#f0f2f5;cursor:default;overflow:hidden;outline:1px dashed rgba(0,0,0,.1)}.workflow-canvas .edges-layer{position:absolute;inset:0;width:100%;height:100%;z-index:1}.workflow-canvas .edges-layer path{stroke-width:2;fill:none}.canvas-content{transform-origin:0 0;width:10000px;height:10000px}.canvas-content{outline:1px dashed red}.canvas-content.panning{cursor:grab}.canvas-content.panning:active{cursor:grabbing}.workflow-canvas.panning{cursor:grab}.workflow-canvas.panning:active{cursor:grabbing}.workflow-canvas.connecting,.workflow-canvas.connecting .workflow-node{cursor:crosshair}.workflow-node{position:absolute;width:160px;cursor:grab;-webkit-user-select:none;user-select:none;z-index:2}.workflow-node:active{cursor:grabbing}.workflow-node .title{font-weight:600;margin-bottom:6px}.workflow-node.connecting-source,.workflow-node.selected,.workflow-node:hover:not(.workflow-node-start,.workflow-node-end){outline:2px dashed #1890ff}.workflow-node-start{background-color:unset;display:flex;justify-content:right}.workflow-node-end{background-color:unset;display:flex}.connector{position:absolute;top:39px;width:12px;height:12px;background:#1890ff;border-radius:50%;transform:translateY(-50%);cursor:crosshair;transition:box-shadow .15s ease-out,transform .15s ease-out}.connector:hover{box-shadow:0 0 0 3px #1890ff4d;transform:translateY(-50%) scale(1.15)}.connector-right{right:-8px;background:orange}.connector-right.reverse,.connector-left{left:-8px}.connector-left.reverse{left:unset;right:-8px}.connector-start{width:16px;height:16px;right:-3px;background:green;box-shadow:0 0 0 3px #1890ff4d}.connector-end{width:16px;height:16px;left:-6px;background:#000;box-shadow:0 0 0 3px #1890ff4d}.edge{cursor:pointer;stroke:#1890ff;stroke-linecap:round;stroke-linejoin:round;stroke-width:2;fill:none;transition:stroke .15s ease}.edge-hit{stroke:transparent;stroke-width:18px!important;fill:none;cursor:pointer}.edge-hit:hover+.edge{stroke:#ff4d4f;stroke-width:2}.workflow-canvas.dragging-point .edge-hit{pointer-events:none}.edge:hover+.edge-label{fill:#1890ff;font-weight:500}.edge-label{font-size:12px;fill:#555;pointer-events:none;-webkit-user-select:none;user-select:none}.blue{stroke:#1890ff!important}.red{stroke:#ff4d4f!important;box-shadow:0 0 0 3px #1890ff4d!important}.workflow-svg{background:transparent;overflow:visible}.waypoint{position:absolute;width:10px;height:10px;background:#ff4d4f;border-radius:50%;transform:translate(-50%,-50%) scale(1);cursor:move;z-index:1;transition:transform .12s ease-out,background-color .12s ease-out,box-shadow .12s ease-out}.waypoint:hover{background:#ff7875;transform:translate(-50%,-50%) scale(1.25);box-shadow:0 0 0 4px #ff787559}.waypoint.dragging{transition:none;transform:translate(-50%,-50%) scale(1.15);pointer-events:none}.workflow-canvas.dragging-point svg{cursor:grabbing}.selection-box{position:absolute;border:1px dashed #1890ff;background:#1890ff1a;pointer-events:none}.toolbar{position:absolute;top:8px;left:8px;display:flex;gap:4px;padding:6px;background:#fff;border-radius:6px;box-shadow:0 2px 8px #00000026;z-index:100}.toolbar .divider{width:1px;background:#e5e5e5;margin:0 2px}.zoom-toolbar{position:absolute;top:8px;left:8px;z-index:10;display:flex;align-items:center;gap:6px;background:#fff;padding:4px 6px;border-radius:6px;box-shadow:0 2px 6px #00000026}.zoom-toolbar span{min-width:40px;text-align:center;font-size:12px}nz-tag.selected{outline:2px dashed #1890ff}.grid-layer{position:absolute;inset:0;pointer-events:none;z-index:0;background-image:radial-gradient(rgba(0,0,0,.08) 1px,transparent 1px);background-size:20px 20px}.grid-layer{background-image:linear-gradient(to right,rgba(0,0,0,.06) 1px,transparent 1px),linear-gradient(to bottom,rgba(0,0,0,.06) 1px,transparent 1px);background-size:20px 20px}.editable-tag{background:#fff;border-style:dashed}\n"] }]
|
|
4202
|
+
}], propDecorators: { lstOrg: [{
|
|
4203
|
+
type: Input
|
|
4204
|
+
}], lstTemplateType: [{
|
|
4205
|
+
type: Input
|
|
4206
|
+
}], lstTemplatePrint: [{
|
|
4207
|
+
type: Input
|
|
4208
|
+
}], template: [{
|
|
4209
|
+
type: Input
|
|
4210
|
+
}], lstStage: [{
|
|
4211
|
+
type: Input
|
|
4212
|
+
}], lstAction: [{
|
|
4213
|
+
type: Input
|
|
4214
|
+
}], onSave: [{
|
|
4215
|
+
type: Output,
|
|
4216
|
+
args: ['onSave']
|
|
4217
|
+
}], canvasRef: [{
|
|
4218
|
+
type: ViewChild,
|
|
4219
|
+
args: ['canvas', { static: true }]
|
|
4220
|
+
}], svgRef: [{
|
|
4221
|
+
type: ViewChild,
|
|
4222
|
+
args: ['svg', { static: true }]
|
|
4223
|
+
}], onKeyDown: [{
|
|
4224
|
+
type: HostListener,
|
|
4225
|
+
args: ['window:keydown', ['$event']]
|
|
4226
|
+
}], inputStageStatusElement: [{
|
|
4227
|
+
type: ViewChild,
|
|
4228
|
+
args: ['inputStageStatusElement', { static: false }]
|
|
4229
|
+
}], inputActionTypeElement: [{
|
|
4230
|
+
type: ViewChild,
|
|
4231
|
+
args: ['inputActionTypeElement', { static: false }]
|
|
4232
|
+
}], inputElement: [{
|
|
4233
|
+
type: ViewChild,
|
|
4234
|
+
args: ['inputElement', { static: false }]
|
|
4235
|
+
}] } });
|
|
4236
|
+
|
|
2685
4237
|
class AutoFocusDirective {
|
|
2686
4238
|
el;
|
|
2687
4239
|
renderer;
|
|
@@ -3202,7 +4754,7 @@ class RouteMonitorService {
|
|
|
3202
4754
|
this.router.events.subscribe(async (event) => {
|
|
3203
4755
|
if (event instanceof NavigationEnd) {
|
|
3204
4756
|
const url = event.urlAfterRedirects;
|
|
3205
|
-
if (url
|
|
4757
|
+
if (url == '/' || url.startsWith('/login') || url == '/no-permission') {
|
|
3206
4758
|
return;
|
|
3207
4759
|
}
|
|
3208
4760
|
const hasMenu = await this.authService.checkUserMenu(url);
|
|
@@ -3329,5 +4881,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.1.7", ngImpor
|
|
|
3329
4881
|
* Generated bundle index. Do not edit.
|
|
3330
4882
|
*/
|
|
3331
4883
|
|
|
3332
|
-
export { AnimatedDigitComponent, AppGlobals, AppStorage, AuthGuard, AutoFocusDirective, BarGraphComponent, BaseComponent, BaseGuardChangeComponent, BaseOverlayComponent, Box, Breadcrumb, CheckModel, CommonService, DashcardComponent, DateInputParserDirective, DateTimeHelper, DoughnutGraphComponent, DownloadHelper, ENUM_ResponseType, ExtendCheckbox, ExtendDatePicker, ExtendDateRangePicker, ExtendInput, ExtendInputNumber, ExtendSelectComponent, ExtendTextArea, GlobalSpinnerComponent, GridFilter, HTTPService, Height, LimitWordsPipe, LineGraphComponent, LoadingService, NoPermission, NotiService, NullIfEmptyDirective, NumberOnlyDirective, OrderOption, PagingData, PagingModel, PdfViewerComponent, RouteMonitorService, TableHeader, ThemeService, TokenStorage, TranslateKey, UnsavedChangesGuard, UpperCaseFirsLetterEachWordDirective, UppercaseDirective, UppercaseFirstLetterDirective, VscService, Width, XLSXHelper, authInterceptor };
|
|
4884
|
+
export { AnimatedDigitComponent, AppGlobals, AppStorage, AuthGuard, AutoFocusDirective, BarGraphComponent, BaseComponent, BaseGuardChangeComponent, BaseOverlayComponent, Box, Breadcrumb, CheckModel, CommonService, DashcardComponent, DateInputParserDirective, DateTimeHelper, DoughnutGraphComponent, DownloadHelper, ENUM_ResponseType, ExtendCheckbox, ExtendDatePicker, ExtendDateRangePicker, ExtendInput, ExtendInputNumber, ExtendSelectComponent, ExtendTextArea, GlobalSpinnerComponent, GridFilter, HTTPService, Height, LimitWordsPipe, LineGraphComponent, LoadingService, NoPermission, NotiService, NullIfEmptyDirective, NumberOnlyDirective, OrderOption, PagingData, PagingModel, PdfViewerComponent, RouteMonitorService, TableHeader, ThemeService, TokenStorage, TranslateKey, UnsavedChangesGuard, UpperCaseFirsLetterEachWordDirective, UppercaseDirective, UppercaseFirstLetterDirective, VscService, Width, WorkflowEditorComponent, XLSXHelper, authInterceptor };
|
|
3333
4885
|
//# sourceMappingURL=brggroup-share-lib.mjs.map
|