[ACS-4721] tags are not displayed properly when resizing browser (#8424)

* ACS-4721 Partial fix issue with resizing window

* ACS-4721 Fixed issue for overlapping view more button with tags, skip calculation if anyone clicked on view more button

* ACS-4721 Unit tests

* ACS-4721 Fixed lint issues

* ACS-4721 Small correction in tests

* ACS-4721 Fixed unit tests

* ACS-4721 Fixed unit tests

* ACS-4721 Resolved issues after opening viewer

* ACS-4721 Formatting

* ACS-4721 Fixed lint issue
This commit is contained in:
AleksanderSklorz 2023-03-30 14:16:04 +02:00 committed by GitHub
parent 3e2683e06b
commit 431b3db08a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 176 additions and 42 deletions

View File

@ -239,6 +239,29 @@ describe('ContentMetadataComponent', () => {
expect(tagService.assignTagsToNode).toHaveBeenCalledWith(node.id, [tag1, tag2]); expect(tagService.assignTagsToNode).toHaveBeenCalledWith(node.id, [tag1, tag2]);
})); }));
it('should call getTagsByNodeId on TagService on save click', fakeAsync( () => {
component.editable = true;
component.displayTags = 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));
const tagPaging = mockTagPaging();
const getTagsByNodeIdSpy = spyOn(tagService, 'getTagsByNodeId').and.returnValue(of(tagPaging));
component.ngOnInit();
spyOn(tagService, 'removeTag').and.returnValue(of(undefined));
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();
expect(tagService.getTagsByNodeId).toHaveBeenCalledWith(node.id);
}));
it('should throw error on unsuccessful save', fakeAsync((done) => { it('should throw error on unsuccessful save', fakeAsync((done) => {
const logService: LogService = TestBed.inject(LogService); const logService: LogService = TestBed.inject(LogService);
component.editable = true; component.editable = true;

View File

@ -270,6 +270,9 @@ export class ContentMetadataComponent implements OnChanges, OnInit, OnDestroy {
this.revertChanges(); this.revertChanges();
Object.assign(this.node, result.updatedNode); Object.assign(this.node, result.updatedNode);
this.nodesApiService.nodeUpdated.next(this.node); this.nodesApiService.nodeUpdated.next(this.node);
if (Object.keys(result).length > 1 && this.displayTags) {
this.loadTagsForNode(this.node.id);
}
} }
this._saving = false; this._saving = false;
}); });

View File

@ -10,7 +10,8 @@
</mat-chip> </mat-chip>
</mat-chip-list> </mat-chip-list>
<button mat-button <button mat-button
*ngIf="limitTagsDisplayed" [hidden]="!limitTagsDisplayed"
[style.left.px]="viewMoreButtonLeftOffset"
class="adf-view-more-button" class="adf-view-more-button"
[class.adf-hidden-btn]="!calculationsDone" [class.adf-hidden-btn]="!calculationsDone"
(click)="displayAllTags($event)" (click)="displayAllTags($event)"

View File

@ -4,13 +4,22 @@
flex-direction: row; flex-direction: row;
width: inherit; width: inherit;
&.adf-flex-column {
flex-direction: column;
}
.adf-view-more-button { .adf-view-more-button {
margin: 4px; margin: 4px;
color: var(--adf-theme-foreground-text-color-054); color: var(--adf-theme-foreground-text-color-054);
position: absolute;
&[hidden] {
visibility: hidden;
}
}
&.adf-flex-column {
flex-direction: column;
.adf-view-more-button {
position: relative;
}
} }
.adf-full-width { .adf-full-width {

View File

@ -44,15 +44,19 @@ describe('TagNodeList', () => {
}, },
{ {
entry: {tag: 'test3', id: 'fb4213c0-729d-466c-9a6c-ee2e937273bf'} entry: {tag: 'test3', id: 'fb4213c0-729d-466c-9a6c-ee2e937273bf'}
},
{
entry: {tag: 'test4', id: 'as4213c0-729d-466c-9a6c-ee2e937273as'}
} }
] ]
} }
}; };
let component: any; let component: TagNodeListComponent;
let fixture: ComponentFixture<TagNodeListComponent>; let fixture: ComponentFixture<TagNodeListComponent>;
let element: HTMLElement; let element: HTMLElement;
let tagService: TagService; let tagService: TagService;
let resizeCallback: ResizeObserverCallback;
function findViewMoreButton(): HTMLButtonElement { function findViewMoreButton(): HTMLButtonElement {
return element.querySelector('.adf-view-more-button'); return element.querySelector('.adf-view-more-button');
@ -70,6 +74,7 @@ describe('TagNodeList', () => {
}); });
beforeEach(() => { beforeEach(() => {
const resizeObserverSpy = spyOn(window, 'ResizeObserver').and.callThrough();
fixture = TestBed.createComponent(TagNodeListComponent); fixture = TestBed.createComponent(TagNodeListComponent);
tagService = TestBed.inject(TagService); tagService = TestBed.inject(TagService);
@ -79,6 +84,7 @@ describe('TagNodeList', () => {
component = fixture.componentInstance; component = fixture.componentInstance;
component.nodeId = 'fake-node-id'; component.nodeId = 'fake-node-id';
fixture.detectChanges(); fixture.detectChanges();
resizeCallback = resizeObserverSpy.calls.mostRecent().args[0];
}); });
describe('Rendering tests', () => { describe('Rendering tests', () => {
@ -137,24 +143,29 @@ describe('TagNodeList', () => {
fixture.detectChanges(); fixture.detectChanges();
await fixture.whenStable(); await fixture.whenStable();
expect(findViewMoreButton()).toBeNull(); expect(findViewMoreButton().hidden).toBeTrue();
expect(findTagChips().length).toBe(3); expect(findTagChips()).toHaveSize(4);
}); });
}); });
describe('Limit tags display', () => { describe('Limit tags display', () => {
let initialEntries: TagEntry[];
async function renderTags(entries?: TagEntry[]): Promise<any> { async function renderTags(entries?: TagEntry[]): Promise<any> {
if (entries) { dataTag.list.entries = entries || initialEntries;
dataTag.list.entries = entries;
}
component.tagsEntries = dataTag.list.entries; component.tagsEntries = dataTag.list.entries;
fixture.detectChanges(); fixture.detectChanges();
await fixture.whenStable(); await fixture.whenStable();
} }
beforeAll(() => {
initialEntries = dataTag.list.entries;
});
beforeEach(() => { beforeEach(() => {
component.limitTagsDisplayed = true; component.limitTagsDisplayed = true;
element.style.maxWidth = '200px'; component.ngOnInit();
element.style.maxWidth = '309px';
}); });
it('should render view more button when limiting is enabled', async () => { it('should render view more button when limiting is enabled', async () => {
@ -162,8 +173,8 @@ describe('TagNodeList', () => {
component.ngOnChanges(); component.ngOnChanges();
fixture.detectChanges(); fixture.detectChanges();
await fixture.whenStable(); await fixture.whenStable();
expect(findViewMoreButton()).not.toBeNull(); expect(findViewMoreButton().hidden).toBeFalse();
expect(findTagChips().length).toBe(component.tagsEntries.length); expect(findTagChips()).toHaveSize(component.tagsEntries.length);
}); });
it('should not render view more button when limiting is enabled and all tags fits into container', async () => { it('should not render view more button when limiting is enabled and all tags fits into container', async () => {
@ -174,8 +185,8 @@ describe('TagNodeList', () => {
fixture.detectChanges(); fixture.detectChanges();
await fixture.whenStable(); await fixture.whenStable();
expect(findViewMoreButton()).toBeNull(); expect(findViewMoreButton().hidden).toBeTrue();
expect(findTagChips().length).toBe(3); expect(findTagChips()).toHaveSize(4);
}); });
it('should display all tags when view more button is clicked', async () => { it('should display all tags when view more button is clicked', async () => {
@ -190,8 +201,8 @@ describe('TagNodeList', () => {
await fixture.whenStable(); await fixture.whenStable();
viewMoreButton = findViewMoreButton(); viewMoreButton = findViewMoreButton();
expect(viewMoreButton).toBeNull(); expect(viewMoreButton.hidden).toBeTrue();
expect(findTagChips().length).toBe(3); expect(findTagChips()).toHaveSize(4);
}); });
it('should not render view more button when tag takes more than one line and there are no more tags', async () => { it('should not render view more button when tag takes more than one line and there are no more tags', async () => {
@ -204,8 +215,8 @@ describe('TagNodeList', () => {
component.ngOnChanges(); component.ngOnChanges();
fixture.detectChanges(); fixture.detectChanges();
await fixture.whenStable(); await fixture.whenStable();
expect(findViewMoreButton()).toBeNull(); expect(findViewMoreButton().hidden).toBeTrue();
expect(findTagChips().length).toBe(component.tagsEntries.length); expect(findTagChips()).toHaveSize(component.tagsEntries.length);
}); });
it('should render view more button when tag takes more than one line and there are more tags', async () => { it('should render view more button when tag takes more than one line and there are more tags', async () => {
@ -223,8 +234,53 @@ describe('TagNodeList', () => {
component.ngOnChanges(); component.ngOnChanges();
fixture.detectChanges(); fixture.detectChanges();
await fixture.whenStable(); await fixture.whenStable();
expect(findViewMoreButton()).not.toBeNull(); const viewMoreButton = findViewMoreButton();
expect(findTagChips().length).toBe(component.tagsEntries.length); expect(viewMoreButton.hidden).toBeFalse();
expect(viewMoreButton.style.left).toBe('0px');
expect(findTagChips()).toHaveSize(component.tagsEntries.length);
});
it('should not render view more button when there is enough space after resizing', async () => {
await renderTags();
component.ngOnChanges();
fixture.detectChanges();
await fixture.whenStable();
element.style.maxWidth = '800px';
resizeCallback([], null);
fixture.detectChanges();
const viewMoreButton = findViewMoreButton();
expect(viewMoreButton.hidden).toBeTrue();
expect(findTagChips()).toHaveSize(4);
});
it('should render view more button when there is not enough space after resizing', async () => {
await renderTags();
element.style.maxWidth = '800px';
component.ngOnChanges();
fixture.detectChanges();
await fixture.whenStable();
element.style.maxWidth = '309px';
resizeCallback([], null);
fixture.detectChanges();
expect(findViewMoreButton().hidden).toBeFalse();
expect(findTagChips()).toHaveSize(component.tagsEntries.length);
});
it('should not render view more button again resizing when there is not enough space if user requested to see all tags', async () => {
await renderTags();
component.ngOnChanges();
fixture.detectChanges();
await fixture.whenStable();
const viewMoreButton = findViewMoreButton();
viewMoreButton.click();
fixture.detectChanges();
element.style.maxWidth = '309px';
resizeCallback([], null);
fixture.detectChanges();
expect(viewMoreButton.hidden).toBeTrue();
expect(findTagChips()).toHaveSize(4);
}); });
}); });
}); });

View File

@ -15,7 +15,22 @@
* limitations under the License. * limitations under the License.
*/ */
import { Component, EventEmitter, Input, OnChanges, Output, ViewEncapsulation, OnDestroy, OnInit, ViewChild, ElementRef, ViewChildren, QueryList } from '@angular/core'; import {
Component,
EventEmitter,
Input,
OnChanges,
Output,
ViewEncapsulation,
OnDestroy,
OnInit,
ViewChild,
ElementRef,
ViewChildren,
QueryList,
ChangeDetectorRef,
AfterViewInit
} from '@angular/core';
import { TagService } from './services/tag.service'; import { TagService } from './services/tag.service';
import { TagEntry } from '@alfresco/js-api'; import { TagEntry } from '@alfresco/js-api';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
@ -33,7 +48,7 @@ import { MatChip } from '@angular/material/chips';
styleUrls: ['./tag-node-list.component.scss'], styleUrls: ['./tag-node-list.component.scss'],
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None
}) })
export class TagNodeListComponent implements OnChanges, OnDestroy, OnInit { export class TagNodeListComponent implements OnChanges, OnDestroy, OnInit, AfterViewInit {
/* eslint no-underscore-dangle: ["error", { "allow": ["_elementRef"] }]*/ /* eslint no-underscore-dangle: ["error", { "allow": ["_elementRef"] }]*/
/** The identifier of a node. */ /** The identifier of a node. */
@Input() @Input()
@ -57,19 +72,29 @@ export class TagNodeListComponent implements OnChanges, OnDestroy, OnInit {
calculationsDone = false; calculationsDone = false;
columnFlexDirection = false; columnFlexDirection = false;
undisplayedTagsCount = 0; undisplayedTagsCount = 0;
viewMoreButtonLeftOffset: number;
/** Emitted when a tag is selected. */ /** Emitted when a tag is selected. */
@Output() @Output()
results = new EventEmitter(); results = new EventEmitter();
private onDestroy$ = new Subject<boolean>(); private onDestroy$ = new Subject<boolean>();
private initialLimitTagsDisplayed: boolean;
private initialTagsEntries: TagEntry[] = [];
private viewMoreButtonLeftOffsetBeforeFlexDirection: number;
private requestedDisplayingAllTags = false;
private resizeObserver = new ResizeObserver(() => {
this.calculateTagsToDisplay();
this.changeDetectorRef.detectChanges();
});
/** /**
* Constructor * Constructor
* *
* @param tagService * @param tagService
* @param changeDetectorRef
*/ */
constructor(private tagService: TagService) { constructor(private tagService: TagService, private changeDetectorRef: ChangeDetectorRef) {
} }
ngOnChanges() { ngOnChanges() {
@ -77,6 +102,7 @@ export class TagNodeListComponent implements OnChanges, OnDestroy, OnInit {
} }
ngOnInit() { ngOnInit() {
this.initialLimitTagsDisplayed = this.limitTagsDisplayed;
this.tagService.refresh this.tagService.refresh
.pipe(takeUntil(this.onDestroy$)) .pipe(takeUntil(this.onDestroy$))
.subscribe(() => this.refreshTag()); .subscribe(() => this.refreshTag());
@ -90,15 +116,21 @@ export class TagNodeListComponent implements OnChanges, OnDestroy, OnInit {
}); });
} }
ngAfterViewInit() {
this.resizeObserver.observe(this.containerView.nativeElement);
}
ngOnDestroy() { ngOnDestroy() {
this.onDestroy$.next(true); this.onDestroy$.next(true);
this.onDestroy$.complete(); this.onDestroy$.complete();
this.resizeObserver.unobserve(this.containerView.nativeElement);
} }
refreshTag() { refreshTag() {
if (this.nodeId) { if (this.nodeId) {
this.tagService.getTagsByNodeId(this.nodeId).subscribe((tagPaging) => { this.tagService.getTagsByNodeId(this.nodeId).subscribe((tagPaging) => {
this.tagsEntries = tagPaging.list.entries; this.tagsEntries = tagPaging.list.entries;
this.initialTagsEntries = tagPaging.list.entries;
this.results.emit(this.tagsEntries); this.results.emit(this.tagsEntries);
}); });
} }
@ -114,30 +146,40 @@ export class TagNodeListComponent implements OnChanges, OnDestroy, OnInit {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
this.limitTagsDisplayed = false; this.limitTagsDisplayed = false;
this.requestedDisplayingAllTags = true;
this.resizeObserver.unobserve(this.containerView.nativeElement);
this.refreshTag(); this.refreshTag();
} }
private calculateTagsToDisplay() { private calculateTagsToDisplay() {
if (!this.requestedDisplayingAllTags) {
this.tagsEntries = this.initialTagsEntries;
this.changeDetectorRef.detectChanges();
this.undisplayedTagsCount = 0;
let tagsToDisplay = 1; let tagsToDisplay = 1;
const containerWidth: number = this.containerView.nativeElement.clientWidth; const containerWidth: number = this.containerView.nativeElement.clientWidth;
const viewMoreBtnWidth: number = this.containerView.nativeElement.children[1].offsetWidth; const viewMoreBtnWidth: number = this.containerView.nativeElement.children[1].offsetWidth;
const tagChipMargin = this.getTagChipMargin(this.tagChips.get(0)); const firstTag = this.tagChips.get(0);
const tagChipsWidth: number = this.tagChips.reduce((acc, val, index) => { const tagChipMargin = firstTag ? this.getTagChipMargin(this.tagChips.get(0)) : 0;
if (containerWidth - viewMoreBtnWidth > acc + val._elementRef.nativeElement.offsetWidth) { const tagChipsWidth: number = this.tagChips.reduce((width, val, index) => {
width += val._elementRef.nativeElement.offsetWidth + tagChipMargin;
if (containerWidth - viewMoreBtnWidth > width) {
tagsToDisplay = index + 1; tagsToDisplay = index + 1;
this.viewMoreButtonLeftOffset = width;
this.viewMoreButtonLeftOffsetBeforeFlexDirection = width;
} }
return acc + val._elementRef.nativeElement.offsetWidth + tagChipMargin; return width;
}, 0); }, 0);
if ((containerWidth - tagChipsWidth) <= 0) { if ((containerWidth - tagChipsWidth) <= 0) {
this.columnFlexDirection = tagsToDisplay === 1 && (containerWidth < (this.tagChips.get(0)._elementRef.nativeElement.offsetWidth + viewMoreBtnWidth)); this.columnFlexDirection = tagsToDisplay === 1 && (containerWidth < (this.tagChips.get(0)._elementRef.nativeElement.offsetWidth + viewMoreBtnWidth));
this.undisplayedTagsCount = this.tagsEntries.length - tagsToDisplay; this.undisplayedTagsCount = this.tagsEntries.length - tagsToDisplay;
this.tagsEntries = this.tagsEntries.slice(0, tagsToDisplay); this.tagsEntries = this.tagsEntries.slice(0, tagsToDisplay);
} }
if (!this.undisplayedTagsCount) { this.limitTagsDisplayed = this.undisplayedTagsCount ? this.initialLimitTagsDisplayed : false;
this.limitTagsDisplayed = false; this.viewMoreButtonLeftOffset = this.columnFlexDirection ? 0 : this.viewMoreButtonLeftOffsetBeforeFlexDirection;
}
this.calculationsDone = true; this.calculationsDone = true;
} }
}
private getTagChipMargin(chip: MatChip): number { private getTagChipMargin(chip: MatChip): number {
const tagChipStyles = window.getComputedStyle(chip._elementRef.nativeElement); const tagChipStyles = window.getComputedStyle(chip._elementRef.nativeElement);