diff --git a/demo-shell-ng2/app/components/files/files.component.html b/demo-shell-ng2/app/components/files/files.component.html index 94d4ff49a2..16fc315961 100644 --- a/demo-shell-ng2/app/components/files/files.component.html +++ b/demo-shell-ng2/app/components/files/files.component.html @@ -117,6 +117,22 @@ + + + + + + + + Promise.resolve(fakeSearch) + } + } +}; diff --git a/ng2-components/ng2-alfresco-search/src/services/search.service.spec.ts b/ng2-components/ng2-alfresco-core/src/services/search.service.spec.ts similarity index 79% rename from ng2-components/ng2-alfresco-search/src/services/search.service.spec.ts rename to ng2-components/ng2-alfresco-core/src/services/search.service.spec.ts index f8e9a9aa08..b644993b9f 100644 --- a/ng2-components/ng2-alfresco-search/src/services/search.service.spec.ts +++ b/ng2-components/ng2-alfresco-core/src/services/search.service.spec.ts @@ -16,9 +16,17 @@ */ import { async, TestBed } from '@angular/core/testing'; -import { AlfrescoApiService, CoreModule } from 'ng2-alfresco-core'; import { fakeApi, fakeError, fakeSearch } from '../assets/search.service.mock'; +import { CookieServiceMock } from './../assets/cookie.service.mock'; +import { AlfrescoApiService } from './alfresco-api.service'; +import { AlfrescoSettingsService } from './alfresco-settings.service'; +import { AppConfigModule } from './app-config.service'; +import { AuthenticationService } from './authentication.service'; +import { CookieService } from './cookie.service'; +import { LogService } from './log.service'; import { SearchService } from './search.service'; +import { StorageService } from './storage.service'; +import { UserPreferencesService } from './user-preferences.service'; declare let jasmine: any; @@ -30,10 +38,18 @@ describe('SearchService', () => { beforeEach(async(() => { TestBed.configureTestingModule({ imports: [ - CoreModule + AppConfigModule ], providers: [ - SearchService + SearchService, + AuthenticationService, + AlfrescoApiService, + AlfrescoSettingsService, + AuthenticationService, + StorageService, + UserPreferencesService, + { provide: CookieService, useClass: CookieServiceMock }, + LogService ] }).compileComponents(); })); diff --git a/ng2-components/ng2-alfresco-search/src/services/search.service.ts b/ng2-components/ng2-alfresco-core/src/services/search.service.ts similarity index 91% rename from ng2-components/ng2-alfresco-search/src/services/search.service.ts rename to ng2-components/ng2-alfresco-core/src/services/search.service.ts index b7792c05e1..bf005d0a3b 100644 --- a/ng2-components/ng2-alfresco-search/src/services/search.service.ts +++ b/ng2-components/ng2-alfresco-core/src/services/search.service.ts @@ -17,8 +17,9 @@ import { Injectable } from '@angular/core'; import { NodePaging } from 'alfresco-js-api'; -import { AlfrescoApiService, AlfrescoAuthenticationService } from 'ng2-alfresco-core'; import { Observable } from 'rxjs/Rx'; +import { AlfrescoApiService } from './alfresco-api.service'; +import { AuthenticationService } from './authentication.service'; /** * Internal service used by Document List component. @@ -26,7 +27,7 @@ import { Observable } from 'rxjs/Rx'; @Injectable() export class SearchService { - constructor(public authService: AlfrescoAuthenticationService, + constructor(public authService: AuthenticationService, private apiService: AlfrescoApiService) { } diff --git a/ng2-components/ng2-alfresco-documentlist/README.md b/ng2-components/ng2-alfresco-documentlist/README.md index 82bbad4d9d..f6efef092f 100644 --- a/ng2-components/ng2-alfresco-documentlist/README.md +++ b/ng2-components/ng2-alfresco-documentlist/README.md @@ -32,6 +32,7 @@ - [Delete - Show notification message with no permission](#delete---show-notification-message-with-no-permission) - [Delete - Disable button checking the permission](#delete---disable-button-checking-the-permission) - [Download](#download) + - [Copy and move](#copy-and-move) + [Folder actions](#folder-actions) * [Context Menu](#context-menu) * [Navigation mode](#navigation-mode) @@ -630,9 +631,12 @@ You can define both folder and document actions at the same time. + icon="content_copy" + target="document" + title="copy" + permission="update" + [disableWithNoPermission]="true" + handler="copy"> @@ -647,6 +651,7 @@ You can define both folder and document actions at the same time. target="document" title="Delete with additional custom callback" handler="delete" + permission="delete" (execute)="myCustomActionAfterDelete($event)"> @@ -679,6 +684,8 @@ All document actions are rendered as a dropdown menu as on the picture below: The following action handlers are provided out-of-box: - **Download** (document) +- **Copy** (document, folder) +- **Move** (document, folder) - **Delete** (document, folder) All system handler names are case-insensitive, `handler="download"` and `handler="DOWNLOAD"` @@ -766,6 +773,40 @@ Initiates download of the corresponding document file. ![Download document action](docs/assets/document-action-download.png) +##### Copy and move + +Shows the destination chooser dialog for copy and move actions + +![Copy/move dialog](docs/assets/document-action-copymovedialog.png) + +```html + + + + + + + + + + + +``` + +![Copy/move document action](docs/assets/document-action-copymove.png) + #### Folder actions Folder actions have the same declaration as document actions except ```taget="folder"``` attribute value. You can define system, custom or combined handlers as well just as with the document actions. diff --git a/ng2-components/ng2-alfresco-documentlist/docs/assets/document-action-copymove.png b/ng2-components/ng2-alfresco-documentlist/docs/assets/document-action-copymove.png new file mode 100644 index 0000000000..8325b86928 Binary files /dev/null and b/ng2-components/ng2-alfresco-documentlist/docs/assets/document-action-copymove.png differ diff --git a/ng2-components/ng2-alfresco-documentlist/docs/assets/document-action-copymovedialog.png b/ng2-components/ng2-alfresco-documentlist/docs/assets/document-action-copymovedialog.png new file mode 100644 index 0000000000..6963fd7327 Binary files /dev/null and b/ng2-components/ng2-alfresco-documentlist/docs/assets/document-action-copymovedialog.png differ diff --git a/ng2-components/ng2-alfresco-documentlist/index.ts b/ng2-components/ng2-alfresco-documentlist/index.ts index cb42ecd42b..90fced6a52 100644 --- a/ng2-components/ng2-alfresco-documentlist/index.ts +++ b/ng2-components/ng2-alfresco-documentlist/index.ts @@ -25,6 +25,7 @@ import { ContentActionListComponent } from './src/components/content-action/cont import { ContentActionComponent } from './src/components/content-action/content-action.component'; import { ContentColumnListComponent } from './src/components/content-column/content-column-list.component'; import { ContentColumnComponent } from './src/components/content-column/content-column.component'; +import { ContentNodeSelectorComponent } from './src/components/content-node-selector/content-node-selector.component'; import { DocumentListComponent } from './src/components/document-list.component'; import { DocumentMenuActionComponent } from './src/components/document-menu-action.component'; import { EmptyFolderContentDirective } from './src/components/empty-folder/empty-folder-content.directive'; @@ -34,6 +35,7 @@ import { MaterialModule } from './src/material.module'; import { DocumentActionsService } from './src/services/document-actions.service'; import { DocumentListService } from './src/services/document-list.service'; import { FolderActionsService } from './src/services/folder-actions.service'; +import { NodeActionsService } from './src/services/node-actions.service'; // components export * from './src/components/document-list.component'; @@ -71,13 +73,15 @@ export const DOCUMENT_LIST_DIRECTIVES: any[] = [ EmptyFolderContentDirective, BreadcrumbComponent, DropdownSitesComponent, - DropdownBreadcrumbComponent + DropdownBreadcrumbComponent, + ContentNodeSelectorComponent ]; export const DOCUMENT_LIST_PROVIDERS: any[] = [ DocumentListService, FolderActionsService, - DocumentActionsService + DocumentActionsService, + NodeActionsService ]; @NgModule({ @@ -92,6 +96,9 @@ export const DOCUMENT_LIST_PROVIDERS: any[] = [ providers: [ ...DOCUMENT_LIST_PROVIDERS ], + entryComponents: [ + ContentNodeSelectorComponent + ], exports: [ DataTableModule, ...DOCUMENT_LIST_DIRECTIVES, diff --git a/ng2-components/ng2-alfresco-documentlist/src/components/content-action/content-action.component.spec.ts b/ng2-components/ng2-alfresco-documentlist/src/components/content-action/content-action.component.spec.ts index 286fcec9d8..aa890c9a55 100644 --- a/ng2-components/ng2-alfresco-documentlist/src/components/content-action/content-action.component.spec.ts +++ b/ng2-components/ng2-alfresco-documentlist/src/components/content-action/content-action.component.spec.ts @@ -17,12 +17,13 @@ import { EventEmitter } from '@angular/core'; import { async, TestBed } from '@angular/core/testing'; -import { AlfrescoContentService, CoreModule } from 'ng2-alfresco-core'; +import { AlfrescoContentService, AlfrescoTranslationService, CoreModule, NotificationService } from 'ng2-alfresco-core'; import { FileNode } from './../../assets/document-library.model.mock'; import { DocumentListServiceMock } from './../../assets/document-list.service.mock'; import { ContentActionHandler } from './../../models/content-action.model'; import { DocumentActionsService } from './../../services/document-actions.service'; import { FolderActionsService } from './../../services/folder-actions.service'; +import { NodeActionsService } from './../../services/node-actions.service'; import { DocumentListComponent } from './../document-list.component'; import { ContentActionListComponent } from './content-action-list.component'; import { ContentActionComponent } from './content-action.component'; @@ -35,6 +36,9 @@ describe('ContentAction', () => { let folderActions: FolderActionsService; let contentService: AlfrescoContentService; + let translateService: AlfrescoTranslationService; + let notificationService: NotificationService; + let nodeActionsService: NodeActionsService; beforeEach(async(() => { TestBed.configureTestingModule({ @@ -49,9 +53,12 @@ describe('ContentAction', () => { beforeEach(() => { contentService = TestBed.get(AlfrescoContentService); + translateService = { addTranslationFolder: () => {}}; + nodeActionsService = new NodeActionsService(null, translateService, null, null); + notificationService = new NotificationService(null); let documentServiceMock = new DocumentListServiceMock(); - documentActions = new DocumentActionsService(null, null); - folderActions = new FolderActionsService(null, contentService); + documentActions = new DocumentActionsService(translateService, notificationService, nodeActionsService); + folderActions = new FolderActionsService(translateService, notificationService, nodeActionsService, null, contentService); documentList = new DocumentListComponent(documentServiceMock, null, null, null); actionList = new ContentActionListComponent(documentList); diff --git a/ng2-components/ng2-alfresco-documentlist/src/components/content-node-selector/content-node-selector.component.html b/ng2-components/ng2-alfresco-documentlist/src/components/content-node-selector/content-node-selector.component.html new file mode 100644 index 0000000000..3b36275ba2 --- /dev/null +++ b/ng2-components/ng2-alfresco-documentlist/src/components/content-node-selector/content-node-selector.component.html @@ -0,0 +1,73 @@ +
{{title}}
+ +
+ + + + + clear + + search + + + + + +
+ + + + + +
+ +
+ +
+ + + + + +
\ No newline at end of file diff --git a/ng2-components/ng2-alfresco-documentlist/src/components/content-node-selector/content-node-selector.component.scss b/ng2-components/ng2-alfresco-documentlist/src/components/content-node-selector/content-node-selector.component.scss new file mode 100644 index 0000000000..7a759813c0 --- /dev/null +++ b/ng2-components/ng2-alfresco-documentlist/src/components/content-node-selector/content-node-selector.component.scss @@ -0,0 +1,116 @@ +@import 'theming'; + +.#{$ADF}-content-node-selector-dialog { + + .mat-dialog-container { + padding: 0; + } + + .#{$ADF}-content-node-selector { + &-title, + &-content, + &-actions { + padding: 16px; + margin: 0; + } + + &-title { + text-transform: capitalize; + } + + &-content { + padding-top: 0; + + &-input { + width: 100%; + + &-icon { + color: rgba(0, 0, 0, 0.38); + cursor: pointer; + + &:hover { + color: rgba(0, 0, 0, 1); + } + } + } + + & /deep/ .mat-input-underline .mat-input-ripple { + height: 1px; + transition: none; + } + + & /deep/ .adf-site-dropdown-list-element { + width: 100%; + margin-bottom: 20px; + + .mat-select-trigger { + font-size: 14px; + } + + .mat-select-placeholder, + &.mat-select { + font-family: 'Muli', "Helvetica", "Arial", sans-serif; + } + } + + &-list { + height: 200px; + overflow: auto; + border: 1px solid rgba(0, 0, 0, 0.07); + + & /deep/ .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; + border-top: none; + height: 30px; + } + + tbody tr { + height: auto !important; + + &:last-child { + .adf-data-table-cell { + border-bottom: none; + } + } + } + } + } + } + + &-actions { + padding: 8px; + background-color: rgb(250, 250, 250); + display: flex; + justify-content: flex-end; + color: rgb(121, 121, 121); + + &:last-child { + margin-bottom: 0px; + } + + &-cancel { + font-weight: normal; + } + + &-choose { + font-weight: normal; + + &[disabled] { + opacity: 0.6; + } + } + } + } +} \ No newline at end of file diff --git a/ng2-components/ng2-alfresco-documentlist/src/components/content-node-selector/content-node-selector.component.spec.ts b/ng2-components/ng2-alfresco-documentlist/src/components/content-node-selector/content-node-selector.component.spec.ts new file mode 100644 index 0000000000..4b7fd8be25 --- /dev/null +++ b/ng2-components/ng2-alfresco-documentlist/src/components/content-node-selector/content-node-selector.component.spec.ts @@ -0,0 +1,377 @@ +/*! + * @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 { DebugElement, EventEmitter } from '@angular/core'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { MD_DIALOG_DATA, MdDialogRef } from '@angular/material'; +import { By } from '@angular/platform-browser'; +import { MinimalNodeEntryEntity, NodePaging } from 'alfresco-js-api'; +import { AlfrescoTranslationService, CoreModule, SearchService, SiteModel } from 'ng2-alfresco-core'; +import { DataTableModule } from 'ng2-alfresco-datatable'; +import { MaterialModule } from '../../material.module'; +import { DocumentListService } from '../../services/document-list.service'; +import { DocumentListComponent } from '../document-list.component'; +import { DocumentMenuActionComponent } from '../document-menu-action.component'; +import { EmptyFolderContentDirective } from '../empty-folder/empty-folder-content.directive'; +import { DropdownSitesComponent } from '../site-dropdown/sites-dropdown.component'; +import { ContentNodeSelectorComponent } from './content-node-selector.component'; + +const ONE_FOLDER_RESULT = { + list: { + entries: [ + { + entry: { + id: '123', name: 'MyFolder', isFile: false, isFolder: true, + createdByUser: { displayName: 'John Doe' }, + modifiedByUser: { displayName: 'John Doe' } + } + } + ] + } +}; + +const NO_RESULT = { + list: { + entries: [] + } +}; + +describe('ContentNodeSelectorComponent', () => { + + let component: ContentNodeSelectorComponent; + let fixture: ComponentFixture; + let element: DebugElement; + let data: any; + let searchService: SearchService; + let searchSpy: jasmine.Spy; + + let _resolve: Function; + let _reject: Function; + + 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; + searchInput.triggerEventHandler('keyup', {}); + fixture.detectChanges(); + } + + function respondWithSearchResults(result) { + _resolve(result); + } + + function setupTestbed(plusProviders) { + TestBed.configureTestingModule({ + imports: [ + CoreModule.forRoot(), + DataTableModule.forRoot(), + MaterialModule + ], + declarations: [ + DocumentListComponent, + DocumentMenuActionComponent, + EmptyFolderContentDirective, + DropdownSitesComponent, + ContentNodeSelectorComponent + ], + providers: [ + AlfrescoTranslationService, + DocumentListService, + SearchService, + ...plusProviders + ] + }); + } + + afterEach(() => { + fixture.destroy(); + TestBed.resetTestingModule(); + }); + + describe('Dialog features', () => { + + beforeEach(async(() => { + data = { + title: 'Move along citizen...', + select: new EventEmitter() + }; + + setupTestbed([{ provide: MD_DIALOG_DATA, useValue: data }]); + TestBed.compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ContentNodeSelectorComponent); + element = fixture.debugElement; + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + 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 trigger the INJECTED select event when selection has been made', (done) => { + const expectedNode = {}; + data.select.subscribe((node) => { + expect(node).toBe(expectedNode); + done(); + }); + + component.chosenNode = expectedNode; + component.choose(); + }); + }); + + describe('Cancel button', () => { + + let dummyMdDialogRef; + + beforeEach(() => { + dummyMdDialogRef = > { close: () => {} }; + }); + + it('should be shown if dialogRef is injected', () => { + const componentInstance = new ContentNodeSelectorComponent(null, null, 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, data, dummyMdDialogRef); + + componentInstance.close(); + + expect(dummyMdDialogRef.close).toHaveBeenCalled(); + }); + }); + }); + + describe('General component features', () => { + + beforeEach(async(() => { + setupTestbed([]); + TestBed.compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ContentNodeSelectorComponent); + element = fixture.debugElement; + component = fixture.componentInstance; + + searchService = TestBed.get(SearchService); + searchSpy = spyOn(searchService, 'getQueryNodesPromise').and.callFake(() => { + return new Promise((resolve, reject) => { + _resolve = resolve; + _reject = reject; + }); + }); + }); + + 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((node) => { + expect(node).toBe(expectedNode); + done(); + }); + + component.chosenNode = expectedNode; + component.choose(); + }); + }); + + describe('Search functionality', () => { + + it('should load the results by calling the search api on search change', () => { + typeToSearchBox('kakarot'); + + expect(searchSpy).toHaveBeenCalledWith('kakarot*', { + include: ['path'], + skipCount: 0, + rootNodeId: undefined, + nodeType: 'cm:folder', + maxItems: 40, + orderBy: null + }); + }); + + it('should NOT call the search api if the searchTerm length is less than 4 characters', () => { + typeToSearchBox('1'); + typeToSearchBox('12'); + typeToSearchBox('123'); + + expect(searchSpy).not.toHaveBeenCalled(); + }); + + xit('should debounce the search call by 500 ms', () => { + + }); + + it('should call the search api on changing the site selectbox\'s value', () => { + typeToSearchBox('vegeta'); + 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(['vegeta*', { + include: ['path'], + skipCount: 0, + rootNodeId: 'namek', + nodeType: 'cm:folder', + maxItems: 40, + orderBy: null + }]); + }); + + it('should show the search icon by default without the X (clear) icon', () => { + 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).not.toBeNull('Search icon should be in the DOM'); + expect(clearIcon).toBeNull('Clear icon should NOT be in the DOM'); + }); + + it('should show the X (clear) icon without the search icon when the search contains at least one character', () => { + fixture.detectChanges(); + typeToSearchBox('123'); + + 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'); + }); + + it('should clear the search field, nodes and chosenNode when clicking on the X (clear) icon', () => { + component.chosenNode = {}; + component.nodes = [ component.chosenNode ]; + component.searchTerm = 'whatever'; + component.searched = true; + + component.clear(); + + expect(component.searched).toBe(false); + expect(component.searchTerm).toBe(''); + expect(component.nodes).toEqual([]); + expect(component.chosenNode).toBeNull(); + }); + + it('should show the default text instead of result list if search was not performed', () => { + let documentList = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-document-list"]')); + expect(documentList).toBeNull('Document list should not be shown by default'); + }); + + it('should show the result list when search was performed', async(() => { + typeToSearchBox(); + respondWithSearchResults(ONE_FOLDER_RESULT); + + fixture.whenStable().then(() => { + 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 after search'); + }); + })); + + it('should show the default text instead of result list if search was cleared', async(() => { + typeToSearchBox(); + respondWithSearchResults(ONE_FOLDER_RESULT); + + fixture.whenStable().then(() => { + fixture.detectChanges(); + 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).toBeNull('Document list should NOT be shown after clearing the search'); + }); + })); + + xit('should do something with pagination or with many results', () => { + + }); + + xit('should trigger some kind of error when error happened during search', () => { + + }); + }); + + 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('Choose button', () => { + + it('should be disabled by default', () => { + fixture.detectChanges(); + + let chooseButton = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-actions-choose"]')); + expect(chooseButton.nativeElement.disabled).toBe(true); + }); + + it('should be enabled when clicking on one element in the list (onNodeSelect)', () => { + fixture.detectChanges(); + + component.onNodeSelect({ detail: { node: { entry: {} } } }); + fixture.detectChanges(); + + let chooseButton = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-actions-choose"]')); + expect(chooseButton.nativeElement.disabled).toBe(false); + }); + + it('should be disabled when deselecting the previously selected element in the list (onNodeUnselect)', () => { + component.onNodeSelect({ detail: { node: { entry: {} } } }); + fixture.detectChanges(); + + component.onNodeUnselect(); + fixture.detectChanges(); + + let chooseButton = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-actions-choose"]')); + expect(chooseButton.nativeElement.disabled).toBe(true); + }); + }); + + describe('Mini integration test', () => { + + xit('should trigger the select event properly when search results are loaded, one element is selected and choose button is clicked', () => { + + }); + }); + + }); +}); diff --git a/ng2-components/ng2-alfresco-documentlist/src/components/content-node-selector/content-node-selector.component.ts b/ng2-components/ng2-alfresco-documentlist/src/components/content-node-selector/content-node-selector.component.ts new file mode 100644 index 0000000000..ced2dc90a7 --- /dev/null +++ b/ng2-components/ng2-alfresco-documentlist/src/components/content-node-selector/content-node-selector.component.ts @@ -0,0 +1,152 @@ +/*! + * @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, Inject, Input, Optional, Output, ViewEncapsulation } from '@angular/core'; +import { MD_DIALOG_DATA, MdDialogRef } from '@angular/material'; +import { MinimalNodeEntryEntity, NodePaging } from 'alfresco-js-api'; +import { AlfrescoTranslationService, SearchOptions, SearchService, SiteModel } from 'ng2-alfresco-core'; + +export interface ContentNodeSelectorComponentData { + title: string; + select: EventEmitter; +} + +@Component({ + selector: 'adf-content-node-selector', + styleUrls: ['./content-node-selector.component.scss'], + templateUrl: './content-node-selector.component.html', + encapsulation: ViewEncapsulation.None +}) +export class ContentNodeSelectorComponent { + + nodes: NodePaging|Array; + siteId: null|string; + searchTerm: string = ''; + searched: boolean = false; + inDialog: boolean = false; + chosenNode: MinimalNodeEntryEntity | null = null; + + @Input() + title: string; + + @Output() + select: EventEmitter = new EventEmitter(); + + constructor(private searchService: SearchService, + @Optional() private translateService: AlfrescoTranslationService, + @Optional() @Inject(MD_DIALOG_DATA) public data?: ContentNodeSelectorComponentData, + @Optional() private containingDialog?: MdDialogRef) { + + if (translateService) { + translateService.addTranslationFolder('ng2-alfresco-documentlist', 'assets/ng2-alfresco-documentlist'); + } + + if (data) { + this.title = data.title; + this.select = data.select; + } + + if (containingDialog) { + this.inDialog = true; + } + } + + /** + * Updates the site attribute and starts a new search + * + * @param chosenSite Sitemodel to search within + */ + siteChanged(chosenSite: SiteModel): void { + this.siteId = chosenSite.guid; + this.querySearch(); + } + + /** + * Updates the searchTerm attribute and starts a new search + * + * @param searchTerm string value to search against + */ + search(searchTerm: string): void { + this.searchTerm = searchTerm; + this.querySearch(); + } + + /** + * Clear the search input + */ + clear(): void { + this.searched = false; + this.searchTerm = ''; + this.nodes = []; + this.chosenNode = null; + } + + /** + * Perform the call to searchService with the proper parameters + */ + private querySearch(): void { + if (this.searchTerm.length > 3) { + const searchTerm = this.searchTerm + '*'; + let searchOpts: SearchOptions = { + include: ['path'], + skipCount: 0, + rootNodeId: this.siteId, + nodeType: 'cm:folder', + maxItems: 40, + orderBy: null + }; + this.searchService + .getNodeQueryResults(searchTerm, searchOpts) + .subscribe( + results => { + this.searched = true; + this.nodes = results; + } + ); + } + } + + /** + * Invoked when user selects a node + * + * @param event CustomEvent for node-select + */ + onNodeSelect(event: any): void { + this.chosenNode = event.detail.node.entry; + } + + /** + * * Invoked when user unselects a node + */ + onNodeUnselect(): void { + this.chosenNode = null; + } + + /** + * Emit event with the chosen node + */ + choose(): void { + this.select.next(this.chosenNode); + } + + /** + * Close the dialog + */ + close(): void { + this.containingDialog.close(); + } +} diff --git a/ng2-components/ng2-alfresco-documentlist/src/components/site-dropdown/sites-dropdown.component.html b/ng2-components/ng2-alfresco-documentlist/src/components/site-dropdown/sites-dropdown.component.html index 9b6adea48d..8f3baed3e0 100644 --- a/ng2-components/ng2-alfresco-documentlist/src/components/site-dropdown/sites-dropdown.component.html +++ b/ng2-components/ng2-alfresco-documentlist/src/components/site-dropdown/sites-dropdown.component.html @@ -1,6 +1,7 @@