[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

@@ -21,11 +21,11 @@ import {
} from '@angular/core';
import { MatDialog } from '@angular/material';
import { ActivatedRoute, Params, Router } from '@angular/router';
import { MinimalNodeEntity, NodePaging, Pagination, MinimalNodeEntryEntity } from 'alfresco-js-api';
import { MinimalNodeEntity, NodePaging, Pagination, MinimalNodeEntryEntity, SiteEntry } from 'alfresco-js-api';
import {
AlfrescoApiService, ContentService, TranslationService,
FileUploadEvent, FolderCreatedEvent, LogService, NotificationService,
SiteModel, UploadService, DataColumn, DataRow, UserPreferencesService,
UploadService, DataColumn, DataRow, UserPreferencesService,
PaginationComponent
} from '@alfresco/adf-core';
@@ -325,8 +325,8 @@ export class FilesComponent implements OnInit, OnChanges, OnDestroy {
}
}
getSiteContent(site: SiteModel) {
this.currentFolderId = site && site.guid ? site.guid : DEFAULT_FOLDER_TO_SHOW;
getSiteContent(site: SiteEntry) {
this.currentFolderId = site && site.entry.guid ? site.entry.guid : DEFAULT_FOLDER_TO_SHOW;
}
getDocumentListCurrentFolderId() {

View File

@@ -75,7 +75,6 @@ for more information about installing and using the source code.
- [Form field model](form-field.model.md)
- [Comment process model](comment-process.model.md)
- [Product version model](product-version.model.md)
- [Site model](site.model.md)
- [User process model](user-process.model.md)
- [Bpm user model](bpm-user.model.md)
- [Ecm user model](ecm-user.model.md)

View File

@@ -6,8 +6,44 @@ Allows a user to select items from a Content Services repository.
## Basic Usage
The component is showed within a material [dialog window](https://material.angular.io/components/dialog/overview) with two action available and it can be opened with the following ways:
### Using Content node dialog service - recommended
```ts
import { ContentNodeDialogService } from '@adf/content-services'
constructor(private contentDialogService: ContentNodeDialogService){}
yourFunctionOnCopyOrMove(){
this.contentDialogService
.openCopyMoveDialog(actionName, targetNode, neededPermissionForAction)
.subscribe((selections: MinimalNodeEntryEntity[]) => {
// place your action here on operation success!
});
}
```
#### Required parameters
The dialog needs this information to be correctly opened :
| Name | Type | Description |
| --- | --- | --- |
| actionName | string | This will be the label for the confirm button of the dialog |
| targetNode | [MinimalNodeEntryEntity](https://github.com/Alfresco/alfresco-js-api/blob/master/src/alfresco-core-rest-api/docs/MinimalNode.md) | the node on which we are asking for copy/move action |
| neededPermissionForAction | string | needed permission to check to perform the relative action (es: copy will need the 'update' permission ) |
the `openCopyMoveDialog` method will return an [observable](http://reactivex.io/rxjs/manual/overview.html#observable) that can where you can subscribe to get the selection result and apply the custom actions.
### Using ContentNodeSelectorComponent
```ts
import { MatDialog } from '@angular/material';
import { ContentNodeSelectorComponentData, ContentNodeSelectorComponent} from '@adf/content-services'
import { Subject } from 'rxjs/Subject';
...
constructor(dialog: MatDialog ... ) {}
@@ -16,7 +52,7 @@ openSelectorDialog() {
data: ContentNodeSelectorComponentData = {
title: "Choose an item",
currentFolderId: someFolderId,
select: new EventEmitter<MinimalNodeEntryEntity[]>()
select: new Subject<MinimalNodeEntryEntity[]>()
};
this.dialog.open(
@@ -29,12 +65,19 @@ openSelectorDialog() {
data.select.subscribe((selections: MinimalNodeEntryEntity[]) => {
// Use or store selection...
},
(error)=>{
//your error handling
},
()=>{
//action called when an action or cancel is clicked on the dialog
this.dialog.closeAll();
});
}
```
With this system your function has to take care of opening/closing the dialog. All the results will be streamed on the select [subject](http://reactivex.io/rxjs/manual/overview.html#subject) present into the `ContentNodeSelectorComponentData` object given to the dialog.
When clicked on the action the data.select stream will be completed.
### Properties
@@ -42,8 +85,44 @@ openSelectorDialog() {
| --- | --- | --- | --- |
| title | string | "" | Text shown at the top of the selector |
| currentFolderId | string | null | Node ID of the folder currently listed |
| rowFilter | RowFilter | null | Custom row filter function |
| imageResolver | ImageResolver | null | Custom image resolver function |
| dropdownHideMyFiles | boolean | false | Hide the "My Files" option added to the site list by default. [See More](https://github.com/Alfresco/alfresco-ng2-components/blob/master/docs/sites-dropdown.component.md)|
| dropdownSiteList | [SitePaging](https://github.com/Alfresco/alfresco-js-api/blob/master/src/alfresco-core-rest-api/docs/SitePaging.md) | | custom site for site dropdown same as siteList. [See More](https://github.com/Alfresco/alfresco-ng2-components/blob/master/docs/sites-dropdown.component.md#properties) |
| rowFilter | RowFilter | null | Custom row filter function. [See More](https://github.com/Alfresco/alfresco-ng2-components/blob/master/docs/document-list.component.md#custom-row-filter)|
| imageResolver | ImageResolver | null | Custom image resolver function. [See More](https://github.com/Alfresco/alfresco-ng2-components/blob/master/docs/document-list.component.md#custom-image-resolver) |
| pageSize | number | 10 | Number of items shown per page in the list |
### Events
| Name | Description |
| --- | --- |
| select | Emitted when the user has selected an item |
### Using ContentNodeSelectorPanelComponent
```html
<adf-content-node-selector-panel
[currentFolderId]="currentFolderId"
[dropdownHideMyFiles]="dropdownHideMyFiles"
[dropdownSiteList]="dropdownSiteList"
[rowFilter]="rowFilter"
[imageResolver]="imageResolver"
(select)="onSelect($event)">
</adf-content-node-selector-panel>
```
This will allow you to use the content node selector without the material dialog.
### Properties
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| currentFolderId | string | null | Node ID of the folder currently listed |
| dropdownHideMyFiles | boolean | false | Hide the "My Files" option added to the site list by default. [See More](https://github.com/Alfresco/alfresco-ng2-components/blob/master/docs/sites-dropdown.component.md)|
| dropdownSiteList | [SitePaging](https://github.com/Alfresco/alfresco-js-api/blob/master/src/alfresco-core-rest-api/docs/SitePaging.md) | | custom site for site dropdown same as siteList. [See More](https://github.com/Alfresco/alfresco-ng2-components/blob/master/docs/sites-dropdown.component.md#properties) |
| rowFilter | RowFilter | null | Custom row filter function. [See More](https://github.com/Alfresco/alfresco-ng2-components/blob/master/docs/document-list.component.md#custom-row-filter)|
| imageResolver | ImageResolver | null | Custom image resolver function. [See More](https://github.com/Alfresco/alfresco-ng2-components/blob/master/docs/document-list.component.md#custom-image-resolver) |
| pageSize | number | 10 | Number of items shown per page in the list |
### Events

View File

@@ -17,7 +17,7 @@ Displays a dropdown menu to show and interact with the sites of the current user
| Attribute | Type | Default | Description |
| --- | --- | --- | --- |
| hideMyFiles | boolean | false | Hide the "My Files" option added to the list by default |
| siteList | any[] | null | A custom list of sites to be displayed by the dropdown. If no value is given, the sites of the current user are displayed by default. A list of objects only with properties 'title' and 'guid' is enough to be able to display the dropdown. |
| siteList | [SitePaging](https://github.com/Alfresco/alfresco-js-api/blob/master/src/alfresco-core-rest-api/docs/SitePaging.md) | null | A custom list of sites to be displayed by the dropdown. If no value is given, the sites of the current user are displayed by default. A list of objects only with properties 'title' and 'guid' is enough to be able to display the dropdown. |
| placeholder | string | 'DROPDOWN.PLACEHOLDER_LABEL' | The placeholder text/the key from translation files for the placeholder text to be shown by default|
### Events

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,15 +1,13 @@
@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;
}
.adf-content-node-selector {
.adf-content-node-selector-dialog {
&-title,
&-content,
&-actions {
@@ -17,160 +15,12 @@
margin: 0;
}
&-title::first-letter {
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;
}
}
}
}
}
&-title::first-letter {
text-transform: uppercase;
}
&-actions {
@@ -206,4 +56,3 @@
}
}
}
}

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,8 +68,6 @@ describe('ContentNodeSelectorComponent', () => {
TestBed.resetTestingModule();
});
describe('Dialog features', () => {
beforeEach(async(() => {
data = {
title: 'Move along citizen...',
@@ -167,517 +121,24 @@ describe('ContentNodeSelectorComponent', () => {
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();
});
});
describe('Cancel button', () => {
let dummyMdDialogRef;
let fakePreference: UserPreferencesService = <UserPreferencesService> jasmine.createSpyObj('UserPreferencesService', ['paginationSize']);
fakePreference.paginationSize = 10;
beforeEach(() => {
dummyMdDialogRef = <MatDialogRef<ContentNodeSelectorComponent>> {
close: () => {
}
};
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();
});
it('should be shown if dialogRef is injected', () => {
const componentInstance = new ContentNodeSelectorComponent(null, null, null, fakePreference, data, dummyMdDialogRef);
expect(componentInstance.inDialog).toBeTruthy();
cancelButton = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-actions-cancel"]'));
cancelButton.triggerEventHandler('click', {});
});
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();
@@ -686,111 +147,21 @@ describe('ContentNodeSelectorComponent', () => {
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);
expect(actionButton.nativeElement.disabled).toBeTruthy();
});
it('should become enabled after loading node with the necessary permissions', () => {
hasPermission = true;
component.documentList.folderNode = entry;
component.documentList.ready.emit();
it('should be enabled when a node is chosen', () => {
component.onSelect([{ id: 'fake' }]);
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
);
expect(actionButton.nativeElement.disabled).toBeFalsy();
});
});
});
});

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;
constructor(@Inject(MAT_DIALOG_DATA) public data: ContentNodeSelectorComponentData) {
this.buttonActionName = data.actionName ? `NODE_SELECTOR.${data.actionName.toUpperCase()}` : 'NODE_SELECTOR.CHOOSE';
}
this.searchInput.valueChanges
.pipe(
debounceTime(this.debounceSearch)
)
.subscribe((searchValue) => {
this.search(searchValue);
});
this.pageSize = this.preferences.paginationSize;
close() {
this.data.select.complete();
}
ngOnInit() {
this.folderIdToShow = this.currentFolderId;
this.paginationStrategy = PaginationStrategy.Infinite;
onSelect(nodeList: MinimalNodeEntryEntity[]) {
this.chosenNode = nodeList;
}
/**
* 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();
}
/**
* 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';

View File

@@ -62,7 +62,7 @@ describe('ContentAction', () => {
beforeEach(() => {
contentService = TestBed.get(ContentService);
nodeActionsService = new NodeActionsService(null, null, null);
nodeActionsService = new NodeActionsService(null, null);
documentActions = new DocumentActionsService(nodeActionsService);
folderActions = new FolderActionsService(nodeActionsService, null, contentService);

View File

@@ -32,7 +32,7 @@ describe('DocumentActionsService', () => {
beforeEach(() => {
documentListService = new DocumentListServiceMock();
contentService = new ContentService(null, null, null, null);
nodeActionsService = new NodeActionsService(null, null, null);
nodeActionsService = new NodeActionsService(null, null);
service = new DocumentActionsService(nodeActionsService, documentListService, contentService);
});

View File

@@ -23,6 +23,7 @@ import { ContentActionHandler } from '../models/content-action.model';
import { DocumentListService } from './document-list.service';
import { FolderActionsService } from './folder-actions.service';
import { NodeActionsService } from './node-actions.service';
import { ContentNodeDialogService } from '../../content-node-selector/content-node-dialog.service';
describe('FolderActionsService', () => {
@@ -37,7 +38,8 @@ describe('FolderActionsService', () => {
FolderActionsService,
NodeActionsService,
TranslationService,
NotificationService
NotificationService,
ContentNodeDialogService
]
}).compileComponents();
}));

View File

@@ -0,0 +1,106 @@
/*!
* @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 { async, TestBed } from '@angular/core/testing';
import { MinimalNodeEntryEntity } from 'alfresco-js-api';
import { AppConfigService } from '@alfresco/adf-core';
import { DocumentListService } from './document-list.service';
import { NodeActionsService } from './node-actions.service';
import { ContentNodeDialogService } from '../../content-node-selector/content-node-dialog.service';
import { Observable } from 'rxjs/Observable';
const fakeNode: MinimalNodeEntryEntity = <MinimalNodeEntryEntity> {
id: 'fake'
};
describe('NodeActionsService', () => {
let service: NodeActionsService;
let documentListService: DocumentListService;
let contentDialogService: ContentNodeDialogService;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [],
providers: [
NodeActionsService,
DocumentListService,
ContentNodeDialogService
]
}).compileComponents();
}));
beforeEach(() => {
let appConfig: AppConfigService = TestBed.get(AppConfigService);
appConfig.config.ecmHost = 'http://localhost:9876/ecm';
service = TestBed.get(NodeActionsService);
documentListService = TestBed.get(DocumentListService);
contentDialogService = TestBed.get(ContentNodeDialogService);
});
it('should be able to create the service', () => {
expect(service).not.toBeNull();
});
it('should be able to copy content', async(() => {
spyOn(documentListService, 'copyNode').and.returnValue(Observable.of('FAKE-OK'));
spyOn(contentDialogService, 'openCopyMoveDialog').and.returnValue(Observable.of([fakeNode]));
service.copyContent(fakeNode, 'allowed').subscribe((value) => {
expect(value).toBe('OPERATION.SUCCES.CONTENT.COPY');
});
}));
it('should be able to move content', async(() => {
spyOn(documentListService, 'moveNode').and.returnValue(Observable.of('FAKE-OK'));
spyOn(contentDialogService, 'openCopyMoveDialog').and.returnValue(Observable.of([fakeNode]));
service.moveContent(fakeNode, 'allowed').subscribe((value) => {
expect(value).toBe('OPERATION.SUCCES.CONTENT.MOVE');
});
}));
it('should be able to move folder', async(() => {
spyOn(documentListService, 'moveNode').and.returnValue(Observable.of('FAKE-OK'));
spyOn(contentDialogService, 'openCopyMoveDialog').and.returnValue(Observable.of([fakeNode]));
service.moveFolder(fakeNode, 'allowed').subscribe((value) => {
expect(value).toBe('OPERATION.SUCCES.FOLDER.MOVE');
});
}));
it('should be able to copy folder', async(() => {
spyOn(documentListService, 'copyNode').and.returnValue(Observable.of('FAKE-OK'));
spyOn(contentDialogService, 'openCopyMoveDialog').and.returnValue(Observable.of([fakeNode]));
service.copyFolder(fakeNode, 'allowed').subscribe((value) => {
expect(value).toBe('OPERATION.SUCCES.FOLDER.COPY');
});
}));
it('should be able to propagate the dialog error', async(() => {
spyOn(documentListService, 'copyNode').and.returnValue(Observable.throw('FAKE-KO'));
spyOn(contentDialogService, 'openCopyMoveDialog').and.returnValue(Observable.of([fakeNode]));
service.copyFolder(fakeNode, '!allowed').subscribe((value) => {
}, (error) => {
expect(error).toBe('FAKE-KO');
});
}));
});

View File

@@ -15,23 +15,17 @@
* limitations under the License.
*/
import { DataColumn } from '@alfresco/adf-core';
import { ContentService } from '@alfresco/adf-core';
import { EventEmitter, Injectable } from '@angular/core';
import { MatDialog } from '@angular/material';
import { Injectable } from '@angular/core';
import { MinimalNodeEntryEntity } from 'alfresco-js-api';
import { Subject } from 'rxjs/Subject';
import { ContentNodeSelectorComponent } from '../../content-node-selector/content-node-selector.component';
import { ContentNodeSelectorComponentData } from '../../content-node-selector/content-node-selector.component-data.interface';
import { ShareDataRow } from '../data/share-data-row.model';
import { DocumentListService } from './document-list.service';
import { ContentNodeDialogService } from '../../content-node-selector/content-node-dialog.service';
@Injectable()
export class NodeActionsService {
constructor(private dialog: MatDialog,
private documentListService?: DocumentListService,
private contentService?: ContentService) {}
constructor(private contentDialogService: ContentNodeDialogService,
private documentListService?: DocumentListService) {}
/**
* Copy content node
@@ -84,51 +78,20 @@ export class NodeActionsService {
private doFileOperation(action: string, type: string, contentEntry: MinimalNodeEntryEntity, permission?: string): Subject<string> {
const observable: Subject<string> = new Subject<string>();
if (this.contentService.hasPermission(contentEntry, permission)) {
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: new EventEmitter<MinimalNodeEntryEntity[]>()
};
this.dialog.open(ContentNodeSelectorComponent, { data, panelClass: 'adf-content-node-selector-dialog', width: '630px' });
data.select.subscribe((selections: MinimalNodeEntryEntity[]) => {
this.contentDialogService
.openCopyMoveDialog(action, contentEntry, permission)
.subscribe((selections: MinimalNodeEntryEntity[]) => {
const selection = selections[0];
this.documentListService[`${action}Node`].call(this.documentListService, contentEntry.id, selection.id)
.subscribe(
observable.next.bind(observable, `OPERATION.SUCCES.${type.toUpperCase()}.${action.toUpperCase()}`),
observable.error.bind(observable)
);
this.dialog.closeAll();
},
(error) => {
observable.error(error);
return observable;
});
return observable;
} else {
observable.error(new Error(JSON.stringify({ error: { statusCode: 403 } })));
return observable;
}
}
private rowFilter(currentNodeId, row: ShareDataRow): boolean {
const node: MinimalNodeEntryEntity = row.node.entry;
if (node.id === currentNodeId || node.isFile) {
return false;
} else {
return true;
}
}
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;
}
}

View File

@@ -9,8 +9,8 @@
[(ngModel)]="siteSelected"
(ngModelChange)="selectedSite()">
<mat-option *ngIf="!hideMyFiles" data-automation-id="site-my-files-option" id="default_site_option" [value]="MY_FILES_VALUE">{{'DROPDOWN.MY_FILES_OPTION' | translate}}</mat-option>
<mat-option *ngFor="let site of siteList" [value]="site.guid">
{{ site.title | translate }}
<mat-option *ngFor="let site of siteList?.list.entries" [value]="site.entry.guid">
{{ site.entry.title | translate}}
</mat-option>
</mat-select>
</mat-form-field>

View File

@@ -171,7 +171,25 @@ describe('DropdownSitesComponent', () => {
}));
it('should load custom sites when the \'siteList\' input property is given a value', async(() => {
component.siteList = [{title: 'PERSONAL_FILES', guid: '-my-'}, {title: 'FILE_LIBRARIES', guid: '-mysites-'}];
component.siteList = {
'list': {
'entries': [
{
'entry': {
'guid': '-my-',
'title': 'PERSONAL_FILES'
}
},
{
'entry': {
'guid': '-mysites-',
'title': 'FILE_LIBRARIES'
}
}
]
}
};
fixture.detectChanges();
openSelectbox();
@@ -236,7 +254,7 @@ describe('DropdownSitesComponent', () => {
});
component.change.subscribe((site) => {
expect(site.guid).toBe('fake-1');
expect(site.entry.guid).toBe('fake-1');
done();
});
});

View File

@@ -15,7 +15,8 @@
* limitations under the License.
*/
import { SiteModel, SitesService } from '@alfresco/adf-core';
import { SitesService } from '@alfresco/adf-core';
import { SitePaging, SiteEntry } from 'alfresco-js-api';
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
@Component({
@@ -29,13 +30,13 @@ export class DropdownSitesComponent implements OnInit {
hideMyFiles: boolean = false;
@Input()
siteList: any[] = null;
siteList: SitePaging = null;
@Input()
placeholder: string = 'DROPDOWN.PLACEHOLDER_LABEL';
@Output()
change: EventEmitter<SiteModel> = new EventEmitter();
change: EventEmitter<SiteEntry> = new EventEmitter();
public MY_FILES_VALUE = 'default';
@@ -52,15 +53,14 @@ export class DropdownSitesComponent implements OnInit {
selectedSite() {
let siteFound;
if (this.siteSelected === this.MY_FILES_VALUE) {
siteFound = new SiteModel();
siteFound = { entry: {}};
}else {
siteFound = this.siteList.find( site => site.guid === this.siteSelected);
siteFound = this.siteList.list.entries.find( site => site.entry.guid === this.siteSelected);
}
this.change.emit(siteFound);
}
setDefaultSiteList() {
this.siteList = [];
this.sitesService.getSites().subscribe((result) => {
this.siteList = result;
},

View File

@@ -1,6 +1,6 @@
@import '../breadcrumb/breadcrumb.component';
@import '../breadcrumb/dropdown-breadcrumb.component';
@import '../content-node-selector/content-node-selector.component';
@import '../content-node-selector/content-node-selector-panel.component';
@import '../content-node-selector/name-location-cell/name-location-cell.component';
@import '../document-list/components/document-list.component';
@@ -14,6 +14,7 @@
@import '../content-metadata/content-metadata.component';
@import '../content-metadata/content-metadata-card.component';
@import '../content-node-selector/content-node-selector.component';
@mixin adf-content-services-theme($theme) {
@include adf-breadcrumb-theme($theme);
@@ -28,4 +29,5 @@
@include adf-dialog-theme($theme);
@include adf-content-metadata-theme($theme);
@include adf-content-metadata-card-theme($theme);
@include adf-content-node-selector-dialog-theme($theme) ;
}

View File

@@ -0,0 +1,7 @@
<div class="adf-upload-folder-widget {{field.className}}"
[class.adf-invalid]="!field.isValid"
[class.adf-readonly]="field.readOnly">
<label class="adf-label" [attr.for]="field.id">{{field.name}}<span *ngIf="isRequired()">*</span></label>
<div class="adf-upload-widget-container">
</div>
</div>

View File

@@ -0,0 +1,12 @@
@import '../form';
.adf {
&-upload-folder-widget {
width: 100%;
word-break: break-all;
padding: 0.4375em 0;
border-top: 0.84375em solid transparent;
}
}

View File

@@ -0,0 +1,16 @@
/*!
* @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.
*/

View File

@@ -0,0 +1,150 @@
/*!
* @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:component-selector */
import { LogService } from '../../../../services/log.service';
import { ThumbnailService } from '../../../../services/thumbnail.service';
import { Component, ElementRef, OnInit, ViewChild, ViewEncapsulation } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { FormService } from '../../../services/form.service';
import { ProcessContentService } from '../../../services/process-content.service';
import { ContentLinkModel } from '../core/content-link.model';
import { baseHost, WidgetComponent } from './../widget.component';
import 'rxjs/add/operator/mergeMap';
@Component({
selector: 'upload-folder-widget',
templateUrl: './upload-folder.widget.html',
styleUrls: ['./upload-folder.widget.scss'],
host: baseHost,
encapsulation: ViewEncapsulation.None
})
export class UploadFolderWidgetComponent extends WidgetComponent implements OnInit {
hasFile: boolean;
displayText: string;
multipleOption: string = '';
mimeTypeIcon: string;
@ViewChild('uploadFiles')
fileInput: ElementRef;
constructor(public formService: FormService,
private logService: LogService,
private thumbnailService: ThumbnailService,
public processContentService: ProcessContentService) {
super(formService);
}
ngOnInit() {
if (this.field &&
this.field.value &&
this.field.value.length > 0) {
this.hasFile = true;
}
this.getMultipleFileParam();
}
removeFile(file: any) {
if (this.field) {
this.removeElementFromList(file);
}
}
onFileChanged(event: any) {
let files = event.target.files;
let filesSaved = [];
if (this.field.json.value) {
filesSaved = [...this.field.json.value];
}
if (files && files.length > 0) {
Observable.from(files).mergeMap(file => this.uploadRawContent(file)).subscribe((res) => {
filesSaved.push(res);
},
(error) => {
this.logService.error('Error uploading file. See console output for more details.');
},
() => {
this.field.value = filesSaved;
this.field.json.value = filesSaved;
});
this.hasFile = true;
}
}
private uploadRawContent(file): Observable<any> {
return this.processContentService.createTemporaryRawRelatedContent(file)
.map((response: any) => {
this.logService.info(response);
return response;
});
}
private getMultipleFileParam() {
if (this.field &&
this.field.params &&
this.field.params.multiple) {
this.multipleOption = this.field.params.multiple ? 'multiple' : '';
}
}
private removeElementFromList(file) {
let index = this.field.value.indexOf(file);
if (index !== -1) {
this.field.value.splice(index, 1);
this.field.json.value = this.field.value;
this.field.updateForm();
}
this.hasFile = this.field.value.length > 0;
this.resetFormValueWithNoFiles();
}
private resetFormValueWithNoFiles() {
if (this.field.value.length === 0) {
this.field.value = [];
this.field.json.value = [];
}
}
getIcon(mimeType) {
return this.thumbnailService.getMimeTypeIcon(mimeType);
}
fileClicked(obj: any): void {
const file = new ContentLinkModel(obj);
let fetch = this.processContentService.getContentPreview(file.id);
if (file.isTypeImage() || file.isTypePdf()) {
fetch = this.processContentService.getFileRawContent(file.id);
}
fetch.subscribe(
(blob: Blob) => {
file.contentBlob = blob;
this.formService.formContentClicked.next(file);
},
(error) => {
this.logService.error('Unable to send event for file ' + file.name);
}
);
}
}

View File

@@ -21,7 +21,6 @@ export * from './card-view-mapitem.model';
export * from './card-view-dateitem.model';
export * from './file.model';
export * from './permissions.enum';
export * from './site.model';
export * from './product-version.model';
export * from './user-process.model';
export * from './comment-process.model';

View File

@@ -1,91 +0,0 @@
/*!
* @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 { Pagination } from 'alfresco-js-api';
export class SiteContentsModel {
id: string;
folderId: string;
constructor(obj?: any) {
if (obj) {
this.id = obj.id || null;
this.folderId = obj.folderId || null;
}
}
}
export class SiteMembersModel {
role: string;
firstName: string;
emailNotificationsEnabled: boolean = false;
company: any;
id: string;
enable: boolean = false;
email: string;
constructor(obj?: any) {
if (obj) {
this.role = obj.role;
this.firstName = obj.firstName || null;
this.emailNotificationsEnabled = obj.emailNotificationsEnabled;
this.company = obj.company || null;
this.id = obj.id || null;
this.enable = obj.enable;
this.email = obj.email;
}
}
}
export class SiteModel {
role: string;
visibility: string;
guid: string;
description: string;
id: string;
preset: string;
title: string;
contents: SiteContentsModel[] = [];
members: SiteMembersModel[] = [];
pagination: Pagination;
constructor(obj?: any) {
if (obj && obj.entry) {
this.role = obj.entry.role || null;
this.visibility = obj.entry.visibility || null;
this.guid = obj.entry.guid || null;
this.description = obj.entry.description || null;
this.id = obj.entry.id || null;
this.preset = obj.entry.preset;
this.title = obj.entry.title;
this.pagination = obj.pagination || null;
if (obj.relations && obj.relations.containers) {
obj.relations.containers.list.entries.forEach((content) => {
this.contents.push(new SiteContentsModel(content.entry));
});
}
if (obj.relations && obj.relations.members) {
obj.relations.members.list.entries.forEach((member) => {
this.members.push(new SiteMembersModel(member.entry));
});
}
}
}
}

View File

@@ -76,7 +76,7 @@ describe('Sites service', () => {
it('Should get a list of users sites', (done) => {
service.getSites().subscribe((data) => {
expect(data[0].title).toBe('FAKE');
expect(data.list.entries[0].entry.title).toBe('FAKE');
done();
});
@@ -111,7 +111,7 @@ describe('Sites service', () => {
it('Should get single sites via siteId', (done) => {
service.getSite('fake-site-id').subscribe((data) => {
expect(data.title).toBe('FAKE-SINGLE-TITLE');
expect(data.entry.title).toBe('FAKE-SINGLE-TITLE');
done();
});

View File

@@ -18,7 +18,6 @@
import { Injectable } from '@angular/core';
import { Response } from '@angular/http';
import { Observable } from 'rxjs/Observable';
import { SiteModel } from '../models/site.model';
import { AlfrescoApiService } from './alfresco-api.service';
import 'rxjs/add/observable/fromPromise';
import 'rxjs/add/operator/catch';
@@ -36,13 +35,11 @@ export class SitesService {
};
const queryOptions = Object.assign({}, defaultOptions, opts);
return Observable.fromPromise(this.apiService.getInstance().core.sitesApi.getSites(queryOptions))
.map((res) => this.convertToModel(res))
.catch(this.handleError);
}
getSite(siteId: string, opts?: any): any {
return Observable.fromPromise(this.apiService.getInstance().core.sitesApi.getSite(siteId, opts))
.map((res: any) => new SiteModel(res))
.catch(this.handleError);
}
@@ -65,18 +62,4 @@ export class SitesService {
console.error(error);
return Observable.throw(error || 'Server error');
}
private convertToModel(response: any) {
let convertedList: SiteModel[] = [];
if (response &&
response.list &&
response.list.entries &&
response.list.entries.length > 0) {
response.list.entries.forEach((element: any) => {
element.pagination = response.list.pagination;
convertedList.push(new SiteModel(element));
});
}
return convertedList;
}
}