[ADF-2065] Refactored Content node selector component (#2778)

* [ADF-2065] created dialog component for content node selector

* [ADF-2065] removing SiteModel from site dropdown to use SitePaging model of js-api

* [ADF-2065] - removed site model and updated documentation

* [ADF-2065] fixed test for site component

* [ADF-2065] refactored content node selector and created content node selector dialog

* [ADF-2065] fixed test on site-api service

* [ADF-2065] added a new content node dialog service to centralise the logic for content node dialog

* [ADF-2065] start adding test for node-actions service|

* [ADF-2065] added test for node-actions service

* [ADF-2065] added test for node action service

* [ADF-2065] renamed components to keep backward compatibility

* [ADF-2065] added input just for backward compatibility

* [ADF-2065] added some changes for backward compatibility and updated documentation

* [ADF-2065] updated documentation for content node selector
This commit is contained in:
Vito
2017-12-14 12:36:08 +00:00
committed by Eugenio Romano
parent d489dd175a
commit 9afa632148
35 changed files with 2028 additions and 1526 deletions

View File

@@ -0,0 +1,84 @@
/*!
* @license
* Copyright 2016 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.
*/
/*tslint:disable: ban*/
import { async, TestBed } from '@angular/core/testing';
import { MinimalNodeEntryEntity } from 'alfresco-js-api';
import { AppConfigService } from '@alfresco/adf-core';
import { DocumentListService } from '../document-list/services/document-list.service';
import { ContentNodeDialogService } from './content-node-dialog.service';
import { MatDialog } from '@angular/material';
const fakeNode: MinimalNodeEntryEntity = <MinimalNodeEntryEntity> {
id: 'fake',
name: 'fake-name'
};
describe('ContentNodeDialogService', () => {
let service: ContentNodeDialogService;
// let documentListService: DocumentListService;
// let contentDialogService: ContentNodeDialogService;
let materialDialog: MatDialog;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [],
providers: [
ContentNodeDialogService,
DocumentListService,
MatDialog
]
}).compileComponents();
}));
beforeEach(() => {
let appConfig: AppConfigService = TestBed.get(AppConfigService);
appConfig.config.ecmHost = 'http://localhost:9876/ecm';
service = TestBed.get(ContentNodeDialogService);
materialDialog = TestBed.get(MatDialog);
spyOn(materialDialog, 'open').and.stub();
spyOn(materialDialog, 'closeAll').and.stub();
});
it('should be able to create the service', () => {
expect(service).not.toBeNull();
});
it('should be able to open the dialog when node has permission', () => {
service.openCopyMoveDialog('fake-action', fakeNode, '!update');
expect(materialDialog.open).toHaveBeenCalled();
});
it('should be able to open the dialog when node has NOT permission', () => {
service.openCopyMoveDialog('fake-action', fakeNode, 'noperm').subscribe(
() => { },
(error) => {
expect(materialDialog.open).not.toHaveBeenCalled();
expect(error.statusCode).toBe(403);
});
});
it('should be able to close the material dialog', () => {
service.close();
expect(materialDialog.closeAll).toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,81 @@
/*!
* @license
* Copyright 2016 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 { MatDialog } from '@angular/material';
import { Injectable } from '@angular/core';
import { ContentService } from '@alfresco/adf-core';
import { Subject } from 'rxjs/Subject';
import { Observable } from 'rxjs/Observable';
import { ShareDataRow } from '../document-list/data/share-data-row.model';
import { MinimalNodeEntryEntity } from 'alfresco-js-api';
import { DataColumn } from '@alfresco/adf-core';
import { DocumentListService } from '../document-list/services/document-list.service';
import { ContentNodeSelectorComponent } from './content-node-selector.component';
import { ContentNodeSelectorComponentData } from './content-node-selector.component-data.interface';
@Injectable()
export class ContentNodeDialogService {
constructor(private dialog: MatDialog,
private contentService?: ContentService,
private documentListService?: DocumentListService) { }
openCopyMoveDialog(action: string, contentEntry: MinimalNodeEntryEntity, permission?: string): Observable<MinimalNodeEntryEntity[]> {
if (this.contentService.hasPermission(contentEntry, permission)) {
const select = new Subject<MinimalNodeEntryEntity[]>();
select.subscribe({
complete: this.close.bind(this)
});
const data: ContentNodeSelectorComponentData = {
title: `${action} '${contentEntry.name}' to ...`,
actionName: action,
currentFolderId: contentEntry.parentId,
rowFilter: this.rowFilter.bind(this, contentEntry.id),
imageResolver: this.imageResolver.bind(this),
select: select
};
this.dialog.open(ContentNodeSelectorComponent, { data, panelClass: 'adf-content-node-selector-dialog', width: '630px' });
return select;
} else {
return Observable.throw({ statusCode: 403 });
}
}
private imageResolver(row: ShareDataRow, col: DataColumn): string | null {
const entry: MinimalNodeEntryEntity = row.node.entry;
if (!this.contentService.hasPermission(entry, 'create')) {
return this.documentListService.getMimeTypeIcon('disable/folder');
}
return null;
}
private rowFilter(currentNodeId, row: ShareDataRow): boolean {
const node: MinimalNodeEntryEntity = row.node.entry;
if (node.id === currentNodeId || node.isFile) {
return false;
} else {
return true;
}
}
close() {
this.dialog.closeAll();
}
}

View File

@@ -0,0 +1,95 @@
<div (node-select)="onNodeSelect($event)">
<mat-form-field floatPlaceholder="never" class="adf-content-node-selector-content-input">
<input matInput
id="searchInput"
[formControl]="searchInput"
type="text"
placeholder="Search"
[value]="searchTerm"
data-automation-id="content-node-selector-search-input">
<mat-icon *ngIf="searchTerm.length > 0"
matSuffix (click)="clear()"
class="adf-content-node-selector-content-input-icon"
data-automation-id="content-node-selector-search-clear">clear
</mat-icon>
<mat-icon *ngIf="searchTerm.length === 0"
matSuffix
class="adf-content-node-selector-content-input-icon"
data-automation-id="content-node-selector-search-icon">search
</mat-icon>
</mat-form-field>
<adf-sites-dropdown
(change)="siteChanged($event)"
[placeholder]="'NODE_SELECTOR.SELECT_LOCATION'"
[hideMyFiles]="dropdownHideMyFiles"
[siteList]="dropdownSiteList"
data-automation-id="content-node-selector-sites-combo"></adf-sites-dropdown>
<adf-toolbar>
<adf-toolbar-title>
<adf-dropdown-breadcrumb *ngIf="needBreadcrumbs()"
class="adf-content-node-selector-content-breadcrumb"
(navigate)="clear()"
[target]="documentList"
[folderNode]="breadcrumbFolderNode"
data-automation-id="content-node-selector-content-breadcrumb">
</adf-dropdown-breadcrumb>
</adf-toolbar-title>
</adf-toolbar>
<div
class="adf-content-node-selector-content-list"
[class.adf-content-node-selector-content-list-searchLayout]="showingSearchResults"
data-automation-id="content-node-selector-content-list">
<adf-document-list
#documentList
adf-highlight
adf-highlight-selector="adf-name-location-cell .adf-name-location-cell-name"
[node]="nodes"
[maxItems]="pageSize"
[skipCount]="skipCount"
[enableInfiniteScrolling]="infiniteScroll"
[rowFilter]="rowFilter"
[imageResolver]="imageResolver"
[currentFolderId]="folderIdToShow"
selectionMode="single"
[contextMenuActions]="false"
[contentActions]="false"
[allowDropFiles]="false"
(folderChange)="onFolderChange()"
(ready)="onFolderLoaded($event)"
(node-dblclick)="onNodeDoubleClick($event)"
data-automation-id="content-node-selector-document-list">
<empty-folder-content>
<ng-template>
<div>{{ 'NODE_SELECTOR.NO_RESULTS' | translate }}</div>
</ng-template>
</empty-folder-content>
<data-columns>
<data-column key="$thumbnail" type="image"></data-column>
<data-column key="name" type="text" class="full-width ellipsis-cell">
<ng-template let-context="$implicit">
<adf-name-location-cell [data]="context.data" [column]="context.col" [row]="context.row"></adf-name-location-cell>
</ng-template>
</data-column>
<data-column key="modifiedAt" type="date" format="timeAgo" class="adf-content-selector-modified-cell"></data-column>
<data-column key="modifiedByUser.displayName" type="text" class="adf-content-selector-modifier-cell"></data-column>
</data-columns>
</adf-document-list>
<adf-infinite-pagination
[pagination]="pagination"
[pageSize]="pageSize"
[loading]="loadingSearchResults"
(loadMore)="getNextPageOfSearch($event)"
data-automation-id="content-node-selector-search-pagination">
{{ 'ADF-DOCUMENT-LIST.LAYOUT.LOAD_MORE' | translate }}
</adf-infinite-pagination>
</div>
</div>

View File

@@ -0,0 +1,161 @@
@mixin adf-content-node-selector-theme($theme) {
$primary: map-get($theme, primary);
$foreground: map-get($theme, foreground);
$background: map-get($theme, background);
.adf-content-node-selector {
&-content {
padding-top: 0;
&-input {
width: 100%;
&-icon {
color: mat-color($foreground, disabled-button);
cursor: pointer;
&:hover {
color: mat-color($foreground, base);
}
}
}
.mat-input-underline .mat-input-ripple {
height: 1px;
transition: none;
}
.adf-site-dropdown-container {
.mat-form-field {
display: block;
margin-bottom: 15px;
}
}
.adf-site-dropdown-list-element {
width: 100%;
margin-bottom: 0;
.mat-select-trigger {
font-size: 14px;
}
}
.adf-toolbar .mat-toolbar {
border-bottom-width: 0;
font-size: 14px;
&.mat-toolbar-single-row {
height: auto;
}
}
&-breadcrumb {
.adf-dropdown-breadcumb-trigger {
outline: none;
.mat-icon {
color: mat-color($foreground, base, 0.45);
&:hover {
color: mat-color($foreground, base, 0.65);
}
}
}
.adf-dropddown-breadcrumb-item-chevron {
color: mat-color($foreground, base, 0.45);
}
}
&-list {
height: 200px;
overflow: auto;
border: 1px solid mat-color($foreground, base, 0.07);
.adf-highlight {
color: mat-color($primary);
}
.adf-data-table {
border: none;
.adf-no-content-container {
text-align: center;
}
thead {
display: none;
}
.adf-data-table-cell {
padding-top: 8px;
padding-bottom: 8px;
height: 30px;
& .adf-name-location-cell-location {
display: none;
}
& .adf-name-location-cell-name {
padding: 0;
}
&--image {
padding-left: 16px;
padding-right: 8px;
}
&--text {
padding-left: 8px;
}
}
tbody tr {
height: auto !important;
&:first-child {
.adf-data-table-cell {
border-top: none;
}
}
&:last-child {
.adf-data-table-cell {
border-bottom: none;
}
}
}
}
&-searchLayout {
.adf-data-table {
.adf-data-table-cell {
height: 56px;
padding-bottom: 24px;
& .adf-name-location-cell-location {
display: block
}
& .adf-name-location-cell-name {
padding: 18px 0 2px 0;
}
&.adf-content-selector-modified-cell {
display: none;
}
&.adf-content-selector-modifier-cell {
display: none;
}
}
}
}
}
}
}
}

View File

@@ -0,0 +1,654 @@
/*!
* @license
* Copyright 2016 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 { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { MinimalNodeEntryEntity, SiteEntry } from 'alfresco-js-api';
import {
AlfrescoApiService,
ContentService,
TranslationService,
SearchService,
SitesService,
UserPreferencesService
} from '@alfresco/adf-core';
import { DataTableModule } from '@alfresco/adf-core';
import { Observable } from 'rxjs/Observable';
import { Observer } from 'rxjs/Observer';
import { MaterialModule } from '../material.module';
import { EmptyFolderContentDirective, DocumentListComponent, DocumentListService } from '../document-list';
import { DropdownSitesComponent } from '../site-dropdown';
import { DropdownBreadcrumbComponent } from '../breadcrumb';
import { ContentNodeSelectorPanelComponent } from './content-node-selector-panel.component';
import { ContentNodeSelectorService } from './content-node-selector.service';
import { NodePaging } from 'alfresco-js-api';
const ONE_FOLDER_RESULT = {
list: {
entries: [
{
entry: {
id: '123', name: 'MyFolder', isFile: false, isFolder: true,
createdByUser: { displayName: 'John Doe' },
modifiedByUser: { displayName: 'John Doe' }
}
}
],
pagination: {
hasMoreItems: true
}
}
};
describe('ContentNodeSelectorComponent', () => {
let component: ContentNodeSelectorPanelComponent;
let fixture: ComponentFixture<ContentNodeSelectorPanelComponent>;
let searchService: SearchService;
let searchSpy: jasmine.Spy;
let _observer: Observer<NodePaging>;
function typeToSearchBox(searchTerm = 'string-to-search') {
let searchInput = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-search-input"]'));
searchInput.nativeElement.value = searchTerm;
component.searchInput.setValue(searchTerm);
fixture.detectChanges();
}
function respondWithSearchResults(result) {
_observer.next(result);
}
function setupTestbed(plusProviders) {
TestBed.configureTestingModule({
imports: [
DataTableModule,
MaterialModule
],
declarations: [
DocumentListComponent,
EmptyFolderContentDirective,
DropdownSitesComponent,
DropdownBreadcrumbComponent,
ContentNodeSelectorPanelComponent
],
providers: [
AlfrescoApiService,
ContentService,
SearchService,
TranslationService,
DocumentListService,
SitesService,
ContentNodeSelectorService,
UserPreferencesService,
...plusProviders
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
});
}
afterEach(() => {
fixture.destroy();
TestBed.resetTestingModule();
});
describe('General component features', () => {
beforeEach(async(() => {
setupTestbed([]);
TestBed.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ContentNodeSelectorPanelComponent);
component = fixture.componentInstance;
component.debounceSearch = 0;
searchService = TestBed.get(SearchService);
searchSpy = spyOn(searchService, 'search').and.callFake(() => {
return Observable.create((observer: Observer<NodePaging>) => {
_observer = observer;
});
});
});
describe('Parameters', () => {
it('should trigger the select event when selection has been made', (done) => {
const expectedNode = <MinimalNodeEntryEntity> {};
component.select.subscribe((nodes) => {
expect(nodes.length).toBe(1);
expect(nodes[0]).toBe(expectedNode);
done();
});
component.chosenNode = expectedNode;
});
});
describe('Breadcrumbs', () => {
let documentListService,
sitesService,
expectedDefaultFolderNode;
beforeEach(() => {
expectedDefaultFolderNode = <MinimalNodeEntryEntity> { path: { elements: [] } };
documentListService = TestBed.get(DocumentListService);
sitesService = TestBed.get(SitesService);
spyOn(documentListService, 'getFolderNode').and.returnValue(Promise.resolve(expectedDefaultFolderNode));
spyOn(documentListService, 'getFolder').and.returnValue(Observable.throw('No results for test'));
spyOn(sitesService, 'getSites').and.returnValue(Observable.of({ list: { entries: [] } }));
spyOn(component.documentList, 'loadFolderNodesByFolderNodeId').and.returnValue(Promise.resolve());
component.currentFolderId = 'cat-girl-nuku-nuku';
fixture.detectChanges();
});
it('should show the breadcrumb for the currentFolderId by default', (done) => {
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
const breadcrumb = fixture.debugElement.query(By.directive(DropdownBreadcrumbComponent));
expect(breadcrumb).not.toBeNull();
expect(breadcrumb.componentInstance.folderNode).toBe(expectedDefaultFolderNode);
done();
});
});
it('should not show the breadcrumb if search was performed as last action', (done) => {
typeToSearchBox();
fixture.detectChanges();
setTimeout(() => {
respondWithSearchResults(ONE_FOLDER_RESULT);
fixture.whenStable().then(() => {
fixture.detectChanges();
const breadcrumb = fixture.debugElement.query(By.directive(DropdownBreadcrumbComponent));
expect(breadcrumb).toBeNull();
done();
});
}, 300);
});
it('should show the breadcrumb again on folder navigation in the results list', (done) => {
typeToSearchBox();
fixture.detectChanges();
setTimeout(() => {
respondWithSearchResults(ONE_FOLDER_RESULT);
fixture.whenStable().then(() => {
fixture.detectChanges();
component.onFolderChange();
fixture.detectChanges();
const breadcrumb = fixture.debugElement.query(By.directive(DropdownBreadcrumbComponent));
expect(breadcrumb).not.toBeNull();
done();
});
}, 300);
});
it('should show the breadcrumb for the selected node when search results are displayed', (done) => {
const alfrescoContentService = TestBed.get(ContentService);
spyOn(alfrescoContentService, 'hasPermission').and.returnValue(true);
typeToSearchBox();
setTimeout(() => {
respondWithSearchResults(ONE_FOLDER_RESULT);
fixture.whenStable().then(() => {
fixture.detectChanges();
const chosenNode = <MinimalNodeEntryEntity> { path: { elements: ['one'] } };
component.onNodeSelect({ detail: { node: { entry: chosenNode } } });
fixture.detectChanges();
const breadcrumb = fixture.debugElement.query(By.directive(DropdownBreadcrumbComponent));
expect(breadcrumb).not.toBeNull();
expect(breadcrumb.componentInstance.folderNode.path).toBe(chosenNode.path);
done();
});
}, 300);
});
it('should NOT show the breadcrumb for the selected node when not on search results list', (done) => {
const alfrescoContentService = TestBed.get(ContentService);
spyOn(alfrescoContentService, 'hasPermission').and.returnValue(true);
typeToSearchBox();
setTimeout(() => {
respondWithSearchResults(ONE_FOLDER_RESULT);
fixture.detectChanges();
component.onFolderChange();
fixture.detectChanges();
const chosenNode = <MinimalNodeEntryEntity> { path: { elements: [] } };
component.onNodeSelect({ detail: { node: { entry: chosenNode } } });
fixture.detectChanges();
const breadcrumb = fixture.debugElement.query(By.directive(DropdownBreadcrumbComponent));
expect(breadcrumb).not.toBeNull();
expect(breadcrumb.componentInstance.folderNode).toBe(expectedDefaultFolderNode);
done();
}, 300);
});
});
describe('Search functionality', () => {
function defaultSearchOptions(searchTerm, rootNodeId = undefined, skipCount = 0) {
const parentFiltering = rootNodeId ? [{ query: `ANCESTOR:'workspace://SpacesStore/${rootNodeId}'` }] : [];
let defaultSearchNode: any = {
query: {
query: searchTerm ? `${searchTerm}* OR name:${searchTerm}*` : searchTerm
},
include: ['path', 'allowableOperations'],
paging: {
maxItems: '25',
skipCount: skipCount.toString()
},
filterQueries: [
{ query: "TYPE:'cm:folder'" },
{ query: 'NOT cm:creator:System' },
...parentFiltering
],
scope: {
locations: ['nodes']
}
};
return defaultSearchNode;
}
beforeEach(() => {
const documentListService = TestBed.get(DocumentListService);
const expectedDefaultFolderNode = <MinimalNodeEntryEntity> { path: { elements: [] } };
spyOn(documentListService, 'getFolderNode').and.returnValue(Promise.resolve(expectedDefaultFolderNode));
spyOn(component.documentList, 'loadFolderNodesByFolderNodeId').and.returnValue(Promise.resolve());
component.currentFolderId = 'cat-girl-nuku-nuku';
fixture.detectChanges();
});
it('should load the results by calling the search api on search change', (done) => {
typeToSearchBox('kakarot');
setTimeout(() => {
expect(searchSpy).toHaveBeenCalledWith(defaultSearchOptions('kakarot'));
done();
}, 300);
});
it('should reset the currently chosen node in case of starting a new search', (done) => {
component.chosenNode = <MinimalNodeEntryEntity> {};
typeToSearchBox('kakarot');
setTimeout(() => {
expect(component.chosenNode).toBeNull();
done();
}, 300);
});
it('should call the search api on changing the site selectbox\'s value', (done) => {
typeToSearchBox('vegeta');
setTimeout(() => {
expect(searchSpy.calls.count()).toBe(1, 'Search count should be one after only one search');
component.siteChanged(<SiteEntry> { entry: { guid: 'namek' } });
expect(searchSpy.calls.count()).toBe(2, 'Search count should be two after the site change');
expect(searchSpy.calls.argsFor(1)).toEqual([defaultSearchOptions('vegeta', 'namek')]);
done();
}, 300);
});
it('should show the search icon by default without the X (clear) icon', (done) => {
fixture.detectChanges();
setTimeout(() => {
let searchIcon = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-search-icon"]'));
let clearIcon = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-search-clear"]'));
expect(searchIcon).not.toBeNull('Search icon should be in the DOM');
expect(clearIcon).toBeNull('Clear icon should NOT be in the DOM');
done();
}, 300);
});
it('should show the X (clear) icon without the search icon when the search contains at least one character', (done) => {
fixture.detectChanges();
typeToSearchBox('123');
setTimeout(() => {
fixture.detectChanges();
let searchIcon = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-search-icon"]'));
let clearIcon = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-search-clear"]'));
expect(searchIcon).toBeNull('Search icon should NOT be in the DOM');
expect(clearIcon).not.toBeNull('Clear icon should be in the DOM');
done();
}, 300);
});
it('should clear the search field, nodes and chosenNode when clicking on the X (clear) icon', () => {
component.chosenNode = <MinimalNodeEntryEntity> {};
component.nodes = {
list: {
entries: [{ entry: component.chosenNode }]
}
};
component.searchTerm = 'piccolo';
component.showingSearchResults = true;
component.clear();
expect(component.searchTerm).toBe('');
expect(component.nodes).toEqual(null);
expect(component.chosenNode).toBeNull();
expect(component.showingSearchResults).toBeFalsy();
});
it('should show the current folder\'s content instead of search results if search was not performed', () => {
let documentList = fixture.debugElement.query(By.directive(DocumentListComponent));
expect(documentList).not.toBeNull('Document list should be shown');
expect(documentList.componentInstance.currentFolderId).toBe('cat-girl-nuku-nuku');
});
it('should pass through the rowFilter to the documentList', () => {
const filter = () => {
};
component.rowFilter = filter;
fixture.detectChanges();
let documentList = fixture.debugElement.query(By.directive(DocumentListComponent));
expect(documentList).not.toBeNull('Document list should be shown');
expect(documentList.componentInstance.rowFilter).toBe(filter);
});
it('should pass through the imageResolver to the documentList', () => {
const resolver = () => 'piccolo';
component.imageResolver = resolver;
fixture.detectChanges();
let documentList = fixture.debugElement.query(By.directive(DocumentListComponent));
expect(documentList).not.toBeNull('Document list should be shown');
expect(documentList.componentInstance.imageResolver).toBe(resolver);
});
it('should show the result list when search was performed', (done) => {
typeToSearchBox();
setTimeout(() => {
respondWithSearchResults(ONE_FOLDER_RESULT);
fixture.detectChanges();
let documentList = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-document-list"]'));
expect(documentList).not.toBeNull('Document list should be shown');
expect(documentList.componentInstance.currentFolderId).toBeNull();
done();
}, 300);
});
xit('should highlight the results when search was performed in the next timeframe', (done) => {
spyOn(component.highlighter, 'highlight');
typeToSearchBox('shenron');
setTimeout(() => {
respondWithSearchResults(ONE_FOLDER_RESULT);
fixture.detectChanges();
expect(component.highlighter.highlight).not.toHaveBeenCalled();
setTimeout(() => {
expect(component.highlighter.highlight).toHaveBeenCalledWith('shenron');
}, 300);
done();
}, 300);
});
it('should show the default text instead of result list if search was cleared', (done) => {
typeToSearchBox();
setTimeout(() => {
respondWithSearchResults(ONE_FOLDER_RESULT);
fixture.detectChanges();
fixture.whenStable().then(() => {
let clearButton = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-search-clear"]'));
expect(clearButton).not.toBeNull('Clear button should be in DOM');
clearButton.triggerEventHandler('click', {});
fixture.detectChanges();
let documentList = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-document-list"]'));
expect(documentList).not.toBeNull('Document list should be shown');
expect(documentList.componentInstance.currentFolderId).toBe('cat-girl-nuku-nuku');
done();
});
}, 300);
});
xit('should reload the original documentlist when clearing the search input', (done) => {
typeToSearchBox('shenron');
setTimeout(() => {
respondWithSearchResults(ONE_FOLDER_RESULT);
typeToSearchBox('');
fixture.detectChanges();
setTimeout(() => {
let documentList = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-document-list"]'));
expect(documentList.componentInstance.currentFolderId).toBe('cat-girl-nuku-nuku');
}, 300);
done();
}, 300);
});
it('should set the folderIdToShow to the default "currentFolderId" if siteId is undefined', (done) => {
component.siteChanged(<SiteEntry> { entry: { guid: 'Kame-Sennin Muten Roshi' } });
fixture.detectChanges();
let documentList = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-document-list"]'));
expect(documentList.componentInstance.currentFolderId).toBe('Kame-Sennin Muten Roshi');
component.siteChanged(<SiteEntry> { entry: { guid: undefined } });
fixture.detectChanges();
documentList = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-document-list"]'));
expect(documentList.componentInstance.currentFolderId).toBe('cat-girl-nuku-nuku');
done();
});
describe('Pagination "Load more" button', () => {
it('should NOT be shown by default', () => {
fixture.detectChanges();
const pagination = fixture.debugElement.query(By.css('[data-automation-id="adf-infinite-pagination-button"]'));
expect(pagination).toBeNull();
});
it('should be shown when diplaying search results', (done) => {
typeToSearchBox('shenron');
setTimeout(() => {
respondWithSearchResults(ONE_FOLDER_RESULT);
fixture.whenStable().then(() => {
fixture.detectChanges();
const pagination = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-search-pagination"]'));
expect(pagination).not.toBeNull();
done();
});
}, 300);
});
it('button\'s callback should load the next batch of results by calling the search api', () => {
const skipCount = 8;
component.searchTerm = 'kakarot';
component.getNextPageOfSearch({ skipCount });
expect(searchSpy).toHaveBeenCalledWith(defaultSearchOptions('kakarot', undefined, skipCount));
});
it('should set its loading state to true after search was started', (done) => {
component.showingSearchResults = true;
component.pagination = { hasMoreItems: true };
typeToSearchBox('shenron');
setTimeout(() => {
fixture.detectChanges();
const spinnerSelector = By.css('[data-automation-id="content-node-selector-search-pagination"] [data-automation-id="adf-infinite-pagination-spinner"]');
const paginationLoading = fixture.debugElement.query(spinnerSelector);
expect(paginationLoading).not.toBeNull();
done();
}, 300);
});
it('should set its loading state to true after search was performed', (done) => {
component.showingSearchResults = true;
component.pagination = { hasMoreItems: true };
typeToSearchBox('shenron');
fixture.detectChanges();
setTimeout(() => {
respondWithSearchResults(ONE_FOLDER_RESULT);
fixture.whenStable().then(() => {
fixture.detectChanges();
const spinnerSelector = By.css('[data-automation-id="content-node-selector-search-pagination"] [data-automation-id="adf-infinite-pagination-spinner"]');
const paginationLoading = fixture.debugElement.query(spinnerSelector);
expect(paginationLoading).toBeNull();
done();
});
}, 300);
});
});
});
describe('Action button for the chosen node', () => {
const entry: MinimalNodeEntryEntity = <MinimalNodeEntryEntity> { list: {entries: [{}]}};
const nodePage: NodePaging = <NodePaging> {list: {}, pagination: {}};
let hasPermission;
beforeEach(() => {
const alfrescoContentService = TestBed.get(ContentService);
spyOn(alfrescoContentService, 'hasPermission').and.callFake(() => hasPermission);
});
it('should become enabled after loading node with the necessary permissions', async(() => {
hasPermission = true;
component.documentList.folderNode = entry;
component.select.subscribe((nodes) => {
expect(nodes).toBeDefined();
expect(nodes).not.toBeNull();
});
component.documentList.ready.emit(nodePage);
fixture.detectChanges();
}));
it('should remain disabled after loading node without the necessary permissions', () => {
hasPermission = false;
component.documentList.folderNode = entry;
component.select.subscribe((nodes) => {
expect(nodes).toBeDefined();
expect(nodes).not.toBeNull();
});
component.documentList.ready.emit(nodePage);
fixture.detectChanges();
});
it('should be enabled when clicking on a node (with the right permissions) in the list (onNodeSelect)', () => {
hasPermission = true;
component.select.subscribe((nodes) => {
expect(nodes).toBeDefined();
expect(nodes).not.toBeNull();
});
component.onNodeSelect({ detail: { node: { entry } } });
fixture.detectChanges();
});
it('should remain disabled when clicking on a node (with the WRONG permissions) in the list (onNodeSelect)', () => {
hasPermission = false;
component.select.subscribe((nodes) => {
expect(nodes).toBeDefined();
expect(nodes).not.toBeNull();
});
component.onNodeSelect({ detail: { node: { entry } } });
fixture.detectChanges();
});
it('should become disabled when clicking on a node (with the WRONG permissions) after previously selecting a right node', () => {
component.select.subscribe((nodes) => {
expect(nodes).toBeDefined();
expect(nodes).not.toBeNull();
});
hasPermission = true;
component.onNodeSelect({ detail: { node: { entry } } });
fixture.detectChanges();
hasPermission = false;
component.onNodeSelect({ detail: { node: { entry } } });
fixture.detectChanges();
});
it('should be disabled when resetting the chosen node', () => {
hasPermission = true;
component.onNodeSelect({ detail: { node: { entry: <MinimalNodeEntryEntity> {} } } });
fixture.detectChanges();
component.select.subscribe((nodes) => {
expect(nodes).toBeDefined();
expect(nodes).not.toBeNull();
});
component.resetChosenNode();
fixture.detectChanges();
});
});
});
});

View File

@@ -0,0 +1,309 @@
/*!
* @license
* Copyright 2016 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,
EventEmitter,
Input,
OnInit,
Output,
ViewChild,
ViewEncapsulation
} from '@angular/core';
import {
AlfrescoApiService,
ContentService,
HighlightDirective,
UserPreferencesService
} from '@alfresco/adf-core';
import { FormControl } from '@angular/forms';
import { MinimalNodeEntryEntity, NodePaging, Pagination, SiteEntry, SitePaging } from 'alfresco-js-api';
import { DocumentListComponent, PaginationStrategy } from '../document-list/components/document-list.component';
import { RowFilter } from '../document-list/data/row-filter.model';
import { ImageResolver } from '../document-list/data/image-resolver.model';
import { ContentNodeSelectorService } from './content-node-selector.service';
import { debounceTime } from 'rxjs/operators';
@Component({
selector: 'adf-content-node-selector-panel',
styleUrls: ['./content-node-selector-panel.component.scss'],
templateUrl: './content-node-selector-panel.component.html',
encapsulation: ViewEncapsulation.None
})
export class ContentNodeSelectorPanelComponent implements OnInit {
nodes: NodePaging | null = null;
siteId: null | string;
searchTerm: string = '';
showingSearchResults: boolean = false;
loadingSearchResults: boolean = false;
inDialog: boolean = false;
_chosenNode: MinimalNodeEntryEntity = null;
folderIdToShow: string | null = null;
paginationStrategy: PaginationStrategy;
pagination: Pagination;
skipCount: number = 0;
infiniteScroll: boolean = false;
@Input()
currentFolderId: string = null;
@Input()
dropdownHideMyFiles: boolean = false;
@Input()
dropdownSiteList: SitePaging = null;
@Input()
rowFilter: RowFilter = null;
@Input()
imageResolver: ImageResolver = null;
@Input()
pageSize: number;
@Output()
select: EventEmitter<MinimalNodeEntryEntity[]> = new EventEmitter<MinimalNodeEntryEntity[]>();
@ViewChild(DocumentListComponent)
documentList: DocumentListComponent;
@ViewChild(HighlightDirective)
highlighter: HighlightDirective;
debounceSearch: number= 200;
searchInput: FormControl = new FormControl();
constructor(private contentNodeSelectorService: ContentNodeSelectorService,
private contentService: ContentService,
private apiService: AlfrescoApiService,
private preferences: UserPreferencesService) {
this.searchInput.valueChanges
.pipe(
debounceTime(this.debounceSearch)
)
.subscribe((searchValue) => {
this.search(searchValue);
});
this.pageSize = this.preferences.paginationSize;
}
set chosenNode(value: MinimalNodeEntryEntity) {
this._chosenNode = value;
this.select.next([value]);
}
get chosenNode() {
return this._chosenNode;
}
ngOnInit() {
this.folderIdToShow = this.currentFolderId;
this.paginationStrategy = PaginationStrategy.Infinite;
}
/**
* Updates the site attribute and starts a new search
*
* @param chosenSite SiteEntry to search within
*/
siteChanged(chosenSite: SiteEntry): void {
this.siteId = chosenSite.entry.guid;
this.updateResults();
}
/**
* Updates the searchTerm attribute and starts a new search
*
* @param searchTerm string value to search against
*/
search(searchTerm: string): void {
this.searchTerm = searchTerm;
this.updateResults();
}
/**
* Returns whether breadcrumb has to be shown or not
*/
needBreadcrumbs() {
const whenInFolderNavigation = !this.showingSearchResults,
whenInSelectingSearchResult = this.showingSearchResults && this.chosenNode;
return whenInFolderNavigation || whenInSelectingSearchResult;
}
/**
* Returns the actually selected|entered folder node or null in case of searching for the breadcrumb
*/
get breadcrumbFolderNode(): MinimalNodeEntryEntity | null {
if (this.showingSearchResults && this.chosenNode) {
return this.chosenNode;
} else {
return this.documentList.folderNode;
}
}
/**
* Clear the search input
*/
clear(): void {
this.searchTerm = '';
this.nodes = null;
this.skipCount = 0;
this.chosenNode = null;
this.showingSearchResults = false;
this.folderIdToShow = this.currentFolderId;
}
/**
* Update the result list depending on the criterias
*/
private updateResults(): void {
if (this.searchTerm.length === 0) {
this.folderIdToShow = this.siteId || this.currentFolderId;
} else {
this.startNewSearch();
}
}
/**
* Load the first page of a new search result
*/
private startNewSearch(): void {
this.nodes = null;
this.skipCount = 0;
this.chosenNode = null;
this.folderIdToShow = null;
this.querySearch();
}
/**
* Loads the next batch of search results
*
* @param event Pagination object
*/
getNextPageOfSearch(event: Pagination): void {
this.infiniteScroll = true;
this.skipCount = event.skipCount;
this.querySearch();
}
/**
* Perform the call to searchService with the proper parameters
*/
private querySearch(): void {
this.loadingSearchResults = true;
this.contentNodeSelectorService.search(this.searchTerm, this.siteId, this.skipCount, this.pageSize)
.subscribe(this.showSearchResults.bind(this));
}
/**
* Show the results of the search
*
* @param results Search results
*/
private showSearchResults(results: NodePaging): void {
this.showingSearchResults = true;
this.loadingSearchResults = false;
// Documentlist hack, since data displaying for preloaded nodes is a little bit messy there
if (!this.nodes) {
this.nodes = results;
} else {
this.documentList.data.loadPage(results, true);
}
this.pagination = results.list.pagination;
this.highlight();
}
/**
* Hightlight the actual searchterm in the next frame
*/
highlight(): void {
setTimeout(() => {
this.highlighter.highlight(this.searchTerm);
}, 0);
}
/**
* Sets showingSearchResults state to be able to differentiate between search results or folder results
*/
onFolderChange(): void {
this.skipCount = 0;
this.infiniteScroll = false;
this.showingSearchResults = false;
}
/**
* Attempts to set the currently loaded node
*/
onFolderLoaded(nodePage: NodePaging): void {
this.attemptNodeSelection(this.documentList.folderNode);
this.pagination = nodePage.list.pagination;
}
/**
* Selects node as chosen if it has the right permission, clears the selection otherwise
*
* @param entry
*/
private attemptNodeSelection(entry: MinimalNodeEntryEntity): void {
if (this.contentService.hasPermission(entry, 'create')) {
this.chosenNode = entry;
} else {
this.resetChosenNode();
}
}
/**
* Clears the chosen node
*/
resetChosenNode(): void {
this.chosenNode = null;
}
/**
* Invoked when user selects a node
*
* @param event CustomEvent for node-select
*/
onNodeSelect(event: any): void {
this.attemptNodeSelection(event.detail.node.entry);
}
onNodeDoubleClick(e: CustomEvent) {
const node: any = e.detail.node.entry;
if (node && node.guid) {
const options = {
maxItems: this.pageSize,
skipCount: this.skipCount,
include: ['path', 'properties', 'allowableOperations']
};
this.apiService.nodesApi.getNode(node.guid, options)
.then(documentLibrary => {
this.documentList.performCustomSourceNavigation(documentLibrary);
});
}
}
}

View File

@@ -15,16 +15,16 @@
* limitations under the License.
*/
import { EventEmitter } from '@angular/core';
import { MinimalNodeEntryEntity } from 'alfresco-js-api';
import { MinimalNodeEntryEntity, SitePaging } from 'alfresco-js-api';
import { Subject } from 'rxjs/Subject';
export interface ContentNodeSelectorComponentData {
title: string;
actionName?: string;
currentFolderId?: string;
currentFolderId: string;
dropdownHideMyFiles?: boolean;
dropdownSiteList?: any[];
dropdownSiteList?: SitePaging;
rowFilter?: any;
imageResolver?: any;
select: EventEmitter<MinimalNodeEntryEntity[]>;
select: Subject<MinimalNodeEntryEntity[]>;
}

View File

@@ -1,111 +1,22 @@
<header matDialogTitle
class="adf-content-node-selector-title"
data-automation-id="content-node-selector-title">{{title}}
class="adf-content-node-selector-dialog-title"
data-automation-id="content-node-selector-title">{{title || data?.title}}
</header>
<section matDialogContent
class="adf-content-node-selector-content"
(node-select)="onNodeSelect($event)">
<mat-form-field floatPlaceholder="never" class="adf-content-node-selector-content-input">
<input matInput
id="searchInput"
[formControl]="searchInput"
type="text"
placeholder="Search"
[value]="searchTerm"
data-automation-id="content-node-selector-search-input">
<mat-icon *ngIf="searchTerm.length > 0"
matSuffix (click)="clear()"
class="adf-content-node-selector-content-input-icon"
data-automation-id="content-node-selector-search-clear">clear
</mat-icon>
<mat-icon *ngIf="searchTerm.length === 0"
matSuffix
class="adf-content-node-selector-content-input-icon"
data-automation-id="content-node-selector-search-icon">search
</mat-icon>
</mat-form-field>
<adf-sites-dropdown
(change)="siteChanged($event)"
[placeholder]="'NODE_SELECTOR.SELECT_LOCATION'"
[hideMyFiles]="dropdownHideMyFiles"
[siteList]="dropdownSiteList"
data-automation-id="content-node-selector-sites-combo"></adf-sites-dropdown>
<adf-toolbar>
<adf-toolbar-title>
<adf-dropdown-breadcrumb *ngIf="needBreadcrumbs()"
class="adf-content-node-selector-content-breadcrumb"
(navigate)="clear()"
[target]="documentList"
[folderNode]="breadcrumbFolderNode"
data-automation-id="content-node-selector-content-breadcrumb">
</adf-dropdown-breadcrumb>
</adf-toolbar-title>
</adf-toolbar>
<div
class="adf-content-node-selector-content-list"
[class.adf-content-node-selector-content-list-searchLayout]="showingSearchResults"
data-automation-id="content-node-selector-content-list">
<adf-document-list
#documentList
adf-highlight
adf-highlight-selector="adf-name-location-cell .adf-name-location-cell-name"
[node]="nodes"
[maxItems]="pageSize"
[skipCount]="skipCount"
[enableInfiniteScrolling]="infiniteScroll"
[rowFilter]="rowFilter"
[imageResolver]="imageResolver"
[currentFolderId]="folderIdToShow"
selectionMode="single"
[contextMenuActions]="false"
[contentActions]="false"
[allowDropFiles]="false"
(folderChange)="onFolderChange()"
(ready)="onFolderLoaded($event)"
(node-dblclick)="onNodeDoubleClick($event)"
data-automation-id="content-node-selector-document-list">
<empty-folder-content>
<ng-template>
<div>{{ 'NODE_SELECTOR.NO_RESULTS' | translate }}</div>
</ng-template>
</empty-folder-content>
<data-columns>
<data-column key="$thumbnail" type="image"></data-column>
<data-column key="name" type="text" class="full-width ellipsis-cell">
<ng-template let-context="$implicit">
<adf-name-location-cell [data]="context.data" [column]="context.col" [row]="context.row"></adf-name-location-cell>
</ng-template>
</data-column>
<data-column key="modifiedAt" type="date" format="timeAgo" class="adf-content-selector-modified-cell"></data-column>
<data-column key="modifiedByUser.displayName" type="text" class="adf-content-selector-modifier-cell"></data-column>
</data-columns>
</adf-document-list>
<adf-infinite-pagination
[pagination]="pagination"
[pageSize]="pageSize"
[loading]="loadingSearchResults"
(loadMore)="getNextPageOfSearch($event)"
data-automation-id="content-node-selector-search-pagination">
{{ 'ADF-DOCUMENT-LIST.LAYOUT.LOAD_MORE' | translate }}
</adf-infinite-pagination>
</div>
class="adf-content-node-selector-dialog-content">
<adf-content-node-selector-panel
[currentFolderId]="currentFolderId || data?.currentFolderId"
[dropdownHideMyFiles]="dropdownHideMyFiles || data?.dropdownHideMyFiles"
[dropdownSiteList]="dropdownSiteList || data?.dropdownSiteList"
[rowFilter]="rowFilter || data?.rowFilter"
[imageResolver]="imageResolver || data?.imageResolver"
(select)="onSelect($event)">
</adf-content-node-selector-panel>
</section>
<footer matDialogActions class="adf-content-node-selector-actions">
<button *ngIf="inDialog"
<button
mat-button
class="adf-content-node-selector-actions-cancel"
(click)="close()"
@@ -115,8 +26,9 @@
<button mat-button
[disabled]="!chosenNode"
class="adf-content-node-selector-actions-choose"
(click)="choose()"
(click)="onClick()"
data-automation-id="content-node-selector-actions-choose">{{ buttonActionName | translate }}
</button>
</footer>

View File

@@ -1,207 +1,56 @@
@mixin adf-content-node-selector-theme($theme) {
@mixin adf-content-node-selector-dialog-theme($theme) {
$primary: map-get($theme, primary);
$foreground: map-get($theme, foreground);
$background: map-get($theme, background);
.adf-content-node-selector-dialog {
.mat-dialog-container {
padding: 0;
}
.mat-dialog-container {
padding: 0;
.adf-content-node-selector-dialog {
&-title,
&-content,
&-actions {
padding: 16px;
margin: 0;
}
.adf-content-node-selector {
&-title,
&-content,
&-actions {
padding: 16px;
margin: 0;
}
&-content{
padding-top: 0;
}
&-title::first-letter {
&-title::first-letter {
text-transform: uppercase;
}
&-actions {
padding: 8px;
background-color: mat-color($background, background);
display: flex;
justify-content: flex-end;
color: mat-color($foreground, secondary-text);
button {
text-transform: uppercase;
}
&-content {
padding-top: 0;
&-input {
width: 100%;
&-icon {
color: mat-color($foreground, disabled-button);
cursor: pointer;
&:hover {
color: mat-color($foreground, base);
}
}
}
.mat-input-underline .mat-input-ripple {
height: 1px;
transition: none;
}
.adf-site-dropdown-container {
.mat-form-field {
display: block;
margin-bottom: 15px;
}
}
.adf-site-dropdown-list-element {
width: 100%;
margin-bottom: 0;
.mat-select-trigger {
font-size: 14px;
}
}
.adf-toolbar .mat-toolbar {
border-bottom-width: 0;
font-size: 14px;
&.mat-toolbar-single-row {
height: auto;
}
}
&-breadcrumb {
.adf-dropdown-breadcumb-trigger {
outline: none;
.mat-icon {
color: mat-color($foreground, base, 0.45);
&:hover {
color: mat-color($foreground, base, 0.65);
}
}
}
.adf-dropddown-breadcrumb-item-chevron {
color: mat-color($foreground, base, 0.45);
}
}
&-list {
height: 200px;
overflow: auto;
border: 1px solid mat-color($foreground, base, 0.07);
.adf-highlight {
color: mat-color($primary);
}
.adf-data-table {
border: none;
.adf-no-content-container {
text-align: center;
}
thead {
display: none;
}
.adf-data-table-cell {
padding-top: 8px;
padding-bottom: 8px;
height: 30px;
& .adf-name-location-cell-location {
display: none;
}
& .adf-name-location-cell-name {
padding: 0;
}
&--image {
padding-left: 16px;
padding-right: 8px;
}
&--text {
padding-left: 8px;
}
}
tbody tr {
height: auto !important;
&:first-child {
.adf-data-table-cell {
border-top: none;
}
}
&:last-child {
.adf-data-table-cell {
border-bottom: none;
}
}
}
}
&-searchLayout {
.adf-data-table {
.adf-data-table-cell {
height: 56px;
padding-bottom: 24px;
& .adf-name-location-cell-location {
display: block
}
& .adf-name-location-cell-name {
padding: 18px 0 2px 0;
}
&.adf-content-selector-modified-cell {
display: none;
}
&.adf-content-selector-modifier-cell {
display: none;
}
}
}
}
}
&:last-child {
margin-bottom: 0px;
}
&-actions {
padding: 8px;
background-color: mat-color($background, background);
display: flex;
justify-content: flex-end;
color: mat-color($foreground, secondary-text);
&-cancel {
font-weight: normal;
}
button {
text-transform: uppercase;
&-choose {
font-weight: normal;
&[disabled] {
opacity: 0.6;
}
&:last-child {
margin-bottom: 0px;
}
&-cancel {
font-weight: normal;
}
&-choose {
font-weight: normal;
&[disabled] {
opacity: 0.6;
}
&:enabled {
color: mat-color($primary);
}
&:enabled {
color: mat-color($primary);
}
}
}

View File

@@ -15,92 +15,48 @@
* limitations under the License.
*/
import { CUSTOM_ELEMENTS_SCHEMA, EventEmitter } from '@angular/core';
import { MAT_DIALOG_DATA } from '@angular/material';
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { EventEmitter } from '@angular/core';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material';
import { By } from '@angular/platform-browser';
import { MinimalNodeEntryEntity } from 'alfresco-js-api';
import {
AlfrescoApiService,
ContentService,
TranslationService,
SearchService,
SiteModel,
SitesService,
UserPreferencesService
} from '@alfresco/adf-core';
import { DataTableModule } from '@alfresco/adf-core';
import { Observable } from 'rxjs/Observable';
import { Observer } from 'rxjs/Observer';
import { MaterialModule } from '../material.module';
import { EmptyFolderContentDirective, DocumentListComponent, DocumentListService } from '../document-list';
import { DropdownSitesComponent } from '../site-dropdown';
import { DropdownBreadcrumbComponent } from '../breadcrumb';
import { ContentNodeSelectorComponent } from './content-node-selector.component';
import { ContentNodeSelectorPanelComponent } from './content-node-selector-panel.component';
import { ContentNodeSelectorService } from './content-node-selector.service';
import { NodePaging } from 'alfresco-js-api';
import { MinimalNodeEntryEntity } from 'alfresco-js-api';
import { MaterialModule } from '../material.module';
import { By } from '@angular/platform-browser';
import {
EmptyFolderContentDirective,
DocumentListComponent,
DocumentListService
} from '../document-list';
import { AlfrescoApiService, ContentService } from '@alfresco/adf-core';
const ONE_FOLDER_RESULT = {
list: {
entries: [
{
entry: {
id: '123', name: 'MyFolder', isFile: false, isFolder: true,
createdByUser: { displayName: 'John Doe' },
modifiedByUser: { displayName: 'John Doe' }
}
}
],
pagination: {
hasMoreItems: true
}
}
};
describe('ContentNodeSelectorDialogComponent', () => {
describe('ContentNodeSelectorComponent', () => {
let component: ContentNodeSelectorComponent;
let fixture: ComponentFixture<ContentNodeSelectorComponent>;
let data: any;
let searchService: SearchService;
let searchSpy: jasmine.Spy;
let apiService: AlfrescoApiService;
let nodesApi;
let _observer: Observer<NodePaging>;
function typeToSearchBox(searchTerm = 'string-to-search') {
let searchInput = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-search-input"]'));
searchInput.nativeElement.value = searchTerm;
component.searchInput.setValue(searchTerm);
fixture.detectChanges();
}
function respondWithSearchResults(result) {
_observer.next(result);
}
function setupTestbed(plusProviders) {
TestBed.configureTestingModule({
imports: [
DataTableModule,
MaterialModule
],
declarations: [
ContentNodeSelectorComponent,
ContentNodeSelectorPanelComponent,
DocumentListComponent,
EmptyFolderContentDirective,
DropdownSitesComponent,
DropdownBreadcrumbComponent,
ContentNodeSelectorComponent
EmptyFolderContentDirective
],
providers: [
ContentNodeSelectorService,
ContentNodeSelectorPanelComponent,
DocumentListService,
AlfrescoApiService,
ContentService,
SearchService,
TranslationService,
DocumentListService,
SitesService,
ContentNodeSelectorService,
UserPreferencesService,
...plusProviders
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
@@ -112,685 +68,100 @@ describe('ContentNodeSelectorComponent', () => {
TestBed.resetTestingModule();
});
describe('Dialog features', () => {
beforeEach(async(() => {
data = {
title: 'Move along citizen...',
actionName: 'move',
select: new EventEmitter<MinimalNodeEntryEntity>(),
rowFilter: () => {
},
imageResolver: () => 'piccolo',
currentFolderId: 'cat-girl-nuku-nuku'
};
beforeEach(async(() => {
data = {
title: 'Move along citizen...',
actionName: 'move',
select: new EventEmitter<MinimalNodeEntryEntity>(),
rowFilter: () => {
},
imageResolver: () => 'piccolo',
currentFolderId: 'cat-girl-nuku-nuku'
};
setupTestbed([{ provide: MAT_DIALOG_DATA, useValue: data }]);
TestBed.compileComponents();
}));
setupTestbed([{ provide: MAT_DIALOG_DATA, useValue: data }]);
TestBed.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ContentNodeSelectorComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
beforeEach(() => {
fixture = TestBed.createComponent(ContentNodeSelectorComponent);
component = fixture.componentInstance;
describe('Data injecting with the "Material dialog way"', () => {
it('should show the INJECTED title', () => {
const titleElement = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-title"]'));
expect(titleElement).not.toBeNull();
expect(titleElement.nativeElement.innerText).toBe('Move along citizen...');
});
it('should have the INJECTED actionName on the name of the choose button', () => {
const actionButton = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-actions-choose"]'));
expect(actionButton).not.toBeNull();
expect(actionButton.nativeElement.innerText).toBe('NODE_SELECTOR.MOVE');
});
it('should pass through the injected currentFolderId to the documentlist', () => {
let documentList = fixture.debugElement.query(By.directive(DocumentListComponent));
expect(documentList).not.toBeNull('Document list should be shown');
expect(documentList.componentInstance.currentFolderId).toBe('cat-girl-nuku-nuku');
});
it('should pass through the injected rowFilter to the documentlist', () => {
let documentList = fixture.debugElement.query(By.directive(DocumentListComponent));
expect(documentList).not.toBeNull('Document list should be shown');
expect(documentList.componentInstance.rowFilter).toBe(data.rowFilter);
});
it('should pass through the injected imageResolver to the documentlist', () => {
let documentList = fixture.debugElement.query(By.directive(DocumentListComponent));
expect(documentList).not.toBeNull('Document list should be shown');
expect(documentList.componentInstance.imageResolver).toBe(data.imageResolver);
});
});
describe('Cancel button', () => {
it('should complete the data stream when user click "CANCEL"', () => {
let cancelButton;
data.select.subscribe(
() => { },
() => { },
() => {
cancelButton = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-actions-cancel"]'));
expect(cancelButton).not.toBeNull();
});
cancelButton = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-actions-cancel"]'));
cancelButton.triggerEventHandler('click', {});
});
it('should not be shown if dialogRef is NOT injected', () => {
const closeButton = fixture.debugElement.query(By.css('[content-node-selector-actions-cancel]'));
expect(closeButton).toBeNull();
});
});
describe('Action button for the chosen node', () => {
it('should be disabled by default', () => {
fixture.detectChanges();
let actionButton = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-actions-choose"]'));
expect(actionButton.nativeElement.disabled).toBeTruthy();
});
describe('Data injecting with the "Material dialog way"', () => {
it('should be enabled when a node is chosen', () => {
component.onSelect([{ id: 'fake' }]);
fixture.detectChanges();
it('should show the INJECTED title', () => {
const titleElement = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-title"]'));
expect(titleElement).not.toBeNull();
expect(titleElement.nativeElement.innerText).toBe('Move along citizen...');
});
it('should have the INJECTED actionName on the name of the choose button', () => {
const actionButton = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-actions-choose"]'));
expect(actionButton).not.toBeNull();
expect(actionButton.nativeElement.innerText).toBe('NODE_SELECTOR.MOVE');
});
it('should pass through the injected currentFolderId to the documentlist', () => {
let documentList = fixture.debugElement.query(By.directive(DocumentListComponent));
expect(documentList).not.toBeNull('Document list should be shown');
expect(documentList.componentInstance.currentFolderId).toBe('cat-girl-nuku-nuku');
});
it('should pass through the injected rowFilter to the documentlist', () => {
let documentList = fixture.debugElement.query(By.directive(DocumentListComponent));
expect(documentList).not.toBeNull('Document list should be shown');
expect(documentList.componentInstance.rowFilter).toBe(data.rowFilter);
});
it('should pass through the injected imageResolver to the documentlist', () => {
let documentList = fixture.debugElement.query(By.directive(DocumentListComponent));
expect(documentList).not.toBeNull('Document list should be shown');
expect(documentList.componentInstance.imageResolver).toBe(data.imageResolver);
});
it('should trigger the INJECTED select event when selection has been made', (done) => {
const expectedNode = <MinimalNodeEntryEntity> {};
data.select.subscribe((nodes) => {
expect(nodes.length).toBe(1);
expect(nodes[0]).toBe(expectedNode);
done();
});
component.chosenNode = expectedNode;
component.choose();
});
let actionButton = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-actions-choose"]'));
expect(actionButton.nativeElement.disabled).toBeFalsy();
});
describe('Cancel button', () => {
let dummyMdDialogRef;
let fakePreference: UserPreferencesService = <UserPreferencesService> jasmine.createSpyObj('UserPreferencesService', ['paginationSize']);
fakePreference.paginationSize = 10;
beforeEach(() => {
dummyMdDialogRef = <MatDialogRef<ContentNodeSelectorComponent>> {
close: () => {
}
};
});
it('should be shown if dialogRef is injected', () => {
const componentInstance = new ContentNodeSelectorComponent(null, null, null, fakePreference, data, dummyMdDialogRef);
expect(componentInstance.inDialog).toBeTruthy();
});
it('should should call the close method in the injected dialogRef', () => {
spyOn(dummyMdDialogRef, 'close');
const componentInstance = new ContentNodeSelectorComponent(null, null, null, fakePreference, data, dummyMdDialogRef);
componentInstance.close();
expect(dummyMdDialogRef.close).toHaveBeenCalled();
});
});
});
describe('General component features', () => {
beforeEach(async(() => {
setupTestbed([]);
TestBed.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ContentNodeSelectorComponent);
component = fixture.componentInstance;
component.debounceSearch = 0;
searchService = TestBed.get(SearchService);
searchSpy = spyOn(searchService, 'search').and.callFake(() => {
return Observable.create((observer: Observer<NodePaging>) => {
_observer = observer;
});
});
apiService = TestBed.get(AlfrescoApiService);
nodesApi = apiService.nodesApi;
});
describe('Parameters', () => {
it('should show the title', () => {
component.title = 'Move along citizen...';
fixture.detectChanges();
const titleElement = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-title"]'));
expect(titleElement).not.toBeNull();
expect(titleElement.nativeElement.innerText).toBe('Move along citizen...');
});
it('should trigger the select event when selection has been made', (done) => {
const expectedNode = <MinimalNodeEntryEntity> {};
component.select.subscribe((nodes) => {
expect(nodes.length).toBe(1);
expect(nodes[0]).toBe(expectedNode);
done();
});
component.chosenNode = expectedNode;
component.choose();
});
});
describe('Breadcrumbs', () => {
let documentListService,
sitesService,
expectedDefaultFolderNode;
beforeEach(() => {
expectedDefaultFolderNode = <MinimalNodeEntryEntity> { path: { elements: [] } };
documentListService = TestBed.get(DocumentListService);
sitesService = TestBed.get(SitesService);
spyOn(documentListService, 'getFolderNode').and.returnValue(Promise.resolve(expectedDefaultFolderNode));
spyOn(documentListService, 'getFolder').and.returnValue(Observable.throw('No results for test'));
spyOn(sitesService, 'getSites').and.returnValue(Observable.of([]));
spyOn(component.documentList, 'loadFolderNodesByFolderNodeId').and.returnValue(Promise.resolve());
component.currentFolderId = 'cat-girl-nuku-nuku';
fixture.detectChanges();
});
it('should show the breadcrumb for the currentFolderId by default', (done) => {
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
const breadcrumb = fixture.debugElement.query(By.directive(DropdownBreadcrumbComponent));
expect(breadcrumb).not.toBeNull();
expect(breadcrumb.componentInstance.folderNode).toBe(expectedDefaultFolderNode);
done();
});
});
it('should not show the breadcrumb if search was performed as last action', (done) => {
typeToSearchBox();
fixture.detectChanges();
setTimeout(() => {
respondWithSearchResults(ONE_FOLDER_RESULT);
fixture.whenStable().then(() => {
fixture.detectChanges();
const breadcrumb = fixture.debugElement.query(By.directive(DropdownBreadcrumbComponent));
expect(breadcrumb).toBeNull();
done();
});
}, 300);
});
it('should show the breadcrumb again on folder navigation in the results list', (done) => {
typeToSearchBox();
fixture.detectChanges();
setTimeout(() => {
respondWithSearchResults(ONE_FOLDER_RESULT);
fixture.whenStable().then(() => {
fixture.detectChanges();
component.onFolderChange();
fixture.detectChanges();
const breadcrumb = fixture.debugElement.query(By.directive(DropdownBreadcrumbComponent));
expect(breadcrumb).not.toBeNull();
done();
});
}, 300);
});
it('should show the breadcrumb for the selected node when search results are displayed', (done) => {
const alfrescoContentService = TestBed.get(ContentService);
spyOn(alfrescoContentService, 'hasPermission').and.returnValue(true);
typeToSearchBox();
setTimeout(() => {
respondWithSearchResults(ONE_FOLDER_RESULT);
fixture.whenStable().then(() => {
fixture.detectChanges();
const chosenNode = <MinimalNodeEntryEntity> { path: { elements: ['one'] } };
component.onNodeSelect({ detail: { node: { entry: chosenNode } } });
fixture.detectChanges();
const breadcrumb = fixture.debugElement.query(By.directive(DropdownBreadcrumbComponent));
expect(breadcrumb).not.toBeNull();
expect(breadcrumb.componentInstance.folderNode.path).toBe(chosenNode.path);
done();
});
}, 300);
});
it('should NOT show the breadcrumb for the selected node when not on search results list', (done) => {
const alfrescoContentService = TestBed.get(ContentService);
spyOn(alfrescoContentService, 'hasPermission').and.returnValue(true);
typeToSearchBox();
setTimeout(() => {
respondWithSearchResults(ONE_FOLDER_RESULT);
fixture.detectChanges();
component.onFolderChange();
fixture.detectChanges();
const chosenNode = <MinimalNodeEntryEntity> { path: { elements: [] } };
component.onNodeSelect({ detail: { node: { entry: chosenNode } } });
fixture.detectChanges();
const breadcrumb = fixture.debugElement.query(By.directive(DropdownBreadcrumbComponent));
expect(breadcrumb).not.toBeNull();
expect(breadcrumb.componentInstance.folderNode).toBe(expectedDefaultFolderNode);
done();
}, 300);
});
});
describe('Search functionality', () => {
function defaultSearchOptions(searchTerm, rootNodeId = undefined, skipCount = 0) {
const parentFiltering = rootNodeId ? [ { query: `ANCESTOR:'workspace://SpacesStore/${rootNodeId}'` } ] : [];
let defaultSearchNode: any = {
query: {
query: searchTerm ? `${searchTerm}* OR name:${searchTerm}*` : searchTerm
},
include: ['path', 'allowableOperations'],
paging: {
maxItems: '25',
skipCount: skipCount.toString()
},
filterQueries: [
{ query: "TYPE:'cm:folder'" },
{ query: 'NOT cm:creator:System' },
...parentFiltering
],
scope: {
locations: ['nodes']
}
};
return defaultSearchNode;
}
beforeEach(() => {
const documentListService = TestBed.get(DocumentListService);
const expectedDefaultFolderNode = <MinimalNodeEntryEntity> { path: { elements: [] } };
spyOn(documentListService, 'getFolderNode').and.returnValue(Promise.resolve(expectedDefaultFolderNode));
spyOn(component.documentList, 'loadFolderNodesByFolderNodeId').and.returnValue(Promise.resolve());
component.currentFolderId = 'cat-girl-nuku-nuku';
fixture.detectChanges();
});
it('should load the results by calling the search api on search change', (done) => {
typeToSearchBox('kakarot');
setTimeout(() => {
expect(searchSpy).toHaveBeenCalledWith(defaultSearchOptions('kakarot'));
done();
}, 300);
});
it('should reset the currently chosen node in case of starting a new search', (done) => {
component.chosenNode = <MinimalNodeEntryEntity> {};
typeToSearchBox('kakarot');
setTimeout(() => {
expect(component.chosenNode).toBeNull();
done();
}, 300);
});
it('should call the search api on changing the site selectbox\'s value', (done) => {
typeToSearchBox('vegeta');
setTimeout(() => {
expect(searchSpy.calls.count()).toBe(1, 'Search count should be one after only one search');
component.siteChanged(<SiteModel> { guid: 'namek' });
expect(searchSpy.calls.count()).toBe(2, 'Search count should be two after the site change');
expect(searchSpy.calls.argsFor(1)).toEqual([defaultSearchOptions('vegeta', 'namek')]);
done();
}, 300);
});
it('should show the search icon by default without the X (clear) icon', (done) => {
fixture.detectChanges();
setTimeout(() => {
let searchIcon = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-search-icon"]'));
let clearIcon = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-search-clear"]'));
expect(searchIcon).not.toBeNull('Search icon should be in the DOM');
expect(clearIcon).toBeNull('Clear icon should NOT be in the DOM');
done();
}, 300);
});
it('should show the X (clear) icon without the search icon when the search contains at least one character', (done) => {
fixture.detectChanges();
typeToSearchBox('123');
setTimeout(() => {
fixture.detectChanges();
let searchIcon = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-search-icon"]'));
let clearIcon = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-search-clear"]'));
expect(searchIcon).toBeNull('Search icon should NOT be in the DOM');
expect(clearIcon).not.toBeNull('Clear icon should be in the DOM');
done();
}, 300);
});
it('should clear the search field, nodes and chosenNode when clicking on the X (clear) icon', () => {
component.chosenNode = <MinimalNodeEntryEntity> {};
component.nodes = {
list: {
entries: [{ entry: component.chosenNode }]
}
};
component.searchTerm = 'piccolo';
component.showingSearchResults = true;
component.clear();
expect(component.searchTerm).toBe('');
expect(component.nodes).toEqual(null);
expect(component.chosenNode).toBeNull();
expect(component.showingSearchResults).toBeFalsy();
});
it('should show the current folder\'s content instead of search results if search was not performed', () => {
let documentList = fixture.debugElement.query(By.directive(DocumentListComponent));
expect(documentList).not.toBeNull('Document list should be shown');
expect(documentList.componentInstance.currentFolderId).toBe('cat-girl-nuku-nuku');
});
it('should pass through the rowFilter to the documentList', () => {
const filter = () => {
};
component.rowFilter = filter;
fixture.detectChanges();
let documentList = fixture.debugElement.query(By.directive(DocumentListComponent));
expect(documentList).not.toBeNull('Document list should be shown');
expect(documentList.componentInstance.rowFilter).toBe(filter);
});
it('should pass through the imageResolver to the documentList', () => {
const resolver = () => 'piccolo';
component.imageResolver = resolver;
fixture.detectChanges();
let documentList = fixture.debugElement.query(By.directive(DocumentListComponent));
expect(documentList).not.toBeNull('Document list should be shown');
expect(documentList.componentInstance.imageResolver).toBe(resolver);
});
it('should show the result list when search was performed', (done) => {
typeToSearchBox();
setTimeout(() => {
respondWithSearchResults(ONE_FOLDER_RESULT);
fixture.detectChanges();
let documentList = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-document-list"]'));
expect(documentList).not.toBeNull('Document list should be shown');
expect(documentList.componentInstance.currentFolderId).toBeNull();
done();
}, 300);
});
xit('should highlight the results when search was performed in the next timeframe', (done) => {
spyOn(component.highlighter, 'highlight');
typeToSearchBox('shenron');
setTimeout(() => {
respondWithSearchResults(ONE_FOLDER_RESULT);
fixture.detectChanges();
expect(component.highlighter.highlight).not.toHaveBeenCalled();
setTimeout(() => {
expect(component.highlighter.highlight).toHaveBeenCalledWith('shenron');
}, 300);
done();
}, 300);
});
it('should show the default text instead of result list if search was cleared', (done) => {
typeToSearchBox();
setTimeout(() => {
respondWithSearchResults(ONE_FOLDER_RESULT);
fixture.detectChanges();
fixture.whenStable().then(() => {
let clearButton = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-search-clear"]'));
expect(clearButton).not.toBeNull('Clear button should be in DOM');
clearButton.triggerEventHandler('click', {});
fixture.detectChanges();
let documentList = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-document-list"]'));
expect(documentList).not.toBeNull('Document list should be shown');
expect(documentList.componentInstance.currentFolderId).toBe('cat-girl-nuku-nuku');
done();
});
}, 300);
});
xit('should reload the original documentlist when clearing the search input', (done) => {
typeToSearchBox('shenron');
setTimeout(() => {
respondWithSearchResults(ONE_FOLDER_RESULT);
typeToSearchBox('');
fixture.detectChanges();
setTimeout(() => {
let documentList = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-document-list"]'));
expect(documentList.componentInstance.currentFolderId).toBe('cat-girl-nuku-nuku');
}, 300);
done();
}, 300);
});
it('should set the folderIdToShow to the default "currentFolderId" if siteId is undefined', (done) => {
component.siteChanged(<SiteModel> { guid: 'Kame-Sennin Muten Roshi' });
fixture.detectChanges();
let documentList = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-document-list"]'));
expect(documentList.componentInstance.currentFolderId).toBe('Kame-Sennin Muten Roshi');
component.siteChanged(<SiteModel> { guid: undefined });
fixture.detectChanges();
documentList = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-document-list"]'));
expect(documentList.componentInstance.currentFolderId).toBe('cat-girl-nuku-nuku');
done();
});
describe('Pagination "Load more" button', () => {
it('should NOT be shown by default', () => {
fixture.detectChanges();
const pagination = fixture.debugElement.query(By.css('[data-automation-id="adf-infinite-pagination-button"]'));
expect(pagination).toBeNull();
});
it('should be shown when diplaying search results', (done) => {
typeToSearchBox('shenron');
setTimeout(() => {
respondWithSearchResults(ONE_FOLDER_RESULT);
fixture.whenStable().then(() => {
fixture.detectChanges();
const pagination = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-search-pagination"]'));
expect(pagination).not.toBeNull();
done();
});
}, 300);
});
it('button\'s callback should load the next batch of results by calling the search api', () => {
const skipCount = 8;
component.searchTerm = 'kakarot';
component.getNextPageOfSearch({ skipCount });
expect(searchSpy).toHaveBeenCalledWith(defaultSearchOptions('kakarot', undefined, skipCount));
});
it('should set its loading state to true after search was started', (done) => {
component.showingSearchResults = true;
component.pagination = { hasMoreItems: true };
typeToSearchBox('shenron');
setTimeout(() => {
fixture.detectChanges();
const spinnerSelector = By.css('[data-automation-id="content-node-selector-search-pagination"] [data-automation-id="adf-infinite-pagination-spinner"]');
const paginationLoading = fixture.debugElement.query(spinnerSelector);
expect(paginationLoading).not.toBeNull();
done();
}, 300);
});
it('should set its loading state to true after search was performed', (done) => {
component.showingSearchResults = true;
component.pagination = { hasMoreItems: true };
typeToSearchBox('shenron');
fixture.detectChanges();
setTimeout(() => {
respondWithSearchResults(ONE_FOLDER_RESULT);
fixture.whenStable().then(() => {
fixture.detectChanges();
const spinnerSelector = By.css('[data-automation-id="content-node-selector-search-pagination"] [data-automation-id="adf-infinite-pagination-spinner"]');
const paginationLoading = fixture.debugElement.query(spinnerSelector);
expect(paginationLoading).toBeNull();
done();
});
}, 300);
});
});
});
describe('Cancel button', () => {
it('should not be shown if dialogRef is NOT injected', () => {
const closeButton = fixture.debugElement.query(By.css('[content-node-selector-actions-cancel]'));
expect(closeButton).toBeNull();
});
});
describe('Action button for the chosen node', () => {
const entry: MinimalNodeEntryEntity = <MinimalNodeEntryEntity> {};
let hasPermission;
beforeEach(() => {
const alfrescoContentService = TestBed.get(ContentService);
spyOn(alfrescoContentService, 'hasPermission').and.callFake(() => hasPermission);
});
it('should be disabled by default', () => {
fixture.detectChanges();
let actionButton = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-actions-choose"]'));
expect(actionButton.nativeElement.disabled).toBe(true);
});
it('should become enabled after loading node with the necessary permissions', () => {
hasPermission = true;
component.documentList.folderNode = entry;
component.documentList.ready.emit();
fixture.detectChanges();
let actionButton = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-actions-choose"]'));
expect(actionButton.nativeElement.disabled).toBe(false);
});
it('should remain disabled after loading node without the necessary permissions', () => {
hasPermission = false;
component.documentList.folderNode = entry;
component.documentList.ready.emit();
fixture.detectChanges();
let actionButton = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-actions-choose"]'));
expect(actionButton.nativeElement.disabled).toBe(true);
});
it('should be enabled when clicking on a node (with the right permissions) in the list (onNodeSelect)', () => {
hasPermission = true;
component.onNodeSelect({ detail: { node: { entry } } });
fixture.detectChanges();
let actionButton = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-actions-choose"]'));
expect(actionButton.nativeElement.disabled).toBe(false);
});
it('should remain disabled when clicking on a node (with the WRONG permissions) in the list (onNodeSelect)', () => {
hasPermission = false;
component.onNodeSelect({ detail: { node: { entry } } });
fixture.detectChanges();
let actionButton = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-actions-choose"]'));
expect(actionButton.nativeElement.disabled).toBe(true);
});
it('should become disabled when clicking on a node (with the WRONG permissions) after previously selecting a right node', () => {
hasPermission = true;
component.onNodeSelect({ detail: { node: { entry } } });
fixture.detectChanges();
hasPermission = false;
component.onNodeSelect({ detail: { node: { entry } } });
fixture.detectChanges();
let actionButton = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-actions-choose"]'));
expect(actionButton.nativeElement.disabled).toBe(true);
});
it('should be disabled when resetting the chosen node', () => {
hasPermission = true;
component.onNodeSelect({ detail: { node: { entry: <MinimalNodeEntryEntity> {} } } });
fixture.detectChanges();
component.resetChosenNode();
fixture.detectChanges();
let actionButton = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-actions-choose"]'));
expect(actionButton.nativeElement.disabled).toBe(true);
});
it('should make the call to get the corresponding node entry to emit when a site node is selected as destination', () => {
spyOn(nodesApi, 'getNode').and.callFake((nodeId) => {
return new Promise(resolve => {
resolve({ entry: { id: nodeId } });
});
});
const siteNode1 = { title: 'my files', guid: '-my-' };
const siteNode2 = { title: 'my sites', guid: '-mysites-' };
component.dropdownSiteList = [siteNode1, siteNode2];
fixture.detectChanges();
component.chosenNode = siteNode1;
fixture.detectChanges();
component.choose();
const options = {
include: ['path', 'properties', 'allowableOperations']
};
expect(nodesApi.getNode).toHaveBeenCalledWith(
'-my-',
options
);
});
});
});
});

View File

@@ -15,345 +15,81 @@
* limitations under the License.
*/
import {
Component,
EventEmitter,
Inject,
Input,
OnInit,
Optional,
Output,
ViewChild,
ViewEncapsulation
} from '@angular/core';
import {
AlfrescoApiService,
ContentService,
HighlightDirective,
SiteModel,
UserPreferencesService
} from '@alfresco/adf-core';
import { FormControl } from '@angular/forms';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material';
import { MinimalNodeEntryEntity, NodePaging, Pagination, Site } from 'alfresco-js-api';
import { DocumentListComponent, PaginationStrategy } from '../document-list/components/document-list.component';
import { Component, Inject, ViewEncapsulation, Input } from '@angular/core';
import { MAT_DIALOG_DATA } from '@angular/material';
import { MinimalNodeEntryEntity, SitePaging } from 'alfresco-js-api';
import { ContentNodeSelectorComponentData } from './content-node-selector.component-data.interface';
import { RowFilter } from '../document-list/data/row-filter.model';
import { ImageResolver } from '../document-list/data/image-resolver.model';
import { ContentNodeSelectorComponentData } from './content-node-selector.component-data.interface';
import { ContentNodeSelectorService } from './content-node-selector.service';
import { debounceTime } from 'rxjs/operators';
@Component({
selector: 'adf-content-node-selector',
styleUrls: ['./content-node-selector.component.scss'],
templateUrl: './content-node-selector.component.html',
styleUrls: ['./content-node-selector.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class ContentNodeSelectorComponent implements OnInit {
export class ContentNodeSelectorComponent {
nodes: NodePaging | null = null;
siteId: null | string;
searchTerm: string = '';
showingSearchResults: boolean = false;
loadingSearchResults: boolean = false;
inDialog: boolean = false;
chosenNode: MinimalNodeEntryEntity | Site | null = null;
folderIdToShow: string | null = null;
paginationStrategy: PaginationStrategy;
pagination: Pagination;
skipCount: number = 0;
infiniteScroll: boolean = false;
buttonActionName: string;
/**
* @deprecated in 2.1.0
*/
@Input()
title: string;
title: string = null;
/**
* @deprecated in 2.1.0
*/
@Input()
actionName: string;
@Input()
currentFolderId: string | null = null;
currentFolderId: string = null;
/**
* @deprecated in 2.1.0
*/
@Input()
dropdownHideMyFiles: boolean = false;
/**
* @deprecated in 2.1.0
*/
@Input()
dropdownSiteList: any[] = null;
dropdownSiteList: SitePaging = null;
/**
* @deprecated in 2.1.0
*/
@Input()
rowFilter: RowFilter = null;
/**
* @deprecated in 2.1.0
*/
@Input()
imageResolver: ImageResolver = null;
/**
* @deprecated in 2.1.0
*/
@Input()
pageSize: number;
@Output()
select: EventEmitter<MinimalNodeEntryEntity[]> = new EventEmitter<MinimalNodeEntryEntity[]>();
buttonActionName: string;
private chosenNode: MinimalNodeEntryEntity[];
@ViewChild(DocumentListComponent)
documentList: DocumentListComponent;
@ViewChild(HighlightDirective)
highlighter: HighlightDirective;
debounceSearch: number= 200;
searchInput: FormControl = new FormControl();
constructor(private contentNodeSelectorService: ContentNodeSelectorService,
private contentService: ContentService,
private apiService: AlfrescoApiService,
private preferences: UserPreferencesService,
@Optional() @Inject(MAT_DIALOG_DATA) data?: ContentNodeSelectorComponentData,
@Optional() private containingDialog?: MatDialogRef<ContentNodeSelectorComponent>) {
if (data) {
this.title = data.title;
this.actionName = data.actionName;
this.select = data.select;
this.currentFolderId = data.currentFolderId;
this.dropdownHideMyFiles = data.dropdownHideMyFiles;
this.dropdownSiteList = data.dropdownSiteList;
this.rowFilter = data.rowFilter;
this.imageResolver = data.imageResolver;
}
this.buttonActionName = this.actionName ? `NODE_SELECTOR.${this.actionName.toUpperCase()}` : 'NODE_SELECTOR.CHOOSE';
if (this.containingDialog) {
this.inDialog = true;
}
this.searchInput.valueChanges
.pipe(
debounceTime(this.debounceSearch)
)
.subscribe((searchValue) => {
this.search(searchValue);
});
this.pageSize = this.preferences.paginationSize;
constructor(@Inject(MAT_DIALOG_DATA) public data: ContentNodeSelectorComponentData) {
this.buttonActionName = data.actionName ? `NODE_SELECTOR.${data.actionName.toUpperCase()}` : 'NODE_SELECTOR.CHOOSE';
}
ngOnInit() {
this.folderIdToShow = this.currentFolderId;
this.paginationStrategy = PaginationStrategy.Infinite;
close() {
this.data.select.complete();
}
/**
* Updates the site attribute and starts a new search
*
* @param chosenSite Sitemodel to search within
*/
siteChanged(chosenSite: SiteModel): void {
this.siteId = chosenSite.guid;
this.updateResults();
onSelect(nodeList: MinimalNodeEntryEntity[]) {
this.chosenNode = nodeList;
}
/**
* Updates the searchTerm attribute and starts a new search
*
* @param searchTerm string value to search against
*/
search(searchTerm: string): void {
this.searchTerm = searchTerm;
this.updateResults();
}
/**
* Returns whether breadcrumb has to be shown or not
*/
needBreadcrumbs() {
const whenInFolderNavigation = !this.showingSearchResults,
whenInSelectingSearchResult = this.showingSearchResults && this.chosenNode;
return whenInFolderNavigation || whenInSelectingSearchResult;
}
/**
* Returns the actually selected|entered folder node or null in case of searching for the breadcrumb
*/
get breadcrumbFolderNode(): MinimalNodeEntryEntity | null {
if (this.showingSearchResults && this.chosenNode) {
return this.chosenNode;
} else {
return this.documentList.folderNode;
}
}
/**
* Clear the search input
*/
clear(): void {
this.searchTerm = '';
this.nodes = null;
this.skipCount = 0;
this.chosenNode = null;
this.showingSearchResults = false;
this.folderIdToShow = this.currentFolderId;
}
/**
* Update the result list depending on the criterias
*/
private updateResults(): void {
if (this.searchTerm.length === 0) {
this.folderIdToShow = this.siteId || this.currentFolderId;
} else {
this.startNewSearch();
}
}
/**
* Load the first page of a new search result
*/
private startNewSearch(): void {
this.nodes = null;
this.skipCount = 0;
this.chosenNode = null;
this.folderIdToShow = null;
this.querySearch();
}
/**
* Loads the next batch of search results
*
* @param event Pagination object
*/
getNextPageOfSearch(event: Pagination): void {
this.infiniteScroll = true;
this.skipCount = event.skipCount;
this.querySearch();
}
/**
* Perform the call to searchService with the proper parameters
*/
private querySearch(): void {
this.loadingSearchResults = true;
this.contentNodeSelectorService.search(this.searchTerm, this.siteId, this.skipCount, this.pageSize)
.subscribe(this.showSearchResults.bind(this));
}
/**
* Show the results of the search
*
* @param results Search results
*/
private showSearchResults(results: NodePaging): void {
this.showingSearchResults = true;
this.loadingSearchResults = false;
// Documentlist hack, since data displaying for preloaded nodes is a little bit messy there
if (!this.nodes) {
this.nodes = results;
} else {
this.documentList.data.loadPage(results, true);
}
this.pagination = results.list.pagination;
this.highlight();
}
/**
* Hightlight the actual searchterm in the next frame
*/
highlight(): void {
setTimeout(() => {
this.highlighter.highlight(this.searchTerm);
}, 0);
}
/**
* Invoked when user selects a node
*
* @param event CustomEvent for node-select
*/
onNodeSelect(event: any): void {
this.attemptNodeSelection(event.detail.node.entry);
}
/**
* Sets showingSearchResults state to be able to differentiate between search results or folder results
*/
onFolderChange(): void {
this.skipCount = 0;
this.infiniteScroll = false;
this.showingSearchResults = false;
}
/**
* Attempts to set the currently loaded node
*/
onFolderLoaded(nodePage: NodePaging): void {
this.attemptNodeSelection(this.documentList.folderNode);
this.pagination = nodePage.list.pagination;
}
/**
* Selects node as chosen if it has the right permission, clears the selection otherwise
*
* @param entry
*/
private attemptNodeSelection(entry: MinimalNodeEntryEntity): void {
if (this.contentService.hasPermission(entry, 'create')) {
this.chosenNode = entry;
} else {
this.resetChosenNode();
}
}
/**
* Clears the chosen node
*/
resetChosenNode(): void {
this.chosenNode = null;
}
/**
* Emit event with the chosen node
*/
choose(): void {
const entry: any = this.chosenNode;
if (entry && entry.guid) {
const options = {
include: ['path', 'properties', 'allowableOperations']
};
this.apiService.nodesApi.getNode(entry.guid, options)
.then(chosenSiteNode => {
this.select.next([chosenSiteNode.entry]);
});
} else {
this.select.next([this.chosenNode]);
}
}
/**
* Close the dialog
*/
close(): void {
this.containingDialog.close();
}
onNodeDoubleClick(e: CustomEvent) {
const node: any = e.detail.node.entry;
if (node && node.guid) {
const options = {
maxItems: this.pageSize,
skipCount: this.skipCount,
include: ['path', 'properties', 'allowableOperations']
};
this.apiService.nodesApi.getNode(node.guid, options)
.then(documentLibrary => {
this.documentList.performCustomSourceNavigation(documentLibrary);
});
}
onClick(): void {
this.data.select.next(this.chosenNode);
this.data.select.complete();
}
}

View File

@@ -21,8 +21,10 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MaterialModule } from '../material.module';
import { TranslateModule } from '@ngx-translate/core';
import { ContentNodeSelectorPanelComponent } from './content-node-selector-panel.component';
import { ContentNodeSelectorComponent } from './content-node-selector.component';
import { ContentNodeSelectorService } from './content-node-selector.service';
import { ContentNodeDialogService } from './content-node-dialog.service';
import { SitesDropdownModule } from '../site-dropdown/sites-dropdown.module';
import { BreadcrumbModule } from '../breadcrumb/breadcrumb.module';
import { PaginationModule, ToolbarModule, DirectiveModule, DataColumnModule, DataTableModule } from '@alfresco/adf-core';
@@ -46,17 +48,19 @@ import { NameLocationCellComponent } from './name-location-cell/name-location-ce
PaginationModule
],
exports: [
ContentNodeSelectorComponent
ContentNodeSelectorPanelComponent, ContentNodeSelectorComponent
],
entryComponents: [
ContentNodeSelectorComponent
ContentNodeSelectorPanelComponent, ContentNodeSelectorComponent
],
declarations: [
ContentNodeSelectorComponent,
NameLocationCellComponent
ContentNodeSelectorPanelComponent,
NameLocationCellComponent,
ContentNodeSelectorComponent
],
providers: [
ContentNodeSelectorService
ContentNodeSelectorService,
ContentNodeDialogService
]
})
export class ContentNodeSelectorModule {}

View File

@@ -16,5 +16,6 @@
*/
export * from './content-node-selector.component-data.interface';
export * from './content-node-selector-panel.component';
export * from './content-node-selector.component';
export * from './content-node-selector.service';