From 59c7d68299dda47c417077124183b4bd4b98bbfd Mon Sep 17 00:00:00 2001
From: Thomas Hunter <thomas.hunter@alfresco.com>
Date: Thu, 6 Oct 2022 14:07:47 +0100
Subject: [PATCH] [ACA-3258] Edit rule dialog actions section (#2692)

* Add actions service

* Create components

* Rebasing

* Add card view component

* Moved actions definition call outside of actions list component

* Localisation of parameter and action labels + read only mode for components

* Remove action option

* Default to one item in array

* Handle change of cardview

* Linting

* Add unit tests

* Fix broken unit tests

* Fix unknown word

* Add private to property
---
 projects/aca-folder-rules/assets/i18n/en.json |  14 +-
 .../src/lib/folder-rules.module.ts            |   4 +
 .../manage-rules.smart-component.html         |  11 +-
 .../manage-rules.smart-component.scss         |   2 +-
 .../manage-rules.smart-component.spec.ts      |  10 +-
 .../manage-rules.smart-component.ts           |  16 +-
 .../src/lib/mock/actions.mock.ts              | 108 +++++++++++++
 .../src/lib/model/rule-action.model.ts        |  22 ++-
 .../rule-action-list.ui-component.html        |  31 ++++
 .../rule-action-list.ui-component.scss        |  15 ++
 .../rule-action-list.ui-component.spec.ts     | 104 ++++++++++++
 .../actions/rule-action-list.ui-component.ts  |  77 +++++++++
 .../actions/rule-action.ui-component.html     |  22 +++
 .../actions/rule-action.ui-component.scss     |   7 +
 .../actions/rule-action.ui-component.spec.ts  |  87 ++++++++++
 .../actions/rule-action.ui-component.ts       | 153 ++++++++++++++++++
 ...rule-composite-condition.ui-component.html |   8 +-
 .../edit-rule-dialog.smart-component.html     |  16 +-
 .../edit-rule-dialog.smart-component.scss     |   7 +
 .../edit-rule-dialog.smart-component.spec.ts  |  28 ++--
 .../edit-rule-dialog.smart-component.ts       |  13 +-
 .../rule-details.ui-component.html            |  11 ++
 .../rule-details.ui-component.scss            |   6 +
 .../rule-details.ui-component.spec.ts         |  11 +-
 .../rule-details/rule-details.ui-component.ts |   9 +-
 .../src/lib/services/actions.service.spec.ts  |  65 ++++++++
 .../src/lib/services/actions.service.ts       |  92 +++++++++++
 .../lib/services/folder-rules.service.spec.ts |   8 +-
 28 files changed, 914 insertions(+), 43 deletions(-)
 create mode 100644 projects/aca-folder-rules/src/lib/mock/actions.mock.ts
 create mode 100644 projects/aca-folder-rules/src/lib/rule-details/actions/rule-action-list.ui-component.html
 create mode 100644 projects/aca-folder-rules/src/lib/rule-details/actions/rule-action-list.ui-component.scss
 create mode 100644 projects/aca-folder-rules/src/lib/rule-details/actions/rule-action-list.ui-component.spec.ts
 create mode 100644 projects/aca-folder-rules/src/lib/rule-details/actions/rule-action-list.ui-component.ts
 create mode 100644 projects/aca-folder-rules/src/lib/rule-details/actions/rule-action.ui-component.html
 create mode 100644 projects/aca-folder-rules/src/lib/rule-details/actions/rule-action.ui-component.scss
 create mode 100644 projects/aca-folder-rules/src/lib/rule-details/actions/rule-action.ui-component.spec.ts
 create mode 100644 projects/aca-folder-rules/src/lib/rule-details/actions/rule-action.ui-component.ts
 create mode 100644 projects/aca-folder-rules/src/lib/services/actions.service.spec.ts
 create mode 100644 projects/aca-folder-rules/src/lib/services/actions.service.ts

diff --git a/projects/aca-folder-rules/assets/i18n/en.json b/projects/aca-folder-rules/assets/i18n/en.json
index 34b394b9b..2ce08d5eb 100644
--- a/projects/aca-folder-rules/assets/i18n/en.json
+++ b/projects/aca-folder-rules/assets/i18n/en.json
@@ -15,6 +15,7 @@
         "NAME": "Name",
         "DESCRIPTION": "Description",
         "WHEN": "When",
+        "PERFORM_ACTIONS": "Perform actions",
         "OPTIONS": "Other options"
       },
       "PLACEHOLDER": {
@@ -70,21 +71,26 @@
         "AND": "And",
         "OR": "Or"
       },
-      "ACTIONS": {
+      "CONDITION_BUTTONS": {
         "ADD_CONDITION": "Add condition",
-        "ADD_GROUP": "Add group",
+        "ADD_GROUP": "Add condition group",
         "REMOVE": "Remove"
       },
       "NO_CONDITIONS": "No conditions",
-      "NO_CONDITIONS_IN_GROUP": "No conditions in the group"
+      "NO_CONDITIONS_IN_GROUP": "No conditions in the group",
+      "ACTION_BUTTONS": {
+        "ADD_ACTION": "Add action",
+        "REMOVE": "Remove"
       },
+      "ACTION_SELECT_PLACEHOLDER": "Select an action"
+    },
     "MANAGE_RULES": {
       "TOOLBAR": {
         "BREADCRUMB": {
           "RULES": "rules"
         },
         "ACTIONS": {
-          "NEW_RULE": "New rule"
+          "CREATE_RULE": "Create rule"
         }
       },
       "EMPTY_RULES_LIST": {
diff --git a/projects/aca-folder-rules/src/lib/folder-rules.module.ts b/projects/aca-folder-rules/src/lib/folder-rules.module.ts
index 3a2e6d939..fab75c403 100644
--- a/projects/aca-folder-rules/src/lib/folder-rules.module.ts
+++ b/projects/aca-folder-rules/src/lib/folder-rules.module.ts
@@ -40,6 +40,8 @@ import { RuleListItemUiComponent } from './rules-list/rule/rule-list-item.ui-com
 import { RulesListUiComponent } from './rules-list/rules-list.ui-component';
 import { RuleTriggersUiComponent } from './rule-details/triggers/rule-triggers.ui-component';
 import { RuleOptionsUiComponent } from './rule-details/options/rule-options.ui-component';
+import { RuleActionListUiComponent } from './rule-details/actions/rule-action-list.ui-component';
+import { RuleActionUiComponent } from './rule-details/actions/rule-action.ui-component';
 
 const routes: Routes = [
   {
@@ -63,6 +65,8 @@ const routes: Routes = [
   declarations: [
     EditRuleDialogSmartComponent,
     ManageRulesSmartComponent,
+    RuleActionListUiComponent,
+    RuleActionUiComponent,
     RuleCompositeConditionUiComponent,
     RuleDetailsUiComponent,
     RuleSimpleConditionUiComponent,
diff --git a/projects/aca-folder-rules/src/lib/manage-rules/manage-rules.smart-component.html b/projects/aca-folder-rules/src/lib/manage-rules/manage-rules.smart-component.html
index ad9866c39..ef52ac195 100644
--- a/projects/aca-folder-rules/src/lib/manage-rules/manage-rules.smart-component.html
+++ b/projects/aca-folder-rules/src/lib/manage-rules/manage-rules.smart-component.html
@@ -12,7 +12,7 @@
   <aca-page-layout-content>
     <div class="main-content">
 
-      <ng-container *ngIf="isLoading$ | async; else onLoaded">
+      <ng-container *ngIf="(rulesLoading$ | async) || (actionsLoading$ | async); else onLoaded">
         <mat-progress-bar color="primary" mode="indeterminate"></mat-progress-bar>
       </ng-container>
 
@@ -25,7 +25,7 @@
                               class="aca-manage-rules__actions-bar__title__breadcrumb"></adf-breadcrumb>
             </adf-toolbar-title>
 
-            <button mat-flat-button color="primary" (click)="openNewRuleDialog()">{{ 'ACA_FOLDER_RULES.MANAGE_RULES.TOOLBAR.ACTIONS.NEW_RULE' | translate }}</button>
+            <button mat-flat-button color="primary" (click)="openNewRuleDialog()">{{ 'ACA_FOLDER_RULES.MANAGE_RULES.TOOLBAR.ACTIONS.CREATE_RULE' | translate }}</button>
           </adf-toolbar>
           <mat-divider></mat-divider>
 
@@ -44,7 +44,12 @@
                 </div>
                 <p>{{ selectedRule.description }}</p>
               </div>
-              <aca-rule-details [readOnly]="true" [preview]="true" [value]="selectedRule"></aca-rule-details>
+              <aca-rule-details
+                [actionDefinitions]="actionDefinitions$ | async"
+                [readOnly]="true"
+                [preview]="true"
+                [value]="selectedRule">
+              </aca-rule-details>
             </div>
           </div>
 
diff --git a/projects/aca-folder-rules/src/lib/manage-rules/manage-rules.smart-component.scss b/projects/aca-folder-rules/src/lib/manage-rules/manage-rules.smart-component.scss
index 74c6e3cd3..a8e4850f2 100644
--- a/projects/aca-folder-rules/src/lib/manage-rules/manage-rules.smart-component.scss
+++ b/projects/aca-folder-rules/src/lib/manage-rules/manage-rules.smart-component.scss
@@ -17,7 +17,7 @@
 
   &__container {
     display: grid;
-    grid-template-columns: 33% 66%;
+    grid-template-columns: minmax(200px,1fr) 3fr;
     padding: 32px;
     overflow: scroll;
 
diff --git a/projects/aca-folder-rules/src/lib/manage-rules/manage-rules.smart-component.spec.ts b/projects/aca-folder-rules/src/lib/manage-rules/manage-rules.smart-component.spec.ts
index 02aa2e1fa..c14bbf569 100644
--- a/projects/aca-folder-rules/src/lib/manage-rules/manage-rules.smart-component.spec.ts
+++ b/projects/aca-folder-rules/src/lib/manage-rules/manage-rules.smart-component.spec.ts
@@ -34,12 +34,14 @@ import { dummyRules } from '../mock/rules.mock';
 import { By } from '@angular/platform-browser';
 import { dummyNodeInfo } from '../mock/node.mock';
 import { MatDialog } from '@angular/material/dialog';
+import { ActionsService } from '../services/actions.service';
 
 describe('ManageRulesSmartComponent', () => {
   let fixture: ComponentFixture<ManageRulesSmartComponent>;
   let component: ManageRulesSmartComponent;
   let debugElement: DebugElement;
   let folderRulesService: FolderRulesService;
+  let actionsService: ActionsService;
 
   beforeEach(
     waitForAsync(() => {
@@ -57,6 +59,7 @@ describe('ManageRulesSmartComponent', () => {
           component = fixture.componentInstance;
           debugElement = fixture.debugElement;
           folderRulesService = TestBed.inject<FolderRulesService>(FolderRulesService);
+          actionsService = TestBed.inject<ActionsService>(ActionsService);
         });
     })
   );
@@ -66,6 +69,7 @@ describe('ManageRulesSmartComponent', () => {
     folderRulesService.folderInfo$ = of(dummyNodeInfo);
     folderRulesService.rulesListing$ = of(dummyRules);
     folderRulesService.loading$ = of(false);
+    actionsService.loading$ = of(false);
 
     fixture.detectChanges();
 
@@ -87,6 +91,7 @@ describe('ManageRulesSmartComponent', () => {
     folderRulesService.rulesListing$ = of([]);
     folderRulesService.loading$ = of(false);
     folderRulesService.deletedRuleId$ = of(null);
+    actionsService.loading$ = of(false);
 
     fixture.detectChanges();
 
@@ -106,6 +111,7 @@ describe('ManageRulesSmartComponent', () => {
     folderRulesService.deletedRuleId$ = of(null);
     folderRulesService.rulesListing$ = of([]);
     folderRulesService.loading$ = of(false);
+    actionsService.loading$ = of(false);
 
     fixture.detectChanges();
 
@@ -120,11 +126,12 @@ describe('ManageRulesSmartComponent', () => {
     expect(ruleDetails).toBeFalsy();
   });
 
-  it('should only show progress bar while loading', () => {
+  it('should only show progress bar while loading', async () => {
     folderRulesService.folderInfo$ = of(null);
     folderRulesService.deletedRuleId$ = of(null);
     folderRulesService.rulesListing$ = of([]);
     folderRulesService.loading$ = of(true);
+    actionsService.loading$ = of(true);
 
     fixture.detectChanges();
 
@@ -145,6 +152,7 @@ describe('ManageRulesSmartComponent', () => {
     folderRulesService.folderInfo$ = of(dummyNodeInfo);
     folderRulesService.rulesListing$ = of(dummyRules);
     folderRulesService.loading$ = of(false);
+    actionsService.loading$ = of(false);
 
     spyOn(component, 'onRuleDelete').and.callThrough();
 
diff --git a/projects/aca-folder-rules/src/lib/manage-rules/manage-rules.smart-component.ts b/projects/aca-folder-rules/src/lib/manage-rules/manage-rules.smart-component.ts
index c7b5d4e28..88c02df32 100644
--- a/projects/aca-folder-rules/src/lib/manage-rules/manage-rules.smart-component.ts
+++ b/projects/aca-folder-rules/src/lib/manage-rules/manage-rules.smart-component.ts
@@ -30,11 +30,13 @@ import { Observable, Subscription } from 'rxjs';
 import { Rule } from '../model/rule.model';
 import { ActivatedRoute } from '@angular/router';
 import { NodeInfo } from '@alfresco/aca-shared/store';
-import { tap } from 'rxjs/operators';
+import { delay, tap } from 'rxjs/operators';
 import { EditRuleDialogSmartComponent } from '../rule-details/edit-rule-dialog.smart-component';
 import { MatDialog } from '@angular/material/dialog';
 import { ConfirmDialogComponent } from '@alfresco/adf-content-services';
 import { NotificationService } from '@alfresco/adf-core';
+import { ActionDefinitionTransformed } from '../model/rule-action.model';
+import { ActionsService } from '../services/actions.service';
 
 @Component({
   selector: 'aca-manage-rules',
@@ -45,8 +47,10 @@ import { NotificationService } from '@alfresco/adf-core';
 })
 export class ManageRulesSmartComponent implements OnInit, OnDestroy {
   rules$: Observable<Rule[]>;
-  isLoading$: Observable<boolean>;
+  rulesLoading$: Observable<boolean>;
+  actionsLoading$: Observable<boolean>;
   folderInfo$: Observable<NodeInfo>;
+  actionDefinitions$: Observable<ActionDefinitionTransformed[]>;
   selectedRule: Rule = null;
   nodeId: string = null;
   deletedRuleSubscription$: Subscription;
@@ -57,10 +61,12 @@ export class ManageRulesSmartComponent implements OnInit, OnDestroy {
     private folderRulesService: FolderRulesService,
     private route: ActivatedRoute,
     private matDialogService: MatDialog,
-    private notificationService: NotificationService
+    private notificationService: NotificationService,
+    private actionsService: ActionsService
   ) {}
 
   ngOnInit(): void {
+    this.actionDefinitions$ = this.actionsService.actionDefinitionsListing$;
     this.rules$ = this.folderRulesService.rulesListing$.pipe(
       tap((rules) => {
         if (!rules.includes(this.selectedRule)) {
@@ -73,8 +79,10 @@ export class ManageRulesSmartComponent implements OnInit, OnDestroy {
         this.folderRulesService.loadRules(this.nodeId);
       }
     });
-    this.isLoading$ = this.folderRulesService.loading$;
+    this.rulesLoading$ = this.folderRulesService.loading$;
+    this.actionsLoading$ = this.actionsService.loading$.pipe(delay(0));
     this.folderInfo$ = this.folderRulesService.folderInfo$;
+    this.actionsService.loadActionDefinitions();
     this.route.params.subscribe((params) => {
       this.nodeId = params.nodeId;
       if (this.nodeId) {
diff --git a/projects/aca-folder-rules/src/lib/mock/actions.mock.ts b/projects/aca-folder-rules/src/lib/mock/actions.mock.ts
new file mode 100644
index 000000000..88a992c9d
--- /dev/null
+++ b/projects/aca-folder-rules/src/lib/mock/actions.mock.ts
@@ -0,0 +1,108 @@
+/*!
+ * @license
+ * Alfresco Example Content Application
+ *
+ * Copyright (C) 2005 - 2020 Alfresco Software Limited
+ *
+ * This file is part of the Alfresco Example Content Application.
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail.  Otherwise, the software is
+ * provided under the following open source license terms:
+ *
+ * The Alfresco Example Content Application is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * The Alfresco Example Content Application is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import { ActionDefinitionList } from '@alfresco/js-api';
+import { ActionDefinitionTransformed, ActionParameterDefinitionTransformed } from '../model/rule-action.model';
+
+export const actionDefListMock: ActionDefinitionList = {
+  list: {
+    pagination: {
+      count: 2,
+      hasMoreItems: false,
+      totalItems: 2,
+      skipCount: 0,
+      maxItems: 100
+    },
+    entries: [
+      {
+        applicableTypes: [],
+        parameterDefinitions: [
+          {
+            name: 'mock-action-parameter-text',
+            type: 'd:text',
+            multiValued: false,
+            mandatory: false,
+            displayLabel: 'Mock action parameter text'
+          },
+          {
+            name: 'mock-action-parameter-boolean',
+            type: 'd:boolean',
+            multiValued: false,
+            mandatory: false
+          }
+        ],
+        name: 'mock-action-1-definition',
+        trackStatus: false,
+        description: 'This is a mock action',
+        id: 'mock-action-1-definition',
+        title: 'Action 1 title'
+      },
+      {
+        applicableTypes: [],
+        name: 'mock-action-2-definition',
+        trackStatus: false,
+        id: 'mock-action-2-definition'
+      }
+    ]
+  }
+};
+
+const actionParam1TransformedMock: ActionParameterDefinitionTransformed = {
+  name: 'mock-action-parameter-text',
+  type: 'd:text',
+  multiValued: false,
+  mandatory: false,
+  displayLabel: 'Mock action parameter text'
+};
+
+const actionParam2TransformedMock: ActionParameterDefinitionTransformed = {
+  name: 'mock-action-parameter-boolean',
+  type: 'd:boolean',
+  multiValued: false,
+  mandatory: false,
+  displayLabel: 'mock-action-parameter-boolean'
+};
+
+const action1TransformedMock: ActionDefinitionTransformed = {
+  id: 'mock-action-1-definition',
+  name: 'mock-action-1-definition',
+  description: 'This is a mock action',
+  title: 'Action 1 title',
+  applicableTypes: [],
+  trackStatus: false,
+  parameterDefinitions: [actionParam1TransformedMock, actionParam2TransformedMock]
+};
+
+const action2TransformedMock: ActionDefinitionTransformed = {
+  id: 'mock-action-2-definition',
+  name: 'mock-action-2-definition',
+  description: '',
+  title: 'mock-action-2-definition',
+  applicableTypes: [],
+  trackStatus: false,
+  parameterDefinitions: []
+};
+
+export const actionsTransformedListMock: ActionDefinitionTransformed[] = [action1TransformedMock, action2TransformedMock];
diff --git a/projects/aca-folder-rules/src/lib/model/rule-action.model.ts b/projects/aca-folder-rules/src/lib/model/rule-action.model.ts
index 1ba35ee4c..8fbeacaa9 100644
--- a/projects/aca-folder-rules/src/lib/model/rule-action.model.ts
+++ b/projects/aca-folder-rules/src/lib/model/rule-action.model.ts
@@ -25,11 +25,23 @@
 
 export interface RuleAction {
   actionDefinitionId: string;
-  params: RuleActionParams;
+  params: { [key: string]: unknown };
 }
 
-export interface RuleActionParams {
-  'deep-copy'?: boolean;
-  'destination-folder'?: string;
-  actionContext?: string;
+export interface ActionDefinitionTransformed {
+  id: string;
+  name: string;
+  description: string;
+  title: string;
+  applicableTypes: string[];
+  trackStatus: boolean;
+  parameterDefinitions: ActionParameterDefinitionTransformed[];
+}
+
+export interface ActionParameterDefinitionTransformed {
+  name: string;
+  type: string;
+  multiValued: boolean;
+  mandatory: boolean;
+  displayLabel: string;
 }
diff --git a/projects/aca-folder-rules/src/lib/rule-details/actions/rule-action-list.ui-component.html b/projects/aca-folder-rules/src/lib/rule-details/actions/rule-action-list.ui-component.html
new file mode 100644
index 000000000..462359061
--- /dev/null
+++ b/projects/aca-folder-rules/src/lib/rule-details/actions/rule-action-list.ui-component.html
@@ -0,0 +1,31 @@
+<div class="aca-rule-action-list__item  " *ngFor="let control of formControls">
+  <aca-rule-action
+    [actionDefinitions]="actionDefinitions"
+    [readOnly]="readOnly"
+    [formControl]="control">
+  </aca-rule-action>
+
+  <button
+    mat-icon-button
+    data-automation-id="rule-action-list-action-menu"
+    *ngIf="!readOnly" [matMenuTriggerFor]="menu">
+    <mat-icon>more_vert</mat-icon>
+  </button>
+
+  <mat-menu #menu>
+    <button
+      mat-menu-item
+      data-automation-id="rule-action-list-remove-action-button"
+      [title]="'ACA_FOLDER_RULES.RULE_DETAILS.ACTION_BUTTONS.REMOVE' | translate"
+      [disabled]="formArray.controls.length <= 1"
+      (click)="removeAction(control)">
+      <mat-icon>delete</mat-icon>
+      <span>{{ 'ACA_FOLDER_RULES.RULE_DETAILS.ACTION_BUTTONS.REMOVE' | translate }}</span>
+    </button>
+  </mat-menu>
+</div>
+
+<button mat-button data-automation-id="rule-action-list-add-action-button" (click)="addAction()" *ngIf="!readOnly">
+  <mat-icon>add</mat-icon>
+  <span>{{ 'ACA_FOLDER_RULES.RULE_DETAILS.ACTION_BUTTONS.ADD_ACTION' | translate }}</span>
+</button>
diff --git a/projects/aca-folder-rules/src/lib/rule-details/actions/rule-action-list.ui-component.scss b/projects/aca-folder-rules/src/lib/rule-details/actions/rule-action-list.ui-component.scss
new file mode 100644
index 000000000..88bfc1c5a
--- /dev/null
+++ b/projects/aca-folder-rules/src/lib/rule-details/actions/rule-action-list.ui-component.scss
@@ -0,0 +1,15 @@
+.aca-rule-action-list {
+  &__item {
+    padding: 4px 8px;
+    border-radius: 8px;
+    display: flex;
+
+    & > .aca-rule-action {
+      flex: 1;
+    }
+
+    &:nth-child(2n) {
+      background-color: hsl(0,0%,95%);
+    }
+  }
+}
diff --git a/projects/aca-folder-rules/src/lib/rule-details/actions/rule-action-list.ui-component.spec.ts b/projects/aca-folder-rules/src/lib/rule-details/actions/rule-action-list.ui-component.spec.ts
new file mode 100644
index 000000000..7a2f99223
--- /dev/null
+++ b/projects/aca-folder-rules/src/lib/rule-details/actions/rule-action-list.ui-component.spec.ts
@@ -0,0 +1,104 @@
+/*!
+ * @license
+ * Alfresco Example Content Application
+ *
+ * Copyright (C) 2005 - 2020 Alfresco Software Limited
+ *
+ * This file is part of the Alfresco Example Content Application.
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail.  Otherwise, the software is
+ * provided under the following open source license terms:
+ *
+ * The Alfresco Example Content Application is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * The Alfresco Example Content Application is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { CoreTestingModule } from '@alfresco/adf-core';
+import { RuleActionListUiComponent } from './rule-action-list.ui-component';
+import { By } from '@angular/platform-browser';
+import { DebugElement } from '@angular/core';
+import { RuleActionUiComponent } from './rule-action.ui-component';
+
+describe('RuleActionListUiComponent', () => {
+  let fixture: ComponentFixture<RuleActionListUiComponent>;
+  let component: RuleActionListUiComponent;
+
+  const getByDataAutomationId = (dataAutomationId: string, index = 0): DebugElement =>
+    fixture.debugElement.queryAll(By.css(`[data-automation-id="${dataAutomationId}"]`))[index];
+
+  beforeEach(() => {
+    TestBed.configureTestingModule({
+      imports: [CoreTestingModule]
+    });
+
+    fixture = TestBed.createComponent(RuleActionListUiComponent);
+    component = fixture.componentInstance;
+
+    component.writeValue([]);
+    fixture.detectChanges();
+  });
+
+  it('should default to 1 empty action when an empty array of actions is written', () => {
+    const acaRuleActions = fixture.debugElement.queryAll(By.directive(RuleActionUiComponent));
+    expect(acaRuleActions.length).toBe(1);
+  });
+
+  it('should add a new action when the "add action" button is clicked', () => {
+    const addActionButton = getByDataAutomationId('rule-action-list-add-action-button').nativeElement as HTMLButtonElement;
+    addActionButton.click();
+    fixture.detectChanges();
+
+    const acaRuleActions = fixture.debugElement.queryAll(By.directive(RuleActionUiComponent));
+    expect(acaRuleActions.length).toBe(2);
+  });
+
+  it('should disable the remove button if there is only one action', () => {
+    const menuButton = getByDataAutomationId('rule-action-list-action-menu', 0).nativeElement as HTMLButtonElement;
+    menuButton.click();
+    fixture.detectChanges();
+
+    const removeActionButton = getByDataAutomationId('rule-action-list-remove-action-button').nativeElement as HTMLButtonElement;
+    expect(removeActionButton.disabled).toBeTrue();
+  });
+
+  it('should enable the remove button if there is more than one action', () => {
+    const addActionButton = getByDataAutomationId('rule-action-list-add-action-button').nativeElement as HTMLButtonElement;
+    addActionButton.click();
+    fixture.detectChanges();
+
+    const menuButton = getByDataAutomationId('rule-action-list-action-menu', 0).nativeElement as HTMLButtonElement;
+    menuButton.click();
+    fixture.detectChanges();
+
+    const removeActionButton = getByDataAutomationId('rule-action-list-remove-action-button').nativeElement as HTMLButtonElement;
+    expect(removeActionButton.disabled).toBeFalse();
+  });
+
+  it('should remove an action when the remove action button is clicked', () => {
+    const addActionButton = getByDataAutomationId('rule-action-list-add-action-button').nativeElement as HTMLButtonElement;
+    addActionButton.click();
+    fixture.detectChanges();
+
+    const menuButton = getByDataAutomationId('rule-action-list-action-menu', 0).nativeElement as HTMLButtonElement;
+    menuButton.click();
+    fixture.detectChanges();
+
+    const removeActionButton = getByDataAutomationId('rule-action-list-remove-action-button').nativeElement as HTMLButtonElement;
+    removeActionButton.click();
+    fixture.detectChanges();
+
+    const acaRuleActions = fixture.debugElement.queryAll(By.directive(RuleActionUiComponent));
+    expect(acaRuleActions.length).toBe(1);
+  });
+});
diff --git a/projects/aca-folder-rules/src/lib/rule-details/actions/rule-action-list.ui-component.ts b/projects/aca-folder-rules/src/lib/rule-details/actions/rule-action-list.ui-component.ts
new file mode 100644
index 000000000..129a03035
--- /dev/null
+++ b/projects/aca-folder-rules/src/lib/rule-details/actions/rule-action-list.ui-component.ts
@@ -0,0 +1,77 @@
+import { Component, forwardRef, Input, OnDestroy, ViewEncapsulation } from '@angular/core';
+import { ControlValueAccessor, FormArray, FormControl, NG_VALUE_ACCESSOR } from '@angular/forms';
+import { ActionDefinitionTransformed, RuleAction } from '../../model/rule-action.model';
+import { Subscription } from 'rxjs';
+
+@Component({
+  selector: 'aca-rule-action-list',
+  templateUrl: './rule-action-list.ui-component.html',
+  styleUrls: ['./rule-action-list.ui-component.scss'],
+  encapsulation: ViewEncapsulation.None,
+  host: { class: 'aca-rule-action-list' },
+  providers: [
+    {
+      provide: NG_VALUE_ACCESSOR,
+      multi: true,
+      useExisting: forwardRef(() => RuleActionListUiComponent)
+    }
+  ]
+})
+export class RuleActionListUiComponent implements ControlValueAccessor, OnDestroy {
+  @Input()
+  actionDefinitions: ActionDefinitionTransformed[] = [];
+  @Input()
+  readOnly = false;
+
+  formArray = new FormArray([]);
+  private formArraySubscription: Subscription;
+
+  get formControls(): FormControl[] {
+    return this.formArray.controls as FormControl[];
+  }
+
+  onChange: (actions: RuleAction[]) => void = () => undefined;
+  onTouch: () => void = () => undefined;
+
+  writeValue(actions: RuleAction[]) {
+    if (actions.length === 0) {
+      actions = [
+        {
+          actionDefinitionId: null,
+          params: {}
+        }
+      ];
+    }
+    this.formArray = new FormArray(actions.map((action: RuleAction) => new FormControl(action)));
+    this.formArraySubscription?.unsubscribe();
+    this.formArraySubscription = this.formArray.valueChanges.subscribe((value: any) => {
+      this.onChange(value);
+      this.onTouch();
+    });
+  }
+
+  registerOnChange(fn: (actions: RuleAction[]) => void) {
+    this.onChange = fn;
+  }
+
+  registerOnTouched(fn: () => void) {
+    this.onTouch = fn;
+  }
+
+  addAction() {
+    const newAction: RuleAction = {
+      actionDefinitionId: null,
+      params: {}
+    };
+    this.formArray.push(new FormControl(newAction));
+  }
+
+  ngOnDestroy() {
+    this.formArraySubscription?.unsubscribe();
+  }
+
+  removeAction(control: FormControl) {
+    const index = this.formArray.value.indexOf(control.value);
+    this.formArray.removeAt(index);
+  }
+}
diff --git a/projects/aca-folder-rules/src/lib/rule-details/actions/rule-action.ui-component.html b/projects/aca-folder-rules/src/lib/rule-details/actions/rule-action.ui-component.html
new file mode 100644
index 000000000..de1879088
--- /dev/null
+++ b/projects/aca-folder-rules/src/lib/rule-details/actions/rule-action.ui-component.html
@@ -0,0 +1,22 @@
+<form class="aca-rule-action__form" [formGroup]="form">
+
+  <mat-form-field>
+    <mat-select
+      formControlName="actionDefinitionId"
+      data-automation-id="rule-action-select"
+      [placeholder]="'ACA_FOLDER_RULES.RULE_DETAILS.ACTION_SELECT_PLACEHOLDER' | translate">
+      <mat-option
+        *ngFor="let actionDefinition of actionDefinitions"
+        [value]="actionDefinition.id">
+        {{ actionDefinition.title }}
+      </mat-option>
+    </mat-select>
+  </mat-form-field>
+
+  <adf-card-view
+    data-automation-id="rule-action-card-view"
+    [properties]="cardViewItems"
+    [editable]="!readOnly">
+  </adf-card-view>
+
+</form>
diff --git a/projects/aca-folder-rules/src/lib/rule-details/actions/rule-action.ui-component.scss b/projects/aca-folder-rules/src/lib/rule-details/actions/rule-action.ui-component.scss
new file mode 100644
index 000000000..65fb675b2
--- /dev/null
+++ b/projects/aca-folder-rules/src/lib/rule-details/actions/rule-action.ui-component.scss
@@ -0,0 +1,7 @@
+.aca-rule-action {
+  &__form {
+    display: flex;
+    flex-direction: row;
+    gap: 20px;
+  }
+}
diff --git a/projects/aca-folder-rules/src/lib/rule-details/actions/rule-action.ui-component.spec.ts b/projects/aca-folder-rules/src/lib/rule-details/actions/rule-action.ui-component.spec.ts
new file mode 100644
index 000000000..e0c458db9
--- /dev/null
+++ b/projects/aca-folder-rules/src/lib/rule-details/actions/rule-action.ui-component.spec.ts
@@ -0,0 +1,87 @@
+/*!
+ * @license
+ * Alfresco Example Content Application
+ *
+ * Copyright (C) 2005 - 2020 Alfresco Software Limited
+ *
+ * This file is part of the Alfresco Example Content Application.
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail.  Otherwise, the software is
+ * provided under the following open source license terms:
+ *
+ * The Alfresco Example Content Application is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * The Alfresco Example Content Application is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { CardViewBoolItemModel, CardViewComponent, CardViewTextItemModel, CoreTestingModule } from '@alfresco/adf-core';
+import { RuleActionUiComponent } from './rule-action.ui-component';
+import { actionsTransformedListMock } from '../../mock/actions.mock';
+import { DebugElement } from '@angular/core';
+import { By } from '@angular/platform-browser';
+
+describe('RuleActionUiComponent', () => {
+  let fixture: ComponentFixture<RuleActionUiComponent>;
+  let component: RuleActionUiComponent;
+
+  const getByDataAutomationId = (dataAutomationId: string): DebugElement =>
+    fixture.debugElement.query(By.css(`[data-automation-id="${dataAutomationId}"]`));
+
+  const changeMatSelectValue = (dataAutomationId: string, value: string) => {
+    const matSelect = getByDataAutomationId(dataAutomationId).nativeElement;
+    matSelect.click();
+    fixture.detectChanges();
+    const matOption = fixture.debugElement.query(By.css(`.mat-option[ng-reflect-value="${value}"]`)).nativeElement;
+    matOption.click();
+    fixture.detectChanges();
+  };
+
+  beforeEach(() => {
+    TestBed.configureTestingModule({
+      imports: [CoreTestingModule]
+    });
+
+    fixture = TestBed.createComponent(RuleActionUiComponent);
+    component = fixture.componentInstance;
+  });
+
+  it('should populate the dropdown selector with the action definitions', () => {
+    component.actionDefinitions = actionsTransformedListMock;
+    fixture.detectChanges();
+
+    const matSelect = getByDataAutomationId('rule-action-select').nativeElement;
+    matSelect.click();
+    fixture.detectChanges();
+
+    const matOptions = fixture.debugElement.queryAll(By.css(`mat-option`));
+    expect(matOptions.length).toBe(2);
+    expect(matOptions[0].nativeElement.innerText).toBe('Action 1 title');
+    expect(matOptions[1].nativeElement.innerText).toBe('mock-action-2-definition');
+  });
+
+  it('should populate the card view with parameters when an action is selected', () => {
+    component.actionDefinitions = actionsTransformedListMock;
+    fixture.detectChanges();
+
+    const cardView = getByDataAutomationId('rule-action-card-view').componentInstance as CardViewComponent;
+    expect(cardView.properties.length).toBe(0);
+
+    changeMatSelectValue('rule-action-select', 'mock-action-1-definition');
+    expect(cardView.properties.length).toBe(2);
+    expect(cardView.properties[0]).toBeInstanceOf(CardViewTextItemModel);
+    expect(cardView.properties[1]).toBeInstanceOf(CardViewBoolItemModel);
+
+    changeMatSelectValue('rule-action-select', 'mock-action-2-definition');
+    expect(cardView.properties.length).toBe(0);
+  });
+});
diff --git a/projects/aca-folder-rules/src/lib/rule-details/actions/rule-action.ui-component.ts b/projects/aca-folder-rules/src/lib/rule-details/actions/rule-action.ui-component.ts
new file mode 100644
index 000000000..cfea506dc
--- /dev/null
+++ b/projects/aca-folder-rules/src/lib/rule-details/actions/rule-action.ui-component.ts
@@ -0,0 +1,153 @@
+import { Component, forwardRef, Input, OnDestroy, ViewEncapsulation } from '@angular/core';
+import { ControlValueAccessor, FormControl, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms';
+import { ActionDefinitionTransformed, RuleAction } from '../../model/rule-action.model';
+import { CardViewItem } from '@alfresco/adf-core/lib/card-view/interfaces/card-view-item.interface';
+import { CardViewBoolItemModel, CardViewTextItemModel, CardViewUpdateService, UpdateNotification } from '@alfresco/adf-core';
+import { ActionParameterDefinition } from '@alfresco/js-api';
+
+@Component({
+  selector: 'aca-rule-action',
+  templateUrl: './rule-action.ui-component.html',
+  styleUrls: ['./rule-action.ui-component.scss'],
+  encapsulation: ViewEncapsulation.None,
+  host: { class: 'aca-rule-action' },
+  providers: [
+    {
+      provide: NG_VALUE_ACCESSOR,
+      multi: true,
+      useExisting: forwardRef(() => RuleActionUiComponent)
+    },
+    CardViewUpdateService
+  ]
+})
+export class RuleActionUiComponent implements ControlValueAccessor, OnDestroy {
+  private _actionDefinitions: ActionDefinitionTransformed[];
+  @Input()
+  get actionDefinitions(): ActionDefinitionTransformed[] {
+    return this._actionDefinitions;
+  }
+  set actionDefinitions(value: ActionDefinitionTransformed[]) {
+    this._actionDefinitions = value.sort((a, b) => a.title.localeCompare(b.title));
+  }
+
+  private _readOnly = false;
+  @Input()
+  get readOnly(): boolean {
+    return this._readOnly;
+  }
+  set readOnly(isReadOnly: boolean) {
+    this.setDisabledState(isReadOnly);
+  }
+
+  form = new FormGroup({
+    actionDefinitionId: new FormControl('copy')
+  });
+
+  cardViewItems: CardViewItem[] = [];
+
+  parameters: { [key: string]: unknown } = {};
+
+  get selectedActionDefinitionId(): string {
+    return this.form.get('actionDefinitionId').value;
+  }
+
+  get selectedActionDefinition(): ActionDefinitionTransformed {
+    return this.actionDefinitions.find((actionDefinition: ActionDefinitionTransformed) => actionDefinition.id === this.selectedActionDefinitionId);
+  }
+
+  private formSubscription = this.form.valueChanges.subscribe(() => {
+    this.setDefaultParameters();
+    this.setCardViewProperties();
+    this.onChange({
+      actionDefinitionId: this.selectedActionDefinitionId,
+      params: this.parameters
+    });
+    this.onTouch();
+  });
+
+  private updateServiceSubscription = this.cardViewUpdateService.itemUpdated$.subscribe((updateNotification: UpdateNotification) => {
+    this.parameters = {
+      ...this.parameters,
+      ...updateNotification.changed
+    };
+    this.onChange({
+      actionDefinitionId: this.selectedActionDefinitionId,
+      params: this.parameters
+    });
+    this.onTouch();
+  });
+
+  onChange: (action: RuleAction) => void = () => undefined;
+  onTouch: () => void = () => undefined;
+
+  constructor(private cardViewUpdateService: CardViewUpdateService) {}
+
+  writeValue(action: RuleAction) {
+    this.form.setValue({
+      actionDefinitionId: action.actionDefinitionId
+    });
+    this.parameters = {
+      ...this.parameters,
+      ...action.params
+    };
+    this.setCardViewProperties();
+  }
+
+  registerOnChange(fn: (action: RuleAction) => void) {
+    this.onChange = fn;
+  }
+
+  registerOnTouched(fn: any) {
+    this.onTouch = fn;
+  }
+
+  ngOnDestroy() {
+    this.formSubscription.unsubscribe();
+    this.updateServiceSubscription.unsubscribe();
+  }
+
+  setCardViewProperties() {
+    this.cardViewItems = (this.selectedActionDefinition?.parameterDefinitions ?? []).map((paramDef) => {
+      const cardViewPropertiesModel = {
+        label: paramDef.displayLabel,
+        key: paramDef.name,
+        editable: true
+      };
+      switch (paramDef.type) {
+        case 'd:boolean':
+          return new CardViewBoolItemModel({
+            ...cardViewPropertiesModel,
+            value: this.parameters[paramDef.name] ?? false
+          });
+        default:
+          return new CardViewTextItemModel({
+            ...cardViewPropertiesModel,
+            value: this.parameters[paramDef.name] ?? ''
+          });
+      }
+    });
+  }
+
+  setDefaultParameters() {
+    this.parameters = {};
+    (this.selectedActionDefinition?.parameterDefinitions ?? []).forEach((paramDef: ActionParameterDefinition) => {
+      switch (paramDef.type) {
+        case 'd:boolean':
+          this.parameters[paramDef.name] = false;
+          break;
+        default:
+          this.parameters[paramDef.name] = '';
+      }
+    });
+  }
+
+  setDisabledState(isDisabled: boolean) {
+    if (isDisabled) {
+      this._readOnly = true;
+      this.form.disable();
+    } else {
+      this._readOnly = false;
+      this.form.enable();
+    }
+  }
+}
diff --git a/projects/aca-folder-rules/src/lib/rule-details/conditions/rule-composite-condition.ui-component.html b/projects/aca-folder-rules/src/lib/rule-details/conditions/rule-composite-condition.ui-component.html
index 32a4ec642..2f022ec24 100644
--- a/projects/aca-folder-rules/src/lib/rule-details/conditions/rule-composite-condition.ui-component.html
+++ b/projects/aca-folder-rules/src/lib/rule-details/conditions/rule-composite-condition.ui-component.html
@@ -48,10 +48,10 @@
     <mat-menu #menu="matMenu">
       <button
         mat-menu-item
-        [title]="'ACA_FOLDER_RULES.RULE_DETAILS.ACTIONS.REMOVE' | translate"
+        [title]="'ACA_FOLDER_RULES.RULE_DETAILS.CONDITION_BUTTONS.REMOVE' | translate"
         (click)="removeCondition(control)">
         <mat-icon>delete</mat-icon>
-        <span>{{ 'ACA_FOLDER_RULES.RULE_DETAILS.ACTIONS.REMOVE' | translate }}</span>
+        <span>{{ 'ACA_FOLDER_RULES.RULE_DETAILS.CONDITION_BUTTONS.REMOVE' | translate }}</span>
       </button>
     </mat-menu>
 
@@ -60,11 +60,11 @@
   <div class="aca-rule-composite-condition__form__actions" *ngIf="!readOnly" data-automation-id="add-actions">
     <button mat-button (click)="addSimpleCondition()" data-automation-id="add-condition-button">
       <mat-icon>add</mat-icon>
-      <span>{{ 'ACA_FOLDER_RULES.RULE_DETAILS.ACTIONS.ADD_CONDITION' | translate }}</span>
+      <span>{{ 'ACA_FOLDER_RULES.RULE_DETAILS.CONDITION_BUTTONS.ADD_CONDITION' | translate }}</span>
     </button>
     <button mat-button (click)="addCompositeCondition()" data-automation-id="add-group-button">
       <mat-icon>add</mat-icon>
-      <span>{{ 'ACA_FOLDER_RULES.RULE_DETAILS.ACTIONS.ADD_GROUP' | translate }}</span>
+      <span>{{ 'ACA_FOLDER_RULES.RULE_DETAILS.CONDITION_BUTTONS.ADD_GROUP' | translate }}</span>
     </button>
   </div>
 </form>
diff --git a/projects/aca-folder-rules/src/lib/rule-details/edit-rule-dialog.smart-component.html b/projects/aca-folder-rules/src/lib/rule-details/edit-rule-dialog.smart-component.html
index b6f2c9a41..c6a1f3134 100644
--- a/projects/aca-folder-rules/src/lib/rule-details/edit-rule-dialog.smart-component.html
+++ b/projects/aca-folder-rules/src/lib/rule-details/edit-rule-dialog.smart-component.html
@@ -8,7 +8,21 @@
 </div>
 
 <mat-dialog-content class="aca-edit-rule-dialog__content">
-  <aca-rule-details (formValidationChanged)="formValid = $event" (formValueChanged)="formValue = $event" [value]="model"></aca-rule-details>
+  <div class="aca-edit-rule-dialog__content__spinner" *ngIf="loading$ | async; else ruleDetails">
+    <mat-progress-spinner
+      color="primary"
+      mode="indeterminate">
+    </mat-progress-spinner>
+  </div>
+
+  <ng-template #ruleDetails>
+    <aca-rule-details
+      [actionDefinitions]="actionDefinitions$ | async"
+      [value]="model"
+      (formValueChanged)="formValue = $event"
+      (formValidationChanged)="formValid = $event">
+    </aca-rule-details>
+  </ng-template>
 </mat-dialog-content>
 
 <mat-dialog-actions align="end" class="aca-edit-rule-dialog__footer">
diff --git a/projects/aca-folder-rules/src/lib/rule-details/edit-rule-dialog.smart-component.scss b/projects/aca-folder-rules/src/lib/rule-details/edit-rule-dialog.smart-component.scss
index 2c78b0b0e..34344e4f8 100644
--- a/projects/aca-folder-rules/src/lib/rule-details/edit-rule-dialog.smart-component.scss
+++ b/projects/aca-folder-rules/src/lib/rule-details/edit-rule-dialog.smart-component.scss
@@ -31,6 +31,13 @@
   &__content {
     margin: 0;
     padding: 0;
+
+    &__spinner {
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      margin: 20px 0
+    }
   }
 
   &__footer {
diff --git a/projects/aca-folder-rules/src/lib/rule-details/edit-rule-dialog.smart-component.spec.ts b/projects/aca-folder-rules/src/lib/rule-details/edit-rule-dialog.smart-component.spec.ts
index 650495ace..9e6c8bdb7 100644
--- a/projects/aca-folder-rules/src/lib/rule-details/edit-rule-dialog.smart-component.spec.ts
+++ b/projects/aca-folder-rules/src/lib/rule-details/edit-rule-dialog.smart-component.spec.ts
@@ -31,9 +31,14 @@ import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
 import { CoreTestingModule } from '@alfresco/adf-core';
 import { RuleCompositeConditionUiComponent } from './conditions/rule-composite-condition.ui-component';
 import { RuleTriggersUiComponent } from './triggers/rule-triggers.ui-component';
+import { RuleActionListUiComponent } from './actions/rule-action-list.ui-component';
+import { RuleActionUiComponent } from './actions/rule-action.ui-component';
+import { ActionsService } from '../services/actions.service';
+import { RuleOptionsUiComponent } from './options/rule-options.ui-component';
 
 describe('EditRuleDialogComponent', () => {
   let fixture: ComponentFixture<EditRuleDialogSmartComponent>;
+  let actionsService: ActionsService;
 
   const dialogRef = {
     close: jasmine.createSpy('close'),
@@ -43,14 +48,26 @@ describe('EditRuleDialogComponent', () => {
   const setupBeforeEach = (dialogOptions: EditRuleDialogOptions = {}) => {
     TestBed.configureTestingModule({
       imports: [CoreTestingModule],
-      declarations: [EditRuleDialogSmartComponent, RuleCompositeConditionUiComponent, RuleDetailsUiComponent, RuleTriggersUiComponent],
+      declarations: [
+        EditRuleDialogSmartComponent,
+        RuleCompositeConditionUiComponent,
+        RuleDetailsUiComponent,
+        RuleTriggersUiComponent,
+        RuleActionListUiComponent,
+        RuleActionUiComponent,
+        RuleOptionsUiComponent
+      ],
       providers: [
         { provide: MatDialogRef, useValue: dialogRef },
         { provide: MAT_DIALOG_DATA, useValue: dialogOptions }
       ]
     });
 
+    actionsService = TestBed.inject(ActionsService);
+    spyOn(actionsService, 'loadActionDefinitions').and.stub();
+
     fixture = TestBed.createComponent(EditRuleDialogSmartComponent);
+    fixture.detectChanges();
   };
 
   describe('No dialog options passed / indifferent', () => {
@@ -59,7 +76,6 @@ describe('EditRuleDialogComponent', () => {
     });
 
     it('should activate the submit button only when a valid state is received', () => {
-      fixture.detectChanges();
       const submitButton = fixture.debugElement.query(By.css('[data-automation-id="edit-rule-dialog-submit"]')).nativeElement as HTMLButtonElement;
       const ruleDetails = fixture.debugElement.query(By.directive(RuleDetailsUiComponent)).componentInstance as RuleDetailsUiComponent;
       ruleDetails.formValidationChanged.emit(true);
@@ -73,14 +89,12 @@ describe('EditRuleDialogComponent', () => {
     });
 
     it('should show a "create" label in the title', () => {
-      fixture.detectChanges();
       const titleElement = fixture.debugElement.query(By.css('[data-automation-id="edit-rule-dialog-title"]')).nativeElement as HTMLDivElement;
 
       expect(titleElement.innerText.trim()).toBe('ACA_FOLDER_RULES.EDIT_RULE_DIALOG.CREATE_TITLE');
     });
 
     it('should show a "create" label in the submit button', () => {
-      fixture.detectChanges();
       const titleElement = fixture.debugElement.query(By.css('[data-automation-id="edit-rule-dialog-submit"]')).nativeElement as HTMLButtonElement;
 
       expect(titleElement.innerText.trim()).toBe('ACA_FOLDER_RULES.EDIT_RULE_DIALOG.CREATE');
@@ -90,9 +104,7 @@ describe('EditRuleDialogComponent', () => {
   describe('With dialog options passed', () => {
     const dialogOptions: EditRuleDialogOptions = {
       model: {
-        id: 'rule-id',
-        name: 'Rule name',
-        description: 'This is the description of the rule'
+        id: 'rule-id'
       }
     };
 
@@ -101,14 +113,12 @@ describe('EditRuleDialogComponent', () => {
     });
 
     it('should show an "update" label in the title', () => {
-      fixture.detectChanges();
       const titleElement = fixture.debugElement.query(By.css('[data-automation-id="edit-rule-dialog-title"]')).nativeElement as HTMLDivElement;
 
       expect(titleElement.innerText.trim()).toBe('ACA_FOLDER_RULES.EDIT_RULE_DIALOG.UPDATE_TITLE');
     });
 
     it('should show an "update" label in the submit button', () => {
-      fixture.detectChanges();
       const titleElement = fixture.debugElement.query(By.css('[data-automation-id="edit-rule-dialog-submit"]')).nativeElement as HTMLButtonElement;
 
       expect(titleElement.innerText.trim()).toBe('ACA_FOLDER_RULES.EDIT_RULE_DIALOG.UPDATE');
diff --git a/projects/aca-folder-rules/src/lib/rule-details/edit-rule-dialog.smart-component.ts b/projects/aca-folder-rules/src/lib/rule-details/edit-rule-dialog.smart-component.ts
index 9eeb06541..9fb69a8d4 100644
--- a/projects/aca-folder-rules/src/lib/rule-details/edit-rule-dialog.smart-component.ts
+++ b/projects/aca-folder-rules/src/lib/rule-details/edit-rule-dialog.smart-component.ts
@@ -23,9 +23,10 @@
  * along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
  */
 
-import { Component, EventEmitter, Inject, Output, ViewEncapsulation } from '@angular/core';
+import { Component, EventEmitter, Inject, OnInit, Output, ViewEncapsulation } from '@angular/core';
 import { MAT_DIALOG_DATA } from '@angular/material/dialog';
 import { Rule } from '../model/rule.model';
+import { ActionsService } from '../services/actions.service';
 
 export interface EditRuleDialogOptions {
   model?: Partial<Rule>;
@@ -38,13 +39,15 @@ export interface EditRuleDialogOptions {
   encapsulation: ViewEncapsulation.None,
   host: { class: 'aca-edit-rule-dialog' }
 })
-export class EditRuleDialogSmartComponent {
+export class EditRuleDialogSmartComponent implements OnInit {
   formValid = false;
   model: Partial<Rule>;
   formValue: Partial<Rule>;
   @Output() submitted = new EventEmitter<Partial<Rule>>();
+  actionDefinitions$ = this.actionsService.actionDefinitionsListing$;
+  loading$ = this.actionsService.loading$;
 
-  constructor(@Inject(MAT_DIALOG_DATA) public options: EditRuleDialogOptions) {
+  constructor(@Inject(MAT_DIALOG_DATA) public options: EditRuleDialogOptions, private actionsService: ActionsService) {
     this.model = this.options?.model || {};
   }
 
@@ -63,4 +66,8 @@ export class EditRuleDialogSmartComponent {
   onSubmit() {
     this.submitted.emit(this.formValue);
   }
+
+  ngOnInit() {
+    this.actionsService.loadActionDefinitions();
+  }
 }
diff --git a/projects/aca-folder-rules/src/lib/rule-details/rule-details.ui-component.html b/projects/aca-folder-rules/src/lib/rule-details/rule-details.ui-component.html
index bc165dfee..5906c9c08 100644
--- a/projects/aca-folder-rules/src/lib/rule-details/rule-details.ui-component.html
+++ b/projects/aca-folder-rules/src/lib/rule-details/rule-details.ui-component.html
@@ -45,6 +45,17 @@
 
   <hr>
 
+  <div class="aca-rule-details__form__row aca-rule-details__form__actions">
+    <div class="label">{{ 'ACA_FOLDER_RULES.RULE_DETAILS.LABEL.PERFORM_ACTIONS' | translate }}</div>
+    <aca-rule-action-list
+      formControlName="actions"
+      [actionDefinitions]="actionDefinitions"
+      [readOnly]="readOnly">
+    </aca-rule-action-list>
+  </div>
+
+  <hr>
+
   <div class="aca-rule-details__form__row">
     <div class="label">{{ 'ACA_FOLDER_RULES.RULE_DETAILS.LABEL.OPTIONS' | translate }}</div>
     <aca-rule-options [form]="form" [preview]="preview" data-automation-id="rule-details-options-component"></aca-rule-options>
diff --git a/projects/aca-folder-rules/src/lib/rule-details/rule-details.ui-component.scss b/projects/aca-folder-rules/src/lib/rule-details/rule-details.ui-component.scss
index a2788dc35..fff9c0de2 100644
--- a/projects/aca-folder-rules/src/lib/rule-details/rule-details.ui-component.scss
+++ b/projects/aca-folder-rules/src/lib/rule-details/rule-details.ui-component.scss
@@ -58,5 +58,11 @@
       margin-left: 16px;
       color: inherit;
     }
+
+    &__actions {
+      .aca-rule-action-list {
+        flex: 1;
+      }
+    }
   }
 }
diff --git a/projects/aca-folder-rules/src/lib/rule-details/rule-details.ui-component.spec.ts b/projects/aca-folder-rules/src/lib/rule-details/rule-details.ui-component.spec.ts
index 839aa249e..1cafd93d6 100644
--- a/projects/aca-folder-rules/src/lib/rule-details/rule-details.ui-component.spec.ts
+++ b/projects/aca-folder-rules/src/lib/rule-details/rule-details.ui-component.spec.ts
@@ -32,6 +32,8 @@ import { RuleCompositeConditionUiComponent } from './conditions/rule-composite-c
 import { RuleTriggersUiComponent } from './triggers/rule-triggers.ui-component';
 import { MatCheckbox } from '@angular/material/checkbox';
 import { RuleOptionsUiComponent } from './options/rule-options.ui-component';
+import { RuleActionListUiComponent } from './actions/rule-action-list.ui-component';
+import { RuleActionUiComponent } from './actions/rule-action.ui-component';
 
 describe('RuleDetailsUiComponent', () => {
   let fixture: ComponentFixture<RuleDetailsUiComponent>;
@@ -56,7 +58,14 @@ describe('RuleDetailsUiComponent', () => {
   beforeEach(() => {
     TestBed.configureTestingModule({
       imports: [CoreTestingModule],
-      declarations: [RuleCompositeConditionUiComponent, RuleDetailsUiComponent, RuleTriggersUiComponent, RuleOptionsUiComponent]
+      declarations: [
+        RuleCompositeConditionUiComponent,
+        RuleDetailsUiComponent,
+        RuleTriggersUiComponent,
+        RuleOptionsUiComponent,
+        RuleActionListUiComponent,
+        RuleActionUiComponent
+      ]
     });
 
     fixture = TestBed.createComponent(RuleDetailsUiComponent);
diff --git a/projects/aca-folder-rules/src/lib/rule-details/rule-details.ui-component.ts b/projects/aca-folder-rules/src/lib/rule-details/rule-details.ui-component.ts
index 7cd0db06d..94b7ae8e6 100644
--- a/projects/aca-folder-rules/src/lib/rule-details/rule-details.ui-component.ts
+++ b/projects/aca-folder-rules/src/lib/rule-details/rule-details.ui-component.ts
@@ -30,6 +30,7 @@ import { distinctUntilChanged, map, takeUntil } from 'rxjs/operators';
 import { Rule } from '../model/rule.model';
 import { ruleCompositeConditionValidator } from './validators/rule-composite-condition.validator';
 import { FolderRulesService } from '../services/folder-rules.service';
+import { ActionDefinitionTransformed } from '../model/rule-action.model';
 
 @Component({
   selector: 'aca-rule-details',
@@ -68,7 +69,8 @@ export class RuleDetailsUiComponent implements OnInit, OnDestroy {
       isAsynchronous: newValue.isAsynchronous || FolderRulesService.emptyRule.isAsynchronous,
       errorScript: newValue.errorScript || FolderRulesService.emptyRule.errorScript,
       isInheritable: newValue.isInheritable || FolderRulesService.emptyRule.isInheritable,
-      isEnabled: newValue.isEnabled || FolderRulesService.emptyRule.isEnabled
+      isEnabled: newValue.isEnabled || FolderRulesService.emptyRule.isEnabled,
+      actions: newValue.actions || FolderRulesService.emptyRule.actions
     };
     if (this.form) {
       this.form.setValue(newValue);
@@ -78,6 +80,8 @@ export class RuleDetailsUiComponent implements OnInit, OnDestroy {
   }
   @Input()
   preview: boolean;
+  @Input()
+  actionDefinitions: ActionDefinitionTransformed[] = [];
 
   @Output()
   formValidationChanged = new EventEmitter<boolean>();
@@ -129,7 +133,8 @@ export class RuleDetailsUiComponent implements OnInit, OnDestroy {
       isAsynchronous: new UntypedFormControl(this.value.isAsynchronous),
       errorScript: new UntypedFormControl(this.value.errorScript),
       isInheritable: new UntypedFormControl(this.value.isInheritable),
-      isEnabled: new UntypedFormControl(this.value.isEnabled)
+      isEnabled: new UntypedFormControl(this.value.isEnabled),
+      actions: new UntypedFormControl(this.value.actions)
     });
     this.readOnly = this._readOnly;
 
diff --git a/projects/aca-folder-rules/src/lib/services/actions.service.spec.ts b/projects/aca-folder-rules/src/lib/services/actions.service.spec.ts
new file mode 100644
index 000000000..302f5c465
--- /dev/null
+++ b/projects/aca-folder-rules/src/lib/services/actions.service.spec.ts
@@ -0,0 +1,65 @@
+/*!
+ * @license
+ * Alfresco Example Content Application
+ *
+ * Copyright (C) 2005 - 2020 Alfresco Software Limited
+ *
+ * This file is part of the Alfresco Example Content Application.
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail.  Otherwise, the software is
+ * provided under the following open source license terms:
+ *
+ * The Alfresco Example Content Application is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * The Alfresco Example Content Application is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import { ActionsService } from './actions.service';
+import { TestBed } from '@angular/core/testing';
+import { CoreTestingModule } from '@alfresco/adf-core';
+import { ActionsApi } from '@alfresco/js-api';
+import { actionDefListMock, actionsTransformedListMock } from '../mock/actions.mock';
+import { take } from 'rxjs/operators';
+
+describe('ActionsService', () => {
+  let actionsService: ActionsService;
+
+  beforeEach(() => {
+    TestBed.configureTestingModule({
+      imports: [CoreTestingModule],
+      providers: [ActionsService]
+    });
+
+    actionsService = TestBed.inject(ActionsService);
+  });
+
+  it('should load the data into the observable', async () => {
+    spyOn(ActionsApi.prototype, 'listActions').and.returnValue(Promise.resolve(actionDefListMock));
+    const actionsPromise = actionsService.actionDefinitionsListing$.pipe(take(2)).toPromise();
+
+    actionsService.loadActionDefinitions();
+
+    const actionsList = await actionsPromise;
+    expect(actionsList).toEqual(actionsTransformedListMock);
+  });
+
+  it('should set loading to true while the request is being sent', async () => {
+    spyOn(ActionsApi.prototype, 'listActions').and.returnValue(Promise.resolve(actionDefListMock));
+    const loadingTruePromise = actionsService.loading$.pipe(take(2)).toPromise();
+    const loadingFalsePromise = actionsService.loading$.pipe(take(3)).toPromise();
+
+    actionsService.loadActionDefinitions();
+
+    expect(await loadingTruePromise).toBeTrue();
+    expect(await loadingFalsePromise).toBeFalse();
+  });
+});
diff --git a/projects/aca-folder-rules/src/lib/services/actions.service.ts b/projects/aca-folder-rules/src/lib/services/actions.service.ts
new file mode 100644
index 000000000..8294b1324
--- /dev/null
+++ b/projects/aca-folder-rules/src/lib/services/actions.service.ts
@@ -0,0 +1,92 @@
+/*!
+ * @license
+ * Alfresco Example Content Application
+ *
+ * Copyright (C) 2005 - 2020 Alfresco Software Limited
+ *
+ * This file is part of the Alfresco Example Content Application.
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail.  Otherwise, the software is
+ * provided under the following open source license terms:
+ *
+ * The Alfresco Example Content Application is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * The Alfresco Example Content Application is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import { Injectable } from '@angular/core';
+import { ActionDefinition, ActionDefinitionEntry, ActionDefinitionList, ActionParameterDefinition, ActionsApi } from '@alfresco/js-api';
+import { AlfrescoApiService } from '@alfresco/adf-core';
+import { BehaviorSubject, from } from 'rxjs';
+import { ActionDefinitionTransformed, ActionParameterDefinitionTransformed } from '../model/rule-action.model';
+import { finalize, map } from 'rxjs/operators';
+
+@Injectable({ providedIn: 'root' })
+export class ActionsService {
+  private actionDefinitionsListingSource = new BehaviorSubject<ActionDefinitionTransformed[]>([]);
+  actionDefinitionsListing$ = this.actionDefinitionsListingSource.asObservable();
+  private loadingSource = new BehaviorSubject<boolean>(false);
+  loading$ = this.loadingSource.asObservable();
+
+  private _actionsApi: ActionsApi;
+  get actionsApi(): ActionsApi {
+    if (!this._actionsApi) {
+      this._actionsApi = new ActionsApi(this.apiService.getInstance());
+    }
+    return this._actionsApi;
+  }
+
+  constructor(private apiService: AlfrescoApiService) {}
+
+  loadActionDefinitions() {
+    this.loadingSource.next(true);
+    from(this.actionsApi.listActions())
+      .pipe(
+        map((list: ActionDefinitionList) => list.list.entries.map((entry) => this.transformActionDefinition(entry))),
+        finalize(() => this.loadingSource.next(false))
+      )
+      .subscribe((obj: ActionDefinitionTransformed[]) => {
+        this.actionDefinitionsListingSource.next(obj);
+      });
+  }
+
+  private transformActionDefinition(obj: ActionDefinition | ActionDefinitionEntry): ActionDefinitionTransformed {
+    if (this.isActionDefinitionEntry(obj)) {
+      obj = obj.entry;
+    }
+    return {
+      id: obj.id,
+      name: obj.name ?? '',
+      description: obj.description ?? '',
+      title: obj.title ?? obj.name ?? '',
+      applicableTypes: obj.applicableTypes ?? [],
+      trackStatus: obj.trackStatus ?? false,
+      parameterDefinitions: (obj.parameterDefinitions ?? []).map((paramDef: ActionParameterDefinition) =>
+        this.transformActionParameterDefinition(paramDef)
+      )
+    };
+  }
+
+  private transformActionParameterDefinition(obj: ActionParameterDefinition): ActionParameterDefinitionTransformed {
+    return {
+      name: obj.name ?? '',
+      type: obj.type ?? '',
+      multiValued: obj.multiValued ?? false,
+      mandatory: obj.mandatory ?? false,
+      displayLabel: obj.displayLabel ?? obj.name ?? ''
+    };
+  }
+
+  private isActionDefinitionEntry(obj): obj is ActionDefinitionEntry {
+    return typeof obj.entry !== 'undefined';
+  }
+}
diff --git a/projects/aca-folder-rules/src/lib/services/folder-rules.service.spec.ts b/projects/aca-folder-rules/src/lib/services/folder-rules.service.spec.ts
index 87f33740a..fa99c3f79 100644
--- a/projects/aca-folder-rules/src/lib/services/folder-rules.service.spec.ts
+++ b/projects/aca-folder-rules/src/lib/services/folder-rules.service.spec.ts
@@ -146,11 +146,9 @@ describe('FolderRulesService', () => {
         .and.returnValue(Promise.resolve(dummyRules[0]));
     });
 
-    it('should send correct POST request and return created rule', function () {
-      folderRulesService.createRule(nodeId, dummyRules[0]).then((result) => {
-        expect(folderRulesService.createRule).toHaveBeenCalledWith(nodeId, dummyRules[0]);
-        expect(result).toEqual(dummyRules[0]);
-      });
+    it('should send correct POST request and return created rule', async () => {
+      const result = await folderRulesService.createRule(nodeId, dummyRules[0]);
+      expect(result).toEqual(dummyRules[0]);
     });
   });
 });