@acorex-platform/module-platform-management-client 0.0.21
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/acorex-platform-module-platform-management-client-acorex-platform-module-platform-management-client-V_ao4WVM.mjs +4599 -0
- package/fesm2022/acorex-platform-module-platform-management-client-acorex-platform-module-platform-management-client-V_ao4WVM.mjs.map +1 -0
- package/fesm2022/acorex-platform-module-platform-management-client-contracts.mjs +179 -0
- package/fesm2022/acorex-platform-module-platform-management-client-contracts.mjs.map +1 -0
- package/fesm2022/acorex-platform-module-platform-management-client-create-user-query.command-2Qu7aoCp.mjs +1542 -0
- package/fesm2022/acorex-platform-module-platform-management-client-create-user-query.command-2Qu7aoCp.mjs.map +1 -0
- package/fesm2022/acorex-platform-module-platform-management-client-create-user-query.types-DdygAori.mjs +71 -0
- package/fesm2022/acorex-platform-module-platform-management-client-create-user-query.types-DdygAori.mjs.map +1 -0
- package/fesm2022/acorex-platform-module-platform-management-client-customize-menu.command-2319iHsD.mjs +231 -0
- package/fesm2022/acorex-platform-module-platform-management-client-customize-menu.command-2319iHsD.mjs.map +1 -0
- package/fesm2022/acorex-platform-module-platform-management-client-data-list-pick-entity-item.command-CJVpqPmY.mjs +52 -0
- package/fesm2022/acorex-platform-module-platform-management-client-data-list-pick-entity-item.command-CJVpqPmY.mjs.map +1 -0
- package/fesm2022/acorex-platform-module-platform-management-client-entity-master-actions-for-ai.util-DmGyoy9o.mjs +55 -0
- package/fesm2022/acorex-platform-module-platform-management-client-entity-master-actions-for-ai.util-DmGyoy9o.mjs.map +1 -0
- package/fesm2022/acorex-platform-module-platform-management-client-execute-token.command-DNuHR-0y.mjs +152 -0
- package/fesm2022/acorex-platform-module-platform-management-client-execute-token.command-DNuHR-0y.mjs.map +1 -0
- package/fesm2022/acorex-platform-module-platform-management-client-list-catalog-scopes.query-ByhDTWE6.mjs +71 -0
- package/fesm2022/acorex-platform-module-platform-management-client-list-catalog-scopes.query-ByhDTWE6.mjs.map +1 -0
- package/fesm2022/acorex-platform-module-platform-management-client-list-editor-widgets.query-DITPMmbw.mjs +68 -0
- package/fesm2022/acorex-platform-module-platform-management-client-list-editor-widgets.query-DITPMmbw.mjs.map +1 -0
- package/fesm2022/acorex-platform-module-platform-management-client-list-platform-navigation-for-ai.query-2oHMITw-.mjs +194 -0
- package/fesm2022/acorex-platform-module-platform-management-client-list-platform-navigation-for-ai.query-2oHMITw-.mjs.map +1 -0
- package/fesm2022/acorex-platform-module-platform-management-client-list-registered-entities.query-DjStd3a8.mjs +136 -0
- package/fesm2022/acorex-platform-module-platform-management-client-list-registered-entities.query-DjStd3a8.mjs.map +1 -0
- package/fesm2022/acorex-platform-module-platform-management-client-list-registered-queries.query-C-IfkTel.mjs +122 -0
- package/fesm2022/acorex-platform-module-platform-management-client-list-registered-queries.query-C-IfkTel.mjs.map +1 -0
- package/fesm2022/acorex-platform-module-platform-management-client-menu-list.component-C5HH5-Dw.mjs +524 -0
- package/fesm2022/acorex-platform-module-platform-management-client-menu-list.component-C5HH5-Dw.mjs.map +1 -0
- package/fesm2022/acorex-platform-module-platform-management-client-platform-console-charts.queries-CQ34R13-.mjs +216 -0
- package/fesm2022/acorex-platform-module-platform-management-client-platform-console-charts.queries-CQ34R13-.mjs.map +1 -0
- package/fesm2022/acorex-platform-module-platform-management-client-platform-console-data.util-BIkULtKk.mjs +81 -0
- package/fesm2022/acorex-platform-module-platform-management-client-platform-console-data.util-BIkULtKk.mjs.map +1 -0
- package/fesm2022/acorex-platform-module-platform-management-client-platform-console-kpi.queries-BkPdhVq_.mjs +137 -0
- package/fesm2022/acorex-platform-module-platform-management-client-platform-console-kpi.queries-BkPdhVq_.mjs.map +1 -0
- package/fesm2022/acorex-platform-module-platform-management-client-rest-api-generator-widget-column.component-CYb2zOSj.mjs +45 -0
- package/fesm2022/acorex-platform-module-platform-management-client-rest-api-generator-widget-column.component-CYb2zOSj.mjs.map +1 -0
- package/fesm2022/acorex-platform-module-platform-management-client-rest-api-generator-widget-edit.component-BBwrzcXa.mjs +178 -0
- package/fesm2022/acorex-platform-module-platform-management-client-rest-api-generator-widget-edit.component-BBwrzcXa.mjs.map +1 -0
- package/fesm2022/acorex-platform-module-platform-management-client-rest-api-generator-widget-view.component-DBlZVz6u.mjs +45 -0
- package/fesm2022/acorex-platform-module-platform-management-client-rest-api-generator-widget-view.component-DBlZVz6u.mjs.map +1 -0
- package/fesm2022/acorex-platform-module-platform-management-client.mjs +2 -0
- package/fesm2022/acorex-platform-module-platform-management-client.mjs.map +1 -0
- package/package.json +28 -0
- package/types/acorex-platform-module-platform-management-client-contracts.d.ts +258 -0
- package/types/acorex-platform-module-platform-management-client.d.ts +548 -0
|
@@ -0,0 +1,4599 @@
|
|
|
1
|
+
import { AXPSessionService, AXP_PERMISSION_DEFINITION_PROVIDER } from '@acorex-platform/framework-client/auth';
|
|
2
|
+
import { AXPSettingsService, AXPMenuProviderService, AXPTokenDefinitionService, AXP_TOKEN_DEFINITION_PROVIDER, AXP_PROTECTED_ROUTE_GUARDS, AXP_MENU_MIDDLEWARE, AXP_MENU_PROVIDER } from '@acorex-platform/framework-client/common';
|
|
3
|
+
import { AXP_DISTRIBUTED_EVENT_LISTENER_PROVIDER, AXP_MODULE_MANIFEST_PROVIDER, AXP_FEATURE_DEFINITION_PROVIDER } from '@acorex-platform/framework-client/core';
|
|
4
|
+
import { AXPRootLayoutComponent } from '@acorex-platform/framework-client/themes/default';
|
|
5
|
+
import { AXP_WIDGETS_EDITOR_CATEGORY, AXPWidgetCoreModule, AXP_WIDGET_DEFINITION_PROVIDER } from '@acorex-platform/framework-client/layout/widget-core';
|
|
6
|
+
import { provideCommandSetups, AXPCommandExecutor, provideQuerySetups } from '@acorex-platform/framework-client/runtime';
|
|
7
|
+
import * as i0 from '@angular/core';
|
|
8
|
+
import { Injectable, inject, NgModule, Injector, input, output, viewChild, signal, DestroyRef, computed, afterNextRender, effect, ViewEncapsulation, ChangeDetectionStrategy, Component, Directive } from '@angular/core';
|
|
9
|
+
import { ROUTES } from '@angular/router';
|
|
10
|
+
import { AXPWidgetGroupEnum } from '@acorex-platform/framework-shared/widget-core';
|
|
11
|
+
import { AXP_NAME_PROPERTY, AXP_DATA_PATH_PROPERTY, AXP_READONLY_PROPERTY, AXP_DISABLED_PROPERTY } from '@acorex-platform/framework-client/layout/widgets';
|
|
12
|
+
import { AXPPlatformManagementFeatureKeys, RootConfig, AXM_TOKEN_RESOLVE_COMMAND_KEY, AXPApplicationsPlatformPermissionKeys, AXPPlatformManagementMenuKeys } from '@acorex-platform/module-platform-management-client/contracts';
|
|
13
|
+
import { AXPPlatformScope, runtimeCommandPayloadData } from '@acorex-platform/framework-shared/core';
|
|
14
|
+
import { AXPMenuCustomizerService, AXP_MENU_CUSTOMIZER_SERVICE } from '@acorex-platform/framework-client/layout/components';
|
|
15
|
+
import { cloneDeep } from 'lodash';
|
|
16
|
+
import { AXPEntityService } from '@acorex-platform/framework-client/layout/entity';
|
|
17
|
+
import { AXDialogService } from '@acorex/components/dialog';
|
|
18
|
+
import { AXTranslationService } from '@acorex/core/translation';
|
|
19
|
+
import { SwUpdate } from '@angular/service-worker';
|
|
20
|
+
import { AXPCommonMenuKeys, AXPCommonPermissionGroupKeys } from '@acorex-platform/module-common-client/contracts';
|
|
21
|
+
import { AXP_ENTITY_DEFINITION_CRUD_SERVICE } from '@acorex-platform/framework-client/domain';
|
|
22
|
+
import { AXPanViewDirective } from '@acorex/cdk/pan-view';
|
|
23
|
+
import * as i1$1 from '@acorex/components/button';
|
|
24
|
+
import { AXButtonModule } from '@acorex/components/button';
|
|
25
|
+
import * as i2 from '@acorex/components/decorators';
|
|
26
|
+
import { AXDecoratorModule } from '@acorex/components/decorators';
|
|
27
|
+
import * as i1 from '@angular/common';
|
|
28
|
+
import { CommonModule } from '@angular/common';
|
|
29
|
+
|
|
30
|
+
const REST_API_GENERATOR_WIDGET_NAME = 'rest-api-generator-editor';
|
|
31
|
+
const REST_API_GENERATOR_IMPORT_DEBOUNCE_MS = 500;
|
|
32
|
+
|
|
33
|
+
//#region ----- Imports -----
|
|
34
|
+
//#endregion
|
|
35
|
+
//#region ----- Config -----
|
|
36
|
+
const AXMRestApiGeneratorWidget = {
|
|
37
|
+
name: REST_API_GENERATOR_WIDGET_NAME,
|
|
38
|
+
title: '@platform-management:query-definitions.wizard.create.rest-api-generator.title',
|
|
39
|
+
description: '@platform-management:query-definitions.wizard.create.rest-api-generator.description',
|
|
40
|
+
icon: 'fa-light fa-cloud-arrow-up',
|
|
41
|
+
categories: AXP_WIDGETS_EDITOR_CATEGORY,
|
|
42
|
+
groups: [AXPWidgetGroupEnum.UtilityWidget],
|
|
43
|
+
type: 'editor',
|
|
44
|
+
defaultFilterWidgetName: 'string-filter',
|
|
45
|
+
options: {
|
|
46
|
+
needLabel: false,
|
|
47
|
+
},
|
|
48
|
+
properties: [AXP_NAME_PROPERTY, AXP_DATA_PATH_PROPERTY, AXP_READONLY_PROPERTY, AXP_DISABLED_PROPERTY],
|
|
49
|
+
components: {
|
|
50
|
+
edit: {
|
|
51
|
+
component: () => import('./acorex-platform-module-platform-management-client-rest-api-generator-widget-edit.component-BBwrzcXa.mjs').then((c) => c.AXMRestApiGeneratorWidgetEditComponent),
|
|
52
|
+
},
|
|
53
|
+
designer: {
|
|
54
|
+
component: () => import('./acorex-platform-module-platform-management-client-rest-api-generator-widget-edit.component-BBwrzcXa.mjs').then((c) => c.AXMRestApiGeneratorWidgetEditComponent),
|
|
55
|
+
},
|
|
56
|
+
view: {
|
|
57
|
+
component: () => import('./acorex-platform-module-platform-management-client-rest-api-generator-widget-view.component-DBlZVz6u.mjs').then((c) => c.AXMRestApiGeneratorWidgetViewComponent),
|
|
58
|
+
},
|
|
59
|
+
column: {
|
|
60
|
+
component: () => import('./acorex-platform-module-platform-management-client-rest-api-generator-widget-column.component-CYb2zOSj.mjs').then((c) => c.AXMRestApiGeneratorWidgetColumnComponent),
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
//#endregion
|
|
65
|
+
|
|
66
|
+
//#region ----- Constants -----
|
|
67
|
+
const HTTP_METHODS = ['GET', 'POST', 'DELETE', 'PUT', 'PATCH'];
|
|
68
|
+
const VALUE_FLAGS = {
|
|
69
|
+
'-X': 'method',
|
|
70
|
+
'--request': 'method',
|
|
71
|
+
'-H': 'header',
|
|
72
|
+
'--header': 'header',
|
|
73
|
+
'-d': 'data',
|
|
74
|
+
'--data': 'data',
|
|
75
|
+
'--data-raw': 'data',
|
|
76
|
+
'--data-binary': 'data',
|
|
77
|
+
'--data-ascii': 'data',
|
|
78
|
+
'--data-urlencode': 'data',
|
|
79
|
+
'--json': 'json',
|
|
80
|
+
'-u': 'user',
|
|
81
|
+
'--user': 'user',
|
|
82
|
+
'--url': 'url',
|
|
83
|
+
'-A': 'user-agent',
|
|
84
|
+
'--user-agent': 'user-agent',
|
|
85
|
+
'-b': 'cookie',
|
|
86
|
+
'--cookie': 'cookie',
|
|
87
|
+
'-e': 'referer',
|
|
88
|
+
'--referer': 'referer',
|
|
89
|
+
'--oauth2-bearer': 'bearer',
|
|
90
|
+
'-F': 'form',
|
|
91
|
+
'--form': 'form',
|
|
92
|
+
'-o': 'ignore',
|
|
93
|
+
'--output': 'ignore',
|
|
94
|
+
'-w': 'ignore',
|
|
95
|
+
'--write-out': 'ignore',
|
|
96
|
+
'-m': 'ignore',
|
|
97
|
+
'--max-time': 'ignore',
|
|
98
|
+
'--connect-timeout': 'ignore',
|
|
99
|
+
'--retry': 'ignore',
|
|
100
|
+
};
|
|
101
|
+
const SKIP_FLAGS = new Set([
|
|
102
|
+
'-v',
|
|
103
|
+
'--verbose',
|
|
104
|
+
'-k',
|
|
105
|
+
'--insecure',
|
|
106
|
+
'--compressed',
|
|
107
|
+
'-s',
|
|
108
|
+
'--silent',
|
|
109
|
+
'-S',
|
|
110
|
+
'--show-error',
|
|
111
|
+
'-L',
|
|
112
|
+
'--location',
|
|
113
|
+
'-i',
|
|
114
|
+
'--include',
|
|
115
|
+
'-I',
|
|
116
|
+
'--head',
|
|
117
|
+
'-#',
|
|
118
|
+
'--progress-bar',
|
|
119
|
+
'--http1.0',
|
|
120
|
+
'--http1.1',
|
|
121
|
+
'--http2',
|
|
122
|
+
'--globoff',
|
|
123
|
+
'-g',
|
|
124
|
+
'--fail',
|
|
125
|
+
'-f',
|
|
126
|
+
'--remote-name',
|
|
127
|
+
'-O',
|
|
128
|
+
'--location-trusted',
|
|
129
|
+
'--no-progress-meter',
|
|
130
|
+
]);
|
|
131
|
+
//#endregion
|
|
132
|
+
//#region ----- Public API -----
|
|
133
|
+
function createEmptyRestApiOutput() {
|
|
134
|
+
return {
|
|
135
|
+
url: '',
|
|
136
|
+
method: 'GET',
|
|
137
|
+
body: '',
|
|
138
|
+
headers: {
|
|
139
|
+
Authorization: '',
|
|
140
|
+
ContentType: '',
|
|
141
|
+
},
|
|
142
|
+
queries: [{}],
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
function normalizeRestApiOutput(value) {
|
|
146
|
+
const empty = createEmptyRestApiOutput();
|
|
147
|
+
if (value == null || typeof value !== 'object') {
|
|
148
|
+
return empty;
|
|
149
|
+
}
|
|
150
|
+
const headers = value.headers && typeof value.headers === 'object' ? value.headers : {};
|
|
151
|
+
const extraHeaders = {};
|
|
152
|
+
for (const [key, headerValue] of Object.entries(headers)) {
|
|
153
|
+
if (key === 'Authorization' || key === 'ContentType' || key === 'tempId') {
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
if (typeof headerValue === 'string' && headerValue.trim()) {
|
|
157
|
+
extraHeaders[key] = headerValue;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
const authorization = headers['Authorization'];
|
|
161
|
+
const contentType = headers['ContentType'];
|
|
162
|
+
return {
|
|
163
|
+
url: typeof value.url === 'string' ? value.url.trim() : '',
|
|
164
|
+
method: normalizeMethod(value.method),
|
|
165
|
+
body: typeof value.body === 'string' ? value.body : value.body == null ? '' : String(value.body),
|
|
166
|
+
headers: {
|
|
167
|
+
Authorization: typeof authorization === 'string' ? authorization : '',
|
|
168
|
+
ContentType: typeof contentType === 'string' ? contentType : '',
|
|
169
|
+
...extraHeaders,
|
|
170
|
+
},
|
|
171
|
+
queries: normalizeQueries(value.queries),
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
function formatRestApiSummary(value) {
|
|
175
|
+
const url = value?.url?.trim() ?? '';
|
|
176
|
+
if (!url) {
|
|
177
|
+
return '';
|
|
178
|
+
}
|
|
179
|
+
return `${value?.method ?? 'GET'} ${url}`;
|
|
180
|
+
}
|
|
181
|
+
function parseHttpRequest(input) {
|
|
182
|
+
const source = input.trim();
|
|
183
|
+
if (!source) {
|
|
184
|
+
return { ok: false };
|
|
185
|
+
}
|
|
186
|
+
if (looksLikeCurl(source)) {
|
|
187
|
+
return wrapParse('curl', parseCurl(source));
|
|
188
|
+
}
|
|
189
|
+
if (/\bfetch\s*\(/i.test(source)) {
|
|
190
|
+
return wrapParse('fetch', parseFetch(source));
|
|
191
|
+
}
|
|
192
|
+
if (/^(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\s+\S+/i.test(source)) {
|
|
193
|
+
return wrapParse('http', parseRawHttp(source));
|
|
194
|
+
}
|
|
195
|
+
if (source.startsWith('{')) {
|
|
196
|
+
return wrapParse('json', parseJsonRequest(source));
|
|
197
|
+
}
|
|
198
|
+
if (/^https?:\/\//i.test(source.split(/\s/, 1)[0] ?? '')) {
|
|
199
|
+
return wrapParse('url', parseUrlOnly(source));
|
|
200
|
+
}
|
|
201
|
+
return { ok: false };
|
|
202
|
+
}
|
|
203
|
+
//#endregion
|
|
204
|
+
//#region ----- Format parsers -----
|
|
205
|
+
function parseCurl(source) {
|
|
206
|
+
const tokens = tokenizeCommandLine(source);
|
|
207
|
+
if (tokens.length === 0) {
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
let index = 0;
|
|
211
|
+
if (tokens[index] === '$') {
|
|
212
|
+
index += 1;
|
|
213
|
+
}
|
|
214
|
+
if (/^curl(?:\.exe)?$/i.test(tokens[index] ?? '')) {
|
|
215
|
+
index += 1;
|
|
216
|
+
}
|
|
217
|
+
const output = createEmptyRestApiOutput();
|
|
218
|
+
let rawUrl = '';
|
|
219
|
+
let body = '';
|
|
220
|
+
let hasJsonFlag = false;
|
|
221
|
+
let hasFormFlag = false;
|
|
222
|
+
while (index < tokens.length) {
|
|
223
|
+
const token = tokens[index] ?? '';
|
|
224
|
+
if (token === '--') {
|
|
225
|
+
const rest = tokens[index + 1];
|
|
226
|
+
if (rest && !rawUrl) {
|
|
227
|
+
rawUrl = rest;
|
|
228
|
+
}
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
if (!token.startsWith('-')) {
|
|
232
|
+
if (!rawUrl) {
|
|
233
|
+
rawUrl = token;
|
|
234
|
+
}
|
|
235
|
+
index += 1;
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
const { name, inlineValue, kind } = resolveFlag(token);
|
|
239
|
+
if (kind === 'skip') {
|
|
240
|
+
index += 1;
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
let flagValue = inlineValue;
|
|
244
|
+
if (flagValue == null && kind !== 'skip') {
|
|
245
|
+
const next = tokens[index + 1];
|
|
246
|
+
if (next != null && !next.startsWith('-')) {
|
|
247
|
+
flagValue = next;
|
|
248
|
+
index += 2;
|
|
249
|
+
}
|
|
250
|
+
else {
|
|
251
|
+
index += 1;
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
index += 1;
|
|
257
|
+
}
|
|
258
|
+
if (kind === 'ignore' || flagValue == null) {
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
switch (name) {
|
|
262
|
+
case 'method':
|
|
263
|
+
output.method = normalizeMethod(flagValue);
|
|
264
|
+
break;
|
|
265
|
+
case 'header':
|
|
266
|
+
applyHeader(output, flagValue);
|
|
267
|
+
break;
|
|
268
|
+
case 'data':
|
|
269
|
+
body = appendBody(body, flagValue);
|
|
270
|
+
break;
|
|
271
|
+
case 'json':
|
|
272
|
+
body = appendBody(body, flagValue);
|
|
273
|
+
hasJsonFlag = true;
|
|
274
|
+
break;
|
|
275
|
+
case 'form':
|
|
276
|
+
body = appendBody(body, flagValue);
|
|
277
|
+
hasFormFlag = true;
|
|
278
|
+
break;
|
|
279
|
+
case 'user':
|
|
280
|
+
applyUser(output, flagValue);
|
|
281
|
+
break;
|
|
282
|
+
case 'url':
|
|
283
|
+
rawUrl = flagValue;
|
|
284
|
+
break;
|
|
285
|
+
case 'user-agent':
|
|
286
|
+
applyHeader(output, `User-Agent: ${flagValue}`);
|
|
287
|
+
break;
|
|
288
|
+
case 'cookie':
|
|
289
|
+
applyHeader(output, `Cookie: ${flagValue}`);
|
|
290
|
+
break;
|
|
291
|
+
case 'referer':
|
|
292
|
+
applyHeader(output, `Referer: ${flagValue}`);
|
|
293
|
+
break;
|
|
294
|
+
case 'bearer':
|
|
295
|
+
output.headers.Authorization = `Bearer ${flagValue}`;
|
|
296
|
+
break;
|
|
297
|
+
default:
|
|
298
|
+
break;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
if (rawUrl.trim()) {
|
|
302
|
+
const split = splitUrlAndQuery(rawUrl);
|
|
303
|
+
output.url = split.url;
|
|
304
|
+
output.queries = split.queries;
|
|
305
|
+
}
|
|
306
|
+
output.body = body;
|
|
307
|
+
if (hasJsonFlag) {
|
|
308
|
+
output.headers.ContentType = output.headers.ContentType || 'application/json';
|
|
309
|
+
}
|
|
310
|
+
else if (hasFormFlag) {
|
|
311
|
+
output.headers.ContentType = output.headers.ContentType || 'multipart/form-data';
|
|
312
|
+
}
|
|
313
|
+
else if (body && !output.headers.ContentType) {
|
|
314
|
+
output.headers.ContentType = looksLikeJson(body) ? 'application/json' : 'application/x-www-form-urlencoded';
|
|
315
|
+
}
|
|
316
|
+
if (body && output.method === 'GET') {
|
|
317
|
+
output.method = 'POST';
|
|
318
|
+
}
|
|
319
|
+
const normalized = normalizeRestApiOutput(output);
|
|
320
|
+
return hasParsedContent(normalized) ? normalized : null;
|
|
321
|
+
}
|
|
322
|
+
function parseFetch(source) {
|
|
323
|
+
const fetchIndex = source.search(/\bfetch\s*\(/i);
|
|
324
|
+
if (fetchIndex < 0) {
|
|
325
|
+
return null;
|
|
326
|
+
}
|
|
327
|
+
const openParen = source.indexOf('(', fetchIndex);
|
|
328
|
+
if (openParen < 0) {
|
|
329
|
+
return null;
|
|
330
|
+
}
|
|
331
|
+
const args = extractCallArguments(source, openParen);
|
|
332
|
+
if (!args) {
|
|
333
|
+
return null;
|
|
334
|
+
}
|
|
335
|
+
const urlRaw = unquote(args[0]?.trim() ?? '');
|
|
336
|
+
if (!urlRaw) {
|
|
337
|
+
return null;
|
|
338
|
+
}
|
|
339
|
+
const output = createEmptyRestApiOutput();
|
|
340
|
+
const split = splitUrlAndQuery(urlRaw);
|
|
341
|
+
output.url = split.url;
|
|
342
|
+
output.queries = split.queries;
|
|
343
|
+
const optionsSrc = args[1]?.trim();
|
|
344
|
+
if (optionsSrc?.startsWith('{')) {
|
|
345
|
+
applyFetchOptions(output, optionsSrc);
|
|
346
|
+
}
|
|
347
|
+
return output.url ? normalizeRestApiOutput(output) : null;
|
|
348
|
+
}
|
|
349
|
+
function parseRawHttp(source) {
|
|
350
|
+
const normalized = source.replace(/\r\n/g, '\n');
|
|
351
|
+
const headerBodySplit = normalized.split(/\n\n/);
|
|
352
|
+
const headerBlock = headerBodySplit[0] ?? '';
|
|
353
|
+
const body = headerBodySplit.slice(1).join('\n\n');
|
|
354
|
+
const lines = headerBlock.split('\n').map((line) => line.trimEnd());
|
|
355
|
+
const requestLine = lines[0]?.trim() ?? '';
|
|
356
|
+
const requestMatch = requestLine.match(/^(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\s+(\S+)(?:\s+HTTP\/[\d.]+)?$/i);
|
|
357
|
+
if (!requestMatch) {
|
|
358
|
+
return null;
|
|
359
|
+
}
|
|
360
|
+
const output = createEmptyRestApiOutput();
|
|
361
|
+
output.method = normalizeMethod(requestMatch[1] ?? 'GET');
|
|
362
|
+
let path = requestMatch[2] ?? '';
|
|
363
|
+
let host = '';
|
|
364
|
+
for (let i = 1; i < lines.length; i += 1) {
|
|
365
|
+
const line = lines[i]?.trim() ?? '';
|
|
366
|
+
if (!line) {
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
applyHeader(output, line);
|
|
370
|
+
const headerMatch = line.match(/^([^:]+):\s*(.*)$/);
|
|
371
|
+
if (headerMatch && headerMatch[1]?.toLowerCase() === 'host') {
|
|
372
|
+
host = headerMatch[2]?.trim() ?? '';
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
if (/^https?:\/\//i.test(path)) {
|
|
376
|
+
const split = splitUrlAndQuery(path);
|
|
377
|
+
output.url = split.url;
|
|
378
|
+
output.queries = split.queries;
|
|
379
|
+
}
|
|
380
|
+
else {
|
|
381
|
+
const origin = host ? `https://${host}` : '';
|
|
382
|
+
const combined = origin ? `${origin}${path.startsWith('/') ? path : `/${path}`}` : path;
|
|
383
|
+
const split = splitUrlAndQuery(combined);
|
|
384
|
+
output.url = split.url;
|
|
385
|
+
output.queries = split.queries;
|
|
386
|
+
}
|
|
387
|
+
output.body = body.trim();
|
|
388
|
+
if (output.body && output.method === 'GET') {
|
|
389
|
+
output.method = 'POST';
|
|
390
|
+
}
|
|
391
|
+
return output.url ? normalizeRestApiOutput(output) : null;
|
|
392
|
+
}
|
|
393
|
+
function parseJsonRequest(source) {
|
|
394
|
+
let parsed;
|
|
395
|
+
try {
|
|
396
|
+
parsed = JSON.parse(source);
|
|
397
|
+
}
|
|
398
|
+
catch {
|
|
399
|
+
return null;
|
|
400
|
+
}
|
|
401
|
+
if (parsed == null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
402
|
+
return null;
|
|
403
|
+
}
|
|
404
|
+
const record = parsed;
|
|
405
|
+
const url = pickString(record, ['url', 'raw_url', 'rawUrl']);
|
|
406
|
+
if (!url) {
|
|
407
|
+
return null;
|
|
408
|
+
}
|
|
409
|
+
const output = createEmptyRestApiOutput();
|
|
410
|
+
const split = splitUrlAndQuery(url);
|
|
411
|
+
output.url = split.url;
|
|
412
|
+
output.queries = split.queries;
|
|
413
|
+
output.method = normalizeMethod(pickString(record, ['method']) ?? 'GET');
|
|
414
|
+
const body = record['body'] ?? record['data'];
|
|
415
|
+
if (typeof body === 'string') {
|
|
416
|
+
output.body = body;
|
|
417
|
+
}
|
|
418
|
+
else if (body != null && typeof body === 'object') {
|
|
419
|
+
output.body = JSON.stringify(body);
|
|
420
|
+
output.headers.ContentType = output.headers.ContentType || 'application/json';
|
|
421
|
+
}
|
|
422
|
+
const headers = record['headers'];
|
|
423
|
+
if (headers && typeof headers === 'object' && !Array.isArray(headers)) {
|
|
424
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
425
|
+
if (typeof value === 'string') {
|
|
426
|
+
applyHeader(output, `${key}: ${value}`);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
const queries = record['queries'] ?? record['query'];
|
|
431
|
+
const fromQueries = queriesFromUnknown(queries);
|
|
432
|
+
if (fromQueries.length > 0 && hasNamedQuery(fromQueries)) {
|
|
433
|
+
output.queries = fromQueries;
|
|
434
|
+
}
|
|
435
|
+
return normalizeRestApiOutput(output);
|
|
436
|
+
}
|
|
437
|
+
function parseUrlOnly(source) {
|
|
438
|
+
const firstLine = source.split(/\s/, 1)[0]?.trim() ?? '';
|
|
439
|
+
if (!/^https?:\/\//i.test(firstLine)) {
|
|
440
|
+
return null;
|
|
441
|
+
}
|
|
442
|
+
const output = createEmptyRestApiOutput();
|
|
443
|
+
const split = splitUrlAndQuery(firstLine);
|
|
444
|
+
output.url = split.url;
|
|
445
|
+
output.queries = split.queries;
|
|
446
|
+
return output.url ? normalizeRestApiOutput(output) : null;
|
|
447
|
+
}
|
|
448
|
+
//#endregion
|
|
449
|
+
//#region ----- Curl helpers -----
|
|
450
|
+
function looksLikeCurl(source) {
|
|
451
|
+
const trimmed = source.trim().replace(/^\$\s+/, '');
|
|
452
|
+
if (!trimmed || trimmed.startsWith('{')) {
|
|
453
|
+
return false;
|
|
454
|
+
}
|
|
455
|
+
if (/^(?:curl(?:\.exe)?)\b/i.test(trimmed)) {
|
|
456
|
+
return true;
|
|
457
|
+
}
|
|
458
|
+
if (/\bcurl(?:\.exe)?\b/i.test(trimmed) && hasCurlFlags(trimmed)) {
|
|
459
|
+
return true;
|
|
460
|
+
}
|
|
461
|
+
return hasCurlFlags(trimmed);
|
|
462
|
+
}
|
|
463
|
+
function hasCurlFlags(source) {
|
|
464
|
+
return /(?:^|[\s\\])(?:-H|--header|-X|--request|-d|--data(?:-raw|-binary|-ascii|-urlencode)?|--json|--url|-F|--form|-A|--user-agent|-u|--user|-b|--cookie|-e|--referer|--oauth2-bearer)\b/.test(source);
|
|
465
|
+
}
|
|
466
|
+
function tokenizeCommandLine(input) {
|
|
467
|
+
const normalized = input
|
|
468
|
+
.replace(/\\\r?\n/g, ' ')
|
|
469
|
+
.replace(/\^\r?\n/g, ' ')
|
|
470
|
+
.replace(/`\r?\n/g, ' ')
|
|
471
|
+
.trim();
|
|
472
|
+
const tokens = [];
|
|
473
|
+
let i = 0;
|
|
474
|
+
while (i < normalized.length) {
|
|
475
|
+
while (i < normalized.length && /\s/.test(normalized[i] ?? '')) {
|
|
476
|
+
i += 1;
|
|
477
|
+
}
|
|
478
|
+
if (i >= normalized.length) {
|
|
479
|
+
break;
|
|
480
|
+
}
|
|
481
|
+
if (normalized[i] === '$' && (normalized[i + 1] === "'" || normalized[i + 1] === '"')) {
|
|
482
|
+
const quote = normalized[i + 1] ?? "'";
|
|
483
|
+
const quoted = readQuoted(normalized, i + 2, quote, true);
|
|
484
|
+
tokens.push(quoted.value);
|
|
485
|
+
i = quoted.nextIndex;
|
|
486
|
+
continue;
|
|
487
|
+
}
|
|
488
|
+
const quote = normalized[i];
|
|
489
|
+
if (quote === "'" || quote === '"' || quote === '`') {
|
|
490
|
+
const quoted = readQuoted(normalized, i + 1, quote, false);
|
|
491
|
+
tokens.push(quoted.value);
|
|
492
|
+
i = quoted.nextIndex;
|
|
493
|
+
continue;
|
|
494
|
+
}
|
|
495
|
+
let value = '';
|
|
496
|
+
while (i < normalized.length && !/\s/.test(normalized[i] ?? '')) {
|
|
497
|
+
const current = normalized[i] ?? '';
|
|
498
|
+
const next = normalized[i + 1] ?? '';
|
|
499
|
+
if (current === '$' && (next === "'" || next === '"') && value.length > 0) {
|
|
500
|
+
break;
|
|
501
|
+
}
|
|
502
|
+
if ((current === "'" || current === '"' || current === '`') && value.length > 0) {
|
|
503
|
+
break;
|
|
504
|
+
}
|
|
505
|
+
value += current;
|
|
506
|
+
i += 1;
|
|
507
|
+
}
|
|
508
|
+
tokens.push(value);
|
|
509
|
+
}
|
|
510
|
+
return tokens;
|
|
511
|
+
}
|
|
512
|
+
function readQuoted(source, start, quote, ansiC) {
|
|
513
|
+
let value = '';
|
|
514
|
+
let i = start;
|
|
515
|
+
while (i < source.length) {
|
|
516
|
+
const current = source[i] ?? '';
|
|
517
|
+
if (ansiC && current === '\\' && i + 1 < source.length) {
|
|
518
|
+
const decoded = decodeAnsiCEscape(source, i);
|
|
519
|
+
value += decoded.value;
|
|
520
|
+
i = decoded.nextIndex;
|
|
521
|
+
continue;
|
|
522
|
+
}
|
|
523
|
+
if (!ansiC && current === '\\' && quote !== "'" && i + 1 < source.length) {
|
|
524
|
+
value += source[i + 1] ?? '';
|
|
525
|
+
i += 2;
|
|
526
|
+
continue;
|
|
527
|
+
}
|
|
528
|
+
if (current === quote) {
|
|
529
|
+
i += 1;
|
|
530
|
+
break;
|
|
531
|
+
}
|
|
532
|
+
value += current;
|
|
533
|
+
i += 1;
|
|
534
|
+
}
|
|
535
|
+
return { value, nextIndex: i };
|
|
536
|
+
}
|
|
537
|
+
function decodeAnsiCEscape(source, index) {
|
|
538
|
+
const next = source[index + 1] ?? '';
|
|
539
|
+
switch (next) {
|
|
540
|
+
case 'n':
|
|
541
|
+
return { value: '\n', nextIndex: index + 2 };
|
|
542
|
+
case 'r':
|
|
543
|
+
return { value: '\r', nextIndex: index + 2 };
|
|
544
|
+
case 't':
|
|
545
|
+
return { value: '\t', nextIndex: index + 2 };
|
|
546
|
+
case 'v':
|
|
547
|
+
return { value: '\v', nextIndex: index + 2 };
|
|
548
|
+
case 'b':
|
|
549
|
+
return { value: '\b', nextIndex: index + 2 };
|
|
550
|
+
case 'f':
|
|
551
|
+
return { value: '\f', nextIndex: index + 2 };
|
|
552
|
+
case 'a':
|
|
553
|
+
return { value: '\u0007', nextIndex: index + 2 };
|
|
554
|
+
case '\\':
|
|
555
|
+
case "'":
|
|
556
|
+
case '"':
|
|
557
|
+
case '?':
|
|
558
|
+
return { value: next, nextIndex: index + 2 };
|
|
559
|
+
case 'x': {
|
|
560
|
+
const hex = source.slice(index + 2, index + 4);
|
|
561
|
+
if (/^[0-9a-fA-F]{2}$/.test(hex)) {
|
|
562
|
+
return { value: String.fromCharCode(Number.parseInt(hex, 16)), nextIndex: index + 4 };
|
|
563
|
+
}
|
|
564
|
+
return { value: next, nextIndex: index + 2 };
|
|
565
|
+
}
|
|
566
|
+
default:
|
|
567
|
+
return { value: next, nextIndex: index + 2 };
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
function resolveFlag(token) {
|
|
571
|
+
const eq = token.indexOf('=');
|
|
572
|
+
let flag = token;
|
|
573
|
+
let inlineValue;
|
|
574
|
+
if (eq > 1) {
|
|
575
|
+
flag = token.slice(0, eq);
|
|
576
|
+
inlineValue = token.slice(eq + 1);
|
|
577
|
+
}
|
|
578
|
+
if (SKIP_FLAGS.has(flag) || SKIP_FLAGS.has(token)) {
|
|
579
|
+
return { name: flag, kind: 'skip' };
|
|
580
|
+
}
|
|
581
|
+
const mapped = VALUE_FLAGS[flag];
|
|
582
|
+
if (mapped) {
|
|
583
|
+
return { name: mapped, inlineValue, kind: mapped };
|
|
584
|
+
}
|
|
585
|
+
if (flag.startsWith('-X') && flag.length > 2 && !flag.startsWith('--')) {
|
|
586
|
+
return { name: 'method', inlineValue: flag.slice(2), kind: 'method' };
|
|
587
|
+
}
|
|
588
|
+
if (flag.startsWith('-H') && flag.length > 2 && !flag.startsWith('--')) {
|
|
589
|
+
return { name: 'header', inlineValue: flag.slice(2), kind: 'header' };
|
|
590
|
+
}
|
|
591
|
+
if (/^-[a-zA-Z]{2,}$/.test(flag) && !flag.startsWith('--')) {
|
|
592
|
+
return { name: flag, kind: 'skip' };
|
|
593
|
+
}
|
|
594
|
+
return { name: flag, inlineValue, kind: 'skip' };
|
|
595
|
+
}
|
|
596
|
+
function appendBody(current, next) {
|
|
597
|
+
if (!current) {
|
|
598
|
+
return next;
|
|
599
|
+
}
|
|
600
|
+
return `${current}&${next}`;
|
|
601
|
+
}
|
|
602
|
+
function applyUser(output, value) {
|
|
603
|
+
const colon = value.indexOf(':');
|
|
604
|
+
const user = colon >= 0 ? value.slice(0, colon) : value;
|
|
605
|
+
const password = colon >= 0 ? value.slice(colon + 1) : '';
|
|
606
|
+
output.headers.Authorization = `Basic ${encodeBase64(`${user}:${password}`)}`;
|
|
607
|
+
}
|
|
608
|
+
//#endregion
|
|
609
|
+
//#region ----- Fetch helpers -----
|
|
610
|
+
function extractCallArguments(source, openParen) {
|
|
611
|
+
let depthParen = 0;
|
|
612
|
+
let depthBrace = 0;
|
|
613
|
+
let depthBracket = 0;
|
|
614
|
+
let quote = null;
|
|
615
|
+
let escaped = false;
|
|
616
|
+
const args = [];
|
|
617
|
+
let current = '';
|
|
618
|
+
for (let i = openParen; i < source.length; i += 1) {
|
|
619
|
+
const ch = source[i] ?? '';
|
|
620
|
+
if (quote) {
|
|
621
|
+
current += ch;
|
|
622
|
+
if (escaped) {
|
|
623
|
+
escaped = false;
|
|
624
|
+
continue;
|
|
625
|
+
}
|
|
626
|
+
if (ch === '\\') {
|
|
627
|
+
escaped = true;
|
|
628
|
+
continue;
|
|
629
|
+
}
|
|
630
|
+
if (ch === quote) {
|
|
631
|
+
quote = null;
|
|
632
|
+
}
|
|
633
|
+
continue;
|
|
634
|
+
}
|
|
635
|
+
if (ch === '"' || ch === "'" || ch === '`') {
|
|
636
|
+
quote = ch;
|
|
637
|
+
if (depthParen > 0) {
|
|
638
|
+
current += ch;
|
|
639
|
+
}
|
|
640
|
+
continue;
|
|
641
|
+
}
|
|
642
|
+
if (ch === '(') {
|
|
643
|
+
depthParen += 1;
|
|
644
|
+
if (depthParen > 1) {
|
|
645
|
+
current += ch;
|
|
646
|
+
}
|
|
647
|
+
continue;
|
|
648
|
+
}
|
|
649
|
+
if (ch === ')') {
|
|
650
|
+
depthParen -= 1;
|
|
651
|
+
if (depthParen === 0) {
|
|
652
|
+
const trimmed = current.trim();
|
|
653
|
+
if (trimmed) {
|
|
654
|
+
args.push(trimmed);
|
|
655
|
+
}
|
|
656
|
+
return args;
|
|
657
|
+
}
|
|
658
|
+
current += ch;
|
|
659
|
+
continue;
|
|
660
|
+
}
|
|
661
|
+
if (ch === '{') {
|
|
662
|
+
depthBrace += 1;
|
|
663
|
+
current += ch;
|
|
664
|
+
continue;
|
|
665
|
+
}
|
|
666
|
+
if (ch === '}') {
|
|
667
|
+
depthBrace -= 1;
|
|
668
|
+
current += ch;
|
|
669
|
+
continue;
|
|
670
|
+
}
|
|
671
|
+
if (ch === '[') {
|
|
672
|
+
depthBracket += 1;
|
|
673
|
+
current += ch;
|
|
674
|
+
continue;
|
|
675
|
+
}
|
|
676
|
+
if (ch === ']') {
|
|
677
|
+
depthBracket -= 1;
|
|
678
|
+
current += ch;
|
|
679
|
+
continue;
|
|
680
|
+
}
|
|
681
|
+
if (ch === ',' && depthParen === 1 && depthBrace === 0 && depthBracket === 0) {
|
|
682
|
+
args.push(current.trim());
|
|
683
|
+
current = '';
|
|
684
|
+
continue;
|
|
685
|
+
}
|
|
686
|
+
current += ch;
|
|
687
|
+
}
|
|
688
|
+
return null;
|
|
689
|
+
}
|
|
690
|
+
function applyFetchOptions(output, optionsSrc) {
|
|
691
|
+
const methodMatch = optionsSrc.match(/method\s*:\s*(['"`])([^'"`]+)\1/i);
|
|
692
|
+
if (methodMatch?.[2]) {
|
|
693
|
+
output.method = normalizeMethod(methodMatch[2]);
|
|
694
|
+
}
|
|
695
|
+
const headersObject = extractNamedObject(optionsSrc, 'headers');
|
|
696
|
+
if (headersObject) {
|
|
697
|
+
applyObjectLiteralHeaders(output, headersObject);
|
|
698
|
+
}
|
|
699
|
+
const stringifyMatch = optionsSrc.match(/body\s*:\s*JSON\.stringify\s*\(([\s\S]+)\)\s*,?/i);
|
|
700
|
+
if (stringifyMatch?.[1]) {
|
|
701
|
+
const raw = stringifyMatch[1].trim().replace(/,\s*$/, '');
|
|
702
|
+
output.body = jsObjectToJson(raw) ?? raw;
|
|
703
|
+
output.headers.ContentType = output.headers.ContentType || 'application/json';
|
|
704
|
+
if (output.method === 'GET') {
|
|
705
|
+
output.method = 'POST';
|
|
706
|
+
}
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
const bodyMatch = optionsSrc.match(/body\s*:\s*(['"`])([\s\S]*?)\1/);
|
|
710
|
+
if (bodyMatch?.[2] != null) {
|
|
711
|
+
output.body = bodyMatch[2];
|
|
712
|
+
if (output.body && !output.headers.ContentType) {
|
|
713
|
+
output.headers.ContentType = looksLikeJson(output.body) ? 'application/json' : output.headers.ContentType;
|
|
714
|
+
}
|
|
715
|
+
if (output.body && output.method === 'GET') {
|
|
716
|
+
output.method = 'POST';
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
function extractNamedObject(source, name) {
|
|
721
|
+
const match = source.match(new RegExp(`${name}\\s*:\\s*\\{`, 'i'));
|
|
722
|
+
if (!match || match.index == null) {
|
|
723
|
+
return null;
|
|
724
|
+
}
|
|
725
|
+
const brace = source.indexOf('{', match.index);
|
|
726
|
+
return extractBalanced(source, brace, '{', '}');
|
|
727
|
+
}
|
|
728
|
+
function applyObjectLiteralHeaders(output, objectSrc) {
|
|
729
|
+
const inner = objectSrc.replace(/^\{/, '').replace(/\}$/, '');
|
|
730
|
+
const pair = /(['"`]?)([A-Za-z0-9\-]+)\1\s*:\s*(['"`])([\s\S]*?)\3/g;
|
|
731
|
+
let match;
|
|
732
|
+
while ((match = pair.exec(inner)) != null) {
|
|
733
|
+
const key = match[2] ?? '';
|
|
734
|
+
const value = match[4] ?? '';
|
|
735
|
+
if (key) {
|
|
736
|
+
applyHeader(output, `${key}: ${value}`);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
function jsObjectToJson(source) {
|
|
741
|
+
const trimmed = source.trim();
|
|
742
|
+
if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
|
|
743
|
+
try {
|
|
744
|
+
return JSON.stringify(JSON.parse(trimmed));
|
|
745
|
+
}
|
|
746
|
+
catch {
|
|
747
|
+
return trimmed;
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
return null;
|
|
751
|
+
}
|
|
752
|
+
//#endregion
|
|
753
|
+
//#region ----- Shared helpers -----
|
|
754
|
+
function wrapParse(format, value) {
|
|
755
|
+
if (!hasParsedContent(value)) {
|
|
756
|
+
return { ok: false };
|
|
757
|
+
}
|
|
758
|
+
return { ok: true, format, value };
|
|
759
|
+
}
|
|
760
|
+
function hasParsedContent(value) {
|
|
761
|
+
if (!value) {
|
|
762
|
+
return false;
|
|
763
|
+
}
|
|
764
|
+
if (value.url.trim()) {
|
|
765
|
+
return true;
|
|
766
|
+
}
|
|
767
|
+
if (value.body.trim()) {
|
|
768
|
+
return true;
|
|
769
|
+
}
|
|
770
|
+
if (value.method !== 'GET') {
|
|
771
|
+
return true;
|
|
772
|
+
}
|
|
773
|
+
if (value.headers.Authorization.trim() || value.headers.ContentType.trim()) {
|
|
774
|
+
return true;
|
|
775
|
+
}
|
|
776
|
+
for (const [key, headerValue] of Object.entries(value.headers)) {
|
|
777
|
+
if (key === 'Authorization' || key === 'ContentType' || key === 'tempId') {
|
|
778
|
+
continue;
|
|
779
|
+
}
|
|
780
|
+
if (typeof headerValue === 'string' && headerValue.trim()) {
|
|
781
|
+
return true;
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
return (value.queries ?? []).some((query) => Object.entries(query).some(([key, queryValue]) => key !== 'tempId' && String(queryValue ?? '').trim()));
|
|
785
|
+
}
|
|
786
|
+
function normalizeMethod(value) {
|
|
787
|
+
const method = typeof value === 'string' ? value.trim().toUpperCase() : '';
|
|
788
|
+
if (method === 'HEAD') {
|
|
789
|
+
return 'GET';
|
|
790
|
+
}
|
|
791
|
+
if (HTTP_METHODS.includes(method)) {
|
|
792
|
+
return method;
|
|
793
|
+
}
|
|
794
|
+
return 'GET';
|
|
795
|
+
}
|
|
796
|
+
function normalizeQueries(value) {
|
|
797
|
+
const items = queriesFromUnknown(value);
|
|
798
|
+
return items.length > 0 ? items : [{}];
|
|
799
|
+
}
|
|
800
|
+
function queriesFromUnknown(value) {
|
|
801
|
+
if (Array.isArray(value)) {
|
|
802
|
+
return value
|
|
803
|
+
.map((item) => queryFromUnknown(item))
|
|
804
|
+
.filter((item) => item != null);
|
|
805
|
+
}
|
|
806
|
+
if (value && typeof value === 'object') {
|
|
807
|
+
return Object.entries(value)
|
|
808
|
+
.filter(([, queryValue]) => queryValue != null)
|
|
809
|
+
.map(([key, queryValue]) => ({
|
|
810
|
+
[key]: String(queryValue),
|
|
811
|
+
tempId: newTempId(),
|
|
812
|
+
}));
|
|
813
|
+
}
|
|
814
|
+
return [];
|
|
815
|
+
}
|
|
816
|
+
function queryFromUnknown(item) {
|
|
817
|
+
if (item == null || typeof item !== 'object') {
|
|
818
|
+
return null;
|
|
819
|
+
}
|
|
820
|
+
const record = item;
|
|
821
|
+
const named = Object.entries(record).filter(([key]) => key !== 'tempId');
|
|
822
|
+
if (named.length === 0) {
|
|
823
|
+
return { tempId: newTempId() };
|
|
824
|
+
}
|
|
825
|
+
const result = { tempId: newTempId() };
|
|
826
|
+
for (const [key, value] of named) {
|
|
827
|
+
result[key] = value == null ? '' : String(value);
|
|
828
|
+
}
|
|
829
|
+
return result;
|
|
830
|
+
}
|
|
831
|
+
function hasNamedQuery(queries) {
|
|
832
|
+
return queries.some((item) => Object.keys(item).some((key) => key !== 'tempId' && key.trim() !== ''));
|
|
833
|
+
}
|
|
834
|
+
function splitUrlAndQuery(raw) {
|
|
835
|
+
const trimmed = raw.trim();
|
|
836
|
+
try {
|
|
837
|
+
const absolute = /^https?:\/\//i.test(trimmed);
|
|
838
|
+
const parsed = absolute ? new URL(trimmed) : new URL(trimmed, 'https://placeholder.local');
|
|
839
|
+
const queries = queriesFromSearch(parsed.search);
|
|
840
|
+
parsed.search = '';
|
|
841
|
+
const url = absolute ? `${parsed.origin}${parsed.pathname}${parsed.hash}` : `${parsed.pathname}${parsed.hash}`;
|
|
842
|
+
return { url, queries };
|
|
843
|
+
}
|
|
844
|
+
catch {
|
|
845
|
+
return { url: trimmed, queries: [{}] };
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
function queriesFromSearch(search) {
|
|
849
|
+
const params = new URLSearchParams(search.startsWith('?') ? search.slice(1) : search);
|
|
850
|
+
const items = [];
|
|
851
|
+
params.forEach((value, key) => {
|
|
852
|
+
items.push({ [key]: value, tempId: newTempId() });
|
|
853
|
+
});
|
|
854
|
+
return items.length > 0 ? items : [{}];
|
|
855
|
+
}
|
|
856
|
+
function applyHeader(output, raw) {
|
|
857
|
+
const match = raw.match(/^([^:]+):\s*(.*)$/);
|
|
858
|
+
if (!match) {
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
861
|
+
const name = (match[1] ?? '').trim();
|
|
862
|
+
const value = (match[2] ?? '').trim();
|
|
863
|
+
if (!name) {
|
|
864
|
+
return;
|
|
865
|
+
}
|
|
866
|
+
const lower = name.toLowerCase();
|
|
867
|
+
if (lower === 'content-type' || lower === 'contenttype') {
|
|
868
|
+
output.headers.ContentType = value;
|
|
869
|
+
return;
|
|
870
|
+
}
|
|
871
|
+
if (lower === 'authorization') {
|
|
872
|
+
output.headers.Authorization = value;
|
|
873
|
+
return;
|
|
874
|
+
}
|
|
875
|
+
output.headers[name] = value;
|
|
876
|
+
}
|
|
877
|
+
function looksLikeJson(value) {
|
|
878
|
+
const trimmed = value.trim();
|
|
879
|
+
return (trimmed.startsWith('{') && trimmed.endsWith('}')) || (trimmed.startsWith('[') && trimmed.endsWith(']'));
|
|
880
|
+
}
|
|
881
|
+
function pickString(record, keys) {
|
|
882
|
+
for (const key of keys) {
|
|
883
|
+
const value = record[key];
|
|
884
|
+
if (typeof value === 'string' && value.trim()) {
|
|
885
|
+
return value.trim();
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
return undefined;
|
|
889
|
+
}
|
|
890
|
+
function unquote(value) {
|
|
891
|
+
const trimmed = value.trim();
|
|
892
|
+
const quote = trimmed[0];
|
|
893
|
+
if ((quote === "'" || quote === '"' || quote === '`') && trimmed.endsWith(quote) && trimmed.length >= 2) {
|
|
894
|
+
return trimmed.slice(1, -1);
|
|
895
|
+
}
|
|
896
|
+
return trimmed;
|
|
897
|
+
}
|
|
898
|
+
function extractBalanced(source, start, open, close) {
|
|
899
|
+
if (source[start] !== open) {
|
|
900
|
+
return null;
|
|
901
|
+
}
|
|
902
|
+
let depth = 0;
|
|
903
|
+
let quote = null;
|
|
904
|
+
let escaped = false;
|
|
905
|
+
for (let i = start; i < source.length; i += 1) {
|
|
906
|
+
const ch = source[i] ?? '';
|
|
907
|
+
if (quote) {
|
|
908
|
+
if (escaped) {
|
|
909
|
+
escaped = false;
|
|
910
|
+
continue;
|
|
911
|
+
}
|
|
912
|
+
if (ch === '\\') {
|
|
913
|
+
escaped = true;
|
|
914
|
+
continue;
|
|
915
|
+
}
|
|
916
|
+
if (ch === quote) {
|
|
917
|
+
quote = null;
|
|
918
|
+
}
|
|
919
|
+
continue;
|
|
920
|
+
}
|
|
921
|
+
if (ch === '"' || ch === "'" || ch === '`') {
|
|
922
|
+
quote = ch;
|
|
923
|
+
continue;
|
|
924
|
+
}
|
|
925
|
+
if (ch === open) {
|
|
926
|
+
depth += 1;
|
|
927
|
+
}
|
|
928
|
+
else if (ch === close) {
|
|
929
|
+
depth -= 1;
|
|
930
|
+
if (depth === 0) {
|
|
931
|
+
return source.slice(start, i + 1);
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
return null;
|
|
936
|
+
}
|
|
937
|
+
function encodeBase64(value) {
|
|
938
|
+
if (typeof btoa !== 'function') {
|
|
939
|
+
return value;
|
|
940
|
+
}
|
|
941
|
+
const bytes = new TextEncoder().encode(value);
|
|
942
|
+
let binary = '';
|
|
943
|
+
bytes.forEach((byte) => {
|
|
944
|
+
binary += String.fromCharCode(byte);
|
|
945
|
+
});
|
|
946
|
+
return btoa(binary);
|
|
947
|
+
}
|
|
948
|
+
function newTempId() {
|
|
949
|
+
return String(Math.random());
|
|
950
|
+
}
|
|
951
|
+
//#endregion
|
|
952
|
+
|
|
953
|
+
//#endregion
|
|
954
|
+
//#region ----- Platform Management Widgets Provider -----
|
|
955
|
+
const WIDGETS = [AXMRestApiGeneratorWidget];
|
|
956
|
+
class AXMPlatformManagementWidgetsProvider {
|
|
957
|
+
getWidgets() {
|
|
958
|
+
return WIDGETS;
|
|
959
|
+
}
|
|
960
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXMPlatformManagementWidgetsProvider, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
961
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXMPlatformManagementWidgetsProvider }); }
|
|
962
|
+
}
|
|
963
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXMPlatformManagementWidgetsProvider, decorators: [{
|
|
964
|
+
type: Injectable
|
|
965
|
+
}] });
|
|
966
|
+
|
|
967
|
+
//#region ---- Imports ----
|
|
968
|
+
//#endregion
|
|
969
|
+
//#region ---- Feature Definition Provider ----
|
|
970
|
+
/**
|
|
971
|
+
* Platform Management Feature Definition Provider.
|
|
972
|
+
* Provides feature definitions for the Platform Management module.
|
|
973
|
+
*/
|
|
974
|
+
class AXPPlatformManagementFeatureDefinitionProvider {
|
|
975
|
+
async provide(context) {
|
|
976
|
+
context.addFeature(AXPPlatformManagementFeatureKeys.MenuCustomization, {
|
|
977
|
+
title: '@platform-management:features.menu-customization.title',
|
|
978
|
+
description: '@platform-management:features.menu-customization.description',
|
|
979
|
+
defaultValue: true,
|
|
980
|
+
type: 'boolean',
|
|
981
|
+
});
|
|
982
|
+
context.addFeature(AXPPlatformManagementFeatureKeys.AutoCheckUpdates, {
|
|
983
|
+
title: '@platform-management:features.auto-check-updates.title',
|
|
984
|
+
description: '@platform-management:features.auto-check-updates.description',
|
|
985
|
+
defaultValue: true,
|
|
986
|
+
type: 'boolean',
|
|
987
|
+
});
|
|
988
|
+
context.addFeature(AXPPlatformManagementFeatureKeys.MaxTokens, {
|
|
989
|
+
title: '@platform-management:features.max-tokens.title',
|
|
990
|
+
description: '@platform-management:features.max-tokens.description',
|
|
991
|
+
defaultValue: 100,
|
|
992
|
+
type: 'number',
|
|
993
|
+
interface: {
|
|
994
|
+
type: 'number-editor',
|
|
995
|
+
options: {
|
|
996
|
+
min: 1,
|
|
997
|
+
max: 1000,
|
|
998
|
+
step: 1,
|
|
999
|
+
},
|
|
1000
|
+
},
|
|
1001
|
+
});
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
//#endregion
|
|
1005
|
+
|
|
1006
|
+
//#region ---- Menu Customization Constants ----
|
|
1007
|
+
/**
|
|
1008
|
+
* Setting key for menu customizations
|
|
1009
|
+
*/
|
|
1010
|
+
const AXP_MENU_CUSTOMIZATION_KEY = 'axp.platform-management.menu-customization';
|
|
1011
|
+
/**
|
|
1012
|
+
* Current version of menu customization schema
|
|
1013
|
+
*/
|
|
1014
|
+
const AXP_MENU_CUSTOMIZATION_VERSION = '1.0.0';
|
|
1015
|
+
/**
|
|
1016
|
+
* Default menu customization
|
|
1017
|
+
*/
|
|
1018
|
+
const AXP_MENU_CUSTOMIZATION_DEFAULT = {
|
|
1019
|
+
version: AXP_MENU_CUSTOMIZATION_VERSION,
|
|
1020
|
+
overrides: {},
|
|
1021
|
+
customItems: [],
|
|
1022
|
+
};
|
|
1023
|
+
/**
|
|
1024
|
+
* Menu management feature configuration
|
|
1025
|
+
*/
|
|
1026
|
+
const MenuManagementConfig = {
|
|
1027
|
+
route: 'menus',
|
|
1028
|
+
title: `@${RootConfig.config.i18n}:menu-management.components.menu-list.title`,
|
|
1029
|
+
icon: 'fa-light fa-bars',
|
|
1030
|
+
};
|
|
1031
|
+
//#endregion
|
|
1032
|
+
|
|
1033
|
+
//#region ---- Custom Menu Middleware ----
|
|
1034
|
+
/**
|
|
1035
|
+
* Middleware that applies menu customizations using the enhanced context API
|
|
1036
|
+
* Applies user/tenant customizations from settings with priority support
|
|
1037
|
+
*/
|
|
1038
|
+
class AXPCustomMenuMiddleware {
|
|
1039
|
+
constructor() {
|
|
1040
|
+
//#region ---- Middleware Properties ----
|
|
1041
|
+
this.name = 'menu-customization';
|
|
1042
|
+
this.priority = 100; // High priority to apply customizations early
|
|
1043
|
+
//#endregion
|
|
1044
|
+
//#region ---- Dependencies ----
|
|
1045
|
+
this.settingsService = inject(AXPSettingsService);
|
|
1046
|
+
//#endregion
|
|
1047
|
+
}
|
|
1048
|
+
//#endregion
|
|
1049
|
+
//#region ---- Middleware Implementation ----
|
|
1050
|
+
async process(context) {
|
|
1051
|
+
try {
|
|
1052
|
+
// Load menu customization from settings
|
|
1053
|
+
const customization = await this.loadCustomization();
|
|
1054
|
+
// Apply overrides to existing menu items using context API
|
|
1055
|
+
this.applyOverrides(context, customization);
|
|
1056
|
+
// Add custom menu items
|
|
1057
|
+
this.addCustomItems(context, customization);
|
|
1058
|
+
// Re-sort after applying priority overrides
|
|
1059
|
+
context.sort();
|
|
1060
|
+
}
|
|
1061
|
+
catch (error) {
|
|
1062
|
+
console.error('Error applying menu customizations:', error);
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
//#endregion
|
|
1066
|
+
//#region ---- Private Methods ----
|
|
1067
|
+
/**
|
|
1068
|
+
* Load menu customization from settings
|
|
1069
|
+
* Merges Tenant and User scope customizations (User overrides Tenant)
|
|
1070
|
+
*/
|
|
1071
|
+
async loadCustomization() {
|
|
1072
|
+
try {
|
|
1073
|
+
// Load both scopes
|
|
1074
|
+
const tenantSettings = this.settingsService.scope(AXPPlatformScope.Tenant);
|
|
1075
|
+
const userSettings = this.settingsService.scope(AXPPlatformScope.User);
|
|
1076
|
+
const tenantCustomization = await tenantSettings.get(AXP_MENU_CUSTOMIZATION_KEY);
|
|
1077
|
+
const userCustomization = await userSettings.get(AXP_MENU_CUSTOMIZATION_KEY);
|
|
1078
|
+
// Merge customizations: Tenant as base, User overrides
|
|
1079
|
+
const merged = {
|
|
1080
|
+
version: userCustomization?.version || tenantCustomization?.version || AXP_MENU_CUSTOMIZATION_DEFAULT.version,
|
|
1081
|
+
overrides: {
|
|
1082
|
+
...(tenantCustomization?.overrides || {}),
|
|
1083
|
+
...(userCustomization?.overrides || {}), // User overrides win
|
|
1084
|
+
},
|
|
1085
|
+
customItems: [...(tenantCustomization?.customItems || []), ...(userCustomization?.customItems || [])],
|
|
1086
|
+
};
|
|
1087
|
+
// Return merged result or default if both are empty
|
|
1088
|
+
const hasCustomizations = Object.keys(merged.overrides).length > 0 || merged.customItems.length > 0;
|
|
1089
|
+
return hasCustomizations ? merged : AXP_MENU_CUSTOMIZATION_DEFAULT;
|
|
1090
|
+
}
|
|
1091
|
+
catch (error) {
|
|
1092
|
+
console.error('Failed to load menu customization:', error);
|
|
1093
|
+
return AXP_MENU_CUSTOMIZATION_DEFAULT;
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
/**
|
|
1097
|
+
* Apply overrides to existing menu items using enhanced context API
|
|
1098
|
+
*/
|
|
1099
|
+
applyOverrides(context, customization) {
|
|
1100
|
+
Object.entries(customization.overrides).forEach(([menuName, override]) => {
|
|
1101
|
+
const finder = context.findByName(menuName);
|
|
1102
|
+
if (!finder.exists()) {
|
|
1103
|
+
return;
|
|
1104
|
+
}
|
|
1105
|
+
// Hide menu item
|
|
1106
|
+
if (override.hidden) {
|
|
1107
|
+
finder.remove();
|
|
1108
|
+
return;
|
|
1109
|
+
}
|
|
1110
|
+
// Apply priority override
|
|
1111
|
+
if (override.priority !== undefined) {
|
|
1112
|
+
finder.setPriority(override.priority);
|
|
1113
|
+
}
|
|
1114
|
+
// Apply property overrides
|
|
1115
|
+
if (override.properties) {
|
|
1116
|
+
finder.update(override.properties);
|
|
1117
|
+
}
|
|
1118
|
+
// Move to different parent
|
|
1119
|
+
if (override.parentName !== undefined) {
|
|
1120
|
+
finder.moveTo(override.parentName);
|
|
1121
|
+
}
|
|
1122
|
+
});
|
|
1123
|
+
}
|
|
1124
|
+
/**
|
|
1125
|
+
* Add custom menu items using context API
|
|
1126
|
+
*/
|
|
1127
|
+
addCustomItems(context, customization) {
|
|
1128
|
+
if (customization.customItems && customization.customItems.length > 0) {
|
|
1129
|
+
context.add(...customization.customItems);
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
//#endregion
|
|
1134
|
+
|
|
1135
|
+
/**
|
|
1136
|
+
* Builds a customization key from a scope key
|
|
1137
|
+
* @param scopeKey The scope key (e.g., 'edition:123', 'tenant:456')
|
|
1138
|
+
* @returns The full customization storage key
|
|
1139
|
+
*/
|
|
1140
|
+
function buildCustomizationKey(scopeKey) {
|
|
1141
|
+
return `${AXP_MENU_CUSTOMIZATION_KEY}.${scopeKey}`;
|
|
1142
|
+
}
|
|
1143
|
+
//#region ---- Menu Management Service ----
|
|
1144
|
+
/**
|
|
1145
|
+
* Constants for priority calculation
|
|
1146
|
+
*/
|
|
1147
|
+
const PRIORITY_BASE_ROOT = 0;
|
|
1148
|
+
const PRIORITY_BASE_NESTED = 1000;
|
|
1149
|
+
const PRIORITY_STEP = 10;
|
|
1150
|
+
class AXPMenuManagementService extends AXPMenuCustomizerService {
|
|
1151
|
+
constructor() {
|
|
1152
|
+
super(...arguments);
|
|
1153
|
+
//#region ---- Dependencies ----
|
|
1154
|
+
this.settingsService = inject(AXPSettingsService);
|
|
1155
|
+
this.menuProviderService = inject(AXPMenuProviderService);
|
|
1156
|
+
}
|
|
1157
|
+
//#endregion
|
|
1158
|
+
//#region ---- AXPMenuCustomizerService Interface Implementation ----
|
|
1159
|
+
/**
|
|
1160
|
+
* Interface implementation: Get menu tree for the given scope key
|
|
1161
|
+
*/
|
|
1162
|
+
async getMenuTree(scopeKey) {
|
|
1163
|
+
return this.getMenuTreeByKey(scopeKey);
|
|
1164
|
+
}
|
|
1165
|
+
/**
|
|
1166
|
+
* Interface implementation: Sync tree changes
|
|
1167
|
+
*/
|
|
1168
|
+
async syncTreeChanges(scopeKey, treeNodes) {
|
|
1169
|
+
return this.syncTreeChangesByKey(scopeKey, treeNodes);
|
|
1170
|
+
}
|
|
1171
|
+
/**
|
|
1172
|
+
* Interface implementation: Show menu item
|
|
1173
|
+
*/
|
|
1174
|
+
async showMenuItem(scopeKey, menuName) {
|
|
1175
|
+
return this.showMenuItemByKey(scopeKey, menuName);
|
|
1176
|
+
}
|
|
1177
|
+
/**
|
|
1178
|
+
* Interface implementation: Hide menu item
|
|
1179
|
+
*/
|
|
1180
|
+
async hideMenuItem(scopeKey, menuName) {
|
|
1181
|
+
return this.hideMenuItemByKey(scopeKey, menuName);
|
|
1182
|
+
}
|
|
1183
|
+
/**
|
|
1184
|
+
* Interface implementation: Add custom menu item
|
|
1185
|
+
*/
|
|
1186
|
+
async addCustomMenuItem(scopeKey, menuItem, parentName) {
|
|
1187
|
+
return this.addCustomMenuItemByKey(scopeKey, menuItem, parentName);
|
|
1188
|
+
}
|
|
1189
|
+
/**
|
|
1190
|
+
* Interface implementation: Update custom menu item
|
|
1191
|
+
*/
|
|
1192
|
+
async updateCustomMenuItem(scopeKey, menuName, menuItem) {
|
|
1193
|
+
return this.updateCustomMenuItemByKey(scopeKey, menuName, menuItem);
|
|
1194
|
+
}
|
|
1195
|
+
/**
|
|
1196
|
+
* Interface implementation: Update menu properties
|
|
1197
|
+
*/
|
|
1198
|
+
async updateMenuProperties(scopeKey, menuName, properties) {
|
|
1199
|
+
return this.updateMenuPropertiesByKey(scopeKey, menuName, properties);
|
|
1200
|
+
}
|
|
1201
|
+
/**
|
|
1202
|
+
* Interface implementation: Delete custom menu item
|
|
1203
|
+
*/
|
|
1204
|
+
async deleteCustomMenuItem(scopeKey, menuName) {
|
|
1205
|
+
return this.deleteCustomMenuItemByKey(scopeKey, menuName);
|
|
1206
|
+
}
|
|
1207
|
+
/**
|
|
1208
|
+
* Interface implementation: Reset customizations
|
|
1209
|
+
*/
|
|
1210
|
+
async resetCustomizations(scopeKey) {
|
|
1211
|
+
return this.resetCustomizationsByKey(scopeKey);
|
|
1212
|
+
}
|
|
1213
|
+
//#endregion
|
|
1214
|
+
//#region ---- Scope-Based Public Methods ----
|
|
1215
|
+
/**
|
|
1216
|
+
* Get menu tree as AXTreeViewNode[] for ax-tree-view component (scope-based)
|
|
1217
|
+
*/
|
|
1218
|
+
async getMenuTreeByScope(scope) {
|
|
1219
|
+
// Get RAW menu items WITHOUT middleware (for management purposes)
|
|
1220
|
+
const baseItems = await this.menuProviderService.rawItems();
|
|
1221
|
+
// Load customizations for the given scope
|
|
1222
|
+
const customization = await this.loadCustomization(scope);
|
|
1223
|
+
// Merge custom items with base items
|
|
1224
|
+
const allItems = [...baseItems, ...customization.customItems];
|
|
1225
|
+
// Apply parent overrides to restructure items BEFORE building tree
|
|
1226
|
+
const restructuredItems = this.applyParentOverrides(allItems, customization);
|
|
1227
|
+
// Convert to tree nodes with metadata
|
|
1228
|
+
return this.convertToTreeNodes(restructuredItems, customization);
|
|
1229
|
+
}
|
|
1230
|
+
/**
|
|
1231
|
+
* Sync tree changes back to customization (scope-based)
|
|
1232
|
+
* Called automatically when tree structure changes via drag-drop
|
|
1233
|
+
*/
|
|
1234
|
+
async syncTreeChangesByScope(scope, treeNodes) {
|
|
1235
|
+
const customization = await this.loadCustomization(scope);
|
|
1236
|
+
// Extract and update priorities and parent relationships from tree structure
|
|
1237
|
+
this.updateCustomizationFromTree(treeNodes, customization);
|
|
1238
|
+
await this.saveCustomization(scope, customization);
|
|
1239
|
+
}
|
|
1240
|
+
/**
|
|
1241
|
+
* Load menu customization for a specific scope
|
|
1242
|
+
*/
|
|
1243
|
+
async loadCustomization(scope) {
|
|
1244
|
+
try {
|
|
1245
|
+
const scopedSettings = this.settingsService.scope(scope);
|
|
1246
|
+
const saved = await scopedSettings.get(AXP_MENU_CUSTOMIZATION_KEY);
|
|
1247
|
+
if (saved && saved.version) {
|
|
1248
|
+
return saved;
|
|
1249
|
+
}
|
|
1250
|
+
return cloneDeep(AXP_MENU_CUSTOMIZATION_DEFAULT);
|
|
1251
|
+
}
|
|
1252
|
+
catch {
|
|
1253
|
+
return cloneDeep(AXP_MENU_CUSTOMIZATION_DEFAULT);
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
/**
|
|
1257
|
+
* Save menu customization for a specific scope
|
|
1258
|
+
*/
|
|
1259
|
+
async saveCustomization(scope, customization) {
|
|
1260
|
+
const scopedSettings = this.settingsService.scope(scope);
|
|
1261
|
+
await scopedSettings.set(AXP_MENU_CUSTOMIZATION_KEY, customization);
|
|
1262
|
+
// Clear menu cache to force reload
|
|
1263
|
+
this.menuProviderService.clearCache();
|
|
1264
|
+
}
|
|
1265
|
+
/**
|
|
1266
|
+
* Hide a menu item (scope-based)
|
|
1267
|
+
*/
|
|
1268
|
+
async hideMenuItemByScope(scope, menuName) {
|
|
1269
|
+
this.validateMenuName(menuName, 'hide');
|
|
1270
|
+
const customization = await this.loadCustomization(scope);
|
|
1271
|
+
this.ensureOverrideExists(customization, menuName);
|
|
1272
|
+
customization.overrides[menuName].hidden = true;
|
|
1273
|
+
await this.saveCustomization(scope, customization);
|
|
1274
|
+
}
|
|
1275
|
+
/**
|
|
1276
|
+
* Show a menu item (scope-based)
|
|
1277
|
+
*/
|
|
1278
|
+
async showMenuItemByScope(scope, menuName) {
|
|
1279
|
+
this.validateMenuName(menuName, 'show');
|
|
1280
|
+
const customization = await this.loadCustomization(scope);
|
|
1281
|
+
if (customization.overrides[menuName]) {
|
|
1282
|
+
delete customization.overrides[menuName].hidden;
|
|
1283
|
+
this.cleanupEmptyOverride(customization, menuName);
|
|
1284
|
+
}
|
|
1285
|
+
await this.saveCustomization(scope, customization);
|
|
1286
|
+
}
|
|
1287
|
+
/**
|
|
1288
|
+
* Update menu item priority (scope-based)
|
|
1289
|
+
*/
|
|
1290
|
+
async updateMenuPriorityByScope(scope, menuName, priority) {
|
|
1291
|
+
this.validateMenuName(menuName, 'update priority for');
|
|
1292
|
+
const customization = await this.loadCustomization(scope);
|
|
1293
|
+
this.ensureOverrideExists(customization, menuName);
|
|
1294
|
+
customization.overrides[menuName].priority = priority;
|
|
1295
|
+
await this.saveCustomization(scope, customization);
|
|
1296
|
+
}
|
|
1297
|
+
/**
|
|
1298
|
+
* Update menu item properties (scope-based)
|
|
1299
|
+
*/
|
|
1300
|
+
async updateMenuPropertiesByScope(scope, menuName, properties) {
|
|
1301
|
+
this.validateMenuName(menuName, 'update properties for');
|
|
1302
|
+
const customization = await this.loadCustomization(scope);
|
|
1303
|
+
this.ensureOverrideExists(customization, menuName);
|
|
1304
|
+
customization.overrides[menuName].properties = {
|
|
1305
|
+
...customization.overrides[menuName].properties,
|
|
1306
|
+
...properties,
|
|
1307
|
+
};
|
|
1308
|
+
await this.saveCustomization(scope, customization);
|
|
1309
|
+
}
|
|
1310
|
+
/**
|
|
1311
|
+
* Add custom menu item (scope-based)
|
|
1312
|
+
*/
|
|
1313
|
+
async addCustomMenuItemByScope(scope, menuItem, parent) {
|
|
1314
|
+
const customization = await this.loadCustomization(scope);
|
|
1315
|
+
// Generate unique name if not provided
|
|
1316
|
+
if (!menuItem.name) {
|
|
1317
|
+
menuItem.name = `custom-menu-${Date.now()}`;
|
|
1318
|
+
}
|
|
1319
|
+
customization.customItems.push(menuItem);
|
|
1320
|
+
await this.saveCustomization(scope, customization);
|
|
1321
|
+
}
|
|
1322
|
+
/**
|
|
1323
|
+
* Update custom menu item (scope-based)
|
|
1324
|
+
*/
|
|
1325
|
+
async updateCustomMenuItemByScope(scope, menuName, menuItem) {
|
|
1326
|
+
this.validateMenuName(menuName, 'update');
|
|
1327
|
+
const customization = await this.loadCustomization(scope);
|
|
1328
|
+
const index = customization.customItems.findIndex((item) => item.name === menuName);
|
|
1329
|
+
if (index === -1) {
|
|
1330
|
+
throw new Error(`Custom menu item '${menuName}' not found`);
|
|
1331
|
+
}
|
|
1332
|
+
customization.customItems[index] = menuItem;
|
|
1333
|
+
await this.saveCustomization(scope, customization);
|
|
1334
|
+
}
|
|
1335
|
+
/**
|
|
1336
|
+
* Delete custom menu item (scope-based)
|
|
1337
|
+
*/
|
|
1338
|
+
async deleteCustomMenuItemByScope(scope, menuName) {
|
|
1339
|
+
this.validateMenuName(menuName, 'delete');
|
|
1340
|
+
const customization = await this.loadCustomization(scope);
|
|
1341
|
+
const initialLength = customization.customItems.length;
|
|
1342
|
+
customization.customItems = customization.customItems.filter((item) => item.name !== menuName);
|
|
1343
|
+
if (customization.customItems.length === initialLength) {
|
|
1344
|
+
throw new Error(`Custom menu item '${menuName}' not found`);
|
|
1345
|
+
}
|
|
1346
|
+
await this.saveCustomization(scope, customization);
|
|
1347
|
+
}
|
|
1348
|
+
/**
|
|
1349
|
+
* Reset all customizations for a scope (scope-based)
|
|
1350
|
+
*/
|
|
1351
|
+
async resetCustomizationsByScope(scope) {
|
|
1352
|
+
await this.saveCustomization(scope, cloneDeep(AXP_MENU_CUSTOMIZATION_DEFAULT));
|
|
1353
|
+
}
|
|
1354
|
+
//#endregion
|
|
1355
|
+
//#region ---- Key-Based Public Methods ----
|
|
1356
|
+
/**
|
|
1357
|
+
* Get menu tree using a generic scope key (e.g., 'edition:123', 'tenant:456')
|
|
1358
|
+
*/
|
|
1359
|
+
async getMenuTreeByKey(scopeKey) {
|
|
1360
|
+
// Get RAW menu items WITHOUT middleware (for management purposes)
|
|
1361
|
+
const baseItems = await this.menuProviderService.rawItems();
|
|
1362
|
+
// Load customizations for the given scope key
|
|
1363
|
+
const customization = await this.loadCustomizationByKey(scopeKey);
|
|
1364
|
+
// Merge custom items with base items
|
|
1365
|
+
const allItems = [...baseItems, ...customization.customItems];
|
|
1366
|
+
// Apply parent overrides to restructure items BEFORE building tree
|
|
1367
|
+
const restructuredItems = this.applyParentOverrides(allItems, customization);
|
|
1368
|
+
// Convert to tree nodes with metadata
|
|
1369
|
+
return this.convertToTreeNodes(restructuredItems, customization);
|
|
1370
|
+
}
|
|
1371
|
+
/**
|
|
1372
|
+
* Sync tree changes using a generic scope key
|
|
1373
|
+
*/
|
|
1374
|
+
async syncTreeChangesByKey(scopeKey, treeNodes) {
|
|
1375
|
+
const customization = await this.loadCustomizationByKey(scopeKey);
|
|
1376
|
+
// Extract and update priorities and parent relationships from tree structure
|
|
1377
|
+
this.updateCustomizationFromTree(treeNodes, customization);
|
|
1378
|
+
await this.saveCustomizationByKey(scopeKey, customization);
|
|
1379
|
+
}
|
|
1380
|
+
/**
|
|
1381
|
+
* Load menu customization using a generic scope key
|
|
1382
|
+
*/
|
|
1383
|
+
async loadCustomizationByKey(scopeKey) {
|
|
1384
|
+
try {
|
|
1385
|
+
const storageKey = buildCustomizationKey(scopeKey);
|
|
1386
|
+
const scopedSettings = this.settingsService.scope(AXPPlatformScope.Tenant);
|
|
1387
|
+
const saved = await scopedSettings.get(storageKey);
|
|
1388
|
+
if (saved && saved.version) {
|
|
1389
|
+
return saved;
|
|
1390
|
+
}
|
|
1391
|
+
return cloneDeep(AXP_MENU_CUSTOMIZATION_DEFAULT);
|
|
1392
|
+
}
|
|
1393
|
+
catch {
|
|
1394
|
+
return cloneDeep(AXP_MENU_CUSTOMIZATION_DEFAULT);
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
/**
|
|
1398
|
+
* Save menu customization using a generic scope key
|
|
1399
|
+
*/
|
|
1400
|
+
async saveCustomizationByKey(scopeKey, customization) {
|
|
1401
|
+
const storageKey = buildCustomizationKey(scopeKey);
|
|
1402
|
+
const scopedSettings = this.settingsService.scope(AXPPlatformScope.Tenant);
|
|
1403
|
+
await scopedSettings.set(storageKey, customization);
|
|
1404
|
+
// Clear menu cache to force reload
|
|
1405
|
+
this.menuProviderService.clearCache();
|
|
1406
|
+
}
|
|
1407
|
+
/**
|
|
1408
|
+
* Hide a menu item using a generic scope key
|
|
1409
|
+
*/
|
|
1410
|
+
async hideMenuItemByKey(scopeKey, menuName) {
|
|
1411
|
+
this.validateMenuName(menuName, 'hide');
|
|
1412
|
+
const customization = await this.loadCustomizationByKey(scopeKey);
|
|
1413
|
+
this.ensureOverrideExists(customization, menuName);
|
|
1414
|
+
customization.overrides[menuName].hidden = true;
|
|
1415
|
+
await this.saveCustomizationByKey(scopeKey, customization);
|
|
1416
|
+
}
|
|
1417
|
+
/**
|
|
1418
|
+
* Show a menu item using a generic scope key
|
|
1419
|
+
*/
|
|
1420
|
+
async showMenuItemByKey(scopeKey, menuName) {
|
|
1421
|
+
this.validateMenuName(menuName, 'show');
|
|
1422
|
+
const customization = await this.loadCustomizationByKey(scopeKey);
|
|
1423
|
+
if (customization.overrides[menuName]) {
|
|
1424
|
+
delete customization.overrides[menuName].hidden;
|
|
1425
|
+
this.cleanupEmptyOverride(customization, menuName);
|
|
1426
|
+
}
|
|
1427
|
+
await this.saveCustomizationByKey(scopeKey, customization);
|
|
1428
|
+
}
|
|
1429
|
+
/**
|
|
1430
|
+
* Update menu item properties using a generic scope key
|
|
1431
|
+
*/
|
|
1432
|
+
async updateMenuPropertiesByKey(scopeKey, menuName, properties) {
|
|
1433
|
+
this.validateMenuName(menuName, 'update properties for');
|
|
1434
|
+
const customization = await this.loadCustomizationByKey(scopeKey);
|
|
1435
|
+
this.ensureOverrideExists(customization, menuName);
|
|
1436
|
+
customization.overrides[menuName].properties = {
|
|
1437
|
+
...customization.overrides[menuName].properties,
|
|
1438
|
+
...properties,
|
|
1439
|
+
};
|
|
1440
|
+
await this.saveCustomizationByKey(scopeKey, customization);
|
|
1441
|
+
}
|
|
1442
|
+
/**
|
|
1443
|
+
* Add custom menu item using a generic scope key
|
|
1444
|
+
*/
|
|
1445
|
+
async addCustomMenuItemByKey(scopeKey, menuItem, parent) {
|
|
1446
|
+
const customization = await this.loadCustomizationByKey(scopeKey);
|
|
1447
|
+
// Generate unique name if not provided
|
|
1448
|
+
if (!menuItem.name) {
|
|
1449
|
+
menuItem.name = `custom-menu-${Date.now()}`;
|
|
1450
|
+
}
|
|
1451
|
+
customization.customItems.push(menuItem);
|
|
1452
|
+
await this.saveCustomizationByKey(scopeKey, customization);
|
|
1453
|
+
}
|
|
1454
|
+
/**
|
|
1455
|
+
* Update custom menu item using a generic scope key
|
|
1456
|
+
*/
|
|
1457
|
+
async updateCustomMenuItemByKey(scopeKey, menuName, menuItem) {
|
|
1458
|
+
this.validateMenuName(menuName, 'update');
|
|
1459
|
+
const customization = await this.loadCustomizationByKey(scopeKey);
|
|
1460
|
+
const index = customization.customItems.findIndex((item) => item.name === menuName);
|
|
1461
|
+
if (index === -1) {
|
|
1462
|
+
throw new Error(`Custom menu item '${menuName}' not found`);
|
|
1463
|
+
}
|
|
1464
|
+
customization.customItems[index] = menuItem;
|
|
1465
|
+
await this.saveCustomizationByKey(scopeKey, customization);
|
|
1466
|
+
}
|
|
1467
|
+
/**
|
|
1468
|
+
* Delete custom menu item using a generic scope key
|
|
1469
|
+
*/
|
|
1470
|
+
async deleteCustomMenuItemByKey(scopeKey, menuName) {
|
|
1471
|
+
this.validateMenuName(menuName, 'delete');
|
|
1472
|
+
const customization = await this.loadCustomizationByKey(scopeKey);
|
|
1473
|
+
const initialLength = customization.customItems.length;
|
|
1474
|
+
customization.customItems = customization.customItems.filter((item) => item.name !== menuName);
|
|
1475
|
+
if (customization.customItems.length === initialLength) {
|
|
1476
|
+
throw new Error(`Custom menu item '${menuName}' not found`);
|
|
1477
|
+
}
|
|
1478
|
+
await this.saveCustomizationByKey(scopeKey, customization);
|
|
1479
|
+
}
|
|
1480
|
+
/**
|
|
1481
|
+
* Reset all customizations using a generic scope key
|
|
1482
|
+
*/
|
|
1483
|
+
async resetCustomizationsByKey(scopeKey) {
|
|
1484
|
+
await this.saveCustomizationByKey(scopeKey, cloneDeep(AXP_MENU_CUSTOMIZATION_DEFAULT));
|
|
1485
|
+
}
|
|
1486
|
+
//#endregion
|
|
1487
|
+
//#region ---- Private Methods ----
|
|
1488
|
+
/**
|
|
1489
|
+
* Apply parent overrides from customization to restructure menu items
|
|
1490
|
+
* This ensures the tree reflects saved parent changes (moves)
|
|
1491
|
+
*/
|
|
1492
|
+
applyParentOverrides(items, customization) {
|
|
1493
|
+
// First, flatten all items into a map for easy lookup
|
|
1494
|
+
const itemMap = this.flattenMenuItems(items);
|
|
1495
|
+
// Build new structure based on effective parent relationships
|
|
1496
|
+
const rootItems = [];
|
|
1497
|
+
const childrenMap = new Map();
|
|
1498
|
+
for (const [itemName, item] of itemMap.entries()) {
|
|
1499
|
+
const override = customization.overrides[itemName];
|
|
1500
|
+
// Determine effective parent (override takes precedence)
|
|
1501
|
+
let effectiveParent;
|
|
1502
|
+
if (override?.parentName !== undefined) {
|
|
1503
|
+
// parentName is explicitly set (could be null for root, or a parent name)
|
|
1504
|
+
effectiveParent = override.parentName;
|
|
1505
|
+
}
|
|
1506
|
+
else {
|
|
1507
|
+
// No override - find original parent by searching original structure
|
|
1508
|
+
effectiveParent = this.findOriginalParent(items, itemName);
|
|
1509
|
+
}
|
|
1510
|
+
if (effectiveParent === null || effectiveParent === undefined) {
|
|
1511
|
+
// Root level item
|
|
1512
|
+
rootItems.push(item);
|
|
1513
|
+
}
|
|
1514
|
+
else {
|
|
1515
|
+
// Child item - add to parent's children
|
|
1516
|
+
if (!childrenMap.has(effectiveParent)) {
|
|
1517
|
+
childrenMap.set(effectiveParent, []);
|
|
1518
|
+
}
|
|
1519
|
+
childrenMap.get(effectiveParent).push(item);
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
// Attach children to their parents recursively
|
|
1523
|
+
const attachChildren = (item) => {
|
|
1524
|
+
if (item.name && childrenMap.has(item.name)) {
|
|
1525
|
+
item.children = childrenMap.get(item.name);
|
|
1526
|
+
item.children?.forEach(attachChildren);
|
|
1527
|
+
}
|
|
1528
|
+
};
|
|
1529
|
+
rootItems.forEach(attachChildren);
|
|
1530
|
+
return rootItems;
|
|
1531
|
+
}
|
|
1532
|
+
/**
|
|
1533
|
+
* Find the original parent name of an item in the original structure
|
|
1534
|
+
* Returns undefined for root items, string for items with parents, null for not found
|
|
1535
|
+
*/
|
|
1536
|
+
findOriginalParent(items, targetName, currentParent) {
|
|
1537
|
+
for (const item of items) {
|
|
1538
|
+
if (item.name === targetName) {
|
|
1539
|
+
return currentParent; // undefined if root, string if has parent
|
|
1540
|
+
}
|
|
1541
|
+
if (item.children) {
|
|
1542
|
+
const found = this.findOriginalParent(item.children, targetName, item.name);
|
|
1543
|
+
if (found !== null) {
|
|
1544
|
+
// Found in children (could be undefined for root of subtree, or string for nested item)
|
|
1545
|
+
return found;
|
|
1546
|
+
}
|
|
1547
|
+
// found === null means not found in this subtree, continue searching
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
return null; // Not found in this level or any children
|
|
1551
|
+
}
|
|
1552
|
+
/**
|
|
1553
|
+
* Convert menu items to AXTreeNode[] with metadata
|
|
1554
|
+
*/
|
|
1555
|
+
convertToTreeNodes(items, customization, parentName) {
|
|
1556
|
+
const nodes = items.map((item) => {
|
|
1557
|
+
const itemName = item.name;
|
|
1558
|
+
const override = itemName ? customization.overrides[itemName] : undefined;
|
|
1559
|
+
const isCustom = itemName ? customization.customItems.some((ci) => ci.name === itemName) : false;
|
|
1560
|
+
// Create metadata
|
|
1561
|
+
const metadata = {
|
|
1562
|
+
isBuiltIn: !isCustom,
|
|
1563
|
+
isCustom,
|
|
1564
|
+
isHidden: override?.hidden || false,
|
|
1565
|
+
originalPriority: item.priority,
|
|
1566
|
+
originalParentName: parentName,
|
|
1567
|
+
};
|
|
1568
|
+
// Apply ALL overrides: priority AND properties (text, icon, description, etc.)
|
|
1569
|
+
const effectivePriority = override?.priority !== undefined ? override.priority : item.priority;
|
|
1570
|
+
const effectiveItem = {
|
|
1571
|
+
...item,
|
|
1572
|
+
...override?.properties, // Apply property overrides (text, icon, description)
|
|
1573
|
+
priority: effectivePriority,
|
|
1574
|
+
};
|
|
1575
|
+
// Create tree node data with effective (overridden) values
|
|
1576
|
+
const nodeData = {
|
|
1577
|
+
menuItem: effectiveItem,
|
|
1578
|
+
metadata,
|
|
1579
|
+
};
|
|
1580
|
+
// Create tree node using effective (overridden) values
|
|
1581
|
+
const treeNode = {
|
|
1582
|
+
id: itemName || item.path || this.generateNodeId(),
|
|
1583
|
+
title: effectiveItem.text || effectiveItem.path || 'Unnamed',
|
|
1584
|
+
icon: effectiveItem.icon,
|
|
1585
|
+
expanded: false,
|
|
1586
|
+
selected: false,
|
|
1587
|
+
['children']: item.children ? this.convertToTreeNodes(item.children, customization, itemName) : undefined,
|
|
1588
|
+
childrenCount: item.children?.length,
|
|
1589
|
+
data: nodeData,
|
|
1590
|
+
};
|
|
1591
|
+
return treeNode;
|
|
1592
|
+
});
|
|
1593
|
+
// Sort by priority
|
|
1594
|
+
nodes.sort((a, b) => {
|
|
1595
|
+
const aPriority = a['data']?.menuItem.priority ?? 0;
|
|
1596
|
+
const bPriority = b['data']?.menuItem.priority ?? 0;
|
|
1597
|
+
return aPriority - bPriority;
|
|
1598
|
+
});
|
|
1599
|
+
return nodes;
|
|
1600
|
+
}
|
|
1601
|
+
/**
|
|
1602
|
+
* Update customization from tree structure
|
|
1603
|
+
* Extracts priorities and parent relationships from tree nodes
|
|
1604
|
+
*/
|
|
1605
|
+
updateCustomizationFromTree(treeNodes, customization, parentName) {
|
|
1606
|
+
// Calculate base priority for this level to maintain relative ordering
|
|
1607
|
+
const basePriority = parentName ? PRIORITY_BASE_NESTED : PRIORITY_BASE_ROOT;
|
|
1608
|
+
const priorityStep = PRIORITY_STEP;
|
|
1609
|
+
treeNodes.forEach((node, index) => {
|
|
1610
|
+
const nodeData = node['data'] || null;
|
|
1611
|
+
const menuItem = nodeData?.menuItem;
|
|
1612
|
+
if (menuItem?.name) {
|
|
1613
|
+
this.ensureOverrideExists(customization, menuItem.name);
|
|
1614
|
+
// Calculate new priority based on current position
|
|
1615
|
+
const newPriority = basePriority + index * priorityStep;
|
|
1616
|
+
// Get current effective priority (considering existing overrides)
|
|
1617
|
+
const currentEffectivePriority = customization.overrides[menuItem.name]?.priority ?? nodeData.metadata.originalPriority ?? 0;
|
|
1618
|
+
// Always update priority to reflect current position in tree
|
|
1619
|
+
// This ensures reordering works correctly even after multiple moves
|
|
1620
|
+
if (newPriority !== currentEffectivePriority) {
|
|
1621
|
+
customization.overrides[menuItem.name].priority = newPriority;
|
|
1622
|
+
}
|
|
1623
|
+
// Determine current effective parent (considering existing overrides)
|
|
1624
|
+
const currentEffectiveParent = customization.overrides[menuItem.name]?.parentName ?? nodeData.metadata.originalParentName;
|
|
1625
|
+
// Update parent relationship if it changed from current effective parent
|
|
1626
|
+
if (parentName !== currentEffectiveParent) {
|
|
1627
|
+
if (parentName) {
|
|
1628
|
+
customization.overrides[menuItem.name].parentName = parentName;
|
|
1629
|
+
}
|
|
1630
|
+
else {
|
|
1631
|
+
// Moving to root - set to null (not undefined) so middleware will process it
|
|
1632
|
+
// moveTo(null) moves the item to root level
|
|
1633
|
+
customization.overrides[menuItem.name].parentName = null;
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
// Clean up empty overrides
|
|
1637
|
+
this.cleanupEmptyOverride(customization, menuItem.name);
|
|
1638
|
+
// Recursively process children
|
|
1639
|
+
const nodeChildren = node['children'];
|
|
1640
|
+
if (nodeChildren && nodeChildren.length > 0) {
|
|
1641
|
+
this.updateCustomizationFromTree(nodeChildren, customization, menuItem.name);
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
});
|
|
1645
|
+
}
|
|
1646
|
+
/**
|
|
1647
|
+
* Validate menu name before operations
|
|
1648
|
+
*/
|
|
1649
|
+
validateMenuName(menuName, operation) {
|
|
1650
|
+
if (!menuName) {
|
|
1651
|
+
throw new Error(`Menu item must have a name to be ${operation}`);
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
/**
|
|
1655
|
+
* Ensure override object exists for a menu item
|
|
1656
|
+
*/
|
|
1657
|
+
ensureOverrideExists(customization, menuName) {
|
|
1658
|
+
if (!customization.overrides[menuName]) {
|
|
1659
|
+
customization.overrides[menuName] = {};
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
/**
|
|
1663
|
+
* Clean up empty override objects
|
|
1664
|
+
*/
|
|
1665
|
+
cleanupEmptyOverride(customization, menuName) {
|
|
1666
|
+
if (Object.keys(customization.overrides[menuName] || {}).length === 0) {
|
|
1667
|
+
delete customization.overrides[menuName];
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
/**
|
|
1671
|
+
* Flatten menu items recursively into a map
|
|
1672
|
+
*/
|
|
1673
|
+
flattenMenuItems(items) {
|
|
1674
|
+
const itemMap = new Map();
|
|
1675
|
+
const flatten = (itemList) => {
|
|
1676
|
+
for (const item of itemList) {
|
|
1677
|
+
if (item.name) {
|
|
1678
|
+
// Clone item without children to rebuild structure
|
|
1679
|
+
itemMap.set(item.name, { ...item, children: undefined });
|
|
1680
|
+
}
|
|
1681
|
+
if (item.children) {
|
|
1682
|
+
flatten(item.children);
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
};
|
|
1686
|
+
flatten(items);
|
|
1687
|
+
return itemMap;
|
|
1688
|
+
}
|
|
1689
|
+
/**
|
|
1690
|
+
* Generate unique ID for tree nodes
|
|
1691
|
+
*/
|
|
1692
|
+
generateNodeId() {
|
|
1693
|
+
return `node-${Date.now()}-${Math.floor(Math.random() * 10000)}`;
|
|
1694
|
+
}
|
|
1695
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXPMenuManagementService, deps: null, target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
1696
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXPMenuManagementService, providedIn: 'root' }); }
|
|
1697
|
+
}
|
|
1698
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXPMenuManagementService, decorators: [{
|
|
1699
|
+
type: Injectable,
|
|
1700
|
+
args: [{ providedIn: 'root' }]
|
|
1701
|
+
}] });
|
|
1702
|
+
|
|
1703
|
+
//#region ---- Imports ----
|
|
1704
|
+
//#endregion
|
|
1705
|
+
//#region ---- Module ----
|
|
1706
|
+
/**
|
|
1707
|
+
* Module for menu management features including the menu customizer command.
|
|
1708
|
+
*/
|
|
1709
|
+
class AXMMenuManagementModule {
|
|
1710
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXMMenuManagementModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
|
|
1711
|
+
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "22.0.8", ngImport: i0, type: AXMMenuManagementModule }); }
|
|
1712
|
+
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXMMenuManagementModule, providers: [
|
|
1713
|
+
{
|
|
1714
|
+
provide: AXP_MENU_CUSTOMIZER_SERVICE,
|
|
1715
|
+
useExisting: AXPMenuManagementService,
|
|
1716
|
+
},
|
|
1717
|
+
provideCommandSetups([
|
|
1718
|
+
{
|
|
1719
|
+
key: 'customize-menu',
|
|
1720
|
+
command: () => import('./acorex-platform-module-platform-management-client-customize-menu.command-2319iHsD.mjs').then((c) => c.AXMCustomizeMenuCommand),
|
|
1721
|
+
},
|
|
1722
|
+
]),
|
|
1723
|
+
] }); }
|
|
1724
|
+
}
|
|
1725
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXMMenuManagementModule, decorators: [{
|
|
1726
|
+
type: NgModule,
|
|
1727
|
+
args: [{
|
|
1728
|
+
providers: [
|
|
1729
|
+
{
|
|
1730
|
+
provide: AXP_MENU_CUSTOMIZER_SERVICE,
|
|
1731
|
+
useExisting: AXPMenuManagementService,
|
|
1732
|
+
},
|
|
1733
|
+
provideCommandSetups([
|
|
1734
|
+
{
|
|
1735
|
+
key: 'customize-menu',
|
|
1736
|
+
command: () => import('./acorex-platform-module-platform-management-client-customize-menu.command-2319iHsD.mjs').then((c) => c.AXMCustomizeMenuCommand),
|
|
1737
|
+
},
|
|
1738
|
+
]),
|
|
1739
|
+
],
|
|
1740
|
+
}]
|
|
1741
|
+
}] });
|
|
1742
|
+
|
|
1743
|
+
/**
|
|
1744
|
+
* Token that returns current ISO date-time string.
|
|
1745
|
+
*/
|
|
1746
|
+
class AXMTokenProvider {
|
|
1747
|
+
constructor() {
|
|
1748
|
+
this.name = 'now';
|
|
1749
|
+
this.title = 'Now';
|
|
1750
|
+
this.description = 'Current date and time in ISO format';
|
|
1751
|
+
}
|
|
1752
|
+
execute() {
|
|
1753
|
+
return Promise.resolve(new Date().toISOString());
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
//#region ----- Imports -----
|
|
1758
|
+
//#endregion
|
|
1759
|
+
//#region ----- Factory Cache -----
|
|
1760
|
+
/**
|
|
1761
|
+
* Global cache for loaded token providers.
|
|
1762
|
+
* Cleared when token entity changes occur.
|
|
1763
|
+
*/
|
|
1764
|
+
let cachedProviders = null;
|
|
1765
|
+
/**
|
|
1766
|
+
* Clears the cached providers, forcing reload on next access.
|
|
1767
|
+
*/
|
|
1768
|
+
function clearDynamicTokenProvidersCache() {
|
|
1769
|
+
cachedProviders = null;
|
|
1770
|
+
}
|
|
1771
|
+
//#endregion
|
|
1772
|
+
//#region ----- Token Data Access -----
|
|
1773
|
+
/**
|
|
1774
|
+
* Resolves the token entity data accessor after the current DI cycle completes.
|
|
1775
|
+
* AXPEntityService cannot be resolved synchronously from the dynamic token provider
|
|
1776
|
+
* factory because AXP_TOKEN_DEFINITION_PROVIDER is still being constructed (cycle via
|
|
1777
|
+
* AXPTokenDefinitionService and expression evaluator scope providers).
|
|
1778
|
+
*/
|
|
1779
|
+
async function getTokenEntityDataAccessor(injector) {
|
|
1780
|
+
await Promise.resolve();
|
|
1781
|
+
return injector
|
|
1782
|
+
.get(AXPEntityService)
|
|
1783
|
+
.withEntity(RootConfig.module.name, RootConfig.entities.token.name)
|
|
1784
|
+
.data();
|
|
1785
|
+
}
|
|
1786
|
+
//#endregion
|
|
1787
|
+
//#region ----- Factory Function -----
|
|
1788
|
+
/**
|
|
1789
|
+
* Factory function that loads tokens from backend and creates providers for each.
|
|
1790
|
+
* Value resolution always runs on the server (`PlatformManagement.Tokens:Resolve`).
|
|
1791
|
+
*/
|
|
1792
|
+
async function createDynamicTokenProviders(injector) {
|
|
1793
|
+
if (cachedProviders) {
|
|
1794
|
+
return cachedProviders;
|
|
1795
|
+
}
|
|
1796
|
+
cachedProviders = loadTokenProviders(injector);
|
|
1797
|
+
return cachedProviders;
|
|
1798
|
+
}
|
|
1799
|
+
async function loadTokenProviders(injector) {
|
|
1800
|
+
const sessionService = injector.get(AXPSessionService);
|
|
1801
|
+
if (!sessionService.isReadyForAppScopedApi) {
|
|
1802
|
+
return [];
|
|
1803
|
+
}
|
|
1804
|
+
const tokensService = await getTokenEntityDataAccessor(injector);
|
|
1805
|
+
try {
|
|
1806
|
+
const queryResult = await tokensService.query({
|
|
1807
|
+
skip: 0,
|
|
1808
|
+
take: 1000,
|
|
1809
|
+
});
|
|
1810
|
+
if (!queryResult?.items?.length) {
|
|
1811
|
+
return [];
|
|
1812
|
+
}
|
|
1813
|
+
return queryResult.items.map((token) => createTokenProvider(token, injector));
|
|
1814
|
+
}
|
|
1815
|
+
catch (error) {
|
|
1816
|
+
console.error('[DynamicTokenProvider] Failed to load tokens from backend:', error);
|
|
1817
|
+
throw error;
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
/**
|
|
1821
|
+
* Creates a token provider that resolves values via the server command.
|
|
1822
|
+
*/
|
|
1823
|
+
function createTokenProvider(token, injector) {
|
|
1824
|
+
const tokenName = token.name;
|
|
1825
|
+
return {
|
|
1826
|
+
name: tokenName,
|
|
1827
|
+
title: token.title,
|
|
1828
|
+
description: token.description,
|
|
1829
|
+
async execute() {
|
|
1830
|
+
const commandExecutor = injector.get(AXPCommandExecutor);
|
|
1831
|
+
const result = await commandExecutor.execute(AXM_TOKEN_RESOLVE_COMMAND_KEY, { tokenName });
|
|
1832
|
+
const __single_result = runtimeCommandPayloadData(result);
|
|
1833
|
+
if (!__single_result || __single_result.value === undefined || __single_result.value === null) {
|
|
1834
|
+
const text = result?.message?.text;
|
|
1835
|
+
const message = (typeof text === 'string' && text) ||
|
|
1836
|
+
`Token "${tokenName}" not found or returned no value`;
|
|
1837
|
+
throw new Error(message);
|
|
1838
|
+
}
|
|
1839
|
+
return __single_result.value;
|
|
1840
|
+
},
|
|
1841
|
+
};
|
|
1842
|
+
}
|
|
1843
|
+
//#endregion
|
|
1844
|
+
|
|
1845
|
+
//#region ---- Imports ----
|
|
1846
|
+
//#endregion
|
|
1847
|
+
//#region ---- Token Refresh Event Listener ----
|
|
1848
|
+
/**
|
|
1849
|
+
* Event listener provider that listens to token entity changes
|
|
1850
|
+
* and clears the token cache when tokens are updated, inserted, or deleted.
|
|
1851
|
+
*/
|
|
1852
|
+
class AXMTokenRefreshProvider {
|
|
1853
|
+
constructor() {
|
|
1854
|
+
this.tokenService = inject(AXPTokenDefinitionService);
|
|
1855
|
+
/**
|
|
1856
|
+
* Event keys to listen to for token entity changes.
|
|
1857
|
+
* Supports wildcard patterns for entity events.
|
|
1858
|
+
* Format: entity.{operation}.{ModuleName}.{EntityName}
|
|
1859
|
+
* Example: entity.updated.PlatformManagement.Tokens
|
|
1860
|
+
*/
|
|
1861
|
+
this.key = `entity.*.${RootConfig.module.name}.${RootConfig.entities.token.name}`;
|
|
1862
|
+
}
|
|
1863
|
+
/**
|
|
1864
|
+
* Executes when a token entity event occurs (inserted, updated, or deleted).
|
|
1865
|
+
* Clears both the token definition service cache and the dynamic providers cache.
|
|
1866
|
+
*/
|
|
1867
|
+
async execute(data) {
|
|
1868
|
+
// Clear token definition service cache
|
|
1869
|
+
this.tokenService.clearDefinitionsCache();
|
|
1870
|
+
// Clear dynamic token providers cache to force reload
|
|
1871
|
+
clearDynamicTokenProvidersCache();
|
|
1872
|
+
}
|
|
1873
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXMTokenRefreshProvider, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
1874
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXMTokenRefreshProvider }); }
|
|
1875
|
+
}
|
|
1876
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXMTokenRefreshProvider, decorators: [{
|
|
1877
|
+
type: Injectable
|
|
1878
|
+
}] });
|
|
1879
|
+
|
|
1880
|
+
//#region ---- Imports ----
|
|
1881
|
+
//#endregion
|
|
1882
|
+
//#region ---- Token Module ----
|
|
1883
|
+
/**
|
|
1884
|
+
* Token feature module.
|
|
1885
|
+
* Provides token definition providers (static and dynamic) and refresh listeners.
|
|
1886
|
+
*/
|
|
1887
|
+
class AXMTokenModule {
|
|
1888
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXMTokenModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
|
|
1889
|
+
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "22.0.8", ngImport: i0, type: AXMTokenModule }); }
|
|
1890
|
+
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXMTokenModule, providers: [
|
|
1891
|
+
// Static token provider (built-in tokens)
|
|
1892
|
+
{
|
|
1893
|
+
provide: AXP_TOKEN_DEFINITION_PROVIDER,
|
|
1894
|
+
useClass: AXMTokenProvider,
|
|
1895
|
+
multi: true,
|
|
1896
|
+
},
|
|
1897
|
+
// Dynamic token provider (tokens from backend)
|
|
1898
|
+
{
|
|
1899
|
+
provide: AXP_TOKEN_DEFINITION_PROVIDER,
|
|
1900
|
+
useFactory: (injector) => createDynamicTokenProviders(injector),
|
|
1901
|
+
deps: [Injector],
|
|
1902
|
+
multi: true,
|
|
1903
|
+
},
|
|
1904
|
+
// Token refresh listener (clears cache on entity changes)
|
|
1905
|
+
{
|
|
1906
|
+
provide: AXP_DISTRIBUTED_EVENT_LISTENER_PROVIDER,
|
|
1907
|
+
useClass: AXMTokenRefreshProvider,
|
|
1908
|
+
multi: true,
|
|
1909
|
+
},
|
|
1910
|
+
// Execute token command
|
|
1911
|
+
provideCommandSetups([
|
|
1912
|
+
{
|
|
1913
|
+
key: `${RootConfig.module.name}.${RootConfig.entities.token.name}:Execute`,
|
|
1914
|
+
command: () => import('./acorex-platform-module-platform-management-client-execute-token.command-DNuHR-0y.mjs').then((c) => c.ExecuteTokenCommand),
|
|
1915
|
+
},
|
|
1916
|
+
]),
|
|
1917
|
+
] }); }
|
|
1918
|
+
}
|
|
1919
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXMTokenModule, decorators: [{
|
|
1920
|
+
type: NgModule,
|
|
1921
|
+
args: [{
|
|
1922
|
+
imports: [],
|
|
1923
|
+
exports: [],
|
|
1924
|
+
declarations: [],
|
|
1925
|
+
providers: [
|
|
1926
|
+
// Static token provider (built-in tokens)
|
|
1927
|
+
{
|
|
1928
|
+
provide: AXP_TOKEN_DEFINITION_PROVIDER,
|
|
1929
|
+
useClass: AXMTokenProvider,
|
|
1930
|
+
multi: true,
|
|
1931
|
+
},
|
|
1932
|
+
// Dynamic token provider (tokens from backend)
|
|
1933
|
+
{
|
|
1934
|
+
provide: AXP_TOKEN_DEFINITION_PROVIDER,
|
|
1935
|
+
useFactory: (injector) => createDynamicTokenProviders(injector),
|
|
1936
|
+
deps: [Injector],
|
|
1937
|
+
multi: true,
|
|
1938
|
+
},
|
|
1939
|
+
// Token refresh listener (clears cache on entity changes)
|
|
1940
|
+
{
|
|
1941
|
+
provide: AXP_DISTRIBUTED_EVENT_LISTENER_PROVIDER,
|
|
1942
|
+
useClass: AXMTokenRefreshProvider,
|
|
1943
|
+
multi: true,
|
|
1944
|
+
},
|
|
1945
|
+
// Execute token command
|
|
1946
|
+
provideCommandSetups([
|
|
1947
|
+
{
|
|
1948
|
+
key: `${RootConfig.module.name}.${RootConfig.entities.token.name}:Execute`,
|
|
1949
|
+
command: () => import('./acorex-platform-module-platform-management-client-execute-token.command-DNuHR-0y.mjs').then((c) => c.ExecuteTokenCommand),
|
|
1950
|
+
},
|
|
1951
|
+
]),
|
|
1952
|
+
],
|
|
1953
|
+
}]
|
|
1954
|
+
}] });
|
|
1955
|
+
|
|
1956
|
+
class AXPUpdaterService {
|
|
1957
|
+
constructor() {
|
|
1958
|
+
const swUpdate = inject(SwUpdate, { optional: true });
|
|
1959
|
+
const dialogService = inject(AXDialogService);
|
|
1960
|
+
const translateService = inject(AXTranslationService);
|
|
1961
|
+
if (swUpdate?.isEnabled) {
|
|
1962
|
+
swUpdate.versionUpdates.subscribe((event) => {
|
|
1963
|
+
if (event.type === 'VERSION_READY') {
|
|
1964
|
+
console.log('New version available');
|
|
1965
|
+
const scope = RootConfig.config.i18n;
|
|
1966
|
+
const dialog = dialogService.open({
|
|
1967
|
+
icon: 'fa-regular fa-info',
|
|
1968
|
+
content: translateService.translateSync('updater.content', { scope: scope }),
|
|
1969
|
+
title: translateService.translateSync('updater.title', { scope: scope }),
|
|
1970
|
+
type: 'primary',
|
|
1971
|
+
orientation: 'horizontal',
|
|
1972
|
+
buttons: [
|
|
1973
|
+
{
|
|
1974
|
+
text: translateService.translateSync('updater.update', { scope: scope }),
|
|
1975
|
+
color: 'primary',
|
|
1976
|
+
onClick: async (e) => {
|
|
1977
|
+
try {
|
|
1978
|
+
document.location.reload();
|
|
1979
|
+
dialog.close();
|
|
1980
|
+
}
|
|
1981
|
+
catch (error) {
|
|
1982
|
+
console.error(error);
|
|
1983
|
+
}
|
|
1984
|
+
},
|
|
1985
|
+
},
|
|
1986
|
+
{
|
|
1987
|
+
text: translateService.translateSync('updater.cancel', { scope: scope }),
|
|
1988
|
+
color: 'default',
|
|
1989
|
+
autofocus: true,
|
|
1990
|
+
onClick: (e) => {
|
|
1991
|
+
dialog.close();
|
|
1992
|
+
},
|
|
1993
|
+
},
|
|
1994
|
+
],
|
|
1995
|
+
closeButton: false,
|
|
1996
|
+
});
|
|
1997
|
+
}
|
|
1998
|
+
});
|
|
1999
|
+
}
|
|
2000
|
+
}
|
|
2001
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXPUpdaterService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
2002
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXPUpdaterService, providedIn: 'root' }); }
|
|
2003
|
+
}
|
|
2004
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXPUpdaterService, decorators: [{
|
|
2005
|
+
type: Injectable,
|
|
2006
|
+
args: [{ providedIn: 'root' }]
|
|
2007
|
+
}], ctorParameters: () => [] });
|
|
2008
|
+
|
|
2009
|
+
class AXMUpdaterModule {
|
|
2010
|
+
constructor(updaterService) {
|
|
2011
|
+
this.updaterService = updaterService;
|
|
2012
|
+
}
|
|
2013
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXMUpdaterModule, deps: [{ token: AXPUpdaterService }], target: i0.ɵɵFactoryTarget.NgModule }); }
|
|
2014
|
+
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "22.0.8", ngImport: i0, type: AXMUpdaterModule }); }
|
|
2015
|
+
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXMUpdaterModule, providers: [AXPUpdaterService] }); }
|
|
2016
|
+
}
|
|
2017
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXMUpdaterModule, decorators: [{
|
|
2018
|
+
type: NgModule,
|
|
2019
|
+
args: [{
|
|
2020
|
+
imports: [],
|
|
2021
|
+
exports: [],
|
|
2022
|
+
declarations: [],
|
|
2023
|
+
providers: [AXPUpdaterService],
|
|
2024
|
+
}]
|
|
2025
|
+
}], ctorParameters: () => [{ type: AXPUpdaterService }] });
|
|
2026
|
+
|
|
2027
|
+
//#region ---- Imports ----
|
|
2028
|
+
//#endregion
|
|
2029
|
+
//#region ---- Manifest Definition ----
|
|
2030
|
+
/**
|
|
2031
|
+
* Platform Management Module Manifest.
|
|
2032
|
+
* Defines module metadata, features, and provider references.
|
|
2033
|
+
*/
|
|
2034
|
+
const PlatformManagementManifest = {
|
|
2035
|
+
name: RootConfig.module.name,
|
|
2036
|
+
version: '1.0.0',
|
|
2037
|
+
title: RootConfig.module.title,
|
|
2038
|
+
icon: RootConfig.module.icon,
|
|
2039
|
+
i18n: RootConfig.config.i18n,
|
|
2040
|
+
type: 'platform',
|
|
2041
|
+
domain: 'platform',
|
|
2042
|
+
dependencies: [
|
|
2043
|
+
'SecurityManagement',
|
|
2044
|
+
'ApplicationManagement',
|
|
2045
|
+
'SystemInsight',
|
|
2046
|
+
'TenantManagement',
|
|
2047
|
+
'SubscriptionManagement',
|
|
2048
|
+
],
|
|
2049
|
+
};
|
|
2050
|
+
//#endregion
|
|
2051
|
+
|
|
2052
|
+
class AXMMenuProvider {
|
|
2053
|
+
constructor() {
|
|
2054
|
+
this.entityService = inject(AXPEntityService);
|
|
2055
|
+
}
|
|
2056
|
+
async provide(context) {
|
|
2057
|
+
const scope = RootConfig.config.i18n;
|
|
2058
|
+
const moduleName = RootConfig.module.name;
|
|
2059
|
+
const platformShellPolicy = {
|
|
2060
|
+
features: [moduleName],
|
|
2061
|
+
permissions: [AXPApplicationsPlatformPermissionKeys.MaintainPlatformShell],
|
|
2062
|
+
};
|
|
2063
|
+
//#region ----- Platform > Tokens -----
|
|
2064
|
+
context.find(AXPCommonMenuKeys.Platform).addItems([
|
|
2065
|
+
{
|
|
2066
|
+
name: AXPPlatformManagementMenuKeys.Tokens,
|
|
2067
|
+
priority: 8,
|
|
2068
|
+
text: `@${scope}:tokens.terms.tokens`,
|
|
2069
|
+
path: this.entityService.createPath(moduleName, RootConfig.entities.token.name),
|
|
2070
|
+
icon: RootConfig.entities.token.icon,
|
|
2071
|
+
policy: platformShellPolicy,
|
|
2072
|
+
},
|
|
2073
|
+
]);
|
|
2074
|
+
//#endregion
|
|
2075
|
+
//#region ----- Configuration > Platform Definitions -----
|
|
2076
|
+
const platformDefinitionsMenu = context.find(AXPCommonMenuKeys.PlatformDefinitions);
|
|
2077
|
+
if (!platformDefinitionsMenu.exists) {
|
|
2078
|
+
context.find(AXPCommonMenuKeys.Configuration).addItems([
|
|
2079
|
+
{
|
|
2080
|
+
name: AXPCommonMenuKeys.PlatformDefinitions,
|
|
2081
|
+
priority: 8946,
|
|
2082
|
+
text: '@general:menus.platform-definitions.title',
|
|
2083
|
+
icon: 'fa-light fa-books',
|
|
2084
|
+
type: 'menu',
|
|
2085
|
+
children: [],
|
|
2086
|
+
policy: platformShellPolicy,
|
|
2087
|
+
},
|
|
2088
|
+
]);
|
|
2089
|
+
}
|
|
2090
|
+
context.find(AXPCommonMenuKeys.PlatformDefinitions).addItems([
|
|
2091
|
+
{
|
|
2092
|
+
name: AXPPlatformManagementMenuKeys.EntityDefinitions,
|
|
2093
|
+
text: `@${scope}:menus.entity-definitions.title`,
|
|
2094
|
+
path: this.entityService.createPath(moduleName, RootConfig.entities.entityDefinition.name),
|
|
2095
|
+
icon: RootConfig.entities.entityDefinition.icon,
|
|
2096
|
+
priority: 1,
|
|
2097
|
+
policy: platformShellPolicy,
|
|
2098
|
+
},
|
|
2099
|
+
{
|
|
2100
|
+
name: AXPPlatformManagementMenuKeys.CommandDefinitions,
|
|
2101
|
+
text: `@${scope}:menus.command-definitions.title`,
|
|
2102
|
+
path: this.entityService.createPath(moduleName, RootConfig.entities.commandDefinition.name),
|
|
2103
|
+
icon: RootConfig.entities.commandDefinition.icon,
|
|
2104
|
+
priority: 2,
|
|
2105
|
+
policy: platformShellPolicy,
|
|
2106
|
+
},
|
|
2107
|
+
{
|
|
2108
|
+
name: AXPPlatformManagementMenuKeys.QueryDefinitions,
|
|
2109
|
+
text: `@${scope}:menus.query-definitions.title`,
|
|
2110
|
+
path: this.entityService.createPath(moduleName, RootConfig.entities.queryDefinition.name),
|
|
2111
|
+
icon: RootConfig.entities.queryDefinition.icon,
|
|
2112
|
+
priority: 3,
|
|
2113
|
+
policy: platformShellPolicy,
|
|
2114
|
+
},
|
|
2115
|
+
{
|
|
2116
|
+
name: AXPPlatformManagementMenuKeys.PluginDefinitions,
|
|
2117
|
+
text: `@${scope}:menus.plugin-definitions.title`,
|
|
2118
|
+
path: this.entityService.createPath(moduleName, RootConfig.entities.pluginDefinition.name),
|
|
2119
|
+
icon: RootConfig.entities.pluginDefinition.icon,
|
|
2120
|
+
priority: 4,
|
|
2121
|
+
policy: platformShellPolicy,
|
|
2122
|
+
},
|
|
2123
|
+
{
|
|
2124
|
+
name: AXPPlatformManagementMenuKeys.Widgets,
|
|
2125
|
+
text: `@${scope}:menus.widgets.title`,
|
|
2126
|
+
path: this.entityService.createPath(moduleName, RootConfig.entities.widget.name),
|
|
2127
|
+
icon: RootConfig.entities.widget.icon,
|
|
2128
|
+
priority: 5,
|
|
2129
|
+
policy: platformShellPolicy,
|
|
2130
|
+
},
|
|
2131
|
+
]);
|
|
2132
|
+
//#endregion
|
|
2133
|
+
}
|
|
2134
|
+
}
|
|
2135
|
+
|
|
2136
|
+
//#endregion
|
|
2137
|
+
//#region ---- Permission Definition Provider ----
|
|
2138
|
+
/**
|
|
2139
|
+
* Registers MaintainPlatformShell under Applications & Platform.
|
|
2140
|
+
*/
|
|
2141
|
+
class AXMPermissionDefinitionProvider {
|
|
2142
|
+
async define(context) {
|
|
2143
|
+
const keys = AXPApplicationsPlatformPermissionKeys;
|
|
2144
|
+
const groups = AXPCommonPermissionGroupKeys;
|
|
2145
|
+
const P = '@common:applications-platform.permissions';
|
|
2146
|
+
const G = '@common:permission-groups';
|
|
2147
|
+
context.addGroup(groups.ApplicationsPlatform.Root, `${G}.applications-platform.title`, `${G}.applications-platform.description`, { order: 110 });
|
|
2148
|
+
context
|
|
2149
|
+
.addGroup(groups.ApplicationsPlatform.TenancyShell, `${G}.applications-platform-tenancy-shell.title`, `${G}.applications-platform-tenancy-shell.description`, { parent: groups.ApplicationsPlatform.Root, order: 20 })
|
|
2150
|
+
.addPermission(keys.MaintainPlatformShell, `${P}.maintain-platform-shell.title`, `${P}.maintain-platform-shell.description`, { modules: ['PlatformManagement'], requiredFeatures: ['PlatformManagement'] })
|
|
2151
|
+
.endPermission()
|
|
2152
|
+
.endGroup();
|
|
2153
|
+
}
|
|
2154
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXMPermissionDefinitionProvider, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
2155
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXMPermissionDefinitionProvider }); }
|
|
2156
|
+
}
|
|
2157
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXMPermissionDefinitionProvider, decorators: [{
|
|
2158
|
+
type: Injectable
|
|
2159
|
+
}] });
|
|
2160
|
+
|
|
2161
|
+
//#region ---- Entity row → registry record ----
|
|
2162
|
+
function mapEntityRowToRegistryRecord(row) {
|
|
2163
|
+
return {
|
|
2164
|
+
name: row.name,
|
|
2165
|
+
active: row.active !== false,
|
|
2166
|
+
description: row.description,
|
|
2167
|
+
catalogTitle: row.catalogTitle,
|
|
2168
|
+
executionMode: row.executionMode,
|
|
2169
|
+
capabilities: row.capabilities,
|
|
2170
|
+
inputOptionDefinitions: row.inputOptionDefinitions,
|
|
2171
|
+
outputOptionDefinitions: row.outputOptionDefinitions,
|
|
2172
|
+
outcomesDefinitionJson: row.outcomesDefinitionJson,
|
|
2173
|
+
aiMetadataJson: row.aiMetadataJson,
|
|
2174
|
+
categories: row.categories,
|
|
2175
|
+
};
|
|
2176
|
+
}
|
|
2177
|
+
//#endregion
|
|
2178
|
+
//#region ---- Platform command catalog CRUD (server-backed) ----
|
|
2179
|
+
/**
|
|
2180
|
+
* Reads command catalog metadata from {@code PlatformManagement.CommandDefinition} entity storage (server DB).
|
|
2181
|
+
* Sidecar source files are never imported in the browser bundle.
|
|
2182
|
+
*/
|
|
2183
|
+
class AXMPlatformCommandCatalogCrudService {
|
|
2184
|
+
constructor() {
|
|
2185
|
+
this.entityService = inject(AXPEntityService);
|
|
2186
|
+
}
|
|
2187
|
+
entityData() {
|
|
2188
|
+
return this.entityService
|
|
2189
|
+
.withEntity(RootConfig.module.name, RootConfig.entities.commandDefinition.name)
|
|
2190
|
+
.data();
|
|
2191
|
+
}
|
|
2192
|
+
async listCommands() {
|
|
2193
|
+
const result = await this.entityData().query({ skip: 0, take: 5000 });
|
|
2194
|
+
const items = (result.items ?? []);
|
|
2195
|
+
return items.map((row) => ({
|
|
2196
|
+
id: String(row.id),
|
|
2197
|
+
definition: mapEntityRowToRegistryRecord(row),
|
|
2198
|
+
}));
|
|
2199
|
+
}
|
|
2200
|
+
async getCommand(id) {
|
|
2201
|
+
const row = await this.entityData().byKey(id);
|
|
2202
|
+
return row ? mapEntityRowToRegistryRecord(row) : null;
|
|
2203
|
+
}
|
|
2204
|
+
// #region ---- Unimplemented catalog stores (Phase 1: commands only) ----
|
|
2205
|
+
async list() {
|
|
2206
|
+
return [];
|
|
2207
|
+
}
|
|
2208
|
+
async getRaw() {
|
|
2209
|
+
return null;
|
|
2210
|
+
}
|
|
2211
|
+
async create() {
|
|
2212
|
+
throw new Error('Entity definition CRUD is not available for command catalog rows.');
|
|
2213
|
+
}
|
|
2214
|
+
async update() {
|
|
2215
|
+
throw new Error('Entity definition CRUD is not available for command catalog rows.');
|
|
2216
|
+
}
|
|
2217
|
+
async delete() {
|
|
2218
|
+
throw new Error('Entity definition CRUD is not available for command catalog rows.');
|
|
2219
|
+
}
|
|
2220
|
+
async listInterfaces() {
|
|
2221
|
+
return [];
|
|
2222
|
+
}
|
|
2223
|
+
async listInterfacesByCategory() {
|
|
2224
|
+
return [];
|
|
2225
|
+
}
|
|
2226
|
+
async getInterface() {
|
|
2227
|
+
return null;
|
|
2228
|
+
}
|
|
2229
|
+
async createInterface() {
|
|
2230
|
+
throw new Error('Not implemented');
|
|
2231
|
+
}
|
|
2232
|
+
async updateInterface() {
|
|
2233
|
+
throw new Error('Not implemented');
|
|
2234
|
+
}
|
|
2235
|
+
async deleteInterface() {
|
|
2236
|
+
throw new Error('Not implemented');
|
|
2237
|
+
}
|
|
2238
|
+
async listPlugins() {
|
|
2239
|
+
return [];
|
|
2240
|
+
}
|
|
2241
|
+
async getPlugin() {
|
|
2242
|
+
return null;
|
|
2243
|
+
}
|
|
2244
|
+
async createPlugin() {
|
|
2245
|
+
throw new Error('Not implemented');
|
|
2246
|
+
}
|
|
2247
|
+
async updatePlugin() {
|
|
2248
|
+
throw new Error('Not implemented');
|
|
2249
|
+
}
|
|
2250
|
+
async deletePlugin() {
|
|
2251
|
+
throw new Error('Not implemented');
|
|
2252
|
+
}
|
|
2253
|
+
async listValidations() {
|
|
2254
|
+
return [];
|
|
2255
|
+
}
|
|
2256
|
+
async getValidation() {
|
|
2257
|
+
return null;
|
|
2258
|
+
}
|
|
2259
|
+
async createValidation() {
|
|
2260
|
+
throw new Error('Not implemented');
|
|
2261
|
+
}
|
|
2262
|
+
async updateValidation() {
|
|
2263
|
+
throw new Error('Not implemented');
|
|
2264
|
+
}
|
|
2265
|
+
async deleteValidation() {
|
|
2266
|
+
throw new Error('Not implemented');
|
|
2267
|
+
}
|
|
2268
|
+
async listRelations() {
|
|
2269
|
+
return [];
|
|
2270
|
+
}
|
|
2271
|
+
async getRelation() {
|
|
2272
|
+
return null;
|
|
2273
|
+
}
|
|
2274
|
+
async createRelation() {
|
|
2275
|
+
throw new Error('Not implemented');
|
|
2276
|
+
}
|
|
2277
|
+
async updateRelation() {
|
|
2278
|
+
throw new Error('Not implemented');
|
|
2279
|
+
}
|
|
2280
|
+
async deleteRelation() {
|
|
2281
|
+
throw new Error('Not implemented');
|
|
2282
|
+
}
|
|
2283
|
+
async listModules() {
|
|
2284
|
+
return [];
|
|
2285
|
+
}
|
|
2286
|
+
async getModule() {
|
|
2287
|
+
return null;
|
|
2288
|
+
}
|
|
2289
|
+
async createModule() {
|
|
2290
|
+
throw new Error('Not implemented');
|
|
2291
|
+
}
|
|
2292
|
+
async updateModule() {
|
|
2293
|
+
throw new Error('Not implemented');
|
|
2294
|
+
}
|
|
2295
|
+
async deleteModule() {
|
|
2296
|
+
throw new Error('Not implemented');
|
|
2297
|
+
}
|
|
2298
|
+
async listAggregates() {
|
|
2299
|
+
return [];
|
|
2300
|
+
}
|
|
2301
|
+
async getAggregate() {
|
|
2302
|
+
return null;
|
|
2303
|
+
}
|
|
2304
|
+
async createAggregate() {
|
|
2305
|
+
throw new Error('Not implemented');
|
|
2306
|
+
}
|
|
2307
|
+
async updateAggregate() {
|
|
2308
|
+
throw new Error('Not implemented');
|
|
2309
|
+
}
|
|
2310
|
+
async deleteAggregate() {
|
|
2311
|
+
throw new Error('Not implemented');
|
|
2312
|
+
}
|
|
2313
|
+
async listMenus() {
|
|
2314
|
+
return [];
|
|
2315
|
+
}
|
|
2316
|
+
async getMenu() {
|
|
2317
|
+
return null;
|
|
2318
|
+
}
|
|
2319
|
+
async createMenu() {
|
|
2320
|
+
throw new Error('Not implemented');
|
|
2321
|
+
}
|
|
2322
|
+
async updateMenu() {
|
|
2323
|
+
throw new Error('Not implemented');
|
|
2324
|
+
}
|
|
2325
|
+
async deleteMenu() {
|
|
2326
|
+
throw new Error('Not implemented');
|
|
2327
|
+
}
|
|
2328
|
+
async listPermissionGroups() {
|
|
2329
|
+
return [];
|
|
2330
|
+
}
|
|
2331
|
+
async getPermissionGroup() {
|
|
2332
|
+
return null;
|
|
2333
|
+
}
|
|
2334
|
+
async createPermissionGroup() {
|
|
2335
|
+
throw new Error('Not implemented');
|
|
2336
|
+
}
|
|
2337
|
+
async updatePermissionGroup() {
|
|
2338
|
+
throw new Error('Not implemented');
|
|
2339
|
+
}
|
|
2340
|
+
async deletePermissionGroup() {
|
|
2341
|
+
throw new Error('Not implemented');
|
|
2342
|
+
}
|
|
2343
|
+
async createCommand() {
|
|
2344
|
+
throw new Error('Not implemented');
|
|
2345
|
+
}
|
|
2346
|
+
async updateCommand() {
|
|
2347
|
+
throw new Error('Not implemented');
|
|
2348
|
+
}
|
|
2349
|
+
async deleteCommand() {
|
|
2350
|
+
throw new Error('Not implemented');
|
|
2351
|
+
}
|
|
2352
|
+
async listQueries() {
|
|
2353
|
+
return [];
|
|
2354
|
+
}
|
|
2355
|
+
async getQuery() {
|
|
2356
|
+
return null;
|
|
2357
|
+
}
|
|
2358
|
+
async createQuery() {
|
|
2359
|
+
throw new Error('Not implemented');
|
|
2360
|
+
}
|
|
2361
|
+
async updateQuery() {
|
|
2362
|
+
throw new Error('Not implemented');
|
|
2363
|
+
}
|
|
2364
|
+
async deleteQuery() {
|
|
2365
|
+
throw new Error('Not implemented');
|
|
2366
|
+
}
|
|
2367
|
+
async listWidgets() {
|
|
2368
|
+
return [];
|
|
2369
|
+
}
|
|
2370
|
+
async getWidget() {
|
|
2371
|
+
return null;
|
|
2372
|
+
}
|
|
2373
|
+
async createWidget() {
|
|
2374
|
+
throw new Error('Not implemented');
|
|
2375
|
+
}
|
|
2376
|
+
async updateWidget() {
|
|
2377
|
+
throw new Error('Not implemented');
|
|
2378
|
+
}
|
|
2379
|
+
async deleteWidget() {
|
|
2380
|
+
throw new Error('Not implemented');
|
|
2381
|
+
}
|
|
2382
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXMPlatformCommandCatalogCrudService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
2383
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXMPlatformCommandCatalogCrudService, providedIn: 'root' }); }
|
|
2384
|
+
}
|
|
2385
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXMPlatformCommandCatalogCrudService, decorators: [{
|
|
2386
|
+
type: Injectable,
|
|
2387
|
+
args: [{ providedIn: 'root' }]
|
|
2388
|
+
}] });
|
|
2389
|
+
function providePlatformCommandCatalogCrudService() {
|
|
2390
|
+
return {
|
|
2391
|
+
provide: AXP_ENTITY_DEFINITION_CRUD_SERVICE,
|
|
2392
|
+
useExisting: AXMPlatformCommandCatalogCrudService,
|
|
2393
|
+
};
|
|
2394
|
+
}
|
|
2395
|
+
|
|
2396
|
+
// Entity Modules
|
|
2397
|
+
//#region ---- Routes Factory ----
|
|
2398
|
+
function routesFactory() {
|
|
2399
|
+
return [
|
|
2400
|
+
{
|
|
2401
|
+
path: ':app/platform-management/menus/:scope',
|
|
2402
|
+
canActivate: [...AXP_PROTECTED_ROUTE_GUARDS],
|
|
2403
|
+
data: { rootLayout: true },
|
|
2404
|
+
loadComponent: () => AXPRootLayoutComponent,
|
|
2405
|
+
children: [
|
|
2406
|
+
{
|
|
2407
|
+
path: '',
|
|
2408
|
+
loadComponent: () => import('./acorex-platform-module-platform-management-client-menu-list.component-C5HH5-Dw.mjs').then((c) => c.AXMMenuListComponent),
|
|
2409
|
+
data: { reuse: true },
|
|
2410
|
+
},
|
|
2411
|
+
],
|
|
2412
|
+
},
|
|
2413
|
+
];
|
|
2414
|
+
}
|
|
2415
|
+
//#endregion
|
|
2416
|
+
class AXMPlatformManagementModule {
|
|
2417
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXMPlatformManagementModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
|
|
2418
|
+
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "22.0.8", ngImport: i0, type: AXMPlatformManagementModule, imports: [AXPWidgetCoreModule,
|
|
2419
|
+
AXMUpdaterModule,
|
|
2420
|
+
AXMTokenModule,
|
|
2421
|
+
AXMMenuManagementModule] }); }
|
|
2422
|
+
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXMPlatformManagementModule, providers: [
|
|
2423
|
+
// Module-specific providers (not part of manifest system)
|
|
2424
|
+
{
|
|
2425
|
+
provide: AXP_MENU_MIDDLEWARE,
|
|
2426
|
+
useClass: AXPCustomMenuMiddleware,
|
|
2427
|
+
multi: true,
|
|
2428
|
+
},
|
|
2429
|
+
{
|
|
2430
|
+
provide: ROUTES,
|
|
2431
|
+
multi: true,
|
|
2432
|
+
useFactory: routesFactory,
|
|
2433
|
+
},
|
|
2434
|
+
// Module Manifest Provider
|
|
2435
|
+
{
|
|
2436
|
+
provide: AXP_MODULE_MANIFEST_PROVIDER,
|
|
2437
|
+
useValue: PlatformManagementManifest,
|
|
2438
|
+
multi: true,
|
|
2439
|
+
},
|
|
2440
|
+
// Provider references (now using tokens)
|
|
2441
|
+
{
|
|
2442
|
+
provide: AXP_PERMISSION_DEFINITION_PROVIDER,
|
|
2443
|
+
useClass: AXMPermissionDefinitionProvider,
|
|
2444
|
+
multi: true,
|
|
2445
|
+
},
|
|
2446
|
+
{
|
|
2447
|
+
provide: AXP_MENU_PROVIDER,
|
|
2448
|
+
useClass: AXMMenuProvider,
|
|
2449
|
+
multi: true,
|
|
2450
|
+
},
|
|
2451
|
+
{
|
|
2452
|
+
provide: AXP_FEATURE_DEFINITION_PROVIDER,
|
|
2453
|
+
useClass: AXPPlatformManagementFeatureDefinitionProvider,
|
|
2454
|
+
multi: true,
|
|
2455
|
+
},
|
|
2456
|
+
{
|
|
2457
|
+
provide: AXP_WIDGET_DEFINITION_PROVIDER,
|
|
2458
|
+
useClass: AXMPlatformManagementWidgetsProvider,
|
|
2459
|
+
multi: true,
|
|
2460
|
+
},
|
|
2461
|
+
providePlatformCommandCatalogCrudService(),
|
|
2462
|
+
provideCommandSetups([
|
|
2463
|
+
{
|
|
2464
|
+
key: 'PlatformManagement:DataListPickEntityItem',
|
|
2465
|
+
command: () => import('./acorex-platform-module-platform-management-client-data-list-pick-entity-item.command-CJVpqPmY.mjs').then((c) => c.AXPDataListPickEntityItemCommand),
|
|
2466
|
+
},
|
|
2467
|
+
{
|
|
2468
|
+
key: 'PlatformManagement:CreateUserQuery',
|
|
2469
|
+
command: () => import('./acorex-platform-module-platform-management-client-create-user-query.command-2Qu7aoCp.mjs').then((c) => c.AXMCreateUserQueryCommand),
|
|
2470
|
+
},
|
|
2471
|
+
]),
|
|
2472
|
+
provideQuerySetups([
|
|
2473
|
+
{
|
|
2474
|
+
key: 'PlatformManagement:ListRegisteredEntities',
|
|
2475
|
+
loader: () => import('./acorex-platform-module-platform-management-client-list-registered-entities.query-DjStd3a8.mjs').then((c) => c.AXMListRegisteredEntitiesQuery),
|
|
2476
|
+
},
|
|
2477
|
+
{
|
|
2478
|
+
key: 'PlatformManagement:ListRegisteredQueries',
|
|
2479
|
+
loader: () => import('./acorex-platform-module-platform-management-client-list-registered-queries.query-C-IfkTel.mjs').then((c) => c.AXMListRegisteredQueriesQuery),
|
|
2480
|
+
},
|
|
2481
|
+
{
|
|
2482
|
+
key: 'PlatformManagement:ListPlatformNavigationForAi',
|
|
2483
|
+
loader: () => import('./acorex-platform-module-platform-management-client-list-platform-navigation-for-ai.query-2oHMITw-.mjs').then((c) => c.AXMListPlatformNavigationForAiQuery),
|
|
2484
|
+
},
|
|
2485
|
+
{
|
|
2486
|
+
key: 'PlatformManagement:ListCatalogScopes',
|
|
2487
|
+
loader: () => import('./acorex-platform-module-platform-management-client-list-catalog-scopes.query-ByhDTWE6.mjs').then((c) => c.AXMListCatalogScopesQuery),
|
|
2488
|
+
},
|
|
2489
|
+
{
|
|
2490
|
+
key: 'PlatformManagement:ListEditorWidgets',
|
|
2491
|
+
loader: () => import('./acorex-platform-module-platform-management-client-list-editor-widgets.query-DITPMmbw.mjs').then((c) => c.AXMListEditorWidgetsQuery),
|
|
2492
|
+
},
|
|
2493
|
+
{
|
|
2494
|
+
key: 'PlatformManagement:Dashboard:TenantsKpi',
|
|
2495
|
+
loader: () => import('./acorex-platform-module-platform-management-client-platform-console-kpi.queries-BkPdhVq_.mjs').then((c) => c.AXMPlatformTenantsKpiQuery),
|
|
2496
|
+
},
|
|
2497
|
+
{
|
|
2498
|
+
key: 'PlatformManagement:Dashboard:UsersKpi',
|
|
2499
|
+
loader: () => import('./acorex-platform-module-platform-management-client-platform-console-kpi.queries-BkPdhVq_.mjs').then((c) => c.AXMPlatformUsersKpiQuery),
|
|
2500
|
+
},
|
|
2501
|
+
{
|
|
2502
|
+
key: 'PlatformManagement:Dashboard:ApplicationsKpi',
|
|
2503
|
+
loader: () => import('./acorex-platform-module-platform-management-client-platform-console-kpi.queries-BkPdhVq_.mjs').then((c) => c.AXMPlatformApplicationsKpiQuery),
|
|
2504
|
+
},
|
|
2505
|
+
{
|
|
2506
|
+
key: 'PlatformManagement:Dashboard:ErrorsKpi',
|
|
2507
|
+
loader: () => import('./acorex-platform-module-platform-management-client-platform-console-kpi.queries-BkPdhVq_.mjs').then((c) => c.AXMPlatformErrorsKpiQuery),
|
|
2508
|
+
},
|
|
2509
|
+
{
|
|
2510
|
+
key: 'PlatformManagement:Dashboard:TenantsByStatusBar',
|
|
2511
|
+
loader: () => import('./acorex-platform-module-platform-management-client-platform-console-charts.queries-CQ34R13-.mjs').then((c) => c.AXMPlatformTenantsByStatusBarQuery),
|
|
2512
|
+
},
|
|
2513
|
+
{
|
|
2514
|
+
key: 'PlatformManagement:Dashboard:ApplicationsByEditionDonut',
|
|
2515
|
+
loader: () => import('./acorex-platform-module-platform-management-client-platform-console-charts.queries-CQ34R13-.mjs').then((c) => c.AXMPlatformApplicationsByEditionDonutQuery),
|
|
2516
|
+
},
|
|
2517
|
+
{
|
|
2518
|
+
key: 'PlatformManagement:Dashboard:ErrorsTrendLine',
|
|
2519
|
+
loader: () => import('./acorex-platform-module-platform-management-client-platform-console-charts.queries-CQ34R13-.mjs').then((c) => c.AXMPlatformErrorsTrendLineQuery),
|
|
2520
|
+
},
|
|
2521
|
+
{
|
|
2522
|
+
key: 'PlatformManagement:Dashboard:JobsByStatusBar',
|
|
2523
|
+
loader: () => import('./acorex-platform-module-platform-management-client-platform-console-charts.queries-CQ34R13-.mjs').then((c) => c.AXMPlatformJobsByStatusBarQuery),
|
|
2524
|
+
},
|
|
2525
|
+
]),
|
|
2526
|
+
], imports: [AXPWidgetCoreModule,
|
|
2527
|
+
AXMUpdaterModule,
|
|
2528
|
+
AXMTokenModule,
|
|
2529
|
+
AXMMenuManagementModule] }); }
|
|
2530
|
+
}
|
|
2531
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXMPlatformManagementModule, decorators: [{
|
|
2532
|
+
type: NgModule,
|
|
2533
|
+
args: [{
|
|
2534
|
+
imports: [
|
|
2535
|
+
AXPWidgetCoreModule,
|
|
2536
|
+
AXMUpdaterModule,
|
|
2537
|
+
AXMTokenModule,
|
|
2538
|
+
AXMMenuManagementModule,
|
|
2539
|
+
// Entity Modules
|
|
2540
|
+
],
|
|
2541
|
+
exports: [],
|
|
2542
|
+
declarations: [],
|
|
2543
|
+
providers: [
|
|
2544
|
+
// Module-specific providers (not part of manifest system)
|
|
2545
|
+
{
|
|
2546
|
+
provide: AXP_MENU_MIDDLEWARE,
|
|
2547
|
+
useClass: AXPCustomMenuMiddleware,
|
|
2548
|
+
multi: true,
|
|
2549
|
+
},
|
|
2550
|
+
{
|
|
2551
|
+
provide: ROUTES,
|
|
2552
|
+
multi: true,
|
|
2553
|
+
useFactory: routesFactory,
|
|
2554
|
+
},
|
|
2555
|
+
// Module Manifest Provider
|
|
2556
|
+
{
|
|
2557
|
+
provide: AXP_MODULE_MANIFEST_PROVIDER,
|
|
2558
|
+
useValue: PlatformManagementManifest,
|
|
2559
|
+
multi: true,
|
|
2560
|
+
},
|
|
2561
|
+
// Provider references (now using tokens)
|
|
2562
|
+
{
|
|
2563
|
+
provide: AXP_PERMISSION_DEFINITION_PROVIDER,
|
|
2564
|
+
useClass: AXMPermissionDefinitionProvider,
|
|
2565
|
+
multi: true,
|
|
2566
|
+
},
|
|
2567
|
+
{
|
|
2568
|
+
provide: AXP_MENU_PROVIDER,
|
|
2569
|
+
useClass: AXMMenuProvider,
|
|
2570
|
+
multi: true,
|
|
2571
|
+
},
|
|
2572
|
+
{
|
|
2573
|
+
provide: AXP_FEATURE_DEFINITION_PROVIDER,
|
|
2574
|
+
useClass: AXPPlatformManagementFeatureDefinitionProvider,
|
|
2575
|
+
multi: true,
|
|
2576
|
+
},
|
|
2577
|
+
{
|
|
2578
|
+
provide: AXP_WIDGET_DEFINITION_PROVIDER,
|
|
2579
|
+
useClass: AXMPlatformManagementWidgetsProvider,
|
|
2580
|
+
multi: true,
|
|
2581
|
+
},
|
|
2582
|
+
providePlatformCommandCatalogCrudService(),
|
|
2583
|
+
provideCommandSetups([
|
|
2584
|
+
{
|
|
2585
|
+
key: 'PlatformManagement:DataListPickEntityItem',
|
|
2586
|
+
command: () => import('./acorex-platform-module-platform-management-client-data-list-pick-entity-item.command-CJVpqPmY.mjs').then((c) => c.AXPDataListPickEntityItemCommand),
|
|
2587
|
+
},
|
|
2588
|
+
{
|
|
2589
|
+
key: 'PlatformManagement:CreateUserQuery',
|
|
2590
|
+
command: () => import('./acorex-platform-module-platform-management-client-create-user-query.command-2Qu7aoCp.mjs').then((c) => c.AXMCreateUserQueryCommand),
|
|
2591
|
+
},
|
|
2592
|
+
]),
|
|
2593
|
+
provideQuerySetups([
|
|
2594
|
+
{
|
|
2595
|
+
key: 'PlatformManagement:ListRegisteredEntities',
|
|
2596
|
+
loader: () => import('./acorex-platform-module-platform-management-client-list-registered-entities.query-DjStd3a8.mjs').then((c) => c.AXMListRegisteredEntitiesQuery),
|
|
2597
|
+
},
|
|
2598
|
+
{
|
|
2599
|
+
key: 'PlatformManagement:ListRegisteredQueries',
|
|
2600
|
+
loader: () => import('./acorex-platform-module-platform-management-client-list-registered-queries.query-C-IfkTel.mjs').then((c) => c.AXMListRegisteredQueriesQuery),
|
|
2601
|
+
},
|
|
2602
|
+
{
|
|
2603
|
+
key: 'PlatformManagement:ListPlatformNavigationForAi',
|
|
2604
|
+
loader: () => import('./acorex-platform-module-platform-management-client-list-platform-navigation-for-ai.query-2oHMITw-.mjs').then((c) => c.AXMListPlatformNavigationForAiQuery),
|
|
2605
|
+
},
|
|
2606
|
+
{
|
|
2607
|
+
key: 'PlatformManagement:ListCatalogScopes',
|
|
2608
|
+
loader: () => import('./acorex-platform-module-platform-management-client-list-catalog-scopes.query-ByhDTWE6.mjs').then((c) => c.AXMListCatalogScopesQuery),
|
|
2609
|
+
},
|
|
2610
|
+
{
|
|
2611
|
+
key: 'PlatformManagement:ListEditorWidgets',
|
|
2612
|
+
loader: () => import('./acorex-platform-module-platform-management-client-list-editor-widgets.query-DITPMmbw.mjs').then((c) => c.AXMListEditorWidgetsQuery),
|
|
2613
|
+
},
|
|
2614
|
+
{
|
|
2615
|
+
key: 'PlatformManagement:Dashboard:TenantsKpi',
|
|
2616
|
+
loader: () => import('./acorex-platform-module-platform-management-client-platform-console-kpi.queries-BkPdhVq_.mjs').then((c) => c.AXMPlatformTenantsKpiQuery),
|
|
2617
|
+
},
|
|
2618
|
+
{
|
|
2619
|
+
key: 'PlatformManagement:Dashboard:UsersKpi',
|
|
2620
|
+
loader: () => import('./acorex-platform-module-platform-management-client-platform-console-kpi.queries-BkPdhVq_.mjs').then((c) => c.AXMPlatformUsersKpiQuery),
|
|
2621
|
+
},
|
|
2622
|
+
{
|
|
2623
|
+
key: 'PlatformManagement:Dashboard:ApplicationsKpi',
|
|
2624
|
+
loader: () => import('./acorex-platform-module-platform-management-client-platform-console-kpi.queries-BkPdhVq_.mjs').then((c) => c.AXMPlatformApplicationsKpiQuery),
|
|
2625
|
+
},
|
|
2626
|
+
{
|
|
2627
|
+
key: 'PlatformManagement:Dashboard:ErrorsKpi',
|
|
2628
|
+
loader: () => import('./acorex-platform-module-platform-management-client-platform-console-kpi.queries-BkPdhVq_.mjs').then((c) => c.AXMPlatformErrorsKpiQuery),
|
|
2629
|
+
},
|
|
2630
|
+
{
|
|
2631
|
+
key: 'PlatformManagement:Dashboard:TenantsByStatusBar',
|
|
2632
|
+
loader: () => import('./acorex-platform-module-platform-management-client-platform-console-charts.queries-CQ34R13-.mjs').then((c) => c.AXMPlatformTenantsByStatusBarQuery),
|
|
2633
|
+
},
|
|
2634
|
+
{
|
|
2635
|
+
key: 'PlatformManagement:Dashboard:ApplicationsByEditionDonut',
|
|
2636
|
+
loader: () => import('./acorex-platform-module-platform-management-client-platform-console-charts.queries-CQ34R13-.mjs').then((c) => c.AXMPlatformApplicationsByEditionDonutQuery),
|
|
2637
|
+
},
|
|
2638
|
+
{
|
|
2639
|
+
key: 'PlatformManagement:Dashboard:ErrorsTrendLine',
|
|
2640
|
+
loader: () => import('./acorex-platform-module-platform-management-client-platform-console-charts.queries-CQ34R13-.mjs').then((c) => c.AXMPlatformErrorsTrendLineQuery),
|
|
2641
|
+
},
|
|
2642
|
+
{
|
|
2643
|
+
key: 'PlatformManagement:Dashboard:JobsByStatusBar',
|
|
2644
|
+
loader: () => import('./acorex-platform-module-platform-management-client-platform-console-charts.queries-CQ34R13-.mjs').then((c) => c.AXMPlatformJobsByStatusBarQuery),
|
|
2645
|
+
},
|
|
2646
|
+
]),
|
|
2647
|
+
],
|
|
2648
|
+
}]
|
|
2649
|
+
}] });
|
|
2650
|
+
|
|
2651
|
+
/** MIME type for native drag payloads carrying `AXPWorkflowActivityPaletteItem` JSON. */
|
|
2652
|
+
const AXP_WORKFLOW_PALETTE_MIME = 'application/x-axp-workflow-palette-item';
|
|
2653
|
+
|
|
2654
|
+
//#region ---- Ids ----
|
|
2655
|
+
let idCounter = 0;
|
|
2656
|
+
function createWorkflowNodeId() {
|
|
2657
|
+
idCounter += 1;
|
|
2658
|
+
return `wf-node-${Date.now()}-${idCounter}`;
|
|
2659
|
+
}
|
|
2660
|
+
function createWorkflowConnectorId() {
|
|
2661
|
+
idCounter += 1;
|
|
2662
|
+
return `wf-conn-${Date.now()}-${idCounter}`;
|
|
2663
|
+
}
|
|
2664
|
+
function createWorkflowPortId(prefix) {
|
|
2665
|
+
idCounter += 1;
|
|
2666
|
+
return `${prefix}-port-${idCounter}`;
|
|
2667
|
+
}
|
|
2668
|
+
//#endregion
|
|
2669
|
+
//#region ---- Defaults ----
|
|
2670
|
+
const DEFAULT_SIZE = { width: 180, height: 72 };
|
|
2671
|
+
const DEFAULT_INBOUND = [{ side: 'left', ratio: 0.5, key: 'in' }];
|
|
2672
|
+
const DEFAULT_OUTBOUND = [{ side: 'right', ratio: 0.5, key: 'out' }];
|
|
2673
|
+
function cloneDiagram(model) {
|
|
2674
|
+
return {
|
|
2675
|
+
nodes: model.nodes.map((n) => ({
|
|
2676
|
+
...n,
|
|
2677
|
+
position: { ...n.position },
|
|
2678
|
+
size: { ...n.size },
|
|
2679
|
+
inboundPorts: n.inboundPorts.map((p) => ({
|
|
2680
|
+
...p,
|
|
2681
|
+
data: p.data ? { ...p.data } : undefined,
|
|
2682
|
+
})),
|
|
2683
|
+
outboundPorts: n.outboundPorts.map((p) => ({
|
|
2684
|
+
...p,
|
|
2685
|
+
data: p.data ? { ...p.data } : undefined,
|
|
2686
|
+
})),
|
|
2687
|
+
data: n.data ? { ...n.data } : undefined,
|
|
2688
|
+
})),
|
|
2689
|
+
connectors: model.connectors.map((c) => ({
|
|
2690
|
+
...c,
|
|
2691
|
+
data: c.data ? { ...c.data } : undefined,
|
|
2692
|
+
})),
|
|
2693
|
+
data: model.data ? { ...model.data } : undefined,
|
|
2694
|
+
};
|
|
2695
|
+
}
|
|
2696
|
+
function createPortsFromPalette(nodeId, item) {
|
|
2697
|
+
const inboundSrc = item.inboundPorts !== undefined ? item.inboundPorts : DEFAULT_INBOUND;
|
|
2698
|
+
const outboundSrc = item.outboundPorts !== undefined ? item.outboundPorts : DEFAULT_OUTBOUND;
|
|
2699
|
+
const inbound = inboundSrc.map((p) => ({
|
|
2700
|
+
...p,
|
|
2701
|
+
id: createWorkflowPortId(`${nodeId}-in`),
|
|
2702
|
+
kind: 'inbound',
|
|
2703
|
+
data: p.data ? { ...p.data } : undefined,
|
|
2704
|
+
}));
|
|
2705
|
+
const outbound = outboundSrc.map((p) => ({
|
|
2706
|
+
...p,
|
|
2707
|
+
id: createWorkflowPortId(`${nodeId}-out`),
|
|
2708
|
+
kind: 'outbound',
|
|
2709
|
+
data: p.data ? { ...p.data } : undefined,
|
|
2710
|
+
}));
|
|
2711
|
+
return { inbound, outbound };
|
|
2712
|
+
}
|
|
2713
|
+
function createNodeFromPaletteItem(item, position) {
|
|
2714
|
+
const id = createWorkflowNodeId();
|
|
2715
|
+
const { inbound, outbound } = createPortsFromPalette(id, item);
|
|
2716
|
+
const shape = item.shape ?? 'rounded-rectangle';
|
|
2717
|
+
const size = item.size ? { ...item.size } : { ...DEFAULT_SIZE };
|
|
2718
|
+
if (shape === 'circle') {
|
|
2719
|
+
const d = Math.min(size.width, size.height);
|
|
2720
|
+
size.width = d;
|
|
2721
|
+
size.height = d;
|
|
2722
|
+
}
|
|
2723
|
+
return {
|
|
2724
|
+
id,
|
|
2725
|
+
type: item.type,
|
|
2726
|
+
title: item.title,
|
|
2727
|
+
icon: item.icon,
|
|
2728
|
+
shape,
|
|
2729
|
+
position: { ...position },
|
|
2730
|
+
size: { ...size },
|
|
2731
|
+
inboundPorts: inbound,
|
|
2732
|
+
outboundPorts: outbound,
|
|
2733
|
+
data: item.data ? { ...item.data } : undefined,
|
|
2734
|
+
lockAspectRatio: item.lockAspectRatio ?? shape === 'circle',
|
|
2735
|
+
snapToGrid: item.snapToGrid,
|
|
2736
|
+
color: item.color,
|
|
2737
|
+
};
|
|
2738
|
+
}
|
|
2739
|
+
/**
|
|
2740
|
+
* Parses drag `dataTransfer` JSON into a partial palette item (requires `type`).
|
|
2741
|
+
* Merge with catalog (`paletteItems`) to obtain `title` and defaults.
|
|
2742
|
+
*/
|
|
2743
|
+
function parsePaletteItemDragPayload(raw) {
|
|
2744
|
+
if (!raw)
|
|
2745
|
+
return null;
|
|
2746
|
+
try {
|
|
2747
|
+
const o = JSON.parse(raw);
|
|
2748
|
+
if (typeof o.type !== 'string')
|
|
2749
|
+
return null;
|
|
2750
|
+
return o;
|
|
2751
|
+
}
|
|
2752
|
+
catch {
|
|
2753
|
+
return null;
|
|
2754
|
+
}
|
|
2755
|
+
}
|
|
2756
|
+
//#endregion
|
|
2757
|
+
//#region ---- Snap ----
|
|
2758
|
+
function snapScalar(value, grid) {
|
|
2759
|
+
if (grid <= 0)
|
|
2760
|
+
return value;
|
|
2761
|
+
return Math.round(value / grid) * grid;
|
|
2762
|
+
}
|
|
2763
|
+
function snapPoint(point, grid) {
|
|
2764
|
+
return { x: snapScalar(point.x, grid), y: snapScalar(point.y, grid) };
|
|
2765
|
+
}
|
|
2766
|
+
//#endregion
|
|
2767
|
+
//#region ---- Port anchor (node-local, top-left origin) ----
|
|
2768
|
+
/**
|
|
2769
|
+
* Inbound/outbound anchor on an axis-aligned ellipse inscribed in `w`×`h` (centered),
|
|
2770
|
+
* so ports sit on the true perimeter for circle/ellipse nodes.
|
|
2771
|
+
*/
|
|
2772
|
+
function portAnchorOnEllipsePerimeter(w, h, side, ratio) {
|
|
2773
|
+
const r = Math.min(0.999, Math.max(0.001, ratio));
|
|
2774
|
+
const cx = w * 0.5;
|
|
2775
|
+
const cy = h * 0.5;
|
|
2776
|
+
const rx = w * 0.5;
|
|
2777
|
+
const ry = h * 0.5;
|
|
2778
|
+
let theta = 0;
|
|
2779
|
+
switch (side) {
|
|
2780
|
+
case 'right':
|
|
2781
|
+
theta = -Math.PI / 2 + Math.PI * r;
|
|
2782
|
+
break;
|
|
2783
|
+
case 'left':
|
|
2784
|
+
theta = Math.PI / 2 + Math.PI * r;
|
|
2785
|
+
break;
|
|
2786
|
+
case 'top':
|
|
2787
|
+
theta = Math.PI + Math.PI * r;
|
|
2788
|
+
break;
|
|
2789
|
+
case 'bottom':
|
|
2790
|
+
theta = Math.PI * r;
|
|
2791
|
+
break;
|
|
2792
|
+
default:
|
|
2793
|
+
theta = 0;
|
|
2794
|
+
}
|
|
2795
|
+
return {
|
|
2796
|
+
x: cx + rx * Math.cos(theta),
|
|
2797
|
+
y: cy + ry * Math.sin(theta),
|
|
2798
|
+
};
|
|
2799
|
+
}
|
|
2800
|
+
function portAnchorOnBoxEdge(w, h, side, ratio) {
|
|
2801
|
+
const r = Math.min(0.999, Math.max(0.001, ratio));
|
|
2802
|
+
switch (side) {
|
|
2803
|
+
case 'top':
|
|
2804
|
+
return { x: w * r, y: 0 };
|
|
2805
|
+
case 'bottom':
|
|
2806
|
+
return { x: w * r, y: h };
|
|
2807
|
+
case 'left':
|
|
2808
|
+
return { x: 0, y: h * r };
|
|
2809
|
+
case 'right':
|
|
2810
|
+
return { x: w, y: h * r };
|
|
2811
|
+
default:
|
|
2812
|
+
return { x: w * 0.5, y: h * 0.5 };
|
|
2813
|
+
}
|
|
2814
|
+
}
|
|
2815
|
+
/** Diamond perimeter of a rotated square inside the node bounds. */
|
|
2816
|
+
function portAnchorOnDiamondPerimeter(w, h, side, ratio) {
|
|
2817
|
+
const r = Math.min(0.999, Math.max(0.001, ratio));
|
|
2818
|
+
const top = { x: w * 0.5, y: 0 };
|
|
2819
|
+
const right = { x: w, y: h * 0.5 };
|
|
2820
|
+
const bottom = { x: w * 0.5, y: h };
|
|
2821
|
+
const left = { x: 0, y: h * 0.5 };
|
|
2822
|
+
const lerp = (a, b, t) => ({
|
|
2823
|
+
x: a.x + (b.x - a.x) * t,
|
|
2824
|
+
y: a.y + (b.y - a.y) * t,
|
|
2825
|
+
});
|
|
2826
|
+
switch (side) {
|
|
2827
|
+
case 'top':
|
|
2828
|
+
return r <= 0.5 ? lerp(left, top, r * 2) : lerp(top, right, (r - 0.5) * 2);
|
|
2829
|
+
case 'right':
|
|
2830
|
+
return r <= 0.5 ? lerp(top, right, r * 2) : lerp(right, bottom, (r - 0.5) * 2);
|
|
2831
|
+
case 'bottom':
|
|
2832
|
+
return r <= 0.5 ? lerp(right, bottom, r * 2) : lerp(bottom, left, (r - 0.5) * 2);
|
|
2833
|
+
case 'left':
|
|
2834
|
+
return r <= 0.5 ? lerp(bottom, left, r * 2) : lerp(left, top, (r - 0.5) * 2);
|
|
2835
|
+
default:
|
|
2836
|
+
return { x: w * 0.5, y: h * 0.5 };
|
|
2837
|
+
}
|
|
2838
|
+
}
|
|
2839
|
+
function portAnchorOnNode(node, port) {
|
|
2840
|
+
const { width: w, height: h } = node.size;
|
|
2841
|
+
const r = Math.min(0.999, Math.max(0.001, port.ratio));
|
|
2842
|
+
if (node.shape === 'circle' || node.shape === 'ellipse') {
|
|
2843
|
+
return portAnchorOnEllipsePerimeter(w, h, port.side, r);
|
|
2844
|
+
}
|
|
2845
|
+
if (node.shape === 'diamond') {
|
|
2846
|
+
return portAnchorOnDiamondPerimeter(w, h, port.side, r);
|
|
2847
|
+
}
|
|
2848
|
+
return portAnchorOnBoxEdge(w, h, port.side, r);
|
|
2849
|
+
}
|
|
2850
|
+
function portCanvasPosition(node, port) {
|
|
2851
|
+
const local = portAnchorOnNode(node, port);
|
|
2852
|
+
return { x: node.position.x + local.x, y: node.position.y + local.y };
|
|
2853
|
+
}
|
|
2854
|
+
function inflateRect(r, padding) {
|
|
2855
|
+
return {
|
|
2856
|
+
x: r.position.x - padding,
|
|
2857
|
+
y: r.position.y - padding,
|
|
2858
|
+
width: r.size.width + padding * 2,
|
|
2859
|
+
height: r.size.height + padding * 2,
|
|
2860
|
+
};
|
|
2861
|
+
}
|
|
2862
|
+
function pointInsideRect(p, r) {
|
|
2863
|
+
return p.x > r.x && p.x < r.x + r.width && p.y > r.y && p.y < r.y + r.height;
|
|
2864
|
+
}
|
|
2865
|
+
/**
|
|
2866
|
+
* Returns true if an axis-aligned segment crosses the interior of any obstacle.
|
|
2867
|
+
* Endpoints that only touch the rectangle's edge are not considered a crossing.
|
|
2868
|
+
*/
|
|
2869
|
+
function segmentCrossesObstacles(a, b, obstacles) {
|
|
2870
|
+
const minX = Math.min(a.x, b.x);
|
|
2871
|
+
const maxX = Math.max(a.x, b.x);
|
|
2872
|
+
const minY = Math.min(a.y, b.y);
|
|
2873
|
+
const maxY = Math.max(a.y, b.y);
|
|
2874
|
+
const tolerance = 0.5;
|
|
2875
|
+
for (const r of obstacles) {
|
|
2876
|
+
const right = r.x + r.width;
|
|
2877
|
+
const bottom = r.y + r.height;
|
|
2878
|
+
if (maxX <= r.x + tolerance || minX >= right - tolerance)
|
|
2879
|
+
continue;
|
|
2880
|
+
if (maxY <= r.y + tolerance || minY >= bottom - tolerance)
|
|
2881
|
+
continue;
|
|
2882
|
+
if (Math.abs(a.y - b.y) < 0.0001) {
|
|
2883
|
+
if (a.y > r.y + tolerance && a.y < bottom - tolerance)
|
|
2884
|
+
return true;
|
|
2885
|
+
}
|
|
2886
|
+
else if (Math.abs(a.x - b.x) < 0.0001) {
|
|
2887
|
+
if (a.x > r.x + tolerance && a.x < right - tolerance)
|
|
2888
|
+
return true;
|
|
2889
|
+
}
|
|
2890
|
+
else {
|
|
2891
|
+
return true;
|
|
2892
|
+
}
|
|
2893
|
+
}
|
|
2894
|
+
return false;
|
|
2895
|
+
}
|
|
2896
|
+
function stepOutward(p, side, length) {
|
|
2897
|
+
switch (side) {
|
|
2898
|
+
case 'left':
|
|
2899
|
+
return { x: p.x - length, y: p.y };
|
|
2900
|
+
case 'right':
|
|
2901
|
+
return { x: p.x + length, y: p.y };
|
|
2902
|
+
case 'top':
|
|
2903
|
+
return { x: p.x, y: p.y - length };
|
|
2904
|
+
case 'bottom':
|
|
2905
|
+
return { x: p.x, y: p.y + length };
|
|
2906
|
+
}
|
|
2907
|
+
}
|
|
2908
|
+
function oppositeSide(side) {
|
|
2909
|
+
switch (side) {
|
|
2910
|
+
case 'left':
|
|
2911
|
+
return 'right';
|
|
2912
|
+
case 'right':
|
|
2913
|
+
return 'left';
|
|
2914
|
+
case 'top':
|
|
2915
|
+
return 'bottom';
|
|
2916
|
+
case 'bottom':
|
|
2917
|
+
return 'top';
|
|
2918
|
+
}
|
|
2919
|
+
}
|
|
2920
|
+
function sideAxis(side) {
|
|
2921
|
+
return side === 'left' || side === 'right' ? 'h' : 'v';
|
|
2922
|
+
}
|
|
2923
|
+
/**
|
|
2924
|
+
* Removes consecutive duplicate points and collapses collinear intermediate waypoints.
|
|
2925
|
+
*/
|
|
2926
|
+
function simplifyWaypoints(points) {
|
|
2927
|
+
const deduped = [];
|
|
2928
|
+
for (const p of points) {
|
|
2929
|
+
const last = deduped[deduped.length - 1];
|
|
2930
|
+
if (last && Math.abs(last.x - p.x) < 0.001 && Math.abs(last.y - p.y) < 0.001)
|
|
2931
|
+
continue;
|
|
2932
|
+
deduped.push(p);
|
|
2933
|
+
}
|
|
2934
|
+
if (deduped.length <= 2)
|
|
2935
|
+
return deduped;
|
|
2936
|
+
const out = [deduped[0]];
|
|
2937
|
+
for (let i = 1; i < deduped.length - 1; i++) {
|
|
2938
|
+
const prev = deduped[i - 1];
|
|
2939
|
+
const curr = deduped[i];
|
|
2940
|
+
const next = deduped[i + 1];
|
|
2941
|
+
const collinearH = Math.abs(prev.y - curr.y) < 0.001 && Math.abs(curr.y - next.y) < 0.001;
|
|
2942
|
+
const collinearV = Math.abs(prev.x - curr.x) < 0.001 && Math.abs(curr.x - next.x) < 0.001;
|
|
2943
|
+
if (!collinearH && !collinearV)
|
|
2944
|
+
out.push(curr);
|
|
2945
|
+
}
|
|
2946
|
+
out.push(deduped[deduped.length - 1]);
|
|
2947
|
+
return out;
|
|
2948
|
+
}
|
|
2949
|
+
/**
|
|
2950
|
+
* Generates candidate waypoint sets that connect `a` to `b` while respecting the entry/exit axes.
|
|
2951
|
+
* Strategies are ordered from "most natural" to "more elaborate detour".
|
|
2952
|
+
*/
|
|
2953
|
+
function generateRouteStrategies(a, aAxis, b, bAxis, obstacles) {
|
|
2954
|
+
const strategies = [];
|
|
2955
|
+
const midX = (a.x + b.x) / 2;
|
|
2956
|
+
const midY = (a.y + b.y) / 2;
|
|
2957
|
+
const obstacleXCuts = [];
|
|
2958
|
+
const obstacleYCuts = [];
|
|
2959
|
+
for (const o of obstacles) {
|
|
2960
|
+
obstacleXCuts.push(o.x - 6);
|
|
2961
|
+
obstacleXCuts.push(o.x + o.width + 6);
|
|
2962
|
+
obstacleYCuts.push(o.y - 6);
|
|
2963
|
+
obstacleYCuts.push(o.y + o.height + 6);
|
|
2964
|
+
}
|
|
2965
|
+
obstacleXCuts.sort((x, y) => Math.abs(x - midX) - Math.abs(y - midX));
|
|
2966
|
+
obstacleYCuts.sort((x, y) => Math.abs(x - midY) - Math.abs(y - midY));
|
|
2967
|
+
if (aAxis === 'h' && bAxis === 'h') {
|
|
2968
|
+
strategies.push([
|
|
2969
|
+
{ x: midX, y: a.y },
|
|
2970
|
+
{ x: midX, y: b.y },
|
|
2971
|
+
]);
|
|
2972
|
+
for (const mx of obstacleXCuts) {
|
|
2973
|
+
strategies.push([
|
|
2974
|
+
{ x: mx, y: a.y },
|
|
2975
|
+
{ x: mx, y: b.y },
|
|
2976
|
+
]);
|
|
2977
|
+
}
|
|
2978
|
+
for (const my of obstacleYCuts) {
|
|
2979
|
+
strategies.push([
|
|
2980
|
+
{ x: a.x, y: my },
|
|
2981
|
+
{ x: b.x, y: my },
|
|
2982
|
+
]);
|
|
2983
|
+
}
|
|
2984
|
+
}
|
|
2985
|
+
else if (aAxis === 'v' && bAxis === 'v') {
|
|
2986
|
+
strategies.push([
|
|
2987
|
+
{ x: a.x, y: midY },
|
|
2988
|
+
{ x: b.x, y: midY },
|
|
2989
|
+
]);
|
|
2990
|
+
for (const my of obstacleYCuts) {
|
|
2991
|
+
strategies.push([
|
|
2992
|
+
{ x: a.x, y: my },
|
|
2993
|
+
{ x: b.x, y: my },
|
|
2994
|
+
]);
|
|
2995
|
+
}
|
|
2996
|
+
for (const mx of obstacleXCuts) {
|
|
2997
|
+
strategies.push([
|
|
2998
|
+
{ x: mx, y: a.y },
|
|
2999
|
+
{ x: mx, y: b.y },
|
|
3000
|
+
]);
|
|
3001
|
+
}
|
|
3002
|
+
}
|
|
3003
|
+
else if (aAxis === 'h' && bAxis === 'v') {
|
|
3004
|
+
strategies.push([{ x: b.x, y: a.y }]);
|
|
3005
|
+
for (const my of obstacleYCuts) {
|
|
3006
|
+
strategies.push([
|
|
3007
|
+
{ x: a.x, y: my },
|
|
3008
|
+
{ x: b.x, y: my },
|
|
3009
|
+
]);
|
|
3010
|
+
}
|
|
3011
|
+
}
|
|
3012
|
+
else {
|
|
3013
|
+
strategies.push([{ x: a.x, y: b.y }]);
|
|
3014
|
+
for (const mx of obstacleXCuts) {
|
|
3015
|
+
strategies.push([
|
|
3016
|
+
{ x: mx, y: a.y },
|
|
3017
|
+
{ x: mx, y: b.y },
|
|
3018
|
+
]);
|
|
3019
|
+
}
|
|
3020
|
+
}
|
|
3021
|
+
return strategies;
|
|
3022
|
+
}
|
|
3023
|
+
/**
|
|
3024
|
+
* Picks the first strategy whose segments do not cross any obstacle.
|
|
3025
|
+
* Falls back to the most natural strategy when every candidate is blocked
|
|
3026
|
+
* (best-effort: keeps the connector readable even in tight layouts).
|
|
3027
|
+
*/
|
|
3028
|
+
function pickCleanStrategy(a, b, strategies, obstacles) {
|
|
3029
|
+
for (const wp of strategies) {
|
|
3030
|
+
const full = [a, ...wp, b];
|
|
3031
|
+
let clean = true;
|
|
3032
|
+
for (let i = 0; i < full.length - 1; i++) {
|
|
3033
|
+
if (segmentCrossesObstacles(full[i], full[i + 1], obstacles)) {
|
|
3034
|
+
clean = false;
|
|
3035
|
+
break;
|
|
3036
|
+
}
|
|
3037
|
+
}
|
|
3038
|
+
if (clean)
|
|
3039
|
+
return wp;
|
|
3040
|
+
}
|
|
3041
|
+
return strategies[0] ?? [];
|
|
3042
|
+
}
|
|
3043
|
+
/**
|
|
3044
|
+
* Routes an obstacle-aware orthogonal path between two free-space points.
|
|
3045
|
+
* The returned array includes the start (`from`), stub-outs from each port,
|
|
3046
|
+
* intermediate waypoints, and the end (`to`).
|
|
3047
|
+
*
|
|
3048
|
+
* Obstacles whose inflated rect already contains a stub point are dropped so
|
|
3049
|
+
* that source/target nodes can still emit/receive their connector without the
|
|
3050
|
+
* router fighting itself — but the middle of the path still routes around them.
|
|
3051
|
+
*/
|
|
3052
|
+
function routeOrthogonalConnector(from, sourceSide, to, targetSide, obstacles = [], options = {}) {
|
|
3053
|
+
const stub = Math.max(0, options.stub ?? 18);
|
|
3054
|
+
const padding = Math.max(0, options.obstaclePadding ?? 10);
|
|
3055
|
+
const aStub = stepOutward(from, sourceSide, stub);
|
|
3056
|
+
const bStub = stepOutward(to, targetSide, stub);
|
|
3057
|
+
const inflated = obstacles.map((o) => inflateRect(o, padding));
|
|
3058
|
+
const safe = inflated.filter((o) => !pointInsideRect(aStub, o) && !pointInsideRect(bStub, o));
|
|
3059
|
+
const strategies = generateRouteStrategies(aStub, sideAxis(sourceSide), bStub, sideAxis(targetSide), safe);
|
|
3060
|
+
const middle = pickCleanStrategy(aStub, bStub, strategies, safe);
|
|
3061
|
+
return simplifyWaypoints([from, aStub, ...middle, bStub, to]);
|
|
3062
|
+
}
|
|
3063
|
+
/** Renders waypoints as straight `L` segments. */
|
|
3064
|
+
function waypointsToOrthogonalPath(points) {
|
|
3065
|
+
if (points.length < 2)
|
|
3066
|
+
return '';
|
|
3067
|
+
const cmds = [`M ${points[0].x} ${points[0].y}`];
|
|
3068
|
+
for (let i = 1; i < points.length; i++) {
|
|
3069
|
+
cmds.push(`L ${points[i].x} ${points[i].y}`);
|
|
3070
|
+
}
|
|
3071
|
+
return cmds.join(' ');
|
|
3072
|
+
}
|
|
3073
|
+
/** Renders waypoints with rounded corners (quadratic curves) between segments. */
|
|
3074
|
+
function waypointsToRoundedPath(points, radius) {
|
|
3075
|
+
if (points.length < 2)
|
|
3076
|
+
return '';
|
|
3077
|
+
if (points.length === 2)
|
|
3078
|
+
return `M ${points[0].x} ${points[0].y} L ${points[1].x} ${points[1].y}`;
|
|
3079
|
+
const cmds = [`M ${points[0].x} ${points[0].y}`];
|
|
3080
|
+
for (let i = 1; i < points.length - 1; i++) {
|
|
3081
|
+
const prev = points[i - 1];
|
|
3082
|
+
const curr = points[i];
|
|
3083
|
+
const next = points[i + 1];
|
|
3084
|
+
const inDx = Math.sign(curr.x - prev.x);
|
|
3085
|
+
const inDy = Math.sign(curr.y - prev.y);
|
|
3086
|
+
const outDx = Math.sign(next.x - curr.x);
|
|
3087
|
+
const outDy = Math.sign(next.y - curr.y);
|
|
3088
|
+
const segIn = Math.hypot(curr.x - prev.x, curr.y - prev.y);
|
|
3089
|
+
const segOut = Math.hypot(next.x - curr.x, next.y - curr.y);
|
|
3090
|
+
const r = Math.max(0, Math.min(radius, segIn * 0.5, segOut * 0.5));
|
|
3091
|
+
const cornerStart = { x: curr.x - inDx * r, y: curr.y - inDy * r };
|
|
3092
|
+
const cornerEnd = { x: curr.x + outDx * r, y: curr.y + outDy * r };
|
|
3093
|
+
cmds.push(`L ${cornerStart.x} ${cornerStart.y}`);
|
|
3094
|
+
cmds.push(`Q ${curr.x} ${curr.y} ${cornerEnd.x} ${cornerEnd.y}`);
|
|
3095
|
+
}
|
|
3096
|
+
const last = points[points.length - 1];
|
|
3097
|
+
cmds.push(`L ${last.x} ${last.y}`);
|
|
3098
|
+
return cmds.join(' ');
|
|
3099
|
+
}
|
|
3100
|
+
/**
|
|
3101
|
+
* Builds the SVG `d` string for a connector. When `options.obstacles` is supplied,
|
|
3102
|
+
* orthogonal/rounded routes detour around the obstacle rectangles instead of
|
|
3103
|
+
* passing through them. The `straight` style always draws a direct line.
|
|
3104
|
+
*/
|
|
3105
|
+
function connectorPath(from, to, style, sourceSide, options = {}) {
|
|
3106
|
+
if (style === 'straight') {
|
|
3107
|
+
return `M ${from.x} ${from.y} L ${to.x} ${to.y}`;
|
|
3108
|
+
}
|
|
3109
|
+
const targetSide = options.targetSide ?? oppositeSide(sourceSide);
|
|
3110
|
+
const waypoints = routeOrthogonalConnector(from, sourceSide, to, targetSide, options.obstacles ?? [], {
|
|
3111
|
+
stub: options.stub,
|
|
3112
|
+
obstaclePadding: options.obstaclePadding,
|
|
3113
|
+
});
|
|
3114
|
+
if (style === 'orthogonal') {
|
|
3115
|
+
return waypointsToOrthogonalPath(waypoints);
|
|
3116
|
+
}
|
|
3117
|
+
return waypointsToRoundedPath(waypoints, options.cornerRadius ?? 14);
|
|
3118
|
+
}
|
|
3119
|
+
//#endregion
|
|
3120
|
+
//#region ---- Model helpers ----
|
|
3121
|
+
function findNode(model, nodeId) {
|
|
3122
|
+
return model.nodes.find((n) => n.id === nodeId);
|
|
3123
|
+
}
|
|
3124
|
+
function findPort(node, portId) {
|
|
3125
|
+
const pi = node.inboundPorts.find((p) => p.id === portId);
|
|
3126
|
+
if (pi)
|
|
3127
|
+
return { port: pi, inbound: true };
|
|
3128
|
+
const po = node.outboundPorts.find((p) => p.id === portId);
|
|
3129
|
+
if (po)
|
|
3130
|
+
return { port: po, inbound: false };
|
|
3131
|
+
return undefined;
|
|
3132
|
+
}
|
|
3133
|
+
function findConnector(model, id) {
|
|
3134
|
+
return model.connectors.find((c) => c.id === id);
|
|
3135
|
+
}
|
|
3136
|
+
/**
|
|
3137
|
+
* Built-in rules: valid ports, not disabled, optional single inbound attachment, no duplicate edge.
|
|
3138
|
+
*/
|
|
3139
|
+
function defaultCanConnectRequest(request) {
|
|
3140
|
+
const { model, sourceNodeId, sourcePortId, targetNodeId, targetPortId } = request;
|
|
3141
|
+
const sn = findNode(model, sourceNodeId);
|
|
3142
|
+
const tn = findNode(model, targetNodeId);
|
|
3143
|
+
if (!sn || !tn)
|
|
3144
|
+
return false;
|
|
3145
|
+
const spInfo = findPort(sn, sourcePortId);
|
|
3146
|
+
const tpInfo = findPort(tn, targetPortId);
|
|
3147
|
+
if (!spInfo || !tpInfo)
|
|
3148
|
+
return false;
|
|
3149
|
+
const sp = spInfo.port;
|
|
3150
|
+
const tp = tpInfo.port;
|
|
3151
|
+
if (sp.kind !== 'outbound' || tp.kind !== 'inbound')
|
|
3152
|
+
return false;
|
|
3153
|
+
if (sp.disabled || tp.disabled)
|
|
3154
|
+
return false;
|
|
3155
|
+
const duplicate = model.connectors.some((c) => c.sourceNodeId === sourceNodeId &&
|
|
3156
|
+
c.sourcePortId === sourcePortId &&
|
|
3157
|
+
c.targetNodeId === targetNodeId &&
|
|
3158
|
+
c.targetPortId === targetPortId);
|
|
3159
|
+
if (duplicate)
|
|
3160
|
+
return false;
|
|
3161
|
+
if (!tp.allowMultiple) {
|
|
3162
|
+
const inboundTaken = model.connectors.some((c) => c.targetNodeId === targetNodeId && c.targetPortId === targetPortId);
|
|
3163
|
+
if (inboundTaken)
|
|
3164
|
+
return false;
|
|
3165
|
+
}
|
|
3166
|
+
return true;
|
|
3167
|
+
}
|
|
3168
|
+
//#endregion
|
|
3169
|
+
//#region ---- Auto layout ----
|
|
3170
|
+
const DEFAULT_ARRANGE_MARGIN = 80;
|
|
3171
|
+
// Defaults leave room for inbound/outbound port labels that sit ~18px outside the
|
|
3172
|
+
// port anchor and may extend further as text. Larger gaps keep adjacent nodes from
|
|
3173
|
+
// having their port labels collide with each other or with routed connectors.
|
|
3174
|
+
const DEFAULT_ARRANGE_PRIMARY_GAP = 160;
|
|
3175
|
+
const DEFAULT_ARRANGE_CROSS_GAP = 110;
|
|
3176
|
+
const DEFAULT_ARRANGE_COMPONENT_GAP = 220;
|
|
3177
|
+
/**
|
|
3178
|
+
* Auto-arranges workflow nodes in a layered layout (left→right by default).
|
|
3179
|
+
* Honors `direction`, `alignment`, `origin`, `nodeIds`, and crossing-reduction options.
|
|
3180
|
+
*/
|
|
3181
|
+
function arrangeWorkflowDiagram(model, options = {}) {
|
|
3182
|
+
if (model.nodes.length === 0) {
|
|
3183
|
+
return model;
|
|
3184
|
+
}
|
|
3185
|
+
const margin = options.margin ?? DEFAULT_ARRANGE_MARGIN;
|
|
3186
|
+
const runtime = {
|
|
3187
|
+
direction: options.direction ?? 'horizontal',
|
|
3188
|
+
alignment: options.alignment ?? 'center',
|
|
3189
|
+
primaryGap: options.horizontalGap ?? DEFAULT_ARRANGE_PRIMARY_GAP,
|
|
3190
|
+
crossGap: options.verticalGap ?? DEFAULT_ARRANGE_CROSS_GAP,
|
|
3191
|
+
componentGap: Math.max(options.verticalGap ?? DEFAULT_ARRANGE_CROSS_GAP, DEFAULT_ARRANGE_COMPONENT_GAP),
|
|
3192
|
+
margin,
|
|
3193
|
+
grid: options.gridSize ?? 0,
|
|
3194
|
+
origin: options.origin ?? { x: margin, y: margin },
|
|
3195
|
+
reduceCrossings: options.reduceCrossings ?? true,
|
|
3196
|
+
groupDisconnected: options.groupDisconnected ?? true,
|
|
3197
|
+
};
|
|
3198
|
+
const subsetIds = options.nodeIds && options.nodeIds.length > 0
|
|
3199
|
+
? new Set(options.nodeIds.filter((id) => model.nodes.some((n) => n.id === id)))
|
|
3200
|
+
: null;
|
|
3201
|
+
const targetNodes = subsetIds ? model.nodes.filter((n) => subsetIds.has(n.id)) : model.nodes;
|
|
3202
|
+
if (targetNodes.length === 0) {
|
|
3203
|
+
return model;
|
|
3204
|
+
}
|
|
3205
|
+
const targetIds = new Set(targetNodes.map((n) => n.id));
|
|
3206
|
+
const relevantConnectors = model.connectors.filter((c) => targetIds.has(c.sourceNodeId) && targetIds.has(c.targetNodeId));
|
|
3207
|
+
const components = splitComponents(targetNodes, relevantConnectors, runtime, options.startNodeId);
|
|
3208
|
+
const positionById = new Map();
|
|
3209
|
+
let crossCursor = runtime.direction === 'horizontal' ? runtime.origin.y : runtime.origin.x;
|
|
3210
|
+
for (const component of components) {
|
|
3211
|
+
layoutComponent(component, positionById, runtime, crossCursor);
|
|
3212
|
+
crossCursor += component.crossSize + runtime.componentGap;
|
|
3213
|
+
}
|
|
3214
|
+
return {
|
|
3215
|
+
...model,
|
|
3216
|
+
nodes: model.nodes.map((n) => {
|
|
3217
|
+
const next = positionById.get(n.id);
|
|
3218
|
+
return next ? { ...n, position: { ...next } } : n;
|
|
3219
|
+
}),
|
|
3220
|
+
};
|
|
3221
|
+
}
|
|
3222
|
+
function splitComponents(nodes, connectors, runtime, startNodeId) {
|
|
3223
|
+
const undirected = new Map();
|
|
3224
|
+
const outgoing = new Map();
|
|
3225
|
+
const incomingCount = new Map();
|
|
3226
|
+
for (const n of nodes) {
|
|
3227
|
+
undirected.set(n.id, new Set());
|
|
3228
|
+
outgoing.set(n.id, new Set());
|
|
3229
|
+
incomingCount.set(n.id, 0);
|
|
3230
|
+
}
|
|
3231
|
+
for (const c of connectors) {
|
|
3232
|
+
if (!undirected.has(c.sourceNodeId) || !undirected.has(c.targetNodeId))
|
|
3233
|
+
continue;
|
|
3234
|
+
undirected.get(c.sourceNodeId).add(c.targetNodeId);
|
|
3235
|
+
undirected.get(c.targetNodeId).add(c.sourceNodeId);
|
|
3236
|
+
outgoing.get(c.sourceNodeId).add(c.targetNodeId);
|
|
3237
|
+
incomingCount.set(c.targetNodeId, (incomingCount.get(c.targetNodeId) ?? 0) + 1);
|
|
3238
|
+
}
|
|
3239
|
+
const sortedNodes = [...nodes].sort(compareNodesForLayout);
|
|
3240
|
+
if (startNodeId) {
|
|
3241
|
+
sortedNodes.sort((a, b) => {
|
|
3242
|
+
if (a.id === startNodeId)
|
|
3243
|
+
return -1;
|
|
3244
|
+
if (b.id === startNodeId)
|
|
3245
|
+
return 1;
|
|
3246
|
+
return 0;
|
|
3247
|
+
});
|
|
3248
|
+
}
|
|
3249
|
+
const visited = new Set();
|
|
3250
|
+
const componentBuckets = [];
|
|
3251
|
+
if (runtime.groupDisconnected) {
|
|
3252
|
+
for (const seed of sortedNodes) {
|
|
3253
|
+
if (visited.has(seed.id))
|
|
3254
|
+
continue;
|
|
3255
|
+
const bucket = [];
|
|
3256
|
+
const queue = [seed.id];
|
|
3257
|
+
visited.add(seed.id);
|
|
3258
|
+
while (queue.length > 0) {
|
|
3259
|
+
const id = queue.shift();
|
|
3260
|
+
const node = nodes.find((n) => n.id === id);
|
|
3261
|
+
if (node)
|
|
3262
|
+
bucket.push(node);
|
|
3263
|
+
for (const next of undirected.get(id) ?? []) {
|
|
3264
|
+
if (!visited.has(next)) {
|
|
3265
|
+
visited.add(next);
|
|
3266
|
+
queue.push(next);
|
|
3267
|
+
}
|
|
3268
|
+
}
|
|
3269
|
+
}
|
|
3270
|
+
componentBuckets.push(bucket);
|
|
3271
|
+
}
|
|
3272
|
+
}
|
|
3273
|
+
else {
|
|
3274
|
+
componentBuckets.push([...nodes]);
|
|
3275
|
+
}
|
|
3276
|
+
const components = [];
|
|
3277
|
+
for (const bucket of componentBuckets) {
|
|
3278
|
+
const bucketIds = new Set(bucket.map((n) => n.id));
|
|
3279
|
+
const bucketConnectors = connectors.filter((c) => bucketIds.has(c.sourceNodeId) && bucketIds.has(c.targetNodeId));
|
|
3280
|
+
const ranks = computeRanks(bucket, outgoing, incomingCount, startNodeId);
|
|
3281
|
+
if (runtime.reduceCrossings) {
|
|
3282
|
+
reduceCrossingsInPlace(ranks, bucketConnectors, runtime.direction);
|
|
3283
|
+
}
|
|
3284
|
+
else {
|
|
3285
|
+
for (const rank of ranks)
|
|
3286
|
+
rank.sort(compareNodesForLayout);
|
|
3287
|
+
}
|
|
3288
|
+
const crossSize = estimateCrossSize(ranks, runtime);
|
|
3289
|
+
components.push({ nodes: bucket, ranks, crossSize });
|
|
3290
|
+
}
|
|
3291
|
+
return components;
|
|
3292
|
+
}
|
|
3293
|
+
function computeRanks(nodes, outgoing, incomingCount, startNodeId) {
|
|
3294
|
+
const rank = new Map();
|
|
3295
|
+
const queue = [];
|
|
3296
|
+
const enqueueRoots = (ids) => {
|
|
3297
|
+
for (const id of ids) {
|
|
3298
|
+
if (!rank.has(id)) {
|
|
3299
|
+
rank.set(id, 0);
|
|
3300
|
+
queue.push(id);
|
|
3301
|
+
}
|
|
3302
|
+
}
|
|
3303
|
+
};
|
|
3304
|
+
const localIds = new Set(nodes.map((n) => n.id));
|
|
3305
|
+
if (startNodeId && localIds.has(startNodeId)) {
|
|
3306
|
+
enqueueRoots([startNodeId]);
|
|
3307
|
+
}
|
|
3308
|
+
else {
|
|
3309
|
+
enqueueRoots(nodes.filter((n) => (incomingCount.get(n.id) ?? 0) === 0).map((n) => n.id));
|
|
3310
|
+
}
|
|
3311
|
+
// First-visit BFS handles cycles safely (shortest-distance ranking).
|
|
3312
|
+
const maxSteps = nodes.length * (nodes.length + 1);
|
|
3313
|
+
let steps = 0;
|
|
3314
|
+
while (queue.length > 0 && steps < maxSteps) {
|
|
3315
|
+
steps += 1;
|
|
3316
|
+
const id = queue.shift();
|
|
3317
|
+
const base = rank.get(id) ?? 0;
|
|
3318
|
+
for (const targetId of outgoing.get(id) ?? []) {
|
|
3319
|
+
if (!localIds.has(targetId) || rank.has(targetId))
|
|
3320
|
+
continue;
|
|
3321
|
+
rank.set(targetId, base + 1);
|
|
3322
|
+
queue.push(targetId);
|
|
3323
|
+
}
|
|
3324
|
+
}
|
|
3325
|
+
let orphanRank = nodes.reduce((max, n) => Math.max(max, rank.get(n.id) ?? 0), 0) + 1;
|
|
3326
|
+
const unranked = nodes.filter((n) => !rank.has(n.id)).sort(compareNodesForLayout);
|
|
3327
|
+
for (const n of unranked) {
|
|
3328
|
+
rank.set(n.id, orphanRank);
|
|
3329
|
+
orphanRank += 1;
|
|
3330
|
+
}
|
|
3331
|
+
const byRank = new Map();
|
|
3332
|
+
for (const n of nodes) {
|
|
3333
|
+
const r = rank.get(n.id) ?? 0;
|
|
3334
|
+
const bucket = byRank.get(r) ?? [];
|
|
3335
|
+
bucket.push(n);
|
|
3336
|
+
byRank.set(r, bucket);
|
|
3337
|
+
}
|
|
3338
|
+
return [...byRank.keys()].sort((a, b) => a - b).map((r) => byRank.get(r) ?? []);
|
|
3339
|
+
}
|
|
3340
|
+
/**
|
|
3341
|
+
* Returns a 0–1 fraction along the cross axis (vertical for horizontal flow) where the port sits on the node.
|
|
3342
|
+
*/
|
|
3343
|
+
function portCrossAxisFraction(node, portId, direction) {
|
|
3344
|
+
const info = findPort(node, portId);
|
|
3345
|
+
if (!info) {
|
|
3346
|
+
return 0.5;
|
|
3347
|
+
}
|
|
3348
|
+
const port = info.port;
|
|
3349
|
+
const r = Math.min(0.999, Math.max(0.001, port.ratio));
|
|
3350
|
+
if (direction === 'horizontal') {
|
|
3351
|
+
switch (port.side) {
|
|
3352
|
+
case 'left':
|
|
3353
|
+
case 'right':
|
|
3354
|
+
return r;
|
|
3355
|
+
case 'top':
|
|
3356
|
+
return 0;
|
|
3357
|
+
case 'bottom':
|
|
3358
|
+
return 1;
|
|
3359
|
+
}
|
|
3360
|
+
}
|
|
3361
|
+
switch (port.side) {
|
|
3362
|
+
case 'top':
|
|
3363
|
+
case 'bottom':
|
|
3364
|
+
return r;
|
|
3365
|
+
case 'left':
|
|
3366
|
+
return 0;
|
|
3367
|
+
case 'right':
|
|
3368
|
+
return 1;
|
|
3369
|
+
}
|
|
3370
|
+
return r;
|
|
3371
|
+
}
|
|
3372
|
+
/**
|
|
3373
|
+
* Barycenter heuristic with port-aware weights: targets inherit both their predecessor's rank index
|
|
3374
|
+
* and the source port's cross-axis position so top/bottom (or left/right) ports map to matching targets.
|
|
3375
|
+
*/
|
|
3376
|
+
function reduceCrossingsInPlace(ranks, connectors, direction) {
|
|
3377
|
+
if (ranks.length === 0)
|
|
3378
|
+
return;
|
|
3379
|
+
ranks[0] = [...ranks[0]].sort(compareNodesForLayout);
|
|
3380
|
+
const nodeById = new Map();
|
|
3381
|
+
for (const rank of ranks) {
|
|
3382
|
+
for (const node of rank) {
|
|
3383
|
+
nodeById.set(node.id, node);
|
|
3384
|
+
}
|
|
3385
|
+
}
|
|
3386
|
+
const indexInRank = ranks.map((rank) => {
|
|
3387
|
+
const map = new Map();
|
|
3388
|
+
for (let i = 0; i < rank.length; i++)
|
|
3389
|
+
map.set(rank[i].id, i);
|
|
3390
|
+
return map;
|
|
3391
|
+
});
|
|
3392
|
+
for (let r = 1; r < ranks.length; r++) {
|
|
3393
|
+
const predRank = ranks[r - 1];
|
|
3394
|
+
const predIndex = indexInRank[r - 1];
|
|
3395
|
+
const incomingFromPred = new Map();
|
|
3396
|
+
const predIds = new Set(predRank.map((n) => n.id));
|
|
3397
|
+
for (const c of connectors) {
|
|
3398
|
+
if (!predIds.has(c.sourceNodeId)) {
|
|
3399
|
+
continue;
|
|
3400
|
+
}
|
|
3401
|
+
const idx = predIndex.get(c.sourceNodeId);
|
|
3402
|
+
if (idx === undefined) {
|
|
3403
|
+
continue;
|
|
3404
|
+
}
|
|
3405
|
+
const sourceNode = nodeById.get(c.sourceNodeId);
|
|
3406
|
+
const portFraction = sourceNode ? portCrossAxisFraction(sourceNode, c.sourcePortId, direction) : 0.5;
|
|
3407
|
+
// Keep port ordering within the same predecessor index (fraction ∈ [0, 1)).
|
|
3408
|
+
const weight = idx + portFraction;
|
|
3409
|
+
const arr = incomingFromPred.get(c.targetNodeId) ?? [];
|
|
3410
|
+
arr.push(weight);
|
|
3411
|
+
incomingFromPred.set(c.targetNodeId, arr);
|
|
3412
|
+
}
|
|
3413
|
+
ranks[r] = [...ranks[r]].sort((a, b) => {
|
|
3414
|
+
const ia = incomingFromPred.get(a.id) ?? [];
|
|
3415
|
+
const ib = incomingFromPred.get(b.id) ?? [];
|
|
3416
|
+
const ba = ia.length > 0 ? ia.reduce((s, v) => s + v, 0) / ia.length : Number.POSITIVE_INFINITY;
|
|
3417
|
+
const bb = ib.length > 0 ? ib.reduce((s, v) => s + v, 0) / ib.length : Number.POSITIVE_INFINITY;
|
|
3418
|
+
if (ba !== bb)
|
|
3419
|
+
return ba - bb;
|
|
3420
|
+
return compareNodesForLayout(a, b);
|
|
3421
|
+
});
|
|
3422
|
+
const refreshed = new Map();
|
|
3423
|
+
for (let i = 0; i < ranks[r].length; i++)
|
|
3424
|
+
refreshed.set(ranks[r][i].id, i);
|
|
3425
|
+
indexInRank[r] = refreshed;
|
|
3426
|
+
}
|
|
3427
|
+
}
|
|
3428
|
+
function estimateCrossSize(ranks, runtime) {
|
|
3429
|
+
let max = 0;
|
|
3430
|
+
for (const rank of ranks) {
|
|
3431
|
+
const total = rank.reduce((sum, n) => sum + (runtime.direction === 'horizontal' ? n.size.height : n.size.width), 0);
|
|
3432
|
+
const gaps = Math.max(0, rank.length - 1) * runtime.crossGap;
|
|
3433
|
+
max = Math.max(max, total + gaps);
|
|
3434
|
+
}
|
|
3435
|
+
return max;
|
|
3436
|
+
}
|
|
3437
|
+
function layoutComponent(component, positionById, runtime, crossStart) {
|
|
3438
|
+
const isHorizontal = runtime.direction === 'horizontal';
|
|
3439
|
+
let primaryCursor = isHorizontal ? runtime.origin.x : runtime.origin.y;
|
|
3440
|
+
for (const rank of component.ranks) {
|
|
3441
|
+
if (rank.length === 0)
|
|
3442
|
+
continue;
|
|
3443
|
+
const crossTotal = rank.reduce((sum, n) => sum + (isHorizontal ? n.size.height : n.size.width), 0);
|
|
3444
|
+
const crossGaps = (rank.length - 1) * runtime.crossGap;
|
|
3445
|
+
const crossLen = crossTotal + crossGaps;
|
|
3446
|
+
let cursor = crossStart;
|
|
3447
|
+
if (runtime.alignment === 'center') {
|
|
3448
|
+
cursor = crossStart + (component.crossSize - crossLen) * 0.5;
|
|
3449
|
+
}
|
|
3450
|
+
else if (runtime.alignment === 'end') {
|
|
3451
|
+
cursor = crossStart + (component.crossSize - crossLen);
|
|
3452
|
+
}
|
|
3453
|
+
let primaryMax = 0;
|
|
3454
|
+
for (const node of rank) {
|
|
3455
|
+
const primary = isHorizontal ? primaryCursor : cursor;
|
|
3456
|
+
const cross = isHorizontal ? cursor : primaryCursor;
|
|
3457
|
+
const raw = isHorizontal ? { x: primary, y: cross } : { x: cross, y: primary };
|
|
3458
|
+
const snapped = runtime.grid > 0 ? snapPoint(raw, runtime.grid) : raw;
|
|
3459
|
+
positionById.set(node.id, snapped);
|
|
3460
|
+
const advance = (isHorizontal ? node.size.height : node.size.width) + runtime.crossGap;
|
|
3461
|
+
cursor += advance;
|
|
3462
|
+
primaryMax = Math.max(primaryMax, isHorizontal ? node.size.width : node.size.height);
|
|
3463
|
+
}
|
|
3464
|
+
primaryCursor += primaryMax + runtime.primaryGap;
|
|
3465
|
+
}
|
|
3466
|
+
}
|
|
3467
|
+
function compareNodesForLayout(a, b) {
|
|
3468
|
+
const titleA = a.title?.trim() ?? '';
|
|
3469
|
+
const titleB = b.title?.trim() ?? '';
|
|
3470
|
+
if (titleA !== titleB) {
|
|
3471
|
+
return titleA.localeCompare(titleB);
|
|
3472
|
+
}
|
|
3473
|
+
return a.id.localeCompare(b.id);
|
|
3474
|
+
}
|
|
3475
|
+
//#endregion
|
|
3476
|
+
|
|
3477
|
+
//#endregion
|
|
3478
|
+
class AXPWorkflowDesignerComponent {
|
|
3479
|
+
//#endregion
|
|
3480
|
+
constructor() {
|
|
3481
|
+
/** Unique SVG fragment id so multiple designers on one page do not clash. */
|
|
3482
|
+
this.svgMarkerId = `axp-wf-arrow-${Math.random().toString(36).slice(2, 10)}`;
|
|
3483
|
+
//#region ---- Inputs / outputs ----
|
|
3484
|
+
this.model = input.required(/* @ts-ignore */
|
|
3485
|
+
...(ngDevMode ? [{ debugName: "model" }] : /* istanbul ignore next */ []));
|
|
3486
|
+
this.modelChange = output();
|
|
3487
|
+
/** When true, the whole designer is non-interactive unless a nested entity sets its own readonly flag. */
|
|
3488
|
+
this.readonly = input(false, /* @ts-ignore */
|
|
3489
|
+
...(ngDevMode ? [{ debugName: "readonly" }] : /* istanbul ignore next */ []));
|
|
3490
|
+
/** Read-only run/history views: emphasize nodes (e.g. current workflow step) without selection chrome. */
|
|
3491
|
+
this.highlightedNodeIds = input([], /* @ts-ignore */
|
|
3492
|
+
...(ngDevMode ? [{ debugName: "highlightedNodeIds" }] : /* istanbul ignore next */ []));
|
|
3493
|
+
this.gridSize = input(20, /* @ts-ignore */
|
|
3494
|
+
...(ngDevMode ? [{ debugName: "gridSize" }] : /* istanbul ignore next */ []));
|
|
3495
|
+
/** Default snap when node does not override `snapToGrid`. */
|
|
3496
|
+
this.snapToGrid = input(true, /* @ts-ignore */
|
|
3497
|
+
...(ngDevMode ? [{ debugName: "snapToGrid" }] : /* istanbul ignore next */ []));
|
|
3498
|
+
this.connectionPreviewStyle = input('orthogonal', /* @ts-ignore */
|
|
3499
|
+
...(ngDevMode ? [{ debugName: "connectionPreviewStyle" }] : /* istanbul ignore next */ []));
|
|
3500
|
+
this.backgroundPattern = input('dots', /* @ts-ignore */
|
|
3501
|
+
...(ngDevMode ? [{ debugName: "backgroundPattern" }] : /* istanbul ignore next */ []));
|
|
3502
|
+
this.zoomChange = output();
|
|
3503
|
+
/** When set, replaces built-in `defaultCanConnectRequest` entirely. */
|
|
3504
|
+
this.canConnect = input(null, /* @ts-ignore */
|
|
3505
|
+
...(ngDevMode ? [{ debugName: "canConnect" }] : /* istanbul ignore next */ []));
|
|
3506
|
+
/** Used to resolve native palette drag payloads (`type`) into full defaults. */
|
|
3507
|
+
this.paletteItems = input([], /* @ts-ignore */
|
|
3508
|
+
...(ngDevMode ? [{ debugName: "paletteItems" }] : /* istanbul ignore next */ []));
|
|
3509
|
+
/** When false, port labels are hidden (ports remain interactive). */
|
|
3510
|
+
this.showPortLabels = input(true, /* @ts-ignore */
|
|
3511
|
+
...(ngDevMode ? [{ debugName: "showPortLabels" }] : /* istanbul ignore next */ []));
|
|
3512
|
+
this.nodeBodyTemplate = input(null, /* @ts-ignore */
|
|
3513
|
+
...(ngDevMode ? [{ debugName: "nodeBodyTemplate" }] : /* istanbul ignore next */ []));
|
|
3514
|
+
this.designerPointerDown = output();
|
|
3515
|
+
this.designerClick = output();
|
|
3516
|
+
this.designerDoubleClick = output();
|
|
3517
|
+
this.nodePointerDown = output();
|
|
3518
|
+
this.nodeClick = output();
|
|
3519
|
+
this.nodeDoubleClick = output();
|
|
3520
|
+
this.nodeContextMenu = output();
|
|
3521
|
+
this.nodeDragStart = output();
|
|
3522
|
+
this.nodeDragEnd = output();
|
|
3523
|
+
this.portPointerDown = output();
|
|
3524
|
+
this.linkDragStart = output();
|
|
3525
|
+
this.linkDragMove = output();
|
|
3526
|
+
this.linkDragEnd = output();
|
|
3527
|
+
this.connectorClick = output();
|
|
3528
|
+
this.connectorDoubleClick = output();
|
|
3529
|
+
this.connectionCreated = output();
|
|
3530
|
+
this.connectionRejected = output();
|
|
3531
|
+
this.activationChange = output();
|
|
3532
|
+
//#endregion
|
|
3533
|
+
//#region ---- State ----
|
|
3534
|
+
this.surfaceRef = viewChild.required('surface', /* @ts-ignore */
|
|
3535
|
+
...(ngDevMode ? [{ debugName: "surfaceRef" }] : /* istanbul ignore next */ []));
|
|
3536
|
+
this.panViewRef = viewChild(AXPanViewDirective, /* @ts-ignore */
|
|
3537
|
+
...(ngDevMode ? [{ debugName: "panViewRef" }] : /* istanbul ignore next */ []));
|
|
3538
|
+
this.activation = signal(null, /* @ts-ignore */
|
|
3539
|
+
...(ngDevMode ? [{ debugName: "activation" }] : /* istanbul ignore next */ []));
|
|
3540
|
+
this.selectedNodeIds = signal(new Set(), /* @ts-ignore */
|
|
3541
|
+
...(ngDevMode ? [{ debugName: "selectedNodeIds" }] : /* istanbul ignore next */ []));
|
|
3542
|
+
this.selectedConnectorIds = signal(new Set(), /* @ts-ignore */
|
|
3543
|
+
...(ngDevMode ? [{ debugName: "selectedConnectorIds" }] : /* istanbul ignore next */ []));
|
|
3544
|
+
this.panX = signal(0, /* @ts-ignore */
|
|
3545
|
+
...(ngDevMode ? [{ debugName: "panX" }] : /* istanbul ignore next */ []));
|
|
3546
|
+
this.panY = signal(0, /* @ts-ignore */
|
|
3547
|
+
...(ngDevMode ? [{ debugName: "panY" }] : /* istanbul ignore next */ []));
|
|
3548
|
+
this.panZoomLevel = signal(100, /* @ts-ignore */
|
|
3549
|
+
...(ngDevMode ? [{ debugName: "panZoomLevel" }] : /* istanbul ignore next */ []));
|
|
3550
|
+
this.linkDrag = signal(null, /* @ts-ignore */
|
|
3551
|
+
...(ngDevMode ? [{ debugName: "linkDrag" }] : /* istanbul ignore next */ []));
|
|
3552
|
+
/** Inbound port under the pointer while dragging a new connector from an outbound port. */
|
|
3553
|
+
this.linkHoverTarget = signal(null, /* @ts-ignore */
|
|
3554
|
+
...(ngDevMode ? [{ debugName: "linkHoverTarget" }] : /* istanbul ignore next */ []));
|
|
3555
|
+
this.nodeMove = null;
|
|
3556
|
+
this.resize = null;
|
|
3557
|
+
this.suppressNextNodeClick = false;
|
|
3558
|
+
this.wrapperDragoverListener = null;
|
|
3559
|
+
this.wrapperDropListener = null;
|
|
3560
|
+
this.wrapperElement = signal(null, /* @ts-ignore */
|
|
3561
|
+
...(ngDevMode ? [{ debugName: "wrapperElement" }] : /* istanbul ignore next */ []));
|
|
3562
|
+
this.wrapperAttachRetryHandle = null;
|
|
3563
|
+
this.wrapperAttachAttempt = 0;
|
|
3564
|
+
this.destroyRef = inject(DestroyRef);
|
|
3565
|
+
//#endregion
|
|
3566
|
+
//#region ---- Computed ----
|
|
3567
|
+
this.gridStyle = computed(() => {
|
|
3568
|
+
const g = this.gridSize();
|
|
3569
|
+
if (this.backgroundPattern() === 'dots') {
|
|
3570
|
+
return {
|
|
3571
|
+
'background-size': `${g}px ${g}px`,
|
|
3572
|
+
'background-image': `radial-gradient(circle, rgba(148, 163, 184, 0.35) 1px, transparent 1.2px)`,
|
|
3573
|
+
};
|
|
3574
|
+
}
|
|
3575
|
+
return {
|
|
3576
|
+
'background-size': `${g}px ${g}px`,
|
|
3577
|
+
'background-image': `
|
|
3578
|
+
linear-gradient(to right, rgba(148, 163, 184, 0.2) 1px, transparent 1px),
|
|
3579
|
+
linear-gradient(to bottom, rgba(148, 163, 184, 0.2) 1px, transparent 1px)
|
|
3580
|
+
`,
|
|
3581
|
+
};
|
|
3582
|
+
}, /* @ts-ignore */
|
|
3583
|
+
...(ngDevMode ? [{ debugName: "gridStyle" }] : /* istanbul ignore next */ []));
|
|
3584
|
+
this.canvasViewport = computed(() => {
|
|
3585
|
+
const model = this.model();
|
|
3586
|
+
const contentPadding = 320;
|
|
3587
|
+
const minCanvasWidth = 1600;
|
|
3588
|
+
const minCanvasHeight = 1000;
|
|
3589
|
+
const nodeMinX = model.nodes.reduce((acc, node) => Math.min(acc, node.position.x), 0);
|
|
3590
|
+
const nodeMinY = model.nodes.reduce((acc, node) => Math.min(acc, node.position.y), 0);
|
|
3591
|
+
const nodeMaxRight = model.nodes.reduce((acc, node) => Math.max(acc, node.position.x + node.size.width), 0);
|
|
3592
|
+
const nodeMaxBottom = model.nodes.reduce((acc, node) => Math.max(acc, node.position.y + node.size.height), 0);
|
|
3593
|
+
const minX = Math.min(0, nodeMinX);
|
|
3594
|
+
const minY = Math.min(0, nodeMinY);
|
|
3595
|
+
const maxRight = Math.max(minCanvasWidth, nodeMaxRight);
|
|
3596
|
+
const maxBottom = Math.max(minCanvasHeight, nodeMaxBottom);
|
|
3597
|
+
const offsetX = -minX + contentPadding;
|
|
3598
|
+
const offsetY = -minY + contentPadding;
|
|
3599
|
+
return {
|
|
3600
|
+
minX,
|
|
3601
|
+
minY,
|
|
3602
|
+
offsetX,
|
|
3603
|
+
offsetY,
|
|
3604
|
+
width: Math.ceil(maxRight - minX + contentPadding * 2),
|
|
3605
|
+
height: Math.ceil(maxBottom - minY + contentPadding * 2),
|
|
3606
|
+
};
|
|
3607
|
+
}, /* @ts-ignore */
|
|
3608
|
+
...(ngDevMode ? [{ debugName: "canvasViewport" }] : /* istanbul ignore next */ []));
|
|
3609
|
+
this.surfaceStyle = computed(() => {
|
|
3610
|
+
const viewport = this.canvasViewport();
|
|
3611
|
+
return {
|
|
3612
|
+
// ...this.gridStyle(),
|
|
3613
|
+
width: `${viewport.width}px`,
|
|
3614
|
+
height: `${viewport.height}px`,
|
|
3615
|
+
};
|
|
3616
|
+
}, /* @ts-ignore */
|
|
3617
|
+
...(ngDevMode ? [{ debugName: "surfaceStyle" }] : /* istanbul ignore next */ []));
|
|
3618
|
+
this.onWindowPointerMoveForNode = (ev) => {
|
|
3619
|
+
const drag = this.nodeMove;
|
|
3620
|
+
if (!drag)
|
|
3621
|
+
return;
|
|
3622
|
+
const model = this.model();
|
|
3623
|
+
const p = this.canvasPointFromClient(ev.clientX, ev.clientY);
|
|
3624
|
+
const dx = p.x - drag.start.x;
|
|
3625
|
+
const dy = p.y - drag.start.y;
|
|
3626
|
+
if (Math.abs(dx) > 1 || Math.abs(dy) > 1) {
|
|
3627
|
+
drag.moved = true;
|
|
3628
|
+
}
|
|
3629
|
+
const nodes = model.nodes.map((n) => {
|
|
3630
|
+
if (!drag.nodeIds.includes(n.id))
|
|
3631
|
+
return n;
|
|
3632
|
+
const origin = drag.origins[n.id];
|
|
3633
|
+
if (!origin)
|
|
3634
|
+
return n;
|
|
3635
|
+
let nx = origin.x + dx;
|
|
3636
|
+
let ny = origin.y + dy;
|
|
3637
|
+
const snapped = this.applySnapForNode(n, { x: nx, y: ny });
|
|
3638
|
+
nx = snapped.x;
|
|
3639
|
+
ny = snapped.y;
|
|
3640
|
+
return { ...n, position: { x: nx, y: ny } };
|
|
3641
|
+
});
|
|
3642
|
+
this.commit({ ...model, nodes });
|
|
3643
|
+
};
|
|
3644
|
+
this.onWindowPointerUpForNode = (ev) => {
|
|
3645
|
+
const drag = this.nodeMove;
|
|
3646
|
+
this.nodeMove = null;
|
|
3647
|
+
window.removeEventListener('pointermove', this.onWindowPointerMoveForNode);
|
|
3648
|
+
window.removeEventListener('pointerup', this.onWindowPointerUpForNode);
|
|
3649
|
+
window.removeEventListener('pointercancel', this.onWindowPointerUpForNode);
|
|
3650
|
+
if (drag?.moved) {
|
|
3651
|
+
this.suppressNextNodeClick = true;
|
|
3652
|
+
}
|
|
3653
|
+
if (drag) {
|
|
3654
|
+
const node = findNode(this.model(), drag.nodeIds[0] ?? '');
|
|
3655
|
+
if (node)
|
|
3656
|
+
this.nodeDragEnd.emit({ node, event: ev });
|
|
3657
|
+
}
|
|
3658
|
+
};
|
|
3659
|
+
this.onWindowPointerMoveForResize = (ev) => {
|
|
3660
|
+
const st = this.resize;
|
|
3661
|
+
if (!st)
|
|
3662
|
+
return;
|
|
3663
|
+
const model = this.model();
|
|
3664
|
+
const node = findNode(model, st.nodeId);
|
|
3665
|
+
if (!node)
|
|
3666
|
+
return;
|
|
3667
|
+
const p = this.canvasPointFromClient(ev.clientX, ev.clientY);
|
|
3668
|
+
const dx = p.x - st.startPointer.x;
|
|
3669
|
+
const dy = p.y - st.startPointer.y;
|
|
3670
|
+
let { width, height } = st.startSize;
|
|
3671
|
+
let { x, y } = st.startPosition;
|
|
3672
|
+
const minW = 72;
|
|
3673
|
+
const minH = 40;
|
|
3674
|
+
const growEast = st.handle.includes('e');
|
|
3675
|
+
const growSouth = st.handle.includes('s');
|
|
3676
|
+
const growWest = st.handle.includes('w');
|
|
3677
|
+
const growNorth = st.handle.includes('n');
|
|
3678
|
+
if (growEast)
|
|
3679
|
+
width = Math.max(minW, st.startSize.width + dx);
|
|
3680
|
+
if (growSouth)
|
|
3681
|
+
height = Math.max(minH, st.startSize.height + dy);
|
|
3682
|
+
if (growWest) {
|
|
3683
|
+
const nw = Math.max(minW, st.startSize.width - dx);
|
|
3684
|
+
x = st.startPosition.x + st.startSize.width - nw;
|
|
3685
|
+
width = nw;
|
|
3686
|
+
}
|
|
3687
|
+
if (growNorth) {
|
|
3688
|
+
const nh = Math.max(minH, st.startSize.height - dy);
|
|
3689
|
+
y = st.startPosition.y + st.startSize.height - nh;
|
|
3690
|
+
height = nh;
|
|
3691
|
+
}
|
|
3692
|
+
if (node.shape === 'circle' || node.lockAspectRatio) {
|
|
3693
|
+
const a = st.aspect;
|
|
3694
|
+
if (growEast || growWest) {
|
|
3695
|
+
height = width / a;
|
|
3696
|
+
if (growNorth)
|
|
3697
|
+
y = st.startPosition.y + st.startSize.height - height;
|
|
3698
|
+
}
|
|
3699
|
+
else {
|
|
3700
|
+
width = height * a;
|
|
3701
|
+
if (growWest)
|
|
3702
|
+
x = st.startPosition.x + st.startSize.width - width;
|
|
3703
|
+
}
|
|
3704
|
+
}
|
|
3705
|
+
const g = this.gridSize();
|
|
3706
|
+
const useSnap = node.snapToGrid ?? this.snapToGrid();
|
|
3707
|
+
if (useSnap && g > 0) {
|
|
3708
|
+
width = Math.max(minW, snapScalar(width, g));
|
|
3709
|
+
height = Math.max(minH, snapScalar(height, g));
|
|
3710
|
+
x = snapScalar(x, g);
|
|
3711
|
+
y = snapScalar(y, g);
|
|
3712
|
+
}
|
|
3713
|
+
const nodes = model.nodes.map((n) => n.id === node.id ? { ...n, position: { x, y }, size: { width, height } } : n);
|
|
3714
|
+
this.commit({ ...model, nodes });
|
|
3715
|
+
};
|
|
3716
|
+
this.onWindowPointerUpForResize = () => {
|
|
3717
|
+
this.resize = null;
|
|
3718
|
+
window.removeEventListener('pointermove', this.onWindowPointerMoveForResize);
|
|
3719
|
+
window.removeEventListener('pointerup', this.onWindowPointerUpForResize);
|
|
3720
|
+
window.removeEventListener('pointercancel', this.onWindowPointerUpForResize);
|
|
3721
|
+
};
|
|
3722
|
+
this.onWindowPointerMoveForLink = (ev) => {
|
|
3723
|
+
const st = this.linkDrag();
|
|
3724
|
+
if (!st)
|
|
3725
|
+
return;
|
|
3726
|
+
const to = this.canvasPointFromClient(ev.clientX, ev.clientY);
|
|
3727
|
+
this.linkDrag.set({ ...st, to });
|
|
3728
|
+
const model = this.model();
|
|
3729
|
+
const node = findNode(model, st.sourceNodeId);
|
|
3730
|
+
const port = node ? findPort(node, st.sourcePortId)?.port : undefined;
|
|
3731
|
+
if (node && port) {
|
|
3732
|
+
this.linkDragMove.emit({ sourceNode: node, sourcePort: port, point: to, event: ev });
|
|
3733
|
+
}
|
|
3734
|
+
const hover = this.pickInboundPortAtPoint(model, to, st.sourceNodeId);
|
|
3735
|
+
this.linkHoverTarget.set(hover ? { nodeId: hover.node.id, portId: hover.port.id } : null);
|
|
3736
|
+
};
|
|
3737
|
+
this.onWindowPointerUpForLink = (ev) => {
|
|
3738
|
+
const st = this.linkDrag();
|
|
3739
|
+
this.linkDrag.set(null);
|
|
3740
|
+
this.linkHoverTarget.set(null);
|
|
3741
|
+
window.removeEventListener('pointermove', this.onWindowPointerMoveForLink);
|
|
3742
|
+
window.removeEventListener('pointerup', this.onWindowPointerUpForLink);
|
|
3743
|
+
window.removeEventListener('pointercancel', this.onWindowPointerUpForLink);
|
|
3744
|
+
if (!st)
|
|
3745
|
+
return;
|
|
3746
|
+
const model = this.model();
|
|
3747
|
+
const sourceNode = findNode(model, st.sourceNodeId);
|
|
3748
|
+
const sourcePort = sourceNode ? findPort(sourceNode, st.sourcePortId)?.port : undefined;
|
|
3749
|
+
if (!sourceNode || !sourcePort)
|
|
3750
|
+
return;
|
|
3751
|
+
const target = this.pickInboundPortAtPoint(model, st.to, sourceNode.id);
|
|
3752
|
+
if (!target) {
|
|
3753
|
+
this.linkDragEnd.emit({ sourceNode, sourcePort, cancelled: true, event: ev });
|
|
3754
|
+
return;
|
|
3755
|
+
}
|
|
3756
|
+
const request = {
|
|
3757
|
+
model,
|
|
3758
|
+
sourceNodeId: st.sourceNodeId,
|
|
3759
|
+
sourcePortId: st.sourcePortId,
|
|
3760
|
+
targetNodeId: target.node.id,
|
|
3761
|
+
targetPortId: target.port.id,
|
|
3762
|
+
};
|
|
3763
|
+
if (!this.evaluateConnect(request)) {
|
|
3764
|
+
this.connectionRejected.emit({ reason: 'canConnect returned false', request, event: ev });
|
|
3765
|
+
this.linkDragEnd.emit({ sourceNode, sourcePort, cancelled: true, event: ev });
|
|
3766
|
+
return;
|
|
3767
|
+
}
|
|
3768
|
+
const connector = {
|
|
3769
|
+
id: createWorkflowConnectorId(),
|
|
3770
|
+
sourceNodeId: st.sourceNodeId,
|
|
3771
|
+
sourcePortId: st.sourcePortId,
|
|
3772
|
+
targetNodeId: target.node.id,
|
|
3773
|
+
targetPortId: target.port.id,
|
|
3774
|
+
data: {},
|
|
3775
|
+
color: sourceNode.color,
|
|
3776
|
+
};
|
|
3777
|
+
const next = {
|
|
3778
|
+
nodes: [...model.nodes],
|
|
3779
|
+
connectors: [...model.connectors, connector],
|
|
3780
|
+
};
|
|
3781
|
+
this.commit(next);
|
|
3782
|
+
this.connectionCreated.emit({ connector, model: next });
|
|
3783
|
+
this.setActivation({ kind: 'connector', id: connector.id });
|
|
3784
|
+
this.linkDragEnd.emit({ sourceNode, sourcePort, cancelled: false, event: ev });
|
|
3785
|
+
};
|
|
3786
|
+
afterNextRender(() => this.attachWrapperDropTargetsWithRetry());
|
|
3787
|
+
effect(() => this.syncWrapperBackgroundStyle());
|
|
3788
|
+
effect(() => {
|
|
3789
|
+
this.zoomChange.emit(this.panZoomLevel());
|
|
3790
|
+
});
|
|
3791
|
+
this.destroyRef.onDestroy(() => this.detachWindowListeners());
|
|
3792
|
+
}
|
|
3793
|
+
zoomIn() {
|
|
3794
|
+
this.panViewRef()?.zoomIn();
|
|
3795
|
+
}
|
|
3796
|
+
zoomOut() {
|
|
3797
|
+
this.panViewRef()?.zoomOut();
|
|
3798
|
+
}
|
|
3799
|
+
zoomReset() {
|
|
3800
|
+
this.panViewRef()?.resetPosition();
|
|
3801
|
+
}
|
|
3802
|
+
getZoomLevel() {
|
|
3803
|
+
return this.panZoomLevel();
|
|
3804
|
+
}
|
|
3805
|
+
//#region ---- Readonly helpers ----
|
|
3806
|
+
isNodeReadonly(node) {
|
|
3807
|
+
return this.readonly() || node.readonly === true;
|
|
3808
|
+
}
|
|
3809
|
+
isConnectorReadonly(c) {
|
|
3810
|
+
return this.readonly() || c.readonly === true;
|
|
3811
|
+
}
|
|
3812
|
+
//#endregion
|
|
3813
|
+
//#region ---- Activation ----
|
|
3814
|
+
isNodeActive(nodeId) {
|
|
3815
|
+
return this.selectedNodeIds().has(nodeId);
|
|
3816
|
+
}
|
|
3817
|
+
isNodeHighlighted(nodeId) {
|
|
3818
|
+
return this.highlightedNodeIds().includes(nodeId);
|
|
3819
|
+
}
|
|
3820
|
+
isConnectorActive(id) {
|
|
3821
|
+
return this.selectedConnectorIds().has(id);
|
|
3822
|
+
}
|
|
3823
|
+
isPortActive(nodeId, portId) {
|
|
3824
|
+
const a = this.activation();
|
|
3825
|
+
return a?.kind === 'port' && a.nodeId === nodeId && a.id === portId;
|
|
3826
|
+
}
|
|
3827
|
+
isPortLinkHoverTarget(nodeId, portId) {
|
|
3828
|
+
if (!this.linkDrag())
|
|
3829
|
+
return false;
|
|
3830
|
+
const h = this.linkHoverTarget();
|
|
3831
|
+
return h !== null && h.nodeId === nodeId && h.portId === portId;
|
|
3832
|
+
}
|
|
3833
|
+
setActivation(next) {
|
|
3834
|
+
this.activation.set(next);
|
|
3835
|
+
this.activationChange.emit(next);
|
|
3836
|
+
}
|
|
3837
|
+
clearSelections() {
|
|
3838
|
+
this.selectedNodeIds.set(new Set());
|
|
3839
|
+
this.selectedConnectorIds.set(new Set());
|
|
3840
|
+
}
|
|
3841
|
+
//#endregion
|
|
3842
|
+
//#region ---- Model commit ----
|
|
3843
|
+
commit(next) {
|
|
3844
|
+
this.modelChange.emit(next);
|
|
3845
|
+
}
|
|
3846
|
+
/**
|
|
3847
|
+
* Auto-arranges nodes into a layered layout and commits the result.
|
|
3848
|
+
*
|
|
3849
|
+
* Honors layout options such as `direction` (`'horizontal'` / `'vertical'`),
|
|
3850
|
+
* `alignment`, `origin`, `nodeIds` (subset arrange), and `reduceCrossings`.
|
|
3851
|
+
* When `gridSize` is omitted the designer's own `gridSize` input is used.
|
|
3852
|
+
*/
|
|
3853
|
+
arrangeNicely(options) {
|
|
3854
|
+
if (this.readonly()) {
|
|
3855
|
+
return;
|
|
3856
|
+
}
|
|
3857
|
+
this.commit(this.computeArrangedLayout(options));
|
|
3858
|
+
}
|
|
3859
|
+
/**
|
|
3860
|
+
* Computes an arranged layout without committing it. Useful for previews,
|
|
3861
|
+
* comparing the result before applying it, or rendering ghost positions.
|
|
3862
|
+
*/
|
|
3863
|
+
computeArrangedLayout(options) {
|
|
3864
|
+
return arrangeWorkflowDiagram(cloneDiagram(this.model()), {
|
|
3865
|
+
gridSize: this.gridSize(),
|
|
3866
|
+
...options,
|
|
3867
|
+
});
|
|
3868
|
+
}
|
|
3869
|
+
resolvePaletteItem(type) {
|
|
3870
|
+
return this.paletteItems().find((i) => i.type === type);
|
|
3871
|
+
}
|
|
3872
|
+
/**
|
|
3873
|
+
* Full drag payload merged with `paletteItems()` entry when the payload omits optional fields.
|
|
3874
|
+
*/
|
|
3875
|
+
resolveDroppedPaletteItem(raw) {
|
|
3876
|
+
const parsed = parsePaletteItemDragPayload(raw);
|
|
3877
|
+
if (!parsed)
|
|
3878
|
+
return null;
|
|
3879
|
+
const base = this.resolvePaletteItem(parsed.type);
|
|
3880
|
+
const item = { ...(base ?? {}), ...parsed };
|
|
3881
|
+
if (typeof item.title !== 'string')
|
|
3882
|
+
return null;
|
|
3883
|
+
return item;
|
|
3884
|
+
}
|
|
3885
|
+
evaluateConnect(request) {
|
|
3886
|
+
const fn = this.canConnect();
|
|
3887
|
+
if (fn)
|
|
3888
|
+
return fn(request);
|
|
3889
|
+
return defaultCanConnectRequest(request);
|
|
3890
|
+
}
|
|
3891
|
+
//#endregion
|
|
3892
|
+
//#region ---- Canvas / surface ----
|
|
3893
|
+
onSurfacePointerDown(event) {
|
|
3894
|
+
if (!this.isBackgroundInteractionTarget(event.target))
|
|
3895
|
+
return;
|
|
3896
|
+
this.designerPointerDown.emit(event);
|
|
3897
|
+
}
|
|
3898
|
+
onSurfaceClick(event) {
|
|
3899
|
+
if (!this.isBackgroundInteractionTarget(event.target))
|
|
3900
|
+
return;
|
|
3901
|
+
this.clearSelections();
|
|
3902
|
+
this.setActivation({ kind: 'canvas', id: 'canvas' });
|
|
3903
|
+
this.designerClick.emit(event);
|
|
3904
|
+
}
|
|
3905
|
+
onSurfaceDoubleClick(event) {
|
|
3906
|
+
if (!this.isBackgroundInteractionTarget(event.target))
|
|
3907
|
+
return;
|
|
3908
|
+
this.designerDoubleClick.emit(event);
|
|
3909
|
+
}
|
|
3910
|
+
isBackgroundInteractionTarget(target) {
|
|
3911
|
+
if (!target)
|
|
3912
|
+
return false;
|
|
3913
|
+
if (target.closest('.axp-workflow-designer__node'))
|
|
3914
|
+
return false;
|
|
3915
|
+
if (target.closest('.axp-workflow-designer__connector-actions'))
|
|
3916
|
+
return false;
|
|
3917
|
+
if (target.closest('.axp-workflow-designer__port-cluster'))
|
|
3918
|
+
return false;
|
|
3919
|
+
if (target.closest('path.axp-workflow-designer__edge-hit'))
|
|
3920
|
+
return false;
|
|
3921
|
+
if (target.closest('ax-button'))
|
|
3922
|
+
return false;
|
|
3923
|
+
return true;
|
|
3924
|
+
}
|
|
3925
|
+
canvasPointFromClient(clientX, clientY) {
|
|
3926
|
+
const surface = this.surfaceRef().nativeElement;
|
|
3927
|
+
const wrapper = surface.closest('.ax-pan-view-wrapper');
|
|
3928
|
+
const basisRect = (wrapper ?? surface).getBoundingClientRect();
|
|
3929
|
+
const localX = clientX - basisRect.left;
|
|
3930
|
+
const localY = clientY - basisRect.top;
|
|
3931
|
+
const computed = getComputedStyle(surface);
|
|
3932
|
+
const transform = computed.transform;
|
|
3933
|
+
if (!transform || transform === 'none') {
|
|
3934
|
+
return this.surfacePointToModel({ x: localX, y: localY });
|
|
3935
|
+
}
|
|
3936
|
+
const matrix = new DOMMatrixReadOnly(transform);
|
|
3937
|
+
const originParts = computed.transformOrigin.split(' ');
|
|
3938
|
+
const originX = Number.parseFloat(originParts[0] ?? '0') || 0;
|
|
3939
|
+
const originY = Number.parseFloat(originParts[1] ?? '0') || 0;
|
|
3940
|
+
// CSS transform is applied around transform-origin, so include origin translation in matrix inversion.
|
|
3941
|
+
const withOrigin = new DOMMatrixReadOnly()
|
|
3942
|
+
.translate(originX, originY)
|
|
3943
|
+
.multiply(matrix)
|
|
3944
|
+
.translate(-originX, -originY);
|
|
3945
|
+
const point = new DOMPoint(localX, localY).matrixTransform(withOrigin.inverse());
|
|
3946
|
+
return this.surfacePointToModel({ x: point.x, y: point.y });
|
|
3947
|
+
}
|
|
3948
|
+
//#endregion
|
|
3949
|
+
//#region ---- Drag / drop (palette) ----
|
|
3950
|
+
onCanvasDragOver(event) {
|
|
3951
|
+
if (this.readonly())
|
|
3952
|
+
return;
|
|
3953
|
+
event.preventDefault();
|
|
3954
|
+
if (event.dataTransfer)
|
|
3955
|
+
event.dataTransfer.dropEffect = 'copy';
|
|
3956
|
+
}
|
|
3957
|
+
onCanvasDrop(event) {
|
|
3958
|
+
if (this.readonly())
|
|
3959
|
+
return;
|
|
3960
|
+
event.preventDefault();
|
|
3961
|
+
event.stopPropagation();
|
|
3962
|
+
const raw = event.dataTransfer?.getData(AXP_WORKFLOW_PALETTE_MIME);
|
|
3963
|
+
const item = this.resolveDroppedPaletteItem(raw);
|
|
3964
|
+
if (!item)
|
|
3965
|
+
return;
|
|
3966
|
+
const pt = this.canvasPointFromClient(event.clientX, event.clientY);
|
|
3967
|
+
const node = createNodeFromPaletteItem(item, this.applySnapToPointForNewNode(item, pt));
|
|
3968
|
+
const model = this.model();
|
|
3969
|
+
const next = {
|
|
3970
|
+
nodes: [...model.nodes, node],
|
|
3971
|
+
connectors: [...model.connectors],
|
|
3972
|
+
};
|
|
3973
|
+
this.commit(next);
|
|
3974
|
+
this.setActivation({ kind: 'node', id: node.id });
|
|
3975
|
+
}
|
|
3976
|
+
applySnapToPointForNewNode(item, pt) {
|
|
3977
|
+
const nodeSnap = item.snapToGrid;
|
|
3978
|
+
const useSnap = nodeSnap ?? this.snapToGrid();
|
|
3979
|
+
const g = this.gridSize();
|
|
3980
|
+
if (!useSnap || g <= 0)
|
|
3981
|
+
return pt;
|
|
3982
|
+
return snapPoint(pt, g);
|
|
3983
|
+
}
|
|
3984
|
+
applySnapForNode(node, pt) {
|
|
3985
|
+
const useSnap = node.snapToGrid ?? this.snapToGrid();
|
|
3986
|
+
const g = this.gridSize();
|
|
3987
|
+
if (!useSnap || g <= 0)
|
|
3988
|
+
return pt;
|
|
3989
|
+
return snapPoint(pt, g);
|
|
3990
|
+
}
|
|
3991
|
+
//#endregion
|
|
3992
|
+
//#region ---- Wrapper drop bridge ----
|
|
3993
|
+
attachWrapperDropTargetsWithRetry() {
|
|
3994
|
+
this.clearWrapperAttachRetry();
|
|
3995
|
+
this.wrapperAttachAttempt = 0;
|
|
3996
|
+
const tryAttach = () => {
|
|
3997
|
+
const attached = this.attachWrapperDropTargets();
|
|
3998
|
+
if (attached) {
|
|
3999
|
+
return;
|
|
4000
|
+
}
|
|
4001
|
+
this.wrapperAttachAttempt += 1;
|
|
4002
|
+
if (this.wrapperAttachAttempt >= 20) {
|
|
4003
|
+
return;
|
|
4004
|
+
}
|
|
4005
|
+
this.wrapperAttachRetryHandle = setTimeout(tryAttach, 50);
|
|
4006
|
+
};
|
|
4007
|
+
tryAttach();
|
|
4008
|
+
}
|
|
4009
|
+
/**
|
|
4010
|
+
* Binds drag/drop to pan-view wrapper so users can drop while panning far away
|
|
4011
|
+
* from current finite surface bounds.
|
|
4012
|
+
*/
|
|
4013
|
+
attachWrapperDropTargets() {
|
|
4014
|
+
const surface = this.surfaceRef().nativeElement;
|
|
4015
|
+
const wrapper = surface.closest('.ax-pan-view-wrapper');
|
|
4016
|
+
if (!wrapper) {
|
|
4017
|
+
return false;
|
|
4018
|
+
}
|
|
4019
|
+
this.detachWrapperDropTargets();
|
|
4020
|
+
this.wrapperElement.set(wrapper);
|
|
4021
|
+
this.wrapperDragoverListener = (event) => {
|
|
4022
|
+
const target = event.target;
|
|
4023
|
+
if (target?.closest('.axp-workflow-designer__surface')) {
|
|
4024
|
+
return;
|
|
4025
|
+
}
|
|
4026
|
+
this.onCanvasDragOver(event);
|
|
4027
|
+
};
|
|
4028
|
+
this.wrapperDropListener = (event) => {
|
|
4029
|
+
const target = event.target;
|
|
4030
|
+
if (target?.closest('.axp-workflow-designer__surface')) {
|
|
4031
|
+
return;
|
|
4032
|
+
}
|
|
4033
|
+
this.onCanvasDrop(event);
|
|
4034
|
+
};
|
|
4035
|
+
wrapper.addEventListener('dragover', this.wrapperDragoverListener);
|
|
4036
|
+
wrapper.addEventListener('drop', this.wrapperDropListener);
|
|
4037
|
+
this.syncWrapperBackgroundStyle();
|
|
4038
|
+
return true;
|
|
4039
|
+
}
|
|
4040
|
+
detachWrapperDropTargets() {
|
|
4041
|
+
const surface = this.surfaceRef().nativeElement;
|
|
4042
|
+
const wrapper = surface.closest('.ax-pan-view-wrapper');
|
|
4043
|
+
if (!wrapper) {
|
|
4044
|
+
this.wrapperElement.set(null);
|
|
4045
|
+
this.wrapperDragoverListener = null;
|
|
4046
|
+
this.wrapperDropListener = null;
|
|
4047
|
+
return;
|
|
4048
|
+
}
|
|
4049
|
+
if (this.wrapperDragoverListener) {
|
|
4050
|
+
wrapper.removeEventListener('dragover', this.wrapperDragoverListener);
|
|
4051
|
+
this.wrapperDragoverListener = null;
|
|
4052
|
+
}
|
|
4053
|
+
if (this.wrapperDropListener) {
|
|
4054
|
+
wrapper.removeEventListener('drop', this.wrapperDropListener);
|
|
4055
|
+
this.wrapperDropListener = null;
|
|
4056
|
+
}
|
|
4057
|
+
this.wrapperElement.set(null);
|
|
4058
|
+
}
|
|
4059
|
+
clearWrapperAttachRetry() {
|
|
4060
|
+
if (this.wrapperAttachRetryHandle != null) {
|
|
4061
|
+
clearTimeout(this.wrapperAttachRetryHandle);
|
|
4062
|
+
this.wrapperAttachRetryHandle = null;
|
|
4063
|
+
}
|
|
4064
|
+
}
|
|
4065
|
+
//#endregion
|
|
4066
|
+
//#region ---- Keyboard ----
|
|
4067
|
+
onWindowKeydown(ev) {
|
|
4068
|
+
if (this.readonly())
|
|
4069
|
+
return;
|
|
4070
|
+
const target = ev.target;
|
|
4071
|
+
if (target?.closest('input,textarea,select,[contenteditable="true"]'))
|
|
4072
|
+
return;
|
|
4073
|
+
if (ev.key === 'Escape') {
|
|
4074
|
+
ev.preventDefault();
|
|
4075
|
+
this.cancelLinkDrag(ev);
|
|
4076
|
+
this.clearSelections();
|
|
4077
|
+
this.setActivation(null);
|
|
4078
|
+
return;
|
|
4079
|
+
}
|
|
4080
|
+
if (ev.key !== 'Delete' && ev.key !== 'Backspace')
|
|
4081
|
+
return;
|
|
4082
|
+
ev.preventDefault();
|
|
4083
|
+
const selectedConnectorIds = this.selectedConnectorIds();
|
|
4084
|
+
const selectedNodeIds = this.selectedNodeIds();
|
|
4085
|
+
const model = this.model();
|
|
4086
|
+
if (selectedConnectorIds.size > 0 || selectedNodeIds.size > 0) {
|
|
4087
|
+
const nodes = model.nodes.filter((n) => !selectedNodeIds.has(n.id));
|
|
4088
|
+
const connectors = model.connectors.filter((c) => !selectedConnectorIds.has(c.id) &&
|
|
4089
|
+
!selectedNodeIds.has(c.sourceNodeId) &&
|
|
4090
|
+
!selectedNodeIds.has(c.targetNodeId));
|
|
4091
|
+
this.commit({ ...model, nodes, connectors });
|
|
4092
|
+
this.clearSelections();
|
|
4093
|
+
this.setActivation(null);
|
|
4094
|
+
return;
|
|
4095
|
+
}
|
|
4096
|
+
const a = this.activation();
|
|
4097
|
+
if (!a || a.kind === 'canvas')
|
|
4098
|
+
return;
|
|
4099
|
+
if (a.kind === 'connector') {
|
|
4100
|
+
const c = findConnector(model, a.id);
|
|
4101
|
+
if (c && !this.isConnectorReadonly(c)) {
|
|
4102
|
+
const connectors = model.connectors.filter((x) => x.id !== c.id);
|
|
4103
|
+
this.commit({ ...model, connectors });
|
|
4104
|
+
this.setActivation(null);
|
|
4105
|
+
}
|
|
4106
|
+
return;
|
|
4107
|
+
}
|
|
4108
|
+
const nodeId = a.kind === 'node' ? a.id : a.nodeId;
|
|
4109
|
+
if (!nodeId)
|
|
4110
|
+
return;
|
|
4111
|
+
const node = findNode(model, nodeId);
|
|
4112
|
+
if (!node || this.isNodeReadonly(node))
|
|
4113
|
+
return;
|
|
4114
|
+
const nodes = model.nodes.filter((n) => n.id !== nodeId);
|
|
4115
|
+
const connectors = model.connectors.filter((c) => c.sourceNodeId !== nodeId && c.targetNodeId !== nodeId);
|
|
4116
|
+
this.commit({ ...model, nodes, connectors });
|
|
4117
|
+
this.setActivation(null);
|
|
4118
|
+
}
|
|
4119
|
+
cancelLinkDrag(ev) {
|
|
4120
|
+
const st = this.linkDrag();
|
|
4121
|
+
if (!st)
|
|
4122
|
+
return;
|
|
4123
|
+
this.linkDrag.set(null);
|
|
4124
|
+
this.linkHoverTarget.set(null);
|
|
4125
|
+
window.removeEventListener('pointermove', this.onWindowPointerMoveForLink);
|
|
4126
|
+
window.removeEventListener('pointerup', this.onWindowPointerUpForLink);
|
|
4127
|
+
window.removeEventListener('pointercancel', this.onWindowPointerUpForLink);
|
|
4128
|
+
const model = this.model();
|
|
4129
|
+
const sourceNode = findNode(model, st.sourceNodeId);
|
|
4130
|
+
const sourcePort = sourceNode ? findPort(sourceNode, st.sourcePortId)?.port : undefined;
|
|
4131
|
+
if (sourceNode && sourcePort) {
|
|
4132
|
+
this.linkDragEnd.emit({ sourceNode, sourcePort, cancelled: true, event: ev });
|
|
4133
|
+
}
|
|
4134
|
+
}
|
|
4135
|
+
//#endregion
|
|
4136
|
+
//#region ---- Node chrome ----
|
|
4137
|
+
shapeClass(shape) {
|
|
4138
|
+
return `axp-workflow-designer__node-shape--${shape}`;
|
|
4139
|
+
}
|
|
4140
|
+
semanticSurfaceClass(color) {
|
|
4141
|
+
return color ? `axp-${color}-surface` : '';
|
|
4142
|
+
}
|
|
4143
|
+
semanticBorderClass(color) {
|
|
4144
|
+
return color ? `axp-${color}-border` : '';
|
|
4145
|
+
}
|
|
4146
|
+
nodeShapeClasses(node) {
|
|
4147
|
+
return ['axp-workflow-designer__node-shape', this.shapeClass(node.shape), this.semanticSurfaceClass(node.color), this.semanticBorderClass(node.borderColor)]
|
|
4148
|
+
.filter(Boolean)
|
|
4149
|
+
.join(' ');
|
|
4150
|
+
}
|
|
4151
|
+
connectorEdgeClasses(c) {
|
|
4152
|
+
const parts = [
|
|
4153
|
+
'axp-workflow-designer__edge',
|
|
4154
|
+
this.semanticSurfaceClass(c.color),
|
|
4155
|
+
this.semanticBorderClass(c.borderColor),
|
|
4156
|
+
this.isConnectorActive(c.id) ? 'axp-workflow-designer__edge--active' : '',
|
|
4157
|
+
];
|
|
4158
|
+
return parts.filter(Boolean).join(' ');
|
|
4159
|
+
}
|
|
4160
|
+
onNodePointerDown(node, event) {
|
|
4161
|
+
if (this.isNodeReadonly(node))
|
|
4162
|
+
return;
|
|
4163
|
+
if (event.button !== 0)
|
|
4164
|
+
return;
|
|
4165
|
+
this.nodePointerDown.emit({ node, event });
|
|
4166
|
+
}
|
|
4167
|
+
onNodeClick(node, event) {
|
|
4168
|
+
if (this.suppressNextNodeClick) {
|
|
4169
|
+
this.suppressNextNodeClick = false;
|
|
4170
|
+
event.stopPropagation();
|
|
4171
|
+
return;
|
|
4172
|
+
}
|
|
4173
|
+
event.stopPropagation();
|
|
4174
|
+
const append = event.ctrlKey || event.metaKey;
|
|
4175
|
+
if (append) {
|
|
4176
|
+
const next = new Set(this.selectedNodeIds());
|
|
4177
|
+
if (next.has(node.id))
|
|
4178
|
+
next.delete(node.id);
|
|
4179
|
+
else
|
|
4180
|
+
next.add(node.id);
|
|
4181
|
+
this.selectedNodeIds.set(next);
|
|
4182
|
+
this.selectedConnectorIds.set(new Set());
|
|
4183
|
+
}
|
|
4184
|
+
else {
|
|
4185
|
+
this.selectedNodeIds.set(new Set([node.id]));
|
|
4186
|
+
this.selectedConnectorIds.set(new Set());
|
|
4187
|
+
}
|
|
4188
|
+
this.setActivation({ kind: 'node', id: node.id });
|
|
4189
|
+
this.nodeClick.emit({ node, event: event });
|
|
4190
|
+
}
|
|
4191
|
+
onNodeDoubleClick(node, event) {
|
|
4192
|
+
event.stopPropagation();
|
|
4193
|
+
this.nodeDoubleClick.emit({ node, event });
|
|
4194
|
+
}
|
|
4195
|
+
onNodeContextMenu(node, event) {
|
|
4196
|
+
if (this.isNodeReadonly(node)) {
|
|
4197
|
+
return;
|
|
4198
|
+
}
|
|
4199
|
+
event.preventDefault();
|
|
4200
|
+
event.stopPropagation();
|
|
4201
|
+
this.selectedNodeIds.set(new Set([node.id]));
|
|
4202
|
+
this.selectedConnectorIds.set(new Set());
|
|
4203
|
+
this.setActivation({ kind: 'node', id: node.id });
|
|
4204
|
+
this.nodeContextMenu.emit({ node, event });
|
|
4205
|
+
}
|
|
4206
|
+
onNodeBodyPointerDown(node, event) {
|
|
4207
|
+
if (this.isNodeReadonly(node))
|
|
4208
|
+
return;
|
|
4209
|
+
if (event.button !== 0)
|
|
4210
|
+
return;
|
|
4211
|
+
if (event.target.closest('.axp-workflow-designer__port-cluster'))
|
|
4212
|
+
return;
|
|
4213
|
+
if (event.target.closest('.axp-workflow-designer__resize'))
|
|
4214
|
+
return;
|
|
4215
|
+
// Ctrl/Cmd click is reserved for multi-select toggling in click handler; do not start drag here.
|
|
4216
|
+
if (event.ctrlKey || event.metaKey)
|
|
4217
|
+
return;
|
|
4218
|
+
event.stopPropagation();
|
|
4219
|
+
const selected = this.selectedNodeIds();
|
|
4220
|
+
const moveIds = selected.has(node.id) && selected.size > 0 ? [...selected] : [node.id];
|
|
4221
|
+
if (!selected.has(node.id) || selected.size === 0) {
|
|
4222
|
+
this.selectedNodeIds.set(new Set([node.id]));
|
|
4223
|
+
this.selectedConnectorIds.set(new Set());
|
|
4224
|
+
}
|
|
4225
|
+
this.setActivation({ kind: 'node', id: node.id });
|
|
4226
|
+
const p = this.canvasPointFromClient(event.clientX, event.clientY);
|
|
4227
|
+
const model = this.model();
|
|
4228
|
+
const origins = {};
|
|
4229
|
+
for (const moveId of moveIds) {
|
|
4230
|
+
const n = findNode(model, moveId);
|
|
4231
|
+
if (n)
|
|
4232
|
+
origins[moveId] = { ...n.position };
|
|
4233
|
+
}
|
|
4234
|
+
this.nodeMove = {
|
|
4235
|
+
nodeIds: moveIds,
|
|
4236
|
+
start: p,
|
|
4237
|
+
origins,
|
|
4238
|
+
moved: false,
|
|
4239
|
+
};
|
|
4240
|
+
this.nodeDragStart.emit({ node, event });
|
|
4241
|
+
window.addEventListener('pointermove', this.onWindowPointerMoveForNode);
|
|
4242
|
+
window.addEventListener('pointerup', this.onWindowPointerUpForNode, { once: false });
|
|
4243
|
+
window.addEventListener('pointercancel', this.onWindowPointerUpForNode, { once: false });
|
|
4244
|
+
event.target.setPointerCapture?.(event.pointerId);
|
|
4245
|
+
}
|
|
4246
|
+
//#endregion
|
|
4247
|
+
//#region ---- Resize ----
|
|
4248
|
+
onResizePointerDown(node, handle, event) {
|
|
4249
|
+
if (this.isNodeReadonly(node))
|
|
4250
|
+
return;
|
|
4251
|
+
if (event.button !== 0)
|
|
4252
|
+
return;
|
|
4253
|
+
event.stopPropagation();
|
|
4254
|
+
this.resize = {
|
|
4255
|
+
nodeId: node.id,
|
|
4256
|
+
handle,
|
|
4257
|
+
startPointer: this.canvasPointFromClient(event.clientX, event.clientY),
|
|
4258
|
+
startPosition: { ...node.position },
|
|
4259
|
+
startSize: { ...node.size },
|
|
4260
|
+
aspect: node.size.width / Math.max(1, node.size.height),
|
|
4261
|
+
};
|
|
4262
|
+
window.addEventListener('pointermove', this.onWindowPointerMoveForResize);
|
|
4263
|
+
window.addEventListener('pointerup', this.onWindowPointerUpForResize);
|
|
4264
|
+
window.addEventListener('pointercancel', this.onWindowPointerUpForResize);
|
|
4265
|
+
}
|
|
4266
|
+
//#endregion
|
|
4267
|
+
//#region ---- Ports & linking ----
|
|
4268
|
+
onPortPointerDown(node, port, event) {
|
|
4269
|
+
if (this.isNodeReadonly(node))
|
|
4270
|
+
return;
|
|
4271
|
+
if (port.readonly)
|
|
4272
|
+
return;
|
|
4273
|
+
if (event.button !== 0)
|
|
4274
|
+
return;
|
|
4275
|
+
event.stopPropagation();
|
|
4276
|
+
this.portPointerDown.emit({ node, port, event });
|
|
4277
|
+
this.setActivation({ kind: 'port', id: port.id, nodeId: node.id });
|
|
4278
|
+
if (port.kind === 'outbound' && !port.disabled) {
|
|
4279
|
+
this.linkHoverTarget.set(null);
|
|
4280
|
+
const from = portCanvasPosition(node, port);
|
|
4281
|
+
this.linkDrag.set({
|
|
4282
|
+
sourceNodeId: node.id,
|
|
4283
|
+
sourcePortId: port.id,
|
|
4284
|
+
from,
|
|
4285
|
+
to: from,
|
|
4286
|
+
});
|
|
4287
|
+
this.linkDragStart.emit({ sourceNode: node, sourcePort: port, event });
|
|
4288
|
+
window.addEventListener('pointermove', this.onWindowPointerMoveForLink);
|
|
4289
|
+
window.addEventListener('pointerup', this.onWindowPointerUpForLink);
|
|
4290
|
+
window.addEventListener('pointercancel', this.onWindowPointerUpForLink);
|
|
4291
|
+
}
|
|
4292
|
+
}
|
|
4293
|
+
pickInboundPortAtPoint(model, point, excludeSourceNodeId) {
|
|
4294
|
+
let best = null;
|
|
4295
|
+
const hitR = 18;
|
|
4296
|
+
for (const node of model.nodes) {
|
|
4297
|
+
if (node.id === excludeSourceNodeId)
|
|
4298
|
+
continue;
|
|
4299
|
+
if (this.isNodeReadonly(node))
|
|
4300
|
+
continue;
|
|
4301
|
+
for (const port of node.inboundPorts) {
|
|
4302
|
+
if (port.disabled || port.readonly)
|
|
4303
|
+
continue;
|
|
4304
|
+
const c = portCanvasPosition(node, port);
|
|
4305
|
+
const d = Math.hypot(c.x - point.x, c.y - point.y);
|
|
4306
|
+
if (d <= hitR && (!best || d < best.d))
|
|
4307
|
+
best = { node, port, d };
|
|
4308
|
+
}
|
|
4309
|
+
}
|
|
4310
|
+
return best ? { node: best.node, port: best.port } : null;
|
|
4311
|
+
}
|
|
4312
|
+
//#endregion
|
|
4313
|
+
//#region ---- Connectors UI ----
|
|
4314
|
+
connectorPathD(c) {
|
|
4315
|
+
const model = this.model();
|
|
4316
|
+
const sn = findNode(model, c.sourceNodeId);
|
|
4317
|
+
const tn = findNode(model, c.targetNodeId);
|
|
4318
|
+
if (!sn || !tn)
|
|
4319
|
+
return '';
|
|
4320
|
+
const sp = findPort(sn, c.sourcePortId)?.port;
|
|
4321
|
+
const tp = findPort(tn, c.targetPortId)?.port;
|
|
4322
|
+
if (!sp || !tp)
|
|
4323
|
+
return '';
|
|
4324
|
+
const from = this.modelPointToSurface(portCanvasPosition(sn, sp));
|
|
4325
|
+
const to = this.trimmedConnectorTarget(from, this.modelPointToSurface(portCanvasPosition(tn, tp)));
|
|
4326
|
+
return connectorPath(from, to, this.connectionPreviewStyle(), sp.side, {
|
|
4327
|
+
targetSide: tp.side,
|
|
4328
|
+
obstacles: this.collectObstaclesInSurface(),
|
|
4329
|
+
});
|
|
4330
|
+
}
|
|
4331
|
+
connectorMidpoint(c) {
|
|
4332
|
+
const model = this.model();
|
|
4333
|
+
const sn = findNode(model, c.sourceNodeId);
|
|
4334
|
+
const tn = findNode(model, c.targetNodeId);
|
|
4335
|
+
if (!sn || !tn)
|
|
4336
|
+
return { x: 0, y: 0 };
|
|
4337
|
+
const sp = findPort(sn, c.sourcePortId)?.port;
|
|
4338
|
+
const tp = findPort(tn, c.targetPortId)?.port;
|
|
4339
|
+
if (!sp || !tp)
|
|
4340
|
+
return { x: 0, y: 0 };
|
|
4341
|
+
const a = this.modelPointToSurface(portCanvasPosition(sn, sp));
|
|
4342
|
+
const b = this.modelPointToSurface(portCanvasPosition(tn, tp));
|
|
4343
|
+
return { x: (a.x + b.x) * 0.5, y: (a.y + b.y) * 0.5 };
|
|
4344
|
+
}
|
|
4345
|
+
onConnectorClick(c, event) {
|
|
4346
|
+
event.stopPropagation();
|
|
4347
|
+
event.preventDefault();
|
|
4348
|
+
const append = event.ctrlKey || event.metaKey;
|
|
4349
|
+
if (append) {
|
|
4350
|
+
const next = new Set(this.selectedConnectorIds());
|
|
4351
|
+
if (next.has(c.id))
|
|
4352
|
+
next.delete(c.id);
|
|
4353
|
+
else
|
|
4354
|
+
next.add(c.id);
|
|
4355
|
+
this.selectedConnectorIds.set(next);
|
|
4356
|
+
this.selectedNodeIds.set(new Set());
|
|
4357
|
+
}
|
|
4358
|
+
else {
|
|
4359
|
+
this.selectedConnectorIds.set(new Set([c.id]));
|
|
4360
|
+
this.selectedNodeIds.set(new Set());
|
|
4361
|
+
}
|
|
4362
|
+
this.setActivation({ kind: 'connector', id: c.id });
|
|
4363
|
+
this.connectorClick.emit({ connector: c, event });
|
|
4364
|
+
}
|
|
4365
|
+
onConnectorDoubleClick(c, event) {
|
|
4366
|
+
event.stopPropagation();
|
|
4367
|
+
event.preventDefault();
|
|
4368
|
+
this.connectorDoubleClick.emit({ connector: c, event });
|
|
4369
|
+
}
|
|
4370
|
+
removeConnector(c) {
|
|
4371
|
+
if (this.isConnectorReadonly(c))
|
|
4372
|
+
return;
|
|
4373
|
+
const model = this.model();
|
|
4374
|
+
const connectors = model.connectors.filter((x) => x.id !== c.id);
|
|
4375
|
+
this.commit({ ...model, connectors });
|
|
4376
|
+
this.setActivation(null);
|
|
4377
|
+
}
|
|
4378
|
+
previewPath() {
|
|
4379
|
+
const st = this.linkDrag();
|
|
4380
|
+
if (!st)
|
|
4381
|
+
return '';
|
|
4382
|
+
const model = this.model();
|
|
4383
|
+
const sn = findNode(model, st.sourceNodeId);
|
|
4384
|
+
const sp = sn ? findPort(sn, st.sourcePortId)?.port : undefined;
|
|
4385
|
+
if (!sn || !sp)
|
|
4386
|
+
return '';
|
|
4387
|
+
const from = this.modelPointToSurface(st.from);
|
|
4388
|
+
const to = this.modelPointToSurface(st.to);
|
|
4389
|
+
const hover = this.linkHoverTarget();
|
|
4390
|
+
const hoverNode = hover ? findNode(model, hover.nodeId) : undefined;
|
|
4391
|
+
const hoverPort = hover && hoverNode ? findPort(hoverNode, hover.portId)?.port : undefined;
|
|
4392
|
+
return connectorPath(from, this.trimmedConnectorTarget(from, to), this.connectionPreviewStyle(), sp.side, {
|
|
4393
|
+
targetSide: hoverPort?.side,
|
|
4394
|
+
obstacles: this.collectObstaclesInSurface(),
|
|
4395
|
+
});
|
|
4396
|
+
}
|
|
4397
|
+
//#endregion
|
|
4398
|
+
//#region ---- Template context ----
|
|
4399
|
+
templateContext(node) {
|
|
4400
|
+
return { $implicit: node, node };
|
|
4401
|
+
}
|
|
4402
|
+
portClusterStyle(node, port) {
|
|
4403
|
+
const local = portAnchorOnNode(node, port);
|
|
4404
|
+
const w = Math.max(1, node.size.width);
|
|
4405
|
+
const h = Math.max(1, node.size.height);
|
|
4406
|
+
return {
|
|
4407
|
+
position: 'absolute',
|
|
4408
|
+
left: `${(local.x / w) * 100}%`,
|
|
4409
|
+
top: `${(local.y / h) * 100}%`,
|
|
4410
|
+
'z-index': '3',
|
|
4411
|
+
};
|
|
4412
|
+
}
|
|
4413
|
+
portLabelText(port) {
|
|
4414
|
+
return (port.label ?? port.key ?? '').trim();
|
|
4415
|
+
}
|
|
4416
|
+
portLabelX(node, port) {
|
|
4417
|
+
return this.portLabelCanvasPoint(node, port).x;
|
|
4418
|
+
}
|
|
4419
|
+
portLabelY(node, port) {
|
|
4420
|
+
return this.portLabelCanvasPoint(node, port).y;
|
|
4421
|
+
}
|
|
4422
|
+
portLabelAnchor(node, port) {
|
|
4423
|
+
const centerX = this.modelPointToSurface({ x: node.position.x + node.size.width * 0.5, y: node.position.y }).x;
|
|
4424
|
+
const anchor = this.modelPointToSurface(portCanvasPosition(node, port));
|
|
4425
|
+
const dx = anchor.x - centerX;
|
|
4426
|
+
if (Math.abs(dx) < 6)
|
|
4427
|
+
return 'middle';
|
|
4428
|
+
return dx < 0 ? 'end' : 'start';
|
|
4429
|
+
}
|
|
4430
|
+
nodeSurfaceLeft(node) {
|
|
4431
|
+
return node.position.x + this.canvasViewport().offsetX;
|
|
4432
|
+
}
|
|
4433
|
+
nodeSurfaceTop(node) {
|
|
4434
|
+
return node.position.y + this.canvasViewport().offsetY;
|
|
4435
|
+
}
|
|
4436
|
+
trimmedConnectorTarget(from, to) {
|
|
4437
|
+
const dx = to.x - from.x;
|
|
4438
|
+
const dy = to.y - from.y;
|
|
4439
|
+
const len = Math.hypot(dx, dy);
|
|
4440
|
+
if (len < 0.001)
|
|
4441
|
+
return to;
|
|
4442
|
+
// Keep arrowhead just outside the 14px port without creating a visible gap.
|
|
4443
|
+
const trim = 7;
|
|
4444
|
+
return {
|
|
4445
|
+
x: to.x - (dx / len) * trim,
|
|
4446
|
+
y: to.y - (dy / len) * trim,
|
|
4447
|
+
};
|
|
4448
|
+
}
|
|
4449
|
+
portLabelCanvasPoint(node, port) {
|
|
4450
|
+
const anchor = this.modelPointToSurface(portCanvasPosition(node, port));
|
|
4451
|
+
const center = {
|
|
4452
|
+
x: node.position.x + node.size.width * 0.5 + this.canvasViewport().offsetX,
|
|
4453
|
+
y: node.position.y + node.size.height * 0.5 + this.canvasViewport().offsetY,
|
|
4454
|
+
};
|
|
4455
|
+
const vx = anchor.x - center.x;
|
|
4456
|
+
const vy = anchor.y - center.y;
|
|
4457
|
+
const len = Math.hypot(vx, vy);
|
|
4458
|
+
if (len < 0.001)
|
|
4459
|
+
return anchor;
|
|
4460
|
+
const outward = 18;
|
|
4461
|
+
return {
|
|
4462
|
+
x: anchor.x + (vx / len) * outward,
|
|
4463
|
+
y: anchor.y + (vy / len) * outward,
|
|
4464
|
+
};
|
|
4465
|
+
}
|
|
4466
|
+
//#endregion
|
|
4467
|
+
//#region ---- Canvas mapping ----
|
|
4468
|
+
modelPointToSurface(point) {
|
|
4469
|
+
const viewport = this.canvasViewport();
|
|
4470
|
+
return {
|
|
4471
|
+
x: point.x + viewport.offsetX,
|
|
4472
|
+
y: point.y + viewport.offsetY,
|
|
4473
|
+
};
|
|
4474
|
+
}
|
|
4475
|
+
surfacePointToModel(point) {
|
|
4476
|
+
const viewport = this.canvasViewport();
|
|
4477
|
+
return {
|
|
4478
|
+
x: point.x - viewport.offsetX,
|
|
4479
|
+
y: point.y - viewport.offsetY,
|
|
4480
|
+
};
|
|
4481
|
+
}
|
|
4482
|
+
/**
|
|
4483
|
+
* Returns node bounding boxes in surface coordinates.
|
|
4484
|
+
*
|
|
4485
|
+
* All nodes are returned so the router can route AROUND every node, including
|
|
4486
|
+
* the source and target of a connector. The router itself drops obstacles that
|
|
4487
|
+
* already contain the stub-out point so the connector can still exit/enter its port.
|
|
4488
|
+
*/
|
|
4489
|
+
collectObstaclesInSurface() {
|
|
4490
|
+
const viewport = this.canvasViewport();
|
|
4491
|
+
const obstacles = [];
|
|
4492
|
+
for (const node of this.model().nodes) {
|
|
4493
|
+
obstacles.push({
|
|
4494
|
+
position: { x: node.position.x + viewport.offsetX, y: node.position.y + viewport.offsetY },
|
|
4495
|
+
size: { width: node.size.width, height: node.size.height },
|
|
4496
|
+
});
|
|
4497
|
+
}
|
|
4498
|
+
return obstacles;
|
|
4499
|
+
}
|
|
4500
|
+
//#endregion
|
|
4501
|
+
//#region ---- Wrapper visuals ----
|
|
4502
|
+
syncWrapperBackgroundStyle() {
|
|
4503
|
+
const wrapper = this.wrapperElement();
|
|
4504
|
+
if (!wrapper) {
|
|
4505
|
+
return;
|
|
4506
|
+
}
|
|
4507
|
+
const gridStyle = this.gridStyle();
|
|
4508
|
+
wrapper.style.backgroundColor = 'var(--ax-sys-color-lightest-surface)';
|
|
4509
|
+
wrapper.style.backgroundImage = String(gridStyle['background-image'] ?? '');
|
|
4510
|
+
wrapper.style.backgroundSize = String(gridStyle['background-size'] ?? '');
|
|
4511
|
+
wrapper.style.borderRadius = '10px';
|
|
4512
|
+
}
|
|
4513
|
+
//#endregion
|
|
4514
|
+
detachWindowListeners() {
|
|
4515
|
+
this.clearWrapperAttachRetry();
|
|
4516
|
+
this.detachWrapperDropTargets();
|
|
4517
|
+
this.linkHoverTarget.set(null);
|
|
4518
|
+
window.removeEventListener('pointermove', this.onWindowPointerMoveForNode);
|
|
4519
|
+
window.removeEventListener('pointerup', this.onWindowPointerUpForNode);
|
|
4520
|
+
window.removeEventListener('pointercancel', this.onWindowPointerUpForNode);
|
|
4521
|
+
window.removeEventListener('pointermove', this.onWindowPointerMoveForResize);
|
|
4522
|
+
window.removeEventListener('pointerup', this.onWindowPointerUpForResize);
|
|
4523
|
+
window.removeEventListener('pointercancel', this.onWindowPointerUpForResize);
|
|
4524
|
+
window.removeEventListener('pointermove', this.onWindowPointerMoveForLink);
|
|
4525
|
+
window.removeEventListener('pointerup', this.onWindowPointerUpForLink);
|
|
4526
|
+
window.removeEventListener('pointercancel', this.onWindowPointerUpForLink);
|
|
4527
|
+
}
|
|
4528
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXPWorkflowDesignerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
4529
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.8", type: AXPWorkflowDesignerComponent, isStandalone: true, selector: "axp-workflow-designer", inputs: { model: { classPropertyName: "model", publicName: "model", isSignal: true, isRequired: true, transformFunction: null }, readonly: { classPropertyName: "readonly", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null }, highlightedNodeIds: { classPropertyName: "highlightedNodeIds", publicName: "highlightedNodeIds", isSignal: true, isRequired: false, transformFunction: null }, gridSize: { classPropertyName: "gridSize", publicName: "gridSize", isSignal: true, isRequired: false, transformFunction: null }, snapToGrid: { classPropertyName: "snapToGrid", publicName: "snapToGrid", isSignal: true, isRequired: false, transformFunction: null }, connectionPreviewStyle: { classPropertyName: "connectionPreviewStyle", publicName: "connectionPreviewStyle", isSignal: true, isRequired: false, transformFunction: null }, backgroundPattern: { classPropertyName: "backgroundPattern", publicName: "backgroundPattern", isSignal: true, isRequired: false, transformFunction: null }, canConnect: { classPropertyName: "canConnect", publicName: "canConnect", isSignal: true, isRequired: false, transformFunction: null }, paletteItems: { classPropertyName: "paletteItems", publicName: "paletteItems", isSignal: true, isRequired: false, transformFunction: null }, showPortLabels: { classPropertyName: "showPortLabels", publicName: "showPortLabels", isSignal: true, isRequired: false, transformFunction: null }, nodeBodyTemplate: { classPropertyName: "nodeBodyTemplate", publicName: "nodeBodyTemplate", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { modelChange: "modelChange", zoomChange: "zoomChange", designerPointerDown: "designerPointerDown", designerClick: "designerClick", designerDoubleClick: "designerDoubleClick", nodePointerDown: "nodePointerDown", nodeClick: "nodeClick", nodeDoubleClick: "nodeDoubleClick", nodeContextMenu: "nodeContextMenu", nodeDragStart: "nodeDragStart", nodeDragEnd: "nodeDragEnd", portPointerDown: "portPointerDown", linkDragStart: "linkDragStart", linkDragMove: "linkDragMove", linkDragEnd: "linkDragEnd", connectorClick: "connectorClick", connectorDoubleClick: "connectorDoubleClick", connectionCreated: "connectionCreated", connectionRejected: "connectionRejected", activationChange: "activationChange" }, host: { listeners: { "window:keydown": "onWindowKeydown($event)" }, classAttribute: "axp-workflow-designer-host" }, viewQueries: [{ propertyName: "surfaceRef", first: true, predicate: ["surface"], descendants: true, isSignal: true }, { propertyName: "panViewRef", first: true, predicate: AXPanViewDirective, descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"axp-workflow-designer\" [ngStyle]=\"gridStyle()\">\n <div\n #surface\n class=\"axp-workflow-designer__surface\"\n axPanView\n [(panX)]=\"panX\"\n [(panY)]=\"panY\"\n [(zoom)]=\"panZoomLevel\"\n [maxZoom]=\"10000\"\n [freeMode]=\"true\"\n [fitContent]=\"true\"\n wrapperClasses=\"h\"\n [ngStyle]=\"surfaceStyle()\"\n (pointerdown)=\"onSurfacePointerDown($event)\"\n (click)=\"onSurfaceClick($event)\"\n (dblclick)=\"onSurfaceDoubleClick($event)\"\n (dragover)=\"onCanvasDragOver($event)\"\n (drop)=\"onCanvasDrop($event)\"\n >\n <svg\n class=\"axp-workflow-designer__svg\"\n width=\"100%\"\n height=\"100%\"\n preserveAspectRatio=\"xMinYMin meet\"\n >\n <defs>\n <marker\n [attr.id]=\"svgMarkerId\"\n markerWidth=\"6\"\n markerHeight=\"6\"\n refX=\"3.9\"\n refY=\"3\"\n orient=\"auto\"\n >\n <path\n d=\"M0,0 L4,3 L0,6 Q1.5,3 0,0 Z\"\n class=\"axp-workflow-designer__arrow-head\"\n ></path>\n </marker>\n </defs>\n @for (c of model().connectors; track c.id) {\n <path\n class=\"axp-workflow-designer__edge-hit\"\n [attr.d]=\"connectorPathD(c)\"\n fill=\"none\"\n stroke=\"transparent\"\n stroke-width=\"16\"\n pointer-events=\"stroke\"\n (click)=\"onConnectorClick(c, $event)\"\n (dblclick)=\"onConnectorDoubleClick(c, $event)\"\n />\n <path\n [class]=\"connectorEdgeClasses(c)\"\n [attr.d]=\"connectorPathD(c)\"\n fill=\"none\"\n [attr.marker-end]=\"'url(#' + svgMarkerId + ')'\"\n />\n }\n @if (linkDrag()) {\n <path\n class=\"axp-workflow-designer__edge-preview\"\n [attr.d]=\"previewPath()\"\n fill=\"none\"\n />\n }\n @if (showPortLabels()) {\n @for (node of model().nodes; track node.id) {\n @for (port of node.inboundPorts; track port.id) {\n @if (portLabelText(port)) {\n <text\n class=\"axp-workflow-designer__port-label-svg\"\n [attr.x]=\"portLabelX(node, port)\"\n [attr.y]=\"portLabelY(node, port)\"\n [attr.text-anchor]=\"portLabelAnchor(node, port)\"\n >\n {{ portLabelText(port) }}\n </text>\n }\n }\n @for (port of node.outboundPorts; track port.id) {\n @if (portLabelText(port)) {\n <text\n class=\"axp-workflow-designer__port-label-svg\"\n [attr.x]=\"portLabelX(node, port)\"\n [attr.y]=\"portLabelY(node, port)\"\n [attr.text-anchor]=\"portLabelAnchor(node, port)\"\n >\n {{ portLabelText(port) }}\n </text>\n }\n }\n }\n }\n </svg>\n\n @for (c of model().connectors; track c.id) {\n @if (isConnectorActive(c.id) && !isConnectorReadonly(c)) {\n <div\n class=\"axp-workflow-designer__connector-actions\"\n [style.left.px]=\"connectorMidpoint(c).x - 16\"\n [style.top.px]=\"connectorMidpoint(c).y - 16\"\n >\n <ax-button\n look=\"blank\"\n class=\"ax-sm axp-workflow-designer__icon-btn\"\n (onClick)=\"removeConnector(c)\"\n >\n <ax-icon class=\"fa-solid fa-minus\"></ax-icon>\n </ax-button>\n </div>\n }\n }\n\n @for (node of model().nodes; track node.id) {\n <div\n class=\"axp-workflow-designer__node\"\n [class.axp-workflow-designer__node--active]=\"isNodeActive(node.id)\"\n [class.axp-workflow-designer__node--highlighted]=\"\n isNodeHighlighted(node.id)\n \"\n [style.left.px]=\"nodeSurfaceLeft(node)\"\n [style.top.px]=\"nodeSurfaceTop(node)\"\n [style.width.px]=\"node.size.width\"\n [style.height.px]=\"node.size.height\"\n (pointerdown)=\"onNodePointerDown(node, $event)\"\n (click)=\"onNodeClick(node, $event)\"\n (dblclick)=\"onNodeDoubleClick(node, $event)\"\n (contextmenu)=\"onNodeContextMenu(node, $event)\"\n >\n <div [class]=\"nodeShapeClasses(node)\">\n <div\n class=\"axp-workflow-designer__node-body\"\n (pointerdown)=\"onNodeBodyPointerDown(node, $event)\"\n >\n @if (nodeBodyTemplate(); as tpl) {\n <ng-container\n [ngTemplateOutlet]=\"tpl\"\n [ngTemplateOutletContext]=\"templateContext(node)\"\n />\n } @else {\n <div class=\"axp-workflow-designer__node-default\">\n @if (node.icon) {\n <ax-icon\n class=\"axp-workflow-designer__node-default-icon\"\n [class]=\"node.icon\"\n ></ax-icon>\n }\n <span class=\"axp-workflow-designer__node-default-title\">{{\n node.title\n }}</span>\n </div>\n }\n </div>\n </div>\n\n @for (port of node.inboundPorts; track port.id) {\n <div\n class=\"axp-workflow-designer__port-cluster\"\n [class.axp-workflow-designer__port-cluster--side-top]=\"\n port.side === 'top'\n \"\n [class.axp-workflow-designer__port-cluster--side-right]=\"\n port.side === 'right'\n \"\n [class.axp-workflow-designer__port-cluster--side-bottom]=\"\n port.side === 'bottom'\n \"\n [class.axp-workflow-designer__port-cluster--side-left]=\"\n port.side === 'left'\n \"\n [ngStyle]=\"portClusterStyle(node, port)\"\n >\n <button\n type=\"button\"\n class=\"axp-workflow-designer__port axp-workflow-designer__port--inbound\"\n [class.axp-workflow-designer__port--active]=\"\n isPortActive(node.id, port.id)\n \"\n [class.axp-workflow-designer__port--link-hover]=\"\n isPortLinkHoverTarget(node.id, port.id)\n \"\n [class.axp-workflow-designer__port--disabled]=\"port.disabled\"\n [disabled]=\"isNodeReadonly(node) || port.disabled === true\"\n (pointerdown)=\"onPortPointerDown(node, port, $event)\"\n ></button>\n </div>\n }\n @for (port of node.outboundPorts; track port.id) {\n <div\n class=\"axp-workflow-designer__port-cluster\"\n [class.axp-workflow-designer__port-cluster--side-top]=\"\n port.side === 'top'\n \"\n [class.axp-workflow-designer__port-cluster--side-right]=\"\n port.side === 'right'\n \"\n [class.axp-workflow-designer__port-cluster--side-bottom]=\"\n port.side === 'bottom'\n \"\n [class.axp-workflow-designer__port-cluster--side-left]=\"\n port.side === 'left'\n \"\n [ngStyle]=\"portClusterStyle(node, port)\"\n >\n <button\n type=\"button\"\n class=\"axp-workflow-designer__port axp-workflow-designer__port--outbound\"\n [class.axp-workflow-designer__port--active]=\"\n isPortActive(node.id, port.id)\n \"\n [class.axp-workflow-designer__port--disabled]=\"port.disabled\"\n [disabled]=\"isNodeReadonly(node) || port.disabled === true\"\n (pointerdown)=\"onPortPointerDown(node, port, $event)\"\n ></button>\n </div>\n }\n\n @if (isNodeActive(node.id) && !isNodeReadonly(node)) {\n <span\n class=\"axp-workflow-designer__resize axp-workflow-designer__resize--nw\"\n (pointerdown)=\"onResizePointerDown(node, 'nw', $event)\"\n ></span>\n <span\n class=\"axp-workflow-designer__resize axp-workflow-designer__resize--ne\"\n (pointerdown)=\"onResizePointerDown(node, 'ne', $event)\"\n ></span>\n <span\n class=\"axp-workflow-designer__resize axp-workflow-designer__resize--sw\"\n (pointerdown)=\"onResizePointerDown(node, 'sw', $event)\"\n ></span>\n <span\n class=\"axp-workflow-designer__resize axp-workflow-designer__resize--se\"\n (pointerdown)=\"onResizePointerDown(node, 'se', $event)\"\n ></span>\n }\n </div>\n }\n </div>\n</div>\n", styles: ["@layer properties;.axp-workflow-designer-host{box-sizing:border-box;display:block;height:100%;width:100%;min-height:360px;user-select:none;-webkit-user-select:none}.axp-workflow-designer{display:flex;height:100%;width:100%;min-height:inherit}.axp-workflow-designer .axp-workflow-designer__surface{position:relative;min-height:100%;min-width:100%;flex:auto;overflow:auto;border-radius:10px;user-select:none;-webkit-user-select:none}.axp-workflow-designer .axp-workflow-designer__surface::ng-deep .pan-view__wrapper{height:100%;width:100%}.axp-workflow-designer .axp-workflow-designer__svg{pointer-events:none;position:absolute;inset:0;height:100%;width:100%;z-index:1}.axp-workflow-designer .axp-workflow-designer__edge-hit{pointer-events:stroke;cursor:pointer}.axp-workflow-designer .axp-workflow-designer__edge{pointer-events:none;stroke:rgb(var(--ax-sys-color-neutral-500));stroke-width:2;transition:stroke .12s ease}.axp-workflow-designer .axp-workflow-designer__edge.primary-surface:not(.axp-workflow-designer__edge--active){stroke:rgb(var(--ax-sys-color-primary-500))}.axp-workflow-designer .axp-workflow-designer__edge.secondary-surface:not(.axp-workflow-designer__edge--active){stroke:rgb(var(--ax-sys-color-secondary-500))}.axp-workflow-designer .axp-workflow-designer__edge.success-surface:not(.axp-workflow-designer__edge--active){stroke:rgb(var(--ax-sys-color-success-500))}.axp-workflow-designer .axp-workflow-designer__edge.warning-surface:not(.axp-workflow-designer__edge--active){stroke:rgb(var(--ax-sys-color-warning-500))}.axp-workflow-designer .axp-workflow-designer__edge.danger-surface:not(.axp-workflow-designer__edge--active){stroke:rgb(var(--ax-sys-color-danger-500))}.axp-workflow-designer .axp-workflow-designer__edge--active{stroke:rgb(var(--ax-sys-color-primary-500));stroke-dasharray:2}.axp-workflow-designer .axp-workflow-designer__edge-preview{pointer-events:none;stroke:rgb(var(--ax-sys-color-primary-400));stroke-width:2;stroke-dasharray:6 4}.axp-workflow-designer .axp-workflow-designer__port-label-svg{fill:rgb(var(--ax-sys-color-on-surface));font-size:10px;font-weight:600;dominant-baseline:middle;pointer-events:none;paint-order:stroke;stroke:rgb(var(--ax-sys-color-surface));stroke-width:4px;stroke-linejoin:round}.axp-workflow-designer .axp-workflow-designer__arrow-head{fill:rgb(var(--ax-sys-color-neutral-500))}.axp-workflow-designer .axp-workflow-designer__connector-actions{position:absolute;display:flex;align-items:center;justify-content:center;z-index:4}.axp-workflow-designer .axp-workflow-designer__icon-btn{border-radius:999px;user-select:none;-webkit-user-select:none}.axp-workflow-designer .axp-workflow-designer__node{position:absolute;box-sizing:border-box;z-index:2;user-select:none;-webkit-user-select:none}.axp-workflow-designer .axp-workflow-designer__node.axp-workflow-designer__node--active .axp-workflow-designer__node-shape{border:2px dotted rgb(var(--ax-sys-color-primary-500))}.axp-workflow-designer .axp-workflow-designer__node.axp-workflow-designer__node--highlighted .axp-workflow-designer__node-shape{border:2px solid rgb(var(--ax-sys-color-primary-500));box-shadow:0 0 0 3px rgb(var(--ax-sys-color-primary-surface))}.axp-workflow-designer .axp-workflow-designer__node-shape{position:absolute;inset:0;box-sizing:border-box;overflow:visible;background:rgb(var(--ax-sys-color-surface))}.axp-workflow-designer .axp-workflow-designer__node-shape.axp-primary-surface{background:rgb(var(--ax-sys-color-primary-surface));color:rgb(var(--ax-sys-color-on-primary-surface))}.axp-workflow-designer .axp-workflow-designer__node-shape.axp-secondary-surface{background:rgb(var(--ax-sys-color-secondary-surface));color:rgb(var(--ax-sys-color-on-secondary-surface))}.axp-workflow-designer .axp-workflow-designer__node-shape.axp-success-surface{background:rgb(var(--ax-sys-color-success-surface));color:rgb(var(--ax-sys-color-on-success-surface))}.axp-workflow-designer .axp-workflow-designer__node-shape.axp-warning-surface{background:rgb(var(--ax-sys-color-warning-surface));color:rgb(var(--ax-sys-color-on-warning-surface))}.axp-workflow-designer .axp-workflow-designer__node-shape.axp-danger-surface{background:rgb(var(--ax-sys-color-danger-surface));color:rgb(var(--ax-sys-color-on-danger-surface))}.axp-workflow-designer .axp-workflow-designer__node-shape.axp-primary-border{border:2px solid rgb(var(--ax-sys-color-border-primary-surface))}.axp-workflow-designer .axp-workflow-designer__node-shape.axp-secondary-border{border:2px solid rgb(var(--ax-sys-color-border-secondary-surface))}.axp-workflow-designer .axp-workflow-designer__node-shape.axp-success-border{border:2px solid rgb(var(--ax-sys-color-border-success-surface))}.axp-workflow-designer .axp-workflow-designer__node-shape.axp-warning-border{border:2px solid rgb(var(--ax-sys-color-border-warning-surface))}.axp-workflow-designer .axp-workflow-designer__node-shape.axp-danger-border{border:2px solid rgb(var(--ax-sys-color-border-danger-surface))}.axp-workflow-designer .axp-workflow-designer__node-shape--rectangle{border-radius:4px}.axp-workflow-designer .axp-workflow-designer__node-shape--rounded-rectangle{border-radius:14px}.axp-workflow-designer .axp-workflow-designer__node-shape--circle,.axp-workflow-designer .axp-workflow-designer__node-shape--ellipse{border-radius:50%}.axp-workflow-designer .axp-workflow-designer__node-shape--diamond{border-radius:0;clip-path:polygon(50% 0%,100% 50%,50% 100%,0% 50%)}.axp-workflow-designer .axp-workflow-designer__node-body{position:absolute;inset:0;display:flex;cursor:grab;align-items:center;justify-content:center;padding:8px 12px;touch-action:none;user-select:none;-webkit-user-select:none}.axp-workflow-designer .axp-workflow-designer__node-default{display:flex;min-width:0px;align-items:center;gap:8px}.axp-workflow-designer .axp-workflow-designer__node-default-icon{flex-shrink:0}.axp-workflow-designer .axp-workflow-designer__node-default-title{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:var(--text-sm, .875rem);line-height:var(--tw-leading, var(--text-sm--line-height, calc(1.25 / .875)));--tw-font-weight: var(--font-weight-semibold, 600);font-weight:var(--font-weight-semibold, 600)}.axp-workflow-designer .axp-workflow-designer__port-cluster{pointer-events:none;position:absolute;overflow:visible;width:0;height:0;transform:translate(-50%,-50%);z-index:3}.axp-workflow-designer .axp-workflow-designer__port{position:absolute;flex-shrink:0;cursor:crosshair;border-radius:calc(infinity * 1px);left:50%;top:50%;transform:translate(-50%,-50%);width:14px;height:14px;padding:0;border:1px solid rgb(var(--ax-sys-color-surface));touch-action:none;pointer-events:auto;transition:transform .12s ease}.axp-workflow-designer .axp-workflow-designer__port:disabled{opacity:.45;cursor:not-allowed}.axp-workflow-designer .axp-workflow-designer__port--inbound{background:rgb(var(--ax-sys-color-success-500))}.axp-workflow-designer .axp-workflow-designer__port--outbound{background:rgb(var(--ax-sys-color-primary-500))}.axp-workflow-designer .axp-workflow-designer__port--active{box-shadow:0 0 0 3px #3b82f659}.axp-workflow-designer .axp-workflow-designer__port--link-hover{border:1px solid rgb(var(--ax-sys-color-primary-500));transform:translate(-50%,-50%) scale(1.12)}.axp-workflow-designer .axp-workflow-designer__port--disabled{opacity:.45;cursor:not-allowed}.axp-workflow-designer .axp-workflow-designer__resize{position:absolute;border-radius:calc(infinity * 1px);width:7px;height:7px;background:rgb(var(--ax-sys-color-primary-500));border:1px solid rgb(var(--ax-sys-color-surface));z-index:4;touch-action:none;user-select:none;-webkit-user-select:none}.axp-workflow-designer .axp-workflow-designer__resize--nw{left:-6px;top:-6px;cursor:nwse-resize}.axp-workflow-designer .axp-workflow-designer__resize--ne{right:-6px;top:-6px;cursor:nesw-resize}.axp-workflow-designer .axp-workflow-designer__resize--sw{left:-6px;bottom:-6px;cursor:nesw-resize}.axp-workflow-designer .axp-workflow-designer__resize--se{right:-6px;bottom:-6px;cursor:nwse-resize}@property --tw-font-weight{syntax: \"*\"; inherits: false;}@layer properties{@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-font-weight: initial}}}\n/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: i1.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "ngmodule", type: AXDecoratorModule }, { kind: "component", type: i2.AXDecoratorIconComponent, selector: "ax-icon", inputs: ["icon"] }, { kind: "ngmodule", type: AXButtonModule }, { kind: "component", type: i1$1.AXButtonComponent, selector: "ax-button", inputs: ["disabled", "size", "tabIndex", "color", "look", "text", "toggleable", "selected", "iconOnly", "type", "loadingText"], outputs: ["onBlur", "onFocus", "onClick", "selectedChange", "toggleableChange", "lookChange", "colorChange", "disabledChange", "loadingTextChange"] }, { kind: "directive", type: AXPanViewDirective, selector: "[axPanView]", inputs: ["zoomStep", "minZoom", "maxZoom", "freeMode", "fitContent", "disablePan", "disableZoom", "wrapperClasses", "panX", "panY", "zoom"], outputs: ["panXChange", "panYChange", "zoomChange", "positionChange"], exportAs: ["axPanView"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None }); }
|
|
4530
|
+
}
|
|
4531
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXPWorkflowDesignerComponent, decorators: [{
|
|
4532
|
+
type: Component,
|
|
4533
|
+
args: [{ selector: 'axp-workflow-designer', imports: [CommonModule, AXDecoratorModule, AXButtonModule, AXPanViewDirective], changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, host: {
|
|
4534
|
+
class: 'axp-workflow-designer-host',
|
|
4535
|
+
'(window:keydown)': 'onWindowKeydown($event)',
|
|
4536
|
+
}, template: "<div class=\"axp-workflow-designer\" [ngStyle]=\"gridStyle()\">\n <div\n #surface\n class=\"axp-workflow-designer__surface\"\n axPanView\n [(panX)]=\"panX\"\n [(panY)]=\"panY\"\n [(zoom)]=\"panZoomLevel\"\n [maxZoom]=\"10000\"\n [freeMode]=\"true\"\n [fitContent]=\"true\"\n wrapperClasses=\"h\"\n [ngStyle]=\"surfaceStyle()\"\n (pointerdown)=\"onSurfacePointerDown($event)\"\n (click)=\"onSurfaceClick($event)\"\n (dblclick)=\"onSurfaceDoubleClick($event)\"\n (dragover)=\"onCanvasDragOver($event)\"\n (drop)=\"onCanvasDrop($event)\"\n >\n <svg\n class=\"axp-workflow-designer__svg\"\n width=\"100%\"\n height=\"100%\"\n preserveAspectRatio=\"xMinYMin meet\"\n >\n <defs>\n <marker\n [attr.id]=\"svgMarkerId\"\n markerWidth=\"6\"\n markerHeight=\"6\"\n refX=\"3.9\"\n refY=\"3\"\n orient=\"auto\"\n >\n <path\n d=\"M0,0 L4,3 L0,6 Q1.5,3 0,0 Z\"\n class=\"axp-workflow-designer__arrow-head\"\n ></path>\n </marker>\n </defs>\n @for (c of model().connectors; track c.id) {\n <path\n class=\"axp-workflow-designer__edge-hit\"\n [attr.d]=\"connectorPathD(c)\"\n fill=\"none\"\n stroke=\"transparent\"\n stroke-width=\"16\"\n pointer-events=\"stroke\"\n (click)=\"onConnectorClick(c, $event)\"\n (dblclick)=\"onConnectorDoubleClick(c, $event)\"\n />\n <path\n [class]=\"connectorEdgeClasses(c)\"\n [attr.d]=\"connectorPathD(c)\"\n fill=\"none\"\n [attr.marker-end]=\"'url(#' + svgMarkerId + ')'\"\n />\n }\n @if (linkDrag()) {\n <path\n class=\"axp-workflow-designer__edge-preview\"\n [attr.d]=\"previewPath()\"\n fill=\"none\"\n />\n }\n @if (showPortLabels()) {\n @for (node of model().nodes; track node.id) {\n @for (port of node.inboundPorts; track port.id) {\n @if (portLabelText(port)) {\n <text\n class=\"axp-workflow-designer__port-label-svg\"\n [attr.x]=\"portLabelX(node, port)\"\n [attr.y]=\"portLabelY(node, port)\"\n [attr.text-anchor]=\"portLabelAnchor(node, port)\"\n >\n {{ portLabelText(port) }}\n </text>\n }\n }\n @for (port of node.outboundPorts; track port.id) {\n @if (portLabelText(port)) {\n <text\n class=\"axp-workflow-designer__port-label-svg\"\n [attr.x]=\"portLabelX(node, port)\"\n [attr.y]=\"portLabelY(node, port)\"\n [attr.text-anchor]=\"portLabelAnchor(node, port)\"\n >\n {{ portLabelText(port) }}\n </text>\n }\n }\n }\n }\n </svg>\n\n @for (c of model().connectors; track c.id) {\n @if (isConnectorActive(c.id) && !isConnectorReadonly(c)) {\n <div\n class=\"axp-workflow-designer__connector-actions\"\n [style.left.px]=\"connectorMidpoint(c).x - 16\"\n [style.top.px]=\"connectorMidpoint(c).y - 16\"\n >\n <ax-button\n look=\"blank\"\n class=\"ax-sm axp-workflow-designer__icon-btn\"\n (onClick)=\"removeConnector(c)\"\n >\n <ax-icon class=\"fa-solid fa-minus\"></ax-icon>\n </ax-button>\n </div>\n }\n }\n\n @for (node of model().nodes; track node.id) {\n <div\n class=\"axp-workflow-designer__node\"\n [class.axp-workflow-designer__node--active]=\"isNodeActive(node.id)\"\n [class.axp-workflow-designer__node--highlighted]=\"\n isNodeHighlighted(node.id)\n \"\n [style.left.px]=\"nodeSurfaceLeft(node)\"\n [style.top.px]=\"nodeSurfaceTop(node)\"\n [style.width.px]=\"node.size.width\"\n [style.height.px]=\"node.size.height\"\n (pointerdown)=\"onNodePointerDown(node, $event)\"\n (click)=\"onNodeClick(node, $event)\"\n (dblclick)=\"onNodeDoubleClick(node, $event)\"\n (contextmenu)=\"onNodeContextMenu(node, $event)\"\n >\n <div [class]=\"nodeShapeClasses(node)\">\n <div\n class=\"axp-workflow-designer__node-body\"\n (pointerdown)=\"onNodeBodyPointerDown(node, $event)\"\n >\n @if (nodeBodyTemplate(); as tpl) {\n <ng-container\n [ngTemplateOutlet]=\"tpl\"\n [ngTemplateOutletContext]=\"templateContext(node)\"\n />\n } @else {\n <div class=\"axp-workflow-designer__node-default\">\n @if (node.icon) {\n <ax-icon\n class=\"axp-workflow-designer__node-default-icon\"\n [class]=\"node.icon\"\n ></ax-icon>\n }\n <span class=\"axp-workflow-designer__node-default-title\">{{\n node.title\n }}</span>\n </div>\n }\n </div>\n </div>\n\n @for (port of node.inboundPorts; track port.id) {\n <div\n class=\"axp-workflow-designer__port-cluster\"\n [class.axp-workflow-designer__port-cluster--side-top]=\"\n port.side === 'top'\n \"\n [class.axp-workflow-designer__port-cluster--side-right]=\"\n port.side === 'right'\n \"\n [class.axp-workflow-designer__port-cluster--side-bottom]=\"\n port.side === 'bottom'\n \"\n [class.axp-workflow-designer__port-cluster--side-left]=\"\n port.side === 'left'\n \"\n [ngStyle]=\"portClusterStyle(node, port)\"\n >\n <button\n type=\"button\"\n class=\"axp-workflow-designer__port axp-workflow-designer__port--inbound\"\n [class.axp-workflow-designer__port--active]=\"\n isPortActive(node.id, port.id)\n \"\n [class.axp-workflow-designer__port--link-hover]=\"\n isPortLinkHoverTarget(node.id, port.id)\n \"\n [class.axp-workflow-designer__port--disabled]=\"port.disabled\"\n [disabled]=\"isNodeReadonly(node) || port.disabled === true\"\n (pointerdown)=\"onPortPointerDown(node, port, $event)\"\n ></button>\n </div>\n }\n @for (port of node.outboundPorts; track port.id) {\n <div\n class=\"axp-workflow-designer__port-cluster\"\n [class.axp-workflow-designer__port-cluster--side-top]=\"\n port.side === 'top'\n \"\n [class.axp-workflow-designer__port-cluster--side-right]=\"\n port.side === 'right'\n \"\n [class.axp-workflow-designer__port-cluster--side-bottom]=\"\n port.side === 'bottom'\n \"\n [class.axp-workflow-designer__port-cluster--side-left]=\"\n port.side === 'left'\n \"\n [ngStyle]=\"portClusterStyle(node, port)\"\n >\n <button\n type=\"button\"\n class=\"axp-workflow-designer__port axp-workflow-designer__port--outbound\"\n [class.axp-workflow-designer__port--active]=\"\n isPortActive(node.id, port.id)\n \"\n [class.axp-workflow-designer__port--disabled]=\"port.disabled\"\n [disabled]=\"isNodeReadonly(node) || port.disabled === true\"\n (pointerdown)=\"onPortPointerDown(node, port, $event)\"\n ></button>\n </div>\n }\n\n @if (isNodeActive(node.id) && !isNodeReadonly(node)) {\n <span\n class=\"axp-workflow-designer__resize axp-workflow-designer__resize--nw\"\n (pointerdown)=\"onResizePointerDown(node, 'nw', $event)\"\n ></span>\n <span\n class=\"axp-workflow-designer__resize axp-workflow-designer__resize--ne\"\n (pointerdown)=\"onResizePointerDown(node, 'ne', $event)\"\n ></span>\n <span\n class=\"axp-workflow-designer__resize axp-workflow-designer__resize--sw\"\n (pointerdown)=\"onResizePointerDown(node, 'sw', $event)\"\n ></span>\n <span\n class=\"axp-workflow-designer__resize axp-workflow-designer__resize--se\"\n (pointerdown)=\"onResizePointerDown(node, 'se', $event)\"\n ></span>\n }\n </div>\n }\n </div>\n</div>\n", styles: ["@layer properties;.axp-workflow-designer-host{box-sizing:border-box;display:block;height:100%;width:100%;min-height:360px;user-select:none;-webkit-user-select:none}.axp-workflow-designer{display:flex;height:100%;width:100%;min-height:inherit}.axp-workflow-designer .axp-workflow-designer__surface{position:relative;min-height:100%;min-width:100%;flex:auto;overflow:auto;border-radius:10px;user-select:none;-webkit-user-select:none}.axp-workflow-designer .axp-workflow-designer__surface::ng-deep .pan-view__wrapper{height:100%;width:100%}.axp-workflow-designer .axp-workflow-designer__svg{pointer-events:none;position:absolute;inset:0;height:100%;width:100%;z-index:1}.axp-workflow-designer .axp-workflow-designer__edge-hit{pointer-events:stroke;cursor:pointer}.axp-workflow-designer .axp-workflow-designer__edge{pointer-events:none;stroke:rgb(var(--ax-sys-color-neutral-500));stroke-width:2;transition:stroke .12s ease}.axp-workflow-designer .axp-workflow-designer__edge.primary-surface:not(.axp-workflow-designer__edge--active){stroke:rgb(var(--ax-sys-color-primary-500))}.axp-workflow-designer .axp-workflow-designer__edge.secondary-surface:not(.axp-workflow-designer__edge--active){stroke:rgb(var(--ax-sys-color-secondary-500))}.axp-workflow-designer .axp-workflow-designer__edge.success-surface:not(.axp-workflow-designer__edge--active){stroke:rgb(var(--ax-sys-color-success-500))}.axp-workflow-designer .axp-workflow-designer__edge.warning-surface:not(.axp-workflow-designer__edge--active){stroke:rgb(var(--ax-sys-color-warning-500))}.axp-workflow-designer .axp-workflow-designer__edge.danger-surface:not(.axp-workflow-designer__edge--active){stroke:rgb(var(--ax-sys-color-danger-500))}.axp-workflow-designer .axp-workflow-designer__edge--active{stroke:rgb(var(--ax-sys-color-primary-500));stroke-dasharray:2}.axp-workflow-designer .axp-workflow-designer__edge-preview{pointer-events:none;stroke:rgb(var(--ax-sys-color-primary-400));stroke-width:2;stroke-dasharray:6 4}.axp-workflow-designer .axp-workflow-designer__port-label-svg{fill:rgb(var(--ax-sys-color-on-surface));font-size:10px;font-weight:600;dominant-baseline:middle;pointer-events:none;paint-order:stroke;stroke:rgb(var(--ax-sys-color-surface));stroke-width:4px;stroke-linejoin:round}.axp-workflow-designer .axp-workflow-designer__arrow-head{fill:rgb(var(--ax-sys-color-neutral-500))}.axp-workflow-designer .axp-workflow-designer__connector-actions{position:absolute;display:flex;align-items:center;justify-content:center;z-index:4}.axp-workflow-designer .axp-workflow-designer__icon-btn{border-radius:999px;user-select:none;-webkit-user-select:none}.axp-workflow-designer .axp-workflow-designer__node{position:absolute;box-sizing:border-box;z-index:2;user-select:none;-webkit-user-select:none}.axp-workflow-designer .axp-workflow-designer__node.axp-workflow-designer__node--active .axp-workflow-designer__node-shape{border:2px dotted rgb(var(--ax-sys-color-primary-500))}.axp-workflow-designer .axp-workflow-designer__node.axp-workflow-designer__node--highlighted .axp-workflow-designer__node-shape{border:2px solid rgb(var(--ax-sys-color-primary-500));box-shadow:0 0 0 3px rgb(var(--ax-sys-color-primary-surface))}.axp-workflow-designer .axp-workflow-designer__node-shape{position:absolute;inset:0;box-sizing:border-box;overflow:visible;background:rgb(var(--ax-sys-color-surface))}.axp-workflow-designer .axp-workflow-designer__node-shape.axp-primary-surface{background:rgb(var(--ax-sys-color-primary-surface));color:rgb(var(--ax-sys-color-on-primary-surface))}.axp-workflow-designer .axp-workflow-designer__node-shape.axp-secondary-surface{background:rgb(var(--ax-sys-color-secondary-surface));color:rgb(var(--ax-sys-color-on-secondary-surface))}.axp-workflow-designer .axp-workflow-designer__node-shape.axp-success-surface{background:rgb(var(--ax-sys-color-success-surface));color:rgb(var(--ax-sys-color-on-success-surface))}.axp-workflow-designer .axp-workflow-designer__node-shape.axp-warning-surface{background:rgb(var(--ax-sys-color-warning-surface));color:rgb(var(--ax-sys-color-on-warning-surface))}.axp-workflow-designer .axp-workflow-designer__node-shape.axp-danger-surface{background:rgb(var(--ax-sys-color-danger-surface));color:rgb(var(--ax-sys-color-on-danger-surface))}.axp-workflow-designer .axp-workflow-designer__node-shape.axp-primary-border{border:2px solid rgb(var(--ax-sys-color-border-primary-surface))}.axp-workflow-designer .axp-workflow-designer__node-shape.axp-secondary-border{border:2px solid rgb(var(--ax-sys-color-border-secondary-surface))}.axp-workflow-designer .axp-workflow-designer__node-shape.axp-success-border{border:2px solid rgb(var(--ax-sys-color-border-success-surface))}.axp-workflow-designer .axp-workflow-designer__node-shape.axp-warning-border{border:2px solid rgb(var(--ax-sys-color-border-warning-surface))}.axp-workflow-designer .axp-workflow-designer__node-shape.axp-danger-border{border:2px solid rgb(var(--ax-sys-color-border-danger-surface))}.axp-workflow-designer .axp-workflow-designer__node-shape--rectangle{border-radius:4px}.axp-workflow-designer .axp-workflow-designer__node-shape--rounded-rectangle{border-radius:14px}.axp-workflow-designer .axp-workflow-designer__node-shape--circle,.axp-workflow-designer .axp-workflow-designer__node-shape--ellipse{border-radius:50%}.axp-workflow-designer .axp-workflow-designer__node-shape--diamond{border-radius:0;clip-path:polygon(50% 0%,100% 50%,50% 100%,0% 50%)}.axp-workflow-designer .axp-workflow-designer__node-body{position:absolute;inset:0;display:flex;cursor:grab;align-items:center;justify-content:center;padding:8px 12px;touch-action:none;user-select:none;-webkit-user-select:none}.axp-workflow-designer .axp-workflow-designer__node-default{display:flex;min-width:0px;align-items:center;gap:8px}.axp-workflow-designer .axp-workflow-designer__node-default-icon{flex-shrink:0}.axp-workflow-designer .axp-workflow-designer__node-default-title{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:var(--text-sm, .875rem);line-height:var(--tw-leading, var(--text-sm--line-height, calc(1.25 / .875)));--tw-font-weight: var(--font-weight-semibold, 600);font-weight:var(--font-weight-semibold, 600)}.axp-workflow-designer .axp-workflow-designer__port-cluster{pointer-events:none;position:absolute;overflow:visible;width:0;height:0;transform:translate(-50%,-50%);z-index:3}.axp-workflow-designer .axp-workflow-designer__port{position:absolute;flex-shrink:0;cursor:crosshair;border-radius:calc(infinity * 1px);left:50%;top:50%;transform:translate(-50%,-50%);width:14px;height:14px;padding:0;border:1px solid rgb(var(--ax-sys-color-surface));touch-action:none;pointer-events:auto;transition:transform .12s ease}.axp-workflow-designer .axp-workflow-designer__port:disabled{opacity:.45;cursor:not-allowed}.axp-workflow-designer .axp-workflow-designer__port--inbound{background:rgb(var(--ax-sys-color-success-500))}.axp-workflow-designer .axp-workflow-designer__port--outbound{background:rgb(var(--ax-sys-color-primary-500))}.axp-workflow-designer .axp-workflow-designer__port--active{box-shadow:0 0 0 3px #3b82f659}.axp-workflow-designer .axp-workflow-designer__port--link-hover{border:1px solid rgb(var(--ax-sys-color-primary-500));transform:translate(-50%,-50%) scale(1.12)}.axp-workflow-designer .axp-workflow-designer__port--disabled{opacity:.45;cursor:not-allowed}.axp-workflow-designer .axp-workflow-designer__resize{position:absolute;border-radius:calc(infinity * 1px);width:7px;height:7px;background:rgb(var(--ax-sys-color-primary-500));border:1px solid rgb(var(--ax-sys-color-surface));z-index:4;touch-action:none;user-select:none;-webkit-user-select:none}.axp-workflow-designer .axp-workflow-designer__resize--nw{left:-6px;top:-6px;cursor:nwse-resize}.axp-workflow-designer .axp-workflow-designer__resize--ne{right:-6px;top:-6px;cursor:nesw-resize}.axp-workflow-designer .axp-workflow-designer__resize--sw{left:-6px;bottom:-6px;cursor:nesw-resize}.axp-workflow-designer .axp-workflow-designer__resize--se{right:-6px;bottom:-6px;cursor:nwse-resize}@property --tw-font-weight{syntax: \"*\"; inherits: false;}@layer properties{@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-font-weight: initial}}}\n/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */\n"] }]
|
|
4537
|
+
}], ctorParameters: () => [], propDecorators: { model: [{ type: i0.Input, args: [{ isSignal: true, alias: "model", required: true }] }], modelChange: [{ type: i0.Output, args: ["modelChange"] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], highlightedNodeIds: [{ type: i0.Input, args: [{ isSignal: true, alias: "highlightedNodeIds", required: false }] }], gridSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "gridSize", required: false }] }], snapToGrid: [{ type: i0.Input, args: [{ isSignal: true, alias: "snapToGrid", required: false }] }], connectionPreviewStyle: [{ type: i0.Input, args: [{ isSignal: true, alias: "connectionPreviewStyle", required: false }] }], backgroundPattern: [{ type: i0.Input, args: [{ isSignal: true, alias: "backgroundPattern", required: false }] }], zoomChange: [{ type: i0.Output, args: ["zoomChange"] }], canConnect: [{ type: i0.Input, args: [{ isSignal: true, alias: "canConnect", required: false }] }], paletteItems: [{ type: i0.Input, args: [{ isSignal: true, alias: "paletteItems", required: false }] }], showPortLabels: [{ type: i0.Input, args: [{ isSignal: true, alias: "showPortLabels", required: false }] }], nodeBodyTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "nodeBodyTemplate", required: false }] }], designerPointerDown: [{ type: i0.Output, args: ["designerPointerDown"] }], designerClick: [{ type: i0.Output, args: ["designerClick"] }], designerDoubleClick: [{ type: i0.Output, args: ["designerDoubleClick"] }], nodePointerDown: [{ type: i0.Output, args: ["nodePointerDown"] }], nodeClick: [{ type: i0.Output, args: ["nodeClick"] }], nodeDoubleClick: [{ type: i0.Output, args: ["nodeDoubleClick"] }], nodeContextMenu: [{ type: i0.Output, args: ["nodeContextMenu"] }], nodeDragStart: [{ type: i0.Output, args: ["nodeDragStart"] }], nodeDragEnd: [{ type: i0.Output, args: ["nodeDragEnd"] }], portPointerDown: [{ type: i0.Output, args: ["portPointerDown"] }], linkDragStart: [{ type: i0.Output, args: ["linkDragStart"] }], linkDragMove: [{ type: i0.Output, args: ["linkDragMove"] }], linkDragEnd: [{ type: i0.Output, args: ["linkDragEnd"] }], connectorClick: [{ type: i0.Output, args: ["connectorClick"] }], connectorDoubleClick: [{ type: i0.Output, args: ["connectorDoubleClick"] }], connectionCreated: [{ type: i0.Output, args: ["connectionCreated"] }], connectionRejected: [{ type: i0.Output, args: ["connectionRejected"] }], activationChange: [{ type: i0.Output, args: ["activationChange"] }], surfaceRef: [{ type: i0.ViewChild, args: ['surface', { isSignal: true }] }], panViewRef: [{ type: i0.ViewChild, args: [i0.forwardRef(() => AXPanViewDirective), { isSignal: true }] }] } });
|
|
4538
|
+
|
|
4539
|
+
/**
|
|
4540
|
+
* Makes any host element a draggable palette source. Set `[axpWorkflowPaletteItem]="item"` and optional outputs.
|
|
4541
|
+
* Drop target: `axp-workflow-designer` canvas (same MIME as drag payload).
|
|
4542
|
+
*/
|
|
4543
|
+
class AXPWorkflowPaletteItemDragDirective {
|
|
4544
|
+
constructor() {
|
|
4545
|
+
/** Palette definition serialized into the drag payload. */
|
|
4546
|
+
this.axpWorkflowPaletteItem = input.required({ ...(ngDevMode ? { debugName: "axpWorkflowPaletteItem" } : /* istanbul ignore next */ {}), alias: 'axpWorkflowPaletteItem' });
|
|
4547
|
+
/** When true, drag is disabled. */
|
|
4548
|
+
this.axpWorkflowPaletteDragDisabled = input(false, { ...(ngDevMode ? { debugName: "axpWorkflowPaletteDragDisabled" } : /* istanbul ignore next */ {}), alias: 'axpWorkflowPaletteDragDisabled' });
|
|
4549
|
+
this.axpWorkflowPaletteDragStart = output({
|
|
4550
|
+
alias: 'axpWorkflowPaletteDragStart',
|
|
4551
|
+
});
|
|
4552
|
+
this.axpWorkflowPaletteDragEnd = output({
|
|
4553
|
+
alias: 'axpWorkflowPaletteDragEnd',
|
|
4554
|
+
});
|
|
4555
|
+
this.hostDraggable = computed(() => (this.axpWorkflowPaletteDragDisabled() ? null : true), /* @ts-ignore */
|
|
4556
|
+
...(ngDevMode ? [{ debugName: "hostDraggable" }] : /* istanbul ignore next */ []));
|
|
4557
|
+
this.lastItem = null;
|
|
4558
|
+
}
|
|
4559
|
+
onDragStart(event) {
|
|
4560
|
+
if (this.axpWorkflowPaletteDragDisabled()) {
|
|
4561
|
+
event.preventDefault();
|
|
4562
|
+
return;
|
|
4563
|
+
}
|
|
4564
|
+
const item = this.axpWorkflowPaletteItem();
|
|
4565
|
+
this.lastItem = item;
|
|
4566
|
+
event.dataTransfer?.setData(AXP_WORKFLOW_PALETTE_MIME, JSON.stringify(item));
|
|
4567
|
+
event.dataTransfer?.setData('text/plain', item.title);
|
|
4568
|
+
if (event.dataTransfer)
|
|
4569
|
+
event.dataTransfer.effectAllowed = 'copy';
|
|
4570
|
+
this.axpWorkflowPaletteDragStart.emit({ item, event });
|
|
4571
|
+
}
|
|
4572
|
+
onDragEnd(event) {
|
|
4573
|
+
this.axpWorkflowPaletteDragEnd.emit({ item: this.lastItem, event });
|
|
4574
|
+
this.lastItem = null;
|
|
4575
|
+
}
|
|
4576
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXPWorkflowPaletteItemDragDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
4577
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.8", type: AXPWorkflowPaletteItemDragDirective, isStandalone: true, selector: "[axpWorkflowPaletteItem]", inputs: { axpWorkflowPaletteItem: { classPropertyName: "axpWorkflowPaletteItem", publicName: "axpWorkflowPaletteItem", isSignal: true, isRequired: true, transformFunction: null }, axpWorkflowPaletteDragDisabled: { classPropertyName: "axpWorkflowPaletteDragDisabled", publicName: "axpWorkflowPaletteDragDisabled", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { axpWorkflowPaletteDragStart: "axpWorkflowPaletteDragStart", axpWorkflowPaletteDragEnd: "axpWorkflowPaletteDragEnd" }, host: { listeners: { "dragstart": "onDragStart($event)", "dragend": "onDragEnd($event)" }, properties: { "attr.draggable": "hostDraggable()" } }, ngImport: i0 }); }
|
|
4578
|
+
}
|
|
4579
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXPWorkflowPaletteItemDragDirective, decorators: [{
|
|
4580
|
+
type: Directive,
|
|
4581
|
+
args: [{
|
|
4582
|
+
selector: '[axpWorkflowPaletteItem]',
|
|
4583
|
+
host: {
|
|
4584
|
+
'[attr.draggable]': 'hostDraggable()',
|
|
4585
|
+
'(dragstart)': 'onDragStart($event)',
|
|
4586
|
+
'(dragend)': 'onDragEnd($event)',
|
|
4587
|
+
},
|
|
4588
|
+
}]
|
|
4589
|
+
}], propDecorators: { axpWorkflowPaletteItem: [{ type: i0.Input, args: [{ isSignal: true, alias: "axpWorkflowPaletteItem", required: true }] }], axpWorkflowPaletteDragDisabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "axpWorkflowPaletteDragDisabled", required: false }] }], axpWorkflowPaletteDragStart: [{ type: i0.Output, args: ["axpWorkflowPaletteDragStart"] }], axpWorkflowPaletteDragEnd: [{ type: i0.Output, args: ["axpWorkflowPaletteDragEnd"] }] } });
|
|
4590
|
+
|
|
4591
|
+
//#region ---- Point & geometry ----
|
|
4592
|
+
//#endregion
|
|
4593
|
+
|
|
4594
|
+
/**
|
|
4595
|
+
* Generated bundle index. Do not edit.
|
|
4596
|
+
*/
|
|
4597
|
+
|
|
4598
|
+
export { AXPMenuManagementService as A, snapPoint as B, snapScalar as C, REST_API_GENERATOR_WIDGET_NAME as R, REST_API_GENERATOR_IMPORT_DEBOUNCE_MS as a, AXMPlatformManagementModule as b, createEmptyRestApiOutput as c, AXPWorkflowDesignerComponent as d, AXPWorkflowPaletteItemDragDirective as e, formatRestApiSummary as f, AXP_WORKFLOW_PALETTE_MIME as g, hasParsedContent as h, arrangeWorkflowDiagram as i, cloneDiagram as j, connectorPath as k, createNodeFromPaletteItem as l, createPortsFromPalette as m, normalizeRestApiOutput as n, createWorkflowConnectorId as o, parseHttpRequest as p, createWorkflowNodeId as q, createWorkflowPortId as r, defaultCanConnectRequest as s, findConnector as t, findNode as u, findPort as v, parsePaletteItemDragPayload as w, portAnchorOnNode as x, portCanvasPosition as y, routeOrthogonalConnector as z };
|
|
4599
|
+
//# sourceMappingURL=acorex-platform-module-platform-management-client-acorex-platform-module-platform-management-client-V_ao4WVM.mjs.map
|