From 96c94988c17cfffb7ad1e1c1077a27ab81a3be24 Mon Sep 17 00:00:00 2001 From: Thomas Hunter Date: Thu, 15 Apr 2021 13:57:26 +0100 Subject: [PATCH] [AAE-4428] Add selected file counter to attach file widget (#6881) * [AAE-4428] Add selected file counter to attach file widget * [AAE-4428] Add getSelectedCount method * [AAE-4428] Add unit tests * [AAE-4428] Changed so TranslationService injection is not necessary * [AAE-4428] Added unit tests to check the element has been injected * spacing * [AAE-4428] Remove unnecessary check in unit test * [AAE-4428] Add node counter directive * [ci skip] Fix linting issues * Remove changing the selection to the nabigated node * Revert removing selection on folder loaded, add unit test * Added documentation Co-authored-by: adomi --- .../directives/node-counter.directive.md | 28 ++++++++++ ...content-node-selector-panel.component.html | 2 +- ...tent-node-selector-panel.component.spec.ts | 28 ++++++++++ .../content-node-selector-panel.component.ts | 4 ++ .../content-node-selector.component.html | 2 +- .../content-node-selector.component.scss | 4 ++ .../content-node-selector.component.spec.ts | 12 ++++ .../content-node-selector.component.ts | 4 ++ .../content-node-selector.module.ts | 4 +- .../directives/content-directive.module.ts | 12 +++- .../directives/node-counter.directive.spec.ts | 56 +++++++++++++++++++ .../lib/directives/node-counter.directive.ts | 55 ++++++++++++++++++ lib/content-services/src/lib/i18n/en.json | 3 + 13 files changed, 208 insertions(+), 6 deletions(-) create mode 100644 docs/content-services/directives/node-counter.directive.md create mode 100644 lib/content-services/src/lib/directives/node-counter.directive.spec.ts create mode 100644 lib/content-services/src/lib/directives/node-counter.directive.ts diff --git a/docs/content-services/directives/node-counter.directive.md b/docs/content-services/directives/node-counter.directive.md new file mode 100644 index 0000000000..8854408239 --- /dev/null +++ b/docs/content-services/directives/node-counter.directive.md @@ -0,0 +1,28 @@ +--- +Title: Node Counter directive +Added: v4.4.0 +Status: Active +Last reviewed: 2021-04-15 +--- + +# [Node Counter directive](../../../lib/content-services/src/lib/directives/node-counter.directive.ts "Defined in node-counter.directive.ts") + +Appends a counter to an element. + +## Basic Usage + +```html + + + ... + + +``` + +## Class members + +### Properties + +| Name | Type | Default value | Description | +| ---- | ---- | ------------- | ----------- | +| counter | `number` | | Number of nodes selected. | diff --git a/lib/content-services/src/lib/content-node-selector/content-node-selector-panel.component.html b/lib/content-services/src/lib/content-node-selector/content-node-selector-panel.component.html index c1eb6b0a10..bc3260a1ef 100644 --- a/lib/content-services/src/lib/content-node-selector/content-node-selector-panel.component.html +++ b/lib/content-services/src/lib/content-node-selector/content-node-selector-panel.component.html @@ -43,7 +43,7 @@
- + {{ 'NODE_SELECTOR.SEARCH_RESULTS' | translate }} diff --git a/lib/content-services/src/lib/content-node-selector/content-node-selector-panel.component.spec.ts b/lib/content-services/src/lib/content-node-selector/content-node-selector-panel.component.spec.ts index aaada7dee0..22548813c7 100644 --- a/lib/content-services/src/lib/content-node-selector/content-node-selector-panel.component.spec.ts +++ b/lib/content-services/src/lib/content-node-selector/content-node-selector-panel.component.spec.ts @@ -372,6 +372,7 @@ describe('ContentNodeSelectorPanelComponent', () => { beforeEach(() => { const documentListService = TestBed.inject(DocumentListService); const expectedDefaultFolderNode = { entry: { path: { elements: [] } } }; + component.isSelectionValid = (node: Node) => node.isFile; spyOn(documentListService, 'getFolderNode').and.returnValue(of(expectedDefaultFolderNode)); spyOn(documentListService, 'getFolder').and.returnValue(of({ @@ -1026,6 +1027,14 @@ describe('ContentNodeSelectorPanelComponent', () => { spyOn(sitesService, 'getSites').and.returnValue(of({ list: { entries: [] } })); }); + it('should the selection become the currently navigated folder when the folder loads (Acts as destination for cases like copy action)', () => { + const fakeFolderNode = { id: 'fakeNodeId', isFolder: true }; + component.documentList.folderNode = fakeFolderNode; + component.onFolderLoaded(); + + expect(component.chosenNode).toEqual([fakeFolderNode]); + }); + describe('in the case when isSelectionValid is a custom function for checking permissions,', () => { beforeEach(() => { @@ -1360,5 +1369,24 @@ describe('ContentNodeSelectorPanelComponent', () => { expect(component.sorting).toEqual(['createdAt', 'desc']); }); }); + + describe('Selected nodes counter', () => { + it('should getSelectedCount return 0 by default', () => { + expect(component.getSelectedCount()).toBe(0); + }); + + it('should getSelectedCount return 1 when node is selected', () => { + component.onCurrentSelection([{ entry: new Node({ id: 'fake', isFile: true }) }]); + + expect(component.getSelectedCount()).toBe(1); + }); + + it('should getSelectedCount return 0 when the chosen nodes are reset', () => { + component.onCurrentSelection([{ entry: new Node({ id: 'fake', isFile: true }) }]); + component.resetChosenNode(); + + expect(component.getSelectedCount()).toBe(0); + }); + }); }); }); diff --git a/lib/content-services/src/lib/content-node-selector/content-node-selector-panel.component.ts b/lib/content-services/src/lib/content-node-selector/content-node-selector-panel.component.ts index 8cdb25bc54..0e8c43eb47 100644 --- a/lib/content-services/src/lib/content-node-selector/content-node-selector-panel.component.ts +++ b/lib/content-services/src/lib/content-node-selector/content-node-selector-panel.component.ts @@ -279,6 +279,10 @@ export class ContentNodeSelectorPanelComponent implements OnInit, OnDestroy { return this._chosenNode; } + getSelectedCount(): number { + return this.chosenNode?.length || 0; + } + ngOnInit() { this.searchInput.valueChanges .pipe( diff --git a/lib/content-services/src/lib/content-node-selector/content-node-selector.component.html b/lib/content-services/src/lib/content-node-selector/content-node-selector.component.html index e26e0d6650..58b0fb33e2 100644 --- a/lib/content-services/src/lib/content-node-selector/content-node-selector.component.html +++ b/lib/content-services/src/lib/content-node-selector/content-node-selector.component.html @@ -35,7 +35,7 @@ - + { expect(emptyListTemplate).toBeNull(); }); }); + + describe('Selected nodes counter', () => { + it('should getSelectedCount return 0 by default', () => { + expect(component.getSelectedCount()).toBe(0); + }); + + it('should getSelectedCount return 1 when a node is selected', () => { + component.onSelect([new Node({ id: 'fake' })]); + + expect(component.getSelectedCount()).toBe(1); + }); + }); }); diff --git a/lib/content-services/src/lib/content-node-selector/content-node-selector.component.ts b/lib/content-services/src/lib/content-node-selector/content-node-selector.component.ts index 7d7f28eeec..a964528335 100644 --- a/lib/content-services/src/lib/content-node-selector/content-node-selector.component.ts +++ b/lib/content-services/src/lib/content-node-selector/content-node-selector.component.ts @@ -107,6 +107,10 @@ export class ContentNodeSelectorComponent implements OnInit { return this.translation.instant(`NODE_SELECTOR.${action}_ITEM`, { name: this.translation.instant(name) }); } + getSelectedCount(): number { + return this.chosenNode?.length || 0; + } + isMultipleSelection(): boolean { return this.data.selectionMode === 'multiple'; } diff --git a/lib/content-services/src/lib/content-node-selector/content-node-selector.module.ts b/lib/content-services/src/lib/content-node-selector/content-node-selector.module.ts index af80442ddf..096921b482 100644 --- a/lib/content-services/src/lib/content-node-selector/content-node-selector.module.ts +++ b/lib/content-services/src/lib/content-node-selector/content-node-selector.module.ts @@ -30,6 +30,7 @@ import { DocumentListModule } from '../document-list/document-list.module'; import { NameLocationCellComponent } from './name-location-cell/name-location-cell.component'; import { UploadModule } from '../upload/upload.module'; import { SearchQueryBuilderService } from '../search/search-query-builder.service'; +import { ContentDirectiveModule } from '../directives/content-directive.module'; @NgModule({ imports: [ @@ -42,7 +43,8 @@ import { SearchQueryBuilderService } from '../search/search-query-builder.servic BreadcrumbModule, SearchModule, DocumentListModule, - UploadModule + UploadModule, + ContentDirectiveModule ], exports: [ ContentNodeSelectorPanelComponent, diff --git a/lib/content-services/src/lib/directives/content-directive.module.ts b/lib/content-services/src/lib/directives/content-directive.module.ts index d2d20339cb..3ed6d233ab 100644 --- a/lib/content-services/src/lib/directives/content-directive.module.ts +++ b/lib/content-services/src/lib/directives/content-directive.module.ts @@ -18,19 +18,25 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { MaterialModule } from '../material.module'; +import { TranslateModule } from '@ngx-translate/core'; import { NodeLockDirective } from './node-lock.directive'; +import { NodeCounterComponent, NodeCounterDirective } from './node-counter.directive'; @NgModule({ imports: [ CommonModule, - MaterialModule + MaterialModule, + TranslateModule ], declarations: [ - NodeLockDirective + NodeLockDirective, + NodeCounterDirective, + NodeCounterComponent ], exports: [ - NodeLockDirective + NodeLockDirective, + NodeCounterDirective ] }) export class ContentDirectiveModule { diff --git a/lib/content-services/src/lib/directives/node-counter.directive.spec.ts b/lib/content-services/src/lib/directives/node-counter.directive.spec.ts new file mode 100644 index 0000000000..231fbbf0a5 --- /dev/null +++ b/lib/content-services/src/lib/directives/node-counter.directive.spec.ts @@ -0,0 +1,56 @@ +/*! + * @license + * Copyright 2019 Alfresco Software, Ltd. + * + * 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 { Component } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { NodeCounterDirective, NodeCounterComponent } from './node-counter.directive'; +import { By } from '@angular/platform-browser'; +import { TranslateModule } from '@ngx-translate/core'; + +@Component({ + template: `
` +}) +class TestComponent { + count: number = 0; +} + +describe('NodeCounterDirective', () => { + let fixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + TranslateModule.forRoot() + ], + declarations: [ + NodeCounterDirective, + NodeCounterComponent, + TestComponent + ] + }); + fixture = TestBed.createComponent(TestComponent); + fixture.detectChanges(); + }); + + it('should display the counter component', () => { + fixture.whenStable().then(() => { + const counterElement = fixture.debugElement.query(By.css('adf-node-counter')); + expect(counterElement).not.toBeNull(); + expect(counterElement.nativeElement.innerText).toBe('NODE_COUNTER.SELECTED_COUNT'); + }); + }); +}); diff --git a/lib/content-services/src/lib/directives/node-counter.directive.ts b/lib/content-services/src/lib/directives/node-counter.directive.ts new file mode 100644 index 0000000000..60ed150d28 --- /dev/null +++ b/lib/content-services/src/lib/directives/node-counter.directive.ts @@ -0,0 +1,55 @@ +/*! + * @license + * Copyright 2019 Alfresco Software, Ltd. + * + * 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 { Directive, Input, Component, OnInit, OnChanges, ComponentFactoryResolver, ViewContainerRef } from '@angular/core'; + +@Directive({ + selector: '[adf-node-counter]' +}) +export class NodeCounterDirective implements OnInit, OnChanges { + @Input('adf-node-counter') + counter: number; + + componentRef: NodeCounterComponent; + + constructor( + private resolver: ComponentFactoryResolver, + public viewContainerRef: ViewContainerRef + ) {} + + ngOnInit() { + const componentFactory = this.resolver.resolveComponentFactory(NodeCounterComponent); + this.componentRef = this.viewContainerRef.createComponent(componentFactory).instance; + this.componentRef.counter = this.counter; + } + + ngOnChanges() { + if (this.componentRef) { + this.componentRef.counter = this.counter; + } + } +} + +@Component({ + selector: 'adf-node-counter', + template: ` +
{{ 'NODE_COUNTER.SELECTED_COUNT' | translate: { count: counter } }}
+ ` +}) +export class NodeCounterComponent { + counter: number; +} diff --git a/lib/content-services/src/lib/i18n/en.json b/lib/content-services/src/lib/i18n/en.json index 87c50b74c3..e8a90cfa47 100644 --- a/lib/content-services/src/lib/i18n/en.json +++ b/lib/content-services/src/lib/i18n/en.json @@ -474,5 +474,8 @@ "OVER-TABLE-MESSAGE" : "Select property aspects", "SELECTED": "Selected" } + }, + "NODE_COUNTER": { + "SELECTED_COUNT": "{{ count }} selected" } }