diff --git a/demo-shell/src/app/components/files/files.component.ts b/demo-shell/src/app/components/files/files.component.ts index 92ab592679..12df8960f7 100644 --- a/demo-shell/src/app/components/files/files.component.ts +++ b/demo-shell/src/app/components/files/files.component.ts @@ -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() { diff --git a/docs/README.md b/docs/README.md index d75ed9ac39..77cc1d8898 100644 --- a/docs/README.md +++ b/docs/README.md @@ -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) diff --git a/docs/content-node-selector.component.md b/docs/content-node-selector.component.md index 509d1615eb..9630284316 100644 --- a/docs/content-node-selector.component.md +++ b/docs/content-node-selector.component.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() + select: new Subject() }; 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 + + +``` + +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 diff --git a/docs/sites-dropdown.component.md b/docs/sites-dropdown.component.md index b82bb12d9e..4011eacacf 100644 --- a/docs/sites-dropdown.component.md +++ b/docs/sites-dropdown.component.md @@ -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 diff --git a/lib/content-services/content-node-selector/content-node-dialog.service.spec.ts b/lib/content-services/content-node-selector/content-node-dialog.service.spec.ts new file mode 100644 index 0000000000..dca9d95a24 --- /dev/null +++ b/lib/content-services/content-node-selector/content-node-dialog.service.spec.ts @@ -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 = { + 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(); + }); + +}); diff --git a/lib/content-services/content-node-selector/content-node-dialog.service.ts b/lib/content-services/content-node-selector/content-node-dialog.service.ts new file mode 100644 index 0000000000..2bfa6cca2d --- /dev/null +++ b/lib/content-services/content-node-selector/content-node-dialog.service.ts @@ -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 { + if (this.contentService.hasPermission(contentEntry, permission)) { + const select = new Subject(); + 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(); + } +} diff --git a/lib/content-services/content-node-selector/content-node-selector-panel.component.html b/lib/content-services/content-node-selector/content-node-selector-panel.component.html new file mode 100644 index 0000000000..ef18759562 --- /dev/null +++ b/lib/content-services/content-node-selector/content-node-selector-panel.component.html @@ -0,0 +1,95 @@ +
+ + + + clear + + + search + + + + + + + + + + + + + +
+ + + +
{{ 'NODE_SELECTOR.NO_RESULTS' | translate }}
+
+
+ + + + + + + + + + + + +
+ + + {{ 'ADF-DOCUMENT-LIST.LAYOUT.LOAD_MORE' | translate }} + +
+
diff --git a/lib/content-services/content-node-selector/content-node-selector-panel.component.scss b/lib/content-services/content-node-selector/content-node-selector-panel.component.scss new file mode 100644 index 0000000000..50024727fc --- /dev/null +++ b/lib/content-services/content-node-selector/content-node-selector-panel.component.scss @@ -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; + } + } + } + } + } + } + } + +} diff --git a/lib/content-services/content-node-selector/content-node-selector-panel.component.spec.ts b/lib/content-services/content-node-selector/content-node-selector-panel.component.spec.ts new file mode 100644 index 0000000000..d007a2c814 --- /dev/null +++ b/lib/content-services/content-node-selector/content-node-selector-panel.component.spec.ts @@ -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; + let searchService: SearchService; + let searchSpy: jasmine.Spy; + + let _observer: Observer; + + 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) => { + _observer = observer; + }); + }); + }); + + describe('Parameters', () => { + + it('should trigger the select event when selection has been made', (done) => { + const expectedNode = {}; + 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 = { 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 = { 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 = { 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 = { 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 = {}; + 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( { 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 = {}; + 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( { 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( { 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 = { list: {entries: [{}]}}; + const nodePage: 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: {} } } }); + fixture.detectChanges(); + + component.select.subscribe((nodes) => { + expect(nodes).toBeDefined(); + expect(nodes).not.toBeNull(); + }); + + component.resetChosenNode(); + fixture.detectChanges(); + }); + }); + }); +}); diff --git a/lib/content-services/content-node-selector/content-node-selector-panel.component.ts b/lib/content-services/content-node-selector/content-node-selector-panel.component.ts new file mode 100644 index 0000000000..f23ce93fda --- /dev/null +++ b/lib/content-services/content-node-selector/content-node-selector-panel.component.ts @@ -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 = new EventEmitter(); + + @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); + }); + } + } +} diff --git a/lib/content-services/content-node-selector/content-node-selector.component-data.interface.ts b/lib/content-services/content-node-selector/content-node-selector.component-data.interface.ts index 66aaff996a..1a8a48fe4d 100644 --- a/lib/content-services/content-node-selector/content-node-selector.component-data.interface.ts +++ b/lib/content-services/content-node-selector/content-node-selector.component-data.interface.ts @@ -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; + select: Subject; } diff --git a/lib/content-services/content-node-selector/content-node-selector.component.html b/lib/content-services/content-node-selector/content-node-selector.component.html index 709dbc122c..6cd69b4732 100644 --- a/lib/content-services/content-node-selector/content-node-selector.component.html +++ b/lib/content-services/content-node-selector/content-node-selector.component.html @@ -1,111 +1,22 @@
{{title}} + class="adf-content-node-selector-dialog-title" + data-automation-id="content-node-selector-title">{{title || data?.title}}
- - - - - clear - - - search - - - - - - - - - - - - - -
- - - -
{{ 'NODE_SELECTOR.NO_RESULTS' | translate }}
-
-
- - - - - - - - - - - - -
- - - {{ 'ADF-DOCUMENT-LIST.LAYOUT.LOAD_MORE' | translate }} - -
- + class="adf-content-node-selector-dialog-content"> + +
- -
+ diff --git a/lib/content-services/content-node-selector/content-node-selector.component.scss b/lib/content-services/content-node-selector/content-node-selector.component.scss index 0d604902b0..c00ce02870 100644 --- a/lib/content-services/content-node-selector/content-node-selector.component.scss +++ b/lib/content-services/content-node-selector/content-node-selector.component.scss @@ -1,207 +1,56 @@ -@mixin adf-content-node-selector-theme($theme) { +@mixin adf-content-node-selector-dialog-theme($theme) { $primary: map-get($theme, primary); $foreground: map-get($theme, foreground); $background: map-get($theme, background); - .adf-content-node-selector-dialog { + .mat-dialog-container { + padding: 0; + } - .mat-dialog-container { - padding: 0; + .adf-content-node-selector-dialog { + &-title, + &-content, + &-actions { + padding: 16px; + margin: 0; } - .adf-content-node-selector { - &-title, - &-content, - &-actions { - padding: 16px; - margin: 0; - } + &-content{ + padding-top: 0; + } - &-title::first-letter { + &-title::first-letter { + text-transform: uppercase; + } + + &-actions { + padding: 8px; + background-color: mat-color($background, background); + display: flex; + justify-content: flex-end; + color: mat-color($foreground, secondary-text); + + button { text-transform: uppercase; } - &-content { - padding-top: 0; - - &-input { - width: 100%; - - &-icon { - color: mat-color($foreground, disabled-button); - cursor: pointer; - - &:hover { - color: mat-color($foreground, base); - } - } - } - - .mat-input-underline .mat-input-ripple { - height: 1px; - transition: none; - } - - .adf-site-dropdown-container { - .mat-form-field { - display: block; - margin-bottom: 15px; - } - } - - .adf-site-dropdown-list-element { - width: 100%; - margin-bottom: 0; - - .mat-select-trigger { - font-size: 14px; - } - - } - - .adf-toolbar .mat-toolbar { - border-bottom-width: 0; - font-size: 14px; - - &.mat-toolbar-single-row { - height: auto; - } - } - - &-breadcrumb { - .adf-dropdown-breadcumb-trigger { - outline: none; - .mat-icon { - color: mat-color($foreground, base, 0.45); - - &:hover { - color: mat-color($foreground, base, 0.65); - } - } - } - - .adf-dropddown-breadcrumb-item-chevron { - color: mat-color($foreground, base, 0.45); - } - } - - &-list { - height: 200px; - overflow: auto; - border: 1px solid mat-color($foreground, base, 0.07); - - .adf-highlight { - color: mat-color($primary); - } - - .adf-data-table { - border: none; - - .adf-no-content-container { - text-align: center; - } - - thead { - display: none; - } - - .adf-data-table-cell { - padding-top: 8px; - padding-bottom: 8px; - height: 30px; - - & .adf-name-location-cell-location { - display: none; - } - - & .adf-name-location-cell-name { - padding: 0; - } - - &--image { - padding-left: 16px; - padding-right: 8px; - } - - &--text { - padding-left: 8px; - } - } - - tbody tr { - height: auto !important; - - &:first-child { - .adf-data-table-cell { - border-top: none; - } - } - - &:last-child { - .adf-data-table-cell { - border-bottom: none; - } - } - } - } - - &-searchLayout { - - .adf-data-table { - .adf-data-table-cell { - height: 56px; - padding-bottom: 24px; - - & .adf-name-location-cell-location { - display: block - } - - & .adf-name-location-cell-name { - padding: 18px 0 2px 0; - } - - &.adf-content-selector-modified-cell { - display: none; - } - - &.adf-content-selector-modifier-cell { - display: none; - } - } - } - } - } + &:last-child { + margin-bottom: 0px; } - &-actions { - padding: 8px; - background-color: mat-color($background, background); - display: flex; - justify-content: flex-end; - color: mat-color($foreground, secondary-text); + &-cancel { + font-weight: normal; + } - button { - text-transform: uppercase; + &-choose { + font-weight: normal; + + &[disabled] { + opacity: 0.6; } - &:last-child { - margin-bottom: 0px; - } - - &-cancel { - font-weight: normal; - } - - &-choose { - font-weight: normal; - - &[disabled] { - opacity: 0.6; - } - - &:enabled { - color: mat-color($primary); - } + &:enabled { + color: mat-color($primary); } } } diff --git a/lib/content-services/content-node-selector/content-node-selector.component.spec.ts b/lib/content-services/content-node-selector/content-node-selector.component.spec.ts index 68da340aaf..31be93bfeb 100644 --- a/lib/content-services/content-node-selector/content-node-selector.component.spec.ts +++ b/lib/content-services/content-node-selector/content-node-selector.component.spec.ts @@ -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; let data: any; - let searchService: SearchService; - let searchSpy: jasmine.Spy; - let apiService: AlfrescoApiService; - let nodesApi; - - let _observer: Observer; - - function typeToSearchBox(searchTerm = 'string-to-search') { - let searchInput = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-search-input"]')); - searchInput.nativeElement.value = searchTerm; - component.searchInput.setValue(searchTerm); - fixture.detectChanges(); - } - - function respondWithSearchResults(result) { - _observer.next(result); - } function setupTestbed(plusProviders) { TestBed.configureTestingModule({ imports: [ - DataTableModule, MaterialModule ], declarations: [ + ContentNodeSelectorComponent, + ContentNodeSelectorPanelComponent, DocumentListComponent, - EmptyFolderContentDirective, - DropdownSitesComponent, - DropdownBreadcrumbComponent, - ContentNodeSelectorComponent + EmptyFolderContentDirective ], providers: [ + ContentNodeSelectorService, + ContentNodeSelectorPanelComponent, + DocumentListService, AlfrescoApiService, ContentService, - SearchService, - TranslationService, DocumentListService, - SitesService, ContentNodeSelectorService, - UserPreferencesService, ...plusProviders ], schemas: [CUSTOM_ELEMENTS_SCHEMA] @@ -112,685 +68,100 @@ describe('ContentNodeSelectorComponent', () => { TestBed.resetTestingModule(); }); - describe('Dialog features', () => { + beforeEach(async(() => { + data = { + title: 'Move along citizen...', + actionName: 'move', + select: new EventEmitter(), + rowFilter: () => { + }, + imageResolver: () => 'piccolo', + currentFolderId: 'cat-girl-nuku-nuku' + }; - beforeEach(async(() => { - data = { - title: 'Move along citizen...', - actionName: 'move', - select: new EventEmitter(), - rowFilter: () => { - }, - imageResolver: () => 'piccolo', - currentFolderId: 'cat-girl-nuku-nuku' - }; + setupTestbed([{ provide: MAT_DIALOG_DATA, useValue: data }]); + TestBed.compileComponents(); + })); - setupTestbed([{ provide: MAT_DIALOG_DATA, useValue: data }]); - TestBed.compileComponents(); - })); + beforeEach(() => { + fixture = TestBed.createComponent(ContentNodeSelectorComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); - beforeEach(() => { - fixture = TestBed.createComponent(ContentNodeSelectorComponent); - component = fixture.componentInstance; + describe('Data injecting with the "Material dialog way"', () => { + + it('should show the INJECTED title', () => { + const titleElement = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-title"]')); + expect(titleElement).not.toBeNull(); + expect(titleElement.nativeElement.innerText).toBe('Move along citizen...'); + }); + + it('should have the INJECTED actionName on the name of the choose button', () => { + const actionButton = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-actions-choose"]')); + expect(actionButton).not.toBeNull(); + expect(actionButton.nativeElement.innerText).toBe('NODE_SELECTOR.MOVE'); + }); + + it('should pass through the injected currentFolderId to the documentlist', () => { + let documentList = fixture.debugElement.query(By.directive(DocumentListComponent)); + expect(documentList).not.toBeNull('Document list should be shown'); + expect(documentList.componentInstance.currentFolderId).toBe('cat-girl-nuku-nuku'); + }); + + it('should pass through the injected rowFilter to the documentlist', () => { + let documentList = fixture.debugElement.query(By.directive(DocumentListComponent)); + expect(documentList).not.toBeNull('Document list should be shown'); + expect(documentList.componentInstance.rowFilter).toBe(data.rowFilter); + }); + + it('should pass through the injected imageResolver to the documentlist', () => { + let documentList = fixture.debugElement.query(By.directive(DocumentListComponent)); + expect(documentList).not.toBeNull('Document list should be shown'); + expect(documentList.componentInstance.imageResolver).toBe(data.imageResolver); + }); + + }); + + describe('Cancel button', () => { + + it('should complete the data stream when user click "CANCEL"', () => { + let cancelButton; + data.select.subscribe( + () => { }, + () => { }, + () => { + cancelButton = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-actions-cancel"]')); + expect(cancelButton).not.toBeNull(); + }); + + cancelButton = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-actions-cancel"]')); + cancelButton.triggerEventHandler('click', {}); + }); + + it('should not be shown if dialogRef is NOT injected', () => { + const closeButton = fixture.debugElement.query(By.css('[content-node-selector-actions-cancel]')); + expect(closeButton).toBeNull(); + }); + }); + + describe('Action button for the chosen node', () => { + + it('should be disabled by default', () => { fixture.detectChanges(); + + let actionButton = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-actions-choose"]')); + expect(actionButton.nativeElement.disabled).toBeTruthy(); }); - describe('Data injecting with the "Material dialog way"', () => { + it('should be enabled when a node is chosen', () => { + component.onSelect([{ id: 'fake' }]); + fixture.detectChanges(); - it('should show the INJECTED title', () => { - const titleElement = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-title"]')); - expect(titleElement).not.toBeNull(); - expect(titleElement.nativeElement.innerText).toBe('Move along citizen...'); - }); - - it('should have the INJECTED actionName on the name of the choose button', () => { - const actionButton = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-actions-choose"]')); - expect(actionButton).not.toBeNull(); - expect(actionButton.nativeElement.innerText).toBe('NODE_SELECTOR.MOVE'); - }); - - it('should pass through the injected currentFolderId to the documentlist', () => { - let documentList = fixture.debugElement.query(By.directive(DocumentListComponent)); - expect(documentList).not.toBeNull('Document list should be shown'); - expect(documentList.componentInstance.currentFolderId).toBe('cat-girl-nuku-nuku'); - }); - - it('should pass through the injected rowFilter to the documentlist', () => { - let documentList = fixture.debugElement.query(By.directive(DocumentListComponent)); - expect(documentList).not.toBeNull('Document list should be shown'); - expect(documentList.componentInstance.rowFilter).toBe(data.rowFilter); - }); - - it('should pass through the injected imageResolver to the documentlist', () => { - let documentList = fixture.debugElement.query(By.directive(DocumentListComponent)); - expect(documentList).not.toBeNull('Document list should be shown'); - expect(documentList.componentInstance.imageResolver).toBe(data.imageResolver); - }); - - it('should trigger the INJECTED select event when selection has been made', (done) => { - const expectedNode = {}; - data.select.subscribe((nodes) => { - expect(nodes.length).toBe(1); - expect(nodes[0]).toBe(expectedNode); - done(); - }); - - component.chosenNode = expectedNode; - component.choose(); - }); + let actionButton = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-actions-choose"]')); + expect(actionButton.nativeElement.disabled).toBeFalsy(); }); - describe('Cancel button', () => { - - let dummyMdDialogRef; - let fakePreference: UserPreferencesService = jasmine.createSpyObj('UserPreferencesService', ['paginationSize']); - fakePreference.paginationSize = 10; - - beforeEach(() => { - dummyMdDialogRef = > { - close: () => { - } - }; - }); - - it('should be shown if dialogRef is injected', () => { - const componentInstance = new ContentNodeSelectorComponent(null, null, null, fakePreference, data, dummyMdDialogRef); - expect(componentInstance.inDialog).toBeTruthy(); - }); - - it('should should call the close method in the injected dialogRef', () => { - spyOn(dummyMdDialogRef, 'close'); - const componentInstance = new ContentNodeSelectorComponent(null, null, null, fakePreference, data, dummyMdDialogRef); - - componentInstance.close(); - - expect(dummyMdDialogRef.close).toHaveBeenCalled(); - }); - }); }); - describe('General component features', () => { - - beforeEach(async(() => { - setupTestbed([]); - TestBed.compileComponents(); - })); - - beforeEach(() => { - fixture = TestBed.createComponent(ContentNodeSelectorComponent); - component = fixture.componentInstance; - component.debounceSearch = 0; - - searchService = TestBed.get(SearchService); - searchSpy = spyOn(searchService, 'search').and.callFake(() => { - return Observable.create((observer: Observer) => { - _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 = {}; - 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 = { 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 = { 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 = { 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 = { 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 = {}; - 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( { 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 = {}; - 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( { 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( { guid: undefined }); - fixture.detectChanges(); - - documentList = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-document-list"]')); - expect(documentList.componentInstance.currentFolderId).toBe('cat-girl-nuku-nuku'); - - done(); - }); - - describe('Pagination "Load more" button', () => { - - it('should NOT be shown by default', () => { - fixture.detectChanges(); - const pagination = fixture.debugElement.query(By.css('[data-automation-id="adf-infinite-pagination-button"]')); - expect(pagination).toBeNull(); - }); - - it('should be shown when diplaying search results', (done) => { - typeToSearchBox('shenron'); - - setTimeout(() => { - respondWithSearchResults(ONE_FOLDER_RESULT); - - fixture.whenStable().then(() => { - fixture.detectChanges(); - const pagination = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-search-pagination"]')); - expect(pagination).not.toBeNull(); - done(); - }); - }, 300); - }); - - it('button\'s callback should load the next batch of results by calling the search api', () => { - const skipCount = 8; - component.searchTerm = 'kakarot'; - - component.getNextPageOfSearch({ skipCount }); - - expect(searchSpy).toHaveBeenCalledWith(defaultSearchOptions('kakarot', undefined, skipCount)); - }); - - it('should set its loading state to true after search was started', (done) => { - component.showingSearchResults = true; - component.pagination = { hasMoreItems: true }; - - typeToSearchBox('shenron'); - - setTimeout(() => { - fixture.detectChanges(); - - const spinnerSelector = By.css('[data-automation-id="content-node-selector-search-pagination"] [data-automation-id="adf-infinite-pagination-spinner"]'); - const paginationLoading = fixture.debugElement.query(spinnerSelector); - expect(paginationLoading).not.toBeNull(); - done(); - }, 300); - }); - - it('should set its loading state to true after search was performed', (done) => { - component.showingSearchResults = true; - component.pagination = { hasMoreItems: true }; - - typeToSearchBox('shenron'); - fixture.detectChanges(); - - setTimeout(() => { - respondWithSearchResults(ONE_FOLDER_RESULT); - - fixture.whenStable().then(() => { - fixture.detectChanges(); - const spinnerSelector = By.css('[data-automation-id="content-node-selector-search-pagination"] [data-automation-id="adf-infinite-pagination-spinner"]'); - const paginationLoading = fixture.debugElement.query(spinnerSelector); - expect(paginationLoading).toBeNull(); - done(); - }); - }, 300); - }); - }); - }); - - describe('Cancel button', () => { - - it('should not be shown if dialogRef is NOT injected', () => { - const closeButton = fixture.debugElement.query(By.css('[content-node-selector-actions-cancel]')); - expect(closeButton).toBeNull(); - }); - }); - - describe('Action button for the chosen node', () => { - - const entry: MinimalNodeEntryEntity = {}; - let hasPermission; - - beforeEach(() => { - const alfrescoContentService = TestBed.get(ContentService); - spyOn(alfrescoContentService, 'hasPermission').and.callFake(() => hasPermission); - }); - - it('should be disabled by default', () => { - fixture.detectChanges(); - - let actionButton = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-actions-choose"]')); - expect(actionButton.nativeElement.disabled).toBe(true); - }); - - it('should become enabled after loading node with the necessary permissions', () => { - hasPermission = true; - component.documentList.folderNode = entry; - component.documentList.ready.emit(); - fixture.detectChanges(); - - let actionButton = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-actions-choose"]')); - expect(actionButton.nativeElement.disabled).toBe(false); - }); - - it('should remain disabled after loading node without the necessary permissions', () => { - hasPermission = false; - component.documentList.folderNode = entry; - component.documentList.ready.emit(); - fixture.detectChanges(); - - let actionButton = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-actions-choose"]')); - expect(actionButton.nativeElement.disabled).toBe(true); - }); - - it('should be enabled when clicking on a node (with the right permissions) in the list (onNodeSelect)', () => { - hasPermission = true; - - component.onNodeSelect({ detail: { node: { entry } } }); - fixture.detectChanges(); - - let actionButton = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-actions-choose"]')); - expect(actionButton.nativeElement.disabled).toBe(false); - }); - - it('should remain disabled when clicking on a node (with the WRONG permissions) in the list (onNodeSelect)', () => { - hasPermission = false; - - component.onNodeSelect({ detail: { node: { entry } } }); - fixture.detectChanges(); - - let actionButton = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-actions-choose"]')); - expect(actionButton.nativeElement.disabled).toBe(true); - }); - - it('should become disabled when clicking on a node (with the WRONG permissions) after previously selecting a right node', () => { - hasPermission = true; - component.onNodeSelect({ detail: { node: { entry } } }); - fixture.detectChanges(); - - hasPermission = false; - component.onNodeSelect({ detail: { node: { entry } } }); - fixture.detectChanges(); - - let actionButton = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-actions-choose"]')); - expect(actionButton.nativeElement.disabled).toBe(true); - }); - - it('should be disabled when resetting the chosen node', () => { - hasPermission = true; - component.onNodeSelect({ detail: { node: { entry: {} } } }); - 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 - ); - }); - - }); - }); }); diff --git a/lib/content-services/content-node-selector/content-node-selector.component.ts b/lib/content-services/content-node-selector/content-node-selector.component.ts index 3951b0fda5..5703253be2 100644 --- a/lib/content-services/content-node-selector/content-node-selector.component.ts +++ b/lib/content-services/content-node-selector/content-node-selector.component.ts @@ -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 = new EventEmitter(); + 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) { - if (data) { - this.title = data.title; - this.actionName = data.actionName; - this.select = data.select; - this.currentFolderId = data.currentFolderId; - this.dropdownHideMyFiles = data.dropdownHideMyFiles; - this.dropdownSiteList = data.dropdownSiteList; - this.rowFilter = data.rowFilter; - this.imageResolver = data.imageResolver; - } - this.buttonActionName = this.actionName ? `NODE_SELECTOR.${this.actionName.toUpperCase()}` : 'NODE_SELECTOR.CHOOSE'; - - if (this.containingDialog) { - this.inDialog = true; - } - - this.searchInput.valueChanges - .pipe( - debounceTime(this.debounceSearch) - ) - .subscribe((searchValue) => { - this.search(searchValue); - }); - - this.pageSize = this.preferences.paginationSize; + constructor(@Inject(MAT_DIALOG_DATA) public data: ContentNodeSelectorComponentData) { + this.buttonActionName = data.actionName ? `NODE_SELECTOR.${data.actionName.toUpperCase()}` : 'NODE_SELECTOR.CHOOSE'; } - ngOnInit() { - this.folderIdToShow = this.currentFolderId; - this.paginationStrategy = PaginationStrategy.Infinite; + close() { + this.data.select.complete(); } - /** - * Updates the site attribute and starts a new search - * - * @param chosenSite Sitemodel to search within - */ - siteChanged(chosenSite: SiteModel): void { - this.siteId = chosenSite.guid; - this.updateResults(); + onSelect(nodeList: MinimalNodeEntryEntity[]) { + this.chosenNode = nodeList; } - /** - * Updates the searchTerm attribute and starts a new search - * - * @param searchTerm string value to search against - */ - search(searchTerm: string): void { - this.searchTerm = searchTerm; - this.updateResults(); - } - - /** - * Returns whether breadcrumb has to be shown or not - */ - needBreadcrumbs() { - const whenInFolderNavigation = !this.showingSearchResults, - whenInSelectingSearchResult = this.showingSearchResults && this.chosenNode; - - return whenInFolderNavigation || whenInSelectingSearchResult; - } - - /** - * Returns the actually selected|entered folder node or null in case of searching for the breadcrumb - */ - get breadcrumbFolderNode(): MinimalNodeEntryEntity | null { - if (this.showingSearchResults && this.chosenNode) { - return this.chosenNode; - } else { - return this.documentList.folderNode; - } - } - - /** - * Clear the search input - */ - clear(): void { - this.searchTerm = ''; - this.nodes = null; - this.skipCount = 0; - this.chosenNode = null; - this.showingSearchResults = false; - this.folderIdToShow = this.currentFolderId; - } - - /** - * Update the result list depending on the criterias - */ - private updateResults(): void { - if (this.searchTerm.length === 0) { - this.folderIdToShow = this.siteId || this.currentFolderId; - } else { - this.startNewSearch(); - } - } - - /** - * Load the first page of a new search result - */ - private startNewSearch(): void { - this.nodes = null; - this.skipCount = 0; - this.chosenNode = null; - this.folderIdToShow = null; - this.querySearch(); - } - - /** - * Loads the next batch of search results - * - * @param event Pagination object - */ - getNextPageOfSearch(event: Pagination): void { - this.infiniteScroll = true; - this.skipCount = event.skipCount; - this.querySearch(); - } - - /** - * Perform the call to searchService with the proper parameters - */ - private querySearch(): void { - this.loadingSearchResults = true; - - this.contentNodeSelectorService.search(this.searchTerm, this.siteId, this.skipCount, this.pageSize) - .subscribe(this.showSearchResults.bind(this)); - } - - /** - * Show the results of the search - * - * @param results Search results - */ - private showSearchResults(results: NodePaging): void { - this.showingSearchResults = true; - this.loadingSearchResults = false; - - // Documentlist hack, since data displaying for preloaded nodes is a little bit messy there - if (!this.nodes) { - this.nodes = results; - } else { - this.documentList.data.loadPage(results, true); - } - - this.pagination = results.list.pagination; - this.highlight(); - } - - /** - * Hightlight the actual searchterm in the next frame - */ - highlight(): void { - setTimeout(() => { - this.highlighter.highlight(this.searchTerm); - }, 0); - } - - /** - * Invoked when user selects a node - * - * @param event CustomEvent for node-select - */ - onNodeSelect(event: any): void { - this.attemptNodeSelection(event.detail.node.entry); - } - - /** - * Sets showingSearchResults state to be able to differentiate between search results or folder results - */ - onFolderChange(): void { - this.skipCount = 0; - this.infiniteScroll = false; - this.showingSearchResults = false; - } - - /** - * Attempts to set the currently loaded node - */ - onFolderLoaded(nodePage: NodePaging): void { - this.attemptNodeSelection(this.documentList.folderNode); - this.pagination = nodePage.list.pagination; - } - - /** - * Selects node as chosen if it has the right permission, clears the selection otherwise - * - * @param entry - */ - private attemptNodeSelection(entry: MinimalNodeEntryEntity): void { - if (this.contentService.hasPermission(entry, 'create')) { - this.chosenNode = entry; - } else { - this.resetChosenNode(); - } - } - - /** - * Clears the chosen node - */ - resetChosenNode(): void { - this.chosenNode = null; - } - - /** - * Emit event with the chosen node - */ - choose(): void { - const entry: any = this.chosenNode; - - if (entry && entry.guid) { - const options = { - include: ['path', 'properties', 'allowableOperations'] - }; - this.apiService.nodesApi.getNode(entry.guid, options) - .then(chosenSiteNode => { - this.select.next([chosenSiteNode.entry]); - }); - - } else { - this.select.next([this.chosenNode]); - - } - } - - /** - * Close the dialog - */ - close(): void { - this.containingDialog.close(); - } - - onNodeDoubleClick(e: CustomEvent) { - const node: any = e.detail.node.entry; - - if (node && node.guid) { - const options = { - maxItems: this.pageSize, - skipCount: this.skipCount, - include: ['path', 'properties', 'allowableOperations'] - }; - - this.apiService.nodesApi.getNode(node.guid, options) - .then(documentLibrary => { - this.documentList.performCustomSourceNavigation(documentLibrary); - }); - } + onClick(): void { + this.data.select.next(this.chosenNode); + this.data.select.complete(); } } diff --git a/lib/content-services/content-node-selector/content-node-selector.module.ts b/lib/content-services/content-node-selector/content-node-selector.module.ts index 780775ce05..d9c974a4bf 100644 --- a/lib/content-services/content-node-selector/content-node-selector.module.ts +++ b/lib/content-services/content-node-selector/content-node-selector.module.ts @@ -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 {} diff --git a/lib/content-services/content-node-selector/public-api.ts b/lib/content-services/content-node-selector/public-api.ts index 585c742fdf..44f25b54f6 100644 --- a/lib/content-services/content-node-selector/public-api.ts +++ b/lib/content-services/content-node-selector/public-api.ts @@ -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'; diff --git a/lib/content-services/document-list/components/content-action/content-action.component.spec.ts b/lib/content-services/document-list/components/content-action/content-action.component.spec.ts index a22c3ff1df..f30f94f7a9 100644 --- a/lib/content-services/document-list/components/content-action/content-action.component.spec.ts +++ b/lib/content-services/document-list/components/content-action/content-action.component.spec.ts @@ -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); diff --git a/lib/content-services/document-list/services/document-actions.service.spec.ts b/lib/content-services/document-list/services/document-actions.service.spec.ts index f49fb7a620..d6b83d5cf4 100644 --- a/lib/content-services/document-list/services/document-actions.service.spec.ts +++ b/lib/content-services/document-list/services/document-actions.service.spec.ts @@ -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); }); diff --git a/lib/content-services/document-list/services/folder-actions.service.spec.ts b/lib/content-services/document-list/services/folder-actions.service.spec.ts index c32baa93d7..371d4846ab 100644 --- a/lib/content-services/document-list/services/folder-actions.service.spec.ts +++ b/lib/content-services/document-list/services/folder-actions.service.spec.ts @@ -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(); })); diff --git a/lib/content-services/document-list/services/node-actions.service.service.spec.ts b/lib/content-services/document-list/services/node-actions.service.service.spec.ts new file mode 100644 index 0000000000..600a94d472 --- /dev/null +++ b/lib/content-services/document-list/services/node-actions.service.service.spec.ts @@ -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 = { + 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'); + }); + })); + +}); diff --git a/lib/content-services/document-list/services/node-actions.service.ts b/lib/content-services/document-list/services/node-actions.service.ts index 42ccb6e951..fbbad226f2 100644 --- a/lib/content-services/document-list/services/node-actions.service.ts +++ b/lib/content-services/document-list/services/node-actions.service.ts @@ -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 { const observable: Subject = new Subject(); - 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() - }; - - 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) + 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; + return observable; } } diff --git a/lib/content-services/site-dropdown/sites-dropdown.component.html b/lib/content-services/site-dropdown/sites-dropdown.component.html index 941335de2c..0663175877 100644 --- a/lib/content-services/site-dropdown/sites-dropdown.component.html +++ b/lib/content-services/site-dropdown/sites-dropdown.component.html @@ -9,8 +9,8 @@ [(ngModel)]="siteSelected" (ngModelChange)="selectedSite()"> {{'DROPDOWN.MY_FILES_OPTION' | translate}} - - {{ site.title | translate }} + + {{ site.entry.title | translate}} diff --git a/lib/content-services/site-dropdown/sites-dropdown.component.spec.ts b/lib/content-services/site-dropdown/sites-dropdown.component.spec.ts index feddc14f95..821f0fa419 100644 --- a/lib/content-services/site-dropdown/sites-dropdown.component.spec.ts +++ b/lib/content-services/site-dropdown/sites-dropdown.component.spec.ts @@ -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(); }); }); diff --git a/lib/content-services/site-dropdown/sites-dropdown.component.ts b/lib/content-services/site-dropdown/sites-dropdown.component.ts index cf86745d9c..0949b44912 100644 --- a/lib/content-services/site-dropdown/sites-dropdown.component.ts +++ b/lib/content-services/site-dropdown/sites-dropdown.component.ts @@ -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 = new EventEmitter(); + change: EventEmitter = 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; }, diff --git a/lib/content-services/styles/_index.scss b/lib/content-services/styles/_index.scss index fa1d3c35bd..198d7257c6 100644 --- a/lib/content-services/styles/_index.scss +++ b/lib/content-services/styles/_index.scss @@ -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) ; } diff --git a/lib/content-services/webscript/webscript.component.spec.ts b/lib/content-services/webscript/webscript.component.spec.ts index a578f5dfe4..b765fc60a0 100644 --- a/lib/content-services/webscript/webscript.component.spec.ts +++ b/lib/content-services/webscript/webscript.component.spec.ts @@ -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'; diff --git a/lib/core/form/components/widgets/upload-folder/upload-folder.widget.html b/lib/core/form/components/widgets/upload-folder/upload-folder.widget.html new file mode 100644 index 0000000000..abe6ce2694 --- /dev/null +++ b/lib/core/form/components/widgets/upload-folder/upload-folder.widget.html @@ -0,0 +1,7 @@ +
+ +
+
+
diff --git a/lib/core/form/components/widgets/upload-folder/upload-folder.widget.scss b/lib/core/form/components/widgets/upload-folder/upload-folder.widget.scss new file mode 100644 index 0000000000..2dddb9ee90 --- /dev/null +++ b/lib/core/form/components/widgets/upload-folder/upload-folder.widget.scss @@ -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; + } +} diff --git a/lib/core/form/components/widgets/upload-folder/upload-folder.widget.spec.ts b/lib/core/form/components/widgets/upload-folder/upload-folder.widget.spec.ts new file mode 100644 index 0000000000..58ad8b656b --- /dev/null +++ b/lib/core/form/components/widgets/upload-folder/upload-folder.widget.spec.ts @@ -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. + */ diff --git a/lib/core/form/components/widgets/upload-folder/upload-folder.widget.ts b/lib/core/form/components/widgets/upload-folder/upload-folder.widget.ts new file mode 100644 index 0000000000..31df392ea2 --- /dev/null +++ b/lib/core/form/components/widgets/upload-folder/upload-folder.widget.ts @@ -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 { + 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); + } + ); + } +} diff --git a/lib/core/models/public-api.ts b/lib/core/models/public-api.ts index 21f4d19b7c..3f85af65ce 100644 --- a/lib/core/models/public-api.ts +++ b/lib/core/models/public-api.ts @@ -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'; diff --git a/lib/core/models/site.model.ts b/lib/core/models/site.model.ts deleted file mode 100644 index 540b452b18..0000000000 --- a/lib/core/models/site.model.ts +++ /dev/null @@ -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)); - }); - } - } - } - -} diff --git a/lib/core/services/sites-api.spec.ts b/lib/core/services/sites-api.spec.ts index a9a55de4d3..39abcb24a5 100644 --- a/lib/core/services/sites-api.spec.ts +++ b/lib/core/services/sites-api.spec.ts @@ -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(); }); diff --git a/lib/core/services/sites.service.ts b/lib/core/services/sites.service.ts index ce71f8b7e3..75b93ed804 100644 --- a/lib/core/services/sites.service.ts +++ b/lib/core/services/sites.service.ts @@ -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; - } }