[ACS-3744] Folder rules styling fixes (#2753)

* Folder rules styling fixes

* Fix not showing isInheritable in read only
This commit is contained in:
Thomas Hunter 2022-11-02 12:51:40 +00:00 committed by GitHub
parent eee6feca1a
commit e354ec3891
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 468 additions and 267 deletions

View File

@ -8,7 +8,7 @@
"CREATE": "Create",
"CREATE_TITLE": "Create a rule",
"UPDATE": "Update",
"UPDATE_TITLE": "Update a rule"
"UPDATE_TITLE": "Edit a rule"
},
"RULE_DETAILS": {
"LABEL": {
@ -38,7 +38,7 @@
"IS_ASYNCHRONOUS": "Run rule in the background",
"DISABLE_RULE": "Disable rule",
"ERROR_SCRIPT": "If errors occur run script",
"SELECT_ACTION": "Select action"
"NO_SCRIPT": "None"
},
"COMPARATORS": {
"EQUALS": "(=) Equals",

View File

@ -37,7 +37,7 @@ import { RuleSimpleConditionUiComponent } from './rule-details/conditions/rule-s
import { GenericErrorModule, PageLayoutModule } from '@alfresco/aca-shared';
import { BreadcrumbModule, DocumentListModule } from '@alfresco/adf-content-services';
import { RuleListItemUiComponent } from './rules-list/rule/rule-list-item.ui-component';
import { RulesListUiComponent } from './rules-list/rules-list.ui-component';
import { RuleListUiComponent } from './rules-list/rule-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';
@ -70,7 +70,7 @@ const routes: Routes = [
RuleCompositeConditionUiComponent,
RuleDetailsUiComponent,
RuleSimpleConditionUiComponent,
RulesListUiComponent,
RuleListUiComponent,
RuleListItemUiComponent,
RuleTriggersUiComponent,
RuleOptionsUiComponent

View File

@ -30,14 +30,22 @@
<mat-divider></mat-divider>
<div class="aca-manage-rules__container" *ngIf="(rules$ | async).length > 0 ; else emptyContent">
<aca-rules-list [rules]="rules$ | async" (ruleSelected)="onRuleSelected($event)"
[selectedRule]="selectedRule" [nodeId]="nodeId"></aca-rules-list>
<aca-rule-list [rules]="rules$ | async" (ruleSelected)="onRuleSelected($event)"
[selectedRule]="selectedRule" [nodeId]="nodeId"></aca-rule-list>
<div class="aca-manage-rules__container__rule-details">
<div class="aca-manage-rules__container__preview">
<div class="aca-manage-rules__container__preview__toolbar">
<span>{{ selectedRule.name }}</span>
<div class="aca-manage-rules__container__preview__toolbar__buttons">
<button mat-icon-button (click)="onRuleDelete()" id="delete-rule-btn">
<div class="aca-manage-rules__container__rule-details__header">
<div class="aca-manage-rules__container__rule-details__header__title">
<div class="aca-manage-rules__container__rule-details__header__title__name">
{{ selectedRule.name }}
</div>
<div class="aca-manage-rules__container__rule-details__header__title__description">
{{ selectedRule.description }}
</div>
</div>
<div class="aca-manage-rules__container__rule-details__header__buttons">
<button mat-stroked-button (click)="onRuleDelete()" id="delete-rule-btn">
<mat-icon>delete_outline</mat-icon>
</button>
<button mat-stroked-button (click)="onRuleUpdate()" id="edit-rule-btn">
@ -45,8 +53,8 @@
</button>
</div>
</div>
<p>{{ selectedRule.description }}</p>
</div>
<div class="aca-manage-rules__container__rule-details__content">
<aca-rule-details
[actionDefinitions]="actionDefinitions$ | async"
[readOnly]="true"
@ -55,6 +63,7 @@
</aca-rule-details>
</div>
</div>
</div>
<ng-template #emptyContent>
<adf-empty-content

View File

@ -17,40 +17,65 @@
&__container {
display: grid;
grid-template-columns: minmax(200px,1fr) 3fr;
padding: 32px;
overflow: scroll;
grid-template-columns: minmax(250px,1fr) 3fr;
padding: 20px;
gap: 12px;
overflow-y: auto;
&__preview {
padding: 0 20px;
&__rule-details {
border: 1px solid var(--theme-border-color);
border-radius: 12px;
overflow-y: scroll;
&__toolbar {
&__header {
position: sticky;
top: 0;
z-index: 1;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
padding: 12px 20px;
border-bottom: 1px solid var(--theme-border-color);
background-color: var(--theme-background-color);
span {
font-style: normal;
font-weight: 700;
font-size: 14px;
line-height: 20px;
&__title {
display: flex;
flex-direction: column;
gap: 4px;
&__name {
font-size: 1.2em;
font-weight: bold;
}
&__description {
font-size: 0.8em;
font-style: italic;
}
}
&__buttons {
display: inline-block;
}
display: flex;
flex-direction: row;
align-items: stretch;
gap: 4px;
button {
color: var(--theme-text-color);
&#delete-rule-btn {
padding: 0 8px;
min-width: unset;
}
p {
font-style: normal;
font-weight: 400;
font-size: 12px;
line-height: 16px;
}
}
&__rule-details {
overflow-x: scroll;
.mat-icon {
// Something pops out of this button for some reason so this is necessary
overflow: hidden !important;
}
}
}
}
}
}
}

View File

@ -79,7 +79,7 @@ describe('ManageRulesSmartComponent', () => {
expect(folderRulesService.loadRules).toHaveBeenCalledOnceWith(component.nodeId);
const rules = debugElement.queryAll(By.css('.aca-rule'));
const rules = debugElement.queryAll(By.css('.aca-rule-list-item'));
const ruleDetails = debugElement.queryAll(By.css('aca-rule-details'));
const deleteRuleBtn = debugElement.query(By.css('#delete-rule-btn'));

View File

@ -41,3 +41,20 @@ export interface Rule {
conditions: RuleCompositeCondition;
actions: RuleAction[];
}
export interface RuleOptions {
isEnabled: boolean;
isInheritable: boolean;
isAsynchronous: boolean;
errorScript: string;
}
export interface RuleForForm {
id: string;
name: string;
description: string;
triggers: RuleTrigger[];
conditions: RuleCompositeCondition;
actions: RuleAction[];
options: RuleOptions;
}

View File

@ -1,34 +1,36 @@
<div class="options-list" [formGroup]="form">
<div class="options-list__asynchronous">
<ng-container [formGroup]="form">
<div class="aca-rule-options__option" *ngIf="!readOnly || isAsynchronousChecked">
<mat-checkbox
formControlName="isAsynchronous"
(change)="toggleScriptSelector()"
[attr.data-automation-id]="'rule-option-checkbox-asynchronous'">
(change)="toggleErrorScriptDropdown($event)"
data-automation-id="rule-option-checkbox-asynchronous">
{{ 'ACA_FOLDER_RULES.RULE_DETAILS.OPTIONS.IS_ASYNCHRONOUS' | translate }}
</mat-checkbox>
<div class="select-action" *ngIf="!preview">
<span>{{ 'ACA_FOLDER_RULES.RULE_DETAILS.OPTIONS.ERROR_SCRIPT' | translate}}:</span>
<mat-form-field>
<mat-select [disabled]="disableSelector" formControlName="errorScript"
placeholder="{{ 'ACA_FOLDER_RULES.RULE_DETAILS.OPTIONS.SELECT_ACTION' | translate}}"
[attr.data-automation-id]="'rule-option-select-errorScript'">
<mat-option>{{ 'ACA_FOLDER_RULES.RULE_DETAILS.OPTIONS.SELECT_ACTION' | translate}}</mat-option>
<mat-select
formControlName="errorScript"
placeholder="{{ 'ACA_FOLDER_RULES.RULE_DETAILS.OPTIONS.ERROR_SCRIPT' | translate}}"
data-automation-id="rule-option-select-errorScript">
<mat-option value="">{{ 'ACA_FOLDER_RULES.RULE_DETAILS.OPTIONS.NO_SCRIPT' | translate }}</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
<div class="options-list__rest">
<div class="aca-rule-options__option" *ngIf="!readOnly || isInheritableChecked">
<mat-checkbox
formControlName="isInheritable"
[attr.data-automation-id]="'rule-option-checkbox-inheritable'">
data-automation-id="rule-option-checkbox-inheritable">
{{ 'ACA_FOLDER_RULES.RULE_DETAILS.OPTIONS.IS_INHERITABLE' | translate }}
</mat-checkbox>
</div>
<div class="aca-rule-options__option" *ngIf="!readOnly">
<mat-checkbox
[attr.data-automation-id]="'rule-option-checkbox-enabled'"
[checked]="!form.get('isEnabled').value" *ngIf="!preview"
(change)="form.get('isEnabled').setValue(!$event.checked)">
formControlName="isDisabled"
data-automation-id="rule-option-checkbox-disabled">
{{ 'ACA_FOLDER_RULES.RULE_DETAILS.OPTIONS.DISABLE_RULE' | translate }}
</mat-checkbox>
</div>
</div>
</ng-container>

View File

@ -1,20 +1,14 @@
.options-list {
.aca-rule-options {
display: flex;
column-gap: 25px;
padding: 0.75em 0;
flex-direction: row;
gap: 24px;
&__rest {
&__option {
display: flex;
flex-wrap: wrap;
column-gap: 25px;
}
flex-direction: column;
}
.select-action {
margin-left: 5px;
margin-top: 12px;
span {
display: block;
&.read-only .mat-checkbox-inner-container {
display: none;
}
}

View File

@ -23,16 +23,16 @@
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
*/
import { ComponentFixture, TestBed, inject, waitForAsync } from '@angular/core/testing';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { CUSTOM_ELEMENTS_SCHEMA, DebugElement } from '@angular/core';
import { FormsModule, ReactiveFormsModule, FormBuilder } from '@angular/forms';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { RuleOptionsUiComponent } from './rule-options.ui-component';
import { CoreTestingModule } from '@alfresco/adf-core';
import { By } from '@angular/platform-browser';
describe('RuleOptionsUiComponent', () => {
let component: RuleOptionsUiComponent;
let fixture: ComponentFixture<RuleOptionsUiComponent>;
let component: RuleOptionsUiComponent;
const getByDataAutomationId = (dataAutomationId: string): DebugElement =>
fixture.debugElement.query(By.css(`[data-automation-id="${dataAutomationId}"]`));
@ -41,54 +41,75 @@ describe('RuleOptionsUiComponent', () => {
((getByDataAutomationId(dataAutomationId).nativeElement as HTMLElement).children[0] as HTMLElement).click();
};
beforeEach(
waitForAsync(() => {
beforeEach(() => {
TestBed.configureTestingModule({
schemas: [CUSTOM_ELEMENTS_SCHEMA],
imports: [FormsModule, ReactiveFormsModule, CoreTestingModule],
declarations: [RuleOptionsUiComponent]
}).compileComponents();
})
);
});
beforeEach(inject([FormBuilder], (fb: FormBuilder) => {
fixture = TestBed.createComponent(RuleOptionsUiComponent);
component = fixture.componentInstance;
component.form = fb.group({
isAsynchronous: [false],
isInheritable: [false],
isEnabled: [true],
errorScript: ['']
component.writeValue({
isEnabled: true,
isInheritable: false,
isAsynchronous: false,
errorScript: ''
});
});
fixture.detectChanges();
}));
it('checkboxes should be falsy by default, selector is disabled', () => {
expect(component).toBeTruthy();
it('should be able to write to the component', () => {
fixture.detectChanges();
expect(getByDataAutomationId('rule-option-checkbox-asynchronous').componentInstance.checked).toBeFalsy();
expect(getByDataAutomationId('rule-option-checkbox-inheritable').componentInstance.checked).toBeFalsy();
expect(getByDataAutomationId('rule-option-checkbox-enabled').componentInstance.checked).toBeFalsy();
expect(getByDataAutomationId('rule-option-checkbox-disabled').componentInstance.checked).toBeFalsy();
expect(getByDataAutomationId('rule-option-select-errorScript').componentInstance.disabled).toBeTruthy();
component.writeValue({
isEnabled: false,
isInheritable: true,
isAsynchronous: true,
errorScript: ''
});
fixture.detectChanges();
expect(getByDataAutomationId('rule-option-checkbox-asynchronous').componentInstance.checked).toBeTruthy();
expect(getByDataAutomationId('rule-option-checkbox-inheritable').componentInstance.checked).toBeTruthy();
expect(getByDataAutomationId('rule-option-checkbox-disabled').componentInstance.checked).toBeTruthy();
expect(getByDataAutomationId('rule-option-select-errorScript').componentInstance.disabled).toBeFalsy();
});
it('should enable selector when async checkbox is truthy', () => {
toggleMatCheckbox('rule-option-checkbox-asynchronous');
fixture.detectChanges();
toggleMatCheckbox('rule-option-checkbox-asynchronous');
fixture.detectChanges();
expect(getByDataAutomationId('rule-option-checkbox-asynchronous').componentInstance.checked).toBeTruthy();
expect(getByDataAutomationId('rule-option-select-errorScript').componentInstance.disabled).toBeFalsy();
});
it('should hide some fields in preview mode', () => {
component.preview = true;
it('should hide disabled checkbox and unselected checkboxes in read-only mode', () => {
component.readOnly = true;
fixture.detectChanges();
expect(getByDataAutomationId('rule-option-checkbox-asynchronous')).toBeFalsy();
expect(getByDataAutomationId('rule-option-checkbox-inheritable')).toBeFalsy();
expect(getByDataAutomationId('rule-option-checkbox-enabled')).toBeFalsy();
expect(getByDataAutomationId('rule-option-select-errorScript')).toBeFalsy();
component.writeValue({
isEnabled: false,
isInheritable: true,
isAsynchronous: true,
errorScript: ''
});
fixture.detectChanges();
expect(getByDataAutomationId('rule-option-checkbox-asynchronous')).toBeTruthy();
expect(getByDataAutomationId('rule-option-checkbox-asynchronous')).toBeTruthy();
expect(getByDataAutomationId('rule-option-checkbox-inheritable')).toBeTruthy();
expect(getByDataAutomationId('rule-option-checkbox-enabled')).toBeFalsy();
expect(getByDataAutomationId('rule-option-select-errorScript')).toBeFalsy();
expect(getByDataAutomationId('rule-option-select-errorScript')).toBeTruthy();
});
});

View File

@ -23,20 +23,98 @@
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
*/
import { Component, Input } from '@angular/core';
import { FormGroup } from '@angular/forms';
import { Component, forwardRef, HostBinding, OnDestroy, ViewEncapsulation } from '@angular/core';
import { AbstractControl, ControlValueAccessor, FormControl, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms';
import { MatCheckboxChange } from '@angular/material/checkbox';
import { RuleOptions } from '../../model/rule.model';
@Component({
selector: 'aca-rule-options',
templateUrl: 'rule-options.ui-component.html',
styleUrls: ['rule-options.ui-component.scss']
styleUrls: ['rule-options.ui-component.scss'],
encapsulation: ViewEncapsulation.None,
host: { class: 'aca-rule-options' },
providers: [
{
provide: NG_VALUE_ACCESSOR,
multi: true,
useExisting: forwardRef(() => RuleOptionsUiComponent)
}
]
})
export class RuleOptionsUiComponent {
@Input() form: FormGroup;
@Input() preview: boolean;
disableSelector = true;
export class RuleOptionsUiComponent implements ControlValueAccessor, OnDestroy {
form = new FormGroup({
isDisabled: new FormControl(),
isInheritable: new FormControl(),
isAsynchronous: new FormControl(),
errorScript: new FormControl()
});
toggleScriptSelector() {
this.disableSelector = !this.disableSelector;
formSubscription = this.form.valueChanges.subscribe((value: any) => {
this.onChange({
isEnabled: !value.isDisabled,
isInheritable: value.isInheritable,
isAsynchronous: value.isAsynchronous,
errorScript: value.errorScript ?? ''
});
this.onTouch();
});
@HostBinding('class.read-only')
readOnly = false;
onChange: (options: RuleOptions) => void = () => undefined;
onTouch: () => void = () => undefined;
get isAsynchronousChecked(): boolean {
return this.form.get('isAsynchronous').value;
}
get isInheritableChecked(): boolean {
return this.form.get('isInheritable').value;
}
writeValue(options: RuleOptions) {
const isAsynchronousFormControl = this.form.get('isAsynchronous');
const errorScriptFormControl = this.form.get('errorScript');
this.form.get('isDisabled').setValue(!options.isEnabled);
this.form.get('isInheritable').setValue(options.isInheritable);
this.form.get('isAsynchronous').setValue(options.isAsynchronous);
errorScriptFormControl.setValue(options.errorScript ?? '');
if (isAsynchronousFormControl.value) {
errorScriptFormControl.enable();
} else {
errorScriptFormControl.disable();
}
}
registerOnChange(fn: () => void) {
this.onChange = fn;
}
registerOnTouched(fn: () => void) {
this.onTouch = fn;
}
setDisabledState(isDisabled: boolean) {
if (isDisabled) {
this.form.disable();
this.readOnly = true;
} else {
this.form.enable();
this.readOnly = false;
}
}
ngOnDestroy() {
this.formSubscription.unsubscribe();
}
toggleErrorScriptDropdown(value: MatCheckboxChange) {
const formControl: AbstractControl = this.form.get('errorScript');
if (value.checked) {
formControl.enable();
} else {
formControl.disable();
}
}
}

View File

@ -1,7 +1,7 @@
<form class="aca-rule-details__form" [formGroup]="form">
<form class="aca-rule-details__form" [ngClass]="{ 'read-only': readOnly }" [formGroup]="form">
<ng-container *ngIf="!preview">
<div class="aca-rule-details__form__row">
<div class="aca-rule-details__form__row aca-rule-details__form__name">
<label for="rule-details-name-input">{{ 'ACA_FOLDER_RULES.RULE_DETAILS.LABEL.NAME' | translate }}</label>
<div>
<mat-form-field floatLabel='never'>
@ -14,7 +14,7 @@
</div>
</div>
<div class="aca-rule-details__form__row">
<div class="aca-rule-details__form__row aca-rule-details__form__description">
<label for="rule-details-description-textarea">{{ 'ACA_FOLDER_RULES.RULE_DETAILS.LABEL.DESCRIPTION' | translate }}</label>
<div>
<mat-form-field floatLabel='never'>
@ -28,8 +28,6 @@
</div>
</ng-container>
<hr>
<div class="aca-rule-details__form__row aca-rule-details__form__triggers">
<div class="label">{{ 'ACA_FOLDER_RULES.RULE_DETAILS.LABEL.WHEN' | translate }}</div>
<div>
@ -38,12 +36,10 @@
</div>
</div>
<hr>
<div class="aca-rule-details__form__conditions">
<aca-rule-composite-condition formControlName="conditions"></aca-rule-composite-condition>
<mat-error class="rule-details-error">{{ getErrorMessage(conditions) | translate }}</mat-error>
<hr>
</div>
<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>
@ -55,11 +51,9 @@
</aca-rule-action-list>
</div>
<hr>
<div class="aca-rule-details__form__row">
<div class="aca-rule-details__form__row" *ngIf="showOptionsSection">
<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>
<aca-rule-options formControlName="options" data-automation-id="rule-details-options-component"></aca-rule-options>
</div>
</form>

View File

@ -1,9 +1,31 @@
.aca-rule-details {
&__form {
padding: 20px;
position: relative;
& > div {
padding: 8px 20px;
width: 100%;
box-sizing: border-box;
overflow-x: auto;
&:not(:nth-child(1)) {
border-top: 1px solid var(--theme-border-color);
}
&.aca-rule-details__form__name {
padding-bottom: 0;
}
&.aca-rule-details__form__description {
padding-top: 0;
border: none;
align-items: flex-start;
}
}
&__row {
display: flex;
align-items: baseline;
gap: 8px;
& > label, & > .label {
@ -11,7 +33,7 @@
width: 20%;
min-width: 100px;
max-width: 150px;
padding-top: 0.75em;
padding: 0.75em 0;
}
& > div {
@ -19,6 +41,7 @@
mat-form-field {
width: 100%;
max-width: 400px;
font-size: inherit;
.mat-form-field-infix {
@ -28,22 +51,14 @@
}
}
&__triggers {
padding: 0.75em 0;
&__conditions {
width: 100%;
& > .label {
padding: 0;
}
}
hr {
border: none;
border-bottom: 1px solid var(--theme-border-color);
}
*:disabled, .mat-select-disabled .mat-select-value {
& > .rule-details-error {
margin-left: 16px;
color: inherit;
}
}
textarea {
min-height: 4em;
@ -54,15 +69,20 @@
height: 1em;
}
& > .aca-rule-composite-condition + .rule-details-error {
margin-left: 16px;
color: inherit;
}
&__actions {
.aca-rule-action-list {
flex: 1;
}
}
&.read-only, .mat-form-field-disabled {
.mat-form-field-underline, .mat-select-arrow-wrapper {
display: none;
}
*:disabled, .mat-select-disabled .mat-select-value {
color: inherit;
}
}
}
}

View File

@ -30,7 +30,6 @@ import { Rule } from '../model/rule.model';
import { By } from '@angular/platform-browser';
import { RuleCompositeConditionUiComponent } from './conditions/rule-composite-condition.ui-component';
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';
@ -46,7 +45,8 @@ describe('RuleDetailsUiComponent', () => {
triggers: ['update', 'outbound'],
isAsynchronous: true,
isInheritable: true,
isEnabled: true
isEnabled: true,
errorScript: ''
};
const getHtmlElement = <T>(dataAutomationId: string) =>
@ -79,16 +79,17 @@ describe('RuleDetailsUiComponent', () => {
const nameInput = getHtmlElement<HTMLInputElement>('rule-details-name-input');
const descriptionTextarea = getHtmlElement<HTMLTextAreaElement>('rule-details-description-textarea');
const ruleTriggersComponent = getComponentInstance<RuleTriggersUiComponent>('rule-details-triggers-component');
const ruleOptionAsynchronous = getComponentInstance<MatCheckbox>('rule-option-checkbox-asynchronous');
const ruleOptionInheritable = getComponentInstance<MatCheckbox>('rule-option-checkbox-inheritable');
const ruleOptionDisabled = getComponentInstance<MatCheckbox>('rule-option-checkbox-enabled');
const ruleOptionsComponent = getComponentInstance<RuleOptionsUiComponent>('rule-details-options-component');
expect(nameInput.value).toBe(testValue.name);
expect(descriptionTextarea.value).toBe(testValue.description);
expect(ruleTriggersComponent.value).toEqual(testValue.triggers);
expect(ruleOptionAsynchronous.checked).toBe(testValue.isAsynchronous);
expect(ruleOptionInheritable.checked).toBe(testValue.isInheritable);
expect(ruleOptionDisabled.checked).toBe(!testValue.isEnabled);
expect(ruleOptionsComponent.form.value).toEqual({
isDisabled: !testValue.isEnabled,
isInheritable: testValue.isInheritable,
isAsynchronous: testValue.isAsynchronous,
errorScript: testValue.errorScript
});
});
it('should modify the form if the value input property is modified', () => {
@ -109,38 +110,46 @@ describe('RuleDetailsUiComponent', () => {
});
it('should be editable if not read-only', () => {
component.value = testValue;
component.readOnly = false;
fixture.detectChanges();
const nameInput = getHtmlElement<HTMLInputElement>('rule-details-name-input');
const descriptionTextarea = getHtmlElement<HTMLTextAreaElement>('rule-details-description-textarea');
const ruleTriggersComponent = getComponentInstance<RuleTriggersUiComponent>('rule-details-triggers-component');
const ruleOptionAsynchronous = getComponentInstance<MatCheckbox>('rule-option-checkbox-asynchronous');
const ruleOptionInheritable = getComponentInstance<MatCheckbox>('rule-option-checkbox-inheritable');
const ruleOptionDisabled = getComponentInstance<MatCheckbox>('rule-option-checkbox-enabled');
const ruleOptionsComponent = getComponentInstance<RuleOptionsUiComponent>('rule-details-options-component');
expect(nameInput.disabled).toBeFalsy();
expect(descriptionTextarea.disabled).toBeFalsy();
expect(ruleTriggersComponent.readOnly).toBeFalsy();
expect(ruleOptionAsynchronous.disabled).toBeFalsy();
expect(ruleOptionInheritable.disabled).toBeFalsy();
expect(ruleOptionDisabled.disabled).toBeFalsy();
expect(ruleOptionsComponent.readOnly).toBeFalsy();
});
it('should not be editable if read-only', () => {
component.value = testValue;
component.readOnly = true;
fixture.detectChanges();
const nameInput = getHtmlElement<HTMLInputElement>('rule-details-name-input');
const descriptionTextarea = getHtmlElement<HTMLTextAreaElement>('rule-details-description-textarea');
const ruleTriggersComponent = getComponentInstance<RuleTriggersUiComponent>('rule-details-triggers-component');
const ruleOptionAsynchronous = getComponentInstance<MatCheckbox>('rule-option-checkbox-asynchronous');
const ruleOptionInheritable = getComponentInstance<MatCheckbox>('rule-option-checkbox-inheritable');
const ruleOptionsComponent = getComponentInstance<RuleOptionsUiComponent>('rule-details-options-component');
expect(nameInput.disabled).toBeTruthy();
expect(descriptionTextarea.disabled).toBeTruthy();
expect(ruleTriggersComponent.readOnly).toBeTruthy();
expect(ruleOptionAsynchronous.disabled).toBeTruthy();
expect(ruleOptionInheritable.disabled).toBeTruthy();
expect(ruleOptionsComponent.readOnly).toBeTruthy();
});
it('should hide the options section entirely in read-only mode if it has no selected options', () => {
component.value = {
...testValue,
isInheritable: false,
isAsynchronous: false
};
component.readOnly = true;
fixture.detectChanges();
expect(getComponentInstance<RuleOptionsUiComponent>('rule-details-options-component')).toBeFalsy();
});
});

View File

@ -27,7 +27,7 @@ import { Component, EventEmitter, Input, OnDestroy, OnInit, Output, ViewEncapsul
import { AbstractControl, UntypedFormGroup, UntypedFormControl, Validators } from '@angular/forms';
import { Subject } from 'rxjs';
import { distinctUntilChanged, map, takeUntil } from 'rxjs/operators';
import { Rule } from '../model/rule.model';
import { Rule, RuleForForm } 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';
@ -57,28 +57,38 @@ export class RuleDetailsUiComponent implements OnInit, OnDestroy {
}
}
}
private _initialValue: Partial<Rule> = FolderRulesService.emptyRule;
private _initialValue: RuleForForm = FolderRulesService.emptyRuleForForm;
@Input()
get value(): Partial<Rule> {
return this.form ? this.form.value : this._initialValue;
let value = this.form ? this.form.value : this._initialValue;
if (value.options) {
value = {
...value,
...(value.options ?? FolderRulesService.emptyRuleOptions)
};
delete value.options;
}
return value;
}
set value(newValue: Partial<Rule>) {
newValue = {
const newValueForForm: RuleForForm = {
id: newValue.id || FolderRulesService.emptyRule.id,
name: newValue.name || FolderRulesService.emptyRule.name,
description: newValue.description || FolderRulesService.emptyRule.description,
triggers: newValue.triggers || FolderRulesService.emptyRule.triggers,
conditions: newValue.conditions || FolderRulesService.emptyRule.conditions,
isAsynchronous: newValue.isAsynchronous || FolderRulesService.emptyRule.isAsynchronous,
errorScript: newValue.errorScript || FolderRulesService.emptyRule.errorScript,
isInheritable: newValue.isInheritable || FolderRulesService.emptyRule.isInheritable,
actions: newValue.actions || FolderRulesService.emptyRule.actions,
options: {
isEnabled: typeof newValue.isInheritable == 'boolean' ? newValue.isEnabled : FolderRulesService.emptyRule.isEnabled,
actions: newValue.actions || FolderRulesService.emptyRule.actions
isInheritable: newValue.isInheritable || FolderRulesService.emptyRule.isInheritable,
isAsynchronous: newValue.isAsynchronous || FolderRulesService.emptyRule.isAsynchronous,
errorScript: newValue.errorScript || FolderRulesService.emptyRule.errorScript
}
};
if (this.form) {
this.form.setValue(newValue);
this.form.setValue(newValueForForm);
} else {
this._initialValue = newValue;
this._initialValue = newValueForForm;
}
}
@Input()
@ -108,17 +118,9 @@ export class RuleDetailsUiComponent implements OnInit, OnDestroy {
get conditions(): UntypedFormControl {
return this.form.get('conditions') as UntypedFormControl;
}
get isAsynchronous(): UntypedFormControl {
return this.form.get('isAsynchronous') as UntypedFormControl;
}
get errorScript(): UntypedFormControl {
return this.form.get('errorScript') as UntypedFormControl;
}
get isInheritable(): UntypedFormControl {
return this.form.get('isInheritable') as UntypedFormControl;
}
get isEnabled(): UntypedFormControl {
return this.form.get('isEnabled') as UntypedFormControl;
get showOptionsSection(): boolean {
return !this.readOnly || this.value.isAsynchronous || this.value.isInheritable;
}
ngOnInit() {
@ -136,11 +138,13 @@ export class RuleDetailsUiComponent implements OnInit, OnDestroy {
},
ruleCompositeConditionValidator()
),
isAsynchronous: new UntypedFormControl(this.value.isAsynchronous),
errorScript: new UntypedFormControl(this.value.errorScript),
isInheritable: new UntypedFormControl(this.value.isInheritable),
isEnabled: new UntypedFormControl(this.value.isEnabled),
actions: new UntypedFormControl(this.value.actions, [Validators.required, ruleActionsValidator(this.actionDefinitions)])
actions: new UntypedFormControl(this.value.actions, [Validators.required, ruleActionsValidator(this.actionDefinitions)]),
options: new UntypedFormControl({
isEnabled: this.value.isEnabled,
isInheritable: this.value.isInheritable,
isAsynchronous: this.value.isAsynchronous,
errorScript: this.value.errorScript
})
});
this.readOnly = this._readOnly;
@ -155,8 +159,8 @@ export class RuleDetailsUiComponent implements OnInit, OnDestroy {
});
this.formValidationChanged.emit(this.form.valid);
this.form.valueChanges.pipe(takeUntil(this.onDestroy$)).subscribe((newFormValue: any) => {
this.formValueChanged.emit(newFormValue);
this.form.valueChanges.pipe(takeUntil(this.onDestroy$)).subscribe(() => {
this.formValueChanged.emit(this.value);
});
}

View File

@ -11,7 +11,6 @@
<mat-checkbox
[attr.data-automation-id]="'rule-trigger-checkbox-' + trigger | lowercase"
[checked]="isTriggerChecked(trigger)"
[disabled]="readOnly"
(change)="onTriggerChange(trigger, $event.checked)">
{{ 'ACA_FOLDER_RULES.RULE_DETAILS.TRIGGERS.' + trigger | uppercase | translate }}
</mat-checkbox>

View File

@ -57,7 +57,7 @@ export class RuleTriggersUiComponent implements ControlValueAccessor {
this.onChange = fn;
}
registerOnTouched(fn: any) {
registerOnTouched(fn: () => void) {
this.onTouch = fn;
}

View File

@ -0,0 +1,10 @@
<div class="aca-rules-list" >
<aca-rule-list-item
matRipple matRippleColor="hsla(0,0%,0%,0.05)"
*ngFor="let rule of rules"
[rule]="rule"
[isSelected]="isSelected(rule)"
[nodeId]="nodeId"
(click)="onRuleClicked(rule)">
</aca-rule-list-item>
</div>

View File

@ -0,0 +1,5 @@
.aca-rule-list {
display: flex;
flex-direction: column;
gap: 4px;
}

View File

@ -24,25 +24,25 @@
*/
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { RulesListUiComponent } from './rules-list.ui-component';
import { RuleListUiComponent } from './rule-list.ui-component';
import { dummyRules } from '../mock/rules.mock';
import { DebugElement } from '@angular/core';
import { By } from '@angular/platform-browser';
import { CoreTestingModule } from '@alfresco/adf-core';
import { AcaFolderRulesModule } from '@alfresco/aca-folder-rules';
describe('RulesListComponent', () => {
let component: RulesListUiComponent;
let fixture: ComponentFixture<RulesListUiComponent>;
describe('RuleListComponent', () => {
let component: RuleListUiComponent;
let fixture: ComponentFixture<RuleListUiComponent>;
let debugElement: DebugElement;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [CoreTestingModule, AcaFolderRulesModule],
declarations: [RulesListUiComponent]
declarations: [RuleListUiComponent]
});
fixture = TestBed.createComponent(RulesListUiComponent);
fixture = TestBed.createComponent(RuleListUiComponent);
component = fixture.componentInstance;
debugElement = fixture.debugElement;
});
@ -54,17 +54,17 @@ describe('RulesListComponent', () => {
fixture.detectChanges();
const rules = debugElement.queryAll(By.css('.aca-rule'));
const rules = debugElement.queryAll(By.css('.aca-rule-list-item'));
expect(rules).toBeTruthy('Could not find rules');
expect(rules.length).toBe(2, 'Unexpected number of rules');
const rule = debugElement.query(By.css('.aca-rule:first-child'));
const title = rule.query(By.css('.rule-info__header__title'));
const description = rule.query(By.css('p'));
const rule = debugElement.query(By.css('.aca-rule-list-item:first-child'));
const name = rule.query(By.css('.aca-rule-list-item__header__name'));
const description = rule.query(By.css('.aca-rule-list-item__description'));
const toggleBtn = rule.query(By.css('mat-slide-toggle'));
expect(title.nativeElement.textContent).toBe(dummyRules[0].name);
expect(name.nativeElement.textContent).toBe(dummyRules[0].name);
expect(toggleBtn).toBeTruthy();
expect(description.nativeElement.textContent).toBe(dummyRules[0].description);
});

View File

@ -23,21 +23,21 @@
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
*/
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { Component, EventEmitter, Input, Output, ViewEncapsulation } from '@angular/core';
import { Rule } from '../model/rule.model';
@Component({
selector: 'aca-rules-list',
templateUrl: 'rules-list.ui-component.html',
styleUrls: ['rules-list.ui-component.scss']
selector: 'aca-rule-list',
templateUrl: 'rule-list.ui-component.html',
styleUrls: ['rule-list.ui-component.scss'],
encapsulation: ViewEncapsulation.None,
host: { class: 'aca-rule-list' }
})
export class RulesListUiComponent {
export class RuleListUiComponent {
@Input()
rules: Rule[];
@Input()
selectedRule: Rule;
@Input()
nodeId: string;

View File

@ -1,9 +1,5 @@
<div class="aca-rule" [class.selected]="isSelected" >
<div class="rule-info">
<div class="rule-info__header">
<span class="rule-info__header__title">{{rule.name}}</span>
<div class="aca-rule-list-item__header">
<span class="aca-rule-list-item__header__name">{{ rule.name }}</span>
<mat-slide-toggle [(ngModel)]="rule.isEnabled" (click)="onToggleClick(!rule.isEnabled)"></mat-slide-toggle>
</div>
<p>{{rule.description}}</p>
</div>
</div>
<div class="aca-rule-list-item__description">{{ rule.description }}</div>

View File

@ -1,6 +1,8 @@
.aca-rule{
.aca-rule-list-item {
display: flex;
padding: 16px 24px;
flex-direction: column;
gap: 4px;
padding: 12px 20px;
border-radius: 12px;
margin-bottom: 8px;
cursor: pointer;
@ -13,24 +15,24 @@
font-size: 12px;
line-height: 16px;
}
}
.rule-info{
width: 100%;
&__header {
display: flex;
justify-content: space-between;
align-items: flex-end;
&__title{
font-weight: 900;
font-size: 14px;
color: #212121;
line-height: 20px;
}
&__name {
font-size: 1.2em;
font-weight: bold;
}
}
.selected{
background: rgba(31, 116, 219, 0.24);
&__description {
font-size: 0.8em;
font-style: italic;
}
&.selected {
background: var(--theme-selected-button-bg-color);
}
}

View File

@ -23,19 +23,25 @@
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
*/
import { Component, Input } from '@angular/core';
import { Component, HostBinding, Input, ViewEncapsulation } from '@angular/core';
import { Rule } from '../../model/rule.model';
import { FolderRulesService } from '../../services/folder-rules.service';
@Component({
selector: 'aca-rule',
selector: 'aca-rule-list-item',
templateUrl: 'rule-list-item.ui-component.html',
styleUrls: ['rule-list-item.ui-component.scss']
styleUrls: ['rule-list-item.ui-component.scss'],
encapsulation: ViewEncapsulation.None,
host: { class: 'aca-rule-list-item' }
})
export class RuleListItemUiComponent {
@Input() rule: Rule;
@Input() isSelected: boolean;
@Input() nodeId: string;
@Input()
rule: Rule;
@Input()
nodeId: string;
@Input()
@HostBinding('class.selected')
isSelected: boolean;
constructor(private folderRulesService: FolderRulesService) {}

View File

@ -1,4 +0,0 @@
<div class="aca-rules-list" >
<aca-rule *ngFor="let rule of rules" [rule]="rule" (click)="onRuleClicked(rule)" [isSelected]="isSelected(rule)"
[nodeId]="nodeId"></aca-rule>
</div>

View File

@ -1,3 +0,0 @@
.aca-rules-list {
margin-right: 24px;
}

View File

@ -27,7 +27,7 @@ import { Injectable } from '@angular/core';
import { AlfrescoApiService } from '@alfresco/adf-core';
import { BehaviorSubject, forkJoin, from, Observable, of } from 'rxjs';
import { catchError, finalize, map } from 'rxjs/operators';
import { Rule } from '../model/rule.model';
import { Rule, RuleForForm, RuleOptions } from '../model/rule.model';
import { ContentApiService } from '@alfresco/aca-shared';
import { NodeInfo } from '@alfresco/aca-shared/store';
import { RuleCompositeCondition } from '../model/rule-composite-condition.model';
@ -46,22 +46,39 @@ export class FolderRulesService {
};
}
public static get emptyRuleOptions(): RuleOptions {
return {
isEnabled: true,
isInheritable: false,
isAsynchronous: false,
errorScript: ''
};
}
public static get emptyRule(): Rule {
return {
id: '',
name: '',
description: '',
isEnabled: true,
isInheritable: false,
isAsynchronous: false,
errorScript: '',
isShared: false,
triggers: ['inbound'],
conditions: FolderRulesService.emptyCompositeCondition,
actions: []
actions: [],
...FolderRulesService.emptyRuleOptions
};
}
public static get emptyRuleForForm(): RuleForForm {
const value = {
...FolderRulesService.emptyRule,
options: FolderRulesService.emptyRuleOptions
};
Object.keys(value.options).forEach((key: string) => {
delete value[key];
});
return value;
}
private rulesListingSource = new BehaviorSubject<Rule[]>([]);
rulesListing$: Observable<Rule[]> = this.rulesListingSource.asObservable();
private folderInfoSource = new BehaviorSubject<NodeInfo>(null);