[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
co-authored by Yasa-Nataliya pkundu rbahirsheth Denys Vuika
parent a7d18cbfe5
commit a900dd2551
74 changed files with 1814 additions and 1652 deletions
@@ -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)">
@@ -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;
@@ -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();
@@ -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);
@@ -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');
});
});
});
@@ -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);
}
}
@@ -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>
@@ -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();
@@ -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);
}
@@ -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;
}
@@ -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>
@@ -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;
@@ -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();
@@ -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;
@@ -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
}
];
@@ -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,
@@ -20,4 +20,6 @@ import { CardViewItem } from '@alfresco/adf-core';
export interface CardViewGroup {
title: string;
properties: CardViewItem[];
editable?: boolean;
expanded?: boolean;
}
@@ -18,4 +18,5 @@
export interface ContentMetadataCustomPanel {
panelTitle: string;
component: string;
expanded?: boolean;
}
@@ -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';
@@ -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', () => {
@@ -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}`);
}
}
+8 -7
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": {
@@ -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
@@ -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;
}
}
@@ -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;
@@ -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);
@@ -26,6 +26,9 @@ import { takeUntil } from 'rxjs/operators';
export abstract class BaseCardView<T extends CardViewItem> implements OnDestroy {
protected cardViewUpdateService = inject(CardViewUpdateService);
@Input()
editable = false;
@Input()
property: T;
@@ -39,6 +42,18 @@ export abstract class BaseCardView<T extends CardViewItem> implements OnDestroy
});
}
get isEditable(): boolean {
return this.editable && this.property.editable;
}
get isReadonlyProperty(): boolean {
return this.editable && !this.property.editable;
}
get hasIcon(): boolean {
return !!this.property.icon;
}
ngOnDestroy(): void {
this.destroy$.next(true);
this.destroy$.complete();
@@ -33,11 +33,7 @@ export class CardViewArrayItemComponent extends BaseCardView<CardViewArrayItemMo
}
showClickableIcon(): boolean {
return this.hasIcon() && this.isClickable();
}
hasIcon(): boolean {
return !!this.property.icon;
return this.hasIcon && this.isClickable();
}
displayCount(): number {
@@ -1,9 +1,9 @@
<ng-container *ngIf="!property.isEmpty() || isEditable()">
<ng-container *ngIf="!property.isEmpty() || isEditable">
<div class="adf-property-value">
<mat-checkbox [attr.data-automation-id]="'card-boolean-' + property.key"
[attr.title]="'CORE.METADATA.ACTIONS.TOGGLE' | translate"
[checked]="property.displayValue"
[disabled]="!isEditable()"
[disabled]="!isEditable"
color="primary"
(change)="changed($event)">
<div [attr.data-automation-id]="'card-boolean-label-' + property.key"
@@ -15,7 +15,7 @@
* limitations under the License.
*/
import { Component, Input } from '@angular/core';
import { Component } from '@angular/core';
import { MatCheckboxChange } from '@angular/material/checkbox';
import { CardViewBoolItemModel } from '../../models/card-view-boolitem.model';
import { BaseCardView } from '../base-card-view';
@@ -26,13 +26,6 @@ import { BaseCardView } from '../base-card-view';
})
export class CardViewBoolItemComponent extends BaseCardView<CardViewBoolItemModel> {
@Input()
editable: boolean;
isEditable() {
return this.editable && this.property.editable;
}
changed(change: MatCheckboxChange) {
this.cardViewUpdateService.update({ ...this.property } as CardViewBoolItemModel, change.checked );
this.property.value = change.checked;
@@ -1,11 +1,12 @@
<label class="adf-property-label"
[attr.data-automation-id]="'card-dateitem-label-' + property.key"
*ngIf="showProperty() || isEditable()"
[attr.for]="'card-view-dateitem-' + property.key">
*ngIf="showProperty() || isEditable"
[attr.for]="'card-view-dateitem-' + property.key"
[ngClass]="{'adf-property-readonly-value': isReadonlyProperty, 'adf-property-value-editable': editable}">
{{ property.label | translate }}
</label>
<div class="adf-property-value adf-property-value-padding-top">
<span *ngIf="!isEditable() && !property.multivalued"
<div class="adf-property-value" [ngClass]="{'adf-property-value-editable': editable, 'adf-property-readonly-value': isReadonlyProperty}">
<span *ngIf="!isEditable && !property.multivalued"
[attr.data-automation-id]="'card-' + property.type + '-value-' + property.key">
<span *ngIf="showProperty()"
[attr.data-automation-id]="'card-dateitem-' + property.key"
@@ -13,45 +14,47 @@
matTooltipShowDelay="1000"
[matTooltip]="'CORE.METADATA.ACTIONS.COPY_TO_CLIPBOARD' | translate">{{ property.displayValue}}</span>
</span>
<div *ngIf="isEditable() && !property.multivalued"
class="adf-dateitem-editable">
<div *ngIf="isEditable && !property.multivalued" class="adf-dateitem-editable">
<div class="adf-dateitem-editable-controls">
<span class="adf-datepicker-toggle"
[attr.data-automation-id]="'datepicker-label-toggle-' + property.key"
(click)="showDatePicker()">
<span *ngIf="showProperty(); else elseEmptyValueBlock"
[attr.data-automation-id]="'card-' + property.type + '-value-' + property.key">
[attr.data-automation-id]="'card-' + property.type + '-value-' + property.key">
{{ property.displayValue }}</span>
</span>
<mat-icon *ngIf="showClearAction()"
class="adf-date-reset-icon"
(click)="onDateClear()"
[attr.title]="'CORE.METADATA.ACTIONS.CLEAR' | translate"
[attr.data-automation-id]="'datepicker-date-clear-' + property.key">
<mat-icon
*ngIf="showClearAction()"
class="adf-date-reset-icon"
(click)="onDateClear()"
[attr.title]="'CORE.METADATA.ACTIONS.CLEAR' | translate"
[attr.data-automation-id]="'datepicker-date-clear-' + property.key">
clear
</mat-icon>
<mat-datetimepicker-toggle [attr.tabindex]="-1"
[attr.title]="'CORE.METADATA.ACTIONS.EDIT' | translate"
[attr.data-automation-id]="'datepickertoggle-' + property.key"
[for]="datetimePicker">
<mat-datetimepicker-toggle
[attr.tabindex]="-1"
[attr.title]="'CORE.METADATA.ACTIONS.EDIT' | translate"
[attr.data-automation-id]="'datepickertoggle-' + property.key"
[for]="datetimePicker">
</mat-datetimepicker-toggle>
</div>
<input class="adf-invisible-date-input"
[attr.tabIndex]="-1"
[matDatetimepicker]="datetimePicker"
[value]="valueDate"
(dateChange)="onDateChanged($event)"
[attr.id]="'card-view-dateitem-' + property.key"
>
<input
class="adf-invisible-date-input"
[attr.tabIndex]="-1"
[matDatetimepicker]="datetimePicker"
[value]="valueDate"
(dateChange)="onDateChanged($event)"
[attr.id]="'card-view-dateitem-' + property.key">
<mat-datetimepicker #datetimePicker
[type]="$any(property).type"
[timeInterval]="5"
[attr.data-automation-id]="'datepicker-' + property.key"
[startAt]="valueDate">
<mat-datetimepicker
#datetimePicker
[type]="$any(property).type"
[timeInterval]="5"
[attr.data-automation-id]="'datepicker-' + property.key"
[startAt]="valueDate">
</mat-datetimepicker>
</div>
<ng-template #elseEmptyValueBlock>
@@ -60,41 +63,37 @@
<div *ngIf="property.multivalued"
class="adf-property-field adf-dateitem-chip-list-container adf-dateitem-editable">
<mat-chip-list #chipList
class="adf-textitem-chip-list">
<mat-chip *ngFor="let propertyValue of property.displayValue; let idx = index"
[removable]="isEditable()"
(removed)="removeValueFromList(idx)">
<mat-chip-list #chipList class="adf-textitem-chip-list">
<mat-chip
*ngFor="let propertyValue of property.displayValue; let idx = index"
[removable]="isEditable"
(removed)="removeValueFromList(idx)">
{{ propertyValue }}
<mat-icon *ngIf="isEditable()"
matChipRemove>cancel</mat-icon>
<mat-icon *ngIf="isEditable" matChipRemove>cancel</mat-icon>
</mat-chip>
</mat-chip-list>
<mat-form-field *ngIf="isEditable()"
class="adf-property-field adf-dateitem-editable-controls"
[floatLabel]="'never'"
(click)="showDatePicker()">
<input matInput
class="adf-invisible-date-input"
[attr.tabIndex]="-1"
[matDatetimepicker]="datetimePicker"
(dateChange)="addDateToList($event)"
[attr.id]="'card-view-dateitem-' + property.key"
>
<mat-datetimepicker-toggle [attr.tabindex]="-1"
matSuffix
[attr.title]="'CORE.METADATA.ACTIONS.EDIT' | translate"
[attr.data-automation-id]="'datepickertoggle-' + property.key"
[for]="datetimePicker">
<div *ngIf="isEditable" class="adf-property-field adf-dateitem-editable-controls" (click)="showDatePicker()">
<input
class="adf-invisible-date-input"
[attr.tabIndex]="-1"
[matDatetimepicker]="datetimePicker"
(dateChange)="addDateToList($event)"
[attr.id]="'card-view-dateitem-' + property.key">
<mat-datetimepicker-toggle
[attr.tabindex]="-1"
matSuffix
[attr.title]="'CORE.METADATA.ACTIONS.EDIT' | translate"
[attr.data-automation-id]="'datepickertoggle-' + property.key"
[for]="datetimePicker">
</mat-datetimepicker-toggle>
<mat-datetimepicker #datetimePicker
[type]="$any(property).type"
[timeInterval]="5"
[attr.data-automation-id]="'datepicker-' + property.key"
[startAt]="valueDate">
<mat-datetimepicker
#datetimePicker
[type]="$any(property).type"
[timeInterval]="5"
[attr.data-automation-id]="'datepicker-' + property.key"
[startAt]="valueDate">
</mat-datetimepicker>
</mat-form-field>
</div>
</div>
</div>
@@ -1,6 +1,21 @@
/* stylelint-disable no-descending-specificity */
.adf {
&-invisible-date-input {
.adf-card-view-dateitem {
.adf-property-value {
padding: 6px 0;
line-height: 20px;
border-bottom: 1px solid var(--adf-metadata-property-panel-border-color);
color: var(--adf-metadata-property-panel-title-color);
&.adf-property-value-editable {
border-radius: 6px;
border-bottom: inherit;
}
&.adf-property-readonly-value {
padding-left: 12px;
}
}
.adf-invisible-date-input {
height: 2px;
width: 0;
overflow: hidden;
@@ -11,33 +26,24 @@
float: right;
}
&-dateitem-chip-list-container.adf-property-field {
.adf-dateitem-chip-list-container.adf-property-field {
margin-bottom: -7px !important;
border-bottom: 0;
cursor: pointer;
.adf-dateitem-editable-controls {
margin-top: 15px;
}
.mat-datetimepicker-toggle {
position: absolute;
right: 0;
top: -20px;
}
}
&-dateitem-editable {
.adf-dateitem-editable {
cursor: pointer;
border-bottom: 1px solid var(--adf-theme-foreground-text-color-042);
padding-bottom: 6px;
padding: 0 12px;
/* stylelint-disable-next-line no-descending-specificity */
&-controls {
display: flex;
align-items: center;
justify-content: space-between;
padding-bottom: 6px;
button.mat-icon-button {
.mat-icon-button {
line-height: 20px;
height: 20px;
width: 20px;
@@ -47,29 +53,21 @@
width: 16px;
height: 16px;
opacity: 0.5;
}
&:hover mat-icon {
opacity: 1;
&:hover {
opacity: 1;
}
}
.adf-datepicker-toggle {
flex: 1 0 auto;
}
mat-icon.adf-date-reset-icon {
.adf-date-reset-icon {
line-height: 10px;
font-size: var(--theme-subheading-2-font-size);
width: 16px;
height: 16px;
position: relative;
top: 4px;
padding-left: 8px;
opacity: 0.3;
}
&:hover mat-icon.adf-date-reset-icon {
opacity: 1;
padding: 0 8px;
}
}
}
@@ -166,7 +166,7 @@ describe('CardViewDateItemComponent', () => {
component.property.editable = true;
fixture.detectChanges();
expect(component.isEditable()).toBe(false);
expect(component.isEditable).toBe(false);
const datePicker = fixture.debugElement.query(By.css(`[data-automation-id="datepicker-${component.property.key}"]`));
const datePickerToggle = fixture.debugElement.query(By.css(`[data-automation-id="datepickertoggle-${component.property.key}"]`));
expect(datePicker).toBeNull('Datepicker should NOT be in DOM');
@@ -20,7 +20,6 @@ import { DateAdapter, MAT_DATE_FORMATS } from '@angular/material/core';
import { DatetimeAdapter, MAT_DATETIME_FORMATS, MatDatetimepickerComponent, MatDatetimepickerInputEvent } from '@mat-datetimepicker/core';
import { CardViewDateItemModel } from '../../models/card-view-dateitem.model';
import { UserPreferencesService, UserPreferenceValues } from '../../../common/services/user-preferences.service';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { BaseCardView } from '../base-card-view';
import { ClipboardService } from '../../../clipboard/clipboard.service';
@@ -45,24 +44,16 @@ import { isValid } from 'date-fns';
})
export class CardViewDateItemComponent extends BaseCardView<CardViewDateItemModel> implements OnInit, OnDestroy {
@Input()
property: CardViewDateItemModel;
displayEmpty = true;
@Input()
editable: boolean = false;
@Input()
displayEmpty: boolean = true;
@Input()
displayClearAction: boolean = true;
displayClearAction = true;
@ViewChild('datetimePicker')
public datepicker: MatDatetimepickerComponent<any>;
valueDate: Date;
private onDestroy$ = new Subject<boolean>();
constructor(
private dateAdapter: DateAdapter<Date>,
private userPreferencesService: UserPreferencesService,
@@ -75,7 +66,7 @@ export class CardViewDateItemComponent extends BaseCardView<CardViewDateItemMode
ngOnInit() {
this.userPreferencesService
.select(UserPreferenceValues.Locale)
.pipe(takeUntil(this.onDestroy$))
.pipe(takeUntil(this.destroy$))
.subscribe((locale) => {
this.property.locale = locale;
});
@@ -94,8 +85,7 @@ export class CardViewDateItemComponent extends BaseCardView<CardViewDateItemMode
}
ngOnDestroy() {
this.onDestroy$.next(true);
this.onDestroy$.complete();
super.ngOnDestroy();
}
showProperty(): boolean {
@@ -106,10 +96,6 @@ export class CardViewDateItemComponent extends BaseCardView<CardViewDateItemMode
return this.displayClearAction && (!this.property.isEmpty() || !!this.property.default);
}
isEditable(): boolean {
return this.editable && this.property.editable;
}
showDatePicker() {
this.datepicker.open();
}
@@ -2,11 +2,8 @@
class="adf-property-label">{{ property.label | translate }}</div>
<div class="adf-property-field">
<div *ngIf="!isEditable()"
class="adf-card-view__key-value-pairs__read-only adf-property-value">
<mat-table #table
[dataSource]="matTableValues"
class="mat-elevation-z8">
<div *ngIf="!isEditable" class="adf-card-view__key-value-pairs__read-only adf-property-value">
<mat-table #table [dataSource]="matTableValues" class="mat-elevation-z8">
<ng-container matColumnDef="name">
<mat-header-cell *matHeaderCellDef>{{ 'CORE.CARDVIEW.KEYVALUEPAIRS.NAME' | translate }}
</mat-header-cell>
@@ -23,50 +20,44 @@
</mat-table>
</div>
<div class="adf-card-view__key-value-pairs adf-property-value"
*ngIf="isEditable() && values && values.length">
<div class="adf-card-view__key-value-pairs adf-property-value" *ngIf="isEditable && values && values.length">
<div class="adf-card-view__key-value-pairs__row">
<div class="adf-card-view__key-value-pairs__col">{{ 'CORE.CARDVIEW.KEYVALUEPAIRS.NAME' | translate }}</div>
<div class="adf-card-view__key-value-pairs__col">{{ 'CORE.CARDVIEW.KEYVALUEPAIRS.VALUE' | translate }}</div>
<div class="adf-property-col-key">{{ 'CORE.CARDVIEW.KEYVALUEPAIRS.NAME' | translate }}</div>
<div class="adf-property-col-value">{{ 'CORE.CARDVIEW.KEYVALUEPAIRS.VALUE' | translate }}</div>
</div>
<div class="adf-card-view__key-value-pairs__row"
*ngFor="let item of values; let i = index">
<div class="adf-card-view__key-value-pairs__col">
<mat-form-field>
<input matInput
placeholder="{{ 'CORE.CARDVIEW.KEYVALUEPAIRS.NAME' | translate }}"
(blur)="onBlur(item.value)"
[attr.data-automation-id]="'card-'+ property.key +'-name-input-' + i"
[(ngModel)]="values[i].name">
</mat-form-field>
</div>
<div class="adf-card-view__key-value-pairs__col">
<mat-form-field>
<input matInput
placeholder="{{ 'CORE.CARDVIEW.KEYVALUEPAIRS.VALUE' | translate }}"
(blur)="onBlur(item.value)"
[attr.data-automation-id]="'card-'+ property.key +'-value-input-' + i"
[(ngModel)]="values[i].value">
<button matSuffix
mat-icon-button
(click)="remove(i)"
class="adf-card-view__key-value-pairs__remove-btn">
<mat-icon>close</mat-icon>
</button>
</mat-form-field>
</div>
<div class="adf-card-view__key-value-pairs__row" *ngFor="let item of values; let i = index">
<input
matInput
class="adf-property-col-key adf-property-value-input"
placeholder="{{ 'CORE.CARDVIEW.KEYVALUEPAIRS.NAME' | translate }}"
(blur)="onBlur(item.value)"
[attr.data-automation-id]="'card-'+ property.key +'-name-input-' + i"
[(ngModel)]="values[i].name">
<input
matInput
class="adf-property-col-value adf-property-value-input"
placeholder="{{ 'CORE.CARDVIEW.KEYVALUEPAIRS.VALUE' | translate }}"
(blur)="onBlur(item.value)"
[attr.data-automation-id]="'card-'+ property.key +'-value-input-' + i"
[(ngModel)]="values[i].value">
<button
mat-icon-button
(click)="remove(i)"
class="adf-property-col-delete">
<mat-icon>close</mat-icon>
</button>
</div>
</div>
<div *ngIf="isEditable()"
class="adf-property-value adf-card-view__key-value-pairs__add-btn-container">
<div *ngIf="isEditable" class="adf-property-value adf-card-view__key-value-pairs__add-btn-container">
<button (click)="add()"
mat-button
class="adf-card-view__key-value-pairs__add-btn"
[attr.data-automation-id]="'card-key-value-pairs-button-' + property.key">
{{ 'CORE.CARDVIEW.KEYVALUEPAIRS.ADD' | translate }}
<mat-icon>add</mat-icon>
{{ 'CORE.CARDVIEW.KEYVALUEPAIRS.ADD' | translate }}
<mat-icon>add</mat-icon>
</button>
</div>
</div>
@@ -1,43 +1,48 @@
.adf-card-view {
&__key-value-pairs {
&__row {
display: flex;
justify-content: center;
}
.adf-card-view-key-value-pairs-item {
.adf-property-col-key {
width: 50%;
margin-right: 4px;
}
&__col {
width: 50%;
.adf-property-col-value {
margin-left: 4px;
width: 50%;
}
.mat-form-field {
width: 100%;
font-size: var(--theme-body-1-font-size);
.adf-property-col-delete {
width: auto;
}
.adf-card-view {
&__key-value-pairs {
&__row {
display: flex;
justify-content: center;
align-items: center;
}
.mat-form-field-appearance-legacy .mat-form-field-label {
color: var(--adf-theme-foreground-text-color-040) !important;
}
}
&__add-btn-container {
display: flex;
justify-content: center;
}
&__add-btn.mat-button {
margin-bottom: 20px;
}
&__read-only {
padding-bottom: 20px;
.mat-table {
box-shadow: none;
&__add-btn-container {
display: flex;
justify-content: center;
}
.mat-header-row,
.mat-row {
padding: 0;
&__add-btn.mat-button {
margin-bottom: 20px;
}
&__read-only {
padding-bottom: 20px;
.mat-table {
box-shadow: none;
}
.mat-header-row,
.mat-row {
padding: 0;
}
}
}
}
}
@@ -75,7 +75,7 @@ describe('CardViewKeyValuePairsItemComponent', () => {
component.ngOnChanges();
fixture.detectChanges();
expect(component.isEditable()).toBe(false);
expect(component.isEditable).toBe(false);
const table = fixture.debugElement.query(By.css('.adf-card-view__key-value-pairs__read-only'));
const form = fixture.debugElement.query(By.css('.adf-card-view__key-value-pairs'));
@@ -111,7 +111,7 @@ describe('CardViewKeyValuePairsItemComponent', () => {
addButton.triggerEventHandler('click', null);
fixture.detectChanges();
const removeButton = fixture.debugElement.query(By.css('.adf-card-view__key-value-pairs__remove-btn'));
const removeButton = fixture.debugElement.query(By.css('.adf-property-col-delete'));
removeButton.triggerEventHandler('click', null);
fixture.detectChanges();
@@ -15,7 +15,7 @@
* limitations under the License.
*/
import { Component, Input, OnChanges, ViewEncapsulation } from '@angular/core';
import { Component, OnChanges, ViewEncapsulation } from '@angular/core';
import { CardViewKeyValuePairsItemModel } from '../../models/card-view.models';
import { CardViewKeyValuePairsItemType } from '../../interfaces/card-view.interfaces';
import { MatTableDataSource } from '@angular/material/table';
@@ -30,10 +30,6 @@ import { BaseCardView } from '../base-card-view';
})
export class CardViewKeyValuePairsItemComponent extends BaseCardView<CardViewKeyValuePairsItemModel> implements OnChanges {
@Input()
editable: boolean = false;
values: CardViewKeyValuePairsItemType[];
matTableValues: MatTableDataSource<CardViewKeyValuePairsItemType>;
@@ -42,10 +38,6 @@ export class CardViewKeyValuePairsItemComponent extends BaseCardView<CardViewKey
this.matTableValues = new MatTableDataSource(this.values);
}
isEditable(): boolean {
return this.editable && this.property.editable;
}
add(): void {
this.values.push({ name: '', value: '' });
}
@@ -55,8 +47,8 @@ export class CardViewKeyValuePairsItemComponent extends BaseCardView<CardViewKey
this.save(true);
}
onBlur(value): void {
if (value.length) {
onBlur(value: any): void {
if (value?.length) {
this.save();
}
}
@@ -26,9 +26,6 @@ import { BaseCardView } from '../base-card-view';
})
export class CardViewMapItemComponent extends BaseCardView<CardViewMapItemModel> {
@Input()
property: CardViewMapItemModel;
@Input()
displayEmpty: boolean = true;
@@ -1,28 +1,30 @@
<ng-container *ngIf="!property.isEmpty() || isEditable()">
<ng-container *ngIf="!property.isEmpty() || isEditable">
<div
[attr.data-automation-id]="'card-select-label-' + property.key"
class="adf-property-label"
[ngClass]="{
'adf-property-value-editable': isEditable,
'adf-property-readonly-value': isReadonlyProperty
}"
>{{ property.label | translate }}</div>
<div class="adf-property-field">
<div
*ngIf="!isEditable()"
class="adf-select-item-padding adf-property-value"
*ngIf="!isEditable"
class="adf-property-value adf-property-read-only"
[attr.data-automation-id]="'select-readonly-value-' + property.key"
data-automation-class="read-only-value">{{ (property.displayValue | async) | translate }}
</div>
<div *ngIf="isEditable()">
<mat-form-field class="adf-select-item-padding-editable adf-property-value">
<div *ngIf="isEditable">
<mat-form-field class="adf-property-value" [ngClass]="{'adf-property-value-editable': isEditable}">
<mat-select
[(value)]="value"
[ngClass]="{ 'adf-property-readonly-value': isReadonlyProperty }"
panelClass="adf-select-filter"
(selectionChange)="onChange($event)"
data-automation-class="select-box"
[aria-label]="property.label | translate"
>
[aria-label]="property.label | translate">
<adf-select-filter-input *ngIf="showInputFilter" (change)="onFilterInputChange($event)"></adf-select-filter-input>
<mat-option *ngIf="showNoneOption()">{{ 'CORE.CARDVIEW.NONE' | translate }}</mat-option>
<mat-option *ngIf="displayNoneOption">{{ 'CORE.CARDVIEW.NONE' | translate }}</mat-option>
<mat-option
*ngFor="let option of list$ | async"
[value]="option.key">
@@ -1,22 +1,36 @@
.mat-form-field-type-mat-select {
width: 100%;
}
.adf-card-view-selectitem {
.adf-property-value {
width: 100%;
}
.adf-select-item-padding {
padding-top: 6px;
padding-bottom: 20px;
}
.adf-select-filter-input {
background: var(--adf-theme-background-card-color);
position: sticky;
top: 0;
left: 0;
right: 0;
z-index: 1;
}
.adf-select-item-padding-editable {
padding-top: 6px;
padding-bottom: 6px;
}
.adf-property-value-editable {
mat-select {
padding: 6px 0 6px 12px;
margin-top: 0;
border-radius: 6px;
.adf-select-filter-input {
background: var(--adf-theme-background-card-color);
position: sticky;
top: 0;
left: 0;
right: 0;
z-index: 1;
.mat-select-value {
color: var(--adf-metadata-action-button-clear-color);
}
}
}
.adf-property-read-only {
padding: 6px 0;
border-bottom: 1px solid var(--adf-metadata-property-panel-border-color);
color: var(--adf-metadata-property-panel-title-color);
}
.adf-property-readonly-value {
color: var(--adf-metadata-property-panel-label-color);
}
}
@@ -108,7 +108,7 @@ describe('CardViewSelectItemComponent', () => {
fixture.detectChanges();
expect(component.value).toEqual('two');
expect(component.isEditable()).toBe(true);
expect(component.isEditable).toBe(true);
const select = await loader.getHarness(MatSelectHarness);
await select.open();
@@ -131,7 +131,7 @@ describe('CardViewSelectItemComponent', () => {
fixture.detectChanges();
expect(component.value).toEqual(2);
expect(component.isEditable()).toBe(true);
expect(component.isEditable).toBe(true);
const select = await loader.getHarness(MatSelectHarness);
await select.open();
@@ -154,7 +154,7 @@ describe('CardViewSelectItemComponent', () => {
component.ngOnChanges();
fixture.detectChanges();
expect(component.isEditable()).toBe(true);
expect(component.isEditable).toBe(true);
const select = await loader.getHarness(MatSelectHarness);
await select.open();
@@ -17,7 +17,7 @@
import { Component, Input, OnChanges, OnDestroy, OnInit, inject, ViewEncapsulation } from '@angular/core';
import { CardViewSelectItemModel } from '../../models/card-view-selectitem.model';
import { BehaviorSubject, combineLatest, Observable, Subject } from 'rxjs';
import { BehaviorSubject, combineLatest, Observable } from 'rxjs';
import { CardViewSelectItemOption } from '../../interfaces/card-view.interfaces';
import { MatSelectChange } from '@angular/material/select';
import { BaseCardView } from '../base-card-view';
@@ -35,8 +35,6 @@ export class CardViewSelectItemComponent extends BaseCardView<CardViewSelectItem
private appConfig = inject(AppConfigService);
static HIDE_FILTER_LIMIT = 5;
@Input() editable: boolean = false;
@Input() options$: Observable<CardViewSelectItemOption<string | number>[]>;
@Input()
@@ -46,11 +44,8 @@ export class CardViewSelectItemComponent extends BaseCardView<CardViewSelectItem
displayEmpty: boolean = true;
value: string | number;
filter$: BehaviorSubject<string> = new BehaviorSubject('');
filter$ = new BehaviorSubject<string>('');
showInputFilter: boolean = false;
private onDestroy$ = new Subject<void>();
list$: Observable<CardViewSelectItemOption<string | number>[]> = null;
ngOnChanges(): void {
@@ -59,8 +54,8 @@ export class CardViewSelectItemComponent extends BaseCardView<CardViewSelectItem
ngOnInit() {
this.getOptions()
.pipe(takeUntil(this.onDestroy$))
.subscribe((options: CardViewSelectItemOption<string>[]) => {
.pipe(takeUntil(this.destroy$))
.subscribe((options) => {
this.showInputFilter = options.length > this.optionsLimit;
});
@@ -71,21 +66,17 @@ export class CardViewSelectItemComponent extends BaseCardView<CardViewSelectItem
this.filter$.next(value.toString());
}
isEditable(): boolean {
return this.editable && this.property.editable;
}
getOptions(): Observable<CardViewSelectItemOption<string | number>[]> {
private getOptions(): Observable<CardViewSelectItemOption<string | number>[]> {
return this.options$ || this.property.options$;
}
getList(): Observable<CardViewSelectItemOption<string | number>[]> {
return combineLatest([this.getOptions(), this.filter$])
.pipe(
map(([items, filter]) => items.filter((item: CardViewSelectItemOption<string>) =>
map(([items, filter]) => items.filter((item) =>
filter ? item.label.toLowerCase().includes(filter.toLowerCase())
: true)),
takeUntil(this.onDestroy$)
takeUntil(this.destroy$)
);
}
@@ -95,17 +86,12 @@ export class CardViewSelectItemComponent extends BaseCardView<CardViewSelectItem
this.property.value = selectedOption;
}
showNoneOption() {
return this.displayNoneOption;
}
get showProperty(): boolean {
return this.displayEmpty || !this.property.isEmpty();
}
ngOnDestroy() {
this.onDestroy$.next();
this.onDestroy$.complete();
super.ngOnDestroy();
}
private get optionsLimit(): number {
@@ -1,70 +1,70 @@
<div [ngSwitch]="templateType">
<div *ngSwitchDefault>
<mat-form-field class="adf-property-field adf-card-textitem-field"
[ngClass]="{ 'adf-property-read-only': !isEditable, 'adf-property-field-has-error mat-form-field-invalid': isEditable && hasErrors }"
[floatLabel]="'never'"
[ngClass]="{
'adf-property-read-only': !isEditable
}"
[floatLabel]="'always'"
appearance="standard">
<mat-label *ngIf="showProperty || isEditable" [attr.data-automation-id]="'card-textitem-label-' + property.key" class="adf-property-label">
<mat-label *ngIf="showProperty || isEditable" [attr.data-automation-id]="'card-textitem-label-' + property.key" class="adf-property-label"
[ngClass]="{
'adf-property-value-editable': editable,
'adf-property-readonly-value': isReadonlyProperty
}">
{{ property.label | translate }}
</mat-label>
<input matInput
*ngIf="!property.multiline"
class="adf-property-value"
title="{{property.label | translate }}"
[placeholder]="property.default"
[attr.aria-label]="property.label | translate"
[formControl]="textInput"
(dblclick)="copyToClipboard(property.displayValue)"
matTooltipShowDelay="1000"
[matTooltip]="'CORE.METADATA.ACTIONS.COPY_TO_CLIPBOARD' | translate"
[matTooltipDisabled]="isEditable"
[attr.data-automation-id]="'card-textitem-value-' + property.key"
(keydown)="undoText($event)">
<textarea matInput
*ngIf="property.multiline"
title="{{property.label | translate }}"
[cdkTextareaAutosize]="true"
[cdkAutosizeMaxRows]="1"
[cdkAutosizeMaxRows]="5"
class="adf-property-value"
[placeholder]="property.default"
[attr.aria-label]="property.label | translate"
[formControl]="textInput"
[attr.data-automation-id]="'card-textitem-value-' + property.key">
</textarea>
<button
*ngIf="isEditable"
matSuffix
class="adf-textitem-clear-icon"
[attr.aria-label]="'CORE.METADATA.ACTIONS.CLEAR' | translate"
(click)="clearValue()">
<mat-icon>cancel</mat-icon>
</button>
<button
*ngIf="isEditable"
matSuffix
class="adf-textitem-edit-icon"
[attr.title]="'CORE.METADATA.ACTIONS.EDIT' | translate">
<mat-icon>mode_edit</mat-icon>
</button>
<input matInput
*ngIf="!property.multiline"
class="adf-property-value"
[ngClass]="{
'adf-property-value-editable': editable,
'adf-property-readonly-value': isReadonlyProperty,
'adf-property-value-has-error': isEditable && hasErrors
}"
title="{{property.label | translate }}"
[placeholder]="property.default"
[attr.aria-label]="property.label | translate"
[formControl]="textInput"
(dblclick)="copyToClipboard(property.displayValue)"
matTooltipShowDelay="1000"
[matTooltip]="'CORE.METADATA.ACTIONS.COPY_TO_CLIPBOARD' | translate"
[matTooltipDisabled]="isEditable"
[attr.data-automation-id]="'card-textitem-value-' + property.key"
(keydown)="undoText($event)">
<textarea matInput
*ngIf="property.multiline"
title="{{property.label | translate }}"
[cdkTextareaAutosize]="true"
[cdkAutosizeMaxRows]="1"
[cdkAutosizeMaxRows]="5"
class="adf-property-value"
[ngClass]="{
'adf-property-value-editable': editable,
'adf-property-readonly-value': isReadonlyProperty
}"
[placeholder]="property.default"
[attr.aria-label]="property.label | translate"
[formControl]="textInput"
[attr.data-automation-id]="'card-textitem-value-' + property.key">
</textarea>
</mat-form-field>
</div>
<div *ngSwitchCase="'chipsTemplate'"
class="adf-property-field adf-textitem-chip-list-container">
<mat-label *ngIf="showLabelForChips" [attr.data-automation-id]="'card-textitem-label-' + property.key" class="adf-property-label">
class="adf-property-field adf-textitem-chip-list-container"
[ngClass]="{'adf-property-read-only': !isEditable}">
<mat-label *ngIf="showLabelForChips" [attr.data-automation-id]="'card-textitem-label-' + property.key" class="adf-property-label"
[ngClass]="{'adf-property-value-editable': editable}">
{{ property.label | translate }}
</mat-label>
<mat-chip-list #chipList
class="adf-textitem-chip-list">
<mat-chip-list #chipList class="adf-textitem-chip-list">
<mat-chip *ngFor="let propertyValue of editedValue; let idx = index"
[removable]="isEditable"
(removed)="removeValueFromList(idx)">
{{ propertyValue }}
<mat-icon *ngIf="isEditable"
matChipRemove>cancel</mat-icon>
<mat-icon *ngIf="isEditable" matChipRemove>cancel</mat-icon>
</mat-chip>
</mat-chip-list>
@@ -74,6 +74,10 @@
[floatLabel]="'never'">
<input matInput
class="adf-property-value"
[ngClass]="{
'adf-property-value-editable': editable,
'adf-property-readonly-value': isReadonlyProperty
}"
title="{{property.label | translate }}"
[placeholder]="editedValue ? '' : property.default | translate"
[attr.aria-label]="property.label | translate"
@@ -81,8 +85,6 @@
[matChipInputAddOnBlur]="true"
(matChipInputTokenEnd)="addValueToList($event)"
[attr.data-automation-id]="'card-textitem-editchipinput-' + property.key">
<mat-icon matSuffix
class="adf-textitem-edit-icon">mode_edit</mat-icon>
</mat-form-field>
</div>
@@ -100,8 +102,13 @@
<input matInput
[type]=property.inputType
class="adf-property-value"
title="{{property.label | translate }}"
[ngClass]="{ 'adf-textitem-clickable-value': !isEditable }"
title="{{ property.label | translate }}"
[ngClass]="{
'adf-property-value-editable': editable,
'adf-textitem-clickable-value': !isEditable,
'adf-property-readonly-value': isReadonlyProperty,
'adf-property-value-has-error': isEditable && hasErrors
}"
[placeholder]="property.default"
[attr.aria-label]="property.label | translate"
[(ngModel)]="editedValue"
@@ -125,9 +132,10 @@
<span class="adf-textitem-default-value">{{ property.default | translate }}</span>
</div>
<mat-error [attr.data-automation-id]="'card-textitem-error-' + property.key"
class="adf-textitem-editable-error"
*ngIf="isEditable && hasErrors">
<mat-error
*ngIf="isEditable && hasErrors"
class="adf-textitem-error"
[attr.data-automation-id]="'card-textitem-error-' + property.key">
<ul>
<li *ngFor="let error of errors">{{ error.message | translate: error }}</li>
</ul>
@@ -1,204 +1,67 @@
.adf {
&-textitem-edit-icon.mat-icon {
font-size: var(--theme-subheading-2-font-size);
width: 16px;
height: 16px;
color: var(--adf-theme-foreground-text-color-025);
.adf-card-view-textitem {
.adf-textitem-error {
font-size: var(--theme-caption-font-size);
padding-top: 6px;
ul {
margin: 0;
padding: 0;
list-style-type: none;
li {
margin: 0;
padding: 0;
}
}
}
&-textitem-action.mat-icon-button {
.adf-textitem-action {
width: 20px;
height: 20px;
line-height: 20px;
color: var(--adf-theme-foreground-text-color-025);
}
&-textitem-action:hover,
&-textitem-action:focus {
color: var(--adf-theme-foreground-text-color);
}
&-update-icon {
padding-left: 13px;
}
&-textitem-readonly {
cursor: pointer !important;
&:hover .adf-textitem-action,
&:focus .adf-textitem-action {
&:hover,
&:focus {
color: var(--adf-theme-foreground-text-color);
}
}
&-textitem-chip-list-container {
margin-bottom: 25px !important;
margin-top: 6px;
.adf-textitem-chip-list-container {
.mat-form-field-label {
margin-top: 6px;
}
}
&-textitem-clickable {
cursor: pointer !important;
.adf-textitem-clickable {
cursor: pointer;
padding-top: 3px;
.adf-textitem-edit-icon.mat-icon {
line-height: 16px;
}
&:hover .adf-textitem-action {
.adf-textitem-action:hover {
color: var(--adf-theme-foreground-text-color);
}
}
&-textitem-clickable-value {
cursor: pointer !important;
color: var(--theme-primary-color) !important;
}
&-textitem-editable {
&-controls {
display: flex;
align-items: center;
mat-icon:not(.adf-button-disabled):hover {
color: var(--adf-theme-foreground-text-color);
cursor: pointer !important;
}
mat-form-field {
width: 100%;
}
input:focus,
textarea:focus {
border: 1px solid var(--theme-accent-color-a200);
}
}
&-error {
font-size: var(--theme-caption-font-size);
padding-top: 6px;
ul {
margin: 0;
padding: 0;
list-style-type: none;
li {
margin: 0;
padding: 0;
}
}
}
}
&-textitem-default-value {
color: var(--adf-theme-foreground-text-color-054);
}
&-textitem-editable .mat-form-field-wrapper {
margin: 0;
padding-bottom: 0;
}
&-textitem-editable .mat-form-field-underline {
display: none;
}
&-textitem-editable .mat-form-field-infix {
padding: 0;
border-top: none;
}
&-textitem-editable .mat-form-field-label-wrapper {
padding-top: 2em;
position: static;
}
&-textitem-editable .mat-form-field-label {
top: 4px;
}
&-textitem-editable .mat-input-element {
font-family: inherit;
position: relative;
padding-top: 6px;
}
&-textitem-editable .mat-input-element:focus {
padding: 5px;
left: -6px;
top: 0;
}
&-textitem-editable input.mat-input-element {
margin-bottom: 2px;
}
&-textitem-editable input.mat-input-element:focus {
margin-bottom: -8px;
}
&-textitem-scroll {
overflow-x: auto;
white-space: nowrap;
display: block;
&::-webkit-scrollbar {
height: 5px;
}
&:hover::-webkit-scrollbar-thumb {
display: block;
background-color: var(--adf-theme-foreground-text-color-025);
border-radius: 2px;
}
}
&-textitem-multiline {
display: block;
}
}
.adf-property-field {
.adf-property-clear-value,
.adf-textitem-edit-value {
width: 30px;
height: 30px;
}
}
.mat-form-field:not(.adf-textitem-chip-list-input) {
.mat-form-field-suffix {
.adf-textitem-edit-icon {
display: block;
}
.adf-textitem-clear-icon {
display: none;
}
.adf-textitem-edit-icon,
.adf-textitem-clear-icon {
background: transparent;
border: none;
&:hover {
.adf-property-field {
.mat-input-element {
color: var(--theme-primary-color);
cursor: pointer;
}
}
}
&:hover {
.adf-textitem-edit-icon {
display: none;
}
.adf-textitem-default-value {
color: var(--adf-theme-foreground-text-color-054);
}
.adf-textitem-clear-icon {
display: block;
}
}
.adf-property-read-only {
border-bottom: 1px solid var(--adf-metadata-property-panel-border-color);
}
.adf-property-readonly-value {
color: var(--adf-metadata-property-panel-label-color);
}
.adf-property-value-editable {
color: var(--adf-metadata-property-panel-title-color);
}
}
@@ -189,24 +189,6 @@ describe('CardViewTextItemComponent', () => {
expect(value).toBe('Lorem ipsum');
});
it('should render the edit icon in case of editable:true', () => {
component.editable = true;
component.property.editable = true;
fixture.detectChanges();
const editIcon = fixture.debugElement.query(By.css('.adf-textitem-edit-icon'));
expect(editIcon).not.toBeNull('Edit icon should be shown');
});
it('should NOT render the edit icon in case of editable:false', async () => {
component.editable = false;
fixture.detectChanges();
await fixture.whenStable();
const editIcon = fixture.debugElement.query(By.css('.adf-textitem-edit-icon'));
expect(editIcon).toBeNull('Edit icon should NOT be shown');
});
it('should NOT render the picker and toggle in case of editable:true but (general) editable:false', async () => {
component.editable = false;
component.property.editable = true;
@@ -313,6 +295,20 @@ describe('CardViewTextItemComponent', () => {
const labelElement = fixture.debugElement.query(By.css(`.adf-property-label`));
expect(labelElement).toBeNull();
});
it('should return true when editable is true, and property.editable is false', () => {
component.editable = true;
component.property.editable = false;
fixture.detectChanges();
expect(component.isReadonlyProperty).toBe(true);
});
it('should return false when editable is false, and property.editable is false', () => {
component.editable = false;
component.property.editable = false;
fixture.detectChanges();
expect(component.isReadonlyProperty).toBe(false);
});
});
describe('clickable', () => {
@@ -491,26 +487,6 @@ describe('CardViewTextItemComponent', () => {
);
});
it('should clear value when clear value icon is clicked', async () => {
spyOn(component, 'update');
component.property.value = 'testValue';
component.property.icon = 'FAKE_ICON';
component.property.clickable = true;
component.property.editable = true;
component.editable = true;
component.property.isValid = () => true;
fixture.detectChanges();
await fixture.whenStable();
fixture.detectChanges();
const clickEl = fixture.debugElement.query(By.css(`.adf-textitem-clear-icon`));
clickEl.triggerEventHandler('click', new MouseEvent('click'));
fixture.detectChanges();
const elementValue = fixture.debugElement.query(By.css(`[data-automation-id="card-textitem-value-${component.property.key}"]`));
expect(elementValue.nativeElement.textContent).toEqual('');
expect(component.update).toHaveBeenCalled();
});
});
describe('Update', () => {
@@ -593,7 +569,7 @@ describe('CardViewTextItemComponent', () => {
component.editable = true;
fixture.detectChanges();
const errorMessage: HTMLElement = fixture.debugElement.nativeElement.querySelector('.adf-textitem-editable-error');
const errorMessage: HTMLElement = fixture.debugElement.nativeElement.querySelector('.adf-textitem-error');
expect(errorMessage.textContent).toBe(expectedErrorMessages[0].message);
});
@@ -602,13 +578,13 @@ describe('CardViewTextItemComponent', () => {
component.editable = true;
fixture.detectChanges();
let errorMessage: HTMLElement = fixture.debugElement.nativeElement.querySelector('.adf-textitem-editable-error');
let errorMessage: HTMLElement = fixture.debugElement.nativeElement.querySelector('.adf-textitem-error');
expect(errorMessage.textContent).toBe(expectedErrorMessages[0].message);
component.editable = false;
fixture.detectChanges();
errorMessage = fixture.debugElement.nativeElement.querySelector('.adf-textitem-editable-error');
errorMessage = fixture.debugElement.nativeElement.querySelector('.adf-textitem-error');
expect(errorMessage).toBeNull();
});
@@ -24,7 +24,6 @@ import { TranslationService } from '../../../translation/translation.service';
import { CardViewItemValidator } from '../../interfaces/card-view-item-validator.interface';
import { UntypedFormControl } from '@angular/forms';
import { debounceTime, takeUntil, filter } from 'rxjs/operators';
import { Subject } from 'rxjs';
export const DEFAULT_SEPARATOR = ', ';
const templateTypes = {
@@ -44,30 +43,24 @@ const templateTypes = {
})
export class CardViewTextItemComponent extends BaseCardView<CardViewTextItemModel> implements OnChanges, OnDestroy {
@Input()
editable: boolean = false;
displayEmpty = true;
@Input()
displayEmpty: boolean = true;
copyToClipboardAction = true;
@Input()
copyToClipboardAction: boolean = true;
@Input()
useChipsForMultiValueProperty: boolean = true;
useChipsForMultiValueProperty = true;
@Input()
multiValueSeparator: string = DEFAULT_SEPARATOR;
@Input()
displayLabelForChips: boolean = false;
displayLabelForChips = false;
editedValue: string | string[];
errors: CardViewItemValidator[];
templateType: string;
textInput: UntypedFormControl = new UntypedFormControl();
private onDestroy$ = new Subject<boolean>();
textInput = new UntypedFormControl();
constructor(private clipboardService: ClipboardService, private translateService: TranslationService, private cd: ChangeDetectorRef) {
super();
@@ -79,7 +72,7 @@ export class CardViewTextItemComponent extends BaseCardView<CardViewTextItemMode
.pipe(
filter((textInputValue) => textInputValue !== this.editedValue && textInputValue !== null),
debounceTime(50),
takeUntil(this.onDestroy$)
takeUntil(this.destroy$)
)
.subscribe((textInputValue) => {
this.editedValue = textInputValue;
@@ -198,8 +191,7 @@ export class CardViewTextItemComponent extends BaseCardView<CardViewTextItemMode
}
ngOnDestroy() {
this.onDestroy$.next(true);
this.onDestroy$.complete();
super.ngOnDestroy();
}
get showProperty(): boolean {
@@ -210,18 +202,10 @@ export class CardViewTextItemComponent extends BaseCardView<CardViewTextItemMode
return this.hasIcon && this.editable;
}
get isEditable(): boolean {
return this.editable && this.property.editable;
}
get isClickable(): boolean {
return this.property.clickable;
}
get hasIcon(): boolean {
return !!this.property.icon;
}
get hasErrors(): boolean {
return !!this.errors?.length;
}
@@ -1,28 +1,110 @@
/* stylelint-disable no-descending-specificity */
.adf-property-list {
background: var(--adf-card-view-background);
border: var(--adf-card-view-border);
border-color: var(--adf-card-view-border-color);
border-radius: var(--adf-card-view-border-radius);
.adf-property {
margin-bottom: 20px;
.adf-property-label {
color: var(--adf-metadata-property-panel-text-color);
display: flex;
padding: 6px 0;
line-height: 20px;
.adf-property-value-padding-top {
margin-top: 6px;
&.adf-property-value-editable {
color: var(--adf-metadata-property-panel-title-color);
}
&.adf-property-readonly-value {
color: var(--adf-metadata-property-panel-label-color);
}
}
.adf-property {
.adf-property-field {
width: 100%;
margin-bottom: -25px;
.mat-form-field-underline {
display: none;
}
.mat-input-element {
text-overflow: ellipsis;
color: var(--adf-metadata-property-panel-title-color);
margin-top: 32px;
padding: 6px 0;
line-height: 20px;
}
.adf-card-view__key-value-pairs__row {
.mat-input-element {
margin-top: 0;
padding-left: 12px;
}
}
.mat-form-field-infix {
display: flex;
border-top-width: 0;
padding: 0;
}
.mat-form-field-flex {
padding-top: 0;
}
.mat-form-field-wrapper {
padding-bottom: 0;
}
.mat-form-field-label {
margin-top: 6px;
padding: 6px 0;
justify-content: center;
display: flex;
flex-direction: column;
height: 20px;
transform: translateY(-1.3437em) scale(1);
}
.mat-form-field-label-wrapper {
padding-top: 0;
top: -1px;
}
}
}
.adf-property-value {
color: var(--adf-metadata-property-panel-text-color);
&.adf-property-value-editable {
color: var(--adf-metadata-property-panel-title-color);
background-color: var(--adf-metadata-buttons-background-color);
border-radius: 6px;
&.mat-input-element {
color: var(--adf-metadata-action-button-clear-color);
padding: 6px 0 6px 12px;
margin: 32px 0 0;
}
}
&.adf-property-readonly-value {
color: var(--adf-metadata-property-panel-label-color);
&.mat-input-element {
color: var(--adf-metadata-property-panel-label-color);
}
}
&-has-error {
border: 1px solid var(--theme-warn-color);
}
&-input {
background-color: var(--adf-metadata-buttons-background-color);
border-radius: 6px;
padding: 6px 0;
margin: 0;
width: 100%;
}
}
}
@@ -186,7 +186,7 @@ export class ThumbnailService {
*
* @returns URL string
*/
public getDefaultMimeTypeIcon(): string {
getDefaultMimeTypeIcon(): string {
return DEFAULT_ICON;
}
}
+4 -5
View File
@@ -193,7 +193,7 @@
},
"METADATA": {
"BASIC": {
"HEADER": "Properties",
"HEADER": "General info",
"NAME": "Name",
"TITLE": "Title",
"DESCRIPTION": "Description",
@@ -401,10 +401,6 @@
"SIDEBAR": {
"THUMBNAILS": {
"PAGE": "Page {{ pageNum }}"
},
"METADATA": {
"MORE_INFORMATION": "More information",
"LESS_INFORMATION": "Less information"
}
},
"PDF_DIALOG": {
@@ -573,5 +569,8 @@
"ADF_DROPDOWN": {
"LOADING": "Loading...",
"SELECTION_ARIA_LABEL": "{{placeholder}} combobox {{selectedOption}}"
},
"INFO_DRAWER": {
"ICON": "Node Icon"
}
}
@@ -1,5 +1,6 @@
<div *ngIf="showHeader" class="adf-info-drawer-layout-header">
<div class="adf-info-drawer-layout-header-title">
<ng-content select="[info-drawer-node-icon]"></ng-content>
<ng-content select="[info-drawer-title]"></ng-content>
</div>
<div class="adf-info-drawer-layout-header-buttons">
@@ -14,44 +14,44 @@ $adf-info-drawer-icon-size-half: 24px !default;
overflow: auto;
width: 100%;
background-color: var(--theme-background-color);
background-color: var(--theme-card-background-color);
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.27);
& .mat-tab-label {
font-weight: bold;
height: 32px;
text-align: left;
text-transform: uppercase;
opacity: 1;
}
&-header {
padding: 13px 0 0 23px;
padding: 8px 0 0 24px;
display: flex;
justify-content: space-between;
align-items: center;
justify-content: space-between;
height: 56px;
margin-bottom: 40px;
&-buttons {
padding-right: 18px;
mat-icon {
cursor: pointer;
}
}
&-title {
width: 197px;
height: 32px;
display: flex;
font-size: $adf-info-drawer-layout-title-font-size;
line-height: 1.6;
letter-spacing: -0.5px;
line-height: 28px;
letter-spacing: 0.15px;
text-align: left;
align-items: start;
color: var(--adf-theme-foreground-text-color-054);
overflow: hidden;
& > div {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
color: var(--adf-metadata-property-panel-title-color);
}
}
}
@@ -1,13 +1,13 @@
<adf-info-drawer-layout [showHeader]="showHeader">
<div role="heading" aria-level="1" *ngIf="title" info-drawer-title>{{ title | translate }}</div>
<img *ngIf="icon" class="adf-info-drawer-icon" alt="{{ 'INFO_DRAWER.ICON' | translate }}" src="{{ icon }}" info-drawer-node-icon>
<div *ngIf="title" role="heading" aria-level="1" title="{{ title | translate }}" info-drawer-title>{{ title | translate }}</div>
<ng-content *ngIf="!title" info-drawer-title select="[info-drawer-title]"></ng-content>
<ng-content info-drawer-buttons select="[info-drawer-buttons]"></ng-content>
<ng-container info-drawer-content *ngIf="showTabLayout(); then tabLayout else singleLayout"></ng-container>
<ng-template #tabLayout>
<mat-tab-group [(selectedIndex)]="selectedIndex" class="adf-info-drawer-tabs" (selectedTabChange)="onTabChange($event)">
<mat-tab-group [(selectedIndex)]="selectedIndex" class="adf-info-drawer-tabs" (selectedTabChange)="onTabChange($event)" [animationDuration]="0">
<mat-tab *ngFor="let contentBlock of contentBlocks"
#tab
[labelClass]="[
@@ -1,12 +1,21 @@
.adf {
&-info-drawer {
display: block;
.adf-metadata-properties-panel {
display: block;
margin: 0;
}
.mat-tab-label {
min-width: 0;
}
& &-layout {
.adf-info-drawer-icon {
display: inline-block;
vertical-align: middle;
margin-right: 10px;
}
&-content {
padding: 0;
@@ -38,16 +47,16 @@
}
.mat-ink-bar {
height: 4px;
height: 2px;
}
.mat-tab-body {
padding: 10px;
overflow-y: hidden;
}
.mat-tab-body-content {
/* stylelint-disable */
overflow: initial;
overflow: auto;
/* stylelint-enable */
}
}
@@ -28,8 +28,8 @@ import { ESCAPE } from '@angular/cdk/keycodes';
describe('InfoDrawerComponent', () => {
let element: HTMLElement;
let component: InfoDrawerComponent;
let fixture: ComponentFixture<InfoDrawerComponent>;
let translateService: TranslateService;
let fixture: ComponentFixture<InfoDrawerComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
@@ -87,24 +87,27 @@ describe('InfoDrawerComponent', () => {
@Component({
template: `
<adf-info-drawer [selectedIndex]="tabIndex" title="Fake Title Custom">
<adf-info-drawer-tab label="Tab1">
</adf-info-drawer-tab>
<adf-info-drawer-tab label="Tab2">
</adf-info-drawer-tab>
<adf-info-drawer-tab label="Tab3" icon="tab-icon">
</adf-info-drawer-tab>
</adf-info-drawer>
`
<adf-info-drawer [selectedIndex]="tabIndex" [icon]="icon" title="Fake Title Custom">
<adf-info-drawer-tab label="Tab1">
</adf-info-drawer-tab>
<adf-info-drawer-tab label="Tab2">
</adf-info-drawer-tab>
<adf-info-drawer-tab label="Tab3" icon="tab-icon">
</adf-info-drawer-tab>
</adf-info-drawer>
`
})
class CustomInfoDrawerComponent extends InfoDrawerComponent {
tabIndex: number;
icon: string;
}
describe('Custom InfoDrawer', () => {
let fixture: ComponentFixture<CustomInfoDrawerComponent>;
let component: CustomInfoDrawerComponent;
let translateService: TranslateService;
const getNodeIcon = () =>
fixture.debugElement.queryAll(By.css('[info-drawer-node-icon]'));
beforeEach(() => {
TestBed.configureTestingModule({
@@ -139,7 +142,7 @@ describe('Custom InfoDrawer', () => {
fixture.detectChanges();
const tab: any = fixture.debugElement.queryAll(By.css('.mat-tab-label-active'));
expect(tab.length).toBe(1);
expect(tab[0].nativeElement.innerText).toContain('TAB1');
expect(tab[0].nativeElement.innerText).toContain('Tab1');
});
it('should select the tab 2 (index 1)', () => {
@@ -147,7 +150,7 @@ describe('Custom InfoDrawer', () => {
fixture.detectChanges();
const tab: any = fixture.debugElement.queryAll(By.css('.mat-tab-label-active'));
expect(tab.length).toBe(1);
expect(tab[0].nativeElement.innerText).toContain('TAB2');
expect(tab[0].nativeElement.innerText).toContain('Tab2');
});
it('should render a tab with icon', () => {
@@ -157,21 +160,33 @@ describe('Custom InfoDrawer', () => {
expect(tab[0].nativeElement.innerText).not.toBe('TAB3');
expect(tab[0].nativeElement.innerText).toContain('tab-icon');
});
it('should render a icon with title', () => {
component.icon = '/assets/images/ft_ic_miscellaneous.svg';
fixture.detectChanges();
const icon = getNodeIcon();
const srcAttribute = icon[0].nativeElement.getAttribute('src');
expect(icon.length).toBe(1);
expect(srcAttribute).toContain('/assets/images/ft_ic_miscellaneous.svg');
});
});
@Component({
template: `
<adf-info-drawer [showHeader]="showHeader" title="Fake Visibility Info Drawer Title">
<adf-info-drawer [showHeader]="showHeader" [icon]="icon" title="Fake Visibility Info Drawer Title">
</adf-info-drawer>
`
`
})
class VisibilityInfoDrawerComponent extends InfoDrawerComponent {
showHeader: boolean;
icon: string;
}
describe('Header visibility InfoDrawer', () => {
let fixture: ComponentFixture<VisibilityInfoDrawerComponent>;
let component: VisibilityInfoDrawerComponent;
const getNodeIcon = () =>
fixture.debugElement.queryAll(By.css('[info-drawer-node-icon]'));
beforeEach(() => {
TestBed.configureTestingModule({
@@ -189,18 +204,24 @@ describe('Header visibility InfoDrawer', () => {
});
it('should show info drawer header by default', () => {
component.icon = '/assets/images/ft_ic_miscellaneous.svg';
fixture.detectChanges();
const title: any = fixture.debugElement.queryAll(By.css('[info-drawer-title]'));
const icon = getNodeIcon();
const srcAttribute = icon[0].nativeElement.getAttribute('src');
expect(title.length).toBe(1);
expect(icon.length).toBe(1);
expect(srcAttribute).toContain('/assets/images/ft_ic_miscellaneous.svg');
expect(title[0].nativeElement.innerText).toBe('Fake Visibility Info Drawer Title');
expect(component.showHeader).toEqual(true);
});
it('should not show info drawer header when showHeader is false', () => {
fixture.detectChanges();
it('should not show info drawer header with icon when showHeader is false', () => {
component.showHeader = false;
fixture.detectChanges();
const title: any = fixture.debugElement.queryAll(By.css('[info-drawer-title]'));
const icon = getNodeIcon();
expect(title.length).toBe(0);
expect(icon.length).toBe(0);
});
});
@@ -47,6 +47,9 @@ export class InfoDrawerComponent {
@Input()
title: string|null = null;
@Input()
icon: string | null = null;
/** The selected index tab. */
@Input()
selectedIndex: number = 0;
@@ -1,3 +1,4 @@
/* stylelint-disable scss/no-global-function-names */
@use './reference-variables' as *;
@use '@angular/material' as mat;
@@ -52,7 +53,6 @@
--adf-identity-user-info-line-height: $adf-ref-line-height,
--adf-identity-user-info-font-size: var(--theme-adf-picture-1-font-size),
--adf-user-info-container-margin-right: $adf-ref-margin-right,
--adf-info-drawer-tab-default-color: mat.get-color-from-palette($accent),
--adf-info-drawer-tab-default-background: mat.get-color-from-palette($background, card),
--adf-info-drawer-tab-default-bottom-line:$adf-ref-tab-bottom-line-default,
@@ -70,8 +70,13 @@
--adf-people-cloud-input-label-focus-color: mat.get-color-from-palette($primary),
--adf-people-cloud-autosuggest-result-active-color: mat.get-color-from-palette($foreground, text),
--adf-people-cloud-autosuggest-result-disabled-color: mat.get-color-from-palette($foreground, secondary-text),
--adf-people-cloud-input-caption-error-color: mat.get-color-from-palette($warn)
--adf-people-cloud-input-caption-error-color: mat.get-color-from-palette($warn),
--adf-metadata-property-panel-border-color: $adf-ref-metadata-property-panel-border-color,
--adf-metadata-buttons-background-color: $adf-ref-metadata-buttons-background-color,
--adf-metadata-action-button-clear-color: $adf-ref-metadata-action-button-clear-color,
--adf-metadata-property-panel-text-color: $adf-ref-metadata-property-panel-text-color,
--adf-metadata-property-panel-label-color: $adf-ref-metadata-property-panel-label-color,
--adf-metadata-property-panel-title-color: $adf-ref-metadata-property-panel-title-color,
);
// propagates SCSS variables into the CSS variables scope
-1
View File
@@ -57,7 +57,6 @@
--adf-theme-foreground-text-color-075: mat.get-color-from-palette($foreground, text, 0.75),
--adf-theme-foreground-text-color-064: mat.get-color-from-palette($foreground, text, 0.64),
--adf-theme-foreground-text-color-054: mat.get-color-from-palette($foreground, text, 0.54),
--adf-theme-foreground-text-color-042: mat.get-color-from-palette($foreground, text, 0.42),
--adf-theme-foreground-text-color-040: mat.get-color-from-palette($foreground, text, 0.4),
--adf-theme-foreground-text-color-027: mat.get-color-from-palette($foreground, text, 0.27),
--adf-theme-foreground-text-color-025: mat.get-color-from-palette($foreground, text, 0.25),
@@ -17,3 +17,9 @@ $adf-ref-line-height: 40px;
$adf-ref-margin-right: 8px;
$adf-ref-tab-bottom-line-default: unset;
$adf-ref-tab-bottom-line-active: unset;
$adf-ref-metadata-property-panel-border-color: rgba(0, 0, 0, 0.12);
$adf-ref-metadata-buttons-background-color: rgba(33, 33, 33, 0.05);
$adf-ref-metadata-action-button-clear-color: rgba(33, 35, 40, 0.698);
$adf-ref-metadata-property-panel-text-color: rgba(33, 35, 40, 0.7);
$adf-ref-metadata-property-panel-label-color: rgba(33, 33, 33, 0.24);
$adf-ref-metadata-property-panel-title-color: rgb(33, 33, 33);
@@ -1,7 +1,18 @@
<adf-content-metadata [displayDefaultProperties]="displayDefaultProperties" [expanded]="expanded" [node]="node"
[preset]="preset" [displayEmpty]="displayEmpty" [editable]="editable" [multi]="multi"
[displayAspect]="displayAspect" [copyToClipboardAction]="copyToClipboardAction"
[useChipsForMultiValueProperty]="useChipsForMultiValueProperty" *ngIf="!loading; else loadingTemplate">
<adf-content-metadata
[displayDefaultProperties]="displayDefaultProperties"
[expanded]="expanded"
[node]="node"
[preset]="preset"
[displayEmpty]="displayEmpty"
[readOnly]="!editable"
[multi]="multi"
[displayAspect]="displayAspect"
[copyToClipboardAction]="copyToClipboardAction"
[useChipsForMultiValueProperty]="useChipsForMultiValueProperty"
[displayTags]="false"
[displayCategories]="false"
*ngIf="!loading; else loadingTemplate"
>
</adf-content-metadata>
<ng-template #loadingTemplate>
<div class="adf-properties-viewer-wrapper-loading">
@@ -222,17 +222,6 @@ describe('TaskHeaderCloudComponent', () => {
const loading = fixture.debugElement.query(By.css('.adf-task-header-loading'));
expect(loading).toBeTruthy();
});
it('should not render edit icon if the task in assigned state and assingned user is different from current logged-in user', () => {
isTaskEditableSpy.and.returnValue(false);
fixture.detectChanges();
const priorityEditIcon = fixture.debugElement.query(By.css(`[data-automation-id="card-textitem-edit-icon-priority"]`));
const descriptionEditIcon = fixture.debugElement.query(By.css(`[data-automation-id="card-textitem-edit-icon-description"]`));
const dueDateEditIcon = fixture.debugElement.query(By.css(`[data-automation-id="datepickertoggle-dueDate"]`));
expect(priorityEditIcon).toBeNull();
expect(descriptionEditIcon).toBeNull();
expect(dueDateEditIcon).toBeNull();
});
});
describe('Task with parentTaskId', () => {
@@ -328,9 +317,7 @@ describe('TaskHeaderCloudComponent', () => {
it('should render edit icon if the task in assigned state and assingee should be current user', () => {
fixture.detectChanges();
const descriptionEditIcon = fixture.debugElement.query(By.css(`[data-automation-id="header-description"] [class*="adf-textitem-edit-icon"]`));
const dueDateEditIcon = fixture.debugElement.query(By.css(`[data-automation-id="datepickertoggle-dueDate"]`));
expect(descriptionEditIcon).not.toBeNull();
expect(dueDateEditIcon).not.toBeNull();
});
@@ -372,16 +359,6 @@ describe('TaskHeaderCloudComponent', () => {
const value = fixture.debugElement.query(By.css(`[data-automation-id="card-textitem-clickable-icon-assignee"]`));
expect(value).toBeNull();
});
it('should not render edit icon if the task in created state and not assigned', () => {
fixture.detectChanges();
const priorityEditIcon = fixture.debugElement.query(By.css(`[data-automation-id="card-textitem-edit-icon-priority"]`));
const descriptionEditIcon = fixture.debugElement.query(By.css(`[data-automation-id="card-textitem-edit-icon-description"]`));
const dueDateEditIcon = fixture.debugElement.query(By.css(`[data-automation-id="datepickertoggle-dueDate"]`));
expect(priorityEditIcon).toBeNull();
expect(descriptionEditIcon).toBeNull();
expect(dueDateEditIcon).toBeNull();
});
});
describe('Completed Task', () => {
@@ -404,16 +381,6 @@ describe('TaskHeaderCloudComponent', () => {
const value = fixture.debugElement.query(By.css(`[data-automation-id="card-textitem-clickable-icon-assignee"]`));
expect(value).toBeNull('Edit icon should NOT be shown');
});
it('should not render edit icon if the task in completed state', () => {
fixture.detectChanges();
const priorityEditIcon = fixture.debugElement.query(By.css(`[data-automation-id="card-textitem-edit-icon-priority"]`));
const descriptionEditIcon = fixture.debugElement.query(By.css(`[data-automation-id="card-textitem-edit-icon-description"]`));
const dueDateEditIcon = fixture.debugElement.query(By.css(`[data-automation-id="datepickertoggle-dueDate"]`));
expect(priorityEditIcon).toBeNull();
expect(descriptionEditIcon).toBeNull();
expect(dueDateEditIcon).toBeNull();
});
});
describe('Suspended Task', () => {
@@ -436,16 +403,6 @@ describe('TaskHeaderCloudComponent', () => {
const value = fixture.debugElement.query(By.css(`[data-automation-id="card-textitem-clickable-icon-assignee"]`));
expect(value).toBeNull();
});
it('should not render edit icon if the task in suspended state', () => {
fixture.detectChanges();
const priorityEditIcon = fixture.debugElement.query(By.css(`[data-automation-id="card-textitem-edit-icon-priority"]`));
const descriptionEditIcon = fixture.debugElement.query(By.css(`[data-automation-id="card-textitem-edit-icon-description"]`));
const dueDateEditIcon = fixture.debugElement.query(By.css(`[data-automation-id="datepickertoggle-dueDate"]`));
expect(priorityEditIcon).toBeNull();
expect(descriptionEditIcon).toBeNull();
expect(dueDateEditIcon).toBeNull();
});
});
describe('Task with candidates', () => {
@@ -25,7 +25,7 @@ export class CardTextItemPage {
clearButton = 'button[data-automation-id*="card-textitem-reset"]';
field = '[data-automation-id*="card-textitem-value"]';
labelLocator = '[data-automation-id*="card-textitem-label"]';
errorMessage = '.adf-textitem-editable-error';
errorMessage = '.adf-textitem-error';
clickableElement = '.adf-textitem-clickable';
readOnlyField = '.adf-property-read-only';