@acorex/platform 21.0.0-next.3 → 21.0.0-next.7
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/auth/index.d.ts +6 -6
- package/common/index.d.ts +10 -5
- package/core/index.d.ts +55 -315
- package/fesm2022/acorex-platform-auth.mjs +18 -22
- package/fesm2022/acorex-platform-auth.mjs.map +1 -1
- package/fesm2022/acorex-platform-common.mjs +62 -38
- package/fesm2022/acorex-platform-common.mjs.map +1 -1
- package/fesm2022/acorex-platform-core.mjs +150 -246
- package/fesm2022/acorex-platform-core.mjs.map +1 -1
- package/fesm2022/acorex-platform-layout-builder.mjs +11 -2
- package/fesm2022/acorex-platform-layout-builder.mjs.map +1 -1
- package/fesm2022/acorex-platform-layout-components.mjs +3 -3
- package/fesm2022/acorex-platform-layout-components.mjs.map +1 -1
- package/fesm2022/acorex-platform-layout-entity.mjs +67 -83
- package/fesm2022/acorex-platform-layout-entity.mjs.map +1 -1
- package/fesm2022/acorex-platform-layout-widget-core.mjs +110 -33
- package/fesm2022/acorex-platform-layout-widget-core.mjs.map +1 -1
- package/fesm2022/acorex-platform-layout-widgets.mjs +19 -11
- package/fesm2022/acorex-platform-layout-widgets.mjs.map +1 -1
- package/fesm2022/{acorex-platform-themes-default-entity-master-list-view.component-DyDa_hyd.mjs → acorex-platform-themes-default-entity-master-list-view.component-DfJEx_bs.mjs} +2 -2
- package/fesm2022/acorex-platform-themes-default-entity-master-list-view.component-DfJEx_bs.mjs.map +1 -0
- package/fesm2022/acorex-platform-themes-default.mjs +2 -2
- package/fesm2022/acorex-platform-workflow.mjs +1268 -1699
- package/fesm2022/acorex-platform-workflow.mjs.map +1 -1
- package/layout/builder/index.d.ts +6 -0
- package/layout/entity/index.d.ts +2 -9
- package/layout/widget-core/index.d.ts +42 -4
- package/package.json +5 -5
- package/workflow/index.d.ts +650 -1074
- package/fesm2022/acorex-platform-themes-default-entity-master-list-view.component-DyDa_hyd.mjs.map +0 -1
|
@@ -1,14 +1,9 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { Injectable, inject, InjectionToken, Optional,
|
|
2
|
+
import { Injectable, inject, InjectionToken, Optional, Inject, NgModule } from '@angular/core';
|
|
3
3
|
import { Subject, filter } from 'rxjs';
|
|
4
4
|
import { cloneDeep, get, set } from 'lodash-es';
|
|
5
|
-
import { setSmart } from '@acorex/platform/core';
|
|
6
|
-
import { AXPCommandService
|
|
7
|
-
import { AXDialogService } from '@acorex/components/dialog';
|
|
8
|
-
import { AXTranslationService } from '@acorex/core/translation';
|
|
9
|
-
import { AXPLayoutBuilderService } from '@acorex/platform/layout/builder';
|
|
10
|
-
import { AXToastService } from '@acorex/components/toast';
|
|
11
|
-
import { Router } from '@angular/router';
|
|
5
|
+
import { setSmart, AXPDataGenerator } from '@acorex/platform/core';
|
|
6
|
+
import { AXPCommandService } from '@acorex/platform/runtime';
|
|
12
7
|
|
|
13
8
|
class AXPWorkflowError extends Error {
|
|
14
9
|
constructor(message, inner = null) {
|
|
@@ -421,541 +416,652 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.12", ngImpo
|
|
|
421
416
|
* Multiple loaders can be provided (multi: true).
|
|
422
417
|
*/
|
|
423
418
|
const AXP_WORKFLOW_DEFINITION_LOADER = new InjectionToken('AXP_WORKFLOW_DEFINITION_LOADER');
|
|
419
|
+
|
|
420
|
+
// ============================================
|
|
421
|
+
// WORKFLOW INSTANCE v3.0.0 TYPES
|
|
422
|
+
// Based on Elsa Workflow Instance schema: https://elsaworkflows.io/schemas/workflow-instance/v3.0.0/schema.json
|
|
423
|
+
// Compatible with Elsa backend while using ACoreX naming conventions
|
|
424
|
+
// ============================================
|
|
425
|
+
|
|
426
|
+
const AXP_ACTIVITY_PROVIDER = new InjectionToken('AXP_ACTIVITY_PROVIDER', {
|
|
427
|
+
factory: () => [],
|
|
428
|
+
});
|
|
429
|
+
const AXP_ACTIVITY_CATEGORY_PROVIDER = new InjectionToken('AXP_ACTIVITY_CATEGORY_PROVIDER', {
|
|
430
|
+
factory: () => [],
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
//#region ---- Imports ----
|
|
434
|
+
//#endregion
|
|
424
435
|
/**
|
|
425
|
-
*
|
|
426
|
-
*
|
|
436
|
+
* Optimized Activity Definition Service
|
|
437
|
+
*
|
|
438
|
+
* Manages activity definitions (metadata) for UI and tooling.
|
|
439
|
+
* Similar to AXPReportDefinitionService - only handles metadata, not execution.
|
|
440
|
+
*
|
|
441
|
+
* Performance optimizations:
|
|
442
|
+
* 1. Uses childrenCount to determine if category has children (no query needed)
|
|
443
|
+
* 2. Uses itemsCount to determine if category has activities (no query needed)
|
|
444
|
+
* 3. Aggressive caching prevents duplicate API calls
|
|
445
|
+
* 4. Single pending request per resource prevents race conditions
|
|
446
|
+
* 5. Lazy loading - only loads data when needed
|
|
427
447
|
*/
|
|
428
|
-
class
|
|
448
|
+
class AXPActivityDefinitionService {
|
|
429
449
|
constructor() {
|
|
430
|
-
|
|
450
|
+
//#region ---- Providers & Caches ----
|
|
451
|
+
this.categoryProviders = inject(AXP_ACTIVITY_CATEGORY_PROVIDER, { optional: true }) || [];
|
|
452
|
+
this.activityProviders = inject(AXP_ACTIVITY_PROVIDER, { optional: true }) || [];
|
|
453
|
+
//#endregion
|
|
454
|
+
//#region ---- Cache Storage ----
|
|
455
|
+
/** Cache for categories by id - O(1) lookup */
|
|
456
|
+
this.categoriesById = new Map();
|
|
457
|
+
/** Cache for categories by parentId - O(1) lookup */
|
|
458
|
+
this.categoriesByParentId = new Map();
|
|
459
|
+
/** Cache for activity definitions by categoryId - O(1) lookup */
|
|
460
|
+
this.activitiesByCategory = new Map();
|
|
461
|
+
/** Cache for individual activity definitions by name - O(1) lookup */
|
|
462
|
+
this.activitiesByName = new Map();
|
|
463
|
+
/** Track which provider index owns each category (by category ID) */
|
|
464
|
+
this.categoryOwnership = new Map(); // Maps categoryId → provider index
|
|
465
|
+
/** Pending API requests to prevent duplicate calls */
|
|
466
|
+
this.pendingCategoriesRequests = new Map();
|
|
467
|
+
this.pendingActivitiesRequests = new Map();
|
|
468
|
+
this.pendingActivityRequests = new Map();
|
|
469
|
+
}
|
|
470
|
+
//#endregion
|
|
471
|
+
//#region ---- Initialization ----
|
|
472
|
+
//#endregion
|
|
473
|
+
//#region ---- Public API: Categories ----
|
|
474
|
+
/**
|
|
475
|
+
* Get categories by parentId with aggressive caching
|
|
476
|
+
*
|
|
477
|
+
* Optimization: Returns cached result immediately if available,
|
|
478
|
+
* preventing unnecessary API calls during navigation
|
|
479
|
+
*
|
|
480
|
+
* @param parentId - Parent category ID (undefined = root categories)
|
|
481
|
+
* @returns Array of categories with count metadata (childrenCount, itemsCount)
|
|
482
|
+
*/
|
|
483
|
+
async getCategories(parentId) {
|
|
484
|
+
// ✅ Fast path: Return cached result
|
|
485
|
+
if (this.categoriesByParentId.has(parentId)) {
|
|
486
|
+
return this.categoriesByParentId.get(parentId);
|
|
487
|
+
}
|
|
488
|
+
// ✅ Prevent duplicate requests: Return pending promise
|
|
489
|
+
if (this.pendingCategoriesRequests.has(parentId)) {
|
|
490
|
+
return this.pendingCategoriesRequests.get(parentId);
|
|
491
|
+
}
|
|
492
|
+
// ✅ Create single request and cache it
|
|
493
|
+
const requestPromise = this.loadCategoriesFromProviders(parentId);
|
|
494
|
+
this.pendingCategoriesRequests.set(parentId, requestPromise);
|
|
495
|
+
return requestPromise;
|
|
431
496
|
}
|
|
432
497
|
/**
|
|
433
|
-
* Get
|
|
434
|
-
*
|
|
435
|
-
*
|
|
436
|
-
*
|
|
498
|
+
* Get single category by ID with O(1) lookup
|
|
499
|
+
*
|
|
500
|
+
* Optimization: Uses Map for instant retrieval, falls back to
|
|
501
|
+
* searching cache, then providers if not found
|
|
437
502
|
*/
|
|
438
|
-
async
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
503
|
+
async getCategoryById(categoryId) {
|
|
504
|
+
// ✅ Fast path: O(1) lookup in cache
|
|
505
|
+
if (this.categoriesById.has(categoryId)) {
|
|
506
|
+
return this.categoriesById.get(categoryId);
|
|
507
|
+
}
|
|
508
|
+
// ✅ Search in cached parent-child lists
|
|
509
|
+
for (const categories of this.categoriesByParentId.values()) {
|
|
510
|
+
const found = categories.find(cat => cat.id === categoryId);
|
|
511
|
+
if (found) {
|
|
512
|
+
this.categoriesById.set(categoryId, found);
|
|
513
|
+
return found;
|
|
446
514
|
}
|
|
447
|
-
|
|
448
|
-
|
|
515
|
+
}
|
|
516
|
+
// ✅ Load root categories if not loaded
|
|
517
|
+
if (!this.categoriesByParentId.has(undefined)) {
|
|
518
|
+
await this.getCategories();
|
|
519
|
+
if (this.categoriesById.has(categoryId)) {
|
|
520
|
+
return this.categoriesById.get(categoryId);
|
|
449
521
|
}
|
|
450
522
|
}
|
|
451
|
-
|
|
523
|
+
// ✅ Breadth-first search through hierarchy
|
|
524
|
+
return this.searchCategoryInHierarchy(categoryId);
|
|
452
525
|
}
|
|
453
526
|
/**
|
|
454
|
-
* Get
|
|
455
|
-
*
|
|
527
|
+
* Get category path from root to specified category
|
|
528
|
+
*
|
|
529
|
+
* Optimization: Builds path using cached categories only
|
|
456
530
|
*/
|
|
457
|
-
async
|
|
458
|
-
const
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
names.forEach((name) => allNames.add(name));
|
|
465
|
-
}
|
|
466
|
-
catch (error) {
|
|
467
|
-
console.warn('[AXPWorkflowDefinitionResolver] Failed to get names from loader:', error);
|
|
468
|
-
}
|
|
531
|
+
async getCategoriesPathById(categoryId) {
|
|
532
|
+
const path = [];
|
|
533
|
+
let currentCategoryId = categoryId;
|
|
534
|
+
while (currentCategoryId) {
|
|
535
|
+
const category = await this.getCategoryById(currentCategoryId);
|
|
536
|
+
if (!category) {
|
|
537
|
+
throw new Error(`Category '${currentCategoryId}' not found`);
|
|
469
538
|
}
|
|
539
|
+
path.unshift(category);
|
|
540
|
+
currentCategoryId = category.parentId;
|
|
470
541
|
}
|
|
471
|
-
return
|
|
472
|
-
}
|
|
473
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.12", ngImport: i0, type: AXPWorkflowDefinitionResolver, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
474
|
-
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.12", ngImport: i0, type: AXPWorkflowDefinitionResolver, providedIn: 'root' }); }
|
|
475
|
-
}
|
|
476
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.12", ngImport: i0, type: AXPWorkflowDefinitionResolver, decorators: [{
|
|
477
|
-
type: Injectable,
|
|
478
|
-
args: [{ providedIn: 'root' }]
|
|
479
|
-
}] });
|
|
480
|
-
|
|
481
|
-
/**
|
|
482
|
-
* Registry service for workflow definitions.
|
|
483
|
-
* Caches loaded definitions and provides change notifications.
|
|
484
|
-
*/
|
|
485
|
-
class AXPWorkflowDefinitionRegistryService {
|
|
486
|
-
constructor() {
|
|
487
|
-
this.resolver = inject(AXPWorkflowDefinitionResolver);
|
|
488
|
-
this.cache = new Map();
|
|
489
|
-
this.onChanged = new Subject();
|
|
542
|
+
return path;
|
|
490
543
|
}
|
|
544
|
+
//#endregion
|
|
545
|
+
//#region ---- Public API: Activity Definitions ----
|
|
491
546
|
/**
|
|
492
|
-
*
|
|
547
|
+
* Get activity definitions for a category with smart caching
|
|
548
|
+
*
|
|
549
|
+
* Optimization: Checks itemsCount before querying
|
|
550
|
+
* - If itemsCount = 0, returns empty array (no API call)
|
|
551
|
+
* - If itemsCount > 0, loads and caches activity definitions
|
|
552
|
+
* - Returns cached result on subsequent calls
|
|
553
|
+
*
|
|
554
|
+
* @param categoryId - Category ID to get activity definitions from
|
|
555
|
+
* @returns Array of activity definitions
|
|
493
556
|
*/
|
|
494
|
-
|
|
495
|
-
|
|
557
|
+
async getActivitiesByCategoryId(categoryId) {
|
|
558
|
+
// ✅ Fast path: Return cached result
|
|
559
|
+
if (this.activitiesByCategory.has(categoryId)) {
|
|
560
|
+
return this.activitiesByCategory.get(categoryId);
|
|
561
|
+
}
|
|
562
|
+
// ✅ Smart optimization: Check itemsCount before querying
|
|
563
|
+
const category = await this.getCategoryById(categoryId);
|
|
564
|
+
if (category && category.itemsCount !== undefined && category.itemsCount === 0) {
|
|
565
|
+
// Category has no activities - cache empty array and skip API call
|
|
566
|
+
const emptyArray = [];
|
|
567
|
+
this.activitiesByCategory.set(categoryId, emptyArray);
|
|
568
|
+
return emptyArray;
|
|
569
|
+
}
|
|
570
|
+
// ✅ Prevent duplicate requests
|
|
571
|
+
if (this.pendingActivitiesRequests.has(categoryId)) {
|
|
572
|
+
return this.pendingActivitiesRequests.get(categoryId);
|
|
573
|
+
}
|
|
574
|
+
// ✅ Load from providers
|
|
575
|
+
const requestPromise = this.loadActivitiesFromProviders(categoryId);
|
|
576
|
+
this.pendingActivitiesRequests.set(categoryId, requestPromise);
|
|
577
|
+
return requestPromise;
|
|
496
578
|
}
|
|
497
579
|
/**
|
|
498
|
-
* Get
|
|
499
|
-
*
|
|
500
|
-
*
|
|
501
|
-
*
|
|
580
|
+
* Get single activity definition by name with O(1) lookup
|
|
581
|
+
*
|
|
582
|
+
* Optimization: Uses Map for instant retrieval
|
|
583
|
+
*
|
|
584
|
+
* @param name - Activity name (unique identifier and command key)
|
|
585
|
+
* @returns Activity definition or undefined if not found
|
|
502
586
|
*/
|
|
503
|
-
async
|
|
504
|
-
//
|
|
505
|
-
if (this.
|
|
506
|
-
return this.
|
|
587
|
+
async getActivityByName(name) {
|
|
588
|
+
// ✅ Fast path: O(1) lookup in cache
|
|
589
|
+
if (this.activitiesByName.has(name)) {
|
|
590
|
+
return this.activitiesByName.get(name);
|
|
507
591
|
}
|
|
508
|
-
//
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
this.cache.set(name, definition);
|
|
512
|
-
this.onChanged.next({ name, action: 'registered' });
|
|
592
|
+
// ✅ Prevent duplicate requests
|
|
593
|
+
if (this.pendingActivityRequests.has(name)) {
|
|
594
|
+
return this.pendingActivityRequests.get(name);
|
|
513
595
|
}
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
* @param definition - The workflow definition to register
|
|
519
|
-
*/
|
|
520
|
-
register(definition) {
|
|
521
|
-
this.cache.set(definition.name, definition);
|
|
522
|
-
this.onChanged.next({ name: definition.name, action: 'registered' });
|
|
596
|
+
// ✅ Load from providers
|
|
597
|
+
const requestPromise = this.loadActivityFromProviders(name);
|
|
598
|
+
this.pendingActivityRequests.set(name, requestPromise);
|
|
599
|
+
return requestPromise;
|
|
523
600
|
}
|
|
524
601
|
/**
|
|
525
|
-
*
|
|
526
|
-
*
|
|
602
|
+
* Get category ID containing a specific activity definition
|
|
603
|
+
*
|
|
604
|
+
* Optimization: Searches cache first, loads on-demand if needed
|
|
527
605
|
*/
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
606
|
+
async getCategoryIdByActivityName(activityName) {
|
|
607
|
+
// ✅ Search in cached activity definitions
|
|
608
|
+
for (const [categoryId, definitions] of this.activitiesByCategory.entries()) {
|
|
609
|
+
if (definitions.some(def => def.name === activityName)) {
|
|
610
|
+
return categoryId;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
// ✅ Try loading the activity definition to find its category
|
|
614
|
+
const definition = await this.getActivityByName(activityName);
|
|
615
|
+
if (definition && definition.category) {
|
|
616
|
+
// Try to find category by name/id
|
|
617
|
+
const categories = await this.getCategories();
|
|
618
|
+
const found = categories.find(cat => cat.id === definition.category || cat.title === definition.category);
|
|
619
|
+
if (found) {
|
|
620
|
+
return found.id;
|
|
621
|
+
}
|
|
532
622
|
}
|
|
623
|
+
return undefined;
|
|
533
624
|
}
|
|
534
625
|
/**
|
|
535
|
-
*
|
|
536
|
-
* @param name - The workflow name to remove
|
|
626
|
+
* Get category path for an activity
|
|
537
627
|
*/
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
628
|
+
async getCategoriesPathByActivityName(activityName) {
|
|
629
|
+
const categoryId = await this.getCategoryIdByActivityName(activityName);
|
|
630
|
+
if (!categoryId) {
|
|
631
|
+
throw new Error(`Activity '${activityName}' not found in any category`);
|
|
542
632
|
}
|
|
633
|
+
return this.getCategoriesPathById(categoryId);
|
|
543
634
|
}
|
|
635
|
+
//#endregion
|
|
636
|
+
//#region ---- Private: Data Loading ----
|
|
544
637
|
/**
|
|
545
|
-
*
|
|
638
|
+
* Load categories from providers and cache results
|
|
639
|
+
*
|
|
640
|
+
* Optimization: Tracks provider ownership to avoid unnecessary API calls
|
|
641
|
+
* - For root (parentId = undefined): Query ALL providers
|
|
642
|
+
* - For children: Only query the provider that owns the parent
|
|
546
643
|
*/
|
|
547
|
-
|
|
548
|
-
|
|
644
|
+
async loadCategoriesFromProviders(parentId) {
|
|
645
|
+
try {
|
|
646
|
+
const resolvedProviders = await Promise.allSettled(this.categoryProviders);
|
|
647
|
+
const categories = [];
|
|
648
|
+
// Determine which provider(s) to query
|
|
649
|
+
const providerIndicesToQuery = parentId
|
|
650
|
+
? this.getProviderIndexForCategory(parentId)
|
|
651
|
+
: null; // Root: query all providers
|
|
652
|
+
for (let i = 0; i < resolvedProviders.length; i++) {
|
|
653
|
+
const p = resolvedProviders[i];
|
|
654
|
+
// Skip if we have a specific provider index and this isn't it
|
|
655
|
+
if (providerIndicesToQuery !== null && !providerIndicesToQuery.includes(i)) {
|
|
656
|
+
continue;
|
|
657
|
+
}
|
|
658
|
+
if (p.status === 'fulfilled' && p.value && typeof p.value.getList === 'function') {
|
|
659
|
+
try {
|
|
660
|
+
const cats = await p.value.getList(parentId);
|
|
661
|
+
if (Array.isArray(cats) && cats.length > 0) {
|
|
662
|
+
categories.push(...cats);
|
|
663
|
+
// ✅ Track ownership: This provider INDEX owns these categories
|
|
664
|
+
cats.forEach(cat => this.categoryOwnership.set(cat.id, i));
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
catch {
|
|
668
|
+
// Continue on error - try other providers
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
// ✅ Cache results for fast subsequent access
|
|
673
|
+
this.categoriesByParentId.set(parentId, categories);
|
|
674
|
+
categories.forEach(cat => this.categoriesById.set(cat.id, cat));
|
|
675
|
+
return categories;
|
|
676
|
+
}
|
|
677
|
+
finally {
|
|
678
|
+
this.pendingCategoriesRequests.delete(parentId);
|
|
679
|
+
}
|
|
549
680
|
}
|
|
550
681
|
/**
|
|
551
|
-
*
|
|
552
|
-
*
|
|
553
|
-
* @returns
|
|
682
|
+
* Get the provider index that owns a specific category
|
|
683
|
+
*
|
|
684
|
+
* @returns Array with provider index, or null if ownership unknown (query all)
|
|
554
685
|
*/
|
|
555
|
-
|
|
556
|
-
|
|
686
|
+
getProviderIndexForCategory(categoryId) {
|
|
687
|
+
const ownerIndex = this.categoryOwnership.get(categoryId);
|
|
688
|
+
if (ownerIndex !== undefined) {
|
|
689
|
+
return [ownerIndex];
|
|
690
|
+
}
|
|
691
|
+
// Ownership unknown - will query all providers (fallback)
|
|
692
|
+
return null;
|
|
557
693
|
}
|
|
558
694
|
/**
|
|
559
|
-
*
|
|
560
|
-
*
|
|
695
|
+
* Load activity definitions from providers and cache results
|
|
696
|
+
*
|
|
697
|
+
* Optimization: Only queries the provider that owns the category
|
|
698
|
+
* Uses provider INDEX to match category provider with activity provider
|
|
561
699
|
*/
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
700
|
+
async loadActivitiesFromProviders(categoryId) {
|
|
701
|
+
try {
|
|
702
|
+
const resolvedProviders = await Promise.allSettled(this.activityProviders);
|
|
703
|
+
const definitions = [];
|
|
704
|
+
// ✅ Smart routing: Get provider INDEX that owns this category
|
|
705
|
+
const ownerIndex = this.categoryOwnership.get(categoryId);
|
|
706
|
+
const providerIndicesToQuery = ownerIndex !== undefined ? [ownerIndex] : null;
|
|
707
|
+
for (let i = 0; i < resolvedProviders.length; i++) {
|
|
708
|
+
const p = resolvedProviders[i];
|
|
709
|
+
// Skip if we have a specific provider index and this isn't it
|
|
710
|
+
if (providerIndicesToQuery !== null && !providerIndicesToQuery.includes(i)) {
|
|
711
|
+
continue;
|
|
712
|
+
}
|
|
713
|
+
if (p.status === 'fulfilled' && p.value && typeof p.value.getList === 'function') {
|
|
714
|
+
try {
|
|
715
|
+
const defs = await p.value.getList(categoryId);
|
|
716
|
+
if (Array.isArray(defs)) {
|
|
717
|
+
definitions.push(...defs);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
catch {
|
|
721
|
+
// Continue on error - try other providers
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
// ✅ Cache results for fast subsequent access
|
|
726
|
+
this.activitiesByCategory.set(categoryId, definitions);
|
|
727
|
+
definitions.forEach(def => {
|
|
728
|
+
if (def.name) {
|
|
729
|
+
this.activitiesByName.set(def.name, def);
|
|
730
|
+
}
|
|
731
|
+
});
|
|
732
|
+
return definitions;
|
|
733
|
+
}
|
|
734
|
+
finally {
|
|
735
|
+
this.pendingActivitiesRequests.delete(categoryId);
|
|
736
|
+
}
|
|
589
737
|
}
|
|
590
738
|
/**
|
|
591
|
-
*
|
|
739
|
+
* Load single activity definition from providers and cache result
|
|
592
740
|
*/
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
741
|
+
async loadActivityFromProviders(name) {
|
|
742
|
+
try {
|
|
743
|
+
const resolvedProviders = await Promise.allSettled(this.activityProviders);
|
|
744
|
+
// Try providers first
|
|
745
|
+
for (const p of resolvedProviders) {
|
|
746
|
+
if (p.status === 'fulfilled' && p.value && typeof p.value.getById === 'function') {
|
|
747
|
+
try {
|
|
748
|
+
const result = await p.value.getById(name);
|
|
749
|
+
if (result) {
|
|
750
|
+
this.activitiesByName.set(name, result);
|
|
751
|
+
return result;
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
catch {
|
|
755
|
+
// Continue on error
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
// Fallback: Search in cached activity definitions
|
|
760
|
+
for (const definitions of this.activitiesByCategory.values()) {
|
|
761
|
+
const found = definitions.find(def => def.name === name);
|
|
762
|
+
if (found) {
|
|
763
|
+
this.activitiesByName.set(name, found);
|
|
764
|
+
return found;
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
return undefined;
|
|
768
|
+
}
|
|
769
|
+
finally {
|
|
770
|
+
this.pendingActivityRequests.delete(name);
|
|
771
|
+
}
|
|
610
772
|
}
|
|
611
773
|
/**
|
|
612
|
-
*
|
|
774
|
+
* Breadth-first search through category hierarchy
|
|
613
775
|
*/
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
776
|
+
async searchCategoryInHierarchy(categoryId) {
|
|
777
|
+
const searchQueue = [undefined];
|
|
778
|
+
const searched = new Set();
|
|
779
|
+
while (searchQueue.length > 0) {
|
|
780
|
+
const parentId = searchQueue.shift();
|
|
781
|
+
if (searched.has(parentId))
|
|
782
|
+
continue;
|
|
783
|
+
searched.add(parentId);
|
|
784
|
+
const categories = await this.getCategories(parentId);
|
|
785
|
+
const found = categories.find(cat => cat.id === categoryId);
|
|
786
|
+
if (found) {
|
|
787
|
+
return found;
|
|
788
|
+
}
|
|
789
|
+
// ✅ Optimization: Only search children if childrenCount > 0
|
|
790
|
+
for (const category of categories) {
|
|
791
|
+
if (category.childrenCount > 0 && !searched.has(category.id)) {
|
|
792
|
+
searchQueue.push(category.id);
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
return undefined;
|
|
617
797
|
}
|
|
798
|
+
//#endregion
|
|
799
|
+
//#region ---- Cache Management ----
|
|
618
800
|
/**
|
|
619
|
-
*
|
|
801
|
+
* Check if category has children (uses cached count)
|
|
620
802
|
*/
|
|
621
|
-
|
|
622
|
-
const
|
|
623
|
-
|
|
624
|
-
throw new Error(`Unknown activity type: ${type}`);
|
|
625
|
-
}
|
|
626
|
-
return factory();
|
|
803
|
+
categoryHasChildren(categoryId) {
|
|
804
|
+
const category = this.categoriesById.get(categoryId);
|
|
805
|
+
return category ? category.childrenCount > 0 : false;
|
|
627
806
|
}
|
|
628
807
|
/**
|
|
629
|
-
*
|
|
808
|
+
* Check if category has activities (uses cached count)
|
|
630
809
|
*/
|
|
631
|
-
|
|
632
|
-
|
|
810
|
+
categoryHasActivities(categoryId) {
|
|
811
|
+
const category = this.categoriesById.get(categoryId);
|
|
812
|
+
return category ? (category.itemsCount ?? 0) > 0 : false;
|
|
633
813
|
}
|
|
634
814
|
/**
|
|
635
|
-
*
|
|
815
|
+
* Clear all caches
|
|
636
816
|
*/
|
|
637
|
-
|
|
638
|
-
|
|
817
|
+
clearAllCache() {
|
|
818
|
+
this.categoriesById.clear();
|
|
819
|
+
this.categoriesByParentId.clear();
|
|
820
|
+
this.activitiesByCategory.clear();
|
|
821
|
+
this.activitiesByName.clear();
|
|
822
|
+
this.categoryOwnership.clear();
|
|
823
|
+
this.pendingCategoriesRequests.clear();
|
|
824
|
+
this.pendingActivitiesRequests.clear();
|
|
825
|
+
this.pendingActivityRequests.clear();
|
|
639
826
|
}
|
|
640
827
|
/**
|
|
641
|
-
*
|
|
828
|
+
* Clear categories cache only
|
|
642
829
|
*/
|
|
643
|
-
|
|
644
|
-
|
|
830
|
+
clearCategoriesCache() {
|
|
831
|
+
this.categoriesById.clear();
|
|
832
|
+
this.categoriesByParentId.clear();
|
|
833
|
+
this.categoryOwnership.clear();
|
|
834
|
+
this.pendingCategoriesRequests.clear();
|
|
645
835
|
}
|
|
646
836
|
/**
|
|
647
|
-
*
|
|
837
|
+
* Clear activities cache only
|
|
648
838
|
*/
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
839
|
+
clearActivitiesCache() {
|
|
840
|
+
this.activitiesByCategory.clear();
|
|
841
|
+
this.activitiesByName.clear();
|
|
842
|
+
this.pendingActivitiesRequests.clear();
|
|
843
|
+
this.pendingActivityRequests.clear();
|
|
844
|
+
}
|
|
845
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.12", ngImport: i0, type: AXPActivityDefinitionService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
846
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.12", ngImport: i0, type: AXPActivityDefinitionService, providedIn: 'root' }); }
|
|
652
847
|
}
|
|
848
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.12", ngImport: i0, type: AXPActivityDefinitionService, decorators: [{
|
|
849
|
+
type: Injectable,
|
|
850
|
+
args: [{
|
|
851
|
+
providedIn: 'root',
|
|
852
|
+
}]
|
|
853
|
+
}] });
|
|
653
854
|
|
|
654
855
|
/**
|
|
655
|
-
* Injection token for
|
|
856
|
+
* Injection token for workflow engine.
|
|
857
|
+
* Default implementation is AXPWorkflowLocalEngine.
|
|
656
858
|
*/
|
|
657
|
-
const
|
|
859
|
+
const AXP_WORKFLOW_ENGINE = new InjectionToken('AXP_WORKFLOW_ENGINE');
|
|
860
|
+
|
|
861
|
+
//#endregion
|
|
658
862
|
/**
|
|
659
|
-
*
|
|
660
|
-
*
|
|
863
|
+
* Service responsible for executing frontend workflow tasks.
|
|
864
|
+
*
|
|
865
|
+
* This service handles the execution of frontend or hybrid tasks using
|
|
866
|
+
* the command infrastructure. It translates command results into
|
|
867
|
+
* workflow-compatible output and outcome format.
|
|
868
|
+
*
|
|
869
|
+
* This service is replaceable per platform and contains all execution logic.
|
|
661
870
|
*/
|
|
662
|
-
class
|
|
871
|
+
class FrontendTaskExecutor {
|
|
663
872
|
constructor() {
|
|
664
|
-
|
|
665
|
-
this.categories = [];
|
|
666
|
-
this.providers = [];
|
|
667
|
-
this.categoryProviders = [];
|
|
668
|
-
this.initialized = false;
|
|
873
|
+
//#region ---- Services & Dependencies ----
|
|
669
874
|
this.commandService = inject(AXPCommandService);
|
|
670
|
-
this.commandRegistry = inject(AXPCommandRegistry);
|
|
671
|
-
}
|
|
672
|
-
/**
|
|
673
|
-
* Register an activity provider.
|
|
674
|
-
*/
|
|
675
|
-
registerProvider(provider) {
|
|
676
|
-
this.providers.push(provider);
|
|
677
|
-
if (this.initialized) {
|
|
678
|
-
this.initializeProvider(provider);
|
|
679
|
-
}
|
|
680
|
-
}
|
|
681
|
-
/**
|
|
682
|
-
* Register a category provider.
|
|
683
|
-
*/
|
|
684
|
-
registerCategoryProvider(provider) {
|
|
685
|
-
this.categoryProviders.push(provider);
|
|
686
|
-
if (this.initialized) {
|
|
687
|
-
this.initializeCategoryProvider(provider);
|
|
688
|
-
}
|
|
689
875
|
}
|
|
876
|
+
//#endregion
|
|
877
|
+
//#region ---- Public Methods ----
|
|
690
878
|
/**
|
|
691
|
-
*
|
|
879
|
+
* Execute a frontend workflow task.
|
|
880
|
+
*
|
|
881
|
+
* Only executes tasks with executionMode 'frontend' or 'both'.
|
|
882
|
+
* Backend tasks should not be passed to this executor.
|
|
883
|
+
*
|
|
884
|
+
* @param task - Task to execute
|
|
885
|
+
* @returns Execution result with output and outcome
|
|
886
|
+
*
|
|
887
|
+
* @throws Error if task is not a frontend task
|
|
692
888
|
*/
|
|
693
|
-
async
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
await this.initializeCategoryProvider(provider);
|
|
699
|
-
}
|
|
700
|
-
// Initialize activity providers
|
|
701
|
-
for (const provider of this.providers) {
|
|
702
|
-
await this.initializeProvider(provider);
|
|
889
|
+
async execute(task) {
|
|
890
|
+
// Validate execution mode
|
|
891
|
+
if (task.executionMode !== 'frontend' && task.executionMode !== 'both') {
|
|
892
|
+
throw new Error(`Task '${task.activityId}' is not a frontend task. ` +
|
|
893
|
+
`Execution mode: ${task.executionMode}. Backend tasks are handled automatically.`);
|
|
703
894
|
}
|
|
704
|
-
this.initialized = true;
|
|
705
|
-
}
|
|
706
|
-
/**
|
|
707
|
-
* Get activity descriptor by key.
|
|
708
|
-
*/
|
|
709
|
-
getDescriptor(key) {
|
|
710
|
-
return this.activityDescriptors.get(key);
|
|
711
|
-
}
|
|
712
|
-
/**
|
|
713
|
-
* Get all activity descriptors.
|
|
714
|
-
*/
|
|
715
|
-
getAllDescriptors() {
|
|
716
|
-
return Array.from(this.activityDescriptors.values());
|
|
717
|
-
}
|
|
718
|
-
/**
|
|
719
|
-
* Get descriptors by category.
|
|
720
|
-
*/
|
|
721
|
-
getDescriptorsByCategory(category) {
|
|
722
|
-
return this.getAllDescriptors().filter(d => d.category === category);
|
|
723
|
-
}
|
|
724
|
-
/**
|
|
725
|
-
* Get all categories.
|
|
726
|
-
*/
|
|
727
|
-
getCategories() {
|
|
728
|
-
return [...this.categories].sort((a, b) => (a.order || 999) - (b.order || 999));
|
|
729
|
-
}
|
|
730
|
-
/**
|
|
731
|
-
* Create an activity instance by key using AXPCommandService.
|
|
732
|
-
*/
|
|
733
|
-
async createActivity(key) {
|
|
734
895
|
try {
|
|
735
896
|
// Check if command exists
|
|
736
|
-
|
|
737
|
-
|
|
897
|
+
const commandExists = this.commandService.exists(task.activityType);
|
|
898
|
+
if (!commandExists) {
|
|
899
|
+
console.warn(`[FrontendTaskExecutor] ⚠️ Frontend activity '${task.activityType}' is not registered. ` +
|
|
900
|
+
`Skipping execution.`);
|
|
901
|
+
return {
|
|
902
|
+
output: null,
|
|
903
|
+
outcome: 'Done'
|
|
904
|
+
};
|
|
905
|
+
}
|
|
906
|
+
// Prepare command input
|
|
907
|
+
// Flatten properties if nested (workflow-studio format)
|
|
908
|
+
let commandInput = task.input || task.config || {};
|
|
909
|
+
if (commandInput['properties'] && typeof commandInput['properties'] === 'object') {
|
|
910
|
+
// Flatten: {properties: {text: "..."}} -> {text: "..."}
|
|
911
|
+
commandInput = { ...commandInput['properties'] };
|
|
912
|
+
}
|
|
913
|
+
// Execute activity via CommandBus
|
|
914
|
+
// Activities registered as AXPCommand return {output, outcomes}
|
|
915
|
+
const result = await this.commandService.execute(task.activityType, commandInput);
|
|
916
|
+
if (!result) {
|
|
917
|
+
return {
|
|
918
|
+
output: null,
|
|
919
|
+
outcome: 'Failed',
|
|
920
|
+
};
|
|
921
|
+
}
|
|
922
|
+
if (!result.success) {
|
|
923
|
+
return {
|
|
924
|
+
output: {
|
|
925
|
+
error: result.message?.text,
|
|
926
|
+
},
|
|
927
|
+
outcome: 'Failed',
|
|
928
|
+
};
|
|
738
929
|
}
|
|
739
|
-
|
|
740
|
-
const
|
|
741
|
-
|
|
742
|
-
|
|
930
|
+
const commandResult = result.data;
|
|
931
|
+
const outcomes = commandResult?.outcomes ?? {};
|
|
932
|
+
// Determine outcome from command results
|
|
933
|
+
// Default to 'Done' if no outcomes specified
|
|
934
|
+
let outcome = 'Done';
|
|
935
|
+
if (Object.keys(outcomes).length > 0) {
|
|
936
|
+
outcome = outcomes['Done'] ? 'Done' : Object.keys(outcomes)[0] || 'Done';
|
|
743
937
|
}
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
938
|
+
return {
|
|
939
|
+
output: commandResult?.output ?? null,
|
|
940
|
+
outcome,
|
|
941
|
+
};
|
|
747
942
|
}
|
|
748
943
|
catch (error) {
|
|
749
|
-
|
|
944
|
+
console.error(`[FrontendTaskExecutor] ❌ Error executing frontend activity '${task.activityType}':`, error);
|
|
945
|
+
return {
|
|
946
|
+
output: { error: error.message || 'Unknown error' },
|
|
947
|
+
outcome: 'Failed'
|
|
948
|
+
};
|
|
750
949
|
}
|
|
751
950
|
}
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
// ============================================
|
|
755
|
-
async initializeProvider(provider) {
|
|
756
|
-
const context = {
|
|
757
|
-
registerActivity: (config) => {
|
|
758
|
-
this.activityDescriptors.set(config.key, config.descriptor);
|
|
759
|
-
}
|
|
760
|
-
};
|
|
761
|
-
await provider.provide(context);
|
|
762
|
-
}
|
|
763
|
-
async initializeCategoryProvider(provider) {
|
|
764
|
-
const context = {
|
|
765
|
-
registerCategories: (categories) => {
|
|
766
|
-
this.categories.push(...categories);
|
|
767
|
-
}
|
|
768
|
-
};
|
|
769
|
-
await provider.provide(context);
|
|
770
|
-
}
|
|
771
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.12", ngImport: i0, type: AXPActivityProviderService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
772
|
-
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.12", ngImport: i0, type: AXPActivityProviderService, providedIn: 'root' }); }
|
|
951
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.12", ngImport: i0, type: FrontendTaskExecutor, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
952
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.12", ngImport: i0, type: FrontendTaskExecutor, providedIn: 'root' }); }
|
|
773
953
|
}
|
|
774
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.12", ngImport: i0, type:
|
|
954
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.12", ngImport: i0, type: FrontendTaskExecutor, decorators: [{
|
|
775
955
|
type: Injectable,
|
|
776
956
|
args: [{
|
|
777
957
|
providedIn: 'root'
|
|
778
958
|
}]
|
|
779
959
|
}] });
|
|
780
960
|
|
|
781
|
-
/**
|
|
782
|
-
* Injection token for activity category providers.
|
|
783
|
-
*/
|
|
784
|
-
const AXP_ACTIVITY_CATEGORY_PROVIDER = new InjectionToken('AXP_ACTIVITY_CATEGORY_PROVIDER');
|
|
785
|
-
class AXPActivityCategoryProviderService {
|
|
786
|
-
constructor(parent, providers = []) {
|
|
787
|
-
this.parent = parent;
|
|
788
|
-
this.providers = providers;
|
|
789
|
-
this.categories = new Map();
|
|
790
|
-
this.isInitialized = false;
|
|
791
|
-
if (!parent) {
|
|
792
|
-
this.initialize();
|
|
793
|
-
}
|
|
794
|
-
}
|
|
795
|
-
async initialize() {
|
|
796
|
-
if (this.isInitialized) {
|
|
797
|
-
return;
|
|
798
|
-
}
|
|
799
|
-
for (const provider of this.providers) {
|
|
800
|
-
await provider.provide({
|
|
801
|
-
registerCategories: (categories) => {
|
|
802
|
-
categories.forEach(cat => this.categories.set(cat.name, cat));
|
|
803
|
-
}
|
|
804
|
-
});
|
|
805
|
-
}
|
|
806
|
-
this.isInitialized = true;
|
|
807
|
-
}
|
|
808
|
-
getAllCategories() {
|
|
809
|
-
return Array.from(this.categories.values()).sort((a, b) => (a.order || 0) - (b.order || 0));
|
|
810
|
-
}
|
|
811
|
-
getCategory(name) {
|
|
812
|
-
return this.categories.get(name);
|
|
813
|
-
}
|
|
814
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.12", ngImport: i0, type: AXPActivityCategoryProviderService, deps: [{ token: AXPActivityCategoryProviderService, optional: true, skipSelf: true }, { token: AXP_ACTIVITY_CATEGORY_PROVIDER, optional: true }], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
815
|
-
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.12", ngImport: i0, type: AXPActivityCategoryProviderService, providedIn: 'root' }); }
|
|
816
|
-
}
|
|
817
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.12", ngImport: i0, type: AXPActivityCategoryProviderService, decorators: [{
|
|
818
|
-
type: Injectable,
|
|
819
|
-
args: [{ providedIn: 'root' }]
|
|
820
|
-
}], ctorParameters: () => [{ type: AXPActivityCategoryProviderService, decorators: [{
|
|
821
|
-
type: Optional
|
|
822
|
-
}, {
|
|
823
|
-
type: SkipSelf
|
|
824
|
-
}] }, { type: undefined, decorators: [{
|
|
825
|
-
type: Optional
|
|
826
|
-
}, {
|
|
827
|
-
type: Inject,
|
|
828
|
-
args: [AXP_ACTIVITY_CATEGORY_PROVIDER]
|
|
829
|
-
}] }] });
|
|
830
|
-
|
|
831
|
-
/**
|
|
832
|
-
* Abstract service for workflow execution operations.
|
|
833
|
-
*
|
|
834
|
-
* This service handles communication with backend for workflow execution.
|
|
835
|
-
*
|
|
836
|
-
* Implementation should be provided in connectivity layer:
|
|
837
|
-
* - Mock implementation: @acorex/connectivity/mock
|
|
838
|
-
* - API implementation: @acorex/connectivity/api
|
|
839
|
-
*
|
|
840
|
-
* @example
|
|
841
|
-
* ```typescript
|
|
842
|
-
* // In connectivity/mock
|
|
843
|
-
* @Injectable()
|
|
844
|
-
* export class AXCWorkflowExecutionService implements AXPWorkflowExecutionService {
|
|
845
|
-
* async startExecution(request: AXPStartWorkflowExecutionRequest): Promise<AXPStartWorkflowExecutionResponse> {
|
|
846
|
-
* // Mock implementation
|
|
847
|
-
* }
|
|
848
|
-
* }
|
|
849
|
-
*
|
|
850
|
-
* // In connectivity/api
|
|
851
|
-
* @Injectable()
|
|
852
|
-
* export class AXCWorkflowExecutionService implements AXPWorkflowExecutionService {
|
|
853
|
-
* constructor(private http: HttpClient) {}
|
|
854
|
-
*
|
|
855
|
-
* async startExecution(request: AXPStartWorkflowExecutionRequest): Promise<AXPStartWorkflowExecutionResponse> {
|
|
856
|
-
* return firstValueFrom(
|
|
857
|
-
* this.http.post<AXPStartWorkflowExecutionResponse>(
|
|
858
|
-
* `${this.config.baseUrl}/api/workflows/${request.workflowId}/start`,
|
|
859
|
-
* { input: request.input }
|
|
860
|
-
* )
|
|
861
|
-
* );
|
|
862
|
-
* }
|
|
863
|
-
* }
|
|
864
|
-
* ```
|
|
865
|
-
*/
|
|
866
|
-
class AXPWorkflowExecutionService {
|
|
867
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.12", ngImport: i0, type: AXPWorkflowExecutionService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
868
|
-
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.12", ngImport: i0, type: AXPWorkflowExecutionService }); }
|
|
869
|
-
}
|
|
870
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.12", ngImport: i0, type: AXPWorkflowExecutionService, decorators: [{
|
|
871
|
-
type: Injectable
|
|
872
|
-
}] });
|
|
873
|
-
|
|
874
961
|
//#endregion
|
|
875
962
|
/**
|
|
876
|
-
*
|
|
963
|
+
* Workflow Manager - Facade for workflow lifecycle orchestration.
|
|
964
|
+
*
|
|
965
|
+
* This service is the ONLY interface the frontend uses to interact with workflows.
|
|
966
|
+
* It follows Clean Architecture principles and does NOT contain execution or business logic.
|
|
877
967
|
*
|
|
878
|
-
*
|
|
879
|
-
* -
|
|
880
|
-
* -
|
|
881
|
-
* -
|
|
968
|
+
* Responsibilities:
|
|
969
|
+
* - Orchestrate workflow lifecycle (start, execute, complete, resume)
|
|
970
|
+
* - Delegate execution to FrontendTaskExecutor
|
|
971
|
+
* - Cache workflow state in memory
|
|
972
|
+
* - Expose a stable API for UI
|
|
882
973
|
*
|
|
974
|
+
* Rules:
|
|
975
|
+
* - No HTTP calls (delegates to AXPWorkflowEngine)
|
|
976
|
+
* - No CommandBus / Command execution (delegates to FrontendTaskExecutor)
|
|
977
|
+
* - No workflow branching logic (backend decides)
|
|
978
|
+
* - No business validation (backend validates)
|
|
979
|
+
* - No backend assumptions (uses abstract runtime service)
|
|
883
980
|
*/
|
|
884
|
-
class
|
|
981
|
+
class AXPWorkflowManager {
|
|
885
982
|
constructor() {
|
|
886
983
|
//#region ---- Services & Dependencies ----
|
|
887
|
-
this.
|
|
888
|
-
this.
|
|
984
|
+
this.workflowEngine = inject(AXP_WORKFLOW_ENGINE);
|
|
985
|
+
this.frontendTaskExecutor = inject(FrontendTaskExecutor);
|
|
889
986
|
//#endregion
|
|
890
|
-
//#region ---- State Cache
|
|
987
|
+
//#region ---- State Cache ----
|
|
891
988
|
/**
|
|
892
989
|
* Cache workflow states in memory for quick access.
|
|
893
|
-
* Key:
|
|
894
|
-
* Value:
|
|
990
|
+
* Key: instanceId
|
|
991
|
+
* Value: AXPWorkflowInstanceState
|
|
895
992
|
*/
|
|
896
993
|
this.stateCache = new Map();
|
|
994
|
+
/**
|
|
995
|
+
* Cache TTL in milliseconds (5 minutes).
|
|
996
|
+
*/
|
|
997
|
+
this.CACHE_TTL = 5 * 60 * 1000;
|
|
897
998
|
}
|
|
898
999
|
//#endregion
|
|
899
1000
|
//#region ---- Public Methods ----
|
|
900
1001
|
/**
|
|
901
|
-
* Start
|
|
1002
|
+
* Start a new workflow instance.
|
|
902
1003
|
*
|
|
1004
|
+
* Creates a new workflow instance in backend and returns instance ID.
|
|
903
1005
|
* Backend decides what to do: returns pendingTask or indicates completion.
|
|
904
|
-
* Frontend only calls API - no business logic here.
|
|
905
1006
|
*
|
|
906
|
-
* @param workflowId - Workflow ID
|
|
907
|
-
* @param input - Initial input data
|
|
908
|
-
* @returns
|
|
1007
|
+
* @param workflowId - Workflow ID to start
|
|
1008
|
+
* @param input - Initial input data (optional)
|
|
1009
|
+
* @returns Start result with instanceId, state, and nextTask
|
|
909
1010
|
*
|
|
910
1011
|
* @example
|
|
911
1012
|
* ```typescript
|
|
912
|
-
* const result = await
|
|
1013
|
+
* const result = await workflowManager.start('my-workflow', { userId: '123' });
|
|
913
1014
|
*
|
|
914
|
-
* if (result.
|
|
915
|
-
* // Execute task if frontend
|
|
916
|
-
* if (result.
|
|
917
|
-
* await
|
|
918
|
-
* await
|
|
1015
|
+
* if (result.success && result.nextTask) {
|
|
1016
|
+
* // Execute task if frontend
|
|
1017
|
+
* if (result.nextTask.executionMode === 'frontend') {
|
|
1018
|
+
* const execResult = await workflowManager.execute(result.nextTask);
|
|
1019
|
+
* await workflowManager.complete(result.instanceId!, result.nextTask, execResult.outcome, execResult.output);
|
|
919
1020
|
* }
|
|
920
1021
|
* }
|
|
921
1022
|
* ```
|
|
922
1023
|
*/
|
|
923
|
-
async
|
|
1024
|
+
async start(workflowId, input = {}) {
|
|
924
1025
|
try {
|
|
925
|
-
const
|
|
926
|
-
|
|
1026
|
+
const response = await this.workflowEngine.start({
|
|
1027
|
+
workflowId,
|
|
1028
|
+
input
|
|
1029
|
+
});
|
|
1030
|
+
// Cache state (normalize Date)
|
|
1031
|
+
const startNormalizedState = { ...response.state };
|
|
1032
|
+
if (startNormalizedState.lastUpdated && !(startNormalizedState.lastUpdated instanceof Date)) {
|
|
1033
|
+
startNormalizedState.lastUpdated = new Date(startNormalizedState.lastUpdated);
|
|
1034
|
+
}
|
|
1035
|
+
this.stateCache.set(response.instanceId, startNormalizedState);
|
|
1036
|
+
return {
|
|
927
1037
|
success: true,
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
1038
|
+
instanceId: response.instanceId,
|
|
1039
|
+
state: startNormalizedState,
|
|
1040
|
+
nextTask: response.pendingTask || null,
|
|
1041
|
+
output: startNormalizedState.output
|
|
932
1042
|
};
|
|
933
|
-
return result;
|
|
934
1043
|
}
|
|
935
1044
|
catch (error) {
|
|
936
|
-
console.error('[
|
|
1045
|
+
console.error('[AXPWorkflowManager] ❌ Error starting workflow:', error);
|
|
937
1046
|
return {
|
|
938
1047
|
success: false,
|
|
939
|
-
error: error.message || 'Failed to start workflow'
|
|
940
|
-
nextTask: null
|
|
1048
|
+
error: error.message || 'Failed to start workflow'
|
|
941
1049
|
};
|
|
942
1050
|
}
|
|
943
1051
|
}
|
|
944
1052
|
/**
|
|
945
|
-
* Execute a frontend task
|
|
1053
|
+
* Execute a frontend task.
|
|
946
1054
|
*
|
|
947
|
-
*
|
|
948
|
-
*
|
|
1055
|
+
* Delegates to FrontendTaskExecutor for actual execution.
|
|
1056
|
+
* Only executes tasks with executionMode 'frontend' or 'both'.
|
|
949
1057
|
*
|
|
950
1058
|
* @param task - Task to execute
|
|
951
1059
|
* @returns Execution result with output and outcome
|
|
1060
|
+
*
|
|
1061
|
+
* @throws Error if task is not a frontend task
|
|
952
1062
|
*/
|
|
953
|
-
async
|
|
954
|
-
|
|
955
|
-
if (task.executionMode !== 'frontend' && task.executionMode !== 'both') {
|
|
956
|
-
throw new Error(`Task '${task.activityId}' is not a frontend task. Backend handles it automatically.`);
|
|
957
|
-
}
|
|
958
|
-
return await this.executeFrontendActivity(task);
|
|
1063
|
+
async execute(task) {
|
|
1064
|
+
return this.frontendTaskExecutor.execute(task);
|
|
959
1065
|
}
|
|
960
1066
|
/**
|
|
961
1067
|
* Complete a task and get next task from backend.
|
|
@@ -963,72 +1069,71 @@ class WorkflowCoordinator {
|
|
|
963
1069
|
* Sends task result to backend API.
|
|
964
1070
|
* Backend decides: next task, fail, or complete workflow.
|
|
965
1071
|
*
|
|
966
|
-
* @param
|
|
1072
|
+
* @param instanceId - Workflow instance ID
|
|
967
1073
|
* @param task - Completed task
|
|
968
1074
|
* @param outcome - Task outcome (e.g., 'Done', 'Confirmed', 'Cancelled')
|
|
969
|
-
* @param output - Task output/result
|
|
970
|
-
* @returns
|
|
1075
|
+
* @param output - Task output/result (optional)
|
|
1076
|
+
* @returns Complete result with next task (if any)
|
|
971
1077
|
*/
|
|
972
|
-
async
|
|
1078
|
+
async complete(instanceId, task, outcome, output) {
|
|
973
1079
|
try {
|
|
974
1080
|
// Send result to backend - backend decides next step
|
|
975
|
-
const response = await this.
|
|
976
|
-
|
|
1081
|
+
const response = await this.workflowEngine.resume({
|
|
1082
|
+
instanceId,
|
|
977
1083
|
stepId: task.activityId,
|
|
978
1084
|
taskToken: task.taskToken,
|
|
979
1085
|
outcome,
|
|
980
1086
|
userInput: output
|
|
981
1087
|
});
|
|
982
1088
|
// Update cache
|
|
1089
|
+
let completeNormalizedState = response.state;
|
|
983
1090
|
if (response.state) {
|
|
984
|
-
|
|
1091
|
+
// Normalize lastUpdated to Date (for cache safety)
|
|
1092
|
+
completeNormalizedState = { ...response.state };
|
|
1093
|
+
if (completeNormalizedState.lastUpdated && !(completeNormalizedState.lastUpdated instanceof Date)) {
|
|
1094
|
+
completeNormalizedState.lastUpdated = new Date(completeNormalizedState.lastUpdated);
|
|
1095
|
+
}
|
|
1096
|
+
this.stateCache.set(instanceId, completeNormalizedState);
|
|
985
1097
|
}
|
|
986
1098
|
return {
|
|
987
1099
|
success: true,
|
|
988
|
-
|
|
1100
|
+
instanceId,
|
|
1101
|
+
state: completeNormalizedState,
|
|
989
1102
|
nextTask: response.nextTask || null,
|
|
990
|
-
|
|
991
|
-
state: response.state
|
|
1103
|
+
output: response.output
|
|
992
1104
|
};
|
|
993
1105
|
}
|
|
994
1106
|
catch (error) {
|
|
1107
|
+
console.error('[AXPWorkflowManager] ❌ Error completing task:', error);
|
|
995
1108
|
return {
|
|
996
1109
|
success: false,
|
|
997
|
-
|
|
998
|
-
|
|
1110
|
+
instanceId,
|
|
1111
|
+
error: error.message || 'Failed to complete task'
|
|
999
1112
|
};
|
|
1000
1113
|
}
|
|
1001
1114
|
}
|
|
1002
1115
|
/**
|
|
1003
|
-
*
|
|
1004
|
-
*
|
|
1005
|
-
* @deprecated Use startWorkflow + executeTask + completeTask pattern instead.
|
|
1006
|
-
* This method is kept for backward compatibility but will be removed.
|
|
1007
|
-
*/
|
|
1008
|
-
async executeWorkflowById(workflowId, input = {}) {
|
|
1009
|
-
// Just start workflow - caller should handle task execution
|
|
1010
|
-
return await this.startWorkflow(workflowId, input);
|
|
1011
|
-
}
|
|
1012
|
-
/**
|
|
1013
|
-
* Resume a suspended workflow (e.g., after user interaction).
|
|
1116
|
+
* Resume a suspended workflow (e.g., after user interaction).
|
|
1014
1117
|
*
|
|
1015
1118
|
* Backend determines nextStep based on outcome and outcomeConnections.
|
|
1016
|
-
* Client only provides
|
|
1119
|
+
* Client only provides instanceId, stepId, outcome, and optional userInput.
|
|
1017
1120
|
*
|
|
1018
|
-
* @param
|
|
1121
|
+
* @param instanceId - Workflow instance ID
|
|
1019
1122
|
* @param stepId - Step ID that was waiting for user input
|
|
1020
1123
|
* @param outcome - User action outcome (e.g., 'Confirmed', 'Cancelled', 'Submitted')
|
|
1021
1124
|
* @param userInput - Optional user input data
|
|
1125
|
+
* @param taskToken - Secure task token (required for secure resumption)
|
|
1126
|
+
* @returns Resume result with next task (if any)
|
|
1022
1127
|
*/
|
|
1023
|
-
async
|
|
1128
|
+
async resume(instanceId, stepId, outcome, userInput, taskToken) {
|
|
1024
1129
|
try {
|
|
1025
1130
|
// Ensure taskToken is provided for secure resumption
|
|
1026
1131
|
if (!taskToken) {
|
|
1027
|
-
throw new Error('Missing taskToken for
|
|
1132
|
+
throw new Error('Missing taskToken for resume operation');
|
|
1028
1133
|
}
|
|
1029
1134
|
// Backend handles everything: checks outcomeConnections and determines nextStep
|
|
1030
|
-
const response = await this.
|
|
1031
|
-
|
|
1135
|
+
const response = await this.workflowEngine.resume({
|
|
1136
|
+
instanceId,
|
|
1032
1137
|
stepId,
|
|
1033
1138
|
taskToken,
|
|
1034
1139
|
outcome,
|
|
@@ -1036,1366 +1141,823 @@ class WorkflowCoordinator {
|
|
|
1036
1141
|
});
|
|
1037
1142
|
// Update cache with state from backend
|
|
1038
1143
|
if (response.state) {
|
|
1039
|
-
|
|
1144
|
+
// Normalize lastUpdated to Date (for cache safety)
|
|
1145
|
+
const normalizedState = { ...response.state };
|
|
1146
|
+
if (normalizedState.lastUpdated && !(normalizedState.lastUpdated instanceof Date)) {
|
|
1147
|
+
normalizedState.lastUpdated = new Date(normalizedState.lastUpdated);
|
|
1148
|
+
}
|
|
1149
|
+
this.stateCache.set(instanceId, normalizedState);
|
|
1150
|
+
}
|
|
1151
|
+
// Normalize state Date for return
|
|
1152
|
+
const normalizedState = response.state ? { ...response.state } : undefined;
|
|
1153
|
+
if (normalizedState && normalizedState.lastUpdated && !(normalizedState.lastUpdated instanceof Date)) {
|
|
1154
|
+
normalizedState.lastUpdated = new Date(normalizedState.lastUpdated);
|
|
1040
1155
|
}
|
|
1041
1156
|
return {
|
|
1042
1157
|
success: true,
|
|
1043
|
-
|
|
1158
|
+
instanceId,
|
|
1159
|
+
state: normalizedState || response.state,
|
|
1044
1160
|
nextTask: response.nextTask || null, // Backend determines this from outcomeConnections
|
|
1045
|
-
|
|
1046
|
-
state: response.state
|
|
1161
|
+
output: response.output
|
|
1047
1162
|
};
|
|
1048
1163
|
}
|
|
1049
1164
|
catch (error) {
|
|
1165
|
+
console.error('[AXPWorkflowManager] ❌ Error resuming workflow:', error);
|
|
1050
1166
|
return {
|
|
1051
1167
|
success: false,
|
|
1052
|
-
|
|
1053
|
-
|
|
1168
|
+
instanceId,
|
|
1169
|
+
error: error.message || 'Failed to resume workflow'
|
|
1054
1170
|
};
|
|
1055
1171
|
}
|
|
1056
1172
|
}
|
|
1057
1173
|
/**
|
|
1058
|
-
* Get workflow
|
|
1174
|
+
* Get workflow instance state.
|
|
1175
|
+
*
|
|
1176
|
+
* Retrieves state from cache (if valid) or from backend.
|
|
1177
|
+
*
|
|
1178
|
+
* @param instanceId - Workflow instance ID
|
|
1179
|
+
* @returns Workflow instance state or null if not found
|
|
1059
1180
|
*/
|
|
1060
|
-
async
|
|
1181
|
+
async getState(instanceId) {
|
|
1061
1182
|
// Check cache first
|
|
1062
|
-
const cached = this.stateCache.get(
|
|
1183
|
+
const cached = this.stateCache.get(instanceId);
|
|
1063
1184
|
if (cached) {
|
|
1064
|
-
//
|
|
1065
|
-
const
|
|
1066
|
-
if (
|
|
1067
|
-
|
|
1185
|
+
// Normalize lastUpdated to Date (for cache safety)
|
|
1186
|
+
const normalizedCached = { ...cached };
|
|
1187
|
+
if (normalizedCached.lastUpdated && !(normalizedCached.lastUpdated instanceof Date)) {
|
|
1188
|
+
normalizedCached.lastUpdated = new Date(normalizedCached.lastUpdated);
|
|
1189
|
+
}
|
|
1190
|
+
// Validate cache age
|
|
1191
|
+
const cacheAge = Date.now() - normalizedCached.lastUpdated.getTime();
|
|
1192
|
+
if (cacheAge < this.CACHE_TTL) {
|
|
1193
|
+
return normalizedCached;
|
|
1068
1194
|
}
|
|
1069
1195
|
}
|
|
1070
1196
|
// Fetch from backend
|
|
1071
1197
|
try {
|
|
1072
|
-
const state = await this.
|
|
1073
|
-
|
|
1198
|
+
const state = await this.workflowEngine.getState({
|
|
1199
|
+
instanceId
|
|
1074
1200
|
});
|
|
1075
|
-
//
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
catch {
|
|
1080
|
-
return null;
|
|
1081
|
-
}
|
|
1082
|
-
}
|
|
1083
|
-
//#endregion
|
|
1084
|
-
//#region ---- Private Methods ----
|
|
1085
|
-
/**
|
|
1086
|
-
* Execute a frontend activity using CommandBus.
|
|
1087
|
-
*
|
|
1088
|
-
* Frontend activities are executed in the browser using AXPCommandService.
|
|
1089
|
-
* Activities can also be executed in both frontend and backend (hybrid mode).
|
|
1090
|
-
*
|
|
1091
|
-
* @param task - Frontend task to execute
|
|
1092
|
-
* @returns Execution result with output and outcome
|
|
1093
|
-
*/
|
|
1094
|
-
async executeFrontendActivity(task) {
|
|
1095
|
-
try {
|
|
1096
|
-
// Check if command exists
|
|
1097
|
-
const commandExists = this.commandService.exists(task.activityType);
|
|
1098
|
-
if (!commandExists) {
|
|
1099
|
-
console.warn(`[WorkflowCoordinator] ⚠️ Frontend activity '${task.activityType}' is not registered. Skipping execution.`);
|
|
1100
|
-
return {
|
|
1101
|
-
output: null,
|
|
1102
|
-
outcome: 'Done'
|
|
1103
|
-
};
|
|
1104
|
-
}
|
|
1105
|
-
// Execute activity via CommandBus
|
|
1106
|
-
// Activities registered as AXPCommand return {output, outcomes}
|
|
1107
|
-
// 🎯 Flatten properties if nested (workflow-studio format)
|
|
1108
|
-
let commandInput = task.input || task.config || {};
|
|
1109
|
-
if (commandInput['properties'] && typeof commandInput['properties'] === 'object') {
|
|
1110
|
-
// Flatten: {properties: {text: "..."}} -> {text: "..."}
|
|
1111
|
-
commandInput = { ...commandInput['properties'] };
|
|
1112
|
-
}
|
|
1113
|
-
const result = await this.commandService.execute(task.activityType, commandInput);
|
|
1114
|
-
if (!result) {
|
|
1115
|
-
return {
|
|
1116
|
-
output: null,
|
|
1117
|
-
outcome: 'Failed',
|
|
1118
|
-
};
|
|
1119
|
-
}
|
|
1120
|
-
if (!result.success) {
|
|
1121
|
-
return {
|
|
1122
|
-
output: {
|
|
1123
|
-
error: result.message?.text,
|
|
1124
|
-
},
|
|
1125
|
-
outcome: 'Failed',
|
|
1126
|
-
};
|
|
1127
|
-
}
|
|
1128
|
-
const commandResult = result.data;
|
|
1129
|
-
const outcomes = commandResult?.outcomes ?? {};
|
|
1130
|
-
let outcome = 'Done';
|
|
1131
|
-
if (Object.keys(outcomes).length > 0) {
|
|
1132
|
-
outcome = outcomes['Done'] ? 'Done' : Object.keys(outcomes)[0] || 'Done';
|
|
1201
|
+
// Normalize lastUpdated to Date (for cache safety)
|
|
1202
|
+
const normalizedState = { ...state };
|
|
1203
|
+
if (normalizedState.lastUpdated && !(normalizedState.lastUpdated instanceof Date)) {
|
|
1204
|
+
normalizedState.lastUpdated = new Date(normalizedState.lastUpdated);
|
|
1133
1205
|
}
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
};
|
|
1206
|
+
// Update cache
|
|
1207
|
+
this.stateCache.set(instanceId, normalizedState);
|
|
1208
|
+
return normalizedState;
|
|
1138
1209
|
}
|
|
1139
1210
|
catch (error) {
|
|
1140
|
-
console.error(
|
|
1141
|
-
return
|
|
1142
|
-
output: { error: error.message || 'Unknown error' },
|
|
1143
|
-
outcome: 'Failed'
|
|
1144
|
-
};
|
|
1211
|
+
console.error('[AXPWorkflowManager] ❌ Error getting workflow state:', error);
|
|
1212
|
+
return null;
|
|
1145
1213
|
}
|
|
1146
1214
|
}
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
* Backend returns executionId, initial state, and first task to execute.
|
|
1150
|
-
*/
|
|
1151
|
-
async startWorkflowExecution(workflowId, input) {
|
|
1152
|
-
const response = await this.workflowExecutionService.startExecution({
|
|
1153
|
-
workflowId,
|
|
1154
|
-
input
|
|
1155
|
-
});
|
|
1156
|
-
// Cache state
|
|
1157
|
-
this.stateCache.set(response.executionId, response.state);
|
|
1158
|
-
return {
|
|
1159
|
-
executionId: response.executionId,
|
|
1160
|
-
state: response.state,
|
|
1161
|
-
pendingTask: response.pendingTask
|
|
1162
|
-
};
|
|
1163
|
-
}
|
|
1164
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.12", ngImport: i0, type: WorkflowCoordinator, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
1165
|
-
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.12", ngImport: i0, type: WorkflowCoordinator, providedIn: 'root' }); }
|
|
1215
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.12", ngImport: i0, type: AXPWorkflowManager, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
1216
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.12", ngImport: i0, type: AXPWorkflowManager, providedIn: 'root' }); }
|
|
1166
1217
|
}
|
|
1167
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.12", ngImport: i0, type:
|
|
1218
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.12", ngImport: i0, type: AXPWorkflowManager, decorators: [{
|
|
1168
1219
|
type: Injectable,
|
|
1169
1220
|
args: [{
|
|
1170
1221
|
providedIn: 'root'
|
|
1171
1222
|
}]
|
|
1172
1223
|
}] });
|
|
1173
1224
|
|
|
1225
|
+
//#endregion
|
|
1174
1226
|
/**
|
|
1175
|
-
*
|
|
1176
|
-
*/
|
|
1177
|
-
|
|
1178
|
-
// Production Workflow Coordinator (Frontend/Backend Separation)
|
|
1179
|
-
|
|
1180
|
-
/**
|
|
1181
|
-
* WriteLine Activity - Simple console logging activity.
|
|
1227
|
+
* Local engine implementation that manages workflow progression and state.
|
|
1182
1228
|
*
|
|
1183
|
-
*
|
|
1184
|
-
*
|
|
1185
|
-
*
|
|
1186
|
-
*
|
|
1187
|
-
*
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
super('WriteLine');
|
|
1192
|
-
}
|
|
1193
|
-
async execute(input) {
|
|
1194
|
-
const text = input.text || '';
|
|
1195
|
-
if (text !== undefined && text !== null) {
|
|
1196
|
-
console.log(`[WriteLine] ${text}`);
|
|
1197
|
-
}
|
|
1198
|
-
return this.createResult(undefined, 'Done');
|
|
1199
|
-
}
|
|
1200
|
-
}
|
|
1201
|
-
|
|
1202
|
-
var writeLine_activity = /*#__PURE__*/Object.freeze({
|
|
1203
|
-
__proto__: null,
|
|
1204
|
-
WriteLine: WriteLine
|
|
1205
|
-
});
|
|
1206
|
-
|
|
1207
|
-
/**
|
|
1208
|
-
* Sequence Activity - Executes activities in sequential order.
|
|
1229
|
+
* This engine:
|
|
1230
|
+
* - Returns frontend/both activities as pendingTask (does NOT execute them)
|
|
1231
|
+
* - Skips backend activities (does not error, continues execution)
|
|
1232
|
+
* - Maintains workflow state in memory
|
|
1233
|
+
* - Does not require backend API calls
|
|
1234
|
+
*
|
|
1235
|
+
* Execution of frontend tasks is handled by AXPWorkflowManager via FrontendTaskExecutor.
|
|
1236
|
+
* This engine only manages workflow progression and state storage.
|
|
1209
1237
|
*
|
|
1210
|
-
*
|
|
1211
|
-
*
|
|
1212
|
-
* const sequence = new Sequence();
|
|
1213
|
-
* sequence.activities = [activity1, activity2, activity3];
|
|
1214
|
-
* await sequence.execute({});
|
|
1215
|
-
* ```
|
|
1238
|
+
* This is the DEFAULT engine provider. Applications can override it with
|
|
1239
|
+
* an API-based engine implementation.
|
|
1216
1240
|
*/
|
|
1217
|
-
class
|
|
1218
|
-
|
|
1219
|
-
|
|
1241
|
+
class AXPWorkflowLocalEngine {
|
|
1242
|
+
//#region ---- Services & Dependencies ----
|
|
1243
|
+
constructor(definitionLoaders = null) {
|
|
1244
|
+
this.definitionLoaders = definitionLoaders;
|
|
1245
|
+
//#endregion
|
|
1246
|
+
//#region ---- Instance Storage ----
|
|
1220
1247
|
/**
|
|
1221
|
-
*
|
|
1248
|
+
* In-memory storage for workflow instances.
|
|
1249
|
+
* Key: instanceId
|
|
1250
|
+
* Value: LocalWorkflowState
|
|
1222
1251
|
*/
|
|
1223
|
-
this.
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
return {
|
|
1231
|
-
success: false,
|
|
1232
|
-
message: result.message,
|
|
1233
|
-
data: {
|
|
1234
|
-
output: undefined,
|
|
1235
|
-
outcomes: {
|
|
1236
|
-
Failed: true,
|
|
1237
|
-
},
|
|
1238
|
-
},
|
|
1239
|
-
};
|
|
1240
|
-
}
|
|
1241
|
-
}
|
|
1242
|
-
return this.createResult(undefined, 'Done');
|
|
1252
|
+
this.instances = new Map();
|
|
1253
|
+
/**
|
|
1254
|
+
* Task token storage for secure resume operations.
|
|
1255
|
+
* Key: taskToken
|
|
1256
|
+
* Value: { instanceId, activityId }
|
|
1257
|
+
*/
|
|
1258
|
+
this.taskTokens = new Map();
|
|
1243
1259
|
}
|
|
1244
|
-
}
|
|
1245
|
-
|
|
1246
|
-
var sequence_activity = /*#__PURE__*/Object.freeze({
|
|
1247
|
-
__proto__: null,
|
|
1248
|
-
Sequence: Sequence
|
|
1249
|
-
});
|
|
1250
|
-
|
|
1251
|
-
/**
|
|
1252
|
-
* Show Confirm Dialog Activity - Displays confirmation dialog to user.
|
|
1253
|
-
*
|
|
1254
|
-
* Has two outcomes:
|
|
1255
|
-
* - 'Confirmed': User clicked confirm/yes
|
|
1256
|
-
* - 'Cancelled': User clicked cancel/no
|
|
1257
|
-
*
|
|
1258
|
-
* Usage:
|
|
1259
|
-
* ```typescript
|
|
1260
|
-
* const dialog = new ShowConfirmDialog();
|
|
1261
|
-
* await dialog.execute({
|
|
1262
|
-
* title: 'Confirm Delete',
|
|
1263
|
-
* message: 'Are you sure?',
|
|
1264
|
-
* color: 'danger'
|
|
1265
|
-
* });
|
|
1266
|
-
* ```
|
|
1267
|
-
*/
|
|
1268
|
-
class ShowConfirmDialog extends Activity {
|
|
1269
1260
|
//#endregion
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
//
|
|
1280
|
-
const
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
}
|
|
1261
|
+
//#region ---- Public Methods (AXPWorkflowEngine) ----
|
|
1262
|
+
/**
|
|
1263
|
+
* Start a new workflow instance.
|
|
1264
|
+
*
|
|
1265
|
+
* Creates an in-memory workflow instance and progresses it.
|
|
1266
|
+
* Frontend/both activities are returned as pendingTask for external execution.
|
|
1267
|
+
* Backend activities are skipped.
|
|
1268
|
+
*/
|
|
1269
|
+
async start(request) {
|
|
1270
|
+
// Generate instance ID
|
|
1271
|
+
const instanceId = AXPDataGenerator.uuid();
|
|
1272
|
+
const now = new Date();
|
|
1273
|
+
// Load workflow definition
|
|
1274
|
+
const definition = await this.getDefinition(request.workflowId);
|
|
1275
|
+
if (!definition) {
|
|
1276
|
+
throw new Error(`Workflow definition not found: ${request.workflowId}`);
|
|
1277
|
+
}
|
|
1278
|
+
// Initialize workflow state
|
|
1279
|
+
const state = {
|
|
1280
|
+
instanceId,
|
|
1281
|
+
workflowId: request.workflowId,
|
|
1282
|
+
status: 'running',
|
|
1283
|
+
variables: {},
|
|
1284
|
+
input: request.input || {},
|
|
1285
|
+
output: undefined,
|
|
1286
|
+
lastUpdated: now,
|
|
1287
|
+
};
|
|
1288
|
+
// Create local state
|
|
1289
|
+
const localState = {
|
|
1290
|
+
instanceId,
|
|
1291
|
+
workflowId: request.workflowId,
|
|
1292
|
+
definition,
|
|
1293
|
+
state,
|
|
1294
|
+
completedActivities: new Set(),
|
|
1295
|
+
activityResults: new Map(),
|
|
1296
|
+
};
|
|
1297
|
+
// Store instance
|
|
1298
|
+
this.instances.set(instanceId, localState);
|
|
1299
|
+
// Execute workflow steps
|
|
1300
|
+
const pendingTask = await this.executeWorkflowSteps(localState);
|
|
1301
|
+
// Update state
|
|
1302
|
+
localState.state.lastUpdated = new Date();
|
|
1303
|
+
return {
|
|
1304
|
+
instanceId,
|
|
1305
|
+
state: localState.state,
|
|
1306
|
+
pendingTask: pendingTask || null,
|
|
1307
|
+
};
|
|
1317
1308
|
}
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
this.dialogService = inject(AXDialogService);
|
|
1347
|
-
this.translationService = inject(AXTranslationService);
|
|
1348
|
-
}
|
|
1349
|
-
async execute(input) {
|
|
1350
|
-
const { title = '', message = '', color = 'primary' } = input;
|
|
1351
|
-
// Translate title and message only if they start with '@' (translation key)
|
|
1352
|
-
// Otherwise use the text as-is
|
|
1353
|
-
const translatedTitle = title
|
|
1354
|
-
? (title.startsWith('@')
|
|
1355
|
-
? await this.translationService.translateAsync(title) || title
|
|
1356
|
-
: title)
|
|
1357
|
-
: '';
|
|
1358
|
-
const translatedMessage = message
|
|
1359
|
-
? (message.startsWith('@')
|
|
1360
|
-
? await this.translationService.translateAsync(message) || message
|
|
1361
|
-
: message)
|
|
1362
|
-
: '';
|
|
1363
|
-
try {
|
|
1364
|
-
await this.dialogService.alert(translatedTitle, translatedMessage, color);
|
|
1365
|
-
return this.createResult({
|
|
1366
|
-
result: true,
|
|
1367
|
-
action: 'ok'
|
|
1368
|
-
}, 'Done');
|
|
1369
|
-
}
|
|
1370
|
-
catch (err) {
|
|
1371
|
-
console.error('[ShowAlertDialog] Error showing dialog:', err);
|
|
1372
|
-
return {
|
|
1373
|
-
success: false,
|
|
1374
|
-
message: {
|
|
1375
|
-
text: err instanceof Error ? err.message : 'Failed to show alert dialog',
|
|
1376
|
-
},
|
|
1377
|
-
data: {
|
|
1378
|
-
output: {
|
|
1379
|
-
result: false,
|
|
1380
|
-
action: 'error',
|
|
1381
|
-
},
|
|
1382
|
-
outcomes: {
|
|
1383
|
-
Failed: true,
|
|
1384
|
-
},
|
|
1385
|
-
},
|
|
1309
|
+
/**
|
|
1310
|
+
* Resume a suspended workflow instance.
|
|
1311
|
+
*
|
|
1312
|
+
* Validates task token, applies externally executed result,
|
|
1313
|
+
* and continues progressing workflow steps.
|
|
1314
|
+
*/
|
|
1315
|
+
async resume(request) {
|
|
1316
|
+
// Validate task token
|
|
1317
|
+
const tokenInfo = this.taskTokens.get(request.taskToken);
|
|
1318
|
+
if (!tokenInfo || tokenInfo.instanceId !== request.instanceId || tokenInfo.activityId !== request.stepId) {
|
|
1319
|
+
throw new Error('Invalid task token');
|
|
1320
|
+
}
|
|
1321
|
+
// Get instance
|
|
1322
|
+
const localState = this.instances.get(request.instanceId);
|
|
1323
|
+
if (!localState) {
|
|
1324
|
+
throw new Error(`Workflow instance not found: ${request.instanceId}`);
|
|
1325
|
+
}
|
|
1326
|
+
// Store activity result (from external execution)
|
|
1327
|
+
localState.activityResults.set(request.stepId, {
|
|
1328
|
+
output: request.userInput || {},
|
|
1329
|
+
outcome: request.outcome,
|
|
1330
|
+
});
|
|
1331
|
+
localState.completedActivities.add(request.stepId);
|
|
1332
|
+
// Merge output/userInput into state variables
|
|
1333
|
+
if (request.userInput) {
|
|
1334
|
+
localState.state.variables = {
|
|
1335
|
+
...localState.state.variables,
|
|
1336
|
+
...(request.userInput || {}),
|
|
1386
1337
|
};
|
|
1387
1338
|
}
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
* Show Dialog Layout Builder Activity - Displays dialog using Layout Builder.
|
|
1398
|
-
*
|
|
1399
|
-
* This activity allows you to create custom dialogs using the Layout Builder API.
|
|
1400
|
-
* It accepts JSON-serializable content (AXPWidgetNode) and actions configuration.
|
|
1401
|
-
*
|
|
1402
|
-
* The content can be created using Layout Builder and then converted to JSON:
|
|
1403
|
-
* ```typescript
|
|
1404
|
-
* const builder = layoutBuilderService.create();
|
|
1405
|
-
* builder.flex(flex => {
|
|
1406
|
-
* flex.setDirection('column')
|
|
1407
|
-
* .formField('First Name', field => {
|
|
1408
|
-
* field.path('firstName');
|
|
1409
|
-
* field.textBox({ placeholder: 'Enter first name' });
|
|
1410
|
-
* });
|
|
1411
|
-
* });
|
|
1412
|
-
* const contentNode = builder.build();
|
|
1413
|
-
* ```
|
|
1414
|
-
*
|
|
1415
|
-
* Usage in Workflow:
|
|
1416
|
-
* ```typescript
|
|
1417
|
-
* const dialog = new ShowDialogLayoutBuilder();
|
|
1418
|
-
* await dialog.execute({
|
|
1419
|
-
* title: 'User Information',
|
|
1420
|
-
* size: 'md',
|
|
1421
|
-
* context: { firstName: '', lastName: '' },
|
|
1422
|
-
* content: {
|
|
1423
|
-
* type: 'flex-layout',
|
|
1424
|
-
* mode: 'edit',
|
|
1425
|
-
* options: {
|
|
1426
|
-
* flexDirection: 'column',
|
|
1427
|
-
* gap: '16px'
|
|
1428
|
-
* },
|
|
1429
|
-
* children: [
|
|
1430
|
-
* {
|
|
1431
|
-
* type: 'form-field',
|
|
1432
|
-
* mode: 'edit',
|
|
1433
|
-
* options: {
|
|
1434
|
-
* label: 'First Name',
|
|
1435
|
-
* showLabel: true
|
|
1436
|
-
* },
|
|
1437
|
-
* children: [{
|
|
1438
|
-
* type: 'text-editor',
|
|
1439
|
-
* path: 'firstName',
|
|
1440
|
-
* options: {
|
|
1441
|
-
* placeholder: 'Enter first name'
|
|
1442
|
-
* }
|
|
1443
|
-
* }]
|
|
1444
|
-
* }
|
|
1445
|
-
* ]
|
|
1446
|
-
* },
|
|
1447
|
-
* actions: {
|
|
1448
|
-
* cancel: '@general:actions.cancel.title',
|
|
1449
|
-
* submit: '@general:actions.submit.title',
|
|
1450
|
-
* custom: [{
|
|
1451
|
-
* title: 'Save Draft',
|
|
1452
|
-
* icon: 'fa-save',
|
|
1453
|
-
* color: 'secondary',
|
|
1454
|
-
* command: { name: 'save-draft' }
|
|
1455
|
-
* }]
|
|
1456
|
-
* }
|
|
1457
|
-
* });
|
|
1458
|
-
* ```
|
|
1459
|
-
*
|
|
1460
|
-
* Usage in JSON Workflow Definition:
|
|
1461
|
-
* ```json
|
|
1462
|
-
* {
|
|
1463
|
-
* "type": "ShowDialogLayoutBuilder",
|
|
1464
|
-
* "properties": {
|
|
1465
|
-
* "title": "User Information",
|
|
1466
|
-
* "size": "md",
|
|
1467
|
-
* "context": { "firstName": "", "lastName": "" },
|
|
1468
|
-
* "content": {
|
|
1469
|
-
* "type": "flex-layout",
|
|
1470
|
-
* "mode": "edit",
|
|
1471
|
-
* "options": {
|
|
1472
|
-
* "flexDirection": "column",
|
|
1473
|
-
* "gap": "16px"
|
|
1474
|
-
* },
|
|
1475
|
-
* "children": [
|
|
1476
|
-
* {
|
|
1477
|
-
* "type": "form-field",
|
|
1478
|
-
* "mode": "edit",
|
|
1479
|
-
* "options": {
|
|
1480
|
-
* "label": "First Name",
|
|
1481
|
-
* "showLabel": true
|
|
1482
|
-
* },
|
|
1483
|
-
* "children": [{
|
|
1484
|
-
* "type": "text-editor",
|
|
1485
|
-
* "path": "firstName",
|
|
1486
|
-
* "options": {
|
|
1487
|
-
* "placeholder": "Enter first name"
|
|
1488
|
-
* }
|
|
1489
|
-
* }]
|
|
1490
|
-
* }
|
|
1491
|
-
* ]
|
|
1492
|
-
* },
|
|
1493
|
-
* "actions": {
|
|
1494
|
-
* "cancel": "@general:actions.cancel.title",
|
|
1495
|
-
* "submit": "@general:actions.submit.title"
|
|
1496
|
-
* }
|
|
1497
|
-
* }
|
|
1498
|
-
* }
|
|
1499
|
-
* ```
|
|
1500
|
-
*/
|
|
1501
|
-
class ShowDialogLayoutBuilder extends Activity {
|
|
1502
|
-
//#endregion
|
|
1503
|
-
constructor() {
|
|
1504
|
-
super('ShowDialogLayoutBuilder');
|
|
1505
|
-
//#region ---- Services & Dependencies ----
|
|
1506
|
-
this.layoutBuilder = inject(AXPLayoutBuilderService);
|
|
1507
|
-
}
|
|
1508
|
-
async execute(input) {
|
|
1509
|
-
const { title = '', size = 'md', context = {}, closeButton = false, message, content, actions } = input;
|
|
1510
|
-
try {
|
|
1511
|
-
// Create dialog using layout builder
|
|
1512
|
-
const dialogRef = await this.layoutBuilder
|
|
1513
|
-
.create()
|
|
1514
|
-
.dialog(dialog => {
|
|
1515
|
-
// Set basic dialog options
|
|
1516
|
-
dialog
|
|
1517
|
-
.setTitle(title)
|
|
1518
|
-
.setSize(size)
|
|
1519
|
-
.setCloseButton(closeButton)
|
|
1520
|
-
.setContext(context);
|
|
1521
|
-
// Set message if provided
|
|
1522
|
-
if (message) {
|
|
1523
|
-
dialog.setMessage(message);
|
|
1524
|
-
}
|
|
1525
|
-
// Set content if provided
|
|
1526
|
-
if (content) {
|
|
1527
|
-
// If content is already a flex-layout, use it directly
|
|
1528
|
-
// Otherwise, wrap it in a flex container with column direction
|
|
1529
|
-
if (content.type === 'flex-layout') {
|
|
1530
|
-
// Set content layout directly (accessing internal state of DialogContainerBuilder)
|
|
1531
|
-
dialog.contentLayout = content;
|
|
1532
|
-
}
|
|
1533
|
-
else {
|
|
1534
|
-
// Wrap content in a flex container
|
|
1535
|
-
const wrappedContent = {
|
|
1536
|
-
type: 'flex-layout',
|
|
1537
|
-
mode: 'edit',
|
|
1538
|
-
options: {
|
|
1539
|
-
flexDirection: 'column',
|
|
1540
|
-
gap: '10px',
|
|
1541
|
-
},
|
|
1542
|
-
children: [content],
|
|
1543
|
-
};
|
|
1544
|
-
dialog.contentLayout = wrappedContent;
|
|
1545
|
-
}
|
|
1546
|
-
}
|
|
1547
|
-
// Set actions if provided
|
|
1548
|
-
if (actions) {
|
|
1549
|
-
dialog.setActions(actionBuilder => {
|
|
1550
|
-
if (actions.cancel) {
|
|
1551
|
-
actionBuilder.cancel(actions.cancel);
|
|
1552
|
-
}
|
|
1553
|
-
if (actions.submit) {
|
|
1554
|
-
actionBuilder.submit(actions.submit);
|
|
1555
|
-
}
|
|
1556
|
-
if (actions.custom) {
|
|
1557
|
-
actions.custom.forEach(action => {
|
|
1558
|
-
actionBuilder.custom(action);
|
|
1559
|
-
});
|
|
1560
|
-
}
|
|
1561
|
-
});
|
|
1562
|
-
}
|
|
1563
|
-
})
|
|
1564
|
-
.show();
|
|
1565
|
-
// Get user action and context
|
|
1566
|
-
const action = dialogRef.action() || 'cancel';
|
|
1567
|
-
const dialogContext = dialogRef.context();
|
|
1568
|
-
// Determine outcomes
|
|
1569
|
-
const cancelled = action === 'cancel';
|
|
1570
|
-
const confirmed = action === 'submit' || (!cancelled && action !== 'error');
|
|
1571
|
-
// Close dialog
|
|
1572
|
-
dialogRef.close();
|
|
1573
|
-
// Return result with appropriate outcome
|
|
1574
|
-
const outcome = cancelled ? 'Cancelled' : confirmed ? 'Confirmed' : 'Done';
|
|
1575
|
-
return this.createResult({
|
|
1576
|
-
context: dialogContext,
|
|
1577
|
-
action,
|
|
1578
|
-
cancelled,
|
|
1579
|
-
confirmed
|
|
1580
|
-
}, outcome);
|
|
1581
|
-
}
|
|
1582
|
-
catch (err) {
|
|
1583
|
-
console.error('[ShowDialogLayoutBuilder] Error showing dialog:', err);
|
|
1584
|
-
return {
|
|
1585
|
-
success: false,
|
|
1586
|
-
message: {
|
|
1587
|
-
text: err instanceof Error ? err.message : 'Failed to show dialog',
|
|
1588
|
-
},
|
|
1589
|
-
data: {
|
|
1590
|
-
output: {
|
|
1591
|
-
context: {},
|
|
1592
|
-
action: 'error',
|
|
1593
|
-
cancelled: true,
|
|
1594
|
-
confirmed: false,
|
|
1595
|
-
},
|
|
1596
|
-
outcomes: {
|
|
1597
|
-
Error: true,
|
|
1598
|
-
},
|
|
1599
|
-
},
|
|
1600
|
-
};
|
|
1339
|
+
// Mark activity as completed and continue progression
|
|
1340
|
+
// Continue progressing workflow steps (skipping backend activities)
|
|
1341
|
+
const nextTask = await this.executeWorkflowSteps(localState);
|
|
1342
|
+
// Update state
|
|
1343
|
+
localState.state.lastUpdated = new Date();
|
|
1344
|
+
// Determine final status
|
|
1345
|
+
if (!nextTask && localState.state.status === 'running') {
|
|
1346
|
+
localState.state.status = 'completed';
|
|
1347
|
+
localState.state.output = localState.state.variables;
|
|
1601
1348
|
}
|
|
1349
|
+
return {
|
|
1350
|
+
output: request.userInput || {},
|
|
1351
|
+
outcomes: { [request.outcome]: true },
|
|
1352
|
+
state: localState.state,
|
|
1353
|
+
nextTask: nextTask || null,
|
|
1354
|
+
};
|
|
1602
1355
|
}
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
* const toast = new ShowToast();
|
|
1611
|
-
* await toast.execute({
|
|
1612
|
-
* color: 'success',
|
|
1613
|
-
* title: 'Success',
|
|
1614
|
-
* message: 'Operation completed successfully!'
|
|
1615
|
-
* });
|
|
1616
|
-
* ```
|
|
1617
|
-
*/
|
|
1618
|
-
class ShowToast extends Activity {
|
|
1619
|
-
constructor() {
|
|
1620
|
-
super('ShowToast');
|
|
1621
|
-
this.toastService = inject(AXToastService);
|
|
1622
|
-
this.translationService = inject(AXTranslationService);
|
|
1623
|
-
}
|
|
1624
|
-
async execute(input) {
|
|
1625
|
-
const { color = 'primary', title = '', message = '', duration = 3000 } = input;
|
|
1626
|
-
// Translate title and message only if they start with '@' (translation key)
|
|
1627
|
-
// Otherwise use the text as-is
|
|
1628
|
-
const translatedTitle = title
|
|
1629
|
-
? (title.startsWith('@')
|
|
1630
|
-
? await this.translationService.translateAsync(title) || title
|
|
1631
|
-
: title)
|
|
1632
|
-
: '';
|
|
1633
|
-
const translatedMessage = message
|
|
1634
|
-
? (message.startsWith('@')
|
|
1635
|
-
? await this.translationService.translateAsync(message) || message
|
|
1636
|
-
: message)
|
|
1637
|
-
: '';
|
|
1638
|
-
try {
|
|
1639
|
-
await this.toastService.show({
|
|
1640
|
-
color: color,
|
|
1641
|
-
title: translatedTitle,
|
|
1642
|
-
content: translatedMessage,
|
|
1643
|
-
closeButton: true,
|
|
1644
|
-
timeOut: duration,
|
|
1645
|
-
timeOutProgress: true,
|
|
1646
|
-
});
|
|
1647
|
-
return this.createResult(undefined, 'Done');
|
|
1356
|
+
/**
|
|
1357
|
+
* Get current workflow instance state.
|
|
1358
|
+
*/
|
|
1359
|
+
async getState(request) {
|
|
1360
|
+
const localState = this.instances.get(request.instanceId);
|
|
1361
|
+
if (!localState) {
|
|
1362
|
+
throw new Error(`Workflow instance not found: ${request.instanceId}`);
|
|
1648
1363
|
}
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
message: {
|
|
1654
|
-
text: err instanceof Error ? err.message : 'Failed to show toast',
|
|
1655
|
-
},
|
|
1656
|
-
data: {
|
|
1657
|
-
output: undefined,
|
|
1658
|
-
outcomes: {
|
|
1659
|
-
Failed: true,
|
|
1660
|
-
},
|
|
1661
|
-
},
|
|
1662
|
-
};
|
|
1364
|
+
// Normalize lastUpdated to Date (for cache safety)
|
|
1365
|
+
const state = { ...localState.state };
|
|
1366
|
+
if (state.lastUpdated && !(state.lastUpdated instanceof Date)) {
|
|
1367
|
+
state.lastUpdated = new Date(state.lastUpdated);
|
|
1663
1368
|
}
|
|
1369
|
+
return state;
|
|
1664
1370
|
}
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
* route: '/users',
|
|
1681
|
-
* params: { id: '123' }
|
|
1682
|
-
* });
|
|
1683
|
-
* ```
|
|
1684
|
-
*/
|
|
1685
|
-
class Navigate extends Activity {
|
|
1686
|
-
constructor() {
|
|
1687
|
-
super('Navigate');
|
|
1688
|
-
this.router = inject(Router);
|
|
1371
|
+
//#endregion
|
|
1372
|
+
//#region ---- Private Methods ----
|
|
1373
|
+
/**
|
|
1374
|
+
* Get workflow definition from available loaders.
|
|
1375
|
+
*/
|
|
1376
|
+
async getDefinition(workflowId) {
|
|
1377
|
+
// Try all loaders in order
|
|
1378
|
+
const loaders = this.definitionLoaders || [];
|
|
1379
|
+
for (const loader of loaders) {
|
|
1380
|
+
const definition = await loader.get(workflowId);
|
|
1381
|
+
if (definition) {
|
|
1382
|
+
return definition;
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
return null;
|
|
1689
1386
|
}
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1387
|
+
/**
|
|
1388
|
+
* Progress workflow steps starting from the current position.
|
|
1389
|
+
*
|
|
1390
|
+
* For frontend/both activities: returns task immediately (suspends workflow).
|
|
1391
|
+
* For backend activities: skips and continues.
|
|
1392
|
+
*
|
|
1393
|
+
* Returns the next pending task (if frontend activity found) or null (if completed).
|
|
1394
|
+
*/
|
|
1395
|
+
async executeWorkflowSteps(localState) {
|
|
1396
|
+
const flowchart = localState.definition.root;
|
|
1397
|
+
const activities = flowchart.activities || [];
|
|
1398
|
+
const connections = flowchart.connections || [];
|
|
1399
|
+
// Build activity map
|
|
1400
|
+
const activityMap = new Map();
|
|
1401
|
+
activities.forEach(activity => {
|
|
1402
|
+
activityMap.set(activity.id, activity);
|
|
1403
|
+
});
|
|
1404
|
+
// Build connection graph
|
|
1405
|
+
const outgoingConnections = new Map();
|
|
1406
|
+
const incomingConnections = new Map();
|
|
1407
|
+
connections.forEach(conn => {
|
|
1408
|
+
const sourceId = conn.source.activity;
|
|
1409
|
+
const targetId = conn.target.activity;
|
|
1410
|
+
if (!outgoingConnections.has(sourceId)) {
|
|
1411
|
+
outgoingConnections.set(sourceId, []);
|
|
1412
|
+
}
|
|
1413
|
+
outgoingConnections.get(sourceId).push(conn);
|
|
1414
|
+
if (!incomingConnections.has(targetId)) {
|
|
1415
|
+
incomingConnections.set(targetId, []);
|
|
1416
|
+
}
|
|
1417
|
+
incomingConnections.get(targetId).push(conn);
|
|
1418
|
+
});
|
|
1419
|
+
// Find starting activity (no incoming connections, or first activity)
|
|
1420
|
+
let currentActivityId = localState.currentActivityId;
|
|
1421
|
+
if (!currentActivityId) {
|
|
1422
|
+
// Find root activity (no incoming connections)
|
|
1423
|
+
for (const activity of activities) {
|
|
1424
|
+
if (!incomingConnections.has(activity.id)) {
|
|
1425
|
+
currentActivityId = activity.id;
|
|
1709
1426
|
break;
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
text: `Unknown navigation mode: ${mode}`,
|
|
1716
|
-
},
|
|
1717
|
-
data: {
|
|
1718
|
-
output: undefined,
|
|
1719
|
-
outcomes: {
|
|
1720
|
-
Failed: true,
|
|
1721
|
-
},
|
|
1722
|
-
},
|
|
1723
|
-
};
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
// If no root found, use first activity
|
|
1430
|
+
if (!currentActivityId && activities.length > 0) {
|
|
1431
|
+
currentActivityId = activities[0].id;
|
|
1724
1432
|
}
|
|
1725
|
-
return this.createResult(undefined, 'Done');
|
|
1726
|
-
}
|
|
1727
|
-
catch (err) {
|
|
1728
|
-
console.error('[Navigate] Error navigating:', err);
|
|
1729
|
-
return {
|
|
1730
|
-
success: false,
|
|
1731
|
-
message: {
|
|
1732
|
-
text: err instanceof Error ? err.message : 'Failed to navigate',
|
|
1733
|
-
},
|
|
1734
|
-
data: {
|
|
1735
|
-
output: undefined,
|
|
1736
|
-
outcomes: {
|
|
1737
|
-
Failed: true,
|
|
1738
|
-
},
|
|
1739
|
-
},
|
|
1740
|
-
};
|
|
1741
1433
|
}
|
|
1434
|
+
// Execute workflow steps
|
|
1435
|
+
while (currentActivityId) {
|
|
1436
|
+
const activity = activityMap.get(currentActivityId);
|
|
1437
|
+
if (!activity) {
|
|
1438
|
+
break;
|
|
1439
|
+
}
|
|
1440
|
+
// Skip if already completed
|
|
1441
|
+
if (localState.completedActivities.has(currentActivityId)) {
|
|
1442
|
+
// Move to next activity
|
|
1443
|
+
const nextId = this.getNextActivityId(currentActivityId, outgoingConnections, localState.activityResults);
|
|
1444
|
+
currentActivityId = nextId ?? undefined;
|
|
1445
|
+
continue;
|
|
1446
|
+
}
|
|
1447
|
+
// Determine execution mode
|
|
1448
|
+
const executionMode = activity.executionMode || 'frontend';
|
|
1449
|
+
// Handle backend activities: skip
|
|
1450
|
+
if (executionMode === 'backend') {
|
|
1451
|
+
console.log(`[WorkflowLocalEngine] ⏭️ Skipping backend activity: ${activity.type} (${activity.id})`);
|
|
1452
|
+
localState.completedActivities.add(currentActivityId);
|
|
1453
|
+
localState.activityResults.set(currentActivityId, {
|
|
1454
|
+
output: null,
|
|
1455
|
+
outcome: 'Done',
|
|
1456
|
+
});
|
|
1457
|
+
// Move to next activity
|
|
1458
|
+
const nextId = this.getNextActivityId(currentActivityId, outgoingConnections, localState.activityResults);
|
|
1459
|
+
currentActivityId = nextId ?? undefined;
|
|
1460
|
+
continue;
|
|
1461
|
+
}
|
|
1462
|
+
// Handle frontend/both activities: return as pendingTask (do NOT execute)
|
|
1463
|
+
if (executionMode === 'frontend' || executionMode === 'both') {
|
|
1464
|
+
// Create task for external execution
|
|
1465
|
+
const task = {
|
|
1466
|
+
taskToken: AXPDataGenerator.uuid(),
|
|
1467
|
+
activityId: activity.id,
|
|
1468
|
+
activityType: activity.type,
|
|
1469
|
+
activityName: activity.name ? activity.name : undefined,
|
|
1470
|
+
executionMode: executionMode,
|
|
1471
|
+
input: activity['properties'] || {},
|
|
1472
|
+
config: activity.customProperties || {},
|
|
1473
|
+
};
|
|
1474
|
+
// Store task token for secure resume
|
|
1475
|
+
this.taskTokens.set(task.taskToken, {
|
|
1476
|
+
instanceId: localState.instanceId,
|
|
1477
|
+
activityId: activity.id,
|
|
1478
|
+
});
|
|
1479
|
+
// Update state to indicate suspension
|
|
1480
|
+
localState.currentActivityId = currentActivityId;
|
|
1481
|
+
localState.state.status = 'suspended';
|
|
1482
|
+
localState.state.currentStepId = currentActivityId;
|
|
1483
|
+
// Return task immediately - AXPWorkflowManager will execute it
|
|
1484
|
+
return task;
|
|
1485
|
+
}
|
|
1486
|
+
// Move to next activity
|
|
1487
|
+
const nextId = this.getNextActivityId(currentActivityId, outgoingConnections, localState.activityResults);
|
|
1488
|
+
currentActivityId = nextId ?? undefined;
|
|
1489
|
+
}
|
|
1490
|
+
// Workflow completed
|
|
1491
|
+
localState.state.status = 'completed';
|
|
1492
|
+
localState.state.output = localState.state.variables;
|
|
1493
|
+
localState.currentActivityId = undefined;
|
|
1494
|
+
return null;
|
|
1742
1495
|
}
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
*/
|
|
1762
|
-
class SetVariable extends Activity {
|
|
1763
|
-
constructor() {
|
|
1764
|
-
super('workflow-activity:set-variable', 'Set Variable');
|
|
1765
|
-
}
|
|
1766
|
-
async execute(input) {
|
|
1767
|
-
const { variableName, value } = input;
|
|
1768
|
-
try {
|
|
1769
|
-
// In a real implementation, this would set the variable in workflow context
|
|
1770
|
-
// For now, we'll just log it
|
|
1771
|
-
console.log(`[SetVariable] Setting ${variableName} = ${JSON.stringify(value)}`);
|
|
1772
|
-
return this.createResult(undefined, 'Done');
|
|
1496
|
+
/**
|
|
1497
|
+
* Get next activity ID based on connections and outcomes.
|
|
1498
|
+
*/
|
|
1499
|
+
getNextActivityId(currentActivityId, outgoingConnections, activityResults) {
|
|
1500
|
+
const connections = outgoingConnections.get(currentActivityId) || [];
|
|
1501
|
+
if (connections.length === 0) {
|
|
1502
|
+
return null; // No outgoing connections - workflow ends
|
|
1503
|
+
}
|
|
1504
|
+
// Get current activity result
|
|
1505
|
+
const result = activityResults.get(currentActivityId);
|
|
1506
|
+
const outcome = result?.outcome || 'Done';
|
|
1507
|
+
// Find connection matching outcome
|
|
1508
|
+
// Outcome matching is typically done via port name (e.g., "Done", "Failed")
|
|
1509
|
+
for (const conn of connections) {
|
|
1510
|
+
const sourcePort = conn.source.port || 'Done';
|
|
1511
|
+
if (sourcePort === outcome) {
|
|
1512
|
+
return conn.target.activity;
|
|
1513
|
+
}
|
|
1773
1514
|
}
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
return
|
|
1777
|
-
success: false,
|
|
1778
|
-
message: {
|
|
1779
|
-
text: err instanceof Error ? err.message : 'Failed to set variable',
|
|
1780
|
-
},
|
|
1781
|
-
data: {
|
|
1782
|
-
output: undefined,
|
|
1783
|
-
outcomes: {
|
|
1784
|
-
Failed: true,
|
|
1785
|
-
},
|
|
1786
|
-
},
|
|
1787
|
-
};
|
|
1515
|
+
// If no matching outcome, use first connection (default path)
|
|
1516
|
+
if (connections.length > 0) {
|
|
1517
|
+
return connections[0].target.activity;
|
|
1788
1518
|
}
|
|
1519
|
+
return null;
|
|
1789
1520
|
}
|
|
1521
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.12", ngImport: i0, type: AXPWorkflowLocalEngine, deps: [{ token: AXP_WORKFLOW_DEFINITION_LOADER, optional: true }], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
1522
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.12", ngImport: i0, type: AXPWorkflowLocalEngine }); }
|
|
1790
1523
|
}
|
|
1524
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.12", ngImport: i0, type: AXPWorkflowLocalEngine, decorators: [{
|
|
1525
|
+
type: Injectable
|
|
1526
|
+
}], ctorParameters: () => [{ type: undefined, decorators: [{
|
|
1527
|
+
type: Optional
|
|
1528
|
+
}, {
|
|
1529
|
+
type: Inject,
|
|
1530
|
+
args: [AXP_WORKFLOW_DEFINITION_LOADER]
|
|
1531
|
+
}] }] });
|
|
1532
|
+
|
|
1533
|
+
// Workflow Runtime Services
|
|
1791
1534
|
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1535
|
+
const AXP_WORKFLOW_PROVIDER = new InjectionToken('AXP_WORKFLOW_PROVIDER', {
|
|
1536
|
+
factory: () => [],
|
|
1537
|
+
});
|
|
1538
|
+
const AXP_WORKFLOW_CATEGORY_PROVIDER = new InjectionToken('AXP_WORKFLOW_CATEGORY_PROVIDER', {
|
|
1539
|
+
factory: () => [],
|
|
1795
1540
|
});
|
|
1796
1541
|
|
|
1542
|
+
//#region ---- Imports ----
|
|
1543
|
+
//#endregion
|
|
1797
1544
|
/**
|
|
1798
|
-
*
|
|
1545
|
+
* Optimized Workflow Definition Service
|
|
1799
1546
|
*
|
|
1800
|
-
*
|
|
1801
|
-
*
|
|
1802
|
-
*
|
|
1803
|
-
*
|
|
1804
|
-
*
|
|
1805
|
-
*
|
|
1806
|
-
*
|
|
1807
|
-
*
|
|
1547
|
+
* Manages workflow definitions (metadata) for UI and tooling.
|
|
1548
|
+
* Similar to AXPActivityDefinitionService - only handles metadata, not execution.
|
|
1549
|
+
*
|
|
1550
|
+
* Performance optimizations:
|
|
1551
|
+
* 1. Uses childrenCount to determine if category has children (no query needed)
|
|
1552
|
+
* 2. Uses itemsCount to determine if category has workflows (no query needed)
|
|
1553
|
+
* 3. Aggressive caching prevents duplicate API calls
|
|
1554
|
+
* 4. Single pending request per resource prevents race conditions
|
|
1555
|
+
* 5. Lazy loading - only loads data when needed
|
|
1808
1556
|
*/
|
|
1809
|
-
class
|
|
1557
|
+
class AXPWorkflowDefinitionService {
|
|
1810
1558
|
constructor() {
|
|
1811
|
-
|
|
1559
|
+
//#region ---- Providers & Caches ----
|
|
1560
|
+
this.categoryProviders = inject(AXP_WORKFLOW_CATEGORY_PROVIDER, { optional: true }) || [];
|
|
1561
|
+
this.workflowProviders = inject(AXP_WORKFLOW_PROVIDER, { optional: true }) || [];
|
|
1562
|
+
//#endregion
|
|
1563
|
+
//#region ---- Cache Storage ----
|
|
1564
|
+
/** Cache for categories by id - O(1) lookup */
|
|
1565
|
+
this.categoriesById = new Map();
|
|
1566
|
+
/** Cache for categories by parentId - O(1) lookup */
|
|
1567
|
+
this.categoriesByParentId = new Map();
|
|
1568
|
+
/** Cache for workflow definitions by categoryId - O(1) lookup */
|
|
1569
|
+
this.workflowsByCategory = new Map();
|
|
1570
|
+
/** Cache for individual workflow definitions by name - O(1) lookup */
|
|
1571
|
+
this.workflowsByName = new Map();
|
|
1572
|
+
/** Track which provider index owns each category (by category ID) */
|
|
1573
|
+
this.categoryOwnership = new Map(); // Maps categoryId → provider index
|
|
1574
|
+
/** Pending API requests to prevent duplicate calls */
|
|
1575
|
+
this.pendingCategoriesRequests = new Map();
|
|
1576
|
+
this.pendingWorkflowsRequests = new Map();
|
|
1577
|
+
this.pendingWorkflowRequests = new Map();
|
|
1812
1578
|
}
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1579
|
+
//#endregion
|
|
1580
|
+
//#region ---- Public API: Categories ----
|
|
1581
|
+
/**
|
|
1582
|
+
* Get categories by parentId with aggressive caching
|
|
1583
|
+
*
|
|
1584
|
+
* Optimization: Returns cached result immediately if available,
|
|
1585
|
+
* preventing unnecessary API calls during navigation
|
|
1586
|
+
*
|
|
1587
|
+
* @param parentId - Parent category ID (undefined = root categories)
|
|
1588
|
+
* @returns Array of categories with count metadata (childrenCount, itemsCount)
|
|
1589
|
+
*/
|
|
1590
|
+
async getCategories(parentId) {
|
|
1591
|
+
// ✅ Fast path: Return cached result
|
|
1592
|
+
if (this.categoriesByParentId.has(parentId)) {
|
|
1593
|
+
return this.categoriesByParentId.get(parentId);
|
|
1820
1594
|
}
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
return
|
|
1824
|
-
success: false,
|
|
1825
|
-
message: {
|
|
1826
|
-
text: err instanceof Error ? err.message : 'Failed to dispatch event',
|
|
1827
|
-
},
|
|
1828
|
-
data: {
|
|
1829
|
-
output: undefined,
|
|
1830
|
-
outcomes: {
|
|
1831
|
-
Failed: true,
|
|
1832
|
-
},
|
|
1833
|
-
},
|
|
1834
|
-
};
|
|
1595
|
+
// ✅ Prevent duplicate requests: Return pending promise
|
|
1596
|
+
if (this.pendingCategoriesRequests.has(parentId)) {
|
|
1597
|
+
return this.pendingCategoriesRequests.get(parentId);
|
|
1835
1598
|
}
|
|
1599
|
+
// ✅ Create single request and cache it
|
|
1600
|
+
const requestPromise = this.loadCategoriesFromProviders(parentId);
|
|
1601
|
+
this.pendingCategoriesRequests.set(parentId, requestPromise);
|
|
1602
|
+
return requestPromise;
|
|
1836
1603
|
}
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
*/
|
|
1855
|
-
class If extends Activity {
|
|
1856
|
-
constructor() {
|
|
1857
|
-
super('If');
|
|
1858
|
-
}
|
|
1859
|
-
async execute(input) {
|
|
1860
|
-
const { condition, thenActivities = [], elseActivities = [] } = input;
|
|
1861
|
-
try {
|
|
1862
|
-
// Evaluate condition (simplified - in real implementation, use expression evaluator)
|
|
1863
|
-
const conditionResult = this.evaluateCondition(condition);
|
|
1864
|
-
let result;
|
|
1865
|
-
let activities;
|
|
1866
|
-
if (conditionResult) {
|
|
1867
|
-
activities = thenActivities;
|
|
1868
|
-
result = { branch: 'then' };
|
|
1604
|
+
/**
|
|
1605
|
+
* Get single category by ID with O(1) lookup
|
|
1606
|
+
*
|
|
1607
|
+
* Optimization: Uses Map for instant retrieval, falls back to
|
|
1608
|
+
* searching cache, then providers if not found
|
|
1609
|
+
*/
|
|
1610
|
+
async getCategoryById(categoryId) {
|
|
1611
|
+
// ✅ Fast path: O(1) lookup in cache
|
|
1612
|
+
if (this.categoriesById.has(categoryId)) {
|
|
1613
|
+
return this.categoriesById.get(categoryId);
|
|
1614
|
+
}
|
|
1615
|
+
// ✅ Search in cached parent-child lists
|
|
1616
|
+
for (const categories of this.categoriesByParentId.values()) {
|
|
1617
|
+
const found = categories.find(cat => cat.id === categoryId);
|
|
1618
|
+
if (found) {
|
|
1619
|
+
this.categoriesById.set(categoryId, found);
|
|
1620
|
+
return found;
|
|
1869
1621
|
}
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1622
|
+
}
|
|
1623
|
+
// ✅ Load root categories if not loaded
|
|
1624
|
+
if (!this.categoriesByParentId.has(undefined)) {
|
|
1625
|
+
await this.getCategories();
|
|
1626
|
+
if (this.categoriesById.has(categoryId)) {
|
|
1627
|
+
return this.categoriesById.get(categoryId);
|
|
1873
1628
|
}
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1629
|
+
}
|
|
1630
|
+
// ✅ Breadth-first search through hierarchy
|
|
1631
|
+
return this.searchCategoryInHierarchy(categoryId);
|
|
1632
|
+
}
|
|
1633
|
+
/**
|
|
1634
|
+
* Get category path from root to specified category
|
|
1635
|
+
*
|
|
1636
|
+
* Optimization: Builds path using cached categories only
|
|
1637
|
+
*/
|
|
1638
|
+
async getCategoriesPathById(categoryId) {
|
|
1639
|
+
const path = [];
|
|
1640
|
+
let currentCategoryId = categoryId;
|
|
1641
|
+
while (currentCategoryId) {
|
|
1642
|
+
const category = await this.getCategoryById(currentCategoryId);
|
|
1643
|
+
if (!category) {
|
|
1644
|
+
throw new Error(`Category '${currentCategoryId}' not found`);
|
|
1889
1645
|
}
|
|
1890
|
-
|
|
1646
|
+
path.unshift(category);
|
|
1647
|
+
currentCategoryId = category.parentId;
|
|
1891
1648
|
}
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1649
|
+
return path;
|
|
1650
|
+
}
|
|
1651
|
+
//#endregion
|
|
1652
|
+
//#region ---- Public API: Workflow Definitions ----
|
|
1653
|
+
/**
|
|
1654
|
+
* Get workflow definitions for a category with smart caching
|
|
1655
|
+
*
|
|
1656
|
+
* Optimization: Checks itemsCount before querying
|
|
1657
|
+
* - If itemsCount = 0, returns empty array (no API call)
|
|
1658
|
+
* - If itemsCount > 0, loads and caches workflow definitions
|
|
1659
|
+
* - Returns cached result on subsequent calls
|
|
1660
|
+
*
|
|
1661
|
+
* @param categoryId - Category ID to get workflow definitions from
|
|
1662
|
+
* @returns Array of workflow definition metadata
|
|
1663
|
+
*/
|
|
1664
|
+
async getWorkflowsByCategoryId(categoryId) {
|
|
1665
|
+
// ✅ Fast path: Return cached result
|
|
1666
|
+
if (this.workflowsByCategory.has(categoryId)) {
|
|
1667
|
+
return this.workflowsByCategory.get(categoryId);
|
|
1668
|
+
}
|
|
1669
|
+
// ✅ Smart optimization: Check itemsCount before querying
|
|
1670
|
+
const category = await this.getCategoryById(categoryId);
|
|
1671
|
+
if (category && category.itemsCount !== undefined && category.itemsCount === 0) {
|
|
1672
|
+
// Category has no workflows - cache empty array and skip API call
|
|
1673
|
+
const emptyArray = [];
|
|
1674
|
+
this.workflowsByCategory.set(categoryId, emptyArray);
|
|
1675
|
+
return emptyArray;
|
|
1676
|
+
}
|
|
1677
|
+
// ✅ Prevent duplicate requests
|
|
1678
|
+
if (this.pendingWorkflowsRequests.has(categoryId)) {
|
|
1679
|
+
return this.pendingWorkflowsRequests.get(categoryId);
|
|
1680
|
+
}
|
|
1681
|
+
// ✅ Load from providers
|
|
1682
|
+
const requestPromise = this.loadWorkflowsFromProviders(categoryId);
|
|
1683
|
+
this.pendingWorkflowsRequests.set(categoryId, requestPromise);
|
|
1684
|
+
return requestPromise;
|
|
1685
|
+
}
|
|
1686
|
+
/**
|
|
1687
|
+
* Get single workflow definition metadata by name with O(1) lookup
|
|
1688
|
+
*
|
|
1689
|
+
* Optimization: Uses Map for instant retrieval
|
|
1690
|
+
*
|
|
1691
|
+
* @param name - Workflow name (unique identifier)
|
|
1692
|
+
* @returns Workflow definition metadata or undefined if not found
|
|
1693
|
+
*/
|
|
1694
|
+
async getWorkflowByName(name) {
|
|
1695
|
+
// ✅ Fast path: O(1) lookup in cache
|
|
1696
|
+
if (this.workflowsByName.has(name)) {
|
|
1697
|
+
return this.workflowsByName.get(name);
|
|
1906
1698
|
}
|
|
1699
|
+
// ✅ Prevent duplicate requests
|
|
1700
|
+
if (this.pendingWorkflowRequests.has(name)) {
|
|
1701
|
+
return this.pendingWorkflowRequests.get(name);
|
|
1702
|
+
}
|
|
1703
|
+
// ✅ Load from providers
|
|
1704
|
+
const requestPromise = this.loadWorkflowFromProviders(name);
|
|
1705
|
+
this.pendingWorkflowRequests.set(name, requestPromise);
|
|
1706
|
+
return requestPromise;
|
|
1907
1707
|
}
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1708
|
+
/**
|
|
1709
|
+
* Get category ID containing a specific workflow definition
|
|
1710
|
+
*
|
|
1711
|
+
* Optimization: Searches cache first, loads on-demand if needed
|
|
1712
|
+
*/
|
|
1713
|
+
async getCategoryIdByWorkflowName(workflowName) {
|
|
1714
|
+
// ✅ Search in cached workflow definitions
|
|
1715
|
+
for (const [categoryId, definitions] of this.workflowsByCategory.entries()) {
|
|
1716
|
+
if (definitions.some(def => def.name === workflowName)) {
|
|
1717
|
+
return categoryId;
|
|
1718
|
+
}
|
|
1911
1719
|
}
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1720
|
+
// ✅ Try loading the workflow definition to find its category
|
|
1721
|
+
const definition = await this.getWorkflowByName(workflowName);
|
|
1722
|
+
if (definition && definition.category) {
|
|
1723
|
+
// Try to find category by name/id
|
|
1724
|
+
const categories = await this.getCategories();
|
|
1725
|
+
const found = categories.find(cat => cat.id === definition.category || cat.title === definition.category);
|
|
1726
|
+
if (found) {
|
|
1727
|
+
return found.id;
|
|
1728
|
+
}
|
|
1915
1729
|
}
|
|
1916
|
-
return
|
|
1730
|
+
return undefined;
|
|
1917
1731
|
}
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
*
|
|
1928
|
-
* Usage:
|
|
1929
|
-
* ```typescript
|
|
1930
|
-
* const whileActivity = new While();
|
|
1931
|
-
* whileActivity.condition = '{{counter < 10}}';
|
|
1932
|
-
* whileActivity.activities = [incrementActivity, logActivity];
|
|
1933
|
-
* ```
|
|
1934
|
-
*/
|
|
1935
|
-
class While extends Activity {
|
|
1936
|
-
constructor() {
|
|
1937
|
-
super('While');
|
|
1732
|
+
/**
|
|
1733
|
+
* Get category path for a workflow
|
|
1734
|
+
*/
|
|
1735
|
+
async getCategoriesPathByWorkflowName(workflowName) {
|
|
1736
|
+
const categoryId = await this.getCategoryIdByWorkflowName(workflowName);
|
|
1737
|
+
if (!categoryId) {
|
|
1738
|
+
throw new Error(`Workflow '${workflowName}' not found in any category`);
|
|
1739
|
+
}
|
|
1740
|
+
return this.getCategoriesPathById(categoryId);
|
|
1938
1741
|
}
|
|
1939
|
-
|
|
1940
|
-
|
|
1742
|
+
//#endregion
|
|
1743
|
+
//#region ---- Private: Data Loading ----
|
|
1744
|
+
/**
|
|
1745
|
+
* Load categories from providers and cache results
|
|
1746
|
+
*
|
|
1747
|
+
* Optimization: Tracks provider ownership to avoid unnecessary API calls
|
|
1748
|
+
* - For root (parentId = undefined): Query ALL providers
|
|
1749
|
+
* - For children: Only query the provider that owns the parent
|
|
1750
|
+
*/
|
|
1751
|
+
async loadCategoriesFromProviders(parentId) {
|
|
1941
1752
|
try {
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
}
|
|
1753
|
+
const resolvedProviders = await Promise.allSettled(this.categoryProviders);
|
|
1754
|
+
const categories = [];
|
|
1755
|
+
// Determine which provider(s) to query
|
|
1756
|
+
const providerIndicesToQuery = parentId
|
|
1757
|
+
? this.getProviderIndexForCategory(parentId)
|
|
1758
|
+
: null; // Root: query all providers
|
|
1759
|
+
for (let i = 0; i < resolvedProviders.length; i++) {
|
|
1760
|
+
const p = resolvedProviders[i];
|
|
1761
|
+
// Skip if we have a specific provider index and this isn't it
|
|
1762
|
+
if (providerIndicesToQuery !== null && !providerIndicesToQuery.includes(i)) {
|
|
1763
|
+
continue;
|
|
1764
|
+
}
|
|
1765
|
+
if (p.status === 'fulfilled' && p.value && typeof p.value.getList === 'function') {
|
|
1766
|
+
try {
|
|
1767
|
+
const cats = await p.value.getList(parentId);
|
|
1768
|
+
if (Array.isArray(cats) && cats.length > 0) {
|
|
1769
|
+
categories.push(...cats);
|
|
1770
|
+
// ✅ Track ownership: This provider INDEX owns these categories
|
|
1771
|
+
cats.forEach(cat => this.categoryOwnership.set(cat.id, i));
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1774
|
+
catch {
|
|
1775
|
+
// Continue on error - try other providers
|
|
1962
1776
|
}
|
|
1963
1777
|
}
|
|
1964
|
-
iteration++;
|
|
1965
|
-
conditionResult = this.evaluateCondition(condition);
|
|
1966
1778
|
}
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
return this.createResult(result, 'Done');
|
|
1779
|
+
// ✅ Cache results for fast subsequent access
|
|
1780
|
+
this.categoriesByParentId.set(parentId, categories);
|
|
1781
|
+
categories.forEach(cat => this.categoriesById.set(cat.id, cat));
|
|
1782
|
+
return categories;
|
|
1972
1783
|
}
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
return {
|
|
1976
|
-
success: false,
|
|
1977
|
-
message: {
|
|
1978
|
-
text: err instanceof Error ? err.message : 'Failed during loop execution',
|
|
1979
|
-
},
|
|
1980
|
-
data: {
|
|
1981
|
-
output: { iterations: 0, completed: false },
|
|
1982
|
-
outcomes: {
|
|
1983
|
-
Failed: true,
|
|
1984
|
-
},
|
|
1985
|
-
},
|
|
1986
|
-
};
|
|
1784
|
+
finally {
|
|
1785
|
+
this.pendingCategoriesRequests.delete(parentId);
|
|
1987
1786
|
}
|
|
1988
1787
|
}
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1788
|
+
/**
|
|
1789
|
+
* Get the provider index that owns a specific category
|
|
1790
|
+
*
|
|
1791
|
+
* @returns Array with provider index, or null if ownership unknown (query all)
|
|
1792
|
+
*/
|
|
1793
|
+
getProviderIndexForCategory(categoryId) {
|
|
1794
|
+
const ownerIndex = this.categoryOwnership.get(categoryId);
|
|
1795
|
+
if (ownerIndex !== undefined) {
|
|
1796
|
+
return [ownerIndex];
|
|
1996
1797
|
}
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
}
|
|
2000
|
-
|
|
2001
|
-
var while_activity = /*#__PURE__*/Object.freeze({
|
|
2002
|
-
__proto__: null,
|
|
2003
|
-
While: While
|
|
2004
|
-
});
|
|
2005
|
-
|
|
2006
|
-
/**
|
|
2007
|
-
* ForEach Activity - Iterates over a collection of items.
|
|
2008
|
-
*
|
|
2009
|
-
* Usage:
|
|
2010
|
-
* ```typescript
|
|
2011
|
-
* const forEach = new ForEach();
|
|
2012
|
-
* await forEach.execute({
|
|
2013
|
-
* items: ['item1', 'item2', 'item3'],
|
|
2014
|
-
* activities: [processItemActivity]
|
|
2015
|
-
* });
|
|
2016
|
-
* ```
|
|
2017
|
-
*/
|
|
2018
|
-
class ForEach extends Activity {
|
|
2019
|
-
constructor() {
|
|
2020
|
-
super('ForEach');
|
|
1798
|
+
// Ownership unknown - will query all providers (fallback)
|
|
1799
|
+
return null;
|
|
2021
1800
|
}
|
|
2022
|
-
|
|
2023
|
-
|
|
1801
|
+
/**
|
|
1802
|
+
* Load workflow definitions from providers and cache results
|
|
1803
|
+
*
|
|
1804
|
+
* Optimization: Only queries the provider that owns the category
|
|
1805
|
+
* Uses provider INDEX to match category provider with workflow provider
|
|
1806
|
+
*/
|
|
1807
|
+
async loadWorkflowsFromProviders(categoryId) {
|
|
2024
1808
|
try {
|
|
2025
|
-
const
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
results,
|
|
2046
|
-
},
|
|
2047
|
-
outcomes: {
|
|
2048
|
-
Failed: true,
|
|
2049
|
-
},
|
|
2050
|
-
},
|
|
2051
|
-
};
|
|
1809
|
+
const resolvedProviders = await Promise.allSettled(this.workflowProviders);
|
|
1810
|
+
const definitions = [];
|
|
1811
|
+
// ✅ Smart routing: Get provider INDEX that owns this category
|
|
1812
|
+
const ownerIndex = this.categoryOwnership.get(categoryId);
|
|
1813
|
+
const providerIndicesToQuery = ownerIndex !== undefined ? [ownerIndex] : null;
|
|
1814
|
+
for (let i = 0; i < resolvedProviders.length; i++) {
|
|
1815
|
+
const p = resolvedProviders[i];
|
|
1816
|
+
// Skip if we have a specific provider index and this isn't it
|
|
1817
|
+
if (providerIndicesToQuery !== null && !providerIndicesToQuery.includes(i)) {
|
|
1818
|
+
continue;
|
|
1819
|
+
}
|
|
1820
|
+
if (p.status === 'fulfilled' && p.value && typeof p.value.getList === 'function') {
|
|
1821
|
+
try {
|
|
1822
|
+
const defs = await p.value.getList(categoryId);
|
|
1823
|
+
if (Array.isArray(defs)) {
|
|
1824
|
+
definitions.push(...defs);
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
catch {
|
|
1828
|
+
// Continue on error - try other providers
|
|
2052
1829
|
}
|
|
2053
1830
|
}
|
|
2054
|
-
results.push({
|
|
2055
|
-
item: currentItem,
|
|
2056
|
-
index,
|
|
2057
|
-
processed: true
|
|
2058
|
-
});
|
|
2059
1831
|
}
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
1832
|
+
// ✅ Cache results for fast subsequent access
|
|
1833
|
+
this.workflowsByCategory.set(categoryId, definitions);
|
|
1834
|
+
definitions.forEach(def => {
|
|
1835
|
+
if (def.name) {
|
|
1836
|
+
this.workflowsByName.set(def.name, def);
|
|
1837
|
+
}
|
|
1838
|
+
});
|
|
1839
|
+
return definitions;
|
|
2066
1840
|
}
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
return {
|
|
2070
|
-
success: false,
|
|
2071
|
-
message: {
|
|
2072
|
-
text: err instanceof Error ? err.message : 'Failed during iteration',
|
|
2073
|
-
},
|
|
2074
|
-
data: {
|
|
2075
|
-
output: { totalItems: 0, processedItems: 0, results: [] },
|
|
2076
|
-
outcomes: {
|
|
2077
|
-
Failed: true,
|
|
2078
|
-
},
|
|
2079
|
-
},
|
|
2080
|
-
};
|
|
1841
|
+
finally {
|
|
1842
|
+
this.pendingWorkflowsRequests.delete(categoryId);
|
|
2081
1843
|
}
|
|
2082
1844
|
}
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
ForEach: ForEach
|
|
2088
|
-
});
|
|
2089
|
-
|
|
2090
|
-
/**
|
|
2091
|
-
* Execute Command Activity - Executes a command through Command Bus.
|
|
2092
|
-
*
|
|
2093
|
-
* Usage:
|
|
2094
|
-
* ```typescript
|
|
2095
|
-
* const executeCmd = new ExecuteCommand();
|
|
2096
|
-
* await executeCmd.execute({
|
|
2097
|
-
* commandKey: 'UserManagement.CreateUser',
|
|
2098
|
-
* input: { name: 'John', email: 'john@example.com' }
|
|
2099
|
-
* });
|
|
2100
|
-
* ```
|
|
2101
|
-
*/
|
|
2102
|
-
class ExecuteCommand extends Activity {
|
|
2103
|
-
constructor() {
|
|
2104
|
-
super('workflow-activity:execute-command', 'Execute Command');
|
|
2105
|
-
this.commandService = inject(AXPCommandService);
|
|
2106
|
-
}
|
|
2107
|
-
async execute(input) {
|
|
2108
|
-
const { commandKey, input: commandInput = {} } = input;
|
|
1845
|
+
/**
|
|
1846
|
+
* Load single workflow definition from providers and cache result
|
|
1847
|
+
*/
|
|
1848
|
+
async loadWorkflowFromProviders(name) {
|
|
2109
1849
|
try {
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
if (!result) {
|
|
2126
|
-
return {
|
|
2127
|
-
success: false,
|
|
2128
|
-
message: {
|
|
2129
|
-
text: `Command '${commandKey}' returned no result`,
|
|
2130
|
-
},
|
|
2131
|
-
data: {
|
|
2132
|
-
output: {
|
|
2133
|
-
commandKey,
|
|
2134
|
-
success: false,
|
|
2135
|
-
executedAt: new Date().toISOString(),
|
|
2136
|
-
},
|
|
2137
|
-
outcomes: {
|
|
2138
|
-
Failed: true,
|
|
2139
|
-
},
|
|
2140
|
-
},
|
|
2141
|
-
};
|
|
1850
|
+
const resolvedProviders = await Promise.allSettled(this.workflowProviders);
|
|
1851
|
+
// Try providers first
|
|
1852
|
+
for (const p of resolvedProviders) {
|
|
1853
|
+
if (p.status === 'fulfilled' && p.value && typeof p.value.getById === 'function') {
|
|
1854
|
+
try {
|
|
1855
|
+
const result = await p.value.getById(name);
|
|
1856
|
+
if (result) {
|
|
1857
|
+
this.workflowsByName.set(name, result);
|
|
1858
|
+
return result;
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
catch {
|
|
1862
|
+
// Continue on error
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
2142
1865
|
}
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
success: false,
|
|
2151
|
-
executedAt: new Date().toISOString(),
|
|
2152
|
-
error: result.message?.text,
|
|
2153
|
-
},
|
|
2154
|
-
outcomes: {
|
|
2155
|
-
Failed: true,
|
|
2156
|
-
},
|
|
2157
|
-
},
|
|
2158
|
-
};
|
|
1866
|
+
// Fallback: Search in cached workflow definitions
|
|
1867
|
+
for (const definitions of this.workflowsByCategory.values()) {
|
|
1868
|
+
const found = definitions.find(def => def.name === name);
|
|
1869
|
+
if (found) {
|
|
1870
|
+
this.workflowsByName.set(name, found);
|
|
1871
|
+
return found;
|
|
1872
|
+
}
|
|
2159
1873
|
}
|
|
2160
|
-
return
|
|
2161
|
-
commandKey,
|
|
2162
|
-
success: true,
|
|
2163
|
-
output: result.data,
|
|
2164
|
-
executedAt: new Date().toISOString(),
|
|
2165
|
-
}, 'Done');
|
|
1874
|
+
return undefined;
|
|
2166
1875
|
}
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
return {
|
|
2170
|
-
success: false,
|
|
2171
|
-
message: {
|
|
2172
|
-
text: err instanceof Error ? err.message : 'Unknown error',
|
|
2173
|
-
},
|
|
2174
|
-
data: {
|
|
2175
|
-
output: {
|
|
2176
|
-
commandKey,
|
|
2177
|
-
success: false,
|
|
2178
|
-
error: err instanceof Error ? err.message : 'Unknown error',
|
|
2179
|
-
},
|
|
2180
|
-
outcomes: {
|
|
2181
|
-
Failed: true,
|
|
2182
|
-
},
|
|
2183
|
-
},
|
|
2184
|
-
};
|
|
1876
|
+
finally {
|
|
1877
|
+
this.pendingWorkflowRequests.delete(name);
|
|
2185
1878
|
}
|
|
2186
1879
|
}
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
super('workflow-activity:execute-query', 'Execute Query');
|
|
2209
|
-
this.queryService = inject(AXPQueryService);
|
|
2210
|
-
}
|
|
2211
|
-
async execute(input) {
|
|
2212
|
-
const { queryKey, input: queryInput = {} } = input;
|
|
2213
|
-
try {
|
|
2214
|
-
// Check if query exists
|
|
2215
|
-
if (!this.queryService.exists(queryKey)) {
|
|
2216
|
-
console.warn(`[ExecuteQuery] Query '${queryKey}' is not registered. Simulating execution.`);
|
|
2217
|
-
// Simulate query execution for unregistered queries
|
|
2218
|
-
const result = {
|
|
2219
|
-
queryKey,
|
|
2220
|
-
success: true,
|
|
2221
|
-
data: [],
|
|
2222
|
-
totalCount: 0,
|
|
2223
|
-
executedAt: new Date().toISOString(),
|
|
2224
|
-
simulated: true
|
|
2225
|
-
};
|
|
2226
|
-
return this.createResult(result, 'Done');
|
|
1880
|
+
/**
|
|
1881
|
+
* Breadth-first search through category hierarchy
|
|
1882
|
+
*/
|
|
1883
|
+
async searchCategoryInHierarchy(categoryId) {
|
|
1884
|
+
const searchQueue = [undefined];
|
|
1885
|
+
const searched = new Set();
|
|
1886
|
+
while (searchQueue.length > 0) {
|
|
1887
|
+
const parentId = searchQueue.shift();
|
|
1888
|
+
if (searched.has(parentId))
|
|
1889
|
+
continue;
|
|
1890
|
+
searched.add(parentId);
|
|
1891
|
+
const categories = await this.getCategories(parentId);
|
|
1892
|
+
const found = categories.find(cat => cat.id === categoryId);
|
|
1893
|
+
if (found) {
|
|
1894
|
+
return found;
|
|
1895
|
+
}
|
|
1896
|
+
// ✅ Optimization: Only search children if childrenCount > 0
|
|
1897
|
+
for (const category of categories) {
|
|
1898
|
+
if (category.childrenCount > 0 && !searched.has(category.id)) {
|
|
1899
|
+
searchQueue.push(category.id);
|
|
1900
|
+
}
|
|
2227
1901
|
}
|
|
2228
|
-
// Execute query through Query Bus
|
|
2229
|
-
const result = await this.queryService.fetch(queryKey, queryInput);
|
|
2230
|
-
return this.createResult({
|
|
2231
|
-
queryKey,
|
|
2232
|
-
success: true,
|
|
2233
|
-
data: result,
|
|
2234
|
-
totalCount: Array.isArray(result) ? result.length : (result?.totalCount || 0),
|
|
2235
|
-
executedAt: new Date().toISOString()
|
|
2236
|
-
}, 'Done');
|
|
2237
|
-
}
|
|
2238
|
-
catch (err) {
|
|
2239
|
-
console.error('[ExecuteQuery] Error executing query:', err);
|
|
2240
|
-
return {
|
|
2241
|
-
success: false,
|
|
2242
|
-
message: {
|
|
2243
|
-
text: err instanceof Error ? err.message : 'Unknown error',
|
|
2244
|
-
},
|
|
2245
|
-
data: {
|
|
2246
|
-
output: {
|
|
2247
|
-
queryKey,
|
|
2248
|
-
success: false,
|
|
2249
|
-
error: err instanceof Error ? err.message : 'Unknown error',
|
|
2250
|
-
},
|
|
2251
|
-
outcomes: {
|
|
2252
|
-
Failed: true,
|
|
2253
|
-
},
|
|
2254
|
-
},
|
|
2255
|
-
};
|
|
2256
1902
|
}
|
|
1903
|
+
return undefined;
|
|
2257
1904
|
}
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
* Start Activity - Marks the start point of a workflow.
|
|
2267
|
-
*
|
|
2268
|
-
* This is a visual marker activity that doesn't perform any actual work.
|
|
2269
|
-
* It's used in workflow designers to clearly indicate where a workflow begins.
|
|
2270
|
-
*
|
|
2271
|
-
* Usage:
|
|
2272
|
-
* ```typescript
|
|
2273
|
-
* const start = new StartActivity();
|
|
2274
|
-
* await start.execute({});
|
|
2275
|
-
* ```
|
|
2276
|
-
*/
|
|
2277
|
-
class StartActivity extends Activity {
|
|
2278
|
-
constructor() {
|
|
2279
|
-
super('StartActivity');
|
|
1905
|
+
//#endregion
|
|
1906
|
+
//#region ---- Cache Management ----
|
|
1907
|
+
/**
|
|
1908
|
+
* Check if category has children (uses cached count)
|
|
1909
|
+
*/
|
|
1910
|
+
categoryHasChildren(categoryId) {
|
|
1911
|
+
const category = this.categoriesById.get(categoryId);
|
|
1912
|
+
return category ? category.childrenCount > 0 : false;
|
|
2280
1913
|
}
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
1914
|
+
/**
|
|
1915
|
+
* Check if category has workflows (uses cached count)
|
|
1916
|
+
*/
|
|
1917
|
+
categoryHasWorkflows(categoryId) {
|
|
1918
|
+
const category = this.categoriesById.get(categoryId);
|
|
1919
|
+
return category ? (category.itemsCount ?? 0) > 0 : false;
|
|
2286
1920
|
}
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
*
|
|
2300
|
-
* Usage:
|
|
2301
|
-
* ```typescript
|
|
2302
|
-
* const end = new EndActivity();
|
|
2303
|
-
* await end.execute({});
|
|
2304
|
-
* ```
|
|
2305
|
-
*/
|
|
2306
|
-
class EndActivity extends Activity {
|
|
2307
|
-
constructor() {
|
|
2308
|
-
super('EndActivity');
|
|
1921
|
+
/**
|
|
1922
|
+
* Clear all caches
|
|
1923
|
+
*/
|
|
1924
|
+
clearAllCache() {
|
|
1925
|
+
this.categoriesById.clear();
|
|
1926
|
+
this.categoriesByParentId.clear();
|
|
1927
|
+
this.workflowsByCategory.clear();
|
|
1928
|
+
this.workflowsByName.clear();
|
|
1929
|
+
this.categoryOwnership.clear();
|
|
1930
|
+
this.pendingCategoriesRequests.clear();
|
|
1931
|
+
this.pendingWorkflowsRequests.clear();
|
|
1932
|
+
this.pendingWorkflowRequests.clear();
|
|
2309
1933
|
}
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
1934
|
+
/**
|
|
1935
|
+
* Clear categories cache only
|
|
1936
|
+
*/
|
|
1937
|
+
clearCategoriesCache() {
|
|
1938
|
+
this.categoriesById.clear();
|
|
1939
|
+
this.categoriesByParentId.clear();
|
|
1940
|
+
this.categoryOwnership.clear();
|
|
1941
|
+
this.pendingCategoriesRequests.clear();
|
|
2314
1942
|
}
|
|
1943
|
+
/**
|
|
1944
|
+
* Clear workflows cache only
|
|
1945
|
+
*/
|
|
1946
|
+
clearWorkflowsCache() {
|
|
1947
|
+
this.workflowsByCategory.clear();
|
|
1948
|
+
this.workflowsByName.clear();
|
|
1949
|
+
this.pendingWorkflowsRequests.clear();
|
|
1950
|
+
this.pendingWorkflowRequests.clear();
|
|
1951
|
+
}
|
|
1952
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.12", ngImport: i0, type: AXPWorkflowDefinitionService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
1953
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.12", ngImport: i0, type: AXPWorkflowDefinitionService, providedIn: 'root' }); }
|
|
2315
1954
|
}
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
}
|
|
2321
|
-
|
|
2322
|
-
/**
|
|
2323
|
-
* Command setups for all built-in workflow activities.
|
|
2324
|
-
* Registers activities as AXPCommand instances.
|
|
2325
|
-
*/
|
|
2326
|
-
const provideWorkflowActivityCommands = () => provideCommandSetups([
|
|
2327
|
-
// Workflow Markers
|
|
2328
|
-
{
|
|
2329
|
-
key: 'workflow-activity:start',
|
|
2330
|
-
command: () => Promise.resolve().then(function () { return startActivity_activity; }).then(m => m.StartActivity),
|
|
2331
|
-
},
|
|
2332
|
-
{
|
|
2333
|
-
key: 'workflow-activity:end',
|
|
2334
|
-
command: () => Promise.resolve().then(function () { return endActivity_activity; }).then(m => m.EndActivity),
|
|
2335
|
-
},
|
|
2336
|
-
// Console Activities
|
|
2337
|
-
{
|
|
2338
|
-
key: 'workflow-activity:write-line',
|
|
2339
|
-
command: () => Promise.resolve().then(function () { return writeLine_activity; }).then(m => m.WriteLine),
|
|
2340
|
-
},
|
|
2341
|
-
// Control Flow Activities
|
|
2342
|
-
{
|
|
2343
|
-
key: 'workflow-activity:sequence',
|
|
2344
|
-
command: () => Promise.resolve().then(function () { return sequence_activity; }).then(m => m.Sequence),
|
|
2345
|
-
},
|
|
2346
|
-
{
|
|
2347
|
-
key: 'workflow-activity:if',
|
|
2348
|
-
command: () => Promise.resolve().then(function () { return if_activity; }).then(m => m.If),
|
|
2349
|
-
},
|
|
2350
|
-
{
|
|
2351
|
-
key: 'workflow-activity:while',
|
|
2352
|
-
command: () => Promise.resolve().then(function () { return while_activity; }).then(m => m.While),
|
|
2353
|
-
},
|
|
2354
|
-
{
|
|
2355
|
-
key: 'workflow-activity:for-each',
|
|
2356
|
-
command: () => Promise.resolve().then(function () { return forEach_activity; }).then(m => m.ForEach),
|
|
2357
|
-
},
|
|
2358
|
-
// Dialog Activities
|
|
2359
|
-
{
|
|
2360
|
-
key: 'workflow-activity:show-confirm-dialog',
|
|
2361
|
-
command: () => Promise.resolve().then(function () { return showConfirmDialog_activity; }).then(m => m.ShowConfirmDialog),
|
|
2362
|
-
},
|
|
2363
|
-
{
|
|
2364
|
-
key: 'workflow-activity:show-alert-dialog',
|
|
2365
|
-
command: () => Promise.resolve().then(function () { return showAlertDialog_activity; }).then(m => m.ShowAlertDialog),
|
|
2366
|
-
},
|
|
2367
|
-
// Notification Activities
|
|
2368
|
-
{
|
|
2369
|
-
key: 'workflow-activity:show-toast',
|
|
2370
|
-
command: () => Promise.resolve().then(function () { return showToast_activity; }).then(m => m.ShowToast),
|
|
2371
|
-
},
|
|
2372
|
-
// Event Activities
|
|
2373
|
-
{
|
|
2374
|
-
key: 'workflow-activity:dispatch-event',
|
|
2375
|
-
command: () => Promise.resolve().then(function () { return dispatchEvent_activity; }).then(m => m.DispatchEvent),
|
|
2376
|
-
},
|
|
2377
|
-
// Variable Activities
|
|
2378
|
-
{
|
|
2379
|
-
key: 'workflow-activity:set-variable',
|
|
2380
|
-
command: () => Promise.resolve().then(function () { return setVariable_activity; }).then(m => m.SetVariable),
|
|
2381
|
-
},
|
|
2382
|
-
// Command & Query Activities
|
|
2383
|
-
{
|
|
2384
|
-
key: 'workflow-activity:execute-command',
|
|
2385
|
-
command: () => Promise.resolve().then(function () { return executeCommand_activity; }).then(m => m.ExecuteCommand),
|
|
2386
|
-
},
|
|
2387
|
-
{
|
|
2388
|
-
key: 'workflow-activity:execute-query',
|
|
2389
|
-
command: () => Promise.resolve().then(function () { return executeQuery_activity; }).then(m => m.ExecuteQuery),
|
|
2390
|
-
},
|
|
2391
|
-
// Navigation Activities
|
|
2392
|
-
{
|
|
2393
|
-
key: 'workflow-activity:navigate',
|
|
2394
|
-
command: () => Promise.resolve().then(function () { return navigate_activity; }).then(m => m.Navigate),
|
|
2395
|
-
},
|
|
2396
|
-
]);
|
|
2397
|
-
|
|
2398
|
-
// Built-in Activities
|
|
1955
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.12", ngImport: i0, type: AXPWorkflowDefinitionService, decorators: [{
|
|
1956
|
+
type: Injectable,
|
|
1957
|
+
args: [{
|
|
1958
|
+
providedIn: 'root',
|
|
1959
|
+
}]
|
|
1960
|
+
}] });
|
|
2399
1961
|
|
|
2400
1962
|
// Workflow Definition Types (Storage/Database)
|
|
2401
1963
|
|
|
@@ -2404,7 +1966,6 @@ class AXPWorkflowModule {
|
|
|
2404
1966
|
return {
|
|
2405
1967
|
ngModule: AXPWorkflowModule,
|
|
2406
1968
|
providers: [
|
|
2407
|
-
provideWorkflowActivityCommands(),
|
|
2408
1969
|
{
|
|
2409
1970
|
provide: 'AXPWorkflowModuleFactory',
|
|
2410
1971
|
useFactory: (registry) => () => {
|
|
@@ -2479,8 +2040,6 @@ class AXPWorkflowModule {
|
|
|
2479
2040
|
* @ignore
|
|
2480
2041
|
*/
|
|
2481
2042
|
constructor(instances) {
|
|
2482
|
-
// Inject AXP_COMMAND_SETUP to trigger command registration factory
|
|
2483
|
-
this._commandSetup = inject(AXP_COMMAND_SETUP, { optional: true });
|
|
2484
2043
|
instances?.forEach((f) => {
|
|
2485
2044
|
f();
|
|
2486
2045
|
});
|
|
@@ -2488,7 +2047,12 @@ class AXPWorkflowModule {
|
|
|
2488
2047
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.12", ngImport: i0, type: AXPWorkflowModule, deps: [{ token: 'AXPWorkflowModuleFactory', optional: true }], target: i0.ɵɵFactoryTarget.NgModule }); }
|
|
2489
2048
|
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "20.3.12", ngImport: i0, type: AXPWorkflowModule }); }
|
|
2490
2049
|
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "20.3.12", ngImport: i0, type: AXPWorkflowModule, providers: [
|
|
2491
|
-
|
|
2050
|
+
AXPWorkflowLocalEngine,
|
|
2051
|
+
{
|
|
2052
|
+
provide: AXP_WORKFLOW_ENGINE,
|
|
2053
|
+
useExisting: AXPWorkflowLocalEngine,
|
|
2054
|
+
},
|
|
2055
|
+
AXPWorkflowManager,
|
|
2492
2056
|
] }); }
|
|
2493
2057
|
}
|
|
2494
2058
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.12", ngImport: i0, type: AXPWorkflowModule, decorators: [{
|
|
@@ -2498,7 +2062,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.12", ngImpo
|
|
|
2498
2062
|
exports: [],
|
|
2499
2063
|
declarations: [],
|
|
2500
2064
|
providers: [
|
|
2501
|
-
|
|
2065
|
+
AXPWorkflowLocalEngine,
|
|
2066
|
+
{
|
|
2067
|
+
provide: AXP_WORKFLOW_ENGINE,
|
|
2068
|
+
useExisting: AXPWorkflowLocalEngine,
|
|
2069
|
+
},
|
|
2070
|
+
AXPWorkflowManager,
|
|
2502
2071
|
],
|
|
2503
2072
|
}]
|
|
2504
2073
|
}], ctorParameters: () => [{ type: undefined, decorators: [{
|
|
@@ -2512,5 +2081,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.12", ngImpo
|
|
|
2512
2081
|
* Generated bundle index. Do not edit.
|
|
2513
2082
|
*/
|
|
2514
2083
|
|
|
2515
|
-
export {
|
|
2084
|
+
export { AXPActivityDefinitionService, AXPWorkflowAction, AXPWorkflowContext, AXPWorkflowDefinitionService, AXPWorkflowError, AXPWorkflowEventService, AXPWorkflowFunction, AXPWorkflowLocalEngine, AXPWorkflowManager, AXPWorkflowModule, AXPWorkflowRegistryService, AXPWorkflowService, AXP_ACTIVITY_CATEGORY_PROVIDER, AXP_ACTIVITY_PROVIDER, AXP_WORKFLOW_CATEGORY_PROVIDER, AXP_WORKFLOW_DEFINITION_LOADER, AXP_WORKFLOW_ENGINE, AXP_WORKFLOW_PROVIDER, FrontendTaskExecutor, createWorkFlowEvent, ofType };
|
|
2516
2085
|
//# sourceMappingURL=acorex-platform-workflow.mjs.map
|