mirror of
https://github.com/Alfresco/alfresco-ng2-components.git
synced 2025-07-24 17:32:15 +00:00
[ACS-4523] Assigning content to categories (#8451)
* [ACS-4523] Add categories management mode component, manage mode completed * [ACS-4523] Add categories management fixes and unit tests * [ACS-4523] Add unit tests to content metadata * ACS-4523] CR fixes * [ACS-4523] Tag and category name controls decoupled * [ACS-4523] CR fixes * [ACS-4523] CR fixes * [ACS-4523] CR fixes * [ACS-4523] Display path for already linked categories * [ACS-4523] Add new license header * [ACS-4523] Fix category service import
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright © 2005-2023 Hyland Software, Inc. and its affiliates. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export enum CategoriesManagementMode {
|
||||
CRUD,
|
||||
ASSIGN
|
||||
}
|
@@ -0,0 +1,85 @@
|
||||
<div class="adf-categories-management">
|
||||
<p *ngIf="!categories.length && !categoryNameControlVisible"
|
||||
class="adf-no-categories-message">
|
||||
{{ isCRUDMode ? 'CATEGORIES_MANAGEMENT.NO_CATEGORIES_CREATED' : 'CATEGORIES_MANAGEMENT.NO_CATEGORIES_ASSIGNED' | translate }}
|
||||
</p>
|
||||
<div class="adf-categories-list"
|
||||
[class.adf-categories-list-fixed]="!categoryNameControlVisible">
|
||||
<span
|
||||
*ngFor="let category of categories"
|
||||
[class.adf-categories-padded]="!isCRUDMode"
|
||||
class="adf-assigned-categories">
|
||||
{{ category.name }}
|
||||
<button
|
||||
data-automation-id="categories-remove-category-button"
|
||||
mat-icon-button
|
||||
[class.adf-btn-padded]="!isCRUDMode"
|
||||
(click)="removeCategory(category)"
|
||||
[attr.title]="isCRUDMode ? 'CATEGORIES_MANAGEMENT.DELETE_CATEGORY' : 'CATEGORIES_MANAGEMENT.UNASSIGN_CATEGORY' | translate"
|
||||
[disabled]="disableRemoval">
|
||||
<mat-icon>remove</mat-icon>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
<div *ngIf="((!categoryNameControlVisible && categories.length)) || categoryNameControlVisible"
|
||||
[hidden]="!categoryNameControlVisible"
|
||||
class="adf-category-name-field">
|
||||
<mat-form-field>
|
||||
<mat-icon matPrefix>search</mat-icon>
|
||||
<mat-label id="adf-category-name-input-label">
|
||||
{{ 'CATEGORIES_MANAGEMENT.NAME' | translate }}
|
||||
</mat-label>
|
||||
<input
|
||||
#categoryNameInput
|
||||
matInput
|
||||
autocomplete="off"
|
||||
[formControl]="categoryNameControl"
|
||||
(keyup.enter)="addCategory()"
|
||||
aria-labelledby="adf-category-name-input-label"
|
||||
adf-auto-focus
|
||||
/>
|
||||
<mat-error [hidden]="!categoryNameControl.invalid">{{ categoryNameErrorMessageKey | translate }}</mat-error>
|
||||
</mat-form-field>
|
||||
<button
|
||||
mat-icon-button
|
||||
[class.adf-btn-padded]="!isCRUDMode"
|
||||
(click)="hideNameInput()"
|
||||
[attr.title]="'CATEGORIES_MANAGEMENT.HIDE_INPUT' | translate">
|
||||
<mat-icon>remove</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="adf-existing-categories-panel" *ngIf="existingCategoriesPanelVisible">
|
||||
<ng-container *ngIf="isCRUDMode && (!existingCategoriesLoading || existingCategories)">
|
||||
<span class="adf-create-category-label"
|
||||
(click)="addCategory()"
|
||||
[hidden]="categoryNameControl.invalid || typing">
|
||||
{{ 'CATEGORIES_MANAGEMENT.GENERIC_CREATE' | translate : { name: categoryNameControl.value } }}
|
||||
</span>
|
||||
</ng-container>
|
||||
<div *ngIf="categoryNameControlVisible" class="adf-categories-list">
|
||||
<ng-container *ngIf="!existingCategoriesLoading && existingCategories">
|
||||
<p class="adf-existing-categories-label">
|
||||
{{ isCRUDMode ? 'CATEGORIES_MANAGEMENT.EXISTING_CATEGORIES' : 'CATEGORIES_MANAGEMENT.SELECT_EXISTING_CATEGORY' | translate }}
|
||||
</p>
|
||||
<mat-selection-list
|
||||
[disabled]="isCRUDMode"
|
||||
(selectionChange)="addCategoryToAssign($event)">
|
||||
<mat-list-option
|
||||
*ngFor="let category of existingCategories"
|
||||
class="adf-category"
|
||||
[value]="category">
|
||||
{{ category.name }}
|
||||
</mat-list-option>
|
||||
<p *ngIf="!existingCategories?.length && !existingCategoriesLoading">
|
||||
{{ 'CATEGORIES_MANAGEMENT.NO_EXISTING_CATEGORIES' | translate }}
|
||||
</p>
|
||||
</mat-selection-list>
|
||||
</ng-container>
|
||||
<mat-spinner
|
||||
*ngIf="existingCategoriesLoading"
|
||||
[diameter]="50"
|
||||
[attr.aria-label]="'CATEGORIES_MANAGEMENT.LOADING' | translate">
|
||||
</mat-spinner>
|
||||
</div>
|
||||
</div>
|
@@ -0,0 +1,79 @@
|
||||
.adf-categories-management {
|
||||
.adf-category-name-field {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
|
||||
mat-form-field {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.adf-btn-padded {
|
||||
margin-right: -14px;
|
||||
}
|
||||
}
|
||||
|
||||
.adf-assigned-categories {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
word-break: break-word;
|
||||
|
||||
.adf-btn-padded {
|
||||
margin-right: -14px;
|
||||
}
|
||||
}
|
||||
|
||||
.adf-categories-padded {
|
||||
padding: 5px 0;
|
||||
}
|
||||
|
||||
[hidden] {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.adf-categories-list {
|
||||
padding-bottom: 10px;
|
||||
|
||||
.mat-list-base .mat-list-item,
|
||||
.mat-list-base .mat-list-option {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
overflow-wrap: anywhere;
|
||||
padding: 5px 0;
|
||||
font-size: 14px;
|
||||
|
||||
&.mat-list-item-disabled {
|
||||
background-color: inherit;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.mat-list-item-content,
|
||||
.mat-list-item-content-reverse {
|
||||
padding: 0;
|
||||
|
||||
.mat-pseudo-checkbox {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.adf-existing-categories-label {
|
||||
font-size: 10px;
|
||||
color: var(--theme-secondary-text-color);
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
mat-spinner {
|
||||
margin: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.adf-existing-categories-panel {
|
||||
.adf-create-category-label {
|
||||
color: var(--theme-primary-color);
|
||||
cursor: pointer;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
}
|
@@ -0,0 +1,487 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright © 2005-2023 Hyland Software, Inc. and its affiliates. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Category, CategoryPaging, ResultNode, ResultSetPaging } from '@alfresco/js-api';
|
||||
import { DebugElement } from '@angular/core';
|
||||
import { ComponentFixture, discardPeriodicTasks, fakeAsync, flush, TestBed, tick } from '@angular/core/testing';
|
||||
import { Validators } from '@angular/forms';
|
||||
import { MatError } from '@angular/material/form-field';
|
||||
import { MatListOption, MatSelectionList } from '@angular/material/list';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { of, Subject } from 'rxjs';
|
||||
import { ContentTestingModule } from '../../testing/content.testing.module';
|
||||
import { CategoriesManagementMode } from './categories-management-mode';
|
||||
import { CategoryService } from '../services/category.service';
|
||||
import { CategoriesManagementComponent } from './categories-management.component';
|
||||
|
||||
describe('CategoriesManagementComponent', () => {
|
||||
let component: CategoriesManagementComponent;
|
||||
let fixture: ComponentFixture<CategoriesManagementComponent>;
|
||||
let categoryService: CategoryService;
|
||||
const classifiableChangedSubject = new Subject<void>();
|
||||
const category1 = new Category({ id: 'test', name: 'testCat' });
|
||||
const category2 = new Category({ id: 'test2', name: 'testCat2' });
|
||||
const category3 = new Category({ id: 'test3', name: 'testCat3' });
|
||||
const category4 = new Category({ id: 'test4', name: 'testCat4' });
|
||||
const resultCat1 = new ResultNode({ id: 'test', name: 'testCat', path: { name: 'general/categories' }});
|
||||
const resultCat2 = new ResultNode({ id: 'test2', name: 'testCat2', path: { name: 'general/categories' }});
|
||||
const categoryPagingResponse: CategoryPaging = { list: { pagination: {}, entries: [ { entry: category1 }, { entry: category2 }]}};
|
||||
const categorySearchResponse: ResultSetPaging = { list: { pagination: {}, entries: [ { entry: resultCat1 }, { entry: resultCat2 }]}};
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [CategoriesManagementComponent],
|
||||
imports: [
|
||||
TranslateModule.forRoot(),
|
||||
ContentTestingModule
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
provide: CategoryService,
|
||||
useValue: {
|
||||
getSubcategories: () => of(categoryPagingResponse),
|
||||
searchCategories: () => of(categorySearchResponse)
|
||||
}
|
||||
}
|
||||
],
|
||||
teardown: { destroyAfterEach: true }
|
||||
});
|
||||
|
||||
fixture = TestBed.createComponent(CategoriesManagementComponent);
|
||||
component = fixture.componentInstance;
|
||||
categoryService = TestBed.inject(CategoryService);
|
||||
});
|
||||
|
||||
function getNoCategoriesMessage(): string {
|
||||
return fixture.debugElement.query(By.css(`.adf-no-categories-message`))?.nativeElement.textContent.trim();
|
||||
}
|
||||
|
||||
function getAssignedCategoriesList(): HTMLSpanElement[] {
|
||||
return fixture.debugElement.queryAll(By.css('.adf-assigned-categories'))?.map((debugElem) => debugElem.nativeElement);
|
||||
}
|
||||
|
||||
function getExistingCategoriesList(): MatListOption[] {
|
||||
return fixture.debugElement.queryAll(By.directive(MatListOption))?.map((debugElem) => debugElem.componentInstance);
|
||||
}
|
||||
|
||||
function createCategory(name: string, addUsingEnter?: boolean, typingTimeout = 300): void {
|
||||
typeCategory(name, typingTimeout);
|
||||
|
||||
if (addUsingEnter) {
|
||||
getCategoryControlInput().dispatchEvent(new KeyboardEvent('keyup', { key: 'Enter' }));
|
||||
} else {
|
||||
getCreateCategoryLabel().click();
|
||||
}
|
||||
|
||||
tick(300);
|
||||
fixture.detectChanges();
|
||||
}
|
||||
|
||||
function getFirstError(): string {
|
||||
return fixture.debugElement.query(By.directive(MatError)).nativeElement.textContent;
|
||||
}
|
||||
|
||||
function getSelectionList(): MatSelectionList {
|
||||
return fixture.debugElement.query(By.directive(MatSelectionList)).componentInstance;
|
||||
}
|
||||
|
||||
function getRemoveCategoryButtons(): HTMLButtonElement[] {
|
||||
return fixture.debugElement.queryAll(By.css(`[data-automation-id="categories-remove-category-button"]`)).map((debugElem) => debugElem.nativeElement);
|
||||
}
|
||||
|
||||
function getCategoryControlInput(): HTMLInputElement {
|
||||
return fixture.debugElement.query(By.css('.adf-category-name-field input'))?.nativeElement;
|
||||
}
|
||||
|
||||
function getCreateCategoryLabel(): HTMLSpanElement {
|
||||
return fixture.debugElement.query(By.css('.adf-existing-categories-panel span'))?.nativeElement;
|
||||
}
|
||||
|
||||
function typeCategory(name: string, timeout = 300): void {
|
||||
component.categoryNameControlVisible = true;
|
||||
fixture.detectChanges();
|
||||
|
||||
const categoryControlInput = getCategoryControlInput();
|
||||
categoryControlInput.value = name;
|
||||
categoryControlInput.dispatchEvent(new InputEvent('input'));
|
||||
|
||||
tick(timeout);
|
||||
fixture.detectChanges();
|
||||
}
|
||||
|
||||
describe('Shared tests', () => {
|
||||
beforeEach(() => {
|
||||
component.managementMode = CategoriesManagementMode.CRUD;
|
||||
component.categories = [category3, category4];
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
describe('Assigned categories list', () => {
|
||||
it('should display all categories assigned/created', () => {
|
||||
const assignedCategories = getAssignedCategoriesList();
|
||||
expect(assignedCategories.length).toBe(2);
|
||||
expect(assignedCategories[0].textContent.trim()).toContain(category3.name);
|
||||
expect(assignedCategories[1].textContent.trim()).toContain(category4.name);
|
||||
});
|
||||
|
||||
it('should unassign/remove specific category after clicking at remove icon and emit categories change', () => {
|
||||
const removeBtns = getRemoveCategoryButtons();
|
||||
const categoriesChangeSpy = spyOn(component.categoriesChange, 'emit');
|
||||
removeBtns[0].click();
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
const assignedCategories = getAssignedCategoriesList();
|
||||
expect(component.categories.length).toBe(1);
|
||||
expect(assignedCategories.length).toBe(1);
|
||||
expect(assignedCategories[0].textContent.trim()).toContain(category4.name);
|
||||
expect(categoriesChangeSpy).toHaveBeenCalledWith(component.categories);
|
||||
});
|
||||
|
||||
it('should disable unassigning/removing categories when disableRemoval is set', () => {
|
||||
component.disableRemoval = true;
|
||||
fixture.detectChanges();
|
||||
const removeBtns = getRemoveCategoryButtons();
|
||||
const allBtnsDisabled = removeBtns.every((removeBtn) => removeBtn.disabled);
|
||||
expect(allBtnsDisabled).toBeTrue();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Category name control', () => {
|
||||
beforeEach(() => {
|
||||
component.categoryNameControlVisible = true;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
it('should be hidden initially', () => {
|
||||
component.categoryNameControlVisible = false;
|
||||
fixture.detectChanges();
|
||||
const categoryControl: HTMLDivElement = fixture.debugElement.query(By.css('.adf-category-name-field')).nativeElement;
|
||||
expect(categoryControl.hidden).toBeTrue();
|
||||
});
|
||||
|
||||
it('should be visible when categoryNameControlVisible is true', () => {
|
||||
const categoryControl = fixture.debugElement.query(By.css('.adf-category-name-field'));
|
||||
expect(categoryControl).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should have correct label and hide button', () => {
|
||||
const categoryControlLabel = fixture.debugElement.query(By.css('#adf-category-name-input-label')).nativeElement;
|
||||
const categoryControlHideBtn: HTMLButtonElement = fixture.debugElement.query(By.css('.adf-category-name-field button')).nativeElement;
|
||||
expect(categoryControlHideBtn).toBeTruthy();
|
||||
expect(categoryControlHideBtn.attributes.getNamedItem('title').textContent.trim()).toBe('CATEGORIES_MANAGEMENT.HIDE_INPUT');
|
||||
expect(categoryControlLabel.textContent.trim()).toBe('CATEGORIES_MANAGEMENT.NAME');
|
||||
});
|
||||
|
||||
it('should hide category control and existing categories panel on clicking hide button', () => {
|
||||
const categoryControlHideBtn: HTMLButtonElement = fixture.debugElement.query(By.css('.adf-category-name-field button')).nativeElement;
|
||||
const controlVisibilityChangeSpy = spyOn(component.categoryNameControlVisibleChange, 'emit').and.callThrough();
|
||||
categoryControlHideBtn.click();
|
||||
fixture.detectChanges();
|
||||
|
||||
const categoryControl: HTMLDivElement = fixture.debugElement.query(By.css('.adf-category-name-field')).nativeElement;
|
||||
expect(categoryControl.hidden).toBeTrue();
|
||||
expect(component.categoryNameControlVisible).toBeFalse();
|
||||
expect(component.existingCategoriesPanelVisible).toBeFalse();
|
||||
expect(controlVisibilityChangeSpy).toHaveBeenCalledOnceWith(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Spinner', () => {
|
||||
function getSpinner(): DebugElement {
|
||||
return fixture.debugElement.query(By.css(`.mat-progress-spinner`));
|
||||
}
|
||||
|
||||
it('should be displayed with correct diameter when existing categories are loading', fakeAsync(() => {
|
||||
typeCategory('Category 1', 0);
|
||||
|
||||
const spinner = getSpinner();
|
||||
expect(spinner).toBeTruthy();
|
||||
expect(spinner.componentInstance.diameter).toBe(50);
|
||||
|
||||
discardPeriodicTasks();
|
||||
flush();
|
||||
}));
|
||||
|
||||
it('should not be displayed when existing categories stopped loading', fakeAsync(() => {
|
||||
typeCategory('Category 1');
|
||||
|
||||
const spinner = getSpinner();
|
||||
expect(spinner).toBeFalsy();
|
||||
}));
|
||||
});
|
||||
|
||||
it('should display correct message when there are no existing categories', fakeAsync(() => {
|
||||
spyOn(categoryService, 'getSubcategories').and.returnValue(of({list: { pagination: {}, entries: []}}));
|
||||
typeCategory('test');
|
||||
|
||||
const noExistingCategoriesMsg = fixture.debugElement.query(By.css('mat-selection-list p'))?.nativeElement.textContent.trim();
|
||||
expect(noExistingCategoriesMsg).toBe('CATEGORIES_MANAGEMENT.NO_EXISTING_CATEGORIES');
|
||||
}));
|
||||
});
|
||||
|
||||
describe('Assign mode', () => {
|
||||
beforeEach(() => {
|
||||
component.managementMode = CategoriesManagementMode.ASSIGN;
|
||||
component.categories = [category3, category4];
|
||||
component.classifiableChanged = classifiableChangedSubject.asObservable();
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should return false for is CRUD mode check', () => {
|
||||
expect(component.isCRUDMode).toBeFalse();
|
||||
});
|
||||
|
||||
it('should display correct no categories message', () => {
|
||||
component.categories = [];
|
||||
fixture.detectChanges();
|
||||
expect(getNoCategoriesMessage()).toBe('CATEGORIES_MANAGEMENT.NO_CATEGORIES_ASSIGNED');
|
||||
});
|
||||
|
||||
it('should store initially assigned categories', () => {
|
||||
expect(component.initialCategories.length).toBe(2);
|
||||
});
|
||||
|
||||
it('should pad assigned categories', () => {
|
||||
const assignedCategories = getAssignedCategoriesList();
|
||||
const allCategoriesPadded = assignedCategories.every((categoryElem) => categoryElem.classList.contains('adf-categories-padded'));
|
||||
expect(allCategoriesPadded).toBeTrue();
|
||||
});
|
||||
|
||||
it('should have correct remove category title', () => {
|
||||
const removeBtns = getRemoveCategoryButtons();
|
||||
const isTitleCorrect = removeBtns.every((removeBtn) => removeBtn.attributes.getNamedItem('title').textContent === 'CATEGORIES_MANAGEMENT.UNASSIGN_CATEGORY');
|
||||
expect(isTitleCorrect).toBeTrue();
|
||||
});
|
||||
|
||||
it('should have no required validator set for category control', () => {
|
||||
expect(component.categoryNameControl.hasValidator(Validators.required)).toBeFalse();
|
||||
});
|
||||
|
||||
it('should display validation error when searching for empty category', fakeAsync(() => {
|
||||
typeCategory(' ');
|
||||
|
||||
expect(getFirstError()).toBe('CATEGORIES_MANAGEMENT.ERRORS.EMPTY_CATEGORY');
|
||||
}));
|
||||
|
||||
it('should clear categories and hide category control when classifiable aspect is removed', () => {
|
||||
const controlVisibilityChangeSpy = spyOn(component.categoryNameControlVisibleChange, 'emit').and.callThrough();
|
||||
classifiableChangedSubject.next();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(controlVisibilityChangeSpy).toHaveBeenCalledOnceWith(false);
|
||||
expect(component.categoryNameControlVisible).toBeFalse();
|
||||
expect(component.categories).toEqual([]);
|
||||
});
|
||||
|
||||
it('should not display create category label', fakeAsync(() => {
|
||||
typeCategory('test');
|
||||
|
||||
expect(getCreateCategoryLabel()).toBeUndefined();
|
||||
}));
|
||||
|
||||
it('should not disable existing categories', fakeAsync(() => {
|
||||
typeCategory('test');
|
||||
|
||||
expect(getSelectionList().disabled).toBeFalse();
|
||||
}));
|
||||
|
||||
it('should add selected category to categories list and remove from existing categories', fakeAsync(() => {
|
||||
const categoriesChangeSpy = spyOn(component.categoriesChange, 'emit').and.callThrough();
|
||||
typeCategory('test');
|
||||
const options = getExistingCategoriesList();
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
options[0]._handleClick();
|
||||
|
||||
expect(component.categories.length).toBe(3);
|
||||
expect(component.categories[2].name).toBe('testCat');
|
||||
expect(component.existingCategories.length).toBe(1);
|
||||
expect(categoriesChangeSpy).toHaveBeenCalledOnceWith(component.categories);
|
||||
discardPeriodicTasks();
|
||||
flush();
|
||||
}));
|
||||
|
||||
it('should remove selected category from categories list and add it back to existing categories', fakeAsync(() => {
|
||||
typeCategory('test');
|
||||
const options = getExistingCategoriesList();
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
options[0]._handleClick();
|
||||
fixture.detectChanges();
|
||||
|
||||
const categoriesChangeSpy = spyOn(component.categoriesChange, 'emit').and.callThrough();
|
||||
const removeCategoryBtns = getRemoveCategoryButtons();
|
||||
removeCategoryBtns[2].click();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.categories.length).toBe(2);
|
||||
expect(component.categories[1].name).toBe('testCat4');
|
||||
expect(categoriesChangeSpy).toHaveBeenCalledOnceWith(component.categories);
|
||||
expect(component.existingCategories.length).toBe(2);
|
||||
discardPeriodicTasks();
|
||||
flush();
|
||||
}));
|
||||
|
||||
it('should not add back to existing categories when category was assigned initially', fakeAsync(() => {
|
||||
typeCategory('test');
|
||||
expect(component.existingCategories.length).toBe(2);
|
||||
const categoriesChangeSpy = spyOn(component.categoriesChange, 'emit').and.callThrough();
|
||||
const removeCategoryBtns = getRemoveCategoryButtons();
|
||||
removeCategoryBtns[0].click();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.categories.length).toBe(1);
|
||||
expect(component.categories[0].name).toBe('testCat4');
|
||||
expect(categoriesChangeSpy).toHaveBeenCalledOnceWith(component.categories);
|
||||
expect(component.existingCategories.length).toBe(2);
|
||||
discardPeriodicTasks();
|
||||
flush();
|
||||
}));
|
||||
});
|
||||
|
||||
describe('CRUD mode', () => {
|
||||
beforeEach(() => {
|
||||
component.managementMode = CategoriesManagementMode.CRUD;
|
||||
component.categories = [category3, category4];
|
||||
fixture.detectChanges();
|
||||
});
|
||||
it('should return true for is CRUD mode check', () => {
|
||||
expect(component.isCRUDMode).toBeTrue();
|
||||
});
|
||||
|
||||
it('should display correct no categories message', () => {
|
||||
component.categories = [];
|
||||
fixture.detectChanges();
|
||||
expect(getNoCategoriesMessage()).toBe('CATEGORIES_MANAGEMENT.NO_CATEGORIES_CREATED');
|
||||
});
|
||||
|
||||
it('should have correct remove category title', () => {
|
||||
const removeBtns = getRemoveCategoryButtons();
|
||||
const isTitleCorrect = removeBtns.every((removeBtn) => removeBtn.attributes.getNamedItem('title').textContent === 'CATEGORIES_MANAGEMENT.DELETE_CATEGORY');
|
||||
expect(isTitleCorrect).toBeTrue();
|
||||
});
|
||||
|
||||
it('should display create category label', fakeAsync(() => {
|
||||
typeCategory('test');
|
||||
|
||||
expect(getCreateCategoryLabel().textContent.trim()).toBe('CATEGORIES_MANAGEMENT.GENERIC_CREATE');
|
||||
}));
|
||||
|
||||
it('should hide create category label when typing', fakeAsync(() => {
|
||||
typeCategory('test', 0);
|
||||
|
||||
expect(getCreateCategoryLabel()).toBeUndefined();
|
||||
discardPeriodicTasks();
|
||||
flush();
|
||||
}));
|
||||
|
||||
it('should disable existing categories', fakeAsync(() => {
|
||||
typeCategory('test');
|
||||
|
||||
expect(getSelectionList().disabled).toBeTrue();
|
||||
}));
|
||||
|
||||
it('should be able to add typed category when create label is clicked', fakeAsync(() => {
|
||||
const categoriesChangeSpy = spyOn(component.categoriesChange, 'emit');
|
||||
createCategory('test');
|
||||
|
||||
expect(component.categories.length).toBe(3);
|
||||
expect(component.categories[2].name).toBe('test');
|
||||
expect(categoriesChangeSpy).toHaveBeenCalledOnceWith(component.categories);
|
||||
}));
|
||||
|
||||
it('should be able to add typed category when enter is hit', fakeAsync(() => {
|
||||
const categoriesChangeSpy = spyOn(component.categoriesChange, 'emit');
|
||||
createCategory('test', true);
|
||||
|
||||
expect(component.categories.length).toBe(3);
|
||||
expect(component.categories[2].name).toBe('test');
|
||||
expect(categoriesChangeSpy).toHaveBeenCalledOnceWith(component.categories);
|
||||
}));
|
||||
|
||||
it('should clear and hide input after category is created', fakeAsync(() => {
|
||||
const controlVisibilityChangeSpy = spyOn(component.categoryNameControlVisibleChange, 'emit');
|
||||
createCategory('test');
|
||||
const categoryControl: HTMLDivElement = fixture.debugElement.query(By.css('.adf-category-name-field')).nativeElement;
|
||||
|
||||
expect(categoryControl.hidden).toBeTrue();
|
||||
expect(controlVisibilityChangeSpy).toHaveBeenCalledOnceWith(false);
|
||||
expect(getExistingCategoriesList()).toEqual([]);
|
||||
expect(component.categoryNameControl.value).toBe('');
|
||||
expect(component.categoryNameControl.untouched).toBeTrue();
|
||||
}));
|
||||
|
||||
it('should be able to remove added category', fakeAsync(() => {
|
||||
createCategory('test');
|
||||
|
||||
const categoriesChangeSpy = spyOn(component.categoriesChange, 'emit');
|
||||
const removeCategoryBtns = getRemoveCategoryButtons();
|
||||
removeCategoryBtns[2].click();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.categories.length).toBe(2);
|
||||
expect(component.categories[1].name).toBe('testCat4');
|
||||
expect(categoriesChangeSpy).toHaveBeenCalledOnceWith(component.categories);
|
||||
}));
|
||||
|
||||
describe('Errors', () => {
|
||||
it('should display validation error when searching for empty category', fakeAsync(() => {
|
||||
typeCategory(' ');
|
||||
|
||||
expect(getFirstError()).toBe('CATEGORIES_MANAGEMENT.ERRORS.EMPTY_CATEGORY');
|
||||
}));
|
||||
|
||||
it('should show error for required', fakeAsync(() => {
|
||||
typeCategory('');
|
||||
|
||||
expect(getFirstError()).toBe('CATEGORIES_MANAGEMENT.ERRORS.REQUIRED');
|
||||
}));
|
||||
|
||||
it('should show error when duplicated already added category', fakeAsync(() => {
|
||||
const category = 'new Cat';
|
||||
|
||||
createCategory(category);
|
||||
typeCategory(category);
|
||||
|
||||
expect(getFirstError()).toBe('CATEGORIES_MANAGEMENT.ERRORS.DUPLICATED_CATEGORY');
|
||||
}));
|
||||
|
||||
it('should show error when duplicated already existing category', fakeAsync(() => {
|
||||
const category = 'testCat2';
|
||||
|
||||
typeCategory(category);
|
||||
|
||||
expect(getFirstError()).toBe('CATEGORIES_MANAGEMENT.ERRORS.ALREADY_EXISTS');
|
||||
}));
|
||||
|
||||
it('should show error for required when not typed anything and blur input', fakeAsync(() => {
|
||||
typeCategory('');
|
||||
getCategoryControlInput().blur();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(getFirstError()).toBe('CATEGORIES_MANAGEMENT.ERRORS.REQUIRED');
|
||||
|
||||
flush();
|
||||
}));
|
||||
|
||||
it('should not display create category label when control has error', fakeAsync(() => {
|
||||
typeCategory(' ');
|
||||
|
||||
expect(getCreateCategoryLabel().hidden).toBeTrue();
|
||||
}));
|
||||
});
|
||||
});
|
||||
});
|
@@ -0,0 +1,332 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright © 2005-2023 Hyland Software, Inc. and its affiliates. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Category } from '@alfresco/js-api';
|
||||
import { Component, ElementRef, EventEmitter, Input, OnDestroy, OnInit, Output, ViewChild, ViewEncapsulation } from '@angular/core';
|
||||
import { FormControl, Validators } from '@angular/forms';
|
||||
import { MatSelectionListChange } from '@angular/material/list';
|
||||
import { EMPTY, Observable, Subject, timer } from 'rxjs';
|
||||
import { debounce, first, map, takeUntil, tap } from 'rxjs/operators';
|
||||
import { CategoriesManagementMode } from './categories-management-mode';
|
||||
import { CategoryService } from '../services/category.service';
|
||||
|
||||
interface CategoryNameControlErrors {
|
||||
duplicatedExistingCategory?: boolean;
|
||||
duplicatedCategory?: boolean;
|
||||
emptyCategory?: boolean;
|
||||
required?: boolean;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'adf-categories-management',
|
||||
templateUrl: './categories-management.component.html',
|
||||
styleUrls: ['./categories-management.component.scss'],
|
||||
encapsulation: ViewEncapsulation.None
|
||||
})
|
||||
export class CategoriesManagementComponent implements OnInit, OnDestroy {
|
||||
readonly nameErrorMessagesByErrors = new Map<keyof CategoryNameControlErrors, string>([
|
||||
['duplicatedExistingCategory', 'ALREADY_EXISTS'],
|
||||
['duplicatedCategory', 'DUPLICATED_CATEGORY'],
|
||||
['emptyCategory', 'EMPTY_CATEGORY'],
|
||||
['required', 'REQUIRED']
|
||||
]);
|
||||
|
||||
private existingCategoryLoaded$ = new Subject<void>();
|
||||
private cancelExistingCategoriesLoading$ = new Subject<void>();
|
||||
private onDestroy$ = new Subject<void>();
|
||||
private _categoryNameControl = new FormControl<string>(
|
||||
'',
|
||||
[
|
||||
this.validateIfNotAlreadyAdded.bind(this),
|
||||
this.validateEmptyCategory,
|
||||
Validators.required
|
||||
],
|
||||
this.validateIfNotAlreadyCreated.bind(this)
|
||||
);
|
||||
private _existingCategories: Category[];
|
||||
private _categoryNameErrorMessageKey = '';
|
||||
private _existingCategoriesLoading = false;
|
||||
private _typing = false;
|
||||
private _existingCategoriesPanelVisible: boolean;
|
||||
private _categoryNameControlVisible = false;
|
||||
private readonly existingCategoriesListLimit = 15;
|
||||
initialCategories: Category[] = [];
|
||||
|
||||
/** Categories to display initially */
|
||||
@Input()
|
||||
categories: Category[] = [];
|
||||
|
||||
/**
|
||||
* Decides if categoryNameControl should be visible. Sets also existing categories panel visibility
|
||||
* and scrolls control into view when visible.
|
||||
*
|
||||
* @param categoryNameControlVisible control visibility.
|
||||
*/
|
||||
@Input()
|
||||
set categoryNameControlVisible(categoryNameControlVisible: boolean) {
|
||||
this._categoryNameControlVisible = categoryNameControlVisible;
|
||||
if (categoryNameControlVisible) {
|
||||
setTimeout(() => {
|
||||
this.categoryNameInputElement.nativeElement.scrollIntoView();
|
||||
});
|
||||
this._existingCategoriesPanelVisible = true;
|
||||
} else {
|
||||
this._existingCategoriesPanelVisible = false;
|
||||
}
|
||||
}
|
||||
|
||||
get categoryNameControlVisible(): boolean {
|
||||
return this._categoryNameControlVisible;
|
||||
}
|
||||
|
||||
/** Emits when classifiable aspect changes */
|
||||
@Input()
|
||||
classifiableChanged: Observable<void>;
|
||||
|
||||
/** Disables remove button in upper categories list */
|
||||
@Input()
|
||||
disableRemoval = false;
|
||||
|
||||
/**
|
||||
* Component mode.
|
||||
* In ASSIGN mode we can only assign/unassign categories from existing list.
|
||||
* In CRUD mode we can create categories.
|
||||
*/
|
||||
@Input()
|
||||
managementMode: CategoriesManagementMode;
|
||||
|
||||
/** ID of a parent category. New categories will be created under this parent */
|
||||
@Input()
|
||||
parentId: string;
|
||||
|
||||
/** Emits when state of upper categories list changes */
|
||||
@Output()
|
||||
categoriesChange = new EventEmitter<Category[]>();
|
||||
|
||||
/** Emits when categoryNameControl visibility changes */
|
||||
@Output()
|
||||
categoryNameControlVisibleChange = new EventEmitter<boolean>();
|
||||
|
||||
@ViewChild('categoryNameInput')
|
||||
private categoryNameInputElement: ElementRef;
|
||||
|
||||
constructor(private categoryService: CategoryService) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.categoryNameControl.valueChanges
|
||||
.pipe(
|
||||
map((name: string) => name.trim()),
|
||||
tap((name: string) => {
|
||||
this._typing = true;
|
||||
if (name) {
|
||||
this._existingCategoriesLoading = true;
|
||||
this._existingCategoriesPanelVisible = true;
|
||||
}
|
||||
this.cancelExistingCategoriesLoading$.next();
|
||||
}),
|
||||
debounce((name: string) => (name ? timer(300) : EMPTY)),
|
||||
takeUntil(this.onDestroy$)
|
||||
)
|
||||
.subscribe((name: string) => this.onNameControlValueChange(name));
|
||||
|
||||
this.categoryNameControl.statusChanges
|
||||
.pipe(takeUntil(this.onDestroy$))
|
||||
.subscribe(() => this.setCategoryNameControlErrorMessageKey());
|
||||
|
||||
this.setCategoryNameControlErrorMessageKey();
|
||||
|
||||
if (!this.isCRUDMode) {
|
||||
this._categoryNameControl.removeValidators(Validators.required);
|
||||
this.categories.forEach((category) => this.initialCategories.push(category));
|
||||
this.classifiableChanged
|
||||
.pipe(takeUntil(this.onDestroy$))
|
||||
.subscribe(() => {
|
||||
this.categories = [];
|
||||
this.categoryNameControlVisible = false;
|
||||
this.categoryNameControlVisibleChange.emit(false);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.onDestroy$.next();
|
||||
this.onDestroy$.complete();
|
||||
this.cancelExistingCategoriesLoading$.next();
|
||||
this.cancelExistingCategoriesLoading$.complete();
|
||||
}
|
||||
|
||||
get categoryNameControl(): FormControl<string> {
|
||||
return this._categoryNameControl;
|
||||
}
|
||||
|
||||
get existingCategories(): Category[] {
|
||||
return this._existingCategories;
|
||||
}
|
||||
|
||||
get categoryNameErrorMessageKey(): string {
|
||||
return this._categoryNameErrorMessageKey;
|
||||
}
|
||||
|
||||
get existingCategoriesLoading(): boolean {
|
||||
return this._existingCategoriesLoading;
|
||||
}
|
||||
|
||||
get typing(): boolean {
|
||||
return this._typing;
|
||||
}
|
||||
|
||||
get existingCategoriesPanelVisible(): boolean {
|
||||
return this._existingCategoriesPanelVisible;
|
||||
}
|
||||
|
||||
get isCRUDMode(): boolean {
|
||||
return this.managementMode === CategoriesManagementMode.CRUD;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hides and emits categoryNameControl and hides existing categories panel.
|
||||
*/
|
||||
hideNameInput() {
|
||||
this.categoryNameControlVisible = false;
|
||||
this.categoryNameControlVisibleChange.emit(false);
|
||||
this._existingCategoriesPanelVisible = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds category that has been typed to a categoryNameControl and hides it afterwards.
|
||||
*/
|
||||
addCategory() {
|
||||
if (this.isCRUDMode && !this._typing && !this.categoryNameControl.invalid) {
|
||||
const newCatName = this.categoryNameControl.value.trim();
|
||||
const newCat = new Category({ id: newCatName, name: newCatName });
|
||||
this.categories.push(newCat);
|
||||
this.hideNameInput();
|
||||
this.categoryNameControl.setValue('');
|
||||
this.categoryNameControl.markAsUntouched();
|
||||
this._existingCategories = null;
|
||||
this.categoriesChange.emit(this.categories);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds existing category to categories list and removes it from existing categories list.
|
||||
*
|
||||
* @param change - selection list change containing selected category
|
||||
*/
|
||||
addCategoryToAssign(change: MatSelectionListChange) {
|
||||
const selectedCategory: Category = change.options[0].value;
|
||||
this.categories.push(selectedCategory);
|
||||
this._existingCategories.splice(this._existingCategories.indexOf(selectedCategory), 1);
|
||||
this.categoryNameControl.updateValueAndValidity();
|
||||
this.categoriesChange.emit(this.categories);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the category from categories list and adds it to existing categories list in ASSIGN mode.
|
||||
*
|
||||
* @param category - category to remove
|
||||
*/
|
||||
removeCategory(category: Category) {
|
||||
this.categories.splice(this.categories.indexOf(category), 1);
|
||||
if (!this.isCRUDMode && !!this._existingCategories && !this.initialCategories.some((cat) => cat.id === category.id)) {
|
||||
this._existingCategories.push(category);
|
||||
this.sortCategoriesList(this._existingCategories);
|
||||
}
|
||||
this.categoryNameControl.updateValueAndValidity({
|
||||
emitEvent: false
|
||||
});
|
||||
this.categoriesChange.emit(this.categories);
|
||||
}
|
||||
|
||||
private onNameControlValueChange(name: string) {
|
||||
this.categoryNameControl.markAsTouched();
|
||||
if (name) {
|
||||
if (this.isCRUDMode) {
|
||||
this.getChildrenCategories(name);
|
||||
} else {
|
||||
this.searchForExistingCategories(name);
|
||||
}
|
||||
} else {
|
||||
this._existingCategories = null;
|
||||
}
|
||||
}
|
||||
|
||||
private searchForExistingCategories(searchTerm: string) {
|
||||
this.categoryService.searchCategories(searchTerm, 0 , this.existingCategoriesListLimit).subscribe((existingCategoriesResult) => {
|
||||
this._existingCategories = existingCategoriesResult.list.entries.map((rowEntry) => {
|
||||
const existingCat = new Category();
|
||||
existingCat.id = rowEntry.entry.id;
|
||||
const path = rowEntry.entry.path.name.split('/').splice(3).join('/');
|
||||
existingCat.name = path ? `${path}/${rowEntry.entry.name}` : rowEntry.entry.name;
|
||||
return existingCat;
|
||||
});
|
||||
this._existingCategories = this._existingCategories.filter((existingCat) => this.categories.find((category) => existingCat.id === category.id) === undefined);
|
||||
this.sortCategoriesList(this._existingCategories);
|
||||
this._existingCategoriesLoading = false;
|
||||
this._typing = false;
|
||||
this.existingCategoryLoaded$.next();
|
||||
});
|
||||
}
|
||||
|
||||
private getChildrenCategories(searchTerm: string) {
|
||||
this.categoryService.getSubcategories(this.parentId).subscribe((childrenCategories) => {
|
||||
this._existingCategories = childrenCategories.list.entries.map((categoryEntry) => categoryEntry.entry);
|
||||
this._existingCategories = this._existingCategories.filter((existingCat) => existingCat.name.toLowerCase().includes(searchTerm.toLowerCase()));
|
||||
this.sortCategoriesList(this._existingCategories);
|
||||
this._existingCategoriesLoading = false;
|
||||
this._typing = false;
|
||||
this.existingCategoryLoaded$.next();
|
||||
});
|
||||
}
|
||||
|
||||
private validateIfNotAlreadyAdded(nameControl: FormControl<string>): CategoryNameControlErrors | null {
|
||||
return this.categories?.some((category) => this.compareCategories(category, nameControl.value)) && this.isCRUDMode
|
||||
? { duplicatedCategory: true }
|
||||
: null;
|
||||
}
|
||||
|
||||
private validateIfNotAlreadyCreated(nameControl: FormControl<string>): Observable<CategoryNameControlErrors | null> {
|
||||
return this.existingCategoryLoaded$.pipe(
|
||||
map<void, CategoryNameControlErrors | null>(() => this.existingCategories.some((category) => this.compareCategories(category, nameControl.value)) && this.isCRUDMode
|
||||
? { duplicatedExistingCategory: true }
|
||||
: null),
|
||||
first()
|
||||
);
|
||||
}
|
||||
|
||||
private compareCategories(category1?: Category, cat2Name?: string): boolean {
|
||||
return category1?.name.trim().toUpperCase() === cat2Name?.trim().toUpperCase();
|
||||
}
|
||||
|
||||
private validateEmptyCategory(categoryNameControl: FormControl<string>): CategoryNameControlErrors | null {
|
||||
return categoryNameControl.value.length && !categoryNameControl.value.trim()
|
||||
? { emptyCategory: true }
|
||||
: null;
|
||||
}
|
||||
|
||||
private setCategoryNameControlErrorMessageKey() {
|
||||
this._categoryNameErrorMessageKey = this.categoryNameControl.invalid
|
||||
? `CATEGORIES_MANAGEMENT.ERRORS.${this.nameErrorMessagesByErrors.get(
|
||||
Object.keys(this.categoryNameControl.errors)[0] as keyof CategoryNameControlErrors
|
||||
)}`
|
||||
: '';
|
||||
}
|
||||
|
||||
private sortCategoriesList(categoriesList: Category[]) {
|
||||
categoriesList.sort((category1, category2) => category1.name.localeCompare(category2.name));
|
||||
}
|
||||
}
|
41
lib/content-services/src/lib/category/category.module.ts
Normal file
41
lib/content-services/src/lib/category/category.module.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright © 2005-2023 Hyland Software, Inc. and its affiliates. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { CoreModule } from '@alfresco/adf-core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { ContentDirectiveModule } from '../directives/content-directive.module';
|
||||
import { MaterialModule } from '../material.module';
|
||||
import { CategoriesManagementComponent } from './categories-management/categories-management.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
CoreModule,
|
||||
MaterialModule,
|
||||
TranslateModule,
|
||||
ContentDirectiveModule
|
||||
],
|
||||
declarations: [
|
||||
CategoriesManagementComponent
|
||||
],
|
||||
exports: [
|
||||
CategoriesManagementComponent
|
||||
]
|
||||
})
|
||||
export class CategoriesModule {}
|
@@ -18,3 +18,6 @@
|
||||
export * from './services/category.service';
|
||||
export * from './services/category-tree-datasource.service';
|
||||
export * from './models/category-node.interface';
|
||||
export * from './category.module';
|
||||
export * from './categories-management/categories-management.component';
|
||||
export * from './categories-management/categories-management-mode';
|
||||
|
@@ -19,6 +19,7 @@ import { CoreTestingModule, UserPreferencesService } from '@alfresco/adf-core';
|
||||
import {
|
||||
CategoryBody,
|
||||
CategoryEntry,
|
||||
CategoryLinkBody,
|
||||
CategoryPaging, PathInfo,
|
||||
RequestQuery, ResultNode,
|
||||
ResultSetPaging,
|
||||
@@ -32,9 +33,12 @@ describe('CategoryService', () => {
|
||||
let userPreferencesService: UserPreferencesService;
|
||||
|
||||
const fakeParentCategoryId = 'testParentId';
|
||||
const fakeCategoryId = 'fakeId';
|
||||
const fakeNodeId = 'fakeNodeId';
|
||||
const fakeCategoriesResponse: CategoryPaging = { list: { pagination: {}, entries: [] }};
|
||||
const fakeCategoryEntry: CategoryEntry = { entry: { id: 'testId', name: 'testName' }};
|
||||
const fakeCategoryBody: CategoryBody = { name: 'updatedName' };
|
||||
const fakeCategoriesLinkBodies: CategoryLinkBody[] = [{ categoryId: fakeCategoryId }];
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
@@ -150,4 +154,25 @@ describe('CategoryService', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should fetch categories linked to node with nodeId with path included', fakeAsync(() => {
|
||||
const getLinkedCategoriesSpy = spyOn(categoryService.categoriesApi, 'getCategoryLinksForNode').and.returnValue(Promise.resolve(fakeCategoriesResponse));
|
||||
categoryService.getCategoryLinksForNode(fakeNodeId).subscribe(() => {
|
||||
expect(getLinkedCategoriesSpy).toHaveBeenCalledOnceWith(fakeNodeId, {include: ['path']});
|
||||
});
|
||||
}));
|
||||
|
||||
it('should unlink categories from node', fakeAsync(() => {
|
||||
const unlinkCategoriesSpy = spyOn(categoryService.categoriesApi, 'unlinkNodeFromCategory').and.returnValue(Promise.resolve());
|
||||
categoryService.unlinkNodeFromCategory(fakeNodeId, fakeCategoryId).subscribe(() => {
|
||||
expect(unlinkCategoriesSpy).toHaveBeenCalledOnceWith(fakeNodeId, fakeCategoryId);
|
||||
});
|
||||
}));
|
||||
|
||||
it('should link categories to a node with nodeId', fakeAsync(() => {
|
||||
const linkCategoriesSpy = spyOn(categoryService.categoriesApi, 'linkNodeToCategory').and.returnValue(Promise.resolve(fakeCategoryEntry));
|
||||
categoryService.linkNodeToCategory(fakeNodeId, fakeCategoriesLinkBodies).subscribe(() => {
|
||||
expect(linkCategoriesSpy).toHaveBeenCalledOnceWith(fakeNodeId, fakeCategoriesLinkBodies);
|
||||
});
|
||||
}));
|
||||
});
|
||||
|
@@ -21,6 +21,7 @@ import {
|
||||
CategoriesApi,
|
||||
CategoryBody,
|
||||
CategoryEntry,
|
||||
CategoryLinkBody,
|
||||
CategoryPaging,
|
||||
RequestQuery,
|
||||
ResultSetPaging,
|
||||
@@ -121,4 +122,36 @@ export class CategoryService {
|
||||
include: ['path']
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* List of categories that node is assigned to
|
||||
*
|
||||
* @param nodeId The identifier of a node.
|
||||
* @return Observable<CategoryPaging> Categories that node is assigned to
|
||||
*/
|
||||
getCategoryLinksForNode(nodeId: string): Observable<CategoryPaging> {
|
||||
return from(this.categoriesApi.getCategoryLinksForNode(nodeId, {include: ['path']}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Unlink category from a node
|
||||
*
|
||||
* @param nodeId The identifier of a node.
|
||||
* @param categoryId The identifier of a category.
|
||||
* @return Observable<void>
|
||||
*/
|
||||
unlinkNodeFromCategory(nodeId: string, categoryId: string): Observable<void> {
|
||||
return from(this.categoriesApi.unlinkNodeFromCategory(nodeId, categoryId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Link node to a category
|
||||
*
|
||||
* @param nodeId The identifier of a node.
|
||||
* @param categoryLinkBodyCreate Array of a categories that node will be linked to.
|
||||
* @return Observable<CategoryEntry>
|
||||
*/
|
||||
linkNodeToCategory(nodeId: string, categoryLinkBodyCreate: CategoryLinkBody[]): Observable<CategoryPaging | CategoryEntry> {
|
||||
return from(this.categoriesApi.linkNodeToCategory(nodeId, categoryLinkBodyCreate));
|
||||
}
|
||||
}
|
||||
|
@@ -9,7 +9,8 @@
|
||||
[multi]="multi"
|
||||
[displayAspect]="displayAspect"
|
||||
[preset]="preset"
|
||||
[displayTags]="true">
|
||||
[displayTags]="true"
|
||||
[displayCategories]="true">
|
||||
</adf-content-metadata>
|
||||
</mat-card-content>
|
||||
<mat-card-footer class="adf-content-metadata-card-footer" fxLayout="row" fxLayoutAlign="space-between stretch">
|
||||
|
@@ -96,6 +96,10 @@ describe('ContentMetadataCardComponent', () => {
|
||||
expect(fixture.debugElement.query(By.directive(ContentMetadataComponent)).componentInstance.displayTags).toBeTrue();
|
||||
});
|
||||
|
||||
it('should assign true to displayCategories for ContentMetadataComponent', () => {
|
||||
expect(fixture.debugElement.query(By.directive(ContentMetadataComponent)).componentInstance.displayCategories).toBeTrue();
|
||||
});
|
||||
|
||||
it('should pass through the preset to the underlying component', () => {
|
||||
const contentMetadataComponent = fixture.debugElement.query(By.directive(ContentMetadataComponent)).componentInstance;
|
||||
|
||||
|
@@ -49,6 +49,35 @@
|
||||
</adf-tags-creator>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="displayCategories">
|
||||
<mat-expansion-panel *ngIf="!editable">
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title>{{ 'CATEGORIES_MANAGEMENT.CATEGORIES_TITLE' | translate }}</mat-panel-title>
|
||||
</mat-expansion-panel-header>
|
||||
<p *ngFor="let category of categories" class="adf-metadata-categories">{{ category.name }}</p>
|
||||
</mat-expansion-panel>
|
||||
<div *ngIf="editable"
|
||||
class="adf-metadata-categories-header">
|
||||
<div class="adf-metadata-categories-title">
|
||||
<p>{{ 'CATEGORIES_MANAGEMENT.CATEGORIES_TITLE' | translate }}</p>
|
||||
<button
|
||||
mat-icon-button
|
||||
[attr.title]="'CATEGORIES_MANAGEMENT.ASSIGN_CATEGORIES' | translate"
|
||||
[hidden]="categoryControlVisible || saving"
|
||||
(click)="categoryControlVisible = true">
|
||||
<mat-icon>add</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
<adf-categories-management
|
||||
[(categoryNameControlVisible)]="categoryControlVisible"
|
||||
[disableRemoval]="saving"
|
||||
[categories]="categories"
|
||||
[managementMode]="categoriesManagementMode"
|
||||
[classifiableChanged]="classifiableChanged"
|
||||
(categoriesChange)="storeCategoriesToAssign($event)">
|
||||
</adf-categories-management>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="expanded">
|
||||
<ng-container *ngIf="groupedProperties$ | async; else loading; let groupedProperties">
|
||||
<div *ngFor="let group of groupedProperties; let first = first;"
|
||||
|
@@ -63,4 +63,26 @@
|
||||
justify-content: space-evenly;
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
&-metadata-categories-header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0 24px;
|
||||
|
||||
.adf-metadata-categories-title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 15px;
|
||||
height: 64px;
|
||||
|
||||
button {
|
||||
margin-right: -14px;
|
||||
}
|
||||
|
||||
[hidden] {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -18,7 +18,7 @@
|
||||
import { ComponentFixture, TestBed, tick, fakeAsync } from '@angular/core/testing';
|
||||
import { DebugElement, SimpleChange } from '@angular/core';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { ClassesApi, MinimalNode, Node, Tag, TagBody, TagEntry, TagPaging, TagPagingList } from '@alfresco/js-api';
|
||||
import { Category, CategoryPaging, ClassesApi, MinimalNode, Node, Tag, TagBody, TagEntry, TagPaging, TagPagingList } from '@alfresco/js-api';
|
||||
import { ContentMetadataComponent } from './content-metadata.component';
|
||||
import { ContentMetadataService } from '../../services/content-metadata.service';
|
||||
import {
|
||||
@@ -33,7 +33,7 @@ import { TranslateModule } from '@ngx-translate/core';
|
||||
import { CardViewContentUpdateService } from '../../../common/services/card-view-content-update.service';
|
||||
import { PropertyGroup } from '../../interfaces/property-group.interface';
|
||||
import { PropertyDescriptorsService } from '../../services/property-descriptors.service';
|
||||
import { TagsCreatorComponent, TagsCreatorMode, TagService } from '@alfresco/adf-content-services';
|
||||
import { CategoriesManagementComponent, CategoriesManagementMode, CategoryService, TagsCreatorComponent, TagsCreatorMode, TagService } from '@alfresco/adf-content-services';
|
||||
|
||||
describe('ContentMetadataComponent', () => {
|
||||
let component: ContentMetadataComponent;
|
||||
@@ -44,6 +44,7 @@ describe('ContentMetadataComponent', () => {
|
||||
let node: Node;
|
||||
let folderNode: Node;
|
||||
let tagService: TagService;
|
||||
let categoryService: CategoryService;
|
||||
|
||||
const preset = 'custom-preset';
|
||||
|
||||
@@ -62,6 +63,10 @@ describe('ContentMetadataComponent', () => {
|
||||
return tagPaging;
|
||||
};
|
||||
|
||||
const category1 = new Category({ id: 'test', name: 'testCat' });
|
||||
const category2 = new Category({ id: 'test2', name: 'testCat2' });
|
||||
const categoryPagingResponse: CategoryPaging = { list: { pagination: {}, entries: [ { entry: category1 }, { entry: category2 }]}};
|
||||
|
||||
const findTagElements = (): DebugElement[] => fixture.debugElement.queryAll(By.css('.adf-metadata-properties-tag'));
|
||||
|
||||
const findCancelButton = (): HTMLButtonElement => fixture.debugElement.query(By.css('[data-automation-id=reset-metadata]')).nativeElement;
|
||||
@@ -83,24 +88,46 @@ describe('ContentMetadataComponent', () => {
|
||||
const findShowingTagInputButton = (): HTMLButtonElement =>
|
||||
fixture.debugElement.query(By.css('[data-automation-id=showing-tag-input-button]')).nativeElement;
|
||||
|
||||
function getCategories(): HTMLParagraphElement[] {
|
||||
return fixture.debugElement.queryAll(By.css('.adf-metadata-categories'))?.map((debugElem) => debugElem.nativeElement);
|
||||
}
|
||||
|
||||
function getCategoriesManagementComponent(): CategoriesManagementComponent {
|
||||
return fixture.debugElement.query(By.directive(CategoriesManagementComponent))?.componentInstance;
|
||||
}
|
||||
|
||||
function getAssignCategoriesBtn(): HTMLButtonElement {
|
||||
return fixture.debugElement.query(By.css('.adf-metadata-categories-title button')).nativeElement;
|
||||
}
|
||||
|
||||
setupTestBed({
|
||||
imports: [
|
||||
TranslateModule.forRoot(),
|
||||
ContentTestingModule
|
||||
],
|
||||
providers: [{
|
||||
provide: LogService,
|
||||
useValue: {
|
||||
error: jasmine.createSpy('error')
|
||||
}
|
||||
}, {
|
||||
provide: TagService,
|
||||
useValue: {
|
||||
getTagsByNodeId: () => EMPTY,
|
||||
removeTag: () => EMPTY,
|
||||
assignTagsToNode: () => EMPTY
|
||||
}
|
||||
}]
|
||||
providers: [
|
||||
{
|
||||
provide: LogService,
|
||||
useValue: {
|
||||
error: jasmine.createSpy('error')
|
||||
}
|
||||
},
|
||||
{
|
||||
provide: TagService,
|
||||
useValue: {
|
||||
getTagsByNodeId: () => EMPTY,
|
||||
removeTag: () => EMPTY,
|
||||
assignTagsToNode: () => EMPTY
|
||||
}
|
||||
},
|
||||
{
|
||||
provide: CategoryService,
|
||||
useValue: {
|
||||
getCategoryLinksForNode: () => EMPTY,
|
||||
linkNodeToCategory: () => EMPTY,
|
||||
unlinkNodeFromCategory: () => EMPTY
|
||||
}
|
||||
}]
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -110,6 +137,7 @@ describe('ContentMetadataComponent', () => {
|
||||
updateService = TestBed.inject(CardViewContentUpdateService);
|
||||
nodesApiService = TestBed.inject(NodesApiService);
|
||||
tagService = TestBed.inject(TagService);
|
||||
categoryService = TestBed.inject(CategoryService);
|
||||
|
||||
node = {
|
||||
id: 'node-id',
|
||||
@@ -1137,11 +1165,202 @@ describe('ContentMetadataComponent', () => {
|
||||
expect(findTagsCreator()).toBeDefined();
|
||||
});
|
||||
|
||||
it('should not show tags creator if editable is true and displayTags is false', () => {
|
||||
component.editable = true;
|
||||
component.displayTags = false;
|
||||
fixture.detectChanges();
|
||||
expect(findTagsCreator()).toBeUndefined();
|
||||
describe('Categories list', () => {
|
||||
beforeEach(() => {
|
||||
component.displayCategories = true;
|
||||
component.node.aspectNames.push('generalclassifiable');
|
||||
spyOn(categoryService, 'getCategoryLinksForNode').and.returnValue(of(categoryPagingResponse));
|
||||
});
|
||||
|
||||
it('should render categories node is assigned to', () => {
|
||||
component.ngOnInit();
|
||||
fixture.detectChanges();
|
||||
|
||||
const categories = getCategories();
|
||||
expect(categories.length).toBe(2);
|
||||
expect(categories[0].textContent).toBe(category1.name);
|
||||
expect(categories[1].textContent).toBe(category2.name);
|
||||
expect(categoryService.getCategoryLinksForNode).toHaveBeenCalledWith(node.id);
|
||||
});
|
||||
|
||||
it('should not render categories after loading categories in ngOnInit if displayCategories is false', () => {
|
||||
component.displayCategories = false;
|
||||
component.ngOnInit();
|
||||
fixture.detectChanges();
|
||||
|
||||
const categories = getCategories();
|
||||
expect(categories).toHaveSize(0);
|
||||
expect(categoryService.getCategoryLinksForNode).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should render categories when ngOnChanges', () => {
|
||||
component.ngOnChanges({ node: new SimpleChange(undefined, node, false)});
|
||||
fixture.detectChanges();
|
||||
|
||||
const categories = getCategories();
|
||||
expect(categories.length).toBe(2);
|
||||
expect(categories[0].textContent).toBe(category1.name);
|
||||
expect(categories[1].textContent).toBe(category2.name);
|
||||
expect(categoryService.getCategoryLinksForNode).toHaveBeenCalledWith(node.id);
|
||||
});
|
||||
|
||||
it('should not render categories after loading categories in ngOnChanges if displayCategories is false', () => {
|
||||
component.displayCategories = false;
|
||||
component.ngOnChanges({
|
||||
node: new SimpleChange(undefined, node, false)
|
||||
});
|
||||
fixture.detectChanges();
|
||||
const categories = getCategories();
|
||||
expect(categories).toHaveSize(0);
|
||||
expect(categoryService.getCategoryLinksForNode).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not reload categories in ngOnChanges if node is not changed', () => {
|
||||
|
||||
component.ngOnChanges({});
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(categoryService.getCategoryLinksForNode).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should render categories after discard changes button is clicked', fakeAsync(() => {
|
||||
component.editable = true;
|
||||
fixture.detectChanges();
|
||||
TestBed.inject(CardViewContentUpdateService).itemUpdated$.next({
|
||||
changed: {}
|
||||
} as UpdateNotification);
|
||||
tick(500);
|
||||
fixture.detectChanges();
|
||||
|
||||
clickOnCancel();
|
||||
component.editable = false;
|
||||
fixture.detectChanges();
|
||||
|
||||
const categories = getCategories();
|
||||
expect(categories.length).toBe(2);
|
||||
expect(categories[0].textContent).toBe(category1.name);
|
||||
expect(categories[1].textContent).toBe(category2.name);
|
||||
expect(categoryService.getCategoryLinksForNode).toHaveBeenCalledWith(node.id);
|
||||
}));
|
||||
|
||||
it('should be hidden when editable is true', () => {
|
||||
component.editable = true;
|
||||
fixture.detectChanges();
|
||||
expect(getCategories().length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Categories management', () => {
|
||||
let categoriesManagementComponent: CategoriesManagementComponent;
|
||||
|
||||
beforeEach(() => {
|
||||
component.editable = true;
|
||||
component.displayCategories = true;
|
||||
component.node.aspectNames.push('generalclassifiable');
|
||||
spyOn(categoryService, 'getCategoryLinksForNode').and.returnValue(of(categoryPagingResponse));
|
||||
fixture.detectChanges();
|
||||
categoriesManagementComponent = getCategoriesManagementComponent();
|
||||
});
|
||||
|
||||
it('should set categoryNameControlVisible to false initially', () => {
|
||||
expect(categoriesManagementComponent.categoryNameControlVisible).toBeFalse();
|
||||
});
|
||||
|
||||
it('should hide assign categories button when categoryNameControlVisible changes to true', () => {
|
||||
categoriesManagementComponent.categoryNameControlVisibleChange.emit(true);
|
||||
fixture.detectChanges();
|
||||
expect(getAssignCategoriesBtn().hasAttribute('hidden')).toBeTrue();
|
||||
});
|
||||
|
||||
it('should show assign categories button when categoryNameControlVisible changes to false', fakeAsync(() => {
|
||||
categoriesManagementComponent.categoryNameControlVisibleChange.emit(true);
|
||||
fixture.detectChanges();
|
||||
tick();
|
||||
categoriesManagementComponent.categoryNameControlVisibleChange.emit(false);
|
||||
fixture.detectChanges();
|
||||
tick(100);
|
||||
expect(getAssignCategoriesBtn().hasAttribute('hidden')).toBeFalse();
|
||||
}));
|
||||
|
||||
it('should have correct mode', () => {
|
||||
expect(categoriesManagementComponent.managementMode).toBe(CategoriesManagementMode.ASSIGN);
|
||||
});
|
||||
|
||||
it('should clear categories and emit event when classifiable changes', (done) => {
|
||||
component.node.aspectNames = [];
|
||||
component.ngOnChanges({ node: new SimpleChange(undefined, node, false)});
|
||||
component.classifiableChanged.subscribe(() => {
|
||||
expect(component.categories).toEqual([]);
|
||||
done();
|
||||
});
|
||||
component.ngOnChanges({ node: new SimpleChange(undefined, node, false)});
|
||||
});
|
||||
|
||||
it('should enable discard and save buttons after emitting categories change event', () => {
|
||||
categoriesManagementComponent.categoriesChange.emit([category1, category2]);
|
||||
fixture.detectChanges();
|
||||
expect(findCancelButton().disabled).toBeFalse();
|
||||
expect(findSaveButton().disabled).toBeFalse();
|
||||
});
|
||||
|
||||
it('should not disable removal initially', () => {
|
||||
expect(categoriesManagementComponent.disableRemoval).toBeFalse();
|
||||
});
|
||||
|
||||
it('should disable removal on saving', () => {
|
||||
categoriesManagementComponent.categoriesChange.emit([]);
|
||||
fixture.detectChanges();
|
||||
|
||||
clickOnSave();
|
||||
expect(categoriesManagementComponent.disableRemoval).toBeTrue();
|
||||
});
|
||||
|
||||
it('should not disable removal if forkJoin fails', fakeAsync( () => {
|
||||
const property = { key: 'properties.property-key', value: 'original-value' } as CardViewBaseItemModel;
|
||||
const expectedNode = { ...node, name: 'some-modified-value' };
|
||||
spyOn(nodesApiService, 'updateNode').and.returnValue(of(expectedNode));
|
||||
component.ngOnInit();
|
||||
spyOn(tagService, 'removeTag').and.returnValue(EMPTY);
|
||||
spyOn(tagService, 'assignTagsToNode').and.returnValue(EMPTY);
|
||||
spyOn(categoryService, 'unlinkNodeFromCategory').and.returnValue(EMPTY);
|
||||
spyOn(categoryService, 'linkNodeToCategory').and.returnValue(throwError({}));
|
||||
|
||||
updateService.update(property, 'updated-value');
|
||||
tick(600);
|
||||
|
||||
fixture.detectChanges();
|
||||
categoriesManagementComponent.categoriesChange.emit([category1, category2]);
|
||||
clickOnSave();
|
||||
|
||||
expect(categoriesManagementComponent.disableRemoval).toBeFalse();
|
||||
}));
|
||||
|
||||
it('should set categoryNameControlVisible to false after saving', () => {
|
||||
categoriesManagementComponent.categoryNameControlVisibleChange.emit(true);
|
||||
categoriesManagementComponent.categoriesChange.emit([]);
|
||||
fixture.detectChanges();
|
||||
|
||||
clickOnSave();
|
||||
expect(categoriesManagementComponent.categoryNameControlVisible).toBeFalse();
|
||||
});
|
||||
|
||||
describe('Setting categories', () => {
|
||||
it('should set correct categories after ngOnInit', () => {
|
||||
component.ngOnInit();
|
||||
|
||||
fixture.detectChanges();
|
||||
expect(categoriesManagementComponent.categories).toEqual([ category1, category2 ]);
|
||||
expect(categoryService.getCategoryLinksForNode).toHaveBeenCalledWith(node.id);
|
||||
});
|
||||
|
||||
it('should set correct tags after ngOnChanges', () => {
|
||||
component.ngOnChanges({ node: new SimpleChange(undefined, node, false)});
|
||||
|
||||
fixture.detectChanges();
|
||||
expect(categoriesManagementComponent.categories).toEqual([ category1, category2 ]);
|
||||
expect(categoryService.getCategoryLinksForNode).toHaveBeenCalledWith(node.id);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
@@ -16,7 +16,7 @@
|
||||
*/
|
||||
|
||||
import { Component, Input, OnChanges, OnDestroy, OnInit, SimpleChanges, ViewEncapsulation } from '@angular/core';
|
||||
import { Node, TagBody, TagEntry, TagPaging } from '@alfresco/js-api';
|
||||
import { Category, CategoryEntry, CategoryLinkBody, CategoryPaging, Node, TagBody, TagEntry, TagPaging } from '@alfresco/js-api';
|
||||
import { Observable, Subject, of, zip, forkJoin } from 'rxjs';
|
||||
import {
|
||||
CardViewItem,
|
||||
@@ -33,6 +33,8 @@ import { CardViewContentUpdateService } from '../../../common/services/card-view
|
||||
import { NodesApiService } from '../../../common/services/nodes-api.service';
|
||||
import { TagsCreatorMode } from '../../../tag/tags-creator/tags-creator-mode';
|
||||
import { TagService } from '../../../tag/services/tag.service';
|
||||
import { CategoryService } from '../../../category/services/category.service';
|
||||
import { CategoriesManagementMode } from '../../../category/categories-management/categories-management-mode';
|
||||
|
||||
const DEFAULT_SEPARATOR = ', ';
|
||||
|
||||
@@ -99,6 +101,19 @@ export class ContentMetadataComponent implements OnChanges, OnInit, OnDestroy {
|
||||
@Input()
|
||||
displayTags = false;
|
||||
|
||||
/** True if categories should be displayed, false otherwise */
|
||||
@Input()
|
||||
displayCategories = false;
|
||||
|
||||
private _assignedTags: string[] = [];
|
||||
private assignedTagsEntries: TagEntry[];
|
||||
private _editable = false;
|
||||
private _tagsCreatorMode = TagsCreatorMode.CREATE_AND_ASSIGN;
|
||||
private _tags: string[] = [];
|
||||
private targetProperty: CardViewBaseItemModel;
|
||||
private classifiableChangedSubject = new Subject<void>();
|
||||
private _saving = false;
|
||||
|
||||
multiValueSeparator: string;
|
||||
basicProperties$: Observable<CardViewItem[]>;
|
||||
groupedProperties$: Observable<CardViewGroup[]>;
|
||||
@@ -106,14 +121,11 @@ export class ContentMetadataComponent implements OnChanges, OnInit, OnDestroy {
|
||||
changedProperties = {};
|
||||
hasMetadataChanged = false;
|
||||
tagNameControlVisible = false;
|
||||
|
||||
private _assignedTags: string[] = [];
|
||||
private assignedTagsEntries: TagEntry[] = [];
|
||||
private _editable = false;
|
||||
private _tagsCreatorMode = TagsCreatorMode.CREATE_AND_ASSIGN;
|
||||
private _tags: string[] = [];
|
||||
private targetProperty: CardViewBaseItemModel;
|
||||
private _saving = false;
|
||||
assignedCategories: Category[] = [];
|
||||
categories: Category[] = [];
|
||||
categoriesManagementMode = CategoriesManagementMode.ASSIGN;
|
||||
categoryControlVisible = false;
|
||||
classifiableChanged = this.classifiableChangedSubject.asObservable();
|
||||
|
||||
constructor(
|
||||
private contentMetadataService: ContentMetadataService,
|
||||
@@ -122,7 +134,8 @@ export class ContentMetadataComponent implements OnChanges, OnInit, OnDestroy {
|
||||
private logService: LogService,
|
||||
private translationService: TranslationService,
|
||||
private appConfig: AppConfigService,
|
||||
private tagService: TagService
|
||||
private tagService: TagService,
|
||||
private categoryService: CategoryService
|
||||
) {
|
||||
this.copyToClipboardAction = this.appConfig.get<boolean>('content-metadata.copy-to-clipboard-action');
|
||||
this.multiValueSeparator = this.appConfig.get<string>('content-metadata.multi-value-pipe-separator') || DEFAULT_SEPARATOR;
|
||||
@@ -194,24 +207,9 @@ export class ContentMetadataComponent implements OnChanges, OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
private loadProperties(node: Node) {
|
||||
if (node) {
|
||||
this.basicProperties$ = this.getProperties(node);
|
||||
this.groupedProperties$ = this.contentMetadataService.getGroupedProperties(node, this.preset);
|
||||
if (this.displayTags) {
|
||||
this.loadTagsForNode(node.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private getProperties(node: Node) {
|
||||
const properties$ = this.contentMetadataService.getBasicProperties(node);
|
||||
const contentTypeProperty$ = this.contentMetadataService.getContentTypeProperty(node);
|
||||
return zip(properties$, contentTypeProperty$)
|
||||
.pipe(map(([properties, contentTypeProperty]) => {
|
||||
const filteredProperties = contentTypeProperty.filter((property) => properties.findIndex((baseProperty) => baseProperty.key === property.key) === -1);
|
||||
return [...properties, ...filteredProperties];
|
||||
}));
|
||||
ngOnDestroy() {
|
||||
this.onDestroy$.next(true);
|
||||
this.onDestroy$.complete();
|
||||
}
|
||||
|
||||
updateChanges(updatedNodeChanges) {
|
||||
@@ -228,11 +226,13 @@ export class ContentMetadataComponent implements OnChanges, OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
/**
|
||||
* Called after clicking save button. It confirms all changes done for metadata. Before clicking on that button they are not saved.
|
||||
* Called after clicking save button. It confirms all changes done for metadata and hides both category and tag name controls.
|
||||
* Before clicking on that button they are not saved.
|
||||
*/
|
||||
saveChanges() {
|
||||
this._saving = true;
|
||||
this.tagNameControlVisible = false;
|
||||
this.categoryControlVisible = false;
|
||||
if (this.hasContentTypeChanged(this.changedProperties)) {
|
||||
this.contentMetadataService.openConfirmDialog(this.changedProperties).subscribe(() => {
|
||||
this.updateNode();
|
||||
@@ -253,34 +253,15 @@ export class ContentMetadataComponent implements OnChanges, OnInit, OnDestroy {
|
||||
this.hasMetadataChanged = true;
|
||||
}
|
||||
|
||||
private updateNode() {
|
||||
forkJoin({
|
||||
updatedNode: this.nodesApiService.updateNode(this.node.id, this.changedProperties),
|
||||
...(this.displayTags ? this.saveTags() : {})
|
||||
}).pipe(
|
||||
catchError((err) => {
|
||||
this.cardViewContentUpdateService.updateElement(this.targetProperty);
|
||||
this.handleUpdateError(err);
|
||||
return of(null);
|
||||
}))
|
||||
.subscribe((result) => {
|
||||
if (result) {
|
||||
if (this.hasContentTypeChanged(this.changedProperties)) {
|
||||
this.cardViewContentUpdateService.updateNodeAspect(this.node);
|
||||
}
|
||||
this.revertChanges();
|
||||
Object.assign(this.node, result.updatedNode);
|
||||
this.nodesApiService.nodeUpdated.next(this.node);
|
||||
if (Object.keys(result).length > 1 && this.displayTags) {
|
||||
this.loadTagsForNode(this.node.id);
|
||||
}
|
||||
}
|
||||
this._saving = false;
|
||||
});
|
||||
}
|
||||
|
||||
private hasContentTypeChanged(changedProperties): boolean {
|
||||
return !!changedProperties?.nodeType;
|
||||
/**
|
||||
* Store all categories that node should be assigned to. Please note that they are just in "stored" state and are not yet saved
|
||||
* until button for saving data is clicked. Calling that function causes that save button is enabled.
|
||||
*
|
||||
* @param categoriesToAssign array of categories to store.
|
||||
*/
|
||||
storeCategoriesToAssign(categoriesToAssign: Category[]) {
|
||||
this.categories = categoriesToAssign;
|
||||
this.hasMetadataChanged = true;
|
||||
}
|
||||
|
||||
revertChanges() {
|
||||
@@ -299,16 +280,11 @@ export class ContentMetadataComponent implements OnChanges, OnInit, OnDestroy {
|
||||
return properties.length > 0;
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.onDestroy$.next(true);
|
||||
this.onDestroy$.complete();
|
||||
}
|
||||
|
||||
public canExpandTheCard(group: CardViewGroup): boolean {
|
||||
canExpandTheCard(group: CardViewGroup): boolean {
|
||||
return group.title === this.displayAspect;
|
||||
}
|
||||
|
||||
public canExpandProperties(): boolean {
|
||||
canExpandProperties(): boolean {
|
||||
return !this.expanded || this.displayAspect === 'Properties';
|
||||
}
|
||||
|
||||
@@ -318,10 +294,106 @@ export class ContentMetadataComponent implements OnChanges, OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
private updateNode() {
|
||||
forkJoin({
|
||||
updatedNode: this.nodesApiService.updateNode(this.node.id, this.changedProperties),
|
||||
...(this.displayTags ? this.saveTags() : {}),
|
||||
...(this.displayCategories ? this.saveCategories() : {})
|
||||
}).pipe(
|
||||
catchError((err) => {
|
||||
this.cardViewContentUpdateService.updateElement(this.targetProperty);
|
||||
this.handleUpdateError(err);
|
||||
this._saving = false;
|
||||
return of(null);
|
||||
}))
|
||||
.subscribe((result) => {
|
||||
if (result) {
|
||||
if (this.hasContentTypeChanged(this.changedProperties)) {
|
||||
this.cardViewContentUpdateService.updateNodeAspect(this.node);
|
||||
}
|
||||
this.revertChanges();
|
||||
Object.assign(this.node, result.updatedNode);
|
||||
this.nodesApiService.nodeUpdated.next(this.node);
|
||||
if (Object.keys(result).length > 1 && this.displayTags) {
|
||||
this.loadTagsForNode(this.node.id);
|
||||
}
|
||||
if (this.displayCategories && !!result.LinkingCategories) {
|
||||
this.assignedCategories = !!result.LinkingCategories.list ?
|
||||
result.LinkingCategories.list.entries.map((entry: CategoryEntry) => entry.entry) :
|
||||
[result.LinkingCategories.entry];
|
||||
}
|
||||
}
|
||||
this._saving = false;
|
||||
});
|
||||
}
|
||||
|
||||
private hasContentTypeChanged(changedProperties): boolean {
|
||||
return !!changedProperties?.nodeType;
|
||||
}
|
||||
|
||||
private loadProperties(node: Node) {
|
||||
if (node) {
|
||||
this.basicProperties$ = this.getProperties(node);
|
||||
this.groupedProperties$ = this.contentMetadataService.getGroupedProperties(node, this.preset);
|
||||
if (this.displayTags) {
|
||||
this.loadTagsForNode(node.id);
|
||||
}
|
||||
if (this.displayCategories) {
|
||||
this.loadCategoriesForNode(node.id);
|
||||
if (!this.node.aspectNames.includes('generalclassifiable')) {
|
||||
this.categories = [];
|
||||
this.classifiableChangedSubject.next();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private getProperties(node: Node) {
|
||||
const properties$ = this.contentMetadataService.getBasicProperties(node);
|
||||
const contentTypeProperty$ = this.contentMetadataService.getContentTypeProperty(node);
|
||||
return zip(properties$, contentTypeProperty$)
|
||||
.pipe(map(([properties, contentTypeProperty]) => {
|
||||
const filteredProperties = contentTypeProperty.filter((property) => properties.findIndex((baseProperty) => baseProperty.key === property.key) === -1);
|
||||
return [...properties, ...filteredProperties];
|
||||
}));
|
||||
}
|
||||
|
||||
private isEmpty(value: any): boolean {
|
||||
return value === undefined || value === null || value === '';
|
||||
}
|
||||
|
||||
private loadCategoriesForNode(nodeId: string) {
|
||||
this.assignedCategories = [];
|
||||
this.categoryService.getCategoryLinksForNode(nodeId).subscribe((categoryPaging) => {
|
||||
this.categories = categoryPaging.list.entries.map((categoryEntry) => {
|
||||
const path = categoryEntry.entry.path ? categoryEntry.entry.path.split('/').splice(3).join('/') : null;
|
||||
categoryEntry.entry.name = path ? `${path}/${categoryEntry.entry.name}` : categoryEntry.entry.name;
|
||||
return categoryEntry.entry;
|
||||
});
|
||||
this.assignedCategories = [...this.categories];
|
||||
});
|
||||
}
|
||||
|
||||
private saveCategories(): { [key: string]: Observable<CategoryPaging | CategoryEntry | void> } {
|
||||
const observables: { [key: string]: Observable<CategoryPaging | CategoryEntry | void> } = {};
|
||||
if (this.categories) {
|
||||
this.assignedCategories.forEach((assignedCategory) => {
|
||||
if (this.categories.every((category) => category.name !== assignedCategory.name)) {
|
||||
observables[`Removing ${assignedCategory.id}`] = this.categoryService.unlinkNodeFromCategory(this.node.id, assignedCategory.id);
|
||||
}
|
||||
});
|
||||
const categoryLinkBodies = this.categories.map((category) => {
|
||||
const categoryLinkBody = new CategoryLinkBody();
|
||||
categoryLinkBody.categoryId = category.id;
|
||||
return categoryLinkBody;
|
||||
});
|
||||
if (categoryLinkBodies.length > 0) {
|
||||
observables['LinkingCategories'] = this.categoryService.linkNodeToCategory(this.node.id, categoryLinkBodies);
|
||||
}
|
||||
}
|
||||
return observables;
|
||||
}
|
||||
|
||||
private loadTagsForNode(id: string) {
|
||||
this.tagService.getTagsByNodeId(id).subscribe((tagPaging) => {
|
||||
this.assignedTagsEntries = tagPaging.list.entries;
|
||||
|
@@ -23,6 +23,7 @@ import { CoreModule } from '@alfresco/adf-core';
|
||||
import { ContentMetadataComponent } from './components/content-metadata/content-metadata.component';
|
||||
import { ContentMetadataCardComponent } from './components/content-metadata-card/content-metadata-card.component';
|
||||
import { TagModule } from '../tag/tag.module';
|
||||
import { CategoriesModule } from '../category/category.module';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
@@ -30,7 +31,8 @@ import { TagModule } from '../tag/tag.module';
|
||||
MaterialModule,
|
||||
FlexLayoutModule,
|
||||
CoreModule,
|
||||
TagModule
|
||||
TagModule,
|
||||
CategoriesModule
|
||||
],
|
||||
exports: [
|
||||
ContentMetadataComponent,
|
||||
|
@@ -50,6 +50,7 @@ import { TreeModule } from './tree/tree.module';
|
||||
import { AlfrescoViewerModule } from './viewer/alfresco-viewer.module';
|
||||
import { ContentUserInfoModule } from './content-user-info/content-user-info.module';
|
||||
import { SecurityControlsServiceModule } from './security/services/security-controls-service.module';
|
||||
import { CategoriesModule } from './category/category.module';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
@@ -84,7 +85,8 @@ import { SecurityControlsServiceModule } from './security/services/security-cont
|
||||
TreeModule,
|
||||
SearchTextModule,
|
||||
AlfrescoViewerModule,
|
||||
SecurityControlsServiceModule
|
||||
SecurityControlsServiceModule,
|
||||
CategoriesModule
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
@@ -123,7 +125,8 @@ import { SecurityControlsServiceModule } from './security/services/security-cont
|
||||
TreeModule,
|
||||
SearchTextModule,
|
||||
AlfrescoViewerModule,
|
||||
SecurityControlsServiceModule
|
||||
SecurityControlsServiceModule,
|
||||
CategoriesModule
|
||||
]
|
||||
})
|
||||
export class ContentModule {
|
||||
|
@@ -172,7 +172,9 @@
|
||||
"ASSIGN_CATEGORIES": "Assign Categories",
|
||||
"UNASSIGN_CATEGORY": "Unassign Category",
|
||||
"DELETE_CATEGORY": "Delete Category",
|
||||
"EXISTING_CATEGORIES": "Existing Categories",
|
||||
"EXISTING_CATEGORIES": "Existing Categories:",
|
||||
"SELECT_EXISTING_CATEGORY": "Select a existing Category:",
|
||||
"NO_EXISTING_CATEGORIES": "No existing Categories",
|
||||
"GENERIC_CREATE": "Create: {{name}}",
|
||||
"NAME": "Category name",
|
||||
"LOADING": "Loading",
|
||||
@@ -182,7 +184,7 @@
|
||||
"REQUIRED": "Category name is required",
|
||||
"EMPTY_CATEGORY": "Category name can't contain only spaces",
|
||||
"ALREADY_EXISTS": "Category already exists",
|
||||
"ALREADY_ADDED_CATEGORY": "Category is already added",
|
||||
"DUPLICATED_CATEGORY": "Category is already added",
|
||||
"CREATE_CATEGORIES": "Error while creating categories",
|
||||
"EXISTING_CATEGORIES": "Some categories already exist"
|
||||
}
|
||||
|
@@ -26,7 +26,7 @@ import { catchError, concatMap, takeUntil } from 'rxjs/operators';
|
||||
import { GenericBucket, GenericFacetResponse, ResultSetContext, ResultSetPaging } from '@alfresco/js-api';
|
||||
import { SearchFilterList } from '../models/search-filter-list.model';
|
||||
import { FacetFieldBucket } from '../models/facet-field-bucket.interface';
|
||||
import { CategoryService } from '../../category';
|
||||
import { CategoryService } from '../../category/services/category.service';
|
||||
|
||||
export interface SelectedBucket {
|
||||
field: FacetField;
|
||||
|
@@ -6,7 +6,7 @@
|
||||
</p>
|
||||
<div
|
||||
class="adf-tags-list"
|
||||
[class.adf-tags-list-with-scrollbar]="tagsListScrollbarVisible"
|
||||
[class.adf-tags-list-fixed]="!tagNameControlVisible"
|
||||
#tagsList>
|
||||
<p
|
||||
*ngFor="let tag of tags"
|
||||
@@ -17,7 +17,7 @@
|
||||
mat-icon-button
|
||||
(click)="removeTag(tag)"
|
||||
[attr.title]="'TAG.TAGS_CREATOR.TOOLTIPS.DELETE_TAG' | translate"
|
||||
[hidden]="disabledTagsRemoving">
|
||||
[disabled]="disabledTagsRemoving">
|
||||
<mat-icon>remove</mat-icon>
|
||||
</button>
|
||||
</p>
|
||||
@@ -67,7 +67,7 @@
|
||||
</ng-container>
|
||||
<div class="adf-tags-list">
|
||||
<mat-selection-list
|
||||
*ngIf="!spinnerVisible || existingTags"
|
||||
*ngIf="!spinnerVisible && existingTags"
|
||||
(selectionChange)="addExistingTagToTagsToAssign($event)"
|
||||
[disabled]="isOnlyCreateMode()">
|
||||
<mat-list-option
|
||||
|
@@ -2,10 +2,6 @@ adf-tags-creator {
|
||||
display: block;
|
||||
margin-left: -24px;
|
||||
|
||||
&.adf-creator-with-existing-tags-panel {
|
||||
background-color: var(--theme-background-color);
|
||||
}
|
||||
|
||||
.adf-label-with-icon-button {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
@@ -49,11 +45,6 @@ adf-tags-creator {
|
||||
padding-left: 10px;
|
||||
padding-right: 0;
|
||||
overflow: auto;
|
||||
|
||||
&.adf-tags-list-with-scrollbar {
|
||||
padding-right: 7px;
|
||||
margin-right: -22px;
|
||||
}
|
||||
}
|
||||
|
||||
.adf-tag {
|
||||
@@ -71,13 +62,10 @@ adf-tags-creator {
|
||||
}
|
||||
|
||||
.adf-existing-tags-panel {
|
||||
background-color: var(--theme-card-background-color);
|
||||
border-top-left-radius: 6px;
|
||||
border-top-right-radius: 6px;
|
||||
padding-top: 12px;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
z-index: 100;
|
||||
|
||||
.adf-existing-tags-label {
|
||||
font-size: 10px;
|
||||
@@ -92,7 +80,6 @@ adf-tags-creator {
|
||||
padding-right: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 75px;
|
||||
|
||||
.adf-tag {
|
||||
margin-bottom: 18px;
|
||||
|
@@ -228,14 +228,14 @@ describe('TagsCreatorComponent', () => {
|
||||
expect(tagElements).toEqual([tag2]);
|
||||
}));
|
||||
|
||||
it('should hide button for removing tag if disabledTagsRemoving is true', fakeAsync(() => {
|
||||
it('should disable button for removing tag if disabledTagsRemoving is true', fakeAsync(() => {
|
||||
const tag1 = 'Tag 1';
|
||||
component.disabledTagsRemoving = true;
|
||||
|
||||
addTagToAddedList(tag1);
|
||||
tick();
|
||||
|
||||
expect(getRemoveTagButtons()[0].hasAttribute('hidden')).toBeTrue();
|
||||
expect(getRemoveTagButtons()[0].hasAttribute('disabled')).toBeTrue();
|
||||
}));
|
||||
|
||||
it('should show button for removing tag if disabledTagsRemoving is false', fakeAsync(() => {
|
||||
@@ -371,17 +371,11 @@ describe('TagsCreatorComponent', () => {
|
||||
expect(getPanel()).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should not be visible when input is visible and empty string is typed in input', fakeAsync(() => {
|
||||
typeTag(' ');
|
||||
|
||||
expect(getPanel()).toBeFalsy();
|
||||
}));
|
||||
|
||||
it('should not be visible when input is visible and nothing has been typed', () => {
|
||||
it('should be visible when input is visible and nothing has been typed to reserve required space', () => {
|
||||
component.tagNameControlVisible = true;
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(getPanel()).toBeFalsy();
|
||||
expect(getPanel()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should not be visible when something has been typed and input has been hidden', fakeAsync(() => {
|
||||
@@ -416,12 +410,12 @@ describe('TagsCreatorComponent', () => {
|
||||
it('should not be visible if typed only spaces', fakeAsync(() => {
|
||||
typeTag(' ');
|
||||
|
||||
expect(getCreateTagLabel()).toBeFalsy();
|
||||
expect(getCreateTagLabel().hidden).toBeTrue();
|
||||
}));
|
||||
|
||||
it('should not be visible if required error occurs', fakeAsync(() => {
|
||||
typeTag('');
|
||||
expect(getCreateTagLabel()).toBeFalsy();
|
||||
expect(getCreateTagLabel().hidden).toBeTrue();
|
||||
}));
|
||||
|
||||
it('should not be visible when trying to duplicate already added tag', fakeAsync(() => {
|
||||
@@ -445,12 +439,6 @@ describe('TagsCreatorComponent', () => {
|
||||
expect(getCreateTagLabel().hasAttribute('hidden')).toBeTruthy();
|
||||
}));
|
||||
|
||||
it('should not be visible if typed nothing', () => {
|
||||
component.tagNameControlVisible = true;
|
||||
fixture.detectChanges();
|
||||
expect(getCreateTagLabel()).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should not be visible during typing', fakeAsync(() => {
|
||||
typeTag('some tag', 0);
|
||||
expect(getCreateTagLabel()).toBeFalsy();
|
||||
|
@@ -80,7 +80,6 @@ export class TagsCreatorComponent implements OnInit, OnDestroy {
|
||||
@Input()
|
||||
set tags(tags: string[]) {
|
||||
this._tags = [...tags];
|
||||
this._spinnerVisible = true;
|
||||
this._initialExistingTags = null;
|
||||
this._existingTags = null;
|
||||
this.loadTags(this.tagNameControl.value);
|
||||
@@ -100,7 +99,7 @@ export class TagsCreatorComponent implements OnInit, OnDestroy {
|
||||
set tagNameControlVisible(tagNameControlVisible: boolean) {
|
||||
this._tagNameControlVisible = tagNameControlVisible;
|
||||
if (tagNameControlVisible) {
|
||||
this._existingTagsPanelVisible = !!this.tagNameControl.value.trim();
|
||||
this._existingTagsPanelVisible = true;
|
||||
setTimeout(() => {
|
||||
this.tagNameInputElement.nativeElement.scrollIntoView();
|
||||
});
|
||||
@@ -182,8 +181,6 @@ export class TagsCreatorComponent implements OnInit, OnDestroy {
|
||||
if (name) {
|
||||
this._spinnerVisible = true;
|
||||
this._existingTagsPanelVisible = true;
|
||||
} else {
|
||||
this._existingTagsPanelVisible = false;
|
||||
}
|
||||
this.existingTagsPanelVisibilityChange.emit(this.existingTagsPanelVisible);
|
||||
this.cancelExistingTagsLoading$.next();
|
||||
@@ -339,6 +336,7 @@ export class TagsCreatorComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
} else {
|
||||
this.existingExactTag = null;
|
||||
this._spinnerVisible = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user