[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
This commit is contained in:
AleksanderSklorz 2023-11-28 14:09:00 +01:00 committed by GitHub
parent 6a326f776a
commit 2c69887e08
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 535 additions and 64 deletions

View File

@ -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",

View File

@ -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": [

View File

@ -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'
}
]
}
];

View File

@ -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<RuleActionUiComponent>;
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<string>).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<string>).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<string>).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<string>).options$.subscribe((options) => {
expect(options).toEqual([
{
key: 'cm:notCategoryRelated',
label: 'Label 2 [cm:notCategoryRelated]'
}
]);
done();
});
});
});
});

View File

@ -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({

View File

@ -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<RuleSimpleConditionUiComponent>;
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'));
});
});

View File

@ -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<Array<MimeType>>('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<Array<MimeType>>('mimeTypes');
}
get isSelectedFieldKnown(): boolean {
const selectedFieldName = this.form.get('field').value;

View File

@ -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<RuleDetailsUiComponent>;
@ -140,4 +142,54 @@ describe('RuleDetailsUiComponent', () => {
expect(getComponentInstance<RuleOptionsUiComponent>('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);
});
});
});

View File

@ -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),

View File

@ -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
});
}
}

View File

@ -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<MetadataTabComponent>;
@ -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');
});
});

View File

@ -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"
>
</adf-content-metadata-card>
`,
@ -54,6 +62,8 @@ import { Actions, ofType } from '@ngrx/effects';
})
export class MetadataTabComponent implements OnInit, OnDestroy {
protected onDestroy$ = new Subject<boolean>();
private _displayCategories = true;
private _displayTags = true;
@Input()
node: Node;
@ -63,6 +73,14 @@ export class MetadataTabComponent implements OnInit, OnDestroy {
editable = false;
customPanels: Observable<ContentMetadataCustomPanel[]>;
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<AppStore>,
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);
});

View File

@ -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 {

View File

@ -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);

View File

@ -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', () => {

View File

@ -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<SidebarTabRef>(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);
}