[ACS-5483] group details view general info and list of assigned users (#9329)

* ACS-5483 Added possibility to load and update group

* ACS-5483 Implemented unsaved changes dialog

* ACS-5483 Removed console log

* ACS-5483 Made dynamic chip list reusable

* ACS-5483 Fix for more than one row chips

* ACS-5483 Fix for pagination

* ACS-5483 Added some fixes

* ACS-5483 Fixed displaying tags for node

* ACS-5483 Renamed css classes

* ACS-5483 Fixed resizing when chips have pagination

* ACS-5483 Clearing code

* ACS-5483 Documentation for dynamic chip list component

* ACS-5483 Documentation for unsaved changes dialog and guard

* ACS-5483 Documentation for group service

* ACS-5483 Unit tests for GroupService

* ACS-5483 Unit tests for dynamic chip list component

* ACS-5483 Changed fdescribe to describe

* ACS-5483 Unit tests for tag node list component

* ACS-5483 Unit tests for unsaved changes dialog component

* ACS-5483 Unit tests for unsaved changes guard

* ACS-5483 Added description field to group models

* ACS-5483 Correction for updating with description

* ACS-5483 Fixed lint issues

* ACS-5483 Addressed PR comments

* ACS-5483 Reduced complexity

* ACS-5483 Reduced complexity

* ACS-5483 Addressed PR comments
This commit is contained in:
AleksanderSklorz
2024-02-15 13:02:24 +01:00
committed by GitHub
parent 8363d09e79
commit 213a73fc36
40 changed files with 1587 additions and 426 deletions

View File

@@ -0,0 +1,134 @@
/*!
* @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 { TestBed } from '@angular/core/testing';
import { ContentTestingModule } from '../../testing/content.testing.module';
import { GroupService } from '@alfresco/adf-content-services';
import { ContentIncludeQuery, GroupEntry } from '@alfresco/js-api';
describe('GroupService', () => {
let service: GroupService;
let group: GroupEntry;
let returnedGroup: GroupEntry;
let opts: ContentIncludeQuery;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [ContentTestingModule]
});
service = TestBed.inject(GroupService);
group = {
entry: {
id: 'some id',
displayName: 'some name',
description: 'some description'
}
};
returnedGroup = JSON.parse(JSON.stringify(group));
opts = {
include: ['description']
};
});
describe('getGroup', () => {
it('should return group returned by GroupsApi', (done) => {
spyOn(service.groupsApi, 'getGroup').and.returnValue(Promise.resolve(returnedGroup));
service.getGroup(group.entry.id, opts).subscribe((groupEntry) => {
expect(groupEntry).toBe(returnedGroup);
expect(service.groupsApi.getGroup).toHaveBeenCalledWith(group.entry.id, {
include: ['description']
});
done();
});
});
it('should return group returned by GroupsApi when description is not supplied', (done) => {
returnedGroup.entry.description = undefined;
spyOn(service.groupsApi, 'getGroup').and.returnValue(Promise.resolve(returnedGroup));
service.getGroup(group.entry.id, opts).subscribe((groupEntry) => {
expect(groupEntry).toEqual({
entry: {
id: returnedGroup.entry.id,
displayName: returnedGroup.entry.displayName,
description: ''
}
});
expect(service.groupsApi.getGroup).toHaveBeenCalledWith(group.entry.id, {
include: ['description']
});
done();
});
});
});
describe('updateGroup', () => {
it('should return updated Group', (done) => {
spyOn(service.groupsApi, 'updateGroup').and.returnValue(Promise.resolve(returnedGroup));
service.updateGroup(group.entry, opts).subscribe((groupEntry) => {
expect(groupEntry).toEqual(returnedGroup);
expect(service.groupsApi.updateGroup).toHaveBeenCalledWith(group.entry.id, {
displayName: group.entry.displayName,
description: group.entry.description
}, {
include: ['description']
});
done();
});
});
it('should return updated Group when description is not supplied', (done) => {
returnedGroup.entry.description = undefined;
spyOn(service.groupsApi, 'updateGroup').and.returnValue(Promise.resolve(returnedGroup));
service.updateGroup(group.entry, opts).subscribe((groupEntry) => {
expect(groupEntry).toEqual({
entry: {
id: returnedGroup.entry.id,
displayName: returnedGroup.entry.displayName,
description: ''
}
});
expect(service.groupsApi.updateGroup).toHaveBeenCalledWith(group.entry.id, {
displayName: group.entry.displayName,
description: group.entry.description
}, {
include: ['description']
});
done();
});
});
it('should allow to update only description', (done) => {
spyOn(service.groupsApi, 'updateGroup').and.returnValue(Promise.resolve(returnedGroup));
group.entry.displayName = undefined;
service.updateGroup(group.entry, opts).subscribe((groupEntry) => {
expect(groupEntry).toEqual(returnedGroup);
expect(service.groupsApi.updateGroup).toHaveBeenCalledWith(group.entry.id, {
displayName: group.entry.displayName,
description: group.entry.description
}, {
include: ['description']
});
done();
});
});
});
});

View File

@@ -16,8 +16,10 @@
*/
import { Injectable } from '@angular/core';
import { GroupEntry, GroupsApi } from '@alfresco/js-api';
import { ContentIncludeQuery, Group, GroupEntry, GroupsApi } from '@alfresco/js-api';
import { AlfrescoApiService } from '@alfresco/adf-core';
import { from, Observable } from 'rxjs';
import { map } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
@@ -48,4 +50,35 @@ export class GroupService {
return accumulator;
}
}
/**
* Returns group for specified id.
*
* @param id id of group to return.
* @param opts additional query parameters
* @returns Observable<GroupEntry> group for specified id.
*/
getGroup(id: string, opts?: ContentIncludeQuery): Observable<GroupEntry> {
return from(this.groupsApi.getGroup(id, opts)).pipe(map((group) => {
group.entry.description ||= '';
return group;
}));
}
/**
* Updates specified group.
*
* @param group group to update.
* @param opts additional query parameters
* @returns Observable<GroupEntry> updated group.
*/
updateGroup(group: Group, opts?: ContentIncludeQuery): Observable<GroupEntry> {
return from(this.groupsApi.updateGroup(group.id, {
displayName: group.displayName,
description: group.description
}, opts)).pipe(map((updatedGroup) => {
updatedGroup.entry.description ||= '';
return updatedGroup;
}));
}
}

View File

@@ -1,20 +1,7 @@
<div class="adf-tag-node-list" [class.adf-flex-column]="limitTagsDisplayed && (!calculationsDone || columnFlexDirection)" #nodeListContainer>
<mat-chip-list [class.adf-full-width]="limitTagsDisplayed && !calculationsDone" role="listbox" [attr.aria-label]="'METADATA.BASIC.TAGS' | translate">
<mat-chip class="adf-tag-chips"
*ngFor="let currentEntry of tagsEntries; let idx = index"
(removed)="removeTag(currentEntry.entry.id)">
<span id="tag_name_{{idx}}">{{currentEntry.entry.tag}}</span>
<mat-icon *ngIf="showDelete" id="tag_chips_delete_{{currentEntry.entry.tag}}"
class="adf-tag-chips-delete-icon" matChipRemove>cancel
</mat-icon>
</mat-chip>
</mat-chip-list>
<button mat-button
[hidden]="!limitTagsDisplayed"
[style.left.px]="viewMoreButtonLeftOffset"
class="adf-view-more-button"
[class.adf-hidden-btn]="!calculationsDone"
(click)="displayAllTags($event)"
>{{ 'TAG_NODE_LIST.VIEW_MORE' | translate: { count: undisplayedTagsCount} }}
</button>
</div>
<adf-dynamic-chip-list
[chips]="tagChips"
[limitChipsDisplayed]="limitTagsDisplayed"
[showDelete]="showDelete"
(displayNext)="refreshTag()"
(removedChip)="removeTag($event)">
</adf-dynamic-chip-list>

View File

@@ -1,61 +0,0 @@
.adf-tag-node-list {
display: flex;
align-items: center;
flex-direction: row;
width: inherit;
.adf-view-more-button {
margin: 4px;
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 {
width: 100%;
}
.adf-hidden-btn {
visibility: hidden;
}
.adf-tag-chips {
color: var(--theme-primary-color-default-contrast);
background-color: var(--theme-primary-color);
height: auto;
word-break: break-word;
}
.adf-tag-chips-delete {
overflow: visible;
cursor: pointer;
height: 17px;
width: 20px;
float: right;
border: 0;
background: none;
padding: 0;
margin: -1px 0 0 10px;
}
.adf-tag-chips-delete-icon {
font-size: var(--theme-title-font-size);
background-repeat: no-repeat;
display: inline-block;
fill: currentcolor;
height: 20px;
width: 20px;
color: var(--theme-primary-color-default-contrast) !important;
}
}

View File

@@ -18,62 +18,16 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { TagNodeListComponent } from './tag-node-list.component';
import { TagService } from './services/tag.service';
import { of } from 'rxjs';
import { Observable, of, Subject } from 'rxjs';
import { ContentTestingModule } from '../testing/content.testing.module';
import { Tag, TagEntry, TagPaging } from '@alfresco/js-api';
import { DynamicChipListComponent } from '@alfresco/adf-core';
import { By } from '@angular/platform-browser';
import { TranslateModule } from '@ngx-translate/core';
import { TagEntry } from '@alfresco/js-api';
describe('TagNodeList', () => {
const dataTag = {
list: {
pagination: {
count: 3,
hasMoreItems: false,
totalItems: 3,
skipCount: 0,
maxItems: 100
},
entries: [
{
entry: {tag: 'test1', id: '0ee933fa-57fc-4587-8a77-b787e814f1d2'}
},
{
entry: {tag: 'test2', id: 'fcb92659-1f10-41b4-9b17-851b72a3b597'}
},
{
entry: {tag: 'test3', id: 'fb4213c0-729d-466c-9a6c-ee2e937273bf'}
},
{
entry: {tag: 'test4', id: 'as4213c0-729d-466c-9a6c-ee2e937273as'}
}
]
}
};
let component: TagNodeListComponent;
let fixture: ComponentFixture<TagNodeListComponent>;
let element: HTMLElement;
let tagService: TagService;
let resizeCallback: ResizeObserverCallback;
/**
* Find 'More' button
*
* @returns native element
*/
function findViewMoreButton(): HTMLButtonElement {
return element.querySelector('.adf-view-more-button');
}
/**
* Get the tag chips
*
* @returns native element list
*/
function findTagChips(): NodeListOf<Element> {
return element.querySelectorAll('.adf-tag-chips');
}
beforeEach(() => {
TestBed.configureTestingModule({
@@ -82,218 +36,176 @@ describe('TagNodeList', () => {
ContentTestingModule
]
});
const resizeObserverSpy = spyOn(window, 'ResizeObserver').and.callThrough();
fixture = TestBed.createComponent(TagNodeListComponent);
tagService = TestBed.inject(TagService);
spyOn(tagService, 'getTagsByNodeId').and.returnValue(of(dataTag));
element = fixture.nativeElement;
component = fixture.componentInstance;
component.nodeId = 'fake-node-id';
fixture.detectChanges();
resizeCallback = resizeObserverSpy.calls.mostRecent().args[0];
});
describe('Rendering tests', () => {
it('Tag list relative a single node should be rendered', async () => {
component.ngOnChanges();
fixture.detectChanges();
await fixture.whenStable();
expect(element.querySelector('#tag_name_0').innerHTML).toBe('test1');
expect(element.querySelector('#tag_name_1').innerHTML).toBe('test2');
expect(element.querySelector('#tag_name_2').innerHTML).toBe('test3');
expect(element.querySelector('#tag_chips_delete_test1')).not.toBe(null);
expect(element.querySelector('#tag_chips_delete_test2')).not.toBe(null);
expect(element.querySelector('#tag_chips_delete_test3')).not.toBe(null);
});
it('Tag list click on delete button should delete the tag', async () => {
spyOn(tagService, 'removeTag').and.returnValue(of(undefined));
component.ngOnChanges();
fixture.detectChanges();
await fixture.whenStable();
const deleteButton: any = element.querySelector('#tag_chips_delete_test1');
deleteButton.click();
expect(tagService.removeTag).toHaveBeenCalledWith('fake-node-id', '0ee933fa-57fc-4587-8a77-b787e814f1d2');
});
it('Should not show the delete tag button if showDelete is false', async () => {
component.showDelete = false;
component.ngOnChanges();
fixture.detectChanges();
await fixture.whenStable();
const deleteButton: any = element.querySelector('#tag_chips_delete_test1');
expect(deleteButton).toBeNull();
});
it('Should show the delete tag button if showDelete is true', async () => {
component.showDelete = true;
component.ngOnChanges();
fixture.detectChanges();
await fixture.whenStable();
const deleteButton: any = element.querySelector('#tag_chips_delete_test1');
expect(deleteButton).not.toBeNull();
});
it('should not render view more button by default', async () => {
component.ngOnChanges();
fixture.detectChanges();
await fixture.whenStable();
expect(findViewMoreButton().hidden).toBeTrue();
expect(findTagChips()).toHaveSize(4);
});
});
describe('Limit tags display', () => {
let initialEntries: TagEntry[];
/**
* Render tags
*
* @param entries tags to render
*/
async function renderTags(entries?: TagEntry[]) {
dataTag.list.entries = entries || initialEntries;
component.tagsEntries = dataTag.list.entries;
fixture.detectChanges();
await fixture.whenStable();
}
beforeAll(() => {
initialEntries = dataTag.list.entries;
});
describe('DynamicChipListComponent', () => {
let dynamicChipListComponent: DynamicChipListComponent;
let tagService: TagService;
let getTagsByNodeIdSpy: jasmine.Spy<(nodeId: string) => Observable<TagPaging>>;
beforeEach(() => {
fixture.detectChanges();
dynamicChipListComponent = fixture.debugElement.query(By.directive(DynamicChipListComponent)).componentInstance;
tagService = TestBed.inject(TagService);
getTagsByNodeIdSpy = spyOn(tagService, 'getTagsByNodeId').and.returnValue(new Subject<TagPaging>());
});
it('should have assigned limitChipsDisplayed to true if true is passed to limitTagsDisplayed', () => {
component.limitTagsDisplayed = true;
component.ngOnInit();
element.style.maxWidth = '309px';
fixture.detectChanges();
expect(dynamicChipListComponent.limitChipsDisplayed).toBeTrue();
});
it('should render view more button when limiting is enabled', async () => {
await renderTags();
component.ngOnChanges();
it('should have assigned limitChipsDisplayed to false if false is passed to limitTagsDisplayed', () => {
component.limitTagsDisplayed = true;
fixture.detectChanges();
await fixture.whenStable();
expect(findViewMoreButton().hidden).toBeFalse();
expect(findTagChips()).toHaveSize(component.tagsEntries.length);
component.limitTagsDisplayed = false;
fixture.detectChanges();
expect(dynamicChipListComponent.limitChipsDisplayed).toBeFalse();
});
it('should not render view more button when limiting is enabled and all tags fits into container', async () => {
await renderTags();
element.style.maxWidth = '800px';
component.ngOnChanges();
fixture.detectChanges();
await fixture.whenStable();
expect(findViewMoreButton().hidden).toBeTrue();
expect(findTagChips()).toHaveSize(4);
it('should have assigned limitChipsDisplayed to false by default if limitTagsDisplayed is not delivered', () => {
expect(dynamicChipListComponent.limitChipsDisplayed).toBeFalse();
});
it('should display all tags when view more button is clicked', async () => {
await renderTags();
component.ngOnChanges();
it('should have assigned showDelete to true if true is passed to showDelete of tag node list', () => {
component.showDelete = false;
fixture.detectChanges();
await fixture.whenStable();
component.showDelete = true;
let viewMoreButton = findViewMoreButton();
viewMoreButton.click();
fixture.detectChanges();
await fixture.whenStable();
viewMoreButton = findViewMoreButton();
expect(viewMoreButton.hidden).toBeTrue();
expect(findTagChips()).toHaveSize(4);
expect(dynamicChipListComponent.showDelete).toBeTrue();
});
it('should not render view more button when tag takes more than one line and there are no more tags', async () => {
await renderTags([{
entry: {
tag: 'VeryLongTag VeryLongTag VeryLongTag VeryLongTag VeryLongTag VeryLongTag VeryLongTag VeryLongTag',
id: '0ee933fa-57fc-4587-8a77-b787e814f1d2'
}
}]);
component.ngOnChanges();
it('should have assigned showDelete to false if false is passed to showDelete of tag node list', () => {
component.showDelete = false;
fixture.detectChanges();
await fixture.whenStable();
expect(findViewMoreButton().hidden).toBeTrue();
expect(findTagChips()).toHaveSize(component.tagsEntries.length);
expect(dynamicChipListComponent.showDelete).toBeFalse();
});
it('should render view more button when tag takes more than one line and there are more tags', async () => {
await renderTags([{
entry: {
tag: 'VeryLongTag VeryLongTag VeryLongTag VeryLongTag VeryLongTag VeryLongTag VeryLongTag VeryLongTag',
id: '0ee933fa-57fc-4587-8a77-b787e814f1d2'
}
}, {
entry: {
tag: 'Some other tag',
id: '0ee933fa-57fc-4587-8a77-b787e814f1d3'
}
}]);
component.ngOnChanges();
fixture.detectChanges();
await fixture.whenStable();
const viewMoreButton = findViewMoreButton();
expect(viewMoreButton.hidden).toBeFalse();
expect(viewMoreButton.style.left).toBe('0px');
expect(findTagChips()).toHaveSize(component.tagsEntries.length);
it('should have assigned showDelete to true by default if showDelete of tag node list is not delivered', () => {
expect(dynamicChipListComponent.showDelete).toBeTrue();
});
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);
});
describe('Assigning chips', () => {
let tagEntry: Tag;
let tagEntries: TagEntry[];
it('should render view more button when there is not enough space after resizing', async () => {
await renderTags();
element.style.maxWidth = '800px';
beforeEach(() => {
tagEntry = {
id: 'some id',
tag: 'some tag'
};
tagEntries = [{
entry: tagEntry
}];
getTagsByNodeIdSpy.and.returnValue(of({
list: {
entries: tagEntries,
pagination: undefined
}
}));
spyOn(component.results, 'emit');
});
component.ngOnChanges();
fixture.detectChanges();
await fixture.whenStable();
element.style.maxWidth = '309px';
resizeCallback([], null);
fixture.detectChanges();
it('should have assigned correct chips initially', () => {
component.nodeId = 'some node id';
tagService.refresh.emit();
expect(findViewMoreButton().hidden).toBeFalse();
expect(findTagChips()).toHaveSize(component.tagsEntries.length);
});
fixture.detectChanges();
expect(dynamicChipListComponent.chips).toEqual([{
id: tagEntry.id,
name: tagEntry.tag
}]);
expect(tagService.getTagsByNodeId).toHaveBeenCalledWith(component.nodeId);
expect(component.results.emit).toHaveBeenCalledWith(tagEntries);
});
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);
it('should not have assigned chips initially if nodeId is not specified', () => {
tagService.refresh.emit();
fixture.detectChanges();
expect(dynamicChipListComponent.chips).toEqual([]);
expect(tagService.getTagsByNodeId).not.toHaveBeenCalled();
expect(component.results.emit).not.toHaveBeenCalled();
});
it('should have assigned correct chips when ngOnChanges is called', () => {
component.nodeId = 'some node id';
component.ngOnChanges();
fixture.detectChanges();
expect(dynamicChipListComponent.chips).toEqual([{
id: tagEntry.id,
name: tagEntry.tag
}]);
expect(tagService.getTagsByNodeId).toHaveBeenCalledWith(component.nodeId);
expect(component.results.emit).toHaveBeenCalledWith(tagEntries);
});
it('should not have assigned chips when ngOnChanges is called and nodeId is not specified', () => {
component.ngOnChanges();
fixture.detectChanges();
expect(dynamicChipListComponent.chips).toEqual([]);
expect(tagService.getTagsByNodeId).not.toHaveBeenCalled();
expect(component.results.emit).not.toHaveBeenCalled();
});
it('should have assigned correct chips when displayNext event from DynamicChipList is triggered', () => {
component.nodeId = 'some node id';
dynamicChipListComponent.displayNext.emit();
fixture.detectChanges();
expect(dynamicChipListComponent.chips).toEqual([{
id: tagEntry.id,
name: tagEntry.tag
}]);
expect(tagService.getTagsByNodeId).toHaveBeenCalledWith(component.nodeId);
expect(component.results.emit).toHaveBeenCalledWith(tagEntries);
});
it('should not have assigned chips when displayNext event from DynamicChipList is triggered and nodeId is not specified', () => {
dynamicChipListComponent.displayNext.emit();
fixture.detectChanges();
expect(dynamicChipListComponent.chips).toEqual([]);
expect(tagService.getTagsByNodeId).not.toHaveBeenCalled();
expect(component.results.emit).not.toHaveBeenCalled();
});
it('should have assigned correct chips when removeTag event from DynamicChipList is triggered', () => {
component.nodeId = 'some node id';
spyOn(tagService, 'removeTag').and.returnValue(of(undefined));
const tag = 'some tag';
dynamicChipListComponent.removedChip.emit(tag);
fixture.detectChanges();
expect(dynamicChipListComponent.chips).toEqual([{
id: tagEntry.id,
name: tagEntry.tag
}]);
expect(tagService.removeTag).toHaveBeenCalledWith(component.nodeId, tag);
expect(tagService.getTagsByNodeId).toHaveBeenCalledWith(component.nodeId);
expect(component.results.emit).toHaveBeenCalledWith(tagEntries);
});
it('should not have assigned chips when removeTag event from DynamicChipList is triggered and nodeId is not specified', () => {
spyOn(tagService, 'removeTag').and.returnValue(of(undefined));
const tag = 'some tag';
dynamicChipListComponent.removedChip.emit(tag);
fixture.detectChanges();
expect(dynamicChipListComponent.chips).toEqual([]);
expect(tagService.removeTag).toHaveBeenCalledWith(component.nodeId, tag);
expect(tagService.getTagsByNodeId).not.toHaveBeenCalled();
expect(component.results.emit).not.toHaveBeenCalled();
});
});
});
});

View File

@@ -15,27 +15,12 @@
* limitations under the License.
*/
import {
Component,
EventEmitter,
Input,
OnChanges,
Output,
ViewEncapsulation,
OnDestroy,
OnInit,
ViewChild,
ElementRef,
ViewChildren,
QueryList,
ChangeDetectorRef,
AfterViewInit
} from '@angular/core';
import { Component, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output, ViewEncapsulation } from '@angular/core';
import { TagService } from './services/tag.service';
import { TagEntry } from '@alfresco/js-api';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { MatChip } from '@angular/material/chips';
import { Chip } from '@alfresco/adf-core';
/**
*
@@ -45,11 +30,9 @@ import { MatChip } from '@angular/material/chips';
@Component({
selector: 'adf-tag-node-list',
templateUrl: './tag-node-list.component.html',
styleUrls: ['./tag-node-list.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class TagNodeListComponent implements OnChanges, OnDestroy, OnInit, AfterViewInit {
/* eslint no-underscore-dangle: ["error", { "allow": ["_elementRef"] }]*/
export class TagNodeListComponent implements OnChanges, OnDestroy, OnInit {
/** The identifier of a node. */
@Input()
nodeId: string;
@@ -62,121 +45,49 @@ export class TagNodeListComponent implements OnChanges, OnDestroy, OnInit, After
@Input()
limitTagsDisplayed = false;
@ViewChild('nodeListContainer')
containerView: ElementRef;
@ViewChildren(MatChip)
tagChips: QueryList<MatChip>;
tagsEntries: TagEntry[] = [];
calculationsDone = false;
columnFlexDirection = false;
undisplayedTagsCount = 0;
viewMoreButtonLeftOffset: number;
/** Emitted when a tag is selected. */
@Output()
results = new EventEmitter();
results = new EventEmitter<TagEntry[]>();
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();
});
private _tagChips: Chip[] = [];
constructor(private tagService: TagService, private changeDetectorRef: ChangeDetectorRef) {
get tagChips(): Chip[] {
return this._tagChips;
}
ngOnChanges() {
constructor(private tagService: TagService) {}
ngOnChanges(): void {
this.refreshTag();
}
ngOnInit() {
this.initialLimitTagsDisplayed = this.limitTagsDisplayed;
ngOnInit(): void {
this.tagService.refresh
.pipe(takeUntil(this.onDestroy$))
.subscribe(() => this.refreshTag());
this.results
.pipe(takeUntil(this.onDestroy$))
.subscribe(() => {
if (this.limitTagsDisplayed && this.tagsEntries.length > 0) {
this.calculateTagsToDisplay();
}
});
}
ngAfterViewInit() {
this.resizeObserver.observe(this.containerView.nativeElement);
}
ngOnDestroy() {
ngOnDestroy(): void {
this.onDestroy$.next(true);
this.onDestroy$.complete();
this.resizeObserver.unobserve(this.containerView.nativeElement);
}
refreshTag() {
refreshTag(): void {
if (this.nodeId) {
this.tagService.getTagsByNodeId(this.nodeId).subscribe((tagPaging) => {
this.tagsEntries = tagPaging.list.entries;
this.initialTagsEntries = tagPaging.list.entries;
this.results.emit(this.tagsEntries);
this._tagChips = tagPaging.list.entries.map((tag) => ({
id: tag.entry.id,
name: tag.entry.tag
}));
this.results.emit(tagPaging.list.entries);
});
}
}
removeTag(tag: string) {
removeTag(tag: string): void {
this.tagService.removeTag(this.nodeId, tag).subscribe(() => {
this.refreshTag();
});
}
displayAllTags(event: Event): void {
event.preventDefault();
event.stopPropagation();
this.limitTagsDisplayed = false;
this.requestedDisplayingAllTags = true;
this.resizeObserver.unobserve(this.containerView.nativeElement);
this.refreshTag();
}
private calculateTagsToDisplay() {
if (!this.requestedDisplayingAllTags) {
this.tagsEntries = this.initialTagsEntries;
this.changeDetectorRef.detectChanges();
this.undisplayedTagsCount = 0;
let tagsToDisplay = 1;
const containerWidth: number = this.containerView.nativeElement.clientWidth;
const viewMoreBtnWidth: number = this.containerView.nativeElement.children[1].offsetWidth;
const firstTag = this.tagChips.get(0);
const tagChipMargin = firstTag ? this.getTagChipMargin(this.tagChips.get(0)) : 0;
const tagChipsWidth: number = this.tagChips.reduce((width, val, index) => {
width += val._elementRef.nativeElement.offsetWidth + tagChipMargin;
if (containerWidth - viewMoreBtnWidth > width) {
tagsToDisplay = index + 1;
this.viewMoreButtonLeftOffset = width;
this.viewMoreButtonLeftOffsetBeforeFlexDirection = width;
}
return width;
}, 0);
if ((containerWidth - tagChipsWidth) <= 0) {
this.columnFlexDirection = tagsToDisplay === 1 && (containerWidth < (this.tagChips.get(0)._elementRef.nativeElement.offsetWidth + viewMoreBtnWidth));
this.undisplayedTagsCount = this.tagsEntries.length - tagsToDisplay;
this.tagsEntries = this.tagsEntries.slice(0, tagsToDisplay);
}
this.limitTagsDisplayed = this.undisplayedTagsCount ? this.initialLimitTagsDisplayed : false;
this.viewMoreButtonLeftOffset = this.columnFlexDirection ? 0 : this.viewMoreButtonLeftOffsetBeforeFlexDirection;
this.calculationsDone = true;
}
}
private getTagChipMargin(chip: MatChip): number {
const tagChipStyles = window.getComputedStyle(chip._elementRef.nativeElement);
return parseInt(tagChipStyles.marginLeft, 10) + parseInt(tagChipStyles.marginRight, 10);
}
}