mirror of
https://github.com/Alfresco/alfresco-ng2-components.git
synced 2025-07-24 17:32:15 +00:00
[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:
@@ -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() {
|
||||
|
@@ -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)
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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();
|
||||
});
|
||||
|
||||
});
|
@@ -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();
|
||||
}
|
||||
}
|
@@ -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>
|
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@@ -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[]>;
|
||||
}
|
||||
|
@@ -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>
|
||||
|
||||
|
@@ -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,162 +15,14 @@
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&-content{
|
||||
padding-top: 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-actions {
|
||||
padding: 8px;
|
||||
background-color: mat-color($background, background);
|
||||
@@ -205,5 +55,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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();
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
@@ -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();
|
||||
}
|
||||
}
|
||||
|
@@ -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 {}
|
||||
|
@@ -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';
|
||||
|
@@ -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);
|
||||
|
||||
|
@@ -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);
|
||||
});
|
||||
|
||||
|
@@ -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();
|
||||
}));
|
||||
|
@@ -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');
|
||||
});
|
||||
}));
|
||||
|
||||
});
|
@@ -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;
|
||||
}
|
||||
}
|
||||
|
@@ -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>
|
||||
|
@@ -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();
|
||||
});
|
||||
});
|
||||
|
@@ -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;
|
||||
},
|
||||
|
@@ -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) ;
|
||||
}
|
||||
|
@@ -37,7 +37,7 @@ describe('WebscriptComponent', () => {
|
||||
declarations: [
|
||||
WebscriptComponent
|
||||
]
|
||||
}).compileComponents().then(()=>{
|
||||
}).compileComponents().then(() => {
|
||||
let appConfig: AppConfigService = TestBed.get(AppConfigService);
|
||||
appConfig.config.ecmHost = 'http://localhost:9876/ecm';
|
||||
|
||||
|
@@ -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>
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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.
|
||||
*/
|
@@ -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);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
@@ -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';
|
||||
|
@@ -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));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -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();
|
||||
});
|
||||
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user