[ACS-5645] Property Panel Feature (#8995)

* [ACS-5645]Added edit functionality for each panel and updated test cases

* metadata e2e fix

* [ACS-5725]fixed failing e2es

* added unit test cases for new functionality

* minor fixes

* minor fixes

* minor fixes

* [ACS-5645]code modification

* [ACS-5645]removed unwanted code

* [ACS-5645]modified the changes

* [ACS-5645]removed unwanted space

* [ACS-5645]removed unwanted code

* [ACS-5645]Implemented changes as per the review comments

* linting fixes

* [ACS-5645]minor fixes

* [ACS-5645] removed unwanted code

* [ACS-5645]modified the change

* [ACS-5645]aligned input

* [ACS-5645]modified changes

* [ACS-5645]Implemented the changes as per the review comments

* [ACS-5645]linting fixes

* [ACS-5645]fixed sonarcloud issue

* [ACS-5645]fixed errors

* [ACS-5645]rename the function

* [ACS-5645]fixes linting

* [ACS-5540]lint fixes

* [ACS-5645]Implemented the changes as per review comments

* [ACS-5645] Removed unused code

* [ACS-5645]linting fixes

* [ACS-5645]fixes for lint

* [ACS-5645] e2e fixes

* [ACS-5645]Added translation

* [ACS-5645]fixes for e2e

* [ACS-5645]fixes for e2e

* [ACS-5645]e2e fixes

* [ACS-5645] Renamed the theme

* [ACS-5645]modified changes

* [ACS-5645] fixed lock-file bug

* [ACS-5645] added tooltips for save and cancel icons

* [ACS-5645] Modified the changes

* [ACS-5645]Modified the changes

* [ACS-5645] Implemented the changes as per the review comments

* [ACS-5645] Implemented the changes as per the review comments

* [ACS-5645]Modified the changes

* [ACS-5645] added group panel lock changes

* [ACS-5645] Resolved sonarcloud issue

* [ACS-5645] added test cases for tags component

* [ACS-5645] updated the documentation

* [ACS-5645] updated the documentation

* [ACS-5645] updated the documentation

* [ACS-5645] Implemented changes as per review comments

* [ACS-5645] lint fixes

* [ACS-5645] Implemented the review comments

* [ACS-5645] added focus

* [ACS-5645] modified the changes

* [ACS-5645] Lint fixes

* [ACS-5645] Lint fixes

* [ACS-5645] Lint fixes

* [ACS-5645] Removed unwanted code

* [ACS-5645] fixed sonarcloud issue

* [ACS-5645] Added missing translation key

* [ACS-5645] renamed the methods

* [ACS-5645]Added edit functionality for each panel and updated test cases

* [ACS-5645]code modification

* [ACS-5645]removed unwanted code

* [ACS-5645]Implemented changes as per the review comments

* [ACS-5645]Implemented the changes as per review comments

* [ACS-5645]linting fixes

* [ACS-5645] fixed lock-file bug

* [ACS-5645] Modified the changes

* [ACS-5645] added group panel lock changes

* [ACS-5645]Added edit functionality for each panel and updated test cases

* minor fixes

* [ACS-5645] Modified the changes

* [ACS-5645] added group panel lock changes

* [ACS-5645]Added edit functionality for each panel and updated test cases

* metadata e2e fix

* [ACS-5725]fixed failing e2es

* minor fixes

* [ACS-5645]removed unwanted code

* [ACS-5645]Implemented changes as per the review comments

* [ACS-5551] property panel design

* [ACS-5551] minor changes

* [ACS-5551]minor change

* [ACS-5551] updated checks for non -editable field

* [ACS-5551] modified the changes

* [ACS-5551] modified changes

* [ACS-5551] content-metadata updated

* [ACS-5551] code updated

* [ACS-5551] remove extra space

* fixed scrollbar issue

* [ACS-5551] margin adjusted

* Fixed  ACS-6110

* [ACS-5551] design updated

* [ACCS-5551] unit test added

* [ACS-5551] margin issue fixed

* scroll issue fixed

* [ACS-5551] color updated

* [ACS-5551] design modify

* [ACS-5551] add missing methods

* [ACS-5654] translation added

* [ACS-5645] style updated

* [ACS-5654] hide toggle button for aspects

* [ACS-5645] theme updated

* [ACS-5645] tags and category tyle update

* [ACS-5645] unit test update

* [ACS-5645] code updated as per comments

* [ACS-5645] linting issue fix

* [ACS-5645] fixed the failed unit test cases

* [ACS-5645] e2e fixes

* [ACS-5645] e2e modify

* [ACS-5645] aspect issue resolved

* [ACS-5645] Address the comments

* [ACS-5645] Address the comments

* [ACS-5645] tags list design modify

* [ACS-5645] design modify for chips

* [ACS-5645] Removed unused property

* [ACS-5645] Stop reload on panel cancel changes

* [ACS-5645] Linting issue fixed

* revert file change

* [ACS-5645] update aspect issue fix

* Revert "[ACS-5645] update aspect issue fix"

This reverts commit 5212112f2293ad4c29afdd7c7faaf897cd3d00f6.

* reduce layout duplicates, header panel component

* code improvements

* remove useless logging

* cleanup css, remove mat-divider, fix tests

* remove useless styles

* cleanup e2e

* cleanup useless events

* rename nodeIcon to just icon

* disable transition animation for tabs

* remove "editable" hacks

* improved naming for state properties

* bug fixes for process cloud

* css stylelint fixes

* rework component, cleanup useless code

* fix allowable operations and readonly state

* wait for button

* cleanup css, disable e2e

* remove demo-shell only content, fix metadata

* restore reset date functionality

* fix incorrect styling

* fix clear date button styles

* cleanup text item styles

* remove useless classes

* text item rework, code cleanup

* style bug fixes

* cleanup useless tests

* fix styles and tests

* bug fixes for select item styles, revert PR changes

* rework categories styles

* rework tags creator styles

* rollback divider module

* fix css variable naming

* fix issue with hidden properties

* fix key value pairs layout and styles

* fix tag creator validation

* remove incorrect styles, raise proper errors

* fix unit tests

* fix theme vars naming

* remove css hacks for date items

* fix error borders

* fix css bugs

* reduce code

* cleanup e2e and en.json

* fix css linting

* cleanup unused template refs

* remove useless div for metadata container

* cleanup expanders api

* cleanup and remove useless tests

* cleanup i18n

* cleanup tests

* cleanup css

* cleanup css

* [ACS-5654] added the missing theme variables

* review comments resolved

* fixed  css issue

* [ACS-5654] removesd extra div

* [ACS-5654] save and cancel button bug fix

* [ACS-5654] unit test fix for expand the panel

* [ACS-5645] design issues fix

* [ACS-5654] cards design fixed

* [ACS-5654] node icon added to thumbnail service

* [ACS-5645] linting issue fixed

* [ACS-5645] thumbnail unit test updated

* [ACS-5645] linting updated

* [ACS-5645] removed extra div

* [ACS-5645] important removed

* [ACS-5645] tags text issue fix

* [ACS-5645] add missed class

* [ACS-5645] removed unused classes

* [ACS-5645]  removed unused code

* revert flags to original state

* fix missing semicolon

* fix linting issues

* reduce code duplication

* code cleanup

* [ACS-5645] unit test fix

* [ACS-5645] e2e fix for edit button

* fix linting issue for e2e

* Replaced getNodeIcon from thumbnail to content service

* fix indentation

* refactor css variable

* use rgba color value

---------

Co-authored-by: Yasa-Nataliya <yasa.nataliya@globallogic.com>
Co-authored-by: pkundu <priyanka.kundu@hyland.com>
Co-authored-by: rbahirsheth <raviraj.bahirsheth@globallogic.com>
Co-authored-by: Denys Vuika <denys.vuika@gmail.com>
This commit is contained in:
Anukriti Singh
2023-12-21 16:37:13 +05:30
committed by GitHub
parent a7d18cbfe5
commit a900dd2551
74 changed files with 1814 additions and 1652 deletions

View File

@@ -1,10 +1,16 @@
<div class="adf-categories-management">
<p *ngIf="!categories.length && !categoryNameControlVisible"
class="adf-no-categories-message">
{{ noCategoriesMsg | translate }}
</p>
<div class="adf-categories-list"
[class.adf-categories-list-fixed]="!categoryNameControlVisible">
<div *ngIf="categoryNameControlVisible" class="adf-category-name-field">
<input #categoryNameInput
matInput
autocomplete="off"
[formControl]="categoryNameControl"
(keyup.enter)="addCategory()"
placeholder="{{'CATEGORIES_MANAGEMENT.CATEGORIES_SEARCH_PLACEHOLDER' | translate }}"
adf-auto-focus
/>
<mat-error *ngIf="categoryNameControl.invalid">{{ categoryNameErrorMessageKey | translate }}</mat-error>
</div>
<div class="adf-categories-list" [class.adf-categories-list-fixed]="!categoryNameControlVisible">
<span
*ngFor="let category of categories"
[class.adf-categories-padded]="!isCRUDMode"
@@ -13,7 +19,6 @@
<button
data-automation-id="categories-remove-category-button"
mat-icon-button
[class.adf-btn-padded]="!isCRUDMode"
(click)="removeCategory(category)"
[attr.title]="removeCategoryTitle | translate"
[disabled]="disableRemoval">
@@ -21,33 +26,9 @@
</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>
<p *ngIf="showEmptyCategoryMessage" class="adf-no-categories-message">
{{ noCategoriesMsg | translate }}
</p>
</div>
<div class="adf-existing-categories-panel" *ngIf="existingCategoriesPanelVisible">
<ng-container *ngIf="isCRUDMode && (!existingCategoriesLoading || existingCategories)">

View File

@@ -1,15 +1,17 @@
.adf-categories-management {
padding-top: 12px;
.adf-category-name-field {
display: flex;
justify-content: space-between;
width: 100%;
color: var(--adf-metadata-property-panel-text-color);
background: var(--adf-metadata-buttons-background-color);
height: 32px;
border-radius: 12px;
align-items: center;
mat-form-field {
width: 100%;
}
.adf-btn-padded {
margin-right: -14px;
input {
padding: 7px 8px;
}
}
@@ -18,10 +20,6 @@
justify-content: space-between;
align-items: center;
word-break: break-word;
.adf-btn-padded {
margin-right: -14px;
}
}
.adf-categories-padded {
@@ -31,11 +29,14 @@
[hidden] {
visibility: hidden;
}
.adf-no-categories-message {
margin-bottom: 0;
height: 30px;
}
}
.adf-categories-list {
padding-bottom: 10px;
.mat-list-base .mat-list-item,
.mat-list-base .mat-list-option {
display: flex;

View File

@@ -223,44 +223,19 @@ describe('CategoriesManagementComponent', () => {
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');
describe('showEmptyCategoryMessage', () => {
it('should return true when categories empty and category in non editable state', () => {
component.categories = [];
component.categoryNameControlVisible = false;
expect(component.showEmptyCategoryMessage).toBeTrue();
});
it('should hide and clear category control and existing categories panel on clicking hide button', fakeAsync(() => {
typeCategory('test');
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);
component.categoryNameControlVisible = true;
fixture.detectChanges();
tick(100);
expect(getCategoryControlInput().value).toBe('');
}));
});
describe('Spinner', () => {
@@ -472,13 +447,8 @@ describe('CategoriesManagementComponent', () => {
expect(categoriesChangeSpy).toHaveBeenCalledOnceWith(component.categories);
}));
it('should clear and hide input after category is created', fakeAsync(() => {
const controlVisibilityChangeSpy = spyOn(component.categoryNameControlVisibleChange, 'emit');
it('should clear input after category is created', fakeAsync(() => {
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();

View File

@@ -181,6 +181,13 @@ export class CategoriesManagementComponent implements OnInit, OnDestroy {
return this._categoryNameControl;
}
/*
* Returns `true` if categories empty and category panel non editable state, otherwise `false`
*/
get showEmptyCategoryMessage(): boolean {
return this.categories.length === 0 && !this.categoryNameControlVisible;
}
get existingCategories(): Category[] {
return this._existingCategories;
}
@@ -205,16 +212,6 @@ export class CategoriesManagementComponent implements OnInit, OnDestroy {
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;
this.clearCategoryNameInput();
}
/**
* Adds category that has been typed to a categoryNameControl and hides it afterwards.
*/
@@ -223,7 +220,6 @@ export class CategoriesManagementComponent implements OnInit, OnDestroy {
const newCatName = this.categoryNameControl.value.trim();
const newCat = new Category({ id: newCatName, name: newCatName });
this.categories.push(newCat);
this.hideNameInput();
this.clearCategoryNameInput();
this._existingCategories = null;
this.categoriesChange.emit(this.categories);

View File

@@ -129,4 +129,52 @@ describe('ContentService', () => {
expect(contentService.hasPermissions(permissionNode, 'manager')).toBeTruthy();
});
});
describe('Node Icons', () => {
let node: Node;
node = {
isFolder: true,
isFile: false,
createdByUser: { id: 'admin', displayName: 'Administrator' },
modifiedAt: new Date('2017-05-24T15:08:55.640Z'),
nodeType: 'cm:content',
content: {
mimeType: 'application/rtf',
mimeTypeName: 'Rich Text Format',
sizeInBytes: 14530
},
createdAt: new Date('2017-05-24T15:08:55.640Z'),
modifiedByUser: { id: 'admin', displayName: 'Administrator' },
name: 'b_txt_file.rtf',
id: 'test node 1',
aspectNames: ['']
} as Node;
it('should resolve folder icon', () => {
expect(contentService.getNodeIcon(node)).toContain('assets/images/ft_ic_folder.svg');
});
it('should resolve link folder icon', () => {
node.nodeType = 'app:folderlink';
expect(contentService.getNodeIcon(node)).toContain('assets/images/ft_ic_folder_shortcut_link.svg');
});
it('should resolve smart folder icon', () => {
node.aspectNames = ['smf:customConfigSmartFolder'];
expect(contentService.getNodeIcon(node)).toContain('assets/images/ft_ic_smart_folder.svg');
});
it('should resolve file icon for content type', () => {
node.isFolder = false;
node.isFile = true;
expect(contentService.getNodeIcon(node)).toContain('assets/images/ft_ic_ms_word.svg');
});
it('should resolve fallback file icon for unknown node', () => {
node.isFolder = false;
node.isFile = false;
expect(contentService.getNodeIcon(node)).toContain('assets/images/ft_ic_miscellaneous');
});
});
});

View File

@@ -18,7 +18,7 @@
import { Injectable } from '@angular/core';
import { ContentApi, Node, NodeEntry } from '@alfresco/js-api';
import { Subject } from 'rxjs';
import { AlfrescoApiService, AuthenticationService } from '@alfresco/adf-core';
import { AlfrescoApiService, AuthenticationService, ThumbnailService } from '@alfresco/adf-core';
import { PermissionsEnum } from '../models/permissions.enum';
import { AllowableOperationsEnum } from '../models/allowable-operations.enum';
@@ -43,7 +43,7 @@ export class ContentService {
return this._contentApi;
}
constructor(public authService: AuthenticationService, public apiService: AlfrescoApiService) {}
constructor(public authService: AuthenticationService, public apiService: AlfrescoApiService, private thumbnailService?: ThumbnailService) {}
/**
* Gets a content URL for the given node.
@@ -145,4 +145,48 @@ export class ContentService {
return hasAllowableOperations;
}
getNodeIcon(node: Node): string {
if (node?.isFolder) {
return this.getFolderIcon(node);
}
if (node?.isFile) {
return this.thumbnailService.getMimeTypeIcon(node?.content?.mimeType);
}
return this.thumbnailService.getDefaultMimeTypeIcon();
}
private getFolderIcon(node: Node): string {
if (this.isSmartFolder(node)) {
return this.thumbnailService.getMimeTypeIcon('smartFolder');
} else if (this.isRuleFolder(node)) {
return this.thumbnailService.getMimeTypeIcon('ruleFolder');
} else if (this.isLinkFolder(node)) {
return this.thumbnailService.getMimeTypeIcon('linkFolder');
} else {
return this.thumbnailService.getMimeTypeIcon('folder');
}
}
isSmartFolder(node: Node): boolean {
if (node) {
return this.hasAspect(node, 'smf:customConfigSmartFolder') || this.hasAspect(node, 'smf:systemConfigSmartFolder');
}
return false;
}
isRuleFolder(node: Node): boolean {
if (node) {
return this.hasAspect(node, 'rule:rules');
}
return false;
}
isLinkFolder(node: Node): boolean {
return node?.nodeType === 'app:folderlink';
}
private hasAspect(node: Node, aspectName: string): boolean {
return node?.aspectNames?.includes(aspectName);
}
}

View File

@@ -5,7 +5,7 @@
[expanded]="expanded"
[node]="node"
[displayEmpty]="displayEmpty"
[editable]="editable"
[readOnly]="!editable"
[multi]="multi"
[displayAspect]="displayAspect"
[preset]="preset"
@@ -24,24 +24,6 @@
data-automation-id="meta-data-card-edit-aspect">
<mat-icon>menu</mat-icon>
</button>
<button *ngIf="!readOnly && hasAllowableOperations()"
mat-icon-button
(click)="toggleEdit()"
[attr.title]="'CORE.METADATA.ACTIONS.EDIT' | translate"
[attr.aria-label]="'CORE.METADATA.ACCESSIBILITY.EDIT' | translate"
data-automation-id="meta-data-card-toggle-edit">
<mat-icon>mode_edit</mat-icon>
</button>
</div>
<button *ngIf="displayDefaultProperties" mat-button (click)="toggleExpanded()" data-automation-id="meta-data-card-toggle-expand">
<ng-container *ngIf="!expanded">
<span data-automation-id="meta-data-card-toggle-expand-label">{{ 'ADF_VIEWER.SIDEBAR.METADATA.MORE_INFORMATION' | translate }}</span>
<mat-icon>keyboard_arrow_down</mat-icon>
</ng-container>
<ng-container *ngIf="expanded">
<span data-automation-id="meta-data-card-toggle-expand-label">{{ 'ADF_VIEWER.SIDEBAR.METADATA.LESS_INFORMATION' | translate }}</span>
<mat-icon>keyboard_arrow_up</mat-icon>
</ng-container>
</button>
</mat-card-footer>
</mat-card>

View File

@@ -114,14 +114,6 @@ describe('ContentMetadataCardComponent', () => {
expect(contentMetadataComponent.displayEmpty).toBe(true);
});
it('should pass through the editable to the underlying component', () => {
component.editable = true;
fixture.detectChanges();
const contentMetadataComponent = fixture.debugElement.query(By.directive(ContentMetadataComponent)).componentInstance;
expect(contentMetadataComponent.editable).toBe(true);
});
it('should pass through the multi to the underlying component', () => {
component.multi = true;
fixture.detectChanges();
@@ -147,55 +139,6 @@ describe('ContentMetadataCardComponent', () => {
expect(contentMetadataComponent).toBeNull();
});
it('should toggle editable by clicking on the button', () => {
component.editable = true;
component.node.allowableOperations = [AllowableOperationsEnum.UPDATE];
fixture.detectChanges();
getToggleEditButton().triggerEventHandler('click', {});
fixture.detectChanges();
expect(component.editable).toBe(false);
});
it('should emit editableChange by clicking on toggle edit button', () => {
component.node.allowableOperations = [AllowableOperationsEnum.UPDATE];
fixture.detectChanges();
spyOn(component.editableChange, 'emit');
getToggleEditButton().nativeElement.click();
expect(component.editableChange.emit).toHaveBeenCalledWith(true);
});
it('should toggle expanded by clicking on the button', () => {
component.expanded = true;
fixture.detectChanges();
const button = fixture.debugElement.query(By.css('[data-automation-id="meta-data-card-toggle-expand"]'));
button.triggerEventHandler('click', {});
fixture.detectChanges();
expect(component.expanded).toBe(false);
});
it('should have the proper text on button while collapsed', () => {
component.expanded = false;
fixture.detectChanges();
const buttonLabel = fixture.debugElement.query(By.css('[data-automation-id="meta-data-card-toggle-expand-label"]'));
expect(buttonLabel.nativeElement.innerText.trim()).toBe('ADF_VIEWER.SIDEBAR.METADATA.MORE_INFORMATION');
});
it('should have the proper text on button while collapsed', () => {
component.expanded = true;
fixture.detectChanges();
const buttonLabel = fixture.debugElement.query(By.css('[data-automation-id="meta-data-card-toggle-expand-label"]'));
expect(buttonLabel.nativeElement.innerText.trim()).toBe('ADF_VIEWER.SIDEBAR.METADATA.LESS_INFORMATION');
});
it('should hide the edit button in readOnly is true', () => {
component.readOnly = true;
fixture.detectChanges();
@@ -211,14 +154,6 @@ describe('ContentMetadataCardComponent', () => {
expect(getToggleEditButton()).toBeNull();
});
it('should show the edit button if node does has `update` permissions', () => {
component.readOnly = false;
component.node.allowableOperations = [AllowableOperationsEnum.UPDATE];
fixture.detectChanges();
expect(getToggleEditButton()).not.toBeNull();
});
it('should expand the card when custom display aspect is valid', () => {
expect(component.expanded).toBeFalsy();

View File

@@ -15,7 +15,7 @@
* limitations under the License.
*/
import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges, ViewEncapsulation } from '@angular/core';
import { Component, Input, OnChanges, SimpleChanges, ViewEncapsulation } from '@angular/core';
import { Node } from '@alfresco/js-api';
import { NodeAspectService } from '../../../aspect-list/services/node-aspect.service';
import { ContentMetadataCustomPanel, PresetConfig } from '../../interfaces/content-metadata.interfaces';
@@ -84,10 +84,6 @@ export class ContentMetadataCardComponent implements OnChanges {
@Input()
customPanels: ContentMetadataCustomPanel[];
/** Emitted when content's editable state is changed. */
@Output()
editableChange = new EventEmitter<boolean>();
private _displayDefaultProperties: boolean = true;
/**
@@ -125,15 +121,6 @@ export class ContentMetadataCardComponent implements OnChanges {
this.expanded = !this._displayDefaultProperties;
}
toggleEdit(): void {
this.editable = !this.editable;
this.editableChange.emit(this.editable);
}
toggleExpanded(): void {
this.expanded = !this.expanded;
}
hasAllowableOperations() {
return this.contentService.hasAllowableOperations(this.node, AllowableOperationsEnum.UPDATE);
}

View File

@@ -0,0 +1,55 @@
/*!
* @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 { CommonModule } from '@angular/common';
import { Component, Input, ViewEncapsulation } from '@angular/core';
import { MatExpansionModule } from '@angular/material/expansion';
import { MatIconModule } from '@angular/material/icon';
import { TranslateModule } from '@ngx-translate/core';
@Component({
standalone: true,
imports: [CommonModule, MatIconModule, MatExpansionModule, TranslateModule],
selector: 'adf-content-metadata-header',
encapsulation: ViewEncapsulation.None,
styles: [
`
adf-content-metadata-header {
display: flex;
align-items: center;
flex: 1;
}
.adf-metadata-properties-title {
font-weight: 700;
font-size: 15px;
padding-left: 12px;
}
`
],
template: `
<ng-container>
<mat-icon>{{ expanded ? 'expand_more' : 'chevron_right' }}</mat-icon>
<mat-panel-title *ngIf="title" class="adf-metadata-properties-title">{{ title | translate }}</mat-panel-title>
<ng-content></ng-content>
</ng-container>
`
})
export class ContentMetadataHeaderComponent {
@Input() title: string = null;
@Input() expanded = true;
}

View File

@@ -1,138 +1,225 @@
<div class="adf-metadata-properties">
<mat-accordion displayMode="flat"
[multi]="multi">
<mat-expansion-panel *ngIf="displayDefaultProperties"
[expanded]="canExpandProperties()"
[attr.data-automation-id]="'adf-metadata-group-properties'">
<mat-expansion-panel-header>
<mat-panel-title class="adf-metadata-properties-title">
{{ 'CORE.METADATA.BASIC.HEADER' | translate }}
</mat-panel-title>
</mat-expansion-panel-header>
<adf-card-view
(keydown)="keyDown($event)"
[properties]="basicProperties$ | async"
[editable]="editable"
[displayEmpty]="displayEmpty"
[copyToClipboardAction]="copyToClipboardAction"
[useChipsForMultiValueProperty]="useChipsForMultiValueProperty"
[multiValueSeparator]="multiValueSeparator">
</adf-card-view>
</mat-expansion-panel>
<ng-container *ngIf="displayTags">
<mat-expansion-panel *ngIf="!editable">
<mat-expansion-panel-header>
<mat-panel-title>{{ 'METADATA.BASIC.TAGS' | translate }}</mat-panel-title>
</mat-expansion-panel-header>
<p *ngFor="let tag of tags" class="adf-metadata-properties-tag">{{ tag }}</p>
</mat-expansion-panel>
<div
*ngIf="editable"
class="adf-metadata-properties-tags">
<div class="adf-metadata-properties-tags-title">
<p>{{ 'METADATA.BASIC.TAGS' | translate }}</p>
<button
data-automation-id="showing-tag-input-button"
<mat-accordion displayMode="flat" [multi]="multi" class="adf-metadata-properties">
<mat-expansion-panel
*ngIf="displayDefaultProperties"
class="adf-content-metadata-panel"
[(expanded)]="isGeneralPanelExpanded"
[attr.data-automation-id]="'adf-metadata-group-properties'"
hideToggle>
<mat-expansion-panel-header>
<adf-content-metadata-header [title]="'CORE.METADATA.BASIC.HEADER'" [expanded]="isGeneralPanelExpanded">
<button *ngIf="canEditGeneralInfo"
mat-icon-button
[attr.title]="'METADATA.BASIC.ADD_TAG_TOOLTIP' | translate"
(click)="tagNameControlVisible = true"
[hidden]="tagNameControlVisible || saving">
<mat-icon>add</mat-icon>
(click)="onToggleGeneralInfoEdit($event)"
[attr.title]="'CORE.METADATA.ACTIONS.EDIT' | translate"
[attr.aria-label]="'CORE.METADATA.ACCESSIBILITY.EDIT' | translate"
data-automation-id="meta-data-general-info-edit"
class="adf-edit-icon-buttons">
<mat-icon>mode_edit</mat-icon>
</button>
<div *ngIf="isEditingGeneralInfo" class="adf-metadata-action-buttons">
<button mat-icon-button
[attr.title]="'CORE.METADATA.ACTIONS.CANCEL' | translate"
(click)="onCancelGeneralInfoEdit($event)"
data-automation-id="reset-metadata"
class="adf-metadata-action-buttons-clear">
<mat-icon>clear</mat-icon>
</button>
<button mat-icon-button
[attr.title]="'CORE.METADATA.ACTIONS.SAVE' | translate"
(click)="onSaveGeneralInfoChanges($event)"
color="primary"
data-automation-id="save-general-info-metadata"
[disabled]="!hasMetadataChanged">
<mat-icon>check</mat-icon>
</button>
</div>
<adf-tags-creator
[(tagNameControlVisible)]="tagNameControlVisible"
(tagsChange)="storeTagsToAssign($event)"
[mode]="tagsCreatorMode"
[tags]="assignedTags"
[disabledTagsRemoving]="saving">
</adf-tags-creator>
</adf-content-metadata-header>
</mat-expansion-panel-header>
<adf-card-view
(keydown)="keyDown($event)"
[properties]="basicProperties$ | async"
[editable]="isEditingModeGeneralInfo"
[displayEmpty]="displayEmpty"
[copyToClipboardAction]="copyToClipboardAction"
[useChipsForMultiValueProperty]="useChipsForMultiValueProperty"
[multiValueSeparator]="multiValueSeparator">
</adf-card-view>
</mat-expansion-panel>
<ng-container *ngIf="displayTags">
<mat-expansion-panel hideToggle [(expanded)]="isTagPanelExpanded" class="adf-content-metadata-panel">
<mat-expansion-panel-header>
<adf-content-metadata-header [title]="'METADATA.BASIC.TAGS'" [expanded]="isTagPanelExpanded">
<button *ngIf="canEditTags"
mat-icon-button
(click)="onToggleTagsEdit($event)"
[attr.title]="'CORE.METADATA.ACTIONS.EDIT' | translate"
[attr.aria-label]="'CORE.METADATA.ACCESSIBILITY.EDIT' | translate"
data-automation-id="showing-tag-input-button"
class="adf-edit-icon-buttons">
<mat-icon>mode_edit</mat-icon>
</button>
<div *ngIf="isEditingTags" class="adf-metadata-action-buttons">
<button mat-icon-button
[attr.title]="'CORE.METADATA.ACTIONS.CANCEL' | translate"
(click)="onCancelTagsEdit($event)"
data-automation-id="reset-tags-metadata"
class="adf-metadata-action-buttons-clear">
<mat-icon>clear</mat-icon>
</button>
<button mat-icon-button
[attr.title]="'CORE.METADATA.ACTIONS.SAVE' | translate"
(click)="onSaveTagsChanges($event)"
color="primary"
data-automation-id="save-tags-metadata"
[disabled]="!hasMetadataChanged">
<mat-icon>check</mat-icon>
</button>
</div>
</adf-content-metadata-header>
</mat-expansion-panel-header>
<div *ngIf="!isEditingModeTags" class="adf-metadata-properties-tags">
<span *ngFor="let tag of tags" class="adf-metadata-properties-tag">{{ tag }}</span>
</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>
<div *ngIf="showEmptyTagMessage" class="adf-metadata-no-item-added">
{{ 'METADATA.BASIC.NO_TAGS_ADDED' | translate }}
</div>
<adf-tags-creator
*ngIf="isEditingModeTags"
class="adf-metadata-properties-tags"
[tagNameControlVisible]="tagNameControlVisible"
(tagsChange)="storeTagsToAssign($event)"
[mode]="tagsCreatorMode"
[tags]="assignedTags"
[disabledTagsRemoving]="saving">
</adf-tags-creator>
</mat-expansion-panel>
</ng-container>
<ng-container *ngIf="displayCategories">
<mat-expansion-panel hideToggle [(expanded)]="isCategoriesPanelExpanded" class="adf-content-metadata-panel">
<mat-expansion-panel-header>
<adf-content-metadata-header [title]="'CATEGORIES_MANAGEMENT.CATEGORIES_TITLE'" [expanded]="isCategoriesPanelExpanded">
<button *ngIf="canEditCategories"
mat-icon-button
(click)="onToggleCategoriesEdit($event)"
[attr.title]="'CORE.METADATA.ACTIONS.EDIT' | translate"
[attr.aria-label]="'CORE.METADATA.ACCESSIBILITY.EDIT' | translate"
data-automation-id="meta-data-categories-edit"
class="adf-categories-button adf-edit-icon-buttons">
<mat-icon>mode_edit</mat-icon>
</button>
<div *ngIf="isEditingCategories" class="adf-metadata-action-buttons">
<button mat-icon-button
[attr.title]="'CORE.METADATA.ACTIONS.CANCEL' | translate"
(click)="onCancelCategoriesEdit($event)"
data-automation-id="reset-metadata"
class="adf-metadata-action-buttons-clear">
<mat-icon>clear</mat-icon>
</button>
<button mat-icon-button
[attr.title]="'CORE.METADATA.ACTIONS.SAVE' | translate"
(click)="onSaveCategoriesChanges($event)"
color="primary"
data-automation-id="save-categories-metadata"
[disabled]="!hasMetadataChanged">
<mat-icon>check</mat-icon>
</button>
</div>
</adf-content-metadata-header>
</mat-expansion-panel-header>
<div *ngIf="!isEditingModeCategories">
<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>
<mat-expansion-panel *ngFor="let customPanel of customPanels" [expanded]="canExpandTheCard(customPanel.panelTitle)">
<mat-expansion-panel-header>
<mat-panel-title>{{ customPanel.panelTitle | translate }}</mat-panel-title>
</mat-expansion-panel-header>
<adf-dynamic-component [id]="customPanel.component" [data]="{ node }"></adf-dynamic-component>
<div *ngIf="showEmptyCategoryMessage" class="adf-metadata-no-item-added">
{{ 'CATEGORIES_MANAGEMENT.NO_CATEGORIES_ADDED' | translate }}
</div>
<adf-categories-management
*ngIf="isEditingModeCategories"
class="adf-metadata-categories-header"
[(categoryNameControlVisible)]="categoryControlVisible"
[disableRemoval]="saving"
[categories]="categories"
[managementMode]="categoriesManagementMode"
[classifiableChanged]="classifiableChanged"
(categoriesChange)="storeCategoriesToAssign($event)">
</adf-categories-management>
</mat-expansion-panel>
<ng-container *ngIf="expanded">
<ng-container *ngIf="groupedProperties$ | async; else loading; let groupedProperties">
<div *ngFor="let group of groupedProperties; let first = first;"
class="adf-metadata-grouped-properties-container">
<mat-expansion-panel *ngIf="showGroup(group) || editable"
[attr.data-automation-id]="'adf-metadata-group-' + group.title"
[expanded]="canExpandTheCard(group.title) || !displayDefaultProperties && first">
<mat-expansion-panel-header>
<mat-panel-title>
{{ group.title | translate }}
</mat-panel-title>
</mat-expansion-panel-header>
</ng-container>
<adf-card-view
(keydown)="keyDown($event)"
[properties]="group.properties"
[editable]="editable"
[displayEmpty]="displayEmpty"
[copyToClipboardAction]="copyToClipboardAction"
[useChipsForMultiValueProperty]="useChipsForMultiValueProperty"
[multiValueSeparator]="multiValueSeparator"
[displayLabelForChips]="true">
</adf-card-view>
</mat-expansion-panel>
<mat-expansion-panel
*ngFor="let customPanel of customPanels"
[expanded]="canExpandTheCard(customPanel.panelTitle)"
(opened)="customPanel.expanded = true"
(closed)="customPanel.expanded = false"
class="adf-content-metadata-panel"
hideToggle>
<mat-expansion-panel-header>
<adf-content-metadata-header class="adf-metadata-custom-panel-title" [title]="customPanel.panelTitle" [expanded]="customPanel.expanded">
</adf-content-metadata-header>
</mat-expansion-panel-header>
<adf-dynamic-component [id]="customPanel.component" [data]="{ node }"></adf-dynamic-component>
</mat-expansion-panel>
<ng-container *ngIf="groupedProperties$ | async; else loading; let groupedProperties">
<div *ngFor="let group of groupedProperties; let first = first;"
class="adf-metadata-grouped-properties-container">
<mat-expansion-panel
[attr.data-automation-id]="'adf-metadata-group-' + group.title"
[expanded]="canExpandTheCard(group.title) || !displayDefaultProperties && first || group.expanded"
(opened)="group.expanded = true"
(closed)="group.expanded = false"
class="adf-content-metadata-panel"
hideToggle>
<mat-expansion-panel-header>
<adf-content-metadata-header [title]="group.title" [expanded]="group.expanded">
<button *ngIf="hasGroupToggleEdit(group)"
mat-icon-button
[attr.title]="'CORE.METADATA.ACTIONS.EDIT' | translate"
[attr.aria-label]="'CORE.METADATA.ACCESSIBILITY.EDIT' | translate"
data-automation-id="meta-data-card-toggle-edit"
class="adf-edit-icon-buttons"
(click)="onToggleGroupEdit(group, $event)">
<mat-icon>mode_edit</mat-icon>
</button>
<div class="adf-metadata-action-buttons" *ngIf="isGroupToggleEditing(group)">
<button mat-icon-button
[attr.title]="'CORE.METADATA.ACTIONS.CANCEL' | translate"
(click)="onCancelGroupEdit(group, $event)"
data-automation-id="reset-metadata"
class="adf-metadata-action-buttons-clear">
<mat-icon>clear</mat-icon>
</button>
<button mat-icon-button
[attr.title]="'CORE.METADATA.ACTIONS.SAVE' | translate"
(click)="onSaveGroupChanges(group, $event)"
color="primary"
data-automation-id="save-metadata"
[disabled]="!hasMetadataChanged">
<mat-icon>check</mat-icon>
</button>
</div>
</adf-content-metadata-header>
</mat-expansion-panel-header>
<div *ngIf="!showGroup(group) && !group.editable" class="adf-metadata-no-item-added">
{{ 'METADATA.BASIC.NO_ITEMS_MESSAGE' | translate: { groupTitle: group.title | translate } }}
</div>
</ng-container>
<ng-template #loading>
<mat-progress-bar mode="indeterminate" [attr.aria-label]="'DATA_LOADING' | translate">
</mat-progress-bar>
</ng-template>
</ng-container>
</mat-accordion>
<adf-card-view
(keydown)="keyDown($event)"
[properties]="group.properties"
[editable]="group.editable"
[displayEmpty]="displayEmpty"
[copyToClipboardAction]="copyToClipboardAction"
[useChipsForMultiValueProperty]="useChipsForMultiValueProperty"
[multiValueSeparator]="multiValueSeparator"
[displayLabelForChips]="true">
</adf-card-view>
</mat-expansion-panel>
</div>
</ng-container>
<div class="adf-metadata-action-buttons"
*ngIf="editable">
<button mat-button
(click)="cancelChanges()"
data-automation-id="reset-metadata"
[disabled]="!hasMetadataChanged">
{{ 'CORE.METADATA.ACTIONS.CANCEL' | translate }}
</button>
<button mat-raised-button
(click)="saveChanges()"
color="primary"
data-automation-id="save-metadata"
[disabled]="!hasMetadataChanged">
{{ 'CORE.METADATA.ACTIONS.SAVE' | translate }}
</button>
</div>
</div>
<ng-template #loading>
<mat-progress-bar mode="indeterminate" [attr.aria-label]="'DATA_LOADING' | translate">
</mat-progress-bar>
</ng-template>
</mat-accordion>

View File

@@ -1,56 +1,64 @@
$panel-properties-height: 56px !default;
.adf {
&-metadata-properties {
.mat-expansion-panel-header.mat-expanded:hover,
.mat-expansion-panel-header.mat-expanded:focus {
background: var(--adf-theme-background-hover-color);
.adf-content-metadata-panel {
box-shadow: none;
border: 1px solid var(--adf-metadata-property-panel-border-color);
border-radius: 12px;
margin: 12px;
}
mat-expansion-panel-header {
height: 64px;
height: $panel-properties-height;
padding: 0 12px;
border-radius: 12px 12px 0 0;
.adf-metadata-properties-title {
font-weight: normal;
font-size: 15px;
&.mat-expanded {
height: $panel-properties-height;
border-bottom: 1px solid var(--adf-metadata-property-panel-border-color);
}
.mat-content {
display: contents;
margin-right: 0;
}
}
.mat-expansion-panel:not([class*='mat-elevation-z']) {
box-shadow: none;
.mat-expansion-panel-content {
.mat-expansion-panel-body {
padding: 0 12px 12px;
}
}
.adf-edit-icon-buttons {
color: var(--adf-theme-foreground-text-color-054);
}
.adf-metadata-properties-tag {
height: 40px;
display: flex;
min-height: 32px;
display: inline-flex;
align-items: center;
margin-top: -14px;
margin-bottom: 1em;
border-radius: 16px;
width: fit-content;
background: var(--adf-metadata-buttons-background-color);
margin-top: 12px;
padding: 6px 12px;
justify-content: center;
margin-left: 8px;
overflow-wrap: anywhere;
}
&:first-of-type {
margin-top: 5px;
}
.adf-metadata-no-item-added {
word-break: break-all;
font-size: 15px;
padding: 16px 0 0 12px;
}
&-tags {
padding: 0 10px 0 26px;
&-title {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 15px;
height: 68px;
}
adf-tags-creator {
margin-top: 19px;
.adf-tags-creation {
padding-right: 0;
padding-left: 12px;
.adf-tag {
margin-top: -14px;
}
}
&.adf-creator-with-existing-tags-panel {
@@ -67,24 +75,19 @@
&-metadata-action-buttons {
display: flex;
justify-content: space-evenly;
margin: 10px;
&-clear {
color: var(--adf-metadata-action-button-clear-color);
}
}
&-metadata-categories-header {
display: flex;
flex-direction: column;
padding: 0 24px;
.adf-metadata-categories-title {
.adf-categories-button {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 15px;
height: 64px;
button {
margin-right: -14px;
}
height: $panel-properties-height;
[hidden] {
visibility: hidden;

View File

@@ -21,16 +21,16 @@ import { By } from '@angular/platform-browser';
import { Category, CategoryPaging, ClassesApi, Node, Tag, TagBody, TagEntry, TagPaging, TagPagingList } from '@alfresco/js-api';
import { ContentMetadataComponent } from './content-metadata.component';
import { ContentMetadataService } from '../../services/content-metadata.service';
import { AppConfigService, CardViewBaseItemModel, CardViewComponent, LogService, UpdateNotification } from '@alfresco/adf-core';
import { AppConfigService, CardViewBaseItemModel, CardViewComponent, NotificationService, UpdateNotification } from '@alfresco/adf-core';
import { NodesApiService } from '../../../common/services/nodes-api.service';
import { EMPTY, of, throwError } from 'rxjs';
import { ContentTestingModule } from '../../../testing/content.testing.module';
import { mockGroupProperties } from './mock-data';
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 {
CardViewGroup,
CategoriesManagementComponent,
CategoriesManagementMode,
CategoryService,
@@ -49,6 +49,8 @@ describe('ContentMetadataComponent', () => {
let folderNode: Node;
let tagService: TagService;
let categoryService: CategoryService;
let getClassSpy: jasmine.Spy;
let notificationService: NotificationService;
const preset = 'custom-preset';
@@ -71,26 +73,38 @@ describe('ContentMetadataComponent', () => {
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 findTagElements = (): DebugElement[] => fixture.debugElement.queryAll(By.css('.adf-metadata-properties .adf-metadata-properties-tag'));
const findCancelButton = (): HTMLButtonElement => fixture.debugElement.query(By.css('[data-automation-id=reset-metadata]')).nativeElement;
const findCancelTagsButton = (): HTMLButtonElement =>
fixture.debugElement.query(By.css('[data-automation-id=reset-tags-metadata]')).nativeElement;
const clickOnCancel = () => {
findCancelButton().click();
fixture.detectChanges();
};
const findSaveButton = (): HTMLButtonElement => fixture.debugElement.query(By.css('[data-automation-id=save-metadata]')).nativeElement;
const findSaveGeneralInfoButton = (): HTMLButtonElement =>
fixture.debugElement.query(By.css('[data-automation-id=save-general-info-metadata]')).nativeElement;
const findSaveTagsButton = (): HTMLButtonElement => fixture.debugElement.query(By.css('[data-automation-id=save-tags-metadata]')).nativeElement;
const findSaveCategoriesButton = (): HTMLButtonElement =>
fixture.debugElement.query(By.css('[data-automation-id=save-categories-metadata]')).nativeElement;
const clickOnSave = () => {
findSaveButton().click();
const clickOnGeneralInfoSave = () => {
findSaveGeneralInfoButton().click();
fixture.detectChanges();
};
const clickOnTagsSave = () => {
findSaveTagsButton().click();
fixture.detectChanges();
};
const findTagsCreator = (): TagsCreatorComponent => fixture.debugElement.query(By.directive(TagsCreatorComponent))?.componentInstance;
const findShowingTagInputButton = (): HTMLButtonElement =>
fixture.debugElement.query(By.css('[data-automation-id=showing-tag-input-button]')).nativeElement;
const getToggleEditButton = () => fixture.debugElement.query(By.css('[data-automation-id="meta-data-general-info-edit"]'));
const getTagsToggleEditButton = () => fixture.debugElement.query(By.css('[data-automation-id="showing-tag-input-button"]'));
const getCategoriesToggleEditButton = () => fixture.debugElement.query(By.css('[data-automation-id="meta-data-categories-edit"]'));
const getGroupToggleEditButton = () => fixture.debugElement.query(By.css('[data-automation-id="meta-data-card-toggle-edit"]'));
/**
* Get metadata categories
@@ -110,22 +124,12 @@ describe('ContentMetadataComponent', () => {
return fixture.debugElement.query(By.directive(CategoriesManagementComponent))?.componentInstance;
}
/**
* Get categories title button
*
* @returns native element
*/
function getAssignCategoriesBtn(): HTMLButtonElement {
return fixture.debugElement.query(By.css('.adf-metadata-categories-title button')).nativeElement;
}
/**
* Update aspect property
*
* @param newValue value to set
*/
async function updateAspectProperty(newValue: string): Promise<void> {
component.editable = true;
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));
@@ -135,8 +139,7 @@ describe('ContentMetadataComponent', () => {
fixture.detectChanges();
await fixture.whenStable();
clickOnSave();
component.onSaveGroupChanges({} as any);
await fixture.whenStable();
}
@@ -144,12 +147,6 @@ describe('ContentMetadataComponent', () => {
TestBed.configureTestingModule({
imports: [TranslateModule.forRoot(), ContentTestingModule],
providers: [
{
provide: LogService,
useValue: {
error: jasmine.createSpy('error')
}
},
{
provide: TagService,
useValue: {
@@ -175,6 +172,9 @@ describe('ContentMetadataComponent', () => {
nodesApiService = TestBed.inject(NodesApiService);
tagService = TestBed.inject(TagService);
categoryService = TestBed.inject(CategoryService);
notificationService = TestBed.inject(NotificationService);
const propertyDescriptorsService = TestBed.inject(PropertyDescriptorsService);
const classesApi = propertyDescriptorsService['classesApi'];
node = {
id: 'node-id',
@@ -197,6 +197,7 @@ describe('ContentMetadataComponent', () => {
component.node = node;
component.preset = preset;
spyOn(contentMetadataService, 'getContentTypeProperty').and.returnValue(of([]));
getClassSpy = spyOn(classesApi, 'getClass');
fixture.detectChanges();
});
@@ -206,7 +207,7 @@ describe('ContentMetadataComponent', () => {
describe('Default input param values', () => {
it('should have editable input param as false by default', () => {
expect(component.editable).toBe(false);
expect(component.isEditingModeGeneralInfo).toBe(false);
});
it('should have displayEmpty input param as false by default', () => {
@@ -272,7 +273,7 @@ describe('ContentMetadataComponent', () => {
}));
it('should call removeTag and assignTagsToNode on TagService on save click', fakeAsync(() => {
component.editable = true;
component.isEditingModeTags = true;
component.displayTags = true;
const property = { key: 'properties.property-key', value: 'original-value' } as CardViewBaseItemModel;
const expectedNode = { ...node, name: 'some-modified-value' };
@@ -284,24 +285,26 @@ describe('ContentMetadataComponent', () => {
spyOn(tagService, 'assignTagsToNode').and.returnValue(EMPTY);
const tagName1 = tagPaging.list.entries[0].entry.tag;
const tagName2 = 'New tag 3';
component.isEditingModeTags = true;
component.readOnly = false;
updateService.update(property, 'updated-value');
tick(600);
fixture.detectChanges();
findTagsCreator().tagsChange.emit([tagName1, tagName2]);
clickOnSave();
const tag1 = new TagBody();
tag1.tag = tagName1;
const tag2 = new TagBody();
tag2.tag = tagName2;
fixture.detectChanges();
tick(600);
clickOnTagsSave();
tick(100);
const tag1 = new TagBody({ tag: tagName1 });
const tag2 = new TagBody({ tag: tagName2 });
expect(tagService.removeTag).toHaveBeenCalledWith(node.id, tagPaging.list.entries[1].entry.id);
expect(tagService.assignTagsToNode).toHaveBeenCalledWith(node.id, [tag1, tag2]);
discardPeriodicTasks();
flush();
}));
it('should call getTagsByNodeId on TagService on save click', fakeAsync(() => {
component.editable = true;
it('should call getTagsByNodeId on TagService on save click', () => {
component.isEditingModeTags = true;
component.displayTags = true;
const property = { key: 'properties.property-key', value: 'original-value' } as CardViewBaseItemModel;
const expectedNode = { ...node, name: 'some-modified-value' };
@@ -313,25 +316,23 @@ describe('ContentMetadataComponent', () => {
spyOn(tagService, 'assignTagsToNode').and.returnValue(of({}));
updateService.update(property, 'updated-value');
tick(600);
fixture.detectChanges();
findTagsCreator().tagsChange.emit([tagPaging.list.entries[0].entry.tag, 'New tag 3']);
getTagsByNodeIdSpy.calls.reset();
clickOnSave();
component.onSaveTagsChanges();
expect(tagService.getTagsByNodeId).toHaveBeenCalledWith(node.id);
}));
});
it('should throw error on unsuccessful save', fakeAsync(() => {
const logService: LogService = TestBed.inject(LogService);
component.editable = true;
component.isEditingModeGeneralInfo = true;
component.readOnly = false;
const property = { key: 'properties.property-key', value: 'original-value' } as CardViewBaseItemModel;
updateService.update(property, 'updated-value');
tick(600);
const sub = contentMetadataService.error.subscribe((err) => {
expect(logService.error).toHaveBeenCalledWith(new Error('My bad'));
expect(err.statusCode).toBe(0);
expect(err.message).toBe('METADATA.ERRORS.GENERIC');
sub.unsubscribe();
@@ -340,13 +341,14 @@ describe('ContentMetadataComponent', () => {
spyOn(nodesApiService, 'updateNode').and.returnValue(throwError(new Error('My bad')));
fixture.detectChanges();
fixture.whenStable().then(() => clickOnSave());
fixture.whenStable().then(() => clickOnGeneralInfoSave());
discardPeriodicTasks();
flush();
}));
it('should open the confirm dialog when content type is changed', fakeAsync(() => {
component.editable = true;
component.isEditingModeGeneralInfo = true;
component.readOnly = false;
const property = { key: 'nodeType', value: 'ft:sbiruli' } as CardViewBaseItemModel;
const expectedNode = { ...node, nodeType: 'ft:sbiruli' };
spyOn(contentMetadataService, 'openConfirmDialog').and.returnValue(of(true));
@@ -357,17 +359,17 @@ describe('ContentMetadataComponent', () => {
fixture.detectChanges();
tick(100);
clickOnSave();
clickOnGeneralInfoSave();
tick(100);
expect(component.node).toEqual(expectedNode);
expect(contentMetadataService.openConfirmDialog).toHaveBeenCalledWith({ nodeType: 'ft:poppoli' });
expect(nodesApiService.updateNode).toHaveBeenCalled();
discardPeriodicTasks();
flush();
}));
it('should call removeTag and assignTagsToNode on TagService after confirming confirmation dialog when content type is changed', fakeAsync(() => {
component.editable = true;
component.displayTags = true;
const property = { key: 'nodeType', value: 'ft:sbiruli' } as CardViewBaseItemModel;
const expectedNode = { ...node, nodeType: 'ft:sbiruli' };
@@ -380,7 +382,8 @@ describe('ContentMetadataComponent', () => {
spyOn(tagService, 'assignTagsToNode').and.returnValue(EMPTY);
const tagName1 = tagPaging.list.entries[0].entry.tag;
const tagName2 = 'New tag 3';
component.isEditingModeTags = true;
component.readOnly = false;
updateService.update(property, 'ft:poppoli');
tick(600);
@@ -388,19 +391,22 @@ describe('ContentMetadataComponent', () => {
findTagsCreator().tagsChange.emit([tagName1, tagName2]);
tick(100);
fixture.detectChanges();
clickOnSave();
clickOnTagsSave();
tick(100);
const tag1 = new TagBody();
tag1.tag = tagName1;
const tag2 = new TagBody();
tag2.tag = tagName2;
const tag1 = new TagBody({ tag: tagName1 });
const tag2 = new TagBody({ tag: tagName2 });
expect(tagService.removeTag).toHaveBeenCalledWith(node.id, tagPaging.list.entries[1].entry.id);
expect(tagService.assignTagsToNode).toHaveBeenCalledWith(node.id, [tag1, tag2]);
discardPeriodicTasks();
flush();
}));
it('should retrigger the load of the properties when the content type has changed', fakeAsync(() => {
component.editable = true;
component.isEditingModeGeneralInfo = true;
component.readOnly = false;
const property = { key: 'nodeType', value: 'ft:sbiruli' } as CardViewBaseItemModel;
const expectedNode = Object.assign({}, node, { nodeType: 'ft:sbiruli' });
spyOn(contentMetadataService, 'openConfirmDialog').and.returnValue(of(true));
@@ -412,21 +418,227 @@ describe('ContentMetadataComponent', () => {
fixture.detectChanges();
tick(100);
clickOnSave();
clickOnGeneralInfoSave();
tick(100);
expect(component.node).toEqual(expectedNode);
expect(updateService.updateNodeAspect).toHaveBeenCalledWith(expectedNode);
discardPeriodicTasks();
flush();
}));
});
describe('toggleEdit', () => {
let showErrorSpy: jasmine.Spy;
const mockGroup: CardViewGroup = {
editable: false,
expanded: false,
title: '',
properties: []
};
beforeEach(() => {
component.currentGroup = mockGroup;
showErrorSpy = spyOn(notificationService, 'showError').and.stub();
});
it('should toggle General Info editing mode', () => {
component.isEditingModeGeneralInfo = false;
component.onToggleGeneralInfoEdit();
expect(component.isEditingModeTags).toBe(false);
expect(component.isEditingModeCategories).toBe(false);
expect(component.currentGroup.editable).toBe(false);
});
it('should toggle Tags editing mode', () => {
component.isEditingModeTags = false;
component.onToggleTagsEdit();
expect(component.isTagPanelExpanded).toBe(component.isEditingModeTags);
expect(component.tagNameControlVisible).toBe(true);
expect(component.isEditingModeCategories).toBe(false);
expect(component.currentGroup.editable).toBe(false);
});
it('should toggle Categories editing mode', () => {
component.isEditingModeCategories = false;
component.onToggleCategoriesEdit();
expect(component.isCategoriesPanelExpanded).toBe(component.isEditingModeCategories);
expect(component.categoryControlVisible).toBe(true);
expect(component.isEditingModeTags).toBe(false);
expect(component.currentGroup.editable).toBe(false);
});
it('should toggle Group editing mode', () => {
component.onToggleGroupEdit(mockGroup);
expect(component.isEditingModeGeneralInfo).toBe(false);
expect(component.currentGroup).toBe(mockGroup);
});
it('should show Snackbar when Editing Panel is Active', () => {
spyOn(component, 'isEditingPanel').and.returnValue(true);
component.onToggleGeneralInfoEdit();
expect(component.isEditingPanel).toHaveBeenCalled();
expect(showErrorSpy).toHaveBeenCalledWith('METADATA.BASIC.SAVE_OR_DISCARD_CHANGES');
});
});
describe('toggleEditMode', () => {
it('should toggle general editable', () => {
component.isEditingModeGeneralInfo = false;
component.onToggleGeneralInfoEdit();
expect(component.isEditingModeGeneralInfo).toBe(true);
});
it('should toggle tags editable', () => {
component.isEditingModeTags = false;
component.onToggleTagsEdit();
expect(component.isEditingModeTags).toBe(true);
});
it('should toggle categories editable', () => {
component.isEditingModeCategories = false;
component.onToggleCategoriesEdit();
expect(component.isEditingModeCategories).toBe(true);
});
it('should toggle group editable', () => {
const group: CardViewGroup = {
editable: false,
expanded: false,
title: '',
properties: []
};
component.currentGroup = null;
component.onToggleGroupEdit(group);
expect(group.editable).toBe(true);
});
});
describe('Permission', () => {
beforeEach(() => {
component.readOnly = false;
component.node.allowableOperations = null;
component.ngOnInit();
});
it('should hide the general info edit button if node does not have `update` permissions', async () => {
fixture.detectChanges();
await fixture.whenStable();
expect(component.readOnly).toBeTrue();
expect(getToggleEditButton()).toBeNull();
});
it('should hide the tags edit button if node does not have `update` permissions', async () => {
fixture.detectChanges();
await fixture.whenStable();
expect(component.readOnly).toBeTrue();
expect(getTagsToggleEditButton()).toBeNull();
});
it('should hide the categories edit button if node does not have `update` permissions', async () => {
fixture.detectChanges();
await fixture.whenStable();
expect(component.readOnly).toBeTrue();
expect(getCategoriesToggleEditButton()).toBeNull();
});
it('should hide the groups edit button if node does not have `update` permissions', () => {
component.readOnly = false;
component.node.allowableOperations = null;
fixture.detectChanges();
expect(getGroupToggleEditButton()).toBeNull();
});
});
describe('hasToggleEdit', () => {
it('should return true when editable is false, readOnly is false, and hasAllowableOperations is true', () => {
component.isEditingModeGeneralInfo = false;
component.readOnly = false;
expect(component.canEditGeneralInfo).toBe(true);
});
it('should return false when editable is true', () => {
component.isEditingModeGeneralInfo = true;
component.readOnly = false;
fixture.detectChanges();
expect(component.canEditGeneralInfo).toBe(false);
expect(component.isEditingGeneralInfo).toBe(true);
});
});
describe('hasTagsToggleEdit', () => {
it('should have hasTagsToggleEdit property as expected', () => {
component.isEditingModeTags = false;
component.readOnly = false;
fixture.detectChanges();
expect(component.canEditTags).toBe(true);
});
it('should return false when editable is true', () => {
component.isEditingModeTags = true;
component.readOnly = false;
fixture.detectChanges();
expect(component.canEditTags).toBe(false);
expect(component.isEditingTags).toBe(true);
});
});
describe('hasGroupToggleEdit', () => {
it('should return true when group is not editable, not read-only', () => {
component.readOnly = false;
const group: CardViewGroup = {
title: 'Group Title',
properties: [],
expanded: true,
editable: false
};
const result = component.hasGroupToggleEdit(group);
expect(result).toBe(true);
});
it('should return true when group is editable, not read-only', () => {
component.readOnly = false;
const group: CardViewGroup = {
title: 'Group Title',
properties: [],
expanded: true,
editable: true
};
const result = component.isGroupToggleEditing(group);
expect(result).toBe(true);
});
});
describe('hasCategoriesToggleEdit', () => {
it('should have hasCategoriesToggleEdit property as expected', () => {
component.isEditingModeCategories = false;
component.readOnly = false;
expect(component.canEditCategories).toBe(true);
});
it('should return false when editable is true', () => {
component.isEditingModeCategories = true;
component.readOnly = false;
fixture.detectChanges();
expect(component.canEditCategories).toBe(false);
expect(component.isEditingCategories).toBe(true);
});
});
describe('Reseting', () => {
it('should reset properties on reset click', async () => {
component.changedProperties = { properties: { 'property-key': 'updated-value' } };
component.hasMetadataChanged = true;
component.tagNameControlVisible = true;
component.categoryControlVisible = true;
component.editable = true;
component.isEditingModeGeneralInfo = true;
component.readOnly = false;
const expectedNode = Object.assign({}, node, { name: 'some-modified-value' });
spyOn(nodesApiService, 'updateNode').and.returnValue(of(expectedNode));
@@ -562,9 +774,7 @@ describe('ContentMetadataComponent', () => {
});
it('should hide card views group when the grouped properties are empty', async () => {
component.expanded = true;
spyOn(contentMetadataService, 'getGroupedProperties').and.returnValue(of([{ properties: [] } as any]));
spyOn(contentMetadataService, 'getGroupedProperties').and.stub();
component.ngOnChanges({ node: new SimpleChange(node, expectedNode, false) });
@@ -577,21 +787,7 @@ describe('ContentMetadataComponent', () => {
it('should display card views group when there is at least one property that is not empty', async () => {
component.expanded = true;
const cardViewGroup = {
title: 'Group 1',
properties: [
{
data: null,
default: null,
displayValue: 'DefaultName',
icon: '',
key: 'properties.cm:default',
label: 'To'
}
]
};
spyOn(contentMetadataService, 'getGroupedProperties').and.returnValue(of([{ properties: [cardViewGroup] } as any]));
spyOn(contentMetadataService, 'getGroupedProperties').and.stub();
component.ngOnChanges({ node: new SimpleChange(node, expectedNode, false) });
@@ -601,6 +797,33 @@ describe('ContentMetadataComponent', () => {
const basicPropertiesGroup = fixture.debugElement.query(By.css('.adf-metadata-grouped-properties-container mat-expansion-panel'));
expect(basicPropertiesGroup).toBeDefined();
});
it('should revert changes for general info panel on cancel', () => {
const spy = spyOn(contentMetadataService, 'getBasicProperties');
component.onCancelGeneralInfoEdit();
expect(spy).toHaveBeenCalled();
});
it('should revert changes for getGroupedProperties panel on cancel', () => {
spyOn(contentMetadataService, 'getGroupedProperties');
component.ngOnChanges({ node: new SimpleChange(node, expectedNode, false) });
component.onCancelGroupEdit({} as CardViewGroup);
expect(contentMetadataService.getGroupedProperties).toHaveBeenCalledWith(expectedNode, 'custom-preset');
});
it('should revert changes for categories panel on cancel', () => {
const spy = spyOn(categoryService, 'getCategoryLinksForNode').and.returnValue(of(categoryPagingResponse));
component.displayCategories = true;
component.onCancelCategoriesEdit();
expect(spy).toHaveBeenCalledWith(expectedNode.id);
});
it('should revert changes for tags panel on cancel', () => {
const spy = spyOn(tagService, 'getTagsByNodeId').and.returnValue(of(mockTagPaging()));
component.displayTags = true;
component.onCancelTagsEdit();
expect(spy).toHaveBeenCalledWith(expectedNode.id);
});
});
describe('Properties displaying', () => {
@@ -725,7 +948,7 @@ describe('ContentMetadataComponent', () => {
'cm:versionable': '*'
});
spyOn(classesApi, 'getClass').and.returnValue(Promise.resolve(verResponse));
getClassSpy.and.returnValue(Promise.resolve(verResponse));
component.ngOnChanges({ node: new SimpleChange(node, expectedNode, false) });
fixture.detectChanges();
@@ -745,7 +968,7 @@ describe('ContentMetadataComponent', () => {
'cm:versionable': '*'
});
spyOn(classesApi, 'getClass').and.returnValue(Promise.resolve(verResponse));
getClassSpy.and.returnValue(Promise.resolve(verResponse));
component.ngOnChanges({ node: new SimpleChange(node, expectedNode, false) });
fixture.detectChanges();
@@ -765,7 +988,7 @@ describe('ContentMetadataComponent', () => {
exclude: 'cm:versionable'
});
spyOn(classesApi, 'getClass').and.returnValue(Promise.resolve(verResponse));
getClassSpy.and.returnValue(Promise.resolve(verResponse));
component.ngOnChanges({ node: new SimpleChange(node, expectedNode, false) });
fixture.detectChanges();
@@ -786,7 +1009,7 @@ describe('ContentMetadataComponent', () => {
'cm:versionable': '*'
});
spyOn(classesApi, 'getClass').and.returnValue(Promise.resolve(verResponse));
getClassSpy.and.returnValue(Promise.resolve(verResponse));
component.ngOnChanges({ node: new SimpleChange(node, expectedNode, false) });
fixture.detectChanges();
@@ -806,7 +1029,7 @@ describe('ContentMetadataComponent', () => {
exclude: ['cm:versionable', 'cm:auditable']
});
spyOn(classesApi, 'getClass').and.returnValue(Promise.resolve(verResponse));
getClassSpy.and.returnValue(Promise.resolve(verResponse));
component.ngOnChanges({ node: new SimpleChange(node, expectedNode, false) });
fixture.detectChanges();
@@ -830,7 +1053,7 @@ describe('ContentMetadataComponent', () => {
'exif:exif': ['exif:pixelXDimension', 'exif:pixelYDimension']
});
spyOn(classesApi, 'getClass').and.returnValue(Promise.resolve(exifResponse));
getClassSpy.and.returnValue(Promise.resolve(exifResponse));
component.ngOnChanges({ node: new SimpleChange(node, expectedNode, false) });
fixture.detectChanges();
@@ -864,7 +1087,7 @@ describe('ContentMetadataComponent', () => {
'exif:exif': ['exif:pixelXDimension', 'exif:pixelYDimension']
});
spyOn(classesApi, 'getClass').and.returnValue(Promise.resolve(exifResponse));
getClassSpy.and.returnValue(Promise.resolve(exifResponse));
component.ngOnChanges({ node: new SimpleChange(node, expectedNode, false) });
fixture.detectChanges();
@@ -880,28 +1103,21 @@ describe('ContentMetadataComponent', () => {
});
describe('Expand the panel', () => {
let expectedNode: Node;
beforeEach(() => {
expectedNode = { ...node, name: 'some-modified-value' };
spyOn(contentMetadataService, 'getGroupedProperties').and.returnValue(of(mockGroupProperties));
component.ngOnChanges({ node: new SimpleChange(node, expectedNode, false) });
component.isGeneralPanelExpanded = false;
});
it('should open and update drawer with expand section dynamically', async () => {
component.displayEmpty = true;
component.displayAspect = 'EXIF';
component.expanded = true;
component.displayEmpty = true;
fixture.detectChanges();
await fixture.whenStable();
let defaultProp = queryDom(fixture);
let exifProp = queryDom(fixture, 'EXIF');
let customProp = queryDom(fixture, 'CUSTOM');
expect(defaultProp.componentInstance.expanded).toBeFalsy();
expect(exifProp.componentInstance.expanded).toBeTruthy();
expect(customProp.componentInstance.expanded).toBeFalsy();
component.displayAspect = 'CUSTOM';
@@ -909,23 +1125,7 @@ describe('ContentMetadataComponent', () => {
await fixture.whenStable();
defaultProp = queryDom(fixture);
exifProp = queryDom(fixture, 'EXIF');
customProp = queryDom(fixture, 'CUSTOM');
expect(defaultProp.componentInstance.expanded).toBeFalsy();
expect(exifProp.componentInstance.expanded).toBeFalsy();
expect(customProp.componentInstance.expanded).toBeTruthy();
component.displayAspect = 'Properties';
fixture.detectChanges();
await fixture.whenStable();
defaultProp = queryDom(fixture);
exifProp = queryDom(fixture, 'EXIF');
customProp = queryDom(fixture, 'CUSTOM');
expect(defaultProp.componentInstance.expanded).toBeTruthy();
expect(exifProp.componentInstance.expanded).toBeFalsy();
expect(customProp.componentInstance.expanded).toBeFalsy();
});
it('should not expand anything if input is wrong', async () => {
@@ -937,11 +1137,16 @@ describe('ContentMetadataComponent', () => {
await fixture.whenStable();
const defaultProp = queryDom(fixture);
const exifProp = queryDom(fixture, 'EXIF');
const customProp = queryDom(fixture, 'CUSTOM');
expect(defaultProp.componentInstance.expanded).toBeFalsy();
expect(exifProp.componentInstance.expanded).toBeFalsy();
expect(customProp.componentInstance.expanded).toBeFalsy();
});
it('should expand the section when displayAspect set as Properties', async () => {
component.displayAspect = 'Properties';
component.ngOnInit();
fixture.detectChanges();
expect(component.isGeneralPanelExpanded).toBeTruthy();
});
});
@@ -1053,7 +1258,8 @@ describe('ContentMetadataComponent', () => {
});
it('should render tags after loading tags after clicking on Cancel button', fakeAsync(() => {
component.editable = true;
component.isEditingModeTags = true;
component.readOnly = false;
fixture.detectChanges();
TestBed.inject(CardViewContentUpdateService).itemUpdated$.next({
changed: {}
@@ -1061,9 +1267,8 @@ describe('ContentMetadataComponent', () => {
tick(500);
fixture.detectChanges();
spyOn(tagService, 'getTagsByNodeId').and.returnValue(of(tagPaging));
clickOnCancel();
component.editable = false;
findCancelTagsButton().click();
component.isEditingModeTags = false;
fixture.detectChanges();
const tagElements = findTagElements();
expect(tagElements).toHaveSize(2);
@@ -1077,7 +1282,7 @@ describe('ContentMetadataComponent', () => {
component.ngOnInit();
fixture.detectChanges();
component.editable = true;
component.isEditingModeTags = true;
fixture.detectChanges();
expect(findTagElements()).toHaveSize(0);
});
@@ -1087,7 +1292,7 @@ describe('ContentMetadataComponent', () => {
let tagsCreator: TagsCreatorComponent;
beforeEach(() => {
component.editable = true;
component.isEditingModeTags = true;
component.displayTags = true;
fixture.detectChanges();
tagsCreator = findTagsCreator();
@@ -1097,51 +1302,29 @@ describe('ContentMetadataComponent', () => {
expect(tagsCreator.tagNameControlVisible).toBeFalse();
});
it('should hide showing tag input button after emitting tagNameControlVisibleChange event with true', () => {
tagsCreator.tagNameControlVisibleChange.emit(true);
fixture.detectChanges();
expect(findShowingTagInputButton().hasAttribute('hidden')).toBeTrue();
});
it('should show showing tag input button after emitting tagNameControlVisibleChange event with false', fakeAsync(() => {
tagsCreator.tagNameControlVisibleChange.emit(true);
fixture.detectChanges();
tick();
tagsCreator.tagNameControlVisibleChange.emit(false);
fixture.detectChanges();
tick(100);
expect(findShowingTagInputButton().hasAttribute('hidden')).toBeFalse();
}));
it('should have assigned correct mode', () => {
it('should load in create and assign mode by default', () => {
expect(tagsCreator.mode).toBe(TagsCreatorMode.CREATE_AND_ASSIGN);
});
it('should enable cancel button after emitting tagsChange event', () => {
component.readOnly = false;
tagsCreator.tagsChange.emit(['New tag 1', 'New tag 2', 'New tag 3']);
fixture.detectChanges();
expect(findCancelButton().disabled).toBeFalse();
expect(findCancelTagsButton().disabled).toBeFalse();
});
it('should enable save button after emitting tagsChange event', () => {
tagsCreator.tagsChange.emit(['New tag 1', 'New tag 2', 'New tag 3']);
component.readOnly = false;
fixture.detectChanges();
expect(findSaveButton().disabled).toBeFalse();
expect(findSaveTagsButton().disabled).toBeFalse();
});
it('should have assigned false to disabledTagsRemoving', () => {
expect(tagsCreator.disabledTagsRemoving).toBeFalse();
});
it('should have assigned true to disabledTagsRemoving after clicking on update button', () => {
tagsCreator.tagsChange.emit([]);
fixture.detectChanges();
clickOnSave();
expect(tagsCreator.disabledTagsRemoving).toBeTrue();
});
it('should have assigned false to disabledTagsRemoving if forkJoin fails', fakeAsync(() => {
it('should have assigned false to disabledTagsRemoving if forkJoin fails', () => {
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));
@@ -1152,24 +1335,15 @@ describe('ContentMetadataComponent', () => {
spyOn(tagService, 'assignTagsToNode').and.returnValue(EMPTY);
const tagName1 = tagPaging.list.entries[0].entry.tag;
const tagName2 = 'New tag 3';
component.isEditingModeTags = true;
component.readOnly = false;
updateService.update(property, 'updated-value');
tick(600);
fixture.detectChanges();
tagsCreator.tagsChange.emit([tagName1, tagName2]);
clickOnSave();
clickOnTagsSave();
expect(tagsCreator.disabledTagsRemoving).toBeFalse();
}));
it('should have assigned false to tagNameControlVisible after clicking on update button', () => {
tagsCreator.tagNameControlVisibleChange.emit(true);
tagsCreator.tagsChange.emit([]);
fixture.detectChanges();
clickOnSave();
expect(tagsCreator.tagNameControlVisible).toBeFalse();
});
describe('Setting tags', () => {
@@ -1199,7 +1373,7 @@ describe('ContentMetadataComponent', () => {
});
it('should show tags creator if editable is true and displayTags is true', () => {
component.editable = true;
component.isEditingModeTags = true;
component.displayTags = true;
fixture.detectChanges();
expect(findTagsCreator()).toBeDefined();
@@ -1263,7 +1437,8 @@ describe('ContentMetadataComponent', () => {
});
it('should render categories after discard changes button is clicked', fakeAsync(() => {
component.editable = true;
component.isEditingModeCategories = true;
component.readOnly = false;
fixture.detectChanges();
TestBed.inject(CardViewContentUpdateService).itemUpdated$.next({
changed: {}
@@ -1272,7 +1447,7 @@ describe('ContentMetadataComponent', () => {
fixture.detectChanges();
clickOnCancel();
component.editable = false;
component.isEditingModeGeneralInfo = false;
fixture.detectChanges();
const categories = getCategories();
@@ -1285,7 +1460,7 @@ describe('ContentMetadataComponent', () => {
}));
it('should be hidden when editable is true', () => {
component.editable = true;
component.isEditingModeCategories = true;
fixture.detectChanges();
expect(getCategories().length).toBe(0);
});
@@ -1295,7 +1470,7 @@ describe('ContentMetadataComponent', () => {
let categoriesManagementComponent: CategoriesManagementComponent;
beforeEach(() => {
component.editable = true;
component.isEditingModeCategories = true;
component.displayCategories = true;
component.node.aspectNames.push('generalclassifiable');
spyOn(categoryService, 'getCategoryLinksForNode').and.returnValue(of(categoryPagingResponse));
@@ -1307,23 +1482,7 @@ describe('ContentMetadataComponent', () => {
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', () => {
it('should load with assign mode by default', () => {
expect(categoriesManagementComponent.managementMode).toBe(CategoriesManagementMode.ASSIGN);
});
@@ -1339,24 +1498,17 @@ describe('ContentMetadataComponent', () => {
it('should enable discard and save buttons after emitting categories change event', () => {
categoriesManagementComponent.categoriesChange.emit([category1, category2]);
component.readOnly =false;
fixture.detectChanges();
expect(findCancelButton().disabled).toBeFalse();
expect(findSaveButton().disabled).toBeFalse();
expect(findSaveCategoriesButton().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(() => {
it('should not disable removal if forkJoin fails', () => {
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));
@@ -1367,24 +1519,14 @@ describe('ContentMetadataComponent', () => {
spyOn(categoryService, 'linkNodeToCategory').and.returnValue(throwError({}));
updateService.update(property, 'updated-value');
tick(600);
component.readOnly = false;
component.isEditingModeCategories = true;
fixture.detectChanges();
categoriesManagementComponent.categoriesChange.emit([category1, category2]);
clickOnSave();
findSaveCategoriesButton();
expect(categoriesManagementComponent.disableRemoval).toBeFalse();
discardPeriodicTasks();
flush();
}));
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', () => {
@@ -1410,7 +1552,7 @@ describe('ContentMetadataComponent', () => {
it('should render correct custom panel with title and component', () => {
component.customPanels = [{ panelTitle: 'testTitle', component: 'testComponent' }];
fixture.detectChanges();
const panelTitle = fixture.debugElement.queryAll(By.css('mat-panel-title'))[1].nativeElement;
const panelTitle = fixture.debugElement.query(By.css('.adf-metadata-custom-panel-title .adf-metadata-properties-title')).nativeElement;
const customComponent = fixture.debugElement.query(By.css('adf-dynamic-component')).nativeElement;
expect(panelTitle.innerText).toEqual('testTitle');
expect(customComponent).toBeDefined();

View File

@@ -15,23 +15,22 @@
* limitations under the License.
*/
import { Component, Input, OnChanges, OnDestroy, OnInit, SimpleChanges, ViewEncapsulation } from '@angular/core';
import {
Category,
CategoryEntry,
CategoryLinkBody,
CategoryPaging,
Node,
TagBody,
TagEntry,
TagPaging
} from '@alfresco/js-api';
Component,
Input,
OnChanges,
OnDestroy,
OnInit,
SimpleChanges,
ViewEncapsulation
} from '@angular/core';
import { Category, CategoryEntry, CategoryLinkBody, CategoryPaging, Node, TagBody, TagEntry, TagPaging } from '@alfresco/js-api';
import { forkJoin, Observable, of, Subject, zip } from 'rxjs';
import {
AppConfigService,
CardViewBaseItemModel,
CardViewItem,
LogService,
NotificationService,
TranslationService,
UpdateNotification
} from '@alfresco/adf-core';
@@ -44,6 +43,8 @@ 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';
import { AllowableOperationsEnum } from '../../../common/models/allowable-operations.enum';
import { ContentService } from '../../../common/services/content.service';
const DEFAULT_SEPARATOR = ', ';
@@ -61,17 +62,6 @@ export class ContentMetadataComponent implements OnChanges, OnInit, OnDestroy {
@Input()
node: Node;
/** Toggles whether the edit button should be shown */
@Input()
set editable(editable: boolean) {
this._editable = editable;
this._assignedTags = [...this.tags];
}
get editable(): boolean {
return this._editable;
}
/** Toggles whether to display empty values in the card view */
@Input()
displayEmpty: boolean = false;
@@ -119,9 +109,15 @@ export class ContentMetadataComponent implements OnChanges, OnInit, OnDestroy {
@Input()
customPanels: ContentMetadataCustomPanel[] = [];
/**
* (optional) This flag sets the metadata in read-only mode,
* preventing changes.
*/
@Input()
readOnly = false;
private _assignedTags: string[] = [];
private assignedTagsEntries: TagEntry[] = [];
private _editable = false;
private _tagsCreatorMode = TagsCreatorMode.CREATE_AND_ASSIGN;
private _tags: string[] = [];
private targetProperty: CardViewBaseItemModel;
@@ -140,16 +136,25 @@ export class ContentMetadataComponent implements OnChanges, OnInit, OnDestroy {
categoriesManagementMode = CategoriesManagementMode.ASSIGN;
categoryControlVisible = false;
classifiableChanged = this.classifiableChangedSubject.asObservable();
isGeneralPanelExpanded = true;
isTagPanelExpanded: boolean;
isCategoriesPanelExpanded: boolean;
currentGroup: CardViewGroup;
isEditingModeGeneralInfo = false;
isEditingModeTags = false;
isEditingModeCategories = false;
constructor(
private contentMetadataService: ContentMetadataService,
private cardViewContentUpdateService: CardViewContentUpdateService,
private nodesApiService: NodesApiService,
private logService: LogService,
private translationService: TranslationService,
private appConfig: AppConfigService,
private tagService: TagService,
private categoryService: CategoryService
private categoryService: CategoryService,
private contentService: ContentService,
private notificationService: NotificationService
) {
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;
@@ -158,23 +163,32 @@ export class ContentMetadataComponent implements OnChanges, OnInit, OnDestroy {
ngOnInit() {
this.cardViewContentUpdateService.itemUpdated$
.pipe(
debounceTime(500),
takeUntil(this.onDestroy$))
.subscribe(
(updatedNode: UpdateNotification) => {
this.hasMetadataChanged = true;
this.targetProperty = updatedNode.target;
this.updateChanges(updatedNode.changed);
}
);
.pipe(debounceTime(500), takeUntil(this.onDestroy$))
.subscribe((updatedNode: UpdateNotification) => {
this.hasMetadataChanged = true;
this.targetProperty = updatedNode.target;
this.updateChanges(updatedNode.changed);
});
this.cardViewContentUpdateService.updatedAspect$.pipe(
debounceTime(500),
takeUntil(this.onDestroy$))
.subscribe((node) => this.loadProperties(node));
this.cardViewContentUpdateService.updatedAspect$
.pipe(debounceTime(500), takeUntil(this.onDestroy$))
.subscribe((node) => {
this.node.aspectNames = node?.aspectNames;
this.loadProperties(node);
});
this.loadProperties(this.node);
this.verifyAllowableOperations();
if (this.displayAspect === 'Properties') {
this.isGeneralPanelExpanded = true;
}
}
private verifyAllowableOperations() {
if (!this.node?.allowableOperations || !this.contentService.hasAllowableOperations(this.node, AllowableOperationsEnum.UPDATE)) {
this.readOnly = true;
}
}
get assignedTags(): string[] {
@@ -194,14 +208,11 @@ export class ContentMetadataComponent implements OnChanges, OnInit, OnDestroy {
}
protected handleUpdateError(error: Error) {
this.logService.error(error);
let statusCode = 0;
try {
statusCode = JSON.parse(error.message).error.statusCode;
} catch {
}
} catch {}
let message = `METADATA.ERRORS.${statusCode}`;
@@ -219,6 +230,10 @@ export class ContentMetadataComponent implements OnChanges, OnInit, OnDestroy {
if (changes.node && !changes.node.firstChange) {
this.loadProperties(changes.node.currentValue);
}
if(changes?.readOnly && changes?.readOnly?.currentValue) {
this.cancelEditing();
this.groupedProperties$ = this.contentMetadataService.getGroupedProperties(this.node, this.preset);
}
}
ngOnDestroy() {
@@ -239,14 +254,36 @@ export class ContentMetadataComponent implements OnChanges, OnInit, OnDestroy {
});
}
/**
* 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() {
private onSave(event?: MouseEvent) {
event?.stopPropagation();
this.onSaveChanges();
this.cancelEditing();
}
onSaveGeneralInfoChanges(event?: MouseEvent) {
this.onSave(event);
}
onSaveTagsChanges(event?: MouseEvent) {
this.onSave(event);
}
onSaveCategoriesChanges(event?: MouseEvent) {
this.onSave(event);
}
onSaveGroupChanges(group: CardViewGroup, event?: MouseEvent) {
this.onSave(event);
group.editable = false;
}
private onSaveChanges() {
this._saving = true;
this.tagNameControlVisible = false;
this.categoryControlVisible = false;
if (this.hasContentTypeChanged(this.changedProperties)) {
this.contentMetadataService.openConfirmDialog(this.changedProperties).subscribe(() => {
this.updateNode();
@@ -285,9 +322,168 @@ export class ContentMetadataComponent implements OnChanges, OnInit, OnDestroy {
this.categoryControlVisible = false;
}
cancelChanges() {
// Returns the editing state of the panel
isEditingPanel(): boolean {
return (
(this.isEditingModeGeneralInfo || this.isEditingModeTags || this.isEditingModeCategories || this.currentGroup?.editable) && this.hasMetadataChanged
);
}
onToggleGeneralInfoEdit(event?: MouseEvent) {
event?.stopPropagation();
if (this.isEditingPanel()) {
this.notificationService.showError('METADATA.BASIC.SAVE_OR_DISCARD_CHANGES');
return;
}
const currentMode = this.isEditingModeGeneralInfo;
this.cancelEditing();
this.isEditingModeGeneralInfo = !currentMode;
this.isGeneralPanelExpanded = true;
this.groupedProperties$ = this.contentMetadataService.getGroupedProperties(this.node, this.preset);
}
onToggleTagsEdit(event?: MouseEvent) {
event?.stopPropagation();
if (this.isEditingPanel()) {
this.notificationService.showError('METADATA.BASIC.SAVE_OR_DISCARD_CHANGES');
return;
}
const currentValue = this.isEditingModeTags;
this.cancelEditing();
this.isEditingModeTags = !currentValue;
this.isTagPanelExpanded = this.isEditingModeTags;
this.tagNameControlVisible = true;
this.groupedProperties$ = this.contentMetadataService.getGroupedProperties(this.node, this.preset);
}
private cancelEditing() {
this.isEditingModeGeneralInfo = false;
this.isEditingModeCategories = false;
this.isEditingModeTags = false;
}
onCancelGeneralInfoEdit(event?: MouseEvent) {
event?.stopPropagation();
this.cancelEditing();
this.revertChanges();
this.loadProperties(this.node);
this.basicProperties$ = this.getProperties(this.node);
}
onCancelCategoriesEdit(event?: MouseEvent) {
event?.stopPropagation();
this.cancelEditing();
this.revertChanges();
this.loadCategoriesForNode(this.node.id);
const aspectNames = this.node.aspectNames || [];
if (!aspectNames.includes('generalclassifiable')) {
this.categories = [];
this.classifiableChangedSubject.next();
}
}
onCancelTagsEdit(event?: MouseEvent) {
event?.stopPropagation();
this.cancelEditing();
this.revertChanges();
this.basicProperties$ = this.getProperties(this.node);
this.loadTagsForNode(this.node.id);
}
onCancelGroupEdit(group: CardViewGroup, event?: MouseEvent) {
event?.stopPropagation();
this.cancelEditing();
this.revertChanges();
group.editable = false;
this.groupedProperties$ = this.contentMetadataService.getGroupedProperties(this.node, this.preset);
}
onToggleCategoriesEdit(event?: MouseEvent) {
event?.stopPropagation();
if (this.isEditingPanel()) {
this.notificationService.showError('METADATA.BASIC.SAVE_OR_DISCARD_CHANGES');
return;
}
const currentValue = this.isEditingModeCategories;
this.cancelEditing();
this.isEditingModeCategories = !currentValue;
this.isCategoriesPanelExpanded = this.isEditingModeCategories;
this.categoryControlVisible = true;
this.groupedProperties$ = this.contentMetadataService.getGroupedProperties(this.node, this.preset);
}
onToggleGroupEdit(group: CardViewGroup, event?: MouseEvent) {
event?.stopPropagation();
if (this.isEditingPanel()) {
this.notificationService.showError('METADATA.BASIC.SAVE_OR_DISCARD_CHANGES');
return;
}
this.cancelEditing();
if (this.currentGroup && this.currentGroup.title !== group.title) {
this.currentGroup.editable = false;
}
group.editable = !group.editable;
this.currentGroup = group.editable ? group : null;
if (group.editable) {
group.expanded = true;
}
}
get showEmptyTagMessage(): boolean {
return this.tags?.length === 0 && !this.isEditingModeTags;
}
get showEmptyCategoryMessage(): boolean {
return this.categories?.length === 0 && !this.isEditingModeCategories;
}
get canEditGeneralInfo(): boolean {
return !this.isEditingModeGeneralInfo && !this.readOnly;
}
get isEditingGeneralInfo(): boolean {
return this.isEditingModeGeneralInfo && !this.readOnly;
}
get canEditTags(): boolean {
return !this.isEditingModeTags && !this.readOnly;
}
get isEditingTags(): boolean {
return this.isEditingModeTags && !this.readOnly;
}
get canEditCategories(): boolean {
return !this.isEditingModeCategories && !this.readOnly;
}
get isEditingCategories(): boolean {
return this.isEditingModeCategories && !this.readOnly;
}
hasGroupToggleEdit(group: CardViewGroup): boolean {
return !group.editable && !this.readOnly;
}
isGroupToggleEditing(group: CardViewGroup): boolean {
return group.editable && !this.readOnly;
}
showGroup(group: CardViewGroup): boolean {
@@ -300,10 +496,6 @@ export class ContentMetadataComponent implements OnChanges, OnInit, OnDestroy {
return groupTitle === this.displayAspect;
}
canExpandProperties(): boolean {
return !this.expanded || this.displayAspect === 'Properties';
}
keyDown(event: KeyboardEvent) {
if (event.keyCode === 37 || event.keyCode === 39) { // ArrowLeft && ArrowRight
event.stopPropagation();
@@ -315,13 +507,15 @@ export class ContentMetadataComponent implements OnChanges, OnInit, OnDestroy {
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);
}))
})
.pipe(
catchError((err) => {
this.cardViewContentUpdateService.updateElement(this.targetProperty);
this.handleUpdateError(err);
this._saving = false;
return of(null);
})
)
.subscribe((result: any) => {
if (result) {
this.updateUndefinedNodeProperties(result.updatedNode);
@@ -335,9 +529,9 @@ export class ContentMetadataComponent implements OnChanges, OnInit, OnDestroy {
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.assignedCategories = result.LinkingCategories.list
? result.LinkingCategories.list.entries.map((entry: CategoryEntry) => entry.entry)
: [result.LinkingCategories.entry];
}
}
this._saving = false;
@@ -358,12 +552,16 @@ export class ContentMetadataComponent implements OnChanges, OnInit, OnDestroy {
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')) {
const aspectNames = node.aspectNames || [];
if (!aspectNames.includes('generalclassifiable')) {
this.categories = [];
this.classifiableChangedSubject.next();
}
@@ -374,11 +572,14 @@ export class ContentMetadataComponent implements OnChanges, OnInit, OnDestroy {
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 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 {
@@ -434,11 +635,14 @@ export class ContentMetadataComponent implements OnChanges, OnInit, OnDestroy {
}
});
if (this.tags.length) {
observables.tagsAssigning = this.tagService.assignTagsToNode(this.node.id, this.tags.map((tag) => {
const tagBody = new TagBody();
tagBody.tag = tag;
return tagBody;
}));
observables.tagsAssigning = this.tagService.assignTagsToNode(
this.node.id,
this.tags.map((tag) => {
const tagBody = new TagBody();
tagBody.tag = tag;
return tagBody;
})
);
}
}
return observables;

View File

@@ -49,7 +49,9 @@ export const mockGroupProperties = [
clickCallBack: null,
displayValue: 400
}
]
],
editable: true,
expanded: true
},
{
title: 'CUSTOM',
@@ -69,6 +71,8 @@ export const mockGroupProperties = [
clickCallBack: null,
displayValue: 400
}
]
],
editable: true,
expanded: true
}
];

View File

@@ -24,6 +24,7 @@ import { ContentMetadataCardComponent } from './components/content-metadata-card
import { TagModule } from '../tag/tag.module';
import { CategoriesModule } from '../category/category.module';
import { ExtensionsModule } from '@alfresco/adf-extensions';
import { ContentMetadataHeaderComponent } from './components/content-metadata/content-metadata-header.component';
@NgModule({
imports: [
@@ -32,11 +33,13 @@ import { ExtensionsModule } from '@alfresco/adf-extensions';
CoreModule,
TagModule,
CategoriesModule,
ExtensionsModule
ExtensionsModule,
ContentMetadataHeaderComponent
],
exports: [
ContentMetadataComponent,
ContentMetadataCardComponent
ContentMetadataCardComponent,
ContentMetadataHeaderComponent
],
declarations: [
ContentMetadataComponent,

View File

@@ -20,4 +20,6 @@ import { CardViewItem } from '@alfresco/adf-core';
export interface CardViewGroup {
title: string;
properties: CardViewItem[];
editable?: boolean;
expanded?: boolean;
}

View File

@@ -18,4 +18,5 @@
export interface ContentMetadataCustomPanel {
panelTitle: string;
component: string;
expanded?: boolean;
}

View File

@@ -16,14 +16,15 @@
*/
export * from './components/content-metadata/content-metadata.component';
export * from './components/content-metadata/content-metadata-header.component';
export * from './components/content-metadata-card/content-metadata-card.component';
export * from './services/basic-properties.service';
export * from './services/content-metadata.service';
export * from './services/property-descriptors.service';
export * from './services/property-groups-translator.service';
export * from './services/config/content-metadata-config.factory';
export * from './services/content-type-property.service';
export * from './services/config/indifferent-config.service';
export * from './services/config/layout-oriented-config.service';
export * from './services/config/aspect-oriented-config.service';

View File

@@ -25,7 +25,7 @@ import {
CardViewDateItemModel,
CardViewIntItemModel,
CardViewFloatItemModel,
LogService,
NotificationService,
CardViewBoolItemModel,
CardViewDatetimeItemModel,
CardViewSelectItemModel,
@@ -42,7 +42,7 @@ describe('PropertyGroupTranslatorService', () => {
let propertyGroup: OrganisedPropertyGroup;
let property: Property;
let propertyValues: { [key: string]: any };
let logService: LogService;
let notificationService: NotificationService;
beforeEach(() => {
TestBed.configureTestingModule({
@@ -51,7 +51,7 @@ describe('PropertyGroupTranslatorService', () => {
ContentTestingModule
]
});
logService = TestBed.inject(LogService);
notificationService = TestBed.inject(NotificationService);
service = TestBed.inject(PropertyGroupTranslatorService);
property = {
@@ -137,7 +137,7 @@ describe('PropertyGroupTranslatorService', () => {
});
it('should log an error if unrecognised type is found', () => {
spyOn(logService, 'error').and.stub();
const showError = spyOn(notificationService, 'showError').and.stub();
property.name = 'FAS:PLAGUE';
property.title = 'The Faro Plague';
@@ -149,7 +149,7 @@ describe('PropertyGroupTranslatorService', () => {
propertyGroups.push(Object.assign({}, propertyGroup));
service.translateToCardViewGroups(propertyGroups, propertyValues, null);
expect(logService.error).toHaveBeenCalledWith('Unknown type for mapping: daemonic:scorcher');
expect(showError).toHaveBeenCalledWith('Unknown type for mapping: daemonic:scorcher');
});
it('should fall back to single-line property type if unrecognised type is found', () => {

View File

@@ -15,7 +15,7 @@
* limitations under the License.
*/
import { Injectable } from '@angular/core';
import { Injectable, inject } from '@angular/core';
import {
CardViewItemProperties,
CardViewItem,
@@ -26,10 +26,10 @@ import {
CardViewDatetimeItemModel,
CardViewIntItemModel,
CardViewFloatItemModel,
LogService,
MultiValuePipe,
AppConfigService,
DecimalNumberPipe
DecimalNumberPipe,
NotificationService
} from '@alfresco/adf-core';
import { Property, CardViewGroup, OrganisedPropertyGroup } from '../interfaces/content-metadata.interfaces';
import { of } from 'rxjs';
@@ -51,10 +51,10 @@ export const RECOGNISED_ECM_TYPES = [D_TEXT, D_MLTEXT, D_DATE, D_DATETIME, D_INT
providedIn: 'root'
})
export class PropertyGroupTranslatorService {
private notificationService = inject(NotificationService);
valueSeparator: string;
constructor(private logService: LogService,
private multiValuePipe: MultiValuePipe,
constructor(private multiValuePipe: MultiValuePipe,
private decimalNumberPipe: DecimalNumberPipe,
private appConfig: AppConfigService) {
this.valueSeparator = this.appConfig.get<string>('content-metadata.multi-value-pipe-separator');
@@ -187,7 +187,7 @@ export class PropertyGroupTranslatorService {
private checkECMTypeValidity(ecmPropertyType: string) {
if (RECOGNISED_ECM_TYPES.indexOf(ecmPropertyType) === -1) {
this.logService.error(`Unknown type for mapping: ${ecmPropertyType}`);
this.notificationService.showError(`Unknown type for mapping: ${ecmPropertyType}`);
}
}

View File

@@ -148,7 +148,7 @@
"NO_EXISTING_TAGS": "No Existing Tags",
"TITLE": "Create Tags",
"CREATE_TAG": "Create: {{tag}}",
"NAME": "Name",
"TAG_SEARCH_PLACEHOLDER": "Search Tags",
"ERRORS": {
"EXISTING_TAG": "Tag already exists",
"ALREADY_ADDED_TAG": "Tag is already added",
@@ -158,8 +158,7 @@
"CREATE_TAGS": "Error while creating the tags"
},
"TOOLTIPS": {
"DELETE_TAG": "Delete tag",
"HIDE_INPUT": "Hide input"
"DELETE_TAG": "Delete tag"
},
"TAGS_LOADING": "Tags loading",
"CREATE_TAGS_SUCCESS": "Tags created successfully"
@@ -169,7 +168,6 @@
"CATEGORIES_TITLE": "Categories",
"NO_CATEGORIES_CREATED": "No Categories created",
"NO_CATEGORIES_ASSIGNED": "No Categories assigned",
"ASSIGN_CATEGORIES": "Assign Categories",
"UNASSIGN_CATEGORY": "Unassign Category",
"DELETE_CATEGORY": "Delete Category",
"EXISTING_CATEGORIES": "Existing Categories:",
@@ -178,7 +176,7 @@
"GENERIC_CREATE": "Create: {{name}}",
"NAME": "Category name",
"LOADING": "Loading",
"HIDE_INPUT": "Hide input",
"NO_CATEGORIES_ADDED": "There are currently no categories added",
"ERRORS": {
"NOT_FOUND": "Categories not found",
"REQUIRED": "Category name is required",
@@ -187,7 +185,8 @@
"DUPLICATED_CATEGORY": "Category is already added",
"CREATE_CATEGORIES": "Error while creating categories",
"EXISTING_CATEGORIES": "Some categories already exist"
}
},
"CATEGORIES_SEARCH_PLACEHOLDER": "Search Categories"
},
"ADF_FILE_UPLOAD": {
"BUTTON": {
@@ -488,7 +487,9 @@
"MODIFIED_DATE": "Modified Date",
"CONTENT_TYPE": "Content Type",
"TAGS": "Tags",
"ADD_TAG_TOOLTIP": "Add tag"
"NO_TAGS_ADDED": "There are currently no tags added",
"NO_ITEMS_MESSAGE": "There are currently no {{ groupTitle }} added",
"SAVE_OR_DISCARD_CHANGES": "Save or discard changes to continue"
},
"CONTENT_TYPE": {
"DIALOG": {

View File

@@ -1,54 +1,37 @@
<div class="adf-tags-creation">
<div *ngIf="tagNameControlVisible" class="adf-tag-name-field">
<input #tagNameInput
class="adf-tag-search-field"
matInput
autocomplete="off"
[formControl]="tagNameControl"
(keyup.enter)="addTag()"
adf-auto-focus
placeholder="{{'TAG.TAGS_CREATOR.TAG_SEARCH_PLACEHOLDER' | translate}}"
/>
<mat-error *ngIf="tagNameControl.invalid && tagNameControl.touched">{{ tagNameErrorMessageKey | translate }}</mat-error>
</div>
<p
class="adf-no-tags-message"
*ngIf="!tags.length && !tagNameControlVisible">
*ngIf="showEmptyTagMessage">
{{ 'TAG.TAGS_CREATOR.NO_TAGS_CREATED' | translate }}
</p>
<div
class="adf-tags-list"
[class.adf-tags-list-fixed]="!tagNameControlVisible"
#tagsList>
<p
*ngFor="let tag of tags"
class="adf-tag adf-label-with-icon-button">
<span *ngFor="let tag of tags" class="adf-tag adf-label-with-icon-button">
{{ tag }}
<button
data-automation-id="remove-tag-button"
mat-icon-button
(click)="removeTag(tag)"
[attr.title]="'TAG.TAGS_CREATOR.TOOLTIPS.DELETE_TAG' | translate"
[disabled]="disabledTagsRemoving">
<mat-icon>remove</mat-icon>
[disabled]="disabledTagsRemoving"
class="adf-remove-tag">
<mat-icon>close</mat-icon>
</button>
</p>
</div>
<div
class="adf-tag-name-field"
*ngIf="(!tagNameControlVisible && tags.length) || tagNameControlVisible"
[hidden]="!tagNameControlVisible">
<mat-form-field *ngIf="tagNameControlVisible">
<mat-icon matPrefix>search</mat-icon>
<mat-label id="adf-tag-name-input-label">
{{ 'TAG.TAGS_CREATOR.NAME' | translate }}
</mat-label>
<input
#tagNameInput
matInput
autocomplete="off"
[formControl]="tagNameControl"
(keyup.enter)="addTag()"
aria-labelledby="adf-tag-name-input-label"
adf-auto-focus
/>
<mat-error [hidden]="!tagNameControl.invalid">{{ tagNameErrorMessageKey | translate }}</mat-error>
</mat-form-field>
<button
data-automation-id="hide-tag-name-input-button"
mat-icon-button
(click)="hideNameInput()"
[attr.title]="'TAG.TAGS_CREATOR.TOOLTIPS.HIDE_INPUT' | translate">
<mat-icon>remove</mat-icon>
</button>
</span>
</div>
</div>
<div

View File

@@ -1,15 +1,25 @@
adf-tags-creator {
display: block;
margin-left: -24px;
.adf-label-with-icon-button {
display: flex;
justify-content: space-between;
background: var(--adf-metadata-buttons-background-color);
width: fit-content;
min-height: 32px;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 16px;
padding: 6px 0;
padding-left: 12px;
.adf-remove-tag {
line-height: 24px;
height: 24px;
transform: scale(0.7);
}
}
.adf-no-tags-message {
margin-left: 9px;
margin-top: 28.5px;
margin-bottom: 0;
height: 30px;
@@ -17,23 +27,22 @@ adf-tags-creator {
.adf-tag-name-field,
.adf-tag-name-field[hidden] {
display: flex;
align-items: center;
min-height: 57px;
padding-top: 10px;
margin-right: 12px;
}
mat-form-field {
width: 100%;
box-sizing: border-box;
padding-right: 3px;
font-size: 14px;
}
.adf-tag-search-field {
background: var(--adf-metadata-buttons-background-color);
height: 32px;
border-radius: 12px;
padding: 0 6px;
}
.adf-create-tag-label {
color: var(--theme-primary-color);
cursor: pointer;
margin-top: -1px;
padding-left: 27px;
padding-left: 12px;
overflow-wrap: anywhere;
display: inline-block;
padding-right: 12px;
@@ -42,25 +51,21 @@ adf-tags-creator {
}
.adf-tags-list {
padding-left: 10px;
padding-right: 0;
overflow: auto;
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.adf-tag {
margin-top: 0;
margin-top: 8px;
overflow-wrap: anywhere;
& + .adf-tag {
margin-top: -14px;
margin-top: 8px;
}
}
.adf-tags-creation {
padding-left: 27px;
padding-right: 22px;
}
.adf-existing-tags-panel {
border-top-left-radius: 6px;
border-top-right-radius: 6px;
@@ -75,7 +80,7 @@ adf-tags-creator {
}
.adf-tags-list {
padding-left: 18px;
padding-left: 12px;
margin-top: -2px;
padding-right: 0;
display: flex;
@@ -94,7 +99,7 @@ adf-tags-creator {
}
.mat-list-item-content-reverse {
padding: 0 6px;
padding: 0;
.mat-pseudo-checkbox {
display: none;
@@ -126,6 +131,6 @@ adf-tags-creator {
[hidden] {
visibility: hidden;
display: unset;
display: none;
}
}

View File

@@ -21,7 +21,7 @@ import { NotificationService } from '@alfresco/adf-core';
import { By } from '@angular/platform-browser';
import { TranslateModule } from '@ngx-translate/core';
import { MatIconModule } from '@angular/material/icon';
import { MatError, MatFormField, MatFormFieldModule } from '@angular/material/form-field';
import { MatError, MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
@@ -38,8 +38,6 @@ describe('TagsCreatorComponent', () => {
let tagService: TagService;
let notificationService: NotificationService;
const tagNameFieldSelector = '.adf-tag-name-field';
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [TagsCreatorComponent],
@@ -113,14 +111,6 @@ describe('TagsCreatorComponent', () => {
return elements.map(el => el.nativeElement);
}
/**
* Click at the hide name input button
*/
function clickAtHideNameInputButton() {
fixture.debugElement.query(By.css(`[data-automation-id="hide-tag-name-input-button"]`)).nativeElement.click();
fixture.detectChanges();
}
/**
* Get newly added tags
*
@@ -301,34 +291,6 @@ describe('TagsCreatorComponent', () => {
});
describe('Tag name field', () => {
it('should be visible if tagNameControlVisible is true', () => {
component.tagNameControlVisible = true;
fixture.detectChanges();
const tagNameField = fixture.debugElement.query(By.css(tagNameFieldSelector));
expect(tagNameField).toBeTruthy();
expect(tagNameField.nativeElement.hasAttribute('hidden')).toBeFalsy();
expect(tagNameField.query(By.directive(MatFormField))).toBeTruthy();
});
it('should be hidden and cleared after clicking button for hiding input', fakeAsync(() => {
component.tagNameControlVisible = true;
typeTag('test');
fixture.detectChanges();
tick(100);
clickAtHideNameInputButton();
const tagNameField = fixture.debugElement.query(By.css(tagNameFieldSelector));
expect(tagNameField).toBeFalsy();
component.tagNameControlVisible = true;
fixture.detectChanges();
tick(100);
expect(getNameInput().value).toBe('');
}));
it('should input be autofocused', fakeAsync(() => {
component.tagNameControlVisible = true;
fixture.detectChanges();
@@ -341,7 +303,6 @@ describe('TagsCreatorComponent', () => {
fixture.detectChanges();
tick(100);
clickAtHideNameInputButton();
component.tagNameControlVisible = true;
fixture.detectChanges();
tick(100);
@@ -349,24 +310,13 @@ describe('TagsCreatorComponent', () => {
expect(getNameInput()).toBe(document.activeElement as HTMLInputElement);
}));
it('should be hidden and cleared on discard changes', fakeAsync(() => {
component.tagNameControlVisible = true;
component.tags = ['Passed tag 1', 'Passed tag 2'];
typeTag('test');
fixture.detectChanges();
tick(100);
expect(getNameInput().value).toBe('test');
component.tagNameControlVisible = false;
fixture.detectChanges();
tick(100);
expect(getNameInput()).toBeFalsy();
component.tagNameControlVisible = true;
fixture.detectChanges();
tick(100);
expect(getNameInput().value).toBe('');
}));
describe('showEmptyTagMessage', () => {
it('should return true when tags empty and non editable state', () => {
component.tags = [];
component.tagNameControlVisible = false;
expect(component.showEmptyTagMessage).toBeTrue();
});
});
describe('Errors', () => {
/**
@@ -487,14 +437,6 @@ describe('TagsCreatorComponent', () => {
expect(getPanel()).toBeTruthy();
});
it('should not be visible when something has been typed and input has been hidden', fakeAsync(() => {
typeTag('some tag');
clickAtHideNameInputButton();
expect(getPanel()).toBeFalsy();
}));
it('should have correct label when mode is Create and Assign', fakeAsync(() => {
component.mode = TagsCreatorMode.CREATE_AND_ASSIGN;

View File

@@ -104,7 +104,7 @@ export class TagsCreatorComponent implements OnInit, OnDestroy {
if (tagNameControlVisible) {
this._existingTagsPanelVisible = true;
setTimeout(() => {
this.tagNameInputElement.nativeElement.scrollIntoView();
this.tagNameInputElement?.nativeElement?.scrollIntoView();
});
} else {
this._existingTagsPanelVisible = false;
@@ -127,11 +127,6 @@ export class TagsCreatorComponent implements OnInit, OnDestroy {
*/
@Output()
tagsChange = new EventEmitter<string[]>();
/**
* Emitted when input is showing or hiding.
*/
@Output()
tagNameControlVisibleChange = new EventEmitter<boolean>();
readonly nameErrorMessagesByErrors = new Map<keyof TagNameControlErrors, string>([
['duplicatedExistingTag', 'EXISTING_TAG'],
@@ -219,6 +214,13 @@ export class TagsCreatorComponent implements OnInit, OnDestroy {
return this._tagNameControl;
}
/*
* Returns `true` if tags empty and non editable state, otherwise `false`
*/
get showEmptyTagMessage(): boolean {
return this.tags?.length === 0 && !this.tagNameControlVisible;
}
get existingTags(): TagEntry[] {
return this._existingTags;
}
@@ -243,17 +245,6 @@ export class TagsCreatorComponent implements OnInit, OnDestroy {
return this._existingTagsPanelVisible;
}
/**
* Hide input for typing name for new tag or for searching. When input is hidden then panel of existing tags is hidden as well.
*/
hideNameInput(): void {
this.tagNameControlVisible = false;
this._existingTagsPanelVisible = false;
this.existingTagsPanelVisibilityChange.emit(this.existingTagsPanelVisible);
this.tagNameControlVisibleChange.emit(this.tagNameControlVisible);
this.clearTagNameInput();
}
/**
* Add tags to top list using value which is set in input. Adding tag is not allowed when value in input is invalid
* or if user is still typing what means that validation for input is not called yet.
@@ -261,7 +252,6 @@ export class TagsCreatorComponent implements OnInit, OnDestroy {
addTag(): void {
if (!this._typing && !this.tagNameControl.invalid) {
this.tags.push(this.tagNameControl.value.trim());
this.hideNameInput();
this.clearTagNameInput();
this.checkScrollbarVisibility();
this.tagsChange.emit(this.tags);