From 2c69887e08b1b0124c396f1925f11c196cca9d7c Mon Sep 17 00:00:00 2001 From: AleksanderSklorz <115619721+AleksanderSklorz@users.noreply.github.com> Date: Tue, 28 Nov 2023 14:09:00 +0100 Subject: [PATCH] [ACS-6252] support disabling the tags and categories feature in the applications (#3533) * ACS-6252 Allow to hide tags and categories from metadata panel and to hide tags column from personal files * ACS-6252 Allow to hide tags column from all other lists * ACS-6252 Allow to hide tags and categories from search filters * ACS-6252 Set type for search field * ACS-6252 Hide displaying tags and categories related operators, properties and aspects in folder rules when that feature is disabled * ACS-6252 Get from service information if tags and categories are disabled * ACS-6252 Handled case when tags and categories configuration is missing in app.config.json * ACS-6252 Unit tests for changes for RuleActionUiComponent * ACS-6252 Unit tests for changes for RuleSimpleConditionUiComponent * ACS-6252 Unit tests for changes for MetadataTabComponent * ACS-6252 Unit tests for changes for app rules * ACS-6252 Unit tests for changes for AppExtensionService * ACS-6252 Removed redundant private from constructor parameter and corrected unit test title * ACS-6252 Hide link to category action if categories feature is disabled * ACS-6252 Move to beforeEach --- app/src/app.config.json | 4 +- .../aca-content/assets/app.extensions.json | 26 ++++- .../mock/action-parameter-constraints.mock.ts | 44 +++++++ .../actions/rule-action.ui-component.spec.ts | 110 +++++++++++++++--- .../actions/rule-action.ui-component.ts | 35 +++++- ...rule-simple-condition.ui-component.spec.ts | 84 ++++++++++--- .../rule-simple-condition.ui-component.ts | 15 ++- .../rule-details.ui-component.spec.ts | 52 +++++++++ .../rule-details/rule-details.ui-component.ts | 5 + .../aca-content/src/lib/aca-content.module.ts | 4 +- .../metadata-tab.component.spec.ts | 69 +++++++++-- .../metadata-tab/metadata-tab.component.ts | 26 ++++- .../aca-shared/rules/src/app.rules.spec.ts | 66 +++++++++++ projects/aca-shared/rules/src/app.rules.ts | 4 + .../services/app.extension.service.spec.ts | 49 +++++++- .../src/lib/services/app.extension.service.ts | 6 +- 16 files changed, 535 insertions(+), 64 deletions(-) diff --git a/app/src/app.config.json b/app/src/app.config.json index d7cfd1e774..c28817d8e8 100644 --- a/app/src/app.config.json +++ b/app/src/app.config.json @@ -12,7 +12,9 @@ "plugins": { "aosPlugin": true, "contentService": true, - "folderRules": true + "folderRules": true, + "tags": true, + "categories": true }, "oauth2": { "host": "{protocol}//{hostname}{:port}/auth/realms/alfresco", diff --git a/projects/aca-content/assets/app.extensions.json b/projects/aca-content/assets/app.extensions.json index 2046ea4c98..a52f7c55b5 100644 --- a/projects/aca-content/assets/app.extensions.json +++ b/projects/aca-content/assets/app.extensions.json @@ -1495,6 +1495,9 @@ "allowOnlyPredefinedValues": true, "field": "TAG" } + }, + "rules": { + "visible": "app.areTagsEnabled" } }, { @@ -1509,6 +1512,9 @@ "allowOnlyPredefinedValues": true, "field": "cm:categories" } + }, + "rules": { + "visible": "app.areCategoriesEnabled" } } ] @@ -2160,7 +2166,10 @@ "type": "text", "sortable": false, "desktopOnly": true, - "order": 60 + "order": 60, + "rules": { + "visible": "app.areTagsEnabled" + } } ], "libraries": [ @@ -2357,7 +2366,10 @@ "type": "text", "sortable": false, "desktopOnly": true, - "order": 80 + "order": 80, + "rules": { + "visible": "app.areTagsEnabled" + } } ], "recent": [ @@ -2422,7 +2434,10 @@ "type": "text", "sortable": false, "desktopOnly": true, - "order": 60 + "order": 60, + "rules": { + "visible": "app.areTagsEnabled" + } } ], "favorites": [ @@ -2497,7 +2512,10 @@ "type": "text", "sortable": false, "desktopOnly": true, - "order": 70 + "order": 70, + "rules": { + "visible": "app.areTagsEnabled" + } } ], "trashcan": [ diff --git a/projects/aca-content/folder-rules/src/mock/action-parameter-constraints.mock.ts b/projects/aca-content/folder-rules/src/mock/action-parameter-constraints.mock.ts index 46899d0615..70078cf6ff 100644 --- a/projects/aca-content/folder-rules/src/mock/action-parameter-constraints.mock.ts +++ b/projects/aca-content/folder-rules/src/mock/action-parameter-constraints.mock.ts @@ -62,3 +62,47 @@ export const rawConstraints = { constraintName: 'ac-aspects' } }; + +export const dummyTagsConstraints: ActionParameterConstraint[] = [ + { + name: 'aspect-name', + constraints: [ + { + value: 'cm:tagscope', + label: 'Label 1' + }, + { + value: 'cm:tagScopeCache', + label: 'Label 2' + }, + { + value: 'cm:notTagRelated', + label: 'Label 3' + }, + { + value: 'cm:taggable', + label: 'Label 4' + } + ] + } +]; + +export const dummyCategoriesConstraints: ActionParameterConstraint[] = [ + { + name: 'aspect-name', + constraints: [ + { + value: 'cm:categories', + label: 'Label 1' + }, + { + value: 'cm:notCategoryRelated', + label: 'Label 2' + }, + { + value: 'cm:generalclassifiable', + label: 'Label 3' + } + ] + } +]; diff --git a/projects/aca-content/folder-rules/src/rule-details/actions/rule-action.ui-component.spec.ts b/projects/aca-content/folder-rules/src/rule-details/actions/rule-action.ui-component.spec.ts index 524707ec8c..9aed9534df 100644 --- a/projects/aca-content/folder-rules/src/rule-details/actions/rule-action.ui-component.spec.ts +++ b/projects/aca-content/folder-rules/src/rule-details/actions/rule-action.ui-component.spec.ts @@ -26,26 +26,27 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { CardViewBoolItemModel, CardViewComponent, CardViewSelectItemModel, CardViewTextItemModel, CoreTestingModule } from '@alfresco/adf-core'; import { RuleActionUiComponent } from './rule-action.ui-component'; import { actionLinkToCategoryTransformedMock, actionsTransformedListMock } from '../../mock/actions.mock'; -import { DebugElement } from '@angular/core'; import { By } from '@angular/platform-browser'; -import { dummyConstraints } from '../../mock/action-parameter-constraints.mock'; +import { dummyCategoriesConstraints, dummyConstraints, dummyTagsConstraints } from '../../mock/action-parameter-constraints.mock'; +import { CategoryService, TagService } from '@alfresco/adf-content-services'; +import { MatSelect } from '@angular/material/select'; describe('RuleActionUiComponent', () => { let fixture: ComponentFixture; let component: RuleActionUiComponent; - const getByDataAutomationId = (dataAutomationId: string): DebugElement => - fixture.debugElement.query(By.css(`[data-automation-id="${dataAutomationId}"]`)); + const getSelectElement = (): HTMLElement => fixture.debugElement.query(By.directive(MatSelect)).nativeElement; - const changeMatSelectValue = (dataAutomationId: string, value: string) => { - const matSelect = getByDataAutomationId(dataAutomationId).nativeElement; - matSelect.click(); + const changeMatSelectValue = (value: string) => { + getSelectElement().click(); fixture.detectChanges(); const matOption = fixture.debugElement.query(By.css(`.mat-option[ng-reflect-value="${value}"]`)).nativeElement; matOption.click(); fixture.detectChanges(); }; + const getPropertiesCardView = (): CardViewComponent => fixture.debugElement.query(By.directive(CardViewComponent)).componentInstance; + beforeEach(() => { TestBed.configureTestingModule({ imports: [CoreTestingModule, RuleActionUiComponent] @@ -59,8 +60,7 @@ describe('RuleActionUiComponent', () => { component.actionDefinitions = actionsTransformedListMock; fixture.detectChanges(); - const matSelect = getByDataAutomationId('rule-action-select').nativeElement; - matSelect.click(); + getSelectElement().click(); fixture.detectChanges(); const matOptions = fixture.debugElement.queryAll(By.css(`mat-option`)); @@ -74,10 +74,10 @@ describe('RuleActionUiComponent', () => { component.parameterConstraints = dummyConstraints; fixture.detectChanges(); - const cardView = getByDataAutomationId('rule-action-card-view').componentInstance as CardViewComponent; + const cardView = getPropertiesCardView(); expect(cardView.properties.length).toBe(0); - changeMatSelectValue('rule-action-select', 'mock-action-1-definition'); + changeMatSelectValue('mock-action-1-definition'); expect(cardView.properties.length).toBe(5); expect(cardView.properties[0]).toBeInstanceOf(CardViewTextItemModel); @@ -86,7 +86,7 @@ describe('RuleActionUiComponent', () => { expect(cardView.properties[3]).toBeInstanceOf(CardViewTextItemModel); expect(cardView.properties[4]).toBeInstanceOf(CardViewSelectItemModel); - changeMatSelectValue('rule-action-select', 'mock-action-2-definition'); + changeMatSelectValue('mock-action-2-definition'); expect(cardView.properties.length).toBe(0); }); @@ -95,13 +95,95 @@ describe('RuleActionUiComponent', () => { component.parameterConstraints = dummyConstraints; fixture.detectChanges(); - const cardView = getByDataAutomationId('rule-action-card-view').componentInstance as CardViewComponent; + const cardView = getPropertiesCardView(); expect(cardView.properties.length).toBe(0); - changeMatSelectValue('rule-action-select', 'mock-action-3-definition'); + changeMatSelectValue('mock-action-3-definition'); expect(cardView.properties[0].icon).toBeFalsy(); expect(cardView.properties[0].value).toBeFalsy(); expect(cardView.properties[0]).toBeInstanceOf(CardViewTextItemModel); }); + + describe('Select options', () => { + beforeEach(() => { + component.actionDefinitions = actionsTransformedListMock; + }); + + it('should not filter out tags related options if tagService.areTagsEnabled returns true', (done) => { + component.parameterConstraints = dummyTagsConstraints; + const tagService = TestBed.inject(TagService); + spyOn(tagService, 'areTagsEnabled').and.returnValue(true); + fixture.detectChanges(); + + changeMatSelectValue('mock-action-1-definition'); + expect(tagService.areTagsEnabled).toHaveBeenCalled(); + (getPropertiesCardView().properties[2] as CardViewSelectItemModel).options$.subscribe((options) => { + expect(options).toEqual( + dummyTagsConstraints[0].constraints.map((constraint) => ({ + key: constraint.value, + label: `${constraint.label} [${constraint.value}]` + })) + ); + done(); + }); + }); + + it('should filter out tags related options if tagService.areTagsEnabled returns false', (done) => { + component.parameterConstraints = dummyTagsConstraints; + const tagService = TestBed.inject(TagService); + spyOn(tagService, 'areTagsEnabled').and.returnValue(false); + fixture.detectChanges(); + + changeMatSelectValue('mock-action-1-definition'); + expect(tagService.areTagsEnabled).toHaveBeenCalled(); + (getPropertiesCardView().properties[2] as CardViewSelectItemModel).options$.subscribe((options) => { + expect(options).toEqual([ + { + key: 'cm:notTagRelated', + label: 'Label 3 [cm:notTagRelated]' + } + ]); + done(); + }); + }); + + it('should not filter out categories related options if categoryService.areCategoriesEnabled returns true', (done) => { + component.parameterConstraints = dummyCategoriesConstraints; + const categoriesService = TestBed.inject(CategoryService); + spyOn(categoriesService, 'areCategoriesEnabled').and.returnValue(true); + fixture.detectChanges(); + + changeMatSelectValue('mock-action-1-definition'); + expect(categoriesService.areCategoriesEnabled).toHaveBeenCalled(); + (getPropertiesCardView().properties[2] as CardViewSelectItemModel).options$.subscribe((options) => { + expect(options).toEqual( + dummyCategoriesConstraints[0].constraints.map((constraint) => ({ + key: constraint.value, + label: `${constraint.label} [${constraint.value}]` + })) + ); + done(); + }); + }); + + it('should filter out categories related options if categoryService.areCategoriesEnabled returns false', (done) => { + component.parameterConstraints = dummyCategoriesConstraints; + const categoryService = TestBed.inject(CategoryService); + spyOn(categoryService, 'areCategoriesEnabled').and.returnValue(false); + fixture.detectChanges(); + + changeMatSelectValue('mock-action-1-definition'); + expect(categoryService.areCategoriesEnabled).toHaveBeenCalled(); + (getPropertiesCardView().properties[2] as CardViewSelectItemModel).options$.subscribe((options) => { + expect(options).toEqual([ + { + key: 'cm:notCategoryRelated', + label: 'Label 2 [cm:notCategoryRelated]' + } + ]); + done(); + }); + }); + }); }); diff --git a/projects/aca-content/folder-rules/src/rule-details/actions/rule-action.ui-component.ts b/projects/aca-content/folder-rules/src/rule-details/actions/rule-action.ui-component.ts index a058933ee6..04ab305108 100644 --- a/projects/aca-content/folder-rules/src/rule-details/actions/rule-action.ui-component.ts +++ b/projects/aca-content/folder-rules/src/rule-details/actions/rule-action.ui-component.ts @@ -37,9 +37,15 @@ import { } from '@alfresco/adf-core'; import { ActionParameterDefinition, Node } from '@alfresco/js-api'; import { of, Subject } from 'rxjs'; -import { takeUntil } from 'rxjs/operators'; +import { map, takeUntil } from 'rxjs/operators'; import { ActionParameterConstraint, ConstraintValue } from '../../model/action-parameter-constraint.model'; -import { ContentNodeSelectorComponent, ContentNodeSelectorComponentData, NodeAction } from '@alfresco/adf-content-services'; +import { + CategoryService, + ContentNodeSelectorComponent, + ContentNodeSelectorComponentData, + NodeAction, + TagService +} from '@alfresco/adf-content-services'; import { MatDialog } from '@angular/material/dialog'; import { TranslateModule, TranslateService } from '@ngx-translate/core'; import { CommonModule } from '@angular/common'; @@ -82,6 +88,9 @@ export class RuleActionUiComponent implements ControlValueAccessor, OnInit, OnCh this._parameterConstraints = value.map((obj) => ({ ...obj, constraints: this.parseConstraintsToSelectOptions(obj.constraints) })); } + private readonly tagsRelatedPropertiesAndAspects = ['cm:tagscope', 'cm:tagScopeCache', 'cm:taggable']; + private readonly categoriesRelatedPropertiesAndAspects = ['cm:categories', 'cm:generalclassifiable']; + isFullWidth = false; form = new FormGroup({ @@ -107,7 +116,13 @@ export class RuleActionUiComponent implements ControlValueAccessor, OnInit, OnCh onChange: (action: RuleAction) => void = () => undefined; onTouch: () => void = () => undefined; - constructor(private cardViewUpdateService: CardViewUpdateService, private dialog: MatDialog, private translate: TranslateService) {} + constructor( + private cardViewUpdateService: CardViewUpdateService, + private dialog: MatDialog, + private translate: TranslateService, + private tagService: TagService, + private categoryService: CategoryService + ) {} writeValue(action: RuleAction) { this.form.setValue({ @@ -170,6 +185,8 @@ export class RuleActionUiComponent implements ControlValueAccessor, OnInit, OnCh } setCardViewProperties() { + const disabledTags = !this.tagService.areTagsEnabled(); + const disabledCategories = !this.categoryService.areCategoriesEnabled(); this.cardViewItems = (this.selectedActionDefinition?.parameterDefinitions ?? []).map((paramDef) => { this.isFullWidth = false; const constraintsForDropdownBox = this._parameterConstraints.find((obj) => obj.name === paramDef.name); @@ -212,7 +229,17 @@ export class RuleActionUiComponent implements ControlValueAccessor, OnInit, OnCh return new CardViewSelectItemModel({ ...cardViewPropertiesModel, value: (this.parameters[paramDef.name] as string) ?? '', - options$: of(constraintsForDropdownBox.constraints) + options$: of(constraintsForDropdownBox.constraints).pipe( + map((options) => { + return options.filter( + (option) => + !( + (disabledTags && this.tagsRelatedPropertiesAndAspects.includes(option.key)) || + (disabledCategories && this.categoriesRelatedPropertiesAndAspects.includes(option.key)) + ) + ); + }) + ) }); } return new CardViewTextItemModel({ diff --git a/projects/aca-content/folder-rules/src/rule-details/conditions/rule-simple-condition.ui-component.spec.ts b/projects/aca-content/folder-rules/src/rule-details/conditions/rule-simple-condition.ui-component.spec.ts index adbb87c3e3..a211c13b0a 100644 --- a/projects/aca-content/folder-rules/src/rule-details/conditions/rule-simple-condition.ui-component.spec.ts +++ b/projects/aca-content/folder-rules/src/rule-details/conditions/rule-simple-condition.ui-component.spec.ts @@ -29,15 +29,19 @@ import { By } from '@angular/platform-browser'; import { DebugElement } from '@angular/core'; import { tagMock, mimeTypeMock, simpleConditionUnknownFieldMock, categoriesListMock } from '../../mock/conditions.mock'; import { MimeType } from './rule-mime-types'; -import { CategoryService } from '@alfresco/adf-content-services'; +import { CategoryService, TagService } from '@alfresco/adf-content-services'; import { of } from 'rxjs'; import { RuleSimpleCondition } from '../../model/rule-simple-condition.model'; import { delay } from 'rxjs/operators'; +import { MatOption } from '@angular/material/core'; +import { RuleConditionField, ruleConditionFields } from './rule-condition-fields'; describe('RuleSimpleConditionUiComponent', () => { let fixture: ComponentFixture; let categoryService: CategoryService; + const fieldSelectAutomationId = 'field-select'; + const getByDataAutomationId = (dataAutomationId: string): DebugElement => fixture.debugElement.query(By.css(`[data-automation-id="${dataAutomationId}"]`)); @@ -57,6 +61,18 @@ describe('RuleSimpleConditionUiComponent', () => { fixture.detectChanges(); }; + const expectConditionFieldsDisplayedAsOptions = (conditionFields: RuleConditionField[]): void => { + fixture.detectChanges(); + getByDataAutomationId(fieldSelectAutomationId).nativeElement.click(); + fixture.detectChanges(); + const options = fixture.debugElement.queryAll(By.directive(MatOption)); + conditionFields.forEach((field, i) => { + const option = options[i]; + expect(field.name).toBe(option.componentInstance.value); + expect(field.label).toBe(option.nativeElement.textContent.trim()); + }); + }; + beforeEach(() => { TestBed.configureTestingModule({ imports: [CoreTestingModule, RuleSimpleConditionUiComponent] @@ -69,7 +85,7 @@ describe('RuleSimpleConditionUiComponent', () => { it('should default the field to the name, the comparator to equals and the value empty', () => { fixture.detectChanges(); - expect(getByDataAutomationId('field-select').componentInstance.value).toBe('cm:name'); + expect(getByDataAutomationId(fieldSelectAutomationId).componentInstance.value).toBe('cm:name'); expect(getByDataAutomationId('comparator-select').componentInstance.value).toBe('equals'); expect(getByDataAutomationId('value-input').nativeElement.value).toBe(''); }); @@ -81,7 +97,7 @@ describe('RuleSimpleConditionUiComponent', () => { expect(fixture.componentInstance.isComparatorHidden).toBeFalsy(); expect(getComputedStyle(comparatorFormField).display).not.toBe('none'); - changeMatSelectValue('field-select', 'category'); + changeMatSelectValue(fieldSelectAutomationId, 'category'); expect(fixture.componentInstance.isComparatorHidden).toBeTruthy(); expect(getComputedStyle(comparatorFormField).display).toBe('none'); @@ -94,7 +110,7 @@ describe('RuleSimpleConditionUiComponent', () => { expect(fixture.componentInstance.isComparatorHidden).toBeFalsy(); expect(getComputedStyle(comparatorFormField).display).not.toBe('none'); - changeMatSelectValue('field-select', 'mimetype'); + changeMatSelectValue(fieldSelectAutomationId, 'mimetype'); expect(fixture.componentInstance.isComparatorHidden).toBeTruthy(); expect(getComputedStyle(comparatorFormField).display).toBe('none'); @@ -108,7 +124,7 @@ describe('RuleSimpleConditionUiComponent', () => { expect(fixture.componentInstance.isComparatorHidden).toBeFalsy(); expect(getComputedStyle(comparatorFormField).display).not.toBe('none'); - changeMatSelectValue('field-select', autoCompleteField); + changeMatSelectValue(fieldSelectAutomationId, autoCompleteField); expect(fixture.componentInstance.isComparatorHidden).toBeTruthy(); expect(getComputedStyle(comparatorFormField).display).toBe('none'); @@ -120,11 +136,11 @@ describe('RuleSimpleConditionUiComponent', () => { changeMatSelectValue('comparator-select', 'contains'); expect(getByDataAutomationId('comparator-select').componentInstance.value).toBe('contains'); - changeMatSelectValue('field-select', 'mimetype'); + changeMatSelectValue(fieldSelectAutomationId, 'mimetype'); expect(onChangeFieldSpy).toHaveBeenCalledTimes(1); expect(getByDataAutomationId('comparator-select').componentInstance.value).toBe('equals'); - changeMatSelectValue('field-select', 'size'); + changeMatSelectValue(fieldSelectAutomationId, 'size'); expect(onChangeFieldSpy).toHaveBeenCalledTimes(2); expect(getByDataAutomationId('comparator-select').componentInstance.value).toBe('equals'); @@ -134,8 +150,8 @@ describe('RuleSimpleConditionUiComponent', () => { fixture.componentInstance.writeValue(simpleConditionUnknownFieldMock); fixture.detectChanges(); - expect(getByDataAutomationId('field-select').componentInstance.value).toBe(simpleConditionUnknownFieldMock.field); - const matSelect = getByDataAutomationId('field-select').nativeElement; + expect(getByDataAutomationId(fieldSelectAutomationId).componentInstance.value).toBe(simpleConditionUnknownFieldMock.field); + const matSelect = getByDataAutomationId(fieldSelectAutomationId).nativeElement; matSelect.click(); fixture.detectChanges(); @@ -147,8 +163,8 @@ describe('RuleSimpleConditionUiComponent', () => { it('should remove the option for the unknown field as soon as another option is selected', () => { fixture.componentInstance.writeValue(simpleConditionUnknownFieldMock); fixture.detectChanges(); - changeMatSelectValue('field-select', 'cm:name'); - const matSelect = getByDataAutomationId('field-select').nativeElement; + changeMatSelectValue(fieldSelectAutomationId, 'cm:name'); + const matSelect = getByDataAutomationId(fieldSelectAutomationId).nativeElement; matSelect.click(); fixture.detectChanges(); @@ -200,7 +216,7 @@ describe('RuleSimpleConditionUiComponent', () => { it('should provide auto-complete option when category is selected', () => { fixture.detectChanges(); - changeMatSelectValue('field-select', 'category'); + changeMatSelectValue(fieldSelectAutomationId, 'category'); expect(getByDataAutomationId('auto-complete-input-field')).toBeTruthy(); expect(fixture.componentInstance.form.get('parameter').value).toEqual(''); @@ -210,7 +226,7 @@ describe('RuleSimpleConditionUiComponent', () => { spyOn(categoryService, 'searchCategories').and.returnValue(of(categoriesListMock)); fixture.detectChanges(); - changeMatSelectValue('field-select', 'category'); + changeMatSelectValue(fieldSelectAutomationId, 'category'); tick(500); expect(categoryService.searchCategories).toHaveBeenCalledWith(''); @@ -221,7 +237,7 @@ describe('RuleSimpleConditionUiComponent', () => { spyOn(categoryService, 'searchCategories').and.returnValue(of(categoriesListMock)); fixture.detectChanges(); - changeMatSelectValue('field-select', 'category'); + changeMatSelectValue(fieldSelectAutomationId, 'category'); tick(500); expect(categoryService.searchCategories).toHaveBeenCalledWith(''); @@ -256,7 +272,7 @@ describe('RuleSimpleConditionUiComponent', () => { it('should show loading spinner while auto-complete options are fetched, and then remove it once it is received', fakeAsync(() => { spyOn(categoryService, 'searchCategories').and.returnValue(of(categoriesListMock).pipe(delay(1000))); fixture.detectChanges(); - changeMatSelectValue('field-select', 'category'); + changeMatSelectValue(fieldSelectAutomationId, 'category'); tick(500); getByDataAutomationId('auto-complete-input-field')?.nativeElement?.click(); let loadingSpinner = getByDataAutomationId('auto-complete-loading-spinner'); @@ -271,7 +287,7 @@ describe('RuleSimpleConditionUiComponent', () => { it('should display correct label for category when user selects a category from auto-complete dropdown', fakeAsync(() => { spyOn(categoryService, 'searchCategories').and.returnValue(of(categoriesListMock)); fixture.detectChanges(); - changeMatSelectValue('field-select', 'category'); + changeMatSelectValue(fieldSelectAutomationId, 'category'); tick(500); getByDataAutomationId('auto-complete-input-field')?.nativeElement?.click(); changeMatSelectValue('folder-rule-auto-complete', categoriesListMock.list.entries[0].entry.id); @@ -283,7 +299,7 @@ describe('RuleSimpleConditionUiComponent', () => { it('should automatically select first category when user focuses out of parameter form field with category option selected', fakeAsync(() => { spyOn(categoryService, 'searchCategories').and.returnValue(of(categoriesListMock)); fixture.detectChanges(); - changeMatSelectValue('field-select', 'category'); + changeMatSelectValue(fieldSelectAutomationId, 'category'); tick(500); const autoCompleteInputField = getByDataAutomationId('auto-complete-input-field')?.nativeElement; autoCompleteInputField.value = 'FakeCat'; @@ -292,4 +308,38 @@ describe('RuleSimpleConditionUiComponent', () => { expect(parameterValue).toEqual(categoriesListMock.list.entries[0].entry.id); discardPeriodicTasks(); })); + + it('should display correct condition field options when tagService.areTagsEnabled returns true', () => { + const tagService = TestBed.inject(TagService); + spyOn(tagService, 'areTagsEnabled').and.returnValue(true); + fixture = TestBed.createComponent(RuleSimpleConditionUiComponent); + + expect(tagService.areTagsEnabled).toHaveBeenCalled(); + expectConditionFieldsDisplayedAsOptions(ruleConditionFields); + }); + + it('should display correct condition field options when tagService.areTagsEnabled returns false', () => { + const tagService = TestBed.inject(TagService); + spyOn(tagService, 'areTagsEnabled').and.returnValue(false); + fixture = TestBed.createComponent(RuleSimpleConditionUiComponent); + + expect(tagService.areTagsEnabled).toHaveBeenCalled(); + expectConditionFieldsDisplayedAsOptions(ruleConditionFields.filter((field) => field.name !== 'tag')); + }); + + it('should display correct condition field options when categoryService.areCategoriesEnabled returns true', () => { + spyOn(categoryService, 'areCategoriesEnabled').and.returnValue(true); + fixture = TestBed.createComponent(RuleSimpleConditionUiComponent); + + expect(categoryService.areCategoriesEnabled).toHaveBeenCalled(); + expectConditionFieldsDisplayedAsOptions(ruleConditionFields); + }); + + it('should display correct condition field options when categoryService.areCategoriesEnabled returns false', () => { + spyOn(categoryService, 'areCategoriesEnabled').and.returnValue(false); + fixture = TestBed.createComponent(RuleSimpleConditionUiComponent); + + expect(categoryService.areCategoriesEnabled).toHaveBeenCalled(); + expectConditionFieldsDisplayedAsOptions(ruleConditionFields.filter((field) => field.name !== 'category')); + }); }); diff --git a/projects/aca-content/folder-rules/src/rule-details/conditions/rule-simple-condition.ui-component.ts b/projects/aca-content/folder-rules/src/rule-details/conditions/rule-simple-condition.ui-component.ts index a3cda946ab..781a96aa3d 100644 --- a/projects/aca-content/folder-rules/src/rule-details/conditions/rule-simple-condition.ui-component.ts +++ b/projects/aca-content/folder-rules/src/rule-details/conditions/rule-simple-condition.ui-component.ts @@ -34,7 +34,7 @@ import { TranslateModule } from '@ngx-translate/core'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatSelectModule } from '@angular/material/select'; import { MatInputModule } from '@angular/material/input'; -import { CategoryService } from '@alfresco/adf-content-services'; +import { CategoryService, TagService } from '@alfresco/adf-content-services'; import { MatAutocompleteModule } from '@angular/material/autocomplete'; import { debounceTime, distinctUntilChanged, first, takeUntil } from 'rxjs/operators'; import { Subject, Subscription } from 'rxjs'; @@ -77,8 +77,6 @@ const AUTOCOMPLETE_OPTIONS_DEBOUNCE_TIME = 500; ] }) export class RuleSimpleConditionUiComponent implements OnInit, ControlValueAccessor, OnDestroy { - readonly fields = ruleConditionFields; - form = new FormGroup({ field: new FormControl('cm:name'), comparator: new FormControl('equals'), @@ -102,8 +100,15 @@ export class RuleSimpleConditionUiComponent implements OnInit, ControlValueAcces this.setDisabledState(isReadOnly); } - constructor(private config: AppConfigService, private categoryService: CategoryService) { - this.mimeTypes = this.config.get>('mimeTypes'); + private readonly disabledTags = !this.tagService.areTagsEnabled(); + private readonly disabledCategories = !this.categoryService.areCategoriesEnabled(); + + readonly fields = ruleConditionFields.filter( + (condition) => !((this.disabledTags && condition.name === 'tag') || (this.disabledCategories && condition.name === 'category')) + ); + + constructor(config: AppConfigService, private categoryService: CategoryService, private tagService: TagService) { + this.mimeTypes = config.get>('mimeTypes'); } get isSelectedFieldKnown(): boolean { const selectedFieldName = this.form.get('field').value; diff --git a/projects/aca-content/folder-rules/src/rule-details/rule-details.ui-component.spec.ts b/projects/aca-content/folder-rules/src/rule-details/rule-details.ui-component.spec.ts index 959239e626..5e7cd8c638 100644 --- a/projects/aca-content/folder-rules/src/rule-details/rule-details.ui-component.spec.ts +++ b/projects/aca-content/folder-rules/src/rule-details/rule-details.ui-component.spec.ts @@ -29,6 +29,8 @@ import { Rule } from '../model/rule.model'; import { By } from '@angular/platform-browser'; import { RuleTriggersUiComponent } from './triggers/rule-triggers.ui-component'; import { RuleOptionsUiComponent } from './options/rule-options.ui-component'; +import { RuleActionListUiComponent } from './actions/rule-action-list.ui-component'; +import { CategoryService } from '@alfresco/adf-content-services'; describe('RuleDetailsUiComponent', () => { let fixture: ComponentFixture; @@ -140,4 +142,54 @@ describe('RuleDetailsUiComponent', () => { expect(getComponentInstance('rule-details-options-component')).toBeFalsy(); }); + + describe('RuleActionListUiComponent', () => { + let categoryService: CategoryService; + + const getRuleActionsListComponent = (): RuleActionListUiComponent => + fixture.debugElement.query(By.directive(RuleActionListUiComponent)).componentInstance; + + beforeEach(() => { + categoryService = TestBed.inject(CategoryService); + component.actionDefinitions = [ + { + id: 'link-category', + name: 'test name', + description: 'some description', + title: 'some title', + applicableTypes: [], + trackStatus: false, + parameterDefinitions: [] + }, + { + id: 'test id', + name: 'test name 2', + description: 'some description', + title: 'some title', + applicableTypes: [], + trackStatus: false, + parameterDefinitions: [] + } + ]; + }); + + it('should have assigned not filtered out category related actions from actionDefinitions if categoryService.areCategoriesEnabled returns true', () => { + spyOn(categoryService, 'areCategoriesEnabled').and.returnValue(true); + + fixture.detectChanges(); + expect(categoryService.areCategoriesEnabled).toHaveBeenCalled(); + expect(component.actionDefinitions.length).toBe(2); + expect(getRuleActionsListComponent().actionDefinitions).toBe(component.actionDefinitions); + }); + + it('should have assigned filter out category related actions from actionDefinitions if categoryService.areCategoriesEnabled returns false', () => { + spyOn(categoryService, 'areCategoriesEnabled').and.returnValue(false); + + fixture.detectChanges(); + expect(categoryService.areCategoriesEnabled).toHaveBeenCalled(); + expect(component.actionDefinitions.length).toBe(1); + expect(component.actionDefinitions[0].id).toBe('test id'); + expect(getRuleActionsListComponent().actionDefinitions).toBe(component.actionDefinitions); + }); + }); }); diff --git a/projects/aca-content/folder-rules/src/rule-details/rule-details.ui-component.ts b/projects/aca-content/folder-rules/src/rule-details/rule-details.ui-component.ts index 40dace9d23..d133756f3c 100644 --- a/projects/aca-content/folder-rules/src/rule-details/rule-details.ui-component.ts +++ b/projects/aca-content/folder-rules/src/rule-details/rule-details.ui-component.ts @@ -40,6 +40,7 @@ import { RuleTriggersUiComponent } from './triggers/rule-triggers.ui-component'; import { RuleCompositeConditionUiComponent } from './conditions/rule-composite-condition.ui-component'; import { RuleActionListUiComponent } from './actions/rule-action-list.ui-component'; import { RuleOptionsUiComponent } from './options/rule-options.ui-component'; +import { CategoryService } from '@alfresco/adf-content-services'; @Component({ standalone: true, @@ -136,7 +137,11 @@ export class RuleDetailsUiComponent implements OnInit, OnDestroy { return !this.readOnly || this.value.isAsynchronous || this.value.isInheritable; } + constructor(private categoryService: CategoryService) {} + ngOnInit() { + const disabledCategory = !this.categoryService.areCategoriesEnabled(); + this.actionDefinitions = this.actionDefinitions.filter((action) => !(disabledCategory && action.id === 'link-category')); this.form = new UntypedFormGroup({ id: new UntypedFormControl(this.value.id), name: new UntypedFormControl(this.value.name || '', Validators.required), diff --git a/projects/aca-content/src/lib/aca-content.module.ts b/projects/aca-content/src/lib/aca-content.module.ts index 47dd3bc2f7..87ade007ff 100644 --- a/projects/aca-content/src/lib/aca-content.module.ts +++ b/projects/aca-content/src/lib/aca-content.module.ts @@ -244,7 +244,9 @@ export class ContentServiceExtensionModule { 'app.isContentServiceEnabled': rules.isContentServiceEnabled, 'app.isUploadSupported': rules.isUploadSupported, 'app.canCreateLibrary': rules.canCreateLibrary, - 'app.isSearchSupported': rules.isSearchSupported + 'app.isSearchSupported': rules.isSearchSupported, + 'app.areTagsEnabled': rules.areTagsEnabled, + 'app.areCategoriesEnabled': rules.areCategoriesEnabled }); } } diff --git a/projects/aca-content/src/lib/components/info-drawer/metadata-tab/metadata-tab.component.spec.ts b/projects/aca-content/src/lib/components/info-drawer/metadata-tab/metadata-tab.component.spec.ts index 7098640b22..d85b2e4cc6 100644 --- a/projects/aca-content/src/lib/components/info-drawer/metadata-tab/metadata-tab.component.spec.ts +++ b/projects/aca-content/src/lib/components/info-drawer/metadata-tab/metadata-tab.component.spec.ts @@ -34,6 +34,7 @@ import { AppExtensionService, NodePermissionService } from '@alfresco/aca-shared import { Actions } from '@ngrx/effects'; import { of, Subject } from 'rxjs'; import { ContentActionType } from '@alfresco/adf-extensions'; +import { CategoryService, ContentMetadataCardComponent, TagService } from '@alfresco/adf-content-services'; describe('MetadataTabComponent', () => { let fixture: ComponentFixture; @@ -251,26 +252,70 @@ describe('MetadataTabComponent', () => { }); }); - describe('displayAspect', () => { + describe('ContentMetadataCardComponent', () => { + const getContentMetadataCard = (): ContentMetadataCardComponent => + fixture.debugElement.query(By.directive(ContentMetadataCardComponent)).componentInstance; + beforeEach(() => { fixture = TestBed.createComponent(MetadataTabComponent); - store = TestBed.inject(Store); component = fixture.componentInstance; }); - it('show pass empty when store is in initial state', () => { - const initialState = fixture.debugElement.query(By.css('adf-content-metadata-card')); - expect(initialState.componentInstance.displayAspect).toBeFalsy(); + describe('displayAspect', () => { + beforeEach(() => { + store = TestBed.inject(Store); + }); + + it('should show pass empty when store is in initial state', () => { + expect(getContentMetadataCard().displayAspect).toBeFalsy(); + }); + + it('should update the exif if store got updated', () => { + store.dispatch(new SetInfoDrawerMetadataAspectAction('EXIF')); + component.displayAspect$.subscribe((aspect) => { + expect(aspect).toBe('EXIF'); + }); + fixture.detectChanges(); + expect(getContentMetadataCard().displayAspect).toBe('EXIF'); + }); }); - it('should update the exif if store got updated', () => { - store.dispatch(new SetInfoDrawerMetadataAspectAction('EXIF')); - component.displayAspect$.subscribe((aspect) => { - expect(aspect).toBe('EXIF'); + describe('Tags and categories', () => { + it('should have assigned displayCategories to true if categoryService.areCategoriesEnabled returns true', () => { + const categoryService = TestBed.inject(CategoryService); + spyOn(categoryService, 'areCategoriesEnabled').and.returnValue(true); + + fixture.detectChanges(); + expect(categoryService.areCategoriesEnabled).toHaveBeenCalled(); + expect(getContentMetadataCard().displayCategories).toBeTrue(); + }); + + it('should have assigned displayCategories to false if categoryService.areCategoriesEnabled returns false', () => { + const categoryService = TestBed.inject(CategoryService); + spyOn(categoryService, 'areCategoriesEnabled').and.returnValue(false); + + fixture.detectChanges(); + expect(categoryService.areCategoriesEnabled).toHaveBeenCalled(); + expect(getContentMetadataCard().displayCategories).toBeFalse(); + }); + + it('should have assigned displayTags to true if tagService.areTagsEnabled returns true', () => { + const tagService = TestBed.inject(TagService); + spyOn(tagService, 'areTagsEnabled').and.returnValue(true); + + fixture.detectChanges(); + expect(tagService.areTagsEnabled).toHaveBeenCalled(); + expect(getContentMetadataCard().displayTags).toBeTrue(); + }); + + it('should have assigned displayTags to false if tagService.areTagsEnabled returns false', () => { + const tagService = TestBed.inject(TagService); + spyOn(tagService, 'areTagsEnabled').and.returnValue(false); + + fixture.detectChanges(); + expect(tagService.areTagsEnabled).toHaveBeenCalled(); + expect(getContentMetadataCard().displayTags).toBeFalse(); }); - fixture.detectChanges(); - const initialState = fixture.debugElement.query(By.css('adf-content-metadata-card')); - expect(initialState.componentInstance.displayAspect).toBe('EXIF'); }); }); diff --git a/projects/aca-content/src/lib/components/info-drawer/metadata-tab/metadata-tab.component.ts b/projects/aca-content/src/lib/components/info-drawer/metadata-tab/metadata-tab.component.ts index 708cbf75da..57ee8cbe7f 100644 --- a/projects/aca-content/src/lib/components/info-drawer/metadata-tab/metadata-tab.component.ts +++ b/projects/aca-content/src/lib/components/info-drawer/metadata-tab/metadata-tab.component.ts @@ -29,7 +29,13 @@ import { AppStore, EditOfflineAction, infoDrawerMetadataAspect, NodeActionTypes import { AppConfigService, NotificationService } from '@alfresco/adf-core'; import { Observable, Subject } from 'rxjs'; import { Store } from '@ngrx/store'; -import { ContentMetadataModule, ContentMetadataService, ContentMetadataCustomPanel } from '@alfresco/adf-content-services'; +import { + ContentMetadataModule, + ContentMetadataService, + ContentMetadataCustomPanel, + TagService, + CategoryService +} from '@alfresco/adf-content-services'; import { filter, map, takeUntil } from 'rxjs/operators'; import { CommonModule } from '@angular/common'; import { Actions, ofType } from '@ngrx/effects'; @@ -46,6 +52,8 @@ import { Actions, ofType } from '@ngrx/effects'; [displayAspect]="displayAspect$ | async" [customPanels]="customPanels | async" [(editable)]="editable" + [displayCategories]="displayCategories" + [displayTags]="displayTags" > `, @@ -54,6 +62,8 @@ import { Actions, ofType } from '@ngrx/effects'; }) export class MetadataTabComponent implements OnInit, OnDestroy { protected onDestroy$ = new Subject(); + private _displayCategories = true; + private _displayTags = true; @Input() node: Node; @@ -63,6 +73,14 @@ export class MetadataTabComponent implements OnInit, OnDestroy { editable = false; customPanels: Observable; + get displayCategories(): boolean { + return this._displayCategories; + } + + get displayTags(): boolean { + return this._displayTags; + } + constructor( private permission: NodePermissionService, protected extensions: AppExtensionService, @@ -70,7 +88,9 @@ export class MetadataTabComponent implements OnInit, OnDestroy { private store: Store, private notificationService: NotificationService, private contentMetadataService: ContentMetadataService, - private actions$: Actions + private actions$: Actions, + private tagService: TagService, + private categoryService: CategoryService ) { if (this.extensions.contentMetadata) { this.appConfig.config['content-metadata'].presets = this.extensions.contentMetadata.presets; @@ -79,6 +99,8 @@ export class MetadataTabComponent implements OnInit, OnDestroy { } ngOnInit() { + this._displayTags = this.tagService.areTagsEnabled(); + this._displayCategories = this.categoryService.areCategoriesEnabled(); this.contentMetadataService.error.pipe(takeUntil(this.onDestroy$)).subscribe((err: { message: string }) => { this.notificationService.showError(err.message); }); diff --git a/projects/aca-shared/rules/src/app.rules.spec.ts b/projects/aca-shared/rules/src/app.rules.spec.ts index 8b95847101..4f3a848a60 100644 --- a/projects/aca-shared/rules/src/app.rules.spec.ts +++ b/projects/aca-shared/rules/src/app.rules.spec.ts @@ -858,6 +858,72 @@ describe('app.evaluators', () => { expect(app.canManagePermissions(context)).toBe(true); }); }); + + describe('areTagsEnabled', () => { + it('should call context.appConfig.get with correct parameters', () => { + const context: any = { + appConfig: { + get: jasmine.createSpy() + } + }; + + app.areTagsEnabled(context); + expect(context.appConfig.get).toHaveBeenCalledWith('plugins.tags', true); + }); + + it('should return true if get from appConfig returns true', () => { + expect( + app.areTagsEnabled({ + appConfig: { + get: () => true + } + } as any) + ).toBeTrue(); + }); + + it('should return false if get from appConfig returns false', () => { + expect( + app.areTagsEnabled({ + appConfig: { + get: () => false + } + } as any) + ).toBeFalse(); + }); + }); + + describe('areCategoriesEnabled', () => { + it('should call context.appConfig.get with correct parameters', () => { + const context: any = { + appConfig: { + get: jasmine.createSpy() + } + }; + + app.areCategoriesEnabled(context); + expect(context.appConfig.get).toHaveBeenCalledWith('plugins.categories', true); + }); + + it('should return true if get from appConfig returns true', () => { + expect( + app.areCategoriesEnabled({ + appConfig: { + get: () => true + } + } as any) + ).toBeTrue(); + }); + + it('should return false if get from appConfig returns false', () => { + expect( + app.areCategoriesEnabled({ + appConfig: { + get: () => false + } + } as any) + ).toBeFalse(); + }); + }); }); function createTestContext(): TestRuleContext { diff --git a/projects/aca-shared/rules/src/app.rules.ts b/projects/aca-shared/rules/src/app.rules.ts index 1c741b1463..df9e89374d 100644 --- a/projects/aca-shared/rules/src/app.rules.ts +++ b/projects/aca-shared/rules/src/app.rules.ts @@ -636,3 +636,7 @@ export function isSmartFolder(context: RuleContext): boolean { } return false; } + +export const areTagsEnabled = (context: AcaRuleContext): boolean => context.appConfig.get('plugins.tags', true); + +export const areCategoriesEnabled = (context: AcaRuleContext): boolean => context.appConfig.get('plugins.categories', true); diff --git a/projects/aca-shared/src/lib/services/app.extension.service.spec.ts b/projects/aca-shared/src/lib/services/app.extension.service.spec.ts index 6f6af0ceab..632eb238cb 100644 --- a/projects/aca-shared/src/lib/services/app.extension.service.spec.ts +++ b/projects/aca-shared/src/lib/services/app.extension.service.spec.ts @@ -1049,13 +1049,14 @@ describe('AppExtensionService', () => { }); describe('search', () => { + let config: ExtensionConfig; + beforeEach(() => { extensions.setEvaluators({ visible: () => true, notVisible: () => false }); - - applyConfig({ + config = { $id: 'test', $name: 'test', $version: '1.0.0', @@ -1105,22 +1106,64 @@ describe('AppExtensionService', () => { } ] } - }); + }; }); it('should load the search extension', () => { + applyConfig(config); expect(service.search.length).toBe(2); expect(service.search[0].id).toBe('app.search'); expect(service.search[1].id).toBe('app.search-1'); }); it('should not load the disabled search extension', () => { + applyConfig(config); expect(service.search.find(({ id }) => id === 'app.search-2')).toBe(undefined, 'disabled configuration shown in the result'); }); it('should not load the not visible search extension', () => { + applyConfig(config); expect(service.search.find(({ id }) => id === 'app.search-3')).toBe(undefined, 'not visible configuration shown in the result'); }); + + it('should contain category if it has no rules field', () => { + applyConfig(config); + const search = service.search[0]; + expect(search.categories.length).toBe(1); + expect(search.categories[0].id).toBe('size'); + }); + + it('should contain category if it has no visible field in rules', () => { + config.features.search[0].categories[0].rules = {}; + + applyConfig(config); + const search = service.search[0]; + expect(search.categories.length).toBe(1); + expect(search.categories[0].id).toBe('size'); + }); + + it('should contain category if it has visible field and extensions.evaluateRule returns true', () => { + spyOn(extensions, 'evaluateRule').and.returnValue(true); + const visible = 'test'; + config.features.search[0].categories[0].rules = { visible }; + + applyConfig(config); + const search = service.search[0]; + expect(extensions.evaluateRule).toHaveBeenCalledWith(visible, service); + expect(search.categories.length).toBe(1); + expect(search.categories[0].id).toBe('size'); + }); + + it('should not contain category if it has visible field and extensions.evaluateRule returns false', () => { + spyOn(extensions, 'evaluateRule').and.returnValue(false); + const visible = 'test'; + config.features.search[0].categories[0].rules = { visible }; + + applyConfig(config); + const search = service.search[0]; + expect(extensions.evaluateRule).toHaveBeenCalledWith(visible, service); + expect(search.categories.length).toBe(0); + }); }); describe('rules', () => { diff --git a/projects/aca-shared/src/lib/services/app.extension.service.ts b/projects/aca-shared/src/lib/services/app.extension.service.ts index 81c01e6a0c..15ada1b5b9 100644 --- a/projects/aca-shared/src/lib/services/app.extension.service.ts +++ b/projects/aca-shared/src/lib/services/app.extension.service.ts @@ -56,6 +56,7 @@ import { ViewerRules } from '../models/viewer.rules'; import { Badge, SettingsGroupRef } from '../models/types'; import { NodePermissionService } from '../services/node-permission.service'; import { filter, map } from 'rxjs/operators'; +import { SearchCategory } from '@alfresco/adf-content-services'; @Injectable({ providedIn: 'root' @@ -167,6 +168,9 @@ export class AppExtensionService implements RuleContext { this.sidebarTabs = this.loader.getElements(config, 'features.sidebar.tabs'); this.contentMetadata = this.loadContentMetadata(config); this.search = this.loadSearchForms(config); + this.search?.forEach((searchSet) => { + searchSet.categories = searchSet.categories?.filter((category) => this.filterVisible(category)); + }); this.documentListPresets = { libraries: this.getDocumentListPreset(config, 'libraries'), @@ -482,7 +486,7 @@ export class AppExtensionService implements RuleContext { }; } - filterVisible(action: ContentActionRef | SettingsGroupRef | SidebarTabRef | DocumentListPresetRef): boolean { + filterVisible(action: ContentActionRef | SettingsGroupRef | SidebarTabRef | DocumentListPresetRef | SearchCategory): boolean { if (action?.rules?.visible) { return this.extensions.evaluateRule(action.rules.visible, this); }