diff --git a/src/app/app.module.ts b/src/app/app.module.ts index a4943d35f..711a02d83 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -85,6 +85,8 @@ import { NodeEffects } from './store/effects/node.effects'; import { environment } from '../environments/environment'; import { RouterEffects } from './store/effects/router.effects'; import { CreateFolderDirective } from './directives/create-folder.directive'; +import { DownloadEffects } from './store/effects/download.effects'; +import { DownloadNodesDirective } from './directives/download-nodes.directive'; @NgModule({ @@ -108,7 +110,7 @@ import { CreateFolderDirective } from './directives/create-folder.directive'; StoreModule.forRoot({ app: appReducer }, { initialState: INITIAL_STATE }), StoreRouterConnectingModule.forRoot({ stateKey: 'router' }), - EffectsModule.forRoot([SnackbarEffects, NodeEffects, RouterEffects]), + EffectsModule.forRoot([SnackbarEffects, NodeEffects, RouterEffects, DownloadEffects]), !environment.production ? StoreDevtoolsModule.instrument({ maxAge: 25 }) : [] ], declarations: [ @@ -143,7 +145,8 @@ import { CreateFolderDirective } from './directives/create-folder.directive'; SortingPreferenceKeyDirective, InfoDrawerComponent, EditFolderDirective, - CreateFolderDirective + CreateFolderDirective, + DownloadNodesDirective ], providers: [ { provide: PageTitleService, useClass: AcaPageTitleService }, diff --git a/src/app/components/files/files.component.ts b/src/app/components/files/files.component.ts index bb25fe00c..3bba704b3 100644 --- a/src/app/components/files/files.component.ts +++ b/src/app/components/files/files.component.ts @@ -139,7 +139,7 @@ export class FilesComponent extends PageComponent implements OnInit, OnDestroy { }); } - onNodeDoubleClick(event) { + onNodeDoubleClick(event: CustomEvent) { if (!!event.detail && !!event.detail.node) { const node: MinimalNodeEntryEntity = event.detail.node.entry; diff --git a/src/app/components/search/search.component.html b/src/app/components/search/search.component.html index 4d2e0347a..de16027fb 100644 --- a/src/app/components/search/search.component.html +++ b/src/app/components/search/search.component.html @@ -2,6 +2,65 @@
+ + + + + + + + + + + + + + + + +
@@ -11,77 +70,87 @@
-
- +
+
+ -
-
-
{{ 'APP.BROWSE.SEARCH.FOUND_RESULTS' | translate: { number: totalResults } }}
- +
+
+
{{ 'APP.BROWSE.SEARCH.FOUND_RESULTS' | translate: { number: totalResults } }}
+ +
+ + + + + + + + + + + + + + + + + + +
+

Your search returned 0 results

+
+
+
+
+ + +
- - - - - - - - - - - - - - - - - - -
-

Your search returned 0 results

-
-
-
-
- - -
+
+ +
diff --git a/src/app/components/search/search.component.ts b/src/app/components/search/search.component.ts index 9a127c429..fdb6c4b82 100644 --- a/src/app/components/search/search.component.ts +++ b/src/app/components/search/search.component.ts @@ -23,23 +23,28 @@ * along with Alfresco. If not, see . */ -import { Component, OnInit, Optional, ViewChild } from '@angular/core'; +import { Component, OnInit, ViewChild } from '@angular/core'; import { MinimalNodeEntryEntity, NodePaging, Pagination } from 'alfresco-js-api'; import { Router, ActivatedRoute, Params } from '@angular/router'; -import { SearchQueryBuilderService, SearchComponent as AdfSearchComponent } from '@alfresco/adf-content-services'; -import { SearchConfigurationService } from '@alfresco/adf-core'; +import { SearchQueryBuilderService, SearchComponent as AdfSearchComponent, NodePermissionService } from '@alfresco/adf-content-services'; +import { SearchConfigurationService, UserPreferencesService, SearchService } from '@alfresco/adf-core'; import { PageComponent } from '../page.component'; +import { Store } from '@ngrx/store'; +import { AppStore } from '../../store/states/app.state'; +import { NavigateToLocationAction } from '../../store/actions'; @Component({ selector: 'app-search', templateUrl: './search.component.html', - styleUrls: ['./search.component.scss'] + styleUrls: ['./search.component.scss'], + providers: [SearchService] }) -export class SearchComponent implements OnInit { +export class SearchComponent extends PageComponent implements OnInit { @ViewChild('search') search: AdfSearchComponent; + searchedWord: string; queryParamName = 'q'; data: NodePaging; totalResults = 0; @@ -48,10 +53,15 @@ export class SearchComponent implements OnInit { sorting = ['name', 'asc']; constructor( - public router: Router, + public permission: NodePermissionService, private queryBuilder: SearchQueryBuilderService, private searchConfiguration: SearchConfigurationService, - @Optional() private route: ActivatedRoute) { + store: Store, + router: Router, + preferences: UserPreferencesService, + route: ActivatedRoute) { + super(preferences, router, route, store); + queryBuilder.paging = { skipCount: 0, maxItems: 25 @@ -59,6 +69,8 @@ export class SearchComponent implements OnInit { } ngOnInit() { + super.ngOnInit(); + this.sorting = this.getSorting(); this.queryBuilder.updated.subscribe(() => { @@ -67,9 +79,9 @@ export class SearchComponent implements OnInit { if (this.route) { this.route.params.forEach((params: Params) => { - const searchedWord = params.hasOwnProperty(this.queryParamName) ? params[this.queryParamName] : null; - if (searchedWord) { - const queryBody = this.searchConfiguration.generateQueryBody(searchedWord, 0, 100); + this.searchedWord = params.hasOwnProperty(this.queryParamName) ? params[this.queryParamName] : null; + if (this.searchedWord) { + const queryBody = this.searchConfiguration.generateQueryBody(this.searchedWord, 0, 100); this.queryBuilder.userQuery = queryBody.query.query; this.queryBuilder.update(); @@ -112,6 +124,10 @@ export class SearchComponent implements OnInit { } onNodeDoubleClick(node: MinimalNodeEntryEntity) { + if (node && node.isFolder) { + this.store.dispatch(new NavigateToLocationAction(node)); + } + if (node && PageComponent.isLockedNode(node)) { event.preventDefault(); diff --git a/src/app/directives/download-nodes.directive.ts b/src/app/directives/download-nodes.directive.ts new file mode 100644 index 000000000..53a790404 --- /dev/null +++ b/src/app/directives/download-nodes.directive.ts @@ -0,0 +1,58 @@ +/*! + * @license + * Alfresco Example Content Application + * + * Copyright (C) 2005 - 2018 Alfresco Software Limited + * + * This file is part of the Alfresco Example Content Application. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * The Alfresco Example Content Application is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * The Alfresco Example Content Application is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ + +import { Directive, HostListener, Input } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { AppStore } from '../store/states/app.state'; +import { MinimalNodeEntity } from '@alfresco/adf-core/node_modules/alfresco-js-api'; +import { DownloadNodesAction } from '../store/actions'; + +@Directive({ + selector: '[acaDownloadNodes]' +}) +export class DownloadNodesDirective { + // tslint:disable-next-line:no-input-rename + @Input('acaDownloadNodes') + nodes: Array | MinimalNodeEntity; + + constructor(private store: Store) {} + + @HostListener('click') + onClick() { + const targets = Array.isArray(this.nodes) ? this.nodes : [this.nodes]; + const toDownload = targets.map(node => { + const { id, nodeId, name, isFile, isFolder } = node.entry; + + return { + id: nodeId || id, + name, + isFile, + isFolder + }; + }); + + this.store.dispatch(new DownloadNodesAction(toDownload)); + } +} diff --git a/src/app/store/actions/node.action.ts b/src/app/store/actions/node.action.ts index e76bd7bc5..4d7cb313a 100644 --- a/src/app/store/actions/node.action.ts +++ b/src/app/store/actions/node.action.ts @@ -5,10 +5,13 @@ export const DELETE_NODES = 'DELETE_NODES'; export const UNDO_DELETE_NODES = 'UNDO_DELETE_NODES'; export const RESTORE_DELETED_NODES = 'RESTORE_DELETED_NODES'; export const PURGE_DELETED_NODES = 'PURGE_DELETED_NODES'; +export const DOWNLOAD_NODES = 'DOWNLOAD_NODES'; export interface NodeInfo { id: string; name: string; + isFile?: boolean; + isFolder?: boolean; } export class SetSelectedNodesAction implements Action { @@ -35,3 +38,8 @@ export class PurgeDeletedNodesAction implements Action { readonly type = PURGE_DELETED_NODES; constructor(public payload: NodeInfo[] = []) {} } + +export class DownloadNodesAction implements Action { + readonly type = DOWNLOAD_NODES; + constructor(public payload: NodeInfo[] = []) {} +} diff --git a/src/app/store/actions/router.action.ts b/src/app/store/actions/router.action.ts index 859f4918c..5bedc85af 100644 --- a/src/app/store/actions/router.action.ts +++ b/src/app/store/actions/router.action.ts @@ -1,8 +1,14 @@ import { Action } from '@ngrx/store'; export const NAVIGATE_ROUTE = 'NAVIGATE_ROUTE'; +export const NAVIGATE_LOCATION = 'NAVIGATE_LOCATION'; export class NavigateRouteAction implements Action { readonly type = NAVIGATE_ROUTE; constructor(public payload: any[]) {} } + +export class NavigateToLocationAction implements Action { + readonly type = NAVIGATE_LOCATION; + constructor(public payload: any) {} +} diff --git a/src/app/store/effects/download.effects.ts b/src/app/store/effects/download.effects.ts new file mode 100644 index 000000000..224c9afef --- /dev/null +++ b/src/app/store/effects/download.effects.ts @@ -0,0 +1,86 @@ +import { Effect, Actions, ofType } from '@ngrx/effects'; +import { Injectable } from '@angular/core'; +import { map } from 'rxjs/operators'; +import { DownloadNodesAction, DOWNLOAD_NODES } from '../actions'; +import { AlfrescoApiService } from '@alfresco/adf-core'; +import { MatDialog } from '@angular/material'; +import { NodeInfo } from '../actions/node.action'; +import { DownloadZipDialogComponent } from '@alfresco/adf-content-services'; + +@Injectable() +export class DownloadEffects { + constructor( + private actions$: Actions, + private apiService: AlfrescoApiService, + private dialog: MatDialog + ) {} + + @Effect({ dispatch: false }) + downloadNode$ = this.actions$.pipe( + ofType(DOWNLOAD_NODES), + map(action => { + if (action.payload && action.payload.length > 0) { + this.downloadNodes(action.payload); + } + }) + ); + + private downloadNodes(nodes: Array) { + if (!nodes || nodes.length === 0) { + return; + } + + if (nodes.length === 1) { + this.downloadNode(nodes[0]); + } else { + this.downloadZip(nodes); + } + } + + private downloadNode(node: NodeInfo) { + if (node) { + if (node.isFolder) { + this.downloadZip([node]); + } else { + this.downloadFile(node); + } + } + } + + private downloadFile(node: NodeInfo) { + if (node) { + this.download( + this.apiService.contentApi.getContentUrl(node.id, true), + node.name + ); + } + } + + private downloadZip(nodes: Array) { + if (nodes && nodes.length > 0) { + const nodeIds = nodes.map(node => node.id); + + this.dialog.open(DownloadZipDialogComponent, { + width: '600px', + disableClose: true, + data: { + nodeIds + } + }); + } + } + + private download(url: string, fileName: string) { + if (url && fileName) { + const link = document.createElement('a'); + + link.style.display = 'none'; + link.download = fileName; + link.href = url; + + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + } + } +} diff --git a/src/app/store/effects/router.effects.ts b/src/app/store/effects/router.effects.ts index b5d761cda..08adb84cd 100644 --- a/src/app/store/effects/router.effects.ts +++ b/src/app/store/effects/router.effects.ts @@ -1,6 +1,9 @@ import { Effect, Actions, ofType } from '@ngrx/effects'; import { Injectable } from '@angular/core'; -import { NavigateRouteAction, NAVIGATE_ROUTE } from '../actions/router.action'; +import { PathInfoEntity, MinimalNodeEntryEntity } from 'alfresco-js-api'; +import { + NavigateRouteAction, NAVIGATE_ROUTE, NavigateToLocationAction, NAVIGATE_LOCATION +} from '../actions/router.action'; import { map } from 'rxjs/operators'; import { Router } from '@angular/router'; @@ -15,4 +18,43 @@ export class RouterEffects { this.router.navigate(action.payload); }) ); + + @Effect({ dispatch: false }) + navigateLocation$ = this.actions$.pipe( + ofType(NAVIGATE_LOCATION), + map(action => { + if (action.payload) { + this.navigateToLocation(action.payload); + } + }) + ); + + private navigateToLocation(node: MinimalNodeEntryEntity) { + let link = null; + const { path } = node; + + if (path && path.name && path.elements) { + const isLibraryPath = this.isLibraryContent(path); + + const parent = path.elements[path.elements.length - 1]; + const area = isLibraryPath ? '/libraries' : '/personal-files'; + + if (!isLibraryPath) { + link = [ area, parent.id ]; + } else { + // parent.id could be 'Site' folder or child as 'documentLibrary' + link = [ area, (parent.name === 'Sites' ? {} : parent.id) ]; + } + } + + this.router.navigate(link); + } + + private isLibraryContent(path: PathInfoEntity): boolean { + if (path && path.elements.length >= 2 && path.elements[1].name === 'Sites') { + return true; + } + + return false; + } }